Skip to content

Commit 667c398

Browse files
committed
query_engine.test.ts pass with pipelines
1 parent e6f860e commit 667c398

File tree

8 files changed

+237
-63
lines changed

8 files changed

+237
-63
lines changed

packages/firestore/src/core/expressions.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,12 @@ import {
7979
Field,
8080
Constant
8181
} from '../lite-api/expressions';
82-
import { FieldPath } from '../model/path';
82+
import {
83+
CREATE_TIME_NAME,
84+
DOCUMENT_KEY_NAME,
85+
FieldPath,
86+
UPDATE_TIME_NAME
87+
} from '../model/path';
8388
import {
8489
FALSE_VALUE,
8590
getVectorValue,
@@ -99,6 +104,7 @@ import {
99104
} from '../model/values';
100105

101106
import { RE2JS } from 're2js';
107+
import { toName, toTimestamp, toVersion } from '../remote/serializer';
102108

103109
export interface EvaluableExpr {
104110
evaluate(
@@ -246,6 +252,27 @@ export class CoreField implements EvaluableExpr {
246252
context: EvaluationContext,
247253
input: PipelineInputOutput
248254
): Value | undefined {
255+
if (this.expr.fieldName() === DOCUMENT_KEY_NAME) {
256+
return {
257+
referenceValue: toName(context.userDataReader.serializer, input.key)
258+
};
259+
}
260+
if (this.expr.fieldName() === UPDATE_TIME_NAME) {
261+
return {
262+
timestampValue: toVersion(
263+
context.userDataReader.serializer,
264+
input.version
265+
)
266+
};
267+
}
268+
if (this.expr.fieldName() === CREATE_TIME_NAME) {
269+
return {
270+
timestampValue: toVersion(
271+
context.userDataReader.serializer,
272+
input.createTime
273+
)
274+
};
275+
}
249276
return (
250277
input.data.field(FieldPath.fromServerFormat(this.expr.fieldName())) ??
251278
undefined
@@ -936,17 +963,17 @@ export class CoreArrayContainsAny implements EvaluableExpr {
936963
context: EvaluationContext,
937964
input: PipelineInputOutput
938965
): Value | undefined {
939-
const evaluated = toEvaluable(this.expr.array).evaluate(context, input);
940-
if (evaluated === undefined || !isArray(evaluated)) {
966+
const evaluatedExpr = toEvaluable(this.expr.array).evaluate(context, input);
967+
if (evaluatedExpr === undefined || !isArray(evaluatedExpr)) {
941968
return undefined;
942969
}
943970

944-
const elements = this.expr.values.map(val =>
971+
const candidates = this.expr.values.map(val =>
945972
toEvaluable(val).evaluate(context, input)
946973
);
947974

948-
for (const element of elements) {
949-
for (const val of evaluated.arrayValue.values ?? []) {
975+
for (const element of candidates) {
976+
for (const val of evaluatedExpr.arrayValue.values ?? []) {
950977
if (element !== undefined && valueEquals(val, element!)) {
951978
return TRUE_VALUE;
952979
}

packages/firestore/src/core/pipeline-util.ts

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,26 @@ import {
6464
Where
6565
} from '../lite-api/stage';
6666
import { Pipeline } from '../lite-api/pipeline';
67-
import { canonifyQuery, Query, queryEquals, stringifyQuery } from './query';
67+
import {
68+
canonifyQuery,
69+
isCollectionGroupQuery,
70+
isDocumentQuery,
71+
LimitType,
72+
Query,
73+
queryEquals,
74+
queryNormalizedOrderBy,
75+
stringifyQuery
76+
} from './query';
6877
import {
6978
canonifyTarget,
7079
Target,
7180
targetEquals,
7281
targetIsPipelineTarget
7382
} from './target';
7483
import { ResourcePath } from '../model/path';
84+
import { Firestore } from '../api/database';
85+
import { doc } from '../lite-api/reference';
86+
import { Direction } from './order_by';
7587

7688
/* eslint @typescript-eslint/no-explicit-any: 0 */
7789

@@ -222,34 +234,37 @@ export function toPipelineFilterCondition(
222234
const value = f.value;
223235
switch (f.op) {
224236
case Operator.LESS_THAN:
225-
return and(field.exists(), field.lt(value));
237+
return and(field.exists(), field.lt(Constant._fromProto(value)));
226238
case Operator.LESS_THAN_OR_EQUAL:
227-
return and(field.exists(), field.lte(value));
239+
return and(field.exists(), field.lte(Constant._fromProto(value)));
228240
case Operator.GREATER_THAN:
229-
return and(field.exists(), field.gt(value));
241+
return and(field.exists(), field.gt(Constant._fromProto(value)));
230242
case Operator.GREATER_THAN_OR_EQUAL:
231-
return and(field.exists(), field.gte(value));
243+
return and(field.exists(), field.gte(Constant._fromProto(value)));
232244
case Operator.EQUAL:
233-
return and(field.exists(), field.eq(value));
245+
return and(field.exists(), field.eq(Constant._fromProto(value)));
234246
case Operator.NOT_EQUAL:
235-
return and(field.exists(), field.neq(value));
247+
return and(field.exists(), field.neq(Constant._fromProto(value)));
236248
case Operator.ARRAY_CONTAINS:
237-
return and(field.exists(), field.arrayContains(value));
249+
return and(
250+
field.exists(),
251+
field.arrayContains(Constant._fromProto(value))
252+
);
238253
case Operator.IN: {
239254
const values = value?.arrayValue?.values?.map((val: any) =>
240-
Constant.of(val)
255+
Constant._fromProto(val)
241256
);
242257
return and(field.exists(), field.in(...values!));
243258
}
244259
case Operator.ARRAY_CONTAINS_ANY: {
245260
const values = value?.arrayValue?.values?.map((val: any) =>
246-
Constant.of(val)
261+
Constant._fromProto(val)
247262
);
248-
return and(field.exists(), field.arrayContainsAny(values!));
263+
return and(field.exists(), field.arrayContainsAny(...values!));
249264
}
250265
case Operator.NOT_IN: {
251266
const values = value?.arrayValue?.values?.map((val: any) =>
252-
Constant.of(val)
267+
Constant._fromProto(val)
253268
);
254269
return and(field.exists(), not(field.in(...values!)));
255270
}
@@ -279,6 +294,56 @@ export function toPipelineFilterCondition(
279294
throw new Error(`Failed to convert filter to pipeline conditions: ${f}`);
280295
}
281296

297+
export function toPipeline(query: Query, db: Firestore): Pipeline {
298+
let pipeline: Pipeline;
299+
if (isCollectionGroupQuery(query)) {
300+
pipeline = db.pipeline().collectionGroup(query.collectionGroup!);
301+
} else if (isDocumentQuery(query)) {
302+
pipeline = db.pipeline().documents([doc(db, query.path.canonicalString())]);
303+
} else {
304+
pipeline = db.pipeline().collection(query.path.canonicalString());
305+
}
306+
307+
// filters
308+
for (const filter of query.filters) {
309+
pipeline = pipeline.where(toPipelineFilterCondition(filter));
310+
}
311+
312+
// orders
313+
const orders = queryNormalizedOrderBy(query);
314+
const existsConditions = orders.map(order =>
315+
Field.of(order.field.canonicalString()).exists()
316+
);
317+
if (existsConditions.length > 1) {
318+
pipeline = pipeline.where(
319+
and(existsConditions[0], ...existsConditions.slice(1))
320+
);
321+
} else {
322+
pipeline = pipeline.where(existsConditions[0]);
323+
}
324+
325+
pipeline = pipeline.sort(
326+
...orders.map(order =>
327+
order.dir === Direction.ASCENDING
328+
? Field.of(order.field.canonicalString()).ascending()
329+
: Field.of(order.field.canonicalString()).descending()
330+
)
331+
);
332+
333+
// cursors and limits
334+
if (query.startAt !== null || query.endAt !== null) {
335+
throw new Error('Cursors are not supported yet.');
336+
}
337+
if (query.limitType === LimitType.Last) {
338+
throw new Error('Limit to last are not supported yet.');
339+
}
340+
if (query.limit !== null) {
341+
pipeline = pipeline.limit(query.limit);
342+
}
343+
344+
return pipeline;
345+
}
346+
282347
function canonifyExpr(expr: Expr): string {
283348
if (expr instanceof Field) {
284349
return `fld(${expr.fieldName()})`;
@@ -534,6 +599,6 @@ export function targetOrPipelineEqual(
534599

535600
export function pipelineHasRanges(pipeline: Pipeline): boolean {
536601
return pipeline.stages.some(
537-
stage => stage.name === Limit.name || stage.name === Offset.name
602+
stage => stage instanceof Limit || stage instanceof Offset
538603
);
539604
}

packages/firestore/src/core/pipeline_run.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import {
1717
CollectionSource,
1818
DatabaseSource,
1919
DocumentsSource,
20+
Exists,
21+
exists,
22+
Field,
2023
Limit,
2124
Offset,
2225
Ordering,
@@ -36,6 +39,7 @@ import { toEvaluable } from './expressions';
3639
import { UserDataReader } from '../lite-api/user_data_reader';
3740
import { Query, queryMatches, queryMatchesAllDocuments } from './query';
3841
import { isPipeline, QueryOrPipeline } from './pipeline-util';
42+
import { DOCUMENT_KEY_NAME } from '../model/path';
3943

4044
export type PipelineInputOutput = MutableDocument;
4145

@@ -78,8 +82,23 @@ export function queryOrPipelineMatches(
7882
}
7983

8084
export function pipelineMatchesAllDocuments(pipeline: Pipeline): boolean {
81-
// TODO(pipeline): implement properly.
82-
return false;
85+
for (const stage of pipeline.stages) {
86+
if (stage instanceof Limit || stage instanceof Offset) {
87+
return false;
88+
}
89+
if (stage instanceof Where) {
90+
if (
91+
stage.condition instanceof Exists &&
92+
stage.condition.expr instanceof Field &&
93+
stage.condition.expr.fieldName() === DOCUMENT_KEY_NAME
94+
) {
95+
continue;
96+
}
97+
return false;
98+
}
99+
}
100+
101+
return true;
83102
}
84103

85104
function evaluate(
@@ -178,8 +197,9 @@ function evaluateCollection(
178197
): Array<PipelineInputOutput> {
179198
return inputs.filter(input => {
180199
return (
200+
input.isFoundDocument() &&
181201
`/${input.key.getCollectionPath().canonicalString()}` ===
182-
coll.collectionPath
202+
coll.collectionPath
183203
);
184204
});
185205
}
@@ -191,7 +211,10 @@ function evaluateCollectionGroup(
191211
): Array<PipelineInputOutput> {
192212
// return those records in input whose collection id is stage.collectionId
193213
return input.filter(input => {
194-
return input.key.getCollectionPath().lastSegment() === stage.collectionId;
214+
return (
215+
input.isFoundDocument() &&
216+
input.key.getCollectionPath().lastSegment() === stage.collectionId
217+
);
195218
});
196219
}
197220

@@ -200,7 +223,7 @@ function evaluateDatabase(
200223
stage: DatabaseSource,
201224
input: Array<PipelineInputOutput>
202225
): Array<PipelineInputOutput> {
203-
return input;
226+
return input.filter(input => input.isFoundDocument());
204227
}
205228

206229
function evaluateDocuments(
@@ -209,7 +232,10 @@ function evaluateDocuments(
209232
input: Array<PipelineInputOutput>
210233
): Array<PipelineInputOutput> {
211234
return input.filter(input => {
212-
return stage.docPaths.includes(input.key.path.canonicalString());
235+
return (
236+
input.isFoundDocument() &&
237+
stage.docPaths.includes(input.key.path.canonicalString())
238+
);
213239
});
214240
}
215241

packages/firestore/src/lite-api/expressions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2150,6 +2150,16 @@ export class Constant extends Expr {
21502150
}
21512151
}
21522152

2153+
/**
2154+
* @private
2155+
* @internal
2156+
*/
2157+
static _fromProto(value: ProtoValue): Constant {
2158+
const result = new Constant(value);
2159+
result._protoValue = value;
2160+
return result;
2161+
}
2162+
21532163
/**
21542164
* @private
21552165
* @internal
@@ -2179,6 +2189,10 @@ export class Constant extends Expr {
21792189
* @internal
21802190
*/
21812191
_readUserData(dataReader: UserDataReader): void {
2192+
if (!!this._protoValue) {
2193+
return;
2194+
}
2195+
21822196
const context = dataReader.createContext(
21832197
UserDataSource.Argument,
21842198
'Constant.of'

packages/firestore/src/lite-api/user_data_reader.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ import {
7575
} from './reference';
7676
import { Timestamp } from './timestamp';
7777
import { VectorValue } from './vector_value';
78+
import { isFirestoreValue } from '../core/pipeline-util';
79+
import { Constant } from './expressions';
7880

7981
const RESERVED_FIELD_REGEX = /^__.*__$/;
8082

@@ -331,7 +333,7 @@ class ParseContextImpl implements ParseContext {
331333
* classes.
332334
*/
333335
export class UserDataReader {
334-
private readonly serializer: JsonProtoSerializer;
336+
readonly serializer: JsonProtoSerializer;
335337

336338
constructor(
337339
private readonly databaseId: DatabaseId,
@@ -797,6 +799,10 @@ export function parseData(
797799
// from firestore-exp.
798800
input = getModularInstance(input);
799801

802+
if (input instanceof Constant) {
803+
return input._getValue();
804+
}
805+
800806
if (looksLikeJsonObject(input)) {
801807
validatePlainObject('Unsupported field value:', context, input);
802808
return parseObject(input, context);

packages/firestore/src/local/query_engine.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ import {
5757
stringifyQueryOrPipeline
5858
} from '../core/pipeline-util';
5959
import * as querystring from 'node:querystring';
60-
import { pipelineMatchesAllDocuments } from '../core/pipeline_run';
60+
import {
61+
pipelineMatches,
62+
pipelineMatchesAllDocuments
63+
} from '../core/pipeline_run';
6164
import { compareByKey } from '../model/document_comparator';
6265

6366
const DEFAULT_INDEX_AUTO_CREATION_MIN_COLLECTION_SIZE = 100;
@@ -428,6 +431,7 @@ export class QueryEngine {
428431
// TODO(pipeline): the order here does not actually matter, not until we implement
429432
// refill logic for pipelines as well.
430433
queryResults = new SortedSet<Document>(compareByKey);
434+
matcher = doc => pipelineMatches(query, doc as MutableDocument);
431435
} else {
432436
// Sort the documents and re-apply the query filter since previously
433437
// matching documents do not necessarily still match the query.

packages/firestore/src/model/path.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { debugAssert, fail } from '../util/assert';
1919
import { Code, FirestoreError } from '../util/error';
2020

2121
export const DOCUMENT_KEY_NAME = '__name__';
22+
export const UPDATE_TIME_NAME = '__update_time__';
23+
export const CREATE_TIME_NAME = '__create_time__';
2224

2325
/**
2426
* Path represents an ordered sequence of string segments.

0 commit comments

Comments
 (0)