|
17 | 17 |
|
18 | 18 | import { Document } from '../model/document';
|
19 | 19 | import { DocumentKey } from '../model/document_key';
|
| 20 | +import { |
| 21 | + FieldIndex, |
| 22 | + fieldIndexGetArraySegment, |
| 23 | + fieldIndexGetDirectionalSegments |
| 24 | +} from '../model/field_index'; |
20 | 25 | import { FieldPath, ResourcePath } from '../model/path';
|
21 | 26 | import {
|
22 | 27 | arrayValueContains,
|
23 | 28 | canonicalId,
|
24 | 29 | isArray,
|
25 | 30 | isReferenceValue,
|
| 31 | + MAX_VALUE, |
| 32 | + MIN_VALUE, |
26 | 33 | typeOrder,
|
27 | 34 | valueCompare,
|
28 |
| - valueEquals |
| 35 | + valueEquals, |
| 36 | + valuesGetLowerBound, |
| 37 | + valuesGetUpperBound, |
| 38 | + valuesMax, |
| 39 | + valuesMin |
29 | 40 | } from '../model/values';
|
30 | 41 | import { Value as ProtoValue } from '../protos/firestore_proto_api';
|
31 | 42 | import { debugAssert, debugCast, fail } from '../util/assert';
|
@@ -187,14 +198,262 @@ export function targetEquals(left: Target, right: Target): boolean {
|
187 | 198 | return boundEquals(left.endAt, right.endAt);
|
188 | 199 | }
|
189 | 200 |
|
190 |
| -export function isDocumentTarget(target: Target): boolean { |
| 201 | +export function targetIsDocumentTarget(target: Target): boolean { |
191 | 202 | return (
|
192 | 203 | DocumentKey.isDocumentKey(target.path) &&
|
193 | 204 | target.collectionGroup === null &&
|
194 | 205 | target.filters.length === 0
|
195 | 206 | );
|
196 | 207 | }
|
197 | 208 |
|
| 209 | +/** Returns the field filters that target the given field path. */ |
| 210 | +export function targetGetFieldFiltersForPath( |
| 211 | + target: Target, |
| 212 | + path: FieldPath |
| 213 | +): FieldFilter[] { |
| 214 | + return target.filters.filter( |
| 215 | + f => f instanceof FieldFilter && f.field.isEqual(path) |
| 216 | + ) as FieldFilter[]; |
| 217 | +} |
| 218 | + |
| 219 | +/** |
| 220 | + * Returns the values that are used in ARRAY_CONTAINS or ARRAY_CONTAINS_ANY |
| 221 | + * filters. Returns `null` if there are no such filters. |
| 222 | + */ |
| 223 | +export function targetGetArrayValues( |
| 224 | + target: Target, |
| 225 | + fieldIndex: FieldIndex |
| 226 | +): ProtoValue[] | null { |
| 227 | + const segment = fieldIndexGetArraySegment(fieldIndex); |
| 228 | + if (segment === undefined) { |
| 229 | + return null; |
| 230 | + } |
| 231 | + |
| 232 | + for (const fieldFilter of targetGetFieldFiltersForPath( |
| 233 | + target, |
| 234 | + segment.fieldPath |
| 235 | + )) { |
| 236 | + switch (fieldFilter.op) { |
| 237 | + case Operator.ARRAY_CONTAINS_ANY: |
| 238 | + return fieldFilter.value.arrayValue!.values || []; |
| 239 | + case Operator.ARRAY_CONTAINS: |
| 240 | + return [fieldFilter.value]; |
| 241 | + default: |
| 242 | + // Remaining filters are not array filters. |
| 243 | + } |
| 244 | + } |
| 245 | + return null; |
| 246 | +} |
| 247 | + |
| 248 | +/** |
| 249 | + * Returns the list of values that are used in != or NOT_IN filters. Returns |
| 250 | + * `null` if there are no such filters. |
| 251 | + */ |
| 252 | +export function targetGetNotInValues( |
| 253 | + target: Target, |
| 254 | + fieldIndex: FieldIndex |
| 255 | +): ProtoValue[] | null { |
| 256 | + const values: ProtoValue[] = []; |
| 257 | + |
| 258 | + for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) { |
| 259 | + for (const fieldFilter of targetGetFieldFiltersForPath( |
| 260 | + target, |
| 261 | + segment.fieldPath |
| 262 | + )) { |
| 263 | + switch (fieldFilter.op) { |
| 264 | + case Operator.EQUAL: |
| 265 | + case Operator.IN: |
| 266 | + // Encode equality prefix, which is encoded in the index value before |
| 267 | + // the inequality (e.g. `a == 'a' && b != 'b'` is encoded to |
| 268 | + // `value != 'ab'`). |
| 269 | + values.push(fieldFilter.value); |
| 270 | + break; |
| 271 | + case Operator.NOT_IN: |
| 272 | + case Operator.NOT_EQUAL: |
| 273 | + // NotIn/NotEqual is always a suffix. There cannot be any remaining |
| 274 | + // segments and hence we can return early here. |
| 275 | + values.push(fieldFilter.value); |
| 276 | + return values; |
| 277 | + default: |
| 278 | + // Remaining filters cannot be used as notIn bounds. |
| 279 | + } |
| 280 | + } |
| 281 | + } |
| 282 | + |
| 283 | + return null; |
| 284 | +} |
| 285 | + |
| 286 | +/** |
| 287 | + * Returns a lower bound of field values that can be used as a starting point to |
| 288 | + * scan the index defined by `fieldIndex`. Returns `null` if no lower bound |
| 289 | + * exists. |
| 290 | + */ |
| 291 | +export function targetGetLowerBound( |
| 292 | + target: Target, |
| 293 | + fieldIndex: FieldIndex |
| 294 | +): Bound | null { |
| 295 | + const values: ProtoValue[] = []; |
| 296 | + let inclusive = true; |
| 297 | + |
| 298 | + // For each segment, retrieve a lower bound if there is a suitable filter or |
| 299 | + // startAt. |
| 300 | + for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) { |
| 301 | + let segmentValue: ProtoValue | undefined = undefined; |
| 302 | + let segmentInclusive = true; |
| 303 | + |
| 304 | + // Process all filters to find a value for the current field segment |
| 305 | + for (const fieldFilter of targetGetFieldFiltersForPath( |
| 306 | + target, |
| 307 | + segment.fieldPath |
| 308 | + )) { |
| 309 | + let filterValue: ProtoValue | undefined = undefined; |
| 310 | + let filterInclusive = true; |
| 311 | + |
| 312 | + switch (fieldFilter.op) { |
| 313 | + case Operator.LESS_THAN: |
| 314 | + case Operator.LESS_THAN_OR_EQUAL: |
| 315 | + filterValue = valuesGetLowerBound(fieldFilter.value); |
| 316 | + break; |
| 317 | + case Operator.EQUAL: |
| 318 | + case Operator.IN: |
| 319 | + case Operator.GREATER_THAN_OR_EQUAL: |
| 320 | + filterValue = fieldFilter.value; |
| 321 | + break; |
| 322 | + case Operator.GREATER_THAN: |
| 323 | + filterValue = fieldFilter.value; |
| 324 | + filterInclusive = false; |
| 325 | + break; |
| 326 | + case Operator.NOT_EQUAL: |
| 327 | + filterValue = MIN_VALUE; |
| 328 | + break; |
| 329 | + case Operator.NOT_IN: |
| 330 | + const length = (fieldFilter.value.arrayValue!.values || []).length; |
| 331 | + filterValue = { |
| 332 | + arrayValue: { values: new Array(length).fill(MIN_VALUE) } |
| 333 | + }; |
| 334 | + break; |
| 335 | + default: |
| 336 | + // Remaining filters cannot be used as lower bounds. |
| 337 | + } |
| 338 | + |
| 339 | + if (valuesMax(segmentValue, filterValue) === filterValue) { |
| 340 | + segmentValue = filterValue; |
| 341 | + segmentInclusive = filterInclusive; |
| 342 | + } |
| 343 | + } |
| 344 | + |
| 345 | + // If there is a startAt bound, compare the values against the existing |
| 346 | + // boundary to see if we can narrow the scope. |
| 347 | + if (target.startAt !== null) { |
| 348 | + for (let i = 0; i < target.orderBy.length; ++i) { |
| 349 | + const orderBy = target.orderBy[i]; |
| 350 | + if (orderBy.field.isEqual(segment.fieldPath)) { |
| 351 | + const cursorValue = target.startAt.position[i]; |
| 352 | + if (valuesMax(segmentValue, cursorValue) === cursorValue) { |
| 353 | + segmentValue = cursorValue; |
| 354 | + segmentInclusive = !target.startAt.before; |
| 355 | + } |
| 356 | + break; |
| 357 | + } |
| 358 | + } |
| 359 | + } |
| 360 | + |
| 361 | + if (segmentValue === undefined) { |
| 362 | + // No lower bound exists |
| 363 | + return null; |
| 364 | + } |
| 365 | + values.push(segmentValue); |
| 366 | + inclusive &&= segmentInclusive; |
| 367 | + } |
| 368 | + return new Bound(values, !inclusive); |
| 369 | +} |
| 370 | +/** |
| 371 | + * Returns an upper bound of field values that can be used as an ending point |
| 372 | + * when scanning the index defined by `fieldIndex`. Returns `null` if no |
| 373 | + * upper bound exists. |
| 374 | + */ |
| 375 | +export function targetGetUpperBound( |
| 376 | + target: Target, |
| 377 | + fieldIndex: FieldIndex |
| 378 | +): Bound | null { |
| 379 | + const values: ProtoValue[] = []; |
| 380 | + let inclusive = true; |
| 381 | + |
| 382 | + // For each segment, retrieve an upper bound if there is a suitable filter or |
| 383 | + // endAt. |
| 384 | + for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) { |
| 385 | + let segmentValue: ProtoValue | undefined = undefined; |
| 386 | + let segmentInclusive = true; |
| 387 | + |
| 388 | + // Process all filters to find a value for the current field segment |
| 389 | + for (const fieldFilter of targetGetFieldFiltersForPath( |
| 390 | + target, |
| 391 | + segment.fieldPath |
| 392 | + )) { |
| 393 | + let filterValue: ProtoValue | undefined = undefined; |
| 394 | + let filterInclusive = true; |
| 395 | + |
| 396 | + switch (fieldFilter.op) { |
| 397 | + case Operator.GREATER_THAN_OR_EQUAL: |
| 398 | + case Operator.GREATER_THAN: |
| 399 | + filterValue = valuesGetUpperBound(fieldFilter.value); |
| 400 | + filterInclusive = false; |
| 401 | + break; |
| 402 | + case Operator.EQUAL: |
| 403 | + case Operator.IN: |
| 404 | + case Operator.LESS_THAN_OR_EQUAL: |
| 405 | + filterValue = fieldFilter.value; |
| 406 | + break; |
| 407 | + case Operator.LESS_THAN: |
| 408 | + filterValue = fieldFilter.value; |
| 409 | + filterInclusive = false; |
| 410 | + break; |
| 411 | + case Operator.NOT_EQUAL: |
| 412 | + filterValue = MAX_VALUE; |
| 413 | + break; |
| 414 | + case Operator.NOT_IN: |
| 415 | + const length = (fieldFilter.value.arrayValue!.values || []).length; |
| 416 | + filterValue = { |
| 417 | + arrayValue: { values: new Array(length).fill(MIN_VALUE) } |
| 418 | + }; |
| 419 | + break; |
| 420 | + default: |
| 421 | + // Remaining filters cannot be used as upper bounds. |
| 422 | + } |
| 423 | + |
| 424 | + if (valuesMin(segmentValue, filterValue) === filterValue) { |
| 425 | + segmentValue = filterValue; |
| 426 | + segmentInclusive = filterInclusive; |
| 427 | + } |
| 428 | + } |
| 429 | + |
| 430 | + // If there is a endAt bound, compare the values against the existing |
| 431 | + // boundary to see if we can narrow the scope. |
| 432 | + if (target.endAt !== null) { |
| 433 | + for (let i = 0; i < target.orderBy.length; ++i) { |
| 434 | + const orderBy = target.orderBy[i]; |
| 435 | + if (orderBy.field.isEqual(segment.fieldPath)) { |
| 436 | + const cursorValue = target.endAt.position[i]; |
| 437 | + if (valuesMin(segmentValue, cursorValue) === cursorValue) { |
| 438 | + segmentValue = cursorValue; |
| 439 | + segmentInclusive = !target.endAt.before; |
| 440 | + } |
| 441 | + break; |
| 442 | + } |
| 443 | + } |
| 444 | + } |
| 445 | + |
| 446 | + if (segmentValue === undefined) { |
| 447 | + // No lower bound exists |
| 448 | + return null; |
| 449 | + } |
| 450 | + values.push(segmentValue); |
| 451 | + inclusive &&= segmentInclusive; |
| 452 | + } |
| 453 | + |
| 454 | + return new Bound(values, !inclusive); |
| 455 | +} |
| 456 | + |
198 | 457 | export abstract class Filter {
|
199 | 458 | abstract matches(doc: Document): boolean;
|
200 | 459 | }
|
@@ -513,6 +772,7 @@ export class ArrayContainsAnyFilter extends FieldFilter {
|
513 | 772 | }
|
514 | 773 | }
|
515 | 774 |
|
| 775 | +// TODO(indexing): Change Bound.before to "inclusive" |
516 | 776 | /**
|
517 | 777 | * Represents a bound of a query.
|
518 | 778 | *
|
|
0 commit comments