Skip to content

Commit c9ae1d5

Browse files
authored
Merge pull request #7641 from googleapis/feat-implicit-orderby
feat(firestore): global option to turn on implicit orderby
2 parents b906b3a + 3ebfb71 commit c9ae1d5

9 files changed

Lines changed: 272 additions & 17 deletions

File tree

.DS_Store

6 KB
Binary file not shown.

handwritten/firestore/dev/src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,13 @@ export class Firestore implements firestore.Firestore {
783783
validateBoolean('settings.ssl', settings.ssl);
784784
}
785785

786+
if (settings.alwaysUseImplicitOrderBy !== undefined) {
787+
validateBoolean(
788+
'settings.alwaysUseImplicitOrderBy',
789+
settings.alwaysUseImplicitOrderBy,
790+
);
791+
}
792+
786793
if (settings.maxIdleChannels !== undefined) {
787794
validateInteger('settings.maxIdleChannels', settings.maxIdleChannels, {
788795
minValue: 0,
@@ -845,6 +852,14 @@ export class Firestore implements firestore.Firestore {
845852
return this._databaseId || DEFAULT_DATABASE_ID;
846853
}
847854

855+
/**
856+
* Whether to always use implicit order by clauses.
857+
* @internal
858+
*/
859+
get alwaysUseImplicitOrderBy(): boolean {
860+
return this._settings.alwaysUseImplicitOrderBy ?? false;
861+
}
862+
848863
/**
849864
* Returns the root path of the database. Validates that
850865
* `initializeIfNeeded()` was called before.

handwritten/firestore/dev/src/reference/query.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,6 +1475,7 @@ export class Query<
14751475
toProto(
14761476
transactionOrReadTime?: Uint8Array | Timestamp | api.ITransactionOptions,
14771477
explainOptions?: firestore.ExplainOptions,
1478+
forceImplicitOrderBy?: boolean,
14781479
): api.IRunQueryRequest {
14791480
const projectId = this.firestore.projectId;
14801481
const databaseId = this.firestore.databaseId;
@@ -1483,18 +1484,18 @@ export class Query<
14831484
databaseId,
14841485
);
14851486

1486-
const structuredQuery = this.toStructuredQuery();
1487+
const structuredQuery = this.toStructuredQuery(forceImplicitOrderBy);
14871488

14881489
// For limitToLast queries, the structured query has to be translated to a version with
14891490
// reversed ordered, and flipped startAt/endAt to work properly.
14901491
if (this._queryOptions.limitType === LimitType.Last) {
1491-
if (!this._queryOptions.hasFieldOrders()) {
1492-
throw new Error(
1493-
'limitToLast() queries require specifying at least one orderBy() clause.',
1494-
);
1495-
}
1492+
const forceImplicit =
1493+
forceImplicitOrderBy || this._firestore.alwaysUseImplicitOrderBy;
1494+
const fieldOrders = forceImplicit
1495+
? this.createImplicitOrderBy()
1496+
: this._queryOptions.fieldOrders;
14961497

1497-
structuredQuery.orderBy = this._queryOptions.fieldOrders!.map(order => {
1498+
structuredQuery.orderBy = fieldOrders.map(order => {
14981499
// Flip the orderBy directions since we want the last results
14991500
const dir =
15001501
order.direction === 'DESCENDING' ? 'ASCENDING' : 'DESCENDING';
@@ -1564,7 +1565,9 @@ export class Query<
15641565
return bundledQuery;
15651566
}
15661567

1567-
private toStructuredQuery(): api.IStructuredQuery {
1568+
private toStructuredQuery(
1569+
forceImplicitOrderBy?: boolean,
1570+
): api.IStructuredQuery {
15681571
const structuredQuery: api.IStructuredQuery = {
15691572
from: [{}],
15701573
};
@@ -1586,9 +1589,19 @@ export class Query<
15861589
).toProto();
15871590
}
15881591

1589-
if (this._queryOptions.hasFieldOrders()) {
1590-
structuredQuery.orderBy = this._queryOptions.fieldOrders.map(o =>
1591-
o.toProto(),
1592+
// orders
1593+
const forceImplicit =
1594+
forceImplicitOrderBy || this._firestore.alwaysUseImplicitOrderBy;
1595+
let fieldOrders = this._queryOptions.fieldOrders;
1596+
if (forceImplicit) {
1597+
fieldOrders = this.createImplicitOrderBy();
1598+
}
1599+
1600+
if (fieldOrders.length > 0) {
1601+
structuredQuery.orderBy = fieldOrders.map(o => o.toProto());
1602+
} else if (this._queryOptions.limitType === LimitType.Last) {
1603+
throw new Error(
1604+
'limitToLast() queries require specifying at least one orderBy() clause.',
15921605
);
15931606
}
15941607

handwritten/firestore/dev/src/watch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -941,7 +941,7 @@ export class QueryWatch<
941941
}
942942

943943
getTarget(resumeToken?: Uint8Array): google.firestore.v1.ITarget {
944-
const query = this.query.toProto();
944+
const query = this.query.toProto(undefined, undefined, true);
945945
return {query, targetId: WATCH_TARGET_ID, resumeToken};
946946
}
947947
}

handwritten/firestore/dev/system-test/query.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2291,5 +2291,55 @@ describe.skipClassic('Query and Pipeline Compare - Enterprise DB', () => {
22912291
expectDocs(await compareQueryAndPipeline(query), 'doc2', 'doc3', 'doc4');
22922292
expectDocs(await queryWithCursor.get(), 'doc2', 'doc3', 'doc4');
22932293
});
2294+
2295+
it('alwaysUseImplicitOrderBy returns same results', async () => {
2296+
const collection = getTestRoot();
2297+
const docs = {
2298+
doc01: {sort: 1},
2299+
doc02: {sort: 2},
2300+
doc03: {sort: 3},
2301+
doc04: {sort: 4},
2302+
doc05: {sort: 5},
2303+
doc06: {sort: 6},
2304+
doc07: {sort: 7},
2305+
doc08: {sort: 8},
2306+
doc09: {sort: 9},
2307+
doc10: {sort: 10},
2308+
};
2309+
2310+
for (const [id, data] of Object.entries(docs)) {
2311+
await collection.doc(id).set(data);
2312+
}
2313+
2314+
const expectedOrder = [
2315+
'doc02',
2316+
'doc03',
2317+
'doc04',
2318+
'doc05',
2319+
'doc06',
2320+
'doc07',
2321+
'doc08',
2322+
'doc09',
2323+
'doc10',
2324+
];
2325+
2326+
// TODO: This test should run against both standard and enterprise
2327+
// and verify the results respectively
2328+
// const originalQuery = collection.where('sort', '>', 1);
2329+
// const originalSnapshot = await originalQuery.get();
2330+
// const originalResult = originalSnapshot.docs.map(d => d.id);
2331+
2332+
const modifiedFirestore = new Firestore({
2333+
...firestore.settings,
2334+
alwaysUseImplicitOrderBy: true,
2335+
});
2336+
const modifiedCollection = modifiedFirestore.collection(collection.id);
2337+
const query = modifiedCollection.where('sort', '>', 1);
2338+
const snapshot = await query.get();
2339+
const result = snapshot.docs.map(d => d.id);
2340+
2341+
// since alwaysUseImplicitOrderBy is true, we expect strict ordering.
2342+
expect(result).to.deep.equal(expectedOrder);
2343+
});
22942344
});
22952345
});

handwritten/firestore/dev/test/aggregateQuery.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {google} from '../protos/firestore_v1_proto_api';
2525
import api = google.firestore.v1;
2626
import * as chaiAsPromised from 'chai-as-promised';
2727
import {setTimeoutHandler} from '../src/backoff';
28+
import * as extend from 'extend';
29+
2830
use(chaiAsPromised);
2931

3032
describe('aggregate query interface', () => {
@@ -100,6 +102,57 @@ describe('aggregate query interface', () => {
100102
});
101103
});
102104

105+
it('supports alwaysUseImplicitOrderBy', async () => {
106+
const result: api.IRunAggregationQueryResponse = {
107+
result: {
108+
aggregateFields: {
109+
aggregate_0: {integerValue: '99'},
110+
},
111+
},
112+
readTime: {seconds: 5, nanos: 6},
113+
};
114+
const overrides: ApiOverride = {
115+
runAggregationQuery: request => {
116+
let actualStructuredQuery =
117+
request!.structuredAggregationQuery?.structuredQuery;
118+
actualStructuredQuery = extend(true, {}, actualStructuredQuery);
119+
expect(actualStructuredQuery).to.deep.equal({
120+
from: [{collectionId: 'collectionId'}],
121+
where: {
122+
fieldFilter: {
123+
field: {fieldPath: 'foo'},
124+
op: 'GREATER_THAN' as api.StructuredQuery.FieldFilter.Operator,
125+
value: {stringValue: 'bar'},
126+
},
127+
},
128+
orderBy: [
129+
{
130+
direction: 'ASCENDING' as api.StructuredQuery.Direction,
131+
field: {fieldPath: 'foo'},
132+
},
133+
{
134+
direction: 'ASCENDING' as api.StructuredQuery.Direction,
135+
field: {fieldPath: '__name__'},
136+
},
137+
],
138+
});
139+
return stream(result);
140+
},
141+
};
142+
143+
firestore = await createInstance(overrides, {
144+
alwaysUseImplicitOrderBy: true,
145+
});
146+
147+
const query = firestore
148+
.collection('collectionId')
149+
.where('foo', '>', 'bar')
150+
.count();
151+
return query.get().then(results => {
152+
expect(results.data().count).to.be.equal(99);
153+
});
154+
});
155+
103156
it('handles stream exception at initialization', async () => {
104157
let attempts = 0;
105158
const query = firestore.collection('collectionId').count();

handwritten/firestore/dev/test/query.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,48 @@ describe('query interface', () => {
609609
expect(results.readTime.isEqual(new Timestamp(5, 6))).to.be.true;
610610
});
611611

612+
it('supports alwaysUseImplicitOrderBy with limitToLast', async () => {
613+
const overrides: ApiOverride = {
614+
runQuery: request => {
615+
queryEquals(
616+
request,
617+
where(fieldFilters('foo', 'GREATER_THAN_OR_EQUAL', 'bar')),
618+
{
619+
orderBy: [
620+
{
621+
field: {fieldPath: 'foo'},
622+
direction: 'DESCENDING',
623+
},
624+
{
625+
field: {fieldPath: '__name__'},
626+
direction: 'DESCENDING',
627+
},
628+
],
629+
limit: {value: 1},
630+
},
631+
);
632+
return stream({readTime: {seconds: 5, nanos: 6}});
633+
},
634+
};
635+
636+
firestore = await createInstance(overrides, {
637+
alwaysUseImplicitOrderBy: true,
638+
});
639+
const query = firestore
640+
.collection('collectionId')
641+
.where('foo', '>=', 'bar')
642+
.limitToLast(1);
643+
await query.get();
644+
});
645+
646+
it('throws for limitToLast without orderBy', async () => {
647+
firestore = await createInstance();
648+
const query = firestore.collection('collectionId').limitToLast(1);
649+
expect(() => query.toProto()).to.throw(
650+
'limitToLast() queries require specifying at least one orderBy() clause.',
651+
);
652+
});
653+
612654
it('retries on stream failure', async () => {
613655
let attempts = 0;
614656
const overrides: ApiOverride = {

0 commit comments

Comments
 (0)