mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
987 lines
31 KiB
TypeScript
987 lines
31 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2023 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { expect } from 'chai';
|
|
|
|
import { it } from '../../util/mocha_extensions';
|
|
import { CompositeIndexTestHelper } from '../util/composite_index_test_helper';
|
|
import {
|
|
where,
|
|
orderBy,
|
|
limit,
|
|
limitToLast,
|
|
or,
|
|
getAggregateFromServer,
|
|
sum,
|
|
average,
|
|
count,
|
|
doc,
|
|
writeBatch,
|
|
collectionGroup,
|
|
FieldPath,
|
|
and,
|
|
getCountFromServer,
|
|
documentId,
|
|
disableNetwork
|
|
} from '../util/firebase_export';
|
|
import { apiDescribe } from '../util/helpers';
|
|
|
|
/*
|
|
* Guidance for Creating Tests:
|
|
* ----------------------------
|
|
* When creating tests that require composite indexes, it is recommended to utilize the
|
|
* "CompositeIndexTestHelper" class. This utility class provides methods for creating
|
|
* and setting test documents and running queries with ease, ensuring proper data
|
|
* isolation and query construction.
|
|
*
|
|
* To get started, please refer to the instructions provided in the README file. This will guide you
|
|
* through setting up your local testing environment and updating the Terraform configuration with
|
|
* any new composite indexes required for your testing scenarios.
|
|
*
|
|
* Note: Whenever feasible, make use of the current document fields (such as 'a,' 'b,' 'author,'
|
|
* 'title') to avoid introducing new composite indexes and surpassing the limit. Refer to the
|
|
* guidelines at https://firebase.google.com/docs/firestore/quotas#indexes for further information.
|
|
*/
|
|
apiDescribe('Composite Index Queries', persistence => {
|
|
// OR Query tests only run when the SDK's local cache is configured to use
|
|
// LRU garbage collection (rather than eager garbage collection) because
|
|
// they validate that the result from server and cache match.
|
|
// eslint-disable-next-line no-restricted-properties
|
|
(persistence.gc === 'lru' ? describe : describe.skip)('OR Queries', () => {
|
|
it('can use query overloads', () => {
|
|
const testDocs = {
|
|
doc1: { a: 1, b: 0 },
|
|
doc2: { a: 2, b: 1 },
|
|
doc3: { a: 3, b: 2 },
|
|
doc4: { a: 1, b: 3 },
|
|
doc5: { a: 1, b: 1 }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
// a == 1, limit 2, b - desc
|
|
await testHelper.assertOnlineAndOfflineResultsMatch(
|
|
coll,
|
|
testHelper.query(
|
|
coll,
|
|
where('a', '==', 1),
|
|
limit(2),
|
|
orderBy('b', 'desc')
|
|
),
|
|
'doc4',
|
|
'doc5'
|
|
);
|
|
});
|
|
});
|
|
|
|
it('can use or queries', () => {
|
|
const testDocs = {
|
|
doc1: { a: 1, b: 0 },
|
|
doc2: { a: 2, b: 1 },
|
|
doc3: { a: 3, b: 2 },
|
|
doc4: { a: 1, b: 3 },
|
|
doc5: { a: 1, b: 1 }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
// with one inequality: a>2 || b==1.
|
|
await testHelper.assertOnlineAndOfflineResultsMatch(
|
|
coll,
|
|
testHelper.compositeQuery(
|
|
coll,
|
|
or(where('a', '>', 2), where('b', '==', 1))
|
|
),
|
|
'doc5',
|
|
'doc2',
|
|
'doc3'
|
|
);
|
|
|
|
// Test with limits (implicit order by ASC): (a==1) || (b > 0) LIMIT 2
|
|
await testHelper.assertOnlineAndOfflineResultsMatch(
|
|
coll,
|
|
testHelper.compositeQuery(
|
|
coll,
|
|
or(where('a', '==', 1), where('b', '>', 0)),
|
|
limit(2)
|
|
),
|
|
'doc1',
|
|
'doc2'
|
|
);
|
|
|
|
// Test with limits (explicit order by): (a==1) || (b > 0) LIMIT_TO_LAST 2
|
|
// Note: The public query API does not allow implicit ordering when limitToLast is used.
|
|
await testHelper.assertOnlineAndOfflineResultsMatch(
|
|
coll,
|
|
testHelper.compositeQuery(
|
|
coll,
|
|
or(where('a', '==', 1), where('b', '>', 0)),
|
|
limitToLast(2),
|
|
orderBy('b')
|
|
),
|
|
'doc3',
|
|
'doc4'
|
|
);
|
|
|
|
// Test with limits (explicit order by ASC): (a==2) || (b == 1) ORDER BY a LIMIT 1
|
|
await testHelper.assertOnlineAndOfflineResultsMatch(
|
|
coll,
|
|
testHelper.compositeQuery(
|
|
coll,
|
|
or(where('a', '==', 2), where('b', '==', 1)),
|
|
limit(1),
|
|
orderBy('a')
|
|
),
|
|
'doc5'
|
|
);
|
|
|
|
// Test with limits (explicit order by DESC): (a==2) || (b == 1) ORDER BY a LIMIT_TO_LAST 1
|
|
await testHelper.assertOnlineAndOfflineResultsMatch(
|
|
coll,
|
|
testHelper.compositeQuery(
|
|
coll,
|
|
or(where('a', '==', 2), where('b', '==', 1)),
|
|
limitToLast(1),
|
|
orderBy('a')
|
|
),
|
|
'doc2'
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Aggregation queries - sum / average', () => {
|
|
it('aggregate query supports collection groups - multi-aggregate', () => {
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestCollection(persistence, async (coll, db) => {
|
|
const collectionGroupId = coll.id;
|
|
const docPaths = [
|
|
`${collectionGroupId}/cg-doc1`,
|
|
`abc/123/${collectionGroupId}/cg-doc2`,
|
|
`zzz${collectionGroupId}/cg-doc3`,
|
|
`abc/123/zzz${collectionGroupId}/cg-doc4`,
|
|
`abc/123/zzz/${collectionGroupId}`
|
|
];
|
|
const batch = writeBatch(db);
|
|
for (const docPath of docPaths) {
|
|
// Add test specific fields to the document value
|
|
batch.set(
|
|
doc(db, docPath),
|
|
testHelper.addTestSpecificFieldsToDoc({ a: 2 })
|
|
);
|
|
}
|
|
await batch.commit();
|
|
const snapshot = await getAggregateFromServer(
|
|
testHelper.query(collectionGroup(db, collectionGroupId)),
|
|
{
|
|
count: count(),
|
|
sum: sum('a'),
|
|
avg: average('a')
|
|
}
|
|
);
|
|
expect(snapshot.data().count).to.equal(2);
|
|
expect(snapshot.data().sum).to.equal(4);
|
|
expect(snapshot.data().avg).to.equal(2);
|
|
});
|
|
});
|
|
|
|
it.skipEmulator.skipEnterprise(
|
|
'performs aggregations on documents with all aggregated fields using getAggregationFromServer',
|
|
() => {
|
|
const testDocs = {
|
|
a: { author: 'authorA', title: 'titleA', pages: 100, year: 1980 },
|
|
b: { author: 'authorB', title: 'titleB', pages: 50, year: 2020 },
|
|
c: { author: 'authorC', title: 'titleC', pages: 150, year: 2021 },
|
|
d: { author: 'authorD', title: 'titleD', pages: 50 }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
const snapshot = await getAggregateFromServer(
|
|
testHelper.query(coll),
|
|
{
|
|
totalPages: sum('pages'),
|
|
averagePages: average('pages'),
|
|
averageYear: average('year'),
|
|
count: count()
|
|
}
|
|
);
|
|
expect(snapshot.data().totalPages).to.equal(300);
|
|
expect(snapshot.data().averagePages).to.equal(100);
|
|
expect(snapshot.data().averageYear).to.equal(2007);
|
|
expect(snapshot.data().count).to.equal(3);
|
|
});
|
|
}
|
|
);
|
|
|
|
it('performs aggregates on multiple fields where one aggregate could cause short-circuit due to NaN using getAggregationFromServer', () => {
|
|
const testDocs = {
|
|
a: {
|
|
author: 'authorA',
|
|
title: 'titleA',
|
|
pages: 100,
|
|
year: 1980,
|
|
rating: 5
|
|
},
|
|
b: {
|
|
author: 'authorB',
|
|
title: 'titleB',
|
|
pages: 50,
|
|
year: 2020,
|
|
rating: 4
|
|
},
|
|
c: {
|
|
author: 'authorC',
|
|
title: 'titleC',
|
|
pages: 100,
|
|
year: 1980,
|
|
rating: Number.NaN
|
|
},
|
|
d: {
|
|
author: 'authorD',
|
|
title: 'titleD',
|
|
pages: 50,
|
|
year: 2020,
|
|
rating: 0
|
|
}
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
const snapshot = await getAggregateFromServer(testHelper.query(coll), {
|
|
totalRating: sum('rating'),
|
|
totalPages: sum('pages'),
|
|
averageYear: average('year')
|
|
});
|
|
expect(snapshot.data().totalRating).to.be.NaN;
|
|
expect(snapshot.data().totalPages).to.equal(300);
|
|
expect(snapshot.data().averageYear).to.equal(2000);
|
|
});
|
|
});
|
|
|
|
it.skipEnterprise(
|
|
'performs aggregates when using `array-contains-any` operator getAggregationFromServer',
|
|
() => {
|
|
const testDocs = {
|
|
a: {
|
|
author: 'authorA',
|
|
title: 'titleA',
|
|
pages: 100,
|
|
year: 1980,
|
|
rating: [5, 1000]
|
|
},
|
|
b: {
|
|
author: 'authorB',
|
|
title: 'titleB',
|
|
pages: 50,
|
|
year: 2020,
|
|
rating: [4]
|
|
},
|
|
c: {
|
|
author: 'authorC',
|
|
title: 'titleC',
|
|
pages: 100,
|
|
year: 1980,
|
|
rating: [2222, 3]
|
|
},
|
|
d: {
|
|
author: 'authorD',
|
|
title: 'titleD',
|
|
pages: 50,
|
|
year: 2020,
|
|
rating: [0]
|
|
}
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
const snapshot = await getAggregateFromServer(
|
|
testHelper.query(
|
|
coll,
|
|
where('rating', 'array-contains-any', [5, 3])
|
|
),
|
|
{
|
|
totalRating: sum('rating'),
|
|
averageRating: average('rating'),
|
|
totalPages: sum('pages'),
|
|
averagePages: average('pages'),
|
|
countOfDocs: count()
|
|
}
|
|
);
|
|
expect(snapshot.data().totalRating).to.equal(0);
|
|
expect(snapshot.data().averageRating).to.be.null;
|
|
expect(snapshot.data().totalPages).to.equal(200);
|
|
expect(snapshot.data().averagePages).to.equal(100);
|
|
expect(snapshot.data().countOfDocs).to.equal(2);
|
|
});
|
|
}
|
|
);
|
|
});
|
|
|
|
describe('Multiple Inequality', () => {
|
|
it('can use multiple inequality filters', async () => {
|
|
const testDocs = {
|
|
doc1: { key: 'a', sort: 0, v: 0 },
|
|
doc2: { key: 'b', sort: 3, v: 1 },
|
|
doc3: { key: 'c', sort: 1, v: 3 },
|
|
doc4: { key: 'd', sort: 2, v: 2 }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
// Multiple inequality fields
|
|
let snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '!=', 'a'),
|
|
where('sort', '<=', 2),
|
|
where('v', '>', 2)
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc3']);
|
|
|
|
// Duplicate inequality fields
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '!=', 'a'),
|
|
where('sort', '<=', 2),
|
|
where('sort', '>', 1)
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc4']);
|
|
|
|
// With multiple IN
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '>=', 'a'),
|
|
where('sort', '<=', 2),
|
|
where('v', 'in', [2, 3, 4]),
|
|
where('sort', 'in', [2, 3])
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc4']);
|
|
|
|
// With NOT-IN
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '>=', 'a'),
|
|
where('sort', '<=', 2),
|
|
where('v', 'not-in', [2, 4, 5])
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc1', 'doc3']);
|
|
|
|
// With orderby
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '>=', 'a'),
|
|
where('sort', '<=', 2),
|
|
orderBy('v', 'desc')
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, [
|
|
'doc3',
|
|
'doc4',
|
|
'doc1'
|
|
]);
|
|
|
|
// With limit
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '>=', 'a'),
|
|
where('sort', '<=', 2),
|
|
orderBy('v', 'desc'),
|
|
limit(2)
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc3', 'doc4']);
|
|
|
|
// With limitToLast
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '>=', 'a'),
|
|
where('sort', '<=', 2),
|
|
orderBy('v', 'desc'),
|
|
limitToLast(2)
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc4', 'doc1']);
|
|
});
|
|
});
|
|
|
|
it('can use on special values', async () => {
|
|
const testDocs = {
|
|
doc1: { key: 'a', sort: 0, v: 0 },
|
|
doc2: { key: 'b', sort: NaN, v: 1 },
|
|
doc3: { key: 'c', sort: null, v: 3 },
|
|
doc4: { key: 'd', v: 0 },
|
|
doc5: { key: 'e', sort: 1 },
|
|
doc6: { key: 'f', sort: 1, v: 1 }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
let snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '!=', 'a'),
|
|
where('sort', '<=', 2)
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc5', 'doc6']);
|
|
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '!=', 'a'),
|
|
where('sort', '<=', 2),
|
|
where('v', '<=', 1)
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc6']);
|
|
});
|
|
});
|
|
|
|
it('can use with array membership', async () => {
|
|
const testDocs = {
|
|
doc1: { key: 'a', sort: 0, v: [0] },
|
|
doc2: { key: 'b', sort: 1, v: [0, 1, 3] },
|
|
doc3: { key: 'c', sort: 1, v: [] },
|
|
doc4: { key: 'd', sort: 2, v: [1] },
|
|
doc5: { key: 'e', sort: 3, v: [2, 4] },
|
|
doc6: { key: 'f', sort: 4, v: [NaN] },
|
|
doc7: { key: 'g', sort: 4, v: [null] }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
let snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '!=', 'a'),
|
|
where('sort', '>=', 1),
|
|
where('v', 'array-contains', 0)
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc2']);
|
|
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '!=', 'a'),
|
|
where('sort', '>=', 1),
|
|
where('v', 'array-contains-any', [0, 1])
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc2', 'doc4']);
|
|
});
|
|
});
|
|
|
|
it('can use with nested field', () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const testData = (n?: number): any => {
|
|
n = n || 1;
|
|
return {
|
|
name: 'room ' + n,
|
|
metadata: {
|
|
createdAt: n
|
|
},
|
|
field: 'field ' + n,
|
|
'field.dot': n,
|
|
'field\\slash': n
|
|
};
|
|
};
|
|
|
|
const testDocs = {
|
|
'doc1': testData(400),
|
|
'doc2': testData(200),
|
|
'doc3': testData(100),
|
|
'doc4': testData(300)
|
|
};
|
|
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
let snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('metadata.createdAt', '<=', 500),
|
|
where('metadata.createdAt', '>', 100),
|
|
where('name', '!=', 'room 200'),
|
|
orderBy('name')
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc4', 'doc1']);
|
|
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('field', '>=', 'field 100'),
|
|
where(new FieldPath('field.dot'), '!=', 300),
|
|
where('field\\slash', '<', 400),
|
|
orderBy('name', 'desc')
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc2', 'doc3']);
|
|
});
|
|
});
|
|
|
|
it('can use with nested composite filters', async () => {
|
|
const testDocs = {
|
|
doc1: { key: 'a', sort: 0, v: 5 },
|
|
doc2: { key: 'aa', sort: 4, v: 4 },
|
|
doc3: { key: 'c', sort: 3, v: 3 },
|
|
doc4: { key: 'b', sort: 2, v: 2 },
|
|
doc5: { key: 'b', sort: 2, v: 1 },
|
|
doc6: { key: 'b', sort: 0, v: 0 }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
let snapshot = await testHelper.getDocs(
|
|
testHelper.compositeQuery(
|
|
coll,
|
|
or(
|
|
and(where('key', '==', 'b'), where('sort', '<=', 2)),
|
|
and(where('key', '!=', 'b'), where('v', '>', 4))
|
|
)
|
|
)
|
|
);
|
|
// Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, [
|
|
'doc1',
|
|
'doc6',
|
|
'doc5',
|
|
'doc4'
|
|
]);
|
|
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.compositeQuery(
|
|
coll,
|
|
or(
|
|
and(where('key', '==', 'b'), where('sort', '<=', 2)),
|
|
and(where('key', '!=', 'b'), where('v', '>', 4))
|
|
),
|
|
orderBy('sort', 'desc'),
|
|
orderBy('key')
|
|
)
|
|
);
|
|
// Ordered by: 'sort' desc, 'key' asc, 'v' asc, __name__ asc
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, [
|
|
'doc5',
|
|
'doc4',
|
|
'doc1',
|
|
'doc6'
|
|
]);
|
|
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.compositeQuery(
|
|
coll,
|
|
and(
|
|
or(
|
|
and(where('key', '==', 'b'), where('sort', '<=', 4)),
|
|
and(where('key', '!=', 'b'), where('v', '>=', 4))
|
|
),
|
|
or(
|
|
and(where('key', '>', 'b'), where('sort', '>=', 1)),
|
|
and(where('key', '<', 'b'), where('v', '>', 0))
|
|
)
|
|
)
|
|
)
|
|
);
|
|
// Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, ['doc1', 'doc2']);
|
|
});
|
|
});
|
|
|
|
it('inequality fields will be implicitly ordered lexicographically', async () => {
|
|
const testDocs = {
|
|
doc1: { key: 'a', sort: 0, v: 5 },
|
|
doc2: { key: 'aa', sort: 4, v: 4 },
|
|
doc3: { key: 'b', sort: 3, v: 3 },
|
|
doc4: { key: 'b', sort: 2, v: 2 },
|
|
doc5: { key: 'b', sort: 2, v: 1 },
|
|
doc6: { key: 'b', sort: 0, v: 0 }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
// Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc
|
|
let snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '!=', 'a'),
|
|
where('sort', '>', 1),
|
|
where('v', 'in', [1, 2, 3, 4])
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, [
|
|
'doc2',
|
|
'doc4',
|
|
'doc5',
|
|
'doc3'
|
|
]);
|
|
|
|
// Implicitly ordered by: 'key' asc, 'sort' asc,__name__ asc
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('sort', '>', 1),
|
|
where('key', '!=', 'a'),
|
|
where('v', 'in', [1, 2, 3, 4])
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, [
|
|
'doc2',
|
|
'doc4',
|
|
'doc5',
|
|
'doc3'
|
|
]);
|
|
});
|
|
});
|
|
|
|
it('can use multiple explicit order by field', async () => {
|
|
const testDocs = {
|
|
doc1: { key: 'a', sort: 5, v: 0 },
|
|
doc2: { key: 'aa', sort: 4, v: 0 },
|
|
doc3: { key: 'b', sort: 3, v: 1 },
|
|
doc4: { key: 'b', sort: 2, v: 1 },
|
|
doc5: { key: 'bb', sort: 1, v: 1 },
|
|
doc6: { key: 'c', sort: 0, v: 2 }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
// Ordered by: 'v' asc, 'key' asc, 'sort' asc, __name__ asc
|
|
let snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '>', 'a'),
|
|
where('sort', '>=', 1),
|
|
orderBy('v')
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, [
|
|
'doc2',
|
|
'doc4',
|
|
'doc3',
|
|
'doc5'
|
|
]);
|
|
|
|
// Ordered by: 'v asc, 'sort' asc, 'key' asc, __name__ asc
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '>', 'a'),
|
|
where('sort', '>=', 1),
|
|
orderBy('v'),
|
|
orderBy('sort')
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, [
|
|
'doc2',
|
|
'doc5',
|
|
'doc4',
|
|
'doc3'
|
|
]);
|
|
|
|
// Implicit order by matches the direction of last explicit order by.
|
|
// Ordered by: 'v' desc, 'key' desc, 'sort' desc, __name__ desc
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '>', 'a'),
|
|
where('sort', '>=', 1),
|
|
orderBy('v', 'desc')
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, [
|
|
'doc5',
|
|
'doc3',
|
|
'doc4',
|
|
'doc2'
|
|
]);
|
|
|
|
// Ordered by: 'v desc, 'sort' asc, 'key' asc, __name__ asc
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '>', 'a'),
|
|
where('sort', '>=', 1),
|
|
orderBy('v', 'desc'),
|
|
orderBy('sort')
|
|
)
|
|
);
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, [
|
|
'doc5',
|
|
'doc4',
|
|
'doc3',
|
|
'doc2'
|
|
]);
|
|
});
|
|
});
|
|
|
|
it('can use in aggregate query', async () => {
|
|
const testDocs = {
|
|
doc1: { key: 'a', sort: 5, v: 0 },
|
|
doc2: { key: 'aa', sort: 4, v: 0 },
|
|
doc3: { key: 'b', sort: 3, v: 1 },
|
|
doc4: { key: 'b', sort: 2, v: 1 },
|
|
doc5: { key: 'bb', sort: 1, v: 1 }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
const snapshot1 = await getCountFromServer(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '>', 'a'),
|
|
where('sort', '>=', 1),
|
|
orderBy('v')
|
|
)
|
|
);
|
|
|
|
expect(snapshot1.data().count).to.equal(4);
|
|
|
|
const snapshot2 = await getAggregateFromServer(
|
|
testHelper.query(
|
|
coll,
|
|
where('key', '>', 'a'),
|
|
where('sort', '>=', 1),
|
|
where('v', '!=', 0)
|
|
),
|
|
{
|
|
count: count(),
|
|
sum: sum('sort'),
|
|
avg: average('v')
|
|
}
|
|
);
|
|
expect(snapshot2.data().count).to.equal(3);
|
|
expect(snapshot2.data().sum).to.equal(6);
|
|
expect(snapshot2.data().avg).to.equal(1);
|
|
});
|
|
});
|
|
|
|
it('can use document ID im multiple inequality query', () => {
|
|
const testDocs = {
|
|
doc1: { key: 'a', sort: 5 },
|
|
doc2: { key: 'aa', sort: 4 },
|
|
doc3: { key: 'b', sort: 3 },
|
|
doc4: { key: 'b', sort: 2 },
|
|
doc5: { key: 'bb', sort: 1 }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
let snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where('sort', '>=', 1),
|
|
where('key', '!=', 'a'),
|
|
where(documentId(), '<', 'doc5')
|
|
)
|
|
);
|
|
// Document Key in inequality field will implicitly ordered to the last.
|
|
// Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, [
|
|
'doc2',
|
|
'doc4',
|
|
'doc3'
|
|
]);
|
|
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where(documentId(), '<', 'doc5'),
|
|
where('sort', '>=', 1),
|
|
where('key', '!=', 'a')
|
|
)
|
|
);
|
|
// Changing filters order will not effect implicit order.
|
|
// Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, [
|
|
'doc2',
|
|
'doc4',
|
|
'doc3'
|
|
]);
|
|
|
|
snapshot = await testHelper.getDocs(
|
|
testHelper.query(
|
|
coll,
|
|
where(documentId(), '<', 'doc5'),
|
|
where('sort', '>=', 1),
|
|
where('key', '!=', 'a'),
|
|
orderBy('sort', 'desc')
|
|
)
|
|
);
|
|
// Ordered by: 'sort' desc,'key' desc, __name__ desc
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot, [
|
|
'doc2',
|
|
'doc3',
|
|
'doc4'
|
|
]);
|
|
});
|
|
});
|
|
|
|
it('can get documents while offline', () => {
|
|
const testDocs = {
|
|
doc1: { key: 'a', sort: 1 },
|
|
doc2: { key: 'aa', sort: 4 },
|
|
doc3: { key: 'b', sort: 3 },
|
|
doc4: { key: 'b', sort: 2 }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(
|
|
persistence.toLruGc(),
|
|
testDocs,
|
|
async (coll, db) => {
|
|
const query_ = testHelper.query(
|
|
coll,
|
|
where('key', '!=', 'a'),
|
|
where('sort', '<=', 3)
|
|
);
|
|
//populate the cache.
|
|
const snapshot1 = await testHelper.getDocs(query_);
|
|
expect(snapshot1.size).to.equal(2);
|
|
|
|
await disableNetwork(db);
|
|
|
|
const snapshot2 = await testHelper.getDocs(query_);
|
|
expect(snapshot2.metadata.fromCache).to.be.true;
|
|
expect(snapshot2.metadata.hasPendingWrites).to.be.false;
|
|
// Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc
|
|
testHelper.assertSnapshotResultIdsMatch(snapshot2, ['doc4', 'doc3']);
|
|
}
|
|
);
|
|
});
|
|
|
|
// eslint-disable-next-line no-restricted-properties
|
|
(persistence.gc === 'lru' ? it : it.skip)(
|
|
'can get same result from server and cache',
|
|
() => {
|
|
const testDocs = {
|
|
doc1: { a: 1, b: 0 },
|
|
doc2: { a: 2, b: 1 },
|
|
doc3: { a: 3, b: 2 },
|
|
doc4: { a: 1, b: 3 },
|
|
doc5: { a: 1, b: 1 }
|
|
};
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestDocs(persistence, testDocs, async coll => {
|
|
// implicit AND: a != 1 && b < 2
|
|
await testHelper.assertOnlineAndOfflineResultsMatch(
|
|
coll,
|
|
testHelper.query(coll, where('a', '!=', 1), where('b', '<', 2)),
|
|
'doc2'
|
|
);
|
|
|
|
// explicit AND: a != 1 && b < 2
|
|
await testHelper.assertOnlineAndOfflineResultsMatch(
|
|
coll,
|
|
testHelper.compositeQuery(
|
|
coll,
|
|
and(where('a', '!=', 1), where('b', '<', 2))
|
|
),
|
|
'doc2'
|
|
);
|
|
|
|
// explicit AND: a < 3 && b not-in [2, 3]
|
|
// Implicitly ordered by: a asc, b asc, __name__ asc
|
|
await testHelper.assertOnlineAndOfflineResultsMatch(
|
|
coll,
|
|
testHelper.compositeQuery(
|
|
coll,
|
|
and(where('a', '<', 3), where('b', 'not-in', [2, 3]))
|
|
),
|
|
'doc1',
|
|
'doc5',
|
|
'doc2'
|
|
);
|
|
|
|
// a <3 && b != 0, implicitly ordered by: a asc, b asc, __name__ asc
|
|
await testHelper.assertOnlineAndOfflineResultsMatch(
|
|
coll,
|
|
testHelper.query(
|
|
coll,
|
|
where('b', '!=', 0),
|
|
where('a', '<', 3),
|
|
limit(2)
|
|
),
|
|
'doc5',
|
|
'doc4'
|
|
);
|
|
|
|
// a <3 && b != 0, ordered by: b desc, a desc, __name__ desc
|
|
await testHelper.assertOnlineAndOfflineResultsMatch(
|
|
coll,
|
|
testHelper.query(
|
|
coll,
|
|
where('a', '<', 3),
|
|
where('b', '!=', 0),
|
|
orderBy('b', 'desc'),
|
|
limit(2)
|
|
),
|
|
'doc4',
|
|
'doc2'
|
|
);
|
|
|
|
// explicit OR: multiple inequality: a>2 || b<1.
|
|
await testHelper.assertOnlineAndOfflineResultsMatch(
|
|
coll,
|
|
testHelper.compositeQuery(
|
|
coll,
|
|
or(where('a', '>', 2), where('b', '<', 1))
|
|
),
|
|
'doc1',
|
|
'doc3'
|
|
);
|
|
});
|
|
}
|
|
);
|
|
|
|
it('inequality query will reject if document key is not the last orderBy field', () => {
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestCollection(persistence, async coll => {
|
|
// Implicitly ordered by: __name__ asc, 'key' asc,
|
|
const queryForRejection = testHelper.query(
|
|
coll,
|
|
where('key', '!=', 42),
|
|
orderBy(documentId())
|
|
);
|
|
|
|
await expect(
|
|
testHelper.getDocs(queryForRejection)
|
|
).to.be.eventually.rejectedWith(
|
|
/order by clause cannot contain more fields after the key/i
|
|
);
|
|
});
|
|
});
|
|
|
|
//
|
|
it.skipEnterprise(
|
|
'inequality query will reject if document key appears only in equality filter',
|
|
() => {
|
|
const testHelper = new CompositeIndexTestHelper();
|
|
return testHelper.withTestCollection(persistence, async coll => {
|
|
const query_ = testHelper.query(
|
|
coll,
|
|
where('key', '!=', 42),
|
|
where(documentId(), '==', 'doc1')
|
|
);
|
|
await expect(
|
|
testHelper.getDocs(query_)
|
|
).to.be.eventually.rejectedWith(
|
|
'Equality on key is not allowed if there are other inequality fields and key does not appear in inequalities.'
|
|
);
|
|
});
|
|
}
|
|
);
|
|
});
|
|
});
|