Files
Mark DuckworthandGitHub 4e0567c63c Vector Type (#8215)
Implement VectorValue type support.
2024-08-12 07:38:01 -06:00

2018 lines
61 KiB
TypeScript

/**
* @license
* Copyright 2019 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 { User } from '../../../src/auth/user';
import { FieldFilter } from '../../../src/core/filter';
import {
LimitType,
newQueryForCollectionGroup,
Query,
queryToTarget,
queryWithAddedFilter,
queryWithAddedOrderBy,
queryWithEndAt,
queryWithLimit,
queryWithStartAt
} from '../../../src/core/query';
import { vector } from '../../../src/lite-api/field_value_impl';
import { Timestamp } from '../../../src/lite-api/timestamp';
import {
displayNameForIndexType,
IndexType
} from '../../../src/local/index_manager';
import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence';
import { Persistence } from '../../../src/local/persistence';
import { documentMap } from '../../../src/model/collections';
import { Document, MutableDocument } from '../../../src/model/document';
import {
IndexKind,
IndexOffset,
IndexState
} from '../../../src/model/field_index';
import { JsonObject } from '../../../src/model/object_value';
import { canonicalId } from '../../../src/model/values';
import {
ApiClientObjectMap as ProtoObjectMap,
Document as ProtoDocument,
Value as ProtoValue
} from '../../../src/protos/firestore_proto_api';
import {
fromDocument,
JsonProtoSerializer,
toName,
toTimestamp,
toVersion
} from '../../../src/remote/serializer';
import { addEqualityMatcher } from '../../util/equality_matcher';
import {
bound,
deletedDoc,
doc,
fieldIndex,
filter,
key,
orderBy,
orFilter,
path,
query,
version,
wrap
} from '../../util/helpers';
import * as persistenceHelpers from './persistence_test_helpers';
import { TestIndexManager } from './test_index_manager';
describe('index_manager.ts top-level functions', () => {
describe('displayNameForIndexType()', () => {
it('IndexType.NONE', () =>
expect(displayNameForIndexType(IndexType.NONE)).to.equal('NONE'));
it('IndexType.FULL', () =>
expect(displayNameForIndexType(IndexType.FULL)).to.equal('FULL'));
it('IndexType.PARTIAL', () =>
expect(displayNameForIndexType(IndexType.PARTIAL)).to.equal('PARTIAL'));
it('invalid IndexType', () =>
// @ts-expect-error: specifying a string to displayNameForIndexType()
// causes a TypeScript compiler error, but is handled gracefully.
expect(displayNameForIndexType('zzyzx')).to.equal(
'[unknown IndexType: zzyzx]'
));
});
});
describe('MemoryIndexManager', async () => {
genericIndexManagerTests(persistenceHelpers.testMemoryEagerPersistence);
});
describe('IndexedDbIndexManager', async () => {
if (!IndexedDbPersistence.isAvailable()) {
console.warn('No IndexedDB. Skipping IndexedDbIndexManager tests.');
return;
}
let persistencePromise: Promise<Persistence>;
const serializer = new JsonProtoSerializer(
persistenceHelpers.TEST_DATABASE_ID,
/* useProto3Json= */ true
);
beforeEach(async () => {
persistencePromise = persistenceHelpers.testIndexedDbPersistence();
});
async function getIndexManager(
user = User.UNAUTHENTICATED
): Promise<TestIndexManager> {
const persistence = await persistencePromise;
return new TestIndexManager(persistence, persistence.getIndexManager(user));
}
genericIndexManagerTests(() => persistencePromise);
let indexManager: TestIndexManager;
beforeEach(async () => {
indexManager = await getIndexManager();
});
it('can add indexes', async () => {
await indexManager.addFieldIndex(fieldIndex('coll1'));
const fieldIndexes = await indexManager.getFieldIndexes();
expect(fieldIndexes).to.have.length(1);
expect(fieldIndexes[0]).to.deep.equal(fieldIndex('coll1', { id: 1 }));
});
it('uses auto-incrementing index id', async () => {
await indexManager.addFieldIndex(fieldIndex('coll1'));
await indexManager.addFieldIndex(fieldIndex('coll2'));
const fieldIndexes = await indexManager.getFieldIndexes();
expect(fieldIndexes).to.have.length(2);
expect(fieldIndexes[0]).to.deep.equal(fieldIndex('coll1', { id: 1 }));
expect(fieldIndexes[1]).to.deep.equal(fieldIndex('coll2', { id: 2 }));
});
it('can get indexes', async () => {
let fieldIndexes = await indexManager.getFieldIndexes('coll1');
expect(fieldIndexes).to.have.length(0);
await indexManager.addFieldIndex(
fieldIndex('coll1', { fields: [['value', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll2', { fields: [['value', IndexKind.CONTAINS]] })
);
fieldIndexes = await indexManager.getFieldIndexes('coll1');
expect(fieldIndexes).to.have.length(1);
expect(fieldIndexes[0]).to.deep.equal(
fieldIndex('coll1', {
id: 1,
fields: [['value', IndexKind.ASCENDING]]
})
);
await indexManager.addFieldIndex(
fieldIndex('coll1', { fields: [['newValue', IndexKind.CONTAINS]] })
);
fieldIndexes = await indexManager.getFieldIndexes('coll1');
expect(fieldIndexes).to.have.length(2);
expect(fieldIndexes[0]).to.deep.equal(
fieldIndex('coll1', {
id: 1,
fields: [['value', IndexKind.ASCENDING]]
})
);
expect(fieldIndexes[1]).to.deep.equal(
fieldIndex('coll1', {
id: 3,
fields: [['newValue', IndexKind.CONTAINS]]
})
);
});
it('can update collection group', async () => {
await indexManager.addFieldIndex(fieldIndex('coll1'));
await indexManager.addFieldIndex(fieldIndex('coll1'));
await indexManager.addFieldIndex(fieldIndex('coll2'));
let fieldIndexes = await indexManager.getFieldIndexes();
expect(fieldIndexes[0].indexState).to.deep.equal(
new IndexState(/* sequenceNumber= */ 0, IndexOffset.min())
);
expect(fieldIndexes[1].indexState).to.deep.equal(
new IndexState(/* sequenceNumber= */ 0, IndexOffset.min())
);
expect(fieldIndexes[2].indexState).to.deep.equal(
new IndexState(/* sequenceNumber= */ 0, IndexOffset.min())
);
const newOffset = new IndexOffset(version(1337), key('coll1/doc'), 42);
await indexManager.updateCollectionGroup('coll1', newOffset);
fieldIndexes = await indexManager.getFieldIndexes();
expect(fieldIndexes[0].indexState).to.deep.equal(
new IndexState(/* sequenceNumber= */ 1, newOffset)
);
expect(fieldIndexes[1].indexState).to.deep.equal(
new IndexState(/* sequenceNumber= */ 1, newOffset)
);
expect(fieldIndexes[2].indexState).to.deep.equal(
new IndexState(/* sequenceNumber= */ 0, IndexOffset.min())
);
});
it('can get next collection group to update', async () => {
let nextCollectionGroup =
await indexManager.getNextCollectionGroupToUpdate();
expect(nextCollectionGroup).to.be.null;
await indexManager.addFieldIndex(fieldIndex('coll1'));
await indexManager.addFieldIndex(fieldIndex('coll2'));
nextCollectionGroup = await indexManager.getNextCollectionGroupToUpdate();
expect(nextCollectionGroup).to.equal('coll1');
await indexManager.updateCollectionGroup('coll1', IndexOffset.min());
nextCollectionGroup = await indexManager.getNextCollectionGroupToUpdate();
expect(nextCollectionGroup).to.equal('coll2');
await indexManager.updateCollectionGroup('coll2', IndexOffset.min());
nextCollectionGroup = await indexManager.getNextCollectionGroupToUpdate();
expect(nextCollectionGroup).to.equal('coll1');
});
it('deleting field index removes entry from collection group', async () => {
const offsetBefore = new IndexOffset(version(1337), key('coll1/doc'), 42);
const offsetAfter = IndexOffset.min();
await indexManager.addFieldIndex(fieldIndex('coll1'));
const actualIndex = (await indexManager.getFieldIndexes())[0];
await indexManager.updateCollectionGroup('coll1', offsetBefore);
let fieldIndexes = await indexManager.getFieldIndexes();
expect(fieldIndexes[0].indexState).to.deep.equal(
new IndexState(1, offsetBefore)
);
// Delete and re-add the index
await indexManager.deleteFieldIndex(actualIndex);
await indexManager.addFieldIndex(fieldIndex('coll1'));
fieldIndexes = await indexManager.getFieldIndexes();
expect(fieldIndexes[0].indexState).to.deep.equal(
new IndexState(0, offsetAfter)
);
});
it('deleting field index removes entry from getNextCollectionGroupToUpdate()', async () => {
await indexManager.addFieldIndex(fieldIndex('coll1'));
expect(await indexManager.getNextCollectionGroupToUpdate()).to.equal(
'coll1'
);
const actualIndex = (await indexManager.getFieldIndexes())[0];
await indexManager.deleteFieldIndex(actualIndex);
expect(await indexManager.getNextCollectionGroupToUpdate()).to.equal(null);
});
it('persist index offset', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll1', { fields: [['value', IndexKind.ASCENDING]] })
);
const offset = new IndexOffset(version(20), key('coll/doc'), 42);
await indexManager.updateCollectionGroup('coll1', offset);
indexManager = await getIndexManager(User.UNAUTHENTICATED);
const indexes = await indexManager.getFieldIndexes('coll1');
expect(indexes).to.have.length(1);
expect(indexes[0].indexState.offset).to.deep.equal(offset);
});
it('changes user', async () => {
let indexManager = await getIndexManager(new User('user1'));
const user1Offset = new IndexOffset(version(1337), key('coll1/doc'), 42);
const user2Offset = IndexOffset.min();
await indexManager.addFieldIndex(fieldIndex('coll1'));
await indexManager.updateCollectionGroup('coll1', user1Offset);
let fieldIndexes = await indexManager.getFieldIndexes();
expect(fieldIndexes[0].indexState).to.deep.equal(
new IndexState(1, user1Offset)
);
indexManager = await getIndexManager(new User('user2'));
fieldIndexes = await indexManager.getFieldIndexes();
expect(fieldIndexes[0].indexState).to.deep.equal(
new IndexState(0, user2Offset)
);
indexManager = await getIndexManager(new User('user1'));
fieldIndexes = await indexManager.getFieldIndexes();
expect(fieldIndexes[0].indexState).to.deep.equal(
new IndexState(1, user1Offset)
);
});
it('adds documents', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['exists', IndexKind.ASCENDING]] })
);
await addDoc('coll/doc1', { 'exists': 1 });
await addDoc('coll/doc2', {});
});
it('applies orderBy', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['count', IndexKind.ASCENDING]] })
);
await addDoc('coll/val1', { 'count': 1 });
await addDoc('coll/val2', { 'not-count': 2 });
await addDoc('coll/val3', { 'count': 3 });
const q = queryWithAddedOrderBy(query('coll'), orderBy('count'));
await verifyResults(q, 'coll/val1', 'coll/val3');
});
it('applies orderBy with not equals filter', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['count', IndexKind.ASCENDING]] })
);
await addDoc('coll/val1', { 'count': 1 });
await addDoc('coll/val2', { 'count': 2 });
const q = queryWithAddedOrderBy(
queryWithAddedFilter(query('coll'), filter('count', '!=', 2)),
orderBy('count')
);
await verifyResults(q, 'coll/val1');
});
it('applies equality filter', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(query('coll'), filter('count', '==', 2));
await verifyResults(q, 'coll/val2');
});
it('applies nested field equality filter', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['a.b', IndexKind.ASCENDING]] })
);
await addDoc('coll/doc1', { 'a': { 'b': 1 } });
await addDoc('coll/doc2', { 'a': { 'b': 2 } });
const q = queryWithAddedFilter(query('coll'), filter('a.b', '==', 2));
await verifyResults(q, 'coll/doc2');
});
it('applies not equals filter', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(query('coll'), filter('count', '!=', 2));
await verifyResults(q, 'coll/val1', 'coll/val3');
});
it('applies equals with not equals filter', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['a', IndexKind.ASCENDING],
['b', IndexKind.ASCENDING]
]
})
);
await addDoc('coll/val1', { 'a': 1, 'b': 1 });
await addDoc('coll/val2', { 'a': 1, 'b': 2 });
await addDoc('coll/val3', { 'a': 2, 'b': 1 });
await addDoc('coll/val4', { 'a': 2, 'b': 2 });
// Verifies that we apply the filter in the order of the field index
let q = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', '==', 1)),
filter('b', '!=', 1)
);
await verifyResults(q, 'coll/val2');
q = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('b', '!=', 1)),
filter('a', '==', 1)
);
await verifyResults(q, 'coll/val2');
});
it('applies array contains with not equals filter', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['a', IndexKind.CONTAINS],
['b', IndexKind.ASCENDING]
]
})
);
await addDoc('coll/val1', { 'a': [1], 'b': 1 });
await addDoc('coll/val2', { 'a': [1], 'b': 2 });
await addDoc('coll/val3', { 'a': [2], 'b': 1 });
await addDoc('coll/val4', { 'a': [2], 'b': 2 });
const q = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', 'array-contains', 1)),
filter('b', '!=', 1)
);
await verifyResults(q, 'coll/val2');
});
it('applies array contains with not equals filter on same field', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['a', IndexKind.CONTAINS],
['a', IndexKind.ASCENDING]
]
})
);
await addDoc('coll/val1', { 'a': [1, 1] });
await addDoc('coll/val2', { 'a': [1, 2] });
await addDoc('coll/val3', { 'a': [2, 1] });
await addDoc('coll/val4', { 'a': [2, 2] });
const q = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', 'array-contains', 1)),
filter('a', '!=', [1, 2])
);
await verifyResults(q, 'coll/val1', 'coll/val3');
});
it('applies equals with not equals filter on same field', async () => {
await setUpSingleValueFilter();
const filtersAndResults: Array<FieldFilter[] | string[]> = [
[filter('count', '>', 1), filter('count', '!=', 2)],
['coll/val3'],
[filter('count', '==', 1), filter('count', '!=', 2)],
['coll/val1'],
[filter('count', '==', 1), filter('count', '!=', 1)],
[],
[filter('count', '>', 2), filter('count', '!=', 2)],
['coll/val3'],
[filter('count', '>=', 2), filter('count', '!=', 2)],
['coll/val3'],
[filter('count', '<=', 2), filter('count', '!=', 2)],
['coll/val1'],
[filter('count', '<=', 2), filter('count', '!=', 1)],
['coll/val2'],
[filter('count', '<', 2), filter('count', '!=', 2)],
['coll/val1'],
[filter('count', '<', 2), filter('count', '!=', 1)],
[],
[filter('count', '>', 2), filter('count', 'not-in', [3])],
[],
[filter('count', '>=', 2), filter('count', 'not-in', [3])],
['coll/val2'],
[filter('count', '>=', 2), filter('count', 'not-in', [3, 3])],
['coll/val2'],
[
filter('count', '>', 1),
filter('count', '<', 3),
filter('count', '!=', 2)
],
[],
[
filter('count', '>=', 1),
filter('count', '<', 3),
filter('count', '!=', 2)
],
['coll/val1'],
[
filter('count', '>=', 1),
filter('count', '<=', 3),
filter('count', '!=', 2)
],
['coll/val1', 'coll/val3'],
[
filter('count', '>', 1),
filter('count', '<=', 3),
filter('count', '!=', 2)
],
['coll/val3']
];
for (let i = 0; i < filtersAndResults.length; i += 2) {
let q = query('coll');
for (const filter of filtersAndResults[i] as FieldFilter[]) {
q = queryWithAddedFilter(q, filter);
}
await verifyResults(q, ...(filtersAndResults[i + 1] as string[]));
}
});
it('applies less than or equals filter', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(query('coll'), filter('count', '<=', 2));
await verifyResults(q, 'coll/val1', 'coll/val2');
});
it('applies greater than or equals filter', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(query('coll'), filter('count', '>=', 2));
await verifyResults(q, 'coll/val2', 'coll/val3');
});
it('applies greater than filter', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(query('coll'), filter('count', '>', 2));
await verifyResults(q, 'coll/val3');
});
it('applies range filter', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('count', '>', 1)),
filter('count', '<', 3)
);
await verifyResults(q, 'coll/val2');
});
it('applies startAt filter', async () => {
await setUpSingleValueFilter();
const q = queryWithStartAt(
queryWithAddedOrderBy(query('coll'), orderBy('count')),
bound([2], true)
);
await verifyResults(q, 'coll/val2', 'coll/val3');
});
it('applies startAt filter with notIn', async () => {
await setUpSingleValueFilter();
const q = queryWithStartAt(
queryWithAddedOrderBy(
queryWithAddedFilter(query('coll'), filter('count', '!=', 2)),
orderBy('count')
),
bound([2], true)
);
await verifyResults(q, 'coll/val3');
});
it('applies startAfter filter', async () => {
await setUpSingleValueFilter();
const q = queryWithStartAt(
queryWithAddedOrderBy(query('coll'), orderBy('count')),
bound([2], false)
);
await verifyResults(q, 'coll/val3');
});
it('applies endAt filter', async () => {
await setUpSingleValueFilter();
const q = queryWithEndAt(
queryWithAddedOrderBy(query('coll'), orderBy('count')),
bound([2], true)
);
await verifyResults(q, 'coll/val1', 'coll/val2');
});
it('applies endBefore filter', async () => {
await setUpSingleValueFilter();
const q = queryWithEndAt(
queryWithAddedOrderBy(query('coll'), orderBy('count')),
bound([2], false)
);
await verifyResults(q, 'coll/val1');
});
it('applies range with bound filter', async () => {
await setUpSingleValueFilter();
const startAt = queryWithEndAt(
queryWithStartAt(
queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('count', '>=', 1)),
filter('count', '<=', 3)
),
orderBy('count')
),
bound([1], false)
),
bound([2], true)
);
await verifyResults(startAt, 'coll/val2');
});
it('applies in filter', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(
query('coll'),
filter('count', 'in', [1, 3])
);
await verifyResults(q, 'coll/val1', 'coll/val3');
});
it('applies not in filter', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(
query('coll'),
filter('count', 'not-in', [1, 2])
);
await verifyResults(q, 'coll/val3');
});
it('applies not in filter with greater than filter', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('count', '>', 1)),
filter('count', 'not-in', [2])
);
await verifyResults(q, 'coll/val3');
});
it('applies not in filter with out of bounds greater than filter', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('count', '>', 2)),
filter('count', 'not-in', [1])
);
await verifyResults(q, 'coll/val3');
});
it('applies array contains filter', async () => {
await setUpArrayValueFilter();
const q = queryWithAddedFilter(
query('coll'),
filter('values', 'array-contains', 1)
);
await verifyResults(q, 'coll/arr1');
});
it('applies array contains any filter', async () => {
await setUpArrayValueFilter();
const q = queryWithAddedFilter(
query('coll'),
filter('values', 'array-contains-any', [1, 2, 4])
);
await verifyResults(q, 'coll/arr1', 'coll/arr2');
});
it('validates that array contains filter only matches array', async () => {
// Set up two field indexes. This causes two index entries to be written,
// but our query should only use one index.
await setUpArrayValueFilter();
await setUpSingleValueFilter();
await addDoc('coll/nonmatching', { 'values': 1 });
const q = queryWithAddedFilter(
query('coll'),
filter('values', 'array-contains-any', [1])
);
await verifyResults(q, 'coll/arr1');
});
it('returns empty result when no index exists', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(
query('coll'),
filter('unknown', '==', true)
);
expect(await indexManager.getIndexType(queryToTarget(q))).to.equal(
IndexType.NONE
);
expect(await indexManager.getDocumentsMatchingTarget(queryToTarget(q))).to
.be.null;
});
it('handles when no matching filter exists', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(
query('coll'),
filter('unknown', '==', true)
);
expect(await indexManager.getIndexType(queryToTarget(q))).to.equal(
IndexType.NONE
);
expect(await indexManager.getDocumentsMatchingTarget(queryToTarget(q))).to
.be.null;
});
it('returns empty results when no matching documents exists', async () => {
await setUpSingleValueFilter();
const q = queryWithAddedFilter(query('coll'), filter('count', '==', -1));
await verifyResults(q);
});
it('filters by field type', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['value', IndexKind.ASCENDING]] })
);
await addDoc('coll/boolean', { 'value': true });
await addDoc('coll/string', { 'value': 'true' });
await addDoc('coll/number', { 'value': 1 });
const q = queryWithAddedFilter(query('coll'), filter('value', '==', true));
await verifyResults(q, 'coll/boolean');
});
it('supports collection group indexes', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll1', { fields: [['value', IndexKind.ASCENDING]] })
);
await addDoc('coll1/doc1', { 'value': true });
await addDoc('coll2/doc2/coll1/doc1', { 'value': true });
await addDoc('coll2/doc2', { 'value': true });
const q = queryWithAddedFilter(
newQueryForCollectionGroup('coll1'),
filter('value', '==', true)
);
await verifyResults(q, 'coll1/doc1', 'coll2/doc2/coll1/doc1');
});
it('applies limit filter', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['value', IndexKind.ASCENDING]] })
);
await addDoc('coll/doc1', { 'value': 1 });
await addDoc('coll/doc2', { 'value': 1 });
await addDoc('coll/doc3', { 'value': 1 });
const q = queryWithLimit(
queryWithAddedFilter(query('coll'), filter('value', '==', 1)),
2,
LimitType.First
);
await verifyResults(q, 'coll/doc1', 'coll/doc2');
});
it('uses ordering for limit filter', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['value', IndexKind.CONTAINS],
['value', IndexKind.ASCENDING]
]
})
);
await addDoc('coll/doc1', { 'value': [1, 'foo'] });
await addDoc('coll/doc2', { 'value': [3, 'foo'] });
await addDoc('coll/doc3', { 'value': [2, 'foo'] });
const q = queryWithLimit(
queryWithAddedOrderBy(
queryWithAddedFilter(
query('coll'),
filter('value', 'array-contains', 'foo')
),
orderBy('value')
),
2,
LimitType.First
);
await verifyResults(q, 'coll/doc1', 'coll/doc3');
});
it('updates index entries', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['value', IndexKind.ASCENDING]] })
);
const q = queryWithAddedOrderBy(query('coll'), orderBy('value'));
await addDoc('coll/doc1', { 'value': true });
await verifyResults(q, 'coll/doc1');
await addDocs(
doc('coll/doc1', 1, {}),
doc('coll/doc2', 1, { 'value': true })
);
await verifyResults(q, 'coll/doc2');
});
it('removes index entries', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['value', IndexKind.ASCENDING]] })
);
const q = queryWithAddedOrderBy(query('coll'), orderBy('value'));
await addDoc('coll/doc1', { 'value': true });
await verifyResults(q, 'coll/doc1');
await addDocs(deletedDoc('coll/doc1', 1));
await verifyResults(q);
});
it('supports order by key', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['count', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['count', IndexKind.DESCENDING]] })
);
await addDoc('coll/val1a', { 'count': 1 });
await addDoc('coll/val1b', { 'count': 1 });
await addDoc('coll/val2', { 'count': 2 });
let q = queryWithAddedOrderBy(query('coll'), orderBy('count'));
await verifyResults(q, 'coll/val1a', 'coll/val1b', 'coll/val2');
q = queryWithAddedOrderBy(query('coll'), orderBy('count', 'desc'));
await verifyResults(q, 'coll/val2', 'coll/val1b', 'coll/val1a');
});
it('supports order by filter', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['count', IndexKind.ASCENDING]] })
);
await addDoc('coll/val1a', { 'count': 1 });
await addDoc('coll/val1b', { 'count': 1 });
await addDoc('coll/val2', { 'count': 2 });
const q = queryWithAddedOrderBy(query('coll'), orderBy('count'));
await verifyResults(q, 'coll/val1a', 'coll/val1b', 'coll/val2');
});
it('supports ascending order with greater than filter', async () => {
await setUpMultipleOrderBys();
const originalQuery = queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', '==', 2)),
filter('b', '==', 2)
),
filter('c', '<', 5)
),
orderBy('c', 'asc')
);
const queryWithNonRestrictedBound = queryWithEndAt(
queryWithStartAt(originalQuery, bound([1], /* inclusive= */ false)),
bound([6], /* inclusive= */ false)
);
const queryWithRestrictedBound = queryWithEndAt(
queryWithStartAt(originalQuery, bound([2], /* inclusive= */ false)),
bound([4], /* inclusive= */ false)
);
await verifyResults(originalQuery, 'coll/val2', 'coll/val3', 'coll/val4');
await verifyResults(
queryWithNonRestrictedBound,
'coll/val2',
'coll/val3',
'coll/val4'
);
await verifyResults(queryWithRestrictedBound, 'coll/val3');
});
it('supports descending order with less than filter', async () => {
await setUpMultipleOrderBys();
const originalQuery = queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', '==', 2)),
filter('b', '==', 2)
),
filter('c', '<', 5)
),
orderBy('c', 'desc')
);
const queryWithNonRestrictedBound = queryWithEndAt(
queryWithStartAt(originalQuery, bound([6], /* inclusive= */ false)),
bound([1], /* inclusive= */ false)
);
const queryWithRestrictedBound = queryWithEndAt(
queryWithStartAt(originalQuery, bound([4], /* inclusive= */ false)),
bound([2], /* inclusive= */ false)
);
await verifyResults(originalQuery, 'coll/val4', 'coll/val3', 'coll/val2');
await verifyResults(
queryWithNonRestrictedBound,
'coll/val4',
'coll/val3',
'coll/val2'
);
await verifyResults(queryWithRestrictedBound, 'coll/val3');
});
it('supports ascending order with greater than filter', async () => {
await setUpMultipleOrderBys();
const originalQuery = queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', '==', 2)),
filter('b', '==', 2)
),
filter('c', '>', 2)
),
orderBy('c', 'asc')
);
const queryWithNonRestrictedBound = queryWithEndAt(
queryWithStartAt(originalQuery, bound([2], /* inclusive= */ false)),
bound([6], /* inclusive= */ false)
);
const queryWithRestrictedBound = queryWithEndAt(
queryWithStartAt(originalQuery, bound([3], /* inclusive= */ false)),
bound([5], /* inclusive= */ false)
);
await verifyResults(originalQuery, 'coll/val3', 'coll/val4', 'coll/val5');
await verifyResults(
queryWithNonRestrictedBound,
'coll/val3',
'coll/val4',
'coll/val5'
);
await verifyResults(queryWithRestrictedBound, 'coll/val4');
});
it('supports descending order with greater than filter', async () => {
await setUpMultipleOrderBys();
const originalQuery = queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', '==', 2)),
filter('b', '==', 2)
),
filter('c', '>', 2)
),
orderBy('c', 'desc')
);
const queryWithNonRestrictedBound = queryWithEndAt(
queryWithStartAt(originalQuery, bound([6], /* inclusive= */ false)),
bound([2], /* inclusive= */ false)
);
const queryWithRestrictedBound = queryWithEndAt(
queryWithStartAt(originalQuery, bound([5], /* inclusive= */ false)),
bound([3], /* inclusive= */ false)
);
await verifyResults(originalQuery, 'coll/val5', 'coll/val4', 'coll/val3');
await verifyResults(
queryWithNonRestrictedBound,
'coll/val5',
'coll/val4',
'coll/val3'
);
await verifyResults(queryWithRestrictedBound, 'coll/val4');
});
it('cannot expand result set from a cursor', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['c', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['c', IndexKind.DESCENDING]] })
);
await addDoc('coll/val1', { 'a': 1, 'b': 1, 'c': 3 });
await addDoc('coll/val2', { 'a': 2, 'b': 2, 'c': 2 });
let testingQuery = queryWithStartAt(
queryWithAddedOrderBy(
queryWithAddedFilter(query('coll'), filter('c', '>', 2)),
orderBy('c', 'asc')
),
bound([2], /* inclusive= */ true)
);
await verifyResults(testingQuery, 'coll/val1');
testingQuery = queryWithStartAt(
queryWithAddedOrderBy(
queryWithAddedFilter(query('coll'), filter('c', '<', 3)),
orderBy('c', 'desc')
),
bound([3], /* inclusive= */ true)
);
await verifyResults(testingQuery, 'coll/val2');
});
it('can have filters on the same field', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['a', IndexKind.ASCENDING],
['b', IndexKind.ASCENDING]
]
})
);
await addDoc('coll/val1', { 'a': 1, 'b': 1 });
await addDoc('coll/val2', { 'a': 2, 'b': 2 });
await addDoc('coll/val3', { 'a': 3, 'b': 3 });
await addDoc('coll/val4', { 'a': 4, 'b': 4 });
let testingQuery = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', '>', 1)),
filter('a', '==', 2)
);
await verifyResults(testingQuery, 'coll/val2');
testingQuery = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', '<=', 1)),
filter('a', '==', 2)
);
await verifyResults(testingQuery);
testingQuery = queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', '>', 1)),
filter('a', '==', 2)
),
orderBy('a')
);
await verifyResults(testingQuery, 'coll/val2');
testingQuery = queryWithAddedOrderBy(
queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', '>', 1)),
filter('a', '==', 2)
),
orderBy('a')
),
orderBy('__name__', 'desc')
);
await verifyResults(testingQuery, 'coll/val2');
testingQuery = queryWithAddedOrderBy(
queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', '>', 1)),
filter('a', '==', 3)
),
orderBy('a')
),
orderBy('b', 'desc')
);
await verifyResults(testingQuery, 'coll/val3');
});
it('can index timestamp fields of different format', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['date', IndexKind.ASCENDING]] })
);
await addDocFromProto('coll/val1', {
'date': { timestampValue: '2016-01-02T10:20:50Z' }
});
await addDocFromProto('coll/val2', {
'date': { timestampValue: '2016-01-02T10:20:50.000000000Z' }
});
await addDocFromProto('coll/val3', {
'date': { timestampValue: '2016-01-02T10:20:50.850Z' }
});
await addDocFromProto('coll/val4', {
'date': { timestampValue: '2016-01-02T10:20:50.850000000Z' }
});
await addDocFromProto('coll/val5', {
'date': { timestampValue: { seconds: 1451730050, nanos: 999999999 } }
});
await addDocFromProto('coll/val6', {
'date': {
timestampValue: toTimestamp(serializer, new Timestamp(1451730050, 1))
}
});
let q = queryWithAddedOrderBy(query('coll'), orderBy('date'));
await verifyResults(
q,
'coll/val1',
'coll/val2',
'coll/val6',
'coll/val3',
'coll/val4',
'coll/val5'
);
q = queryWithAddedFilter(
query('coll'),
filter('date', '==', new Timestamp(1451730050, 850000000))
);
await verifyResults(q, 'coll/val3', 'coll/val4');
q = queryWithAddedFilter(
query('coll'),
filter('date', '>=', new Timestamp(1451730050, 850000000))
);
await verifyResults(q, 'coll/val3', 'coll/val4', 'coll/val5');
q = queryWithAddedFilter(
query('coll'),
filter('date', '>', new Timestamp(1451730050, 0))
);
await verifyResults(q, 'coll/val6', 'coll/val3', 'coll/val4', 'coll/val5');
});
it('can index VectorValue fields', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['embedding', IndexKind.ASCENDING]] })
);
await addDoc('coll/arr1', { 'embedding': [1, 2, 3] });
await addDoc('coll/map2', { 'embedding': {} });
await addDoc('coll/doc3', { 'embedding': vector([4, 5, 6]) });
await addDoc('coll/doc4', { 'embedding': vector([5]) });
let q = queryWithAddedOrderBy(query('coll'), orderBy('embedding'));
await verifyResults(q, 'coll/arr1', 'coll/doc4', 'coll/doc3', 'coll/map2');
q = queryWithAddedFilter(
query('coll'),
filter('embedding', '==', vector([4, 5, 6]))
);
await verifyResults(q, 'coll/doc3');
q = queryWithAddedFilter(
query('coll'),
filter('embedding', '>', vector([4, 5, 6]))
);
await verifyResults(q);
q = queryWithAddedFilter(
query('coll'),
filter('embedding', '>=', vector([4]))
);
await verifyResults(q, 'coll/doc4', 'coll/doc3');
});
it('support advances queries', async () => {
// This test compares local query results with those received from the Java
// Server SDK.
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['null', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['int', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['float', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['string', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['multi', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['array', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['array', IndexKind.DESCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['array', IndexKind.CONTAINS]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['map', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['map.field', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['prefix', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['prefix', IndexKind.ASCENDING],
['suffix', IndexKind.ASCENDING]
]
})
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['a', IndexKind.ASCENDING],
['b', IndexKind.ASCENDING]
]
})
);
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['a', IndexKind.DESCENDING],
['b', IndexKind.ASCENDING]
]
})
);
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['a', IndexKind.ASCENDING],
['b', IndexKind.DESCENDING]
]
})
);
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['a', IndexKind.DESCENDING],
['b', IndexKind.DESCENDING]
]
})
);
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['b', IndexKind.ASCENDING],
['a', IndexKind.ASCENDING]
]
})
);
const docs = [
{},
{ 'int': 1, 'array': [1, 'foo'] },
{ 'array': [2, 'foo'] },
{ 'int': 3, 'array': [3, 'foo'] },
{ 'array': 'foo' },
{ 'array': [1] },
{ 'float': -0.0, 'string': 'a' },
{ 'float': 0, 'string': 'ab' },
{ 'float': 0.0, 'string': 'b' },
{ 'float': NaN },
{ 'multi': true },
{ 'multi': 1 },
{ 'multi': 'string' },
{ 'multi': [] },
{ 'null': null },
{ 'prefix': [1, 2], 'suffix': null },
{ 'prefix': [1], 'suffix': 2 },
{ 'map': {} },
{ 'map': { 'field': true } },
{ 'map': { 'field': false } },
{ 'a': 0, 'b': 0 },
{ 'a': 0, 'b': 1 },
{ 'a': 1, 'b': 0 },
{ 'a': 1, 'b': 1 },
{ 'a': 2, 'b': 0 },
{ 'a': 2, 'b': 1 }
];
for (const doc of docs) {
await addDoc('coll/' + canonicalId(wrap(doc)), doc);
}
const q = query('coll');
await verifyResults(
queryWithAddedOrderBy(q, orderBy('int')),
'coll/{array:[1,foo],int:1}',
'coll/{array:[3,foo],int:3}'
);
await verifyResults(
queryWithAddedFilter(q, filter('float', '==', NaN)),
'coll/{float:NaN}'
);
await verifyResults(
queryWithAddedFilter(q, filter('float', '==', -0.0)),
'coll/{float:-0,string:a}',
'coll/{float:0,string:ab}',
'coll/{float:0,string:b}'
);
await verifyResults(
queryWithAddedFilter(q, filter('float', '==', 0)),
'coll/{float:-0,string:a}',
'coll/{float:0,string:ab}',
'coll/{float:0,string:b}'
);
await verifyResults(
queryWithAddedFilter(q, filter('float', '==', 0.0)),
'coll/{float:-0,string:a}',
'coll/{float:0,string:ab}',
'coll/{float:0,string:b}'
);
await verifyResults(
queryWithAddedFilter(q, filter('string', '==', 'a')),
'coll/{float:-0,string:a}'
);
await verifyResults(
queryWithAddedFilter(q, filter('string', '>', 'a')),
'coll/{float:0,string:ab}',
'coll/{float:0,string:b}'
);
await verifyResults(
queryWithAddedFilter(q, filter('string', '>=', 'a')),
'coll/{float:-0,string:a}',
'coll/{float:0,string:ab}',
'coll/{float:0,string:b}'
);
await verifyResults(
queryWithAddedFilter(q, filter('string', '<', 'b')),
'coll/{float:-0,string:a}',
'coll/{float:0,string:ab}'
);
await verifyResults(
queryWithAddedFilter(q, filter('string', '<', 'coll')),
'coll/{float:-0,string:a}',
'coll/{float:0,string:ab}',
'coll/{float:0,string:b}'
);
await verifyResults(
queryWithAddedFilter(
queryWithAddedFilter(q, filter('string', '>', 'a')),
filter('string', '<', 'b')
),
'coll/{float:0,string:ab}'
);
await verifyResults(
queryWithAddedFilter(q, filter('array', 'array-contains', 'foo')),
'coll/{array:[1,foo],int:1}',
'coll/{array:[2,foo]}',
'coll/{array:[3,foo],int:3}'
);
await verifyResults(
queryWithAddedFilter(
q,
filter('array', 'array-contains-any', [1, 'foo'])
),
'coll/{array:[1,foo],int:1}',
'coll/{array:[1]}',
'coll/{array:[2,foo]}',
'coll/{array:[3,foo],int:3}'
);
await verifyResults(
queryWithAddedFilter(q, filter('multi', '>=', true)),
'coll/{multi:true}'
);
await verifyResults(
queryWithAddedFilter(q, filter('multi', '>=', 0)),
'coll/{multi:1}'
);
await verifyResults(
queryWithAddedFilter(q, filter('multi', '>=', '')),
'coll/{multi:string}'
);
await verifyResults(
queryWithAddedFilter(q, filter('multi', '>=', [])),
'coll/{multi:[]}'
);
await verifyResults(
queryWithAddedFilter(q, filter('multi', '!=', true)),
'coll/{multi:1}',
'coll/{multi:string}',
'coll/{multi:[]}'
);
await verifyResults(
queryWithAddedFilter(q, filter('multi', 'in', [true, 1])),
'coll/{multi:true}',
'coll/{multi:1}'
);
await verifyResults(
queryWithAddedFilter(q, filter('multi', 'not-in', [true, 1])),
'coll/{multi:string}',
'coll/{multi:[]}'
);
await verifyResults(
queryWithStartAt(
queryWithAddedOrderBy(q, orderBy('array')),
bound([[2]], true)
),
'coll/{array:[2,foo]}',
'coll/{array:[3,foo],int:3}'
);
await verifyResults(
queryWithStartAt(
queryWithAddedOrderBy(q, orderBy('array', 'desc')),
bound([[2]], true)
),
'coll/{array:[1,foo],int:1}',
'coll/{array:[1]}',
'coll/{array:foo}'
);
await verifyResults(
queryWithLimit(
queryWithStartAt(
queryWithAddedOrderBy(q, orderBy('array', 'desc')),
bound([[2]], true)
),
2,
LimitType.First
),
'coll/{array:[1,foo],int:1}',
'coll/{array:[1]}'
);
await verifyResults(
queryWithStartAt(
queryWithAddedOrderBy(q, orderBy('array')),
bound([[2]], false)
),
'coll/{array:[2,foo]}',
'coll/{array:[3,foo],int:3}'
);
await verifyResults(
queryWithStartAt(
queryWithAddedOrderBy(q, orderBy('array', 'desc')),
bound([[2]], false)
),
'coll/{array:[1,foo],int:1}',
'coll/{array:[1]}',
'coll/{array:foo}'
);
await verifyResults(
queryWithLimit(
queryWithStartAt(
queryWithAddedOrderBy(q, orderBy('array', 'desc')),
bound([[2]], true)
),
2,
LimitType.First
),
'coll/{array:[1,foo],int:1}',
'coll/{array:[1]}'
);
await verifyResults(
queryWithStartAt(
queryWithAddedOrderBy(q, orderBy('array')),
bound([[2, 'foo']], false)
),
'coll/{array:[3,foo],int:3}'
);
await verifyResults(
queryWithStartAt(
queryWithAddedOrderBy(q, orderBy('array', 'desc')),
bound([[2, 'foo']], false)
),
'coll/{array:[1,foo],int:1}',
'coll/{array:[1]}',
'coll/{array:foo}'
);
await verifyResults(
queryWithLimit(
queryWithStartAt(
queryWithAddedOrderBy(q, orderBy('array', 'desc')),
bound([[2, 'foo']], false)
),
2,
LimitType.First
),
'coll/{array:[1,foo],int:1}',
'coll/{array:[1]}'
);
await verifyResults(
queryWithEndAt(
queryWithAddedOrderBy(q, orderBy('array')),
bound([[2]], true)
),
'coll/{array:foo}',
'coll/{array:[1]}',
'coll/{array:[1,foo],int:1}'
);
await verifyResults(
queryWithEndAt(
queryWithAddedOrderBy(q, orderBy('array', 'desc')),
bound([[2]], true)
),
'coll/{array:[3,foo],int:3}',
'coll/{array:[2,foo]}'
);
await verifyResults(
queryWithEndAt(
queryWithAddedOrderBy(q, orderBy('array')),
bound([[2]], false)
),
'coll/{array:foo}',
'coll/{array:[1]}',
'coll/{array:[1,foo],int:1}'
);
await verifyResults(
queryWithLimit(
queryWithEndAt(
queryWithAddedOrderBy(q, orderBy('array')),
bound([[2]], false)
),
2,
LimitType.First
),
'coll/{array:foo}',
'coll/{array:[1]}'
);
await verifyResults(
queryWithEndAt(
queryWithAddedOrderBy(q, orderBy('array', 'desc')),
bound([[2]], false)
),
'coll/{array:[3,foo],int:3}',
'coll/{array:[2,foo]}'
);
await verifyResults(
queryWithEndAt(
queryWithAddedOrderBy(q, orderBy('array')),
bound([[2, 'foo']], false)
),
'coll/{array:foo}',
'coll/{array:[1]}',
'coll/{array:[1,foo],int:1}'
);
await verifyResults(
queryWithLimit(
queryWithEndAt(
queryWithAddedOrderBy(q, orderBy('array')),
bound([[2, 'foo']], false)
),
2,
LimitType.First
),
'coll/{array:foo}',
'coll/{array:[1]}'
);
await verifyResults(
queryWithEndAt(
queryWithAddedOrderBy(q, orderBy('array', 'desc')),
bound([[2, 'foo']], false)
),
'coll/{array:[3,foo],int:3}'
);
await verifyResults(
queryWithLimit(
queryWithAddedOrderBy(
queryWithAddedOrderBy(q, orderBy('a')),
orderBy('b')
),
1,
LimitType.First
),
'coll/{a:0,b:0}'
);
await verifyResults(
queryWithLimit(
queryWithAddedOrderBy(
queryWithAddedOrderBy(q, orderBy('a', 'desc')),
orderBy('b')
),
1,
LimitType.First
),
'coll/{a:2,b:0}'
);
await verifyResults(
queryWithLimit(
queryWithAddedOrderBy(
queryWithAddedOrderBy(q, orderBy('a')),
orderBy('b', 'desc')
),
1,
LimitType.First
),
'coll/{a:0,b:1}'
);
await verifyResults(
queryWithLimit(
queryWithAddedOrderBy(
queryWithAddedOrderBy(q, orderBy('a', 'desc')),
orderBy('b', 'desc')
),
1,
LimitType.First
),
'coll/{a:2,b:1}'
);
await verifyResults(
queryWithAddedFilter(
queryWithAddedFilter(q, filter('a', '>', 0)),
filter('b', '==', 1)
),
'coll/{a:1,b:1}',
'coll/{a:2,b:1}'
);
await verifyResults(
queryWithAddedFilter(
queryWithAddedFilter(q, filter('a', '==', 1)),
filter('b', '==', 1)
),
'coll/{a:1,b:1}'
);
await verifyResults(
queryWithAddedFilter(
queryWithAddedFilter(q, filter('a', '!=', 0)),
filter('b', '==', 1)
),
'coll/{a:1,b:1}',
'coll/{a:2,b:1}'
);
await verifyResults(
queryWithAddedFilter(
queryWithAddedFilter(q, filter('b', '==', 1)),
filter('a', '!=', 0)
),
'coll/{a:1,b:1}',
'coll/{a:2,b:1}'
);
await verifyResults(
queryWithAddedFilter(q, filter('a', 'not-in', [0, 1])),
'coll/{a:2,b:0}',
'coll/{a:2,b:1}'
);
await verifyResults(
queryWithAddedFilter(
queryWithAddedFilter(q, filter('a', 'not-in', [0, 1])),
filter('b', '==', 1)
),
'coll/{a:2,b:1}'
);
await verifyResults(
queryWithAddedFilter(
queryWithAddedFilter(q, filter('b', '==', 1)),
filter('a', 'not-in', [0, 1])
),
'coll/{a:2,b:1}'
);
await verifyResults(
queryWithAddedFilter(q, filter('null', '==', null)),
'coll/{null:null}'
);
await verifyResults(
queryWithAddedOrderBy(q, orderBy('null')),
'coll/{null:null}'
);
await verifyResults(
queryWithAddedFilter(q, filter('prefix', '==', [1, 2])),
'coll/{prefix:[1,2],suffix:null}'
);
await verifyResults(
queryWithAddedFilter(
queryWithAddedFilter(q, filter('prefix', '==', [1])),
filter('suffix', '==', 2)
),
'coll/{prefix:[1],suffix:2}'
);
await verifyResults(
queryWithAddedFilter(q, filter('map', '==', {})),
'coll/{map:{}}'
);
await verifyResults(
queryWithAddedFilter(q, filter('map', '==', { 'field': true })),
'coll/{map:{field:true}}'
);
await verifyResults(
queryWithAddedFilter(q, filter('map.field', '==', true)),
'coll/{map:{field:true}}'
);
await verifyResults(
queryWithAddedOrderBy(q, orderBy('map')),
'coll/{map:{}}',
'coll/{map:{field:false}}',
'coll/{map:{field:true}}'
);
await verifyResults(
queryWithAddedOrderBy(q, orderBy('map.field')),
'coll/{map:{field:false}}',
'coll/{map:{field:true}}'
);
});
it('serves partial and full index', async () => {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['b', IndexKind.ASCENDING]] })
);
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['c', IndexKind.ASCENDING],
['d', IndexKind.ASCENDING]
]
})
);
const query1 = queryWithAddedFilter(query('coll'), filter('a', '==', 1));
await validateIsFullIndex(query1);
const query2 = queryWithAddedFilter(query('coll'), filter('b', '==', 1));
await validateIsFullIndex(query2);
const query3 = queryWithAddedOrderBy(
queryWithAddedFilter(query('coll'), filter('a', '==', 1)),
orderBy('a')
);
await validateIsFullIndex(query3);
const query4 = queryWithAddedOrderBy(
queryWithAddedFilter(query('coll'), filter('b', '==', 1)),
orderBy('b')
);
await validateIsFullIndex(query4);
const query5 = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', '==', 1)),
filter('b', '==', 1)
);
await validateIsPartialIndex(query5);
const query6 = queryWithAddedOrderBy(
queryWithAddedFilter(query('coll'), filter('a', '==', 1)),
orderBy('b')
);
await validateIsPartialIndex(query6);
const query7 = queryWithAddedOrderBy(
queryWithAddedFilter(query('coll'), filter('b', '==', 1)),
orderBy('a')
);
await validateIsPartialIndex(query7);
const query8 = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
filter('d', '==', 1)
);
await validateIsFullIndex(query8);
const query9 = queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
filter('d', '==', 1)
),
orderBy('c')
);
await validateIsFullIndex(query9);
const query10 = queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
filter('d', '==', 1)
),
orderBy('d')
);
await validateIsFullIndex(query10);
const query11 = queryWithAddedOrderBy(
queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
filter('d', '==', 1)
),
orderBy('c')
),
orderBy('d')
);
await validateIsFullIndex(query11);
const query12 = queryWithAddedOrderBy(
queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
filter('d', '==', 1)
),
orderBy('d')
),
orderBy('c')
);
await validateIsFullIndex(query12);
const query13 = queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
filter('d', '==', 1)
),
orderBy('e')
);
await validateIsPartialIndex(query13);
const query14 = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
filter('d', '<=', 1)
);
await validateIsFullIndex(query14);
const query15 = queryWithAddedOrderBy(
queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
filter('d', '>', 1)
),
orderBy('d')
);
await validateIsFullIndex(query15);
});
it('createTargetIndexes() creates full indexes for each sub-target', async () => {
const query_ = queryWithAddedFilter(
query('coll'),
orFilter(filter('a', '==', 1), filter('b', '==', 2), filter('c', '==', 3))
);
const subQuery1 = queryWithAddedFilter(query('coll'), filter('a', '==', 1));
const subQuery2 = queryWithAddedFilter(query('coll'), filter('b', '==', 2));
const subQuery3 = queryWithAddedFilter(query('coll'), filter('c', '==', 3));
await validateIsNoneIndex(query_);
await validateIsNoneIndex(subQuery1);
await validateIsNoneIndex(subQuery2);
await validateIsNoneIndex(subQuery3);
await indexManager.createTargetIndexes(queryToTarget(query_));
await validateIsFullIndex(query_);
await validateIsFullIndex(subQuery1);
await validateIsFullIndex(subQuery2);
await validateIsFullIndex(subQuery3);
});
it('createTargetIndexes() upgrades a partial index to a full index', async () => {
const query_ = queryWithAddedFilter(
queryWithAddedFilter(query('coll'), filter('a', '==', 1)),
filter('b', '==', 2)
);
const subQuery1 = queryWithAddedFilter(query('coll'), filter('a', '==', 1));
const subQuery2 = queryWithAddedFilter(query('coll'), filter('b', '==', 2));
await indexManager.createTargetIndexes(queryToTarget(subQuery1));
await validateIsPartialIndex(query_);
await validateIsFullIndex(subQuery1);
await validateIsNoneIndex(subQuery2);
await indexManager.createTargetIndexes(queryToTarget(query_));
await validateIsFullIndex(query_);
await validateIsFullIndex(subQuery1);
await validateIsNoneIndex(subQuery2);
});
it('createTargetIndexes() does nothing if a full index already exists', async () => {
const query_ = query('coll');
await indexManager.createTargetIndexes(queryToTarget(query_));
await validateIsFullIndex(query_);
await indexManager.createTargetIndexes(queryToTarget(query_));
await validateIsFullIndex(query_);
});
it('deleteAllFieldIndexes() deletes all indexes', async () => {
// Create some indexes.
const query1 = queryWithAddedFilter(query('coll'), filter('a', '==', 42));
await indexManager.createTargetIndexes(queryToTarget(query1));
await validateIsFullIndex(query1);
const query2 = queryWithAddedFilter(query('coll'), filter('b', '==', 42));
await indexManager.createTargetIndexes(queryToTarget(query2));
await validateIsFullIndex(query2);
// Verify that deleteAllFieldIndexes() deletes the indexes.
await indexManager.deleteAllFieldIndexes();
await validateIsNoneIndex(query1);
await validateIsNoneIndex(query2);
});
async function validateIsPartialIndex(query: Query): Promise<void> {
await validateIndexType(query, IndexType.PARTIAL);
}
async function validateIsFullIndex(query: Query): Promise<void> {
await validateIndexType(query, IndexType.FULL);
}
async function validateIsNoneIndex(query: Query): Promise<void> {
await validateIndexType(query, IndexType.NONE);
}
async function validateIndexType(
query: Query,
expectedIndexType: IndexType
): Promise<void> {
const indexType = await indexManager.getIndexType(queryToTarget(query));
expect(
indexType,
'index type is ' +
displayNameForIndexType(indexType) +
' but expected ' +
displayNameForIndexType(expectedIndexType)
).to.equal(expectedIndexType);
}
async function setUpSingleValueFilter(): Promise<void> {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['count', IndexKind.ASCENDING]] })
);
await addDoc('coll/val1', { 'count': 1 });
await addDoc('coll/val2', { 'count': 2 });
await addDoc('coll/val3', { 'count': 3 });
}
async function setUpArrayValueFilter(): Promise<void> {
await indexManager.addFieldIndex(
fieldIndex('coll', { fields: [['values', IndexKind.CONTAINS]] })
);
await addDoc('coll/arr1', { 'values': [1, 2, 3] });
await addDoc('coll/arr2', { 'values': [4, 5, 6] });
await addDoc('coll/arr3', { 'values': [7, 8, 9] });
}
async function setUpMultipleOrderBys(): Promise<void> {
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['a', IndexKind.ASCENDING],
['b', IndexKind.DESCENDING],
['c', IndexKind.ASCENDING]
]
})
);
await indexManager.addFieldIndex(
fieldIndex('coll', {
fields: [
['a', IndexKind.DESCENDING],
['b', IndexKind.ASCENDING],
['c', IndexKind.DESCENDING]
]
})
);
await addDoc('coll/val1', { 'a': 1, 'b': 1, 'c': 3 });
await addDoc('coll/val2', { 'a': 2, 'b': 2, 'c': 2 });
await addDoc('coll/val3', { 'a': 2, 'b': 2, 'c': 3 });
await addDoc('coll/val4', { 'a': 2, 'b': 2, 'c': 4 });
await addDoc('coll/val5', { 'a': 2, 'b': 2, 'c': 5 });
await addDoc('coll/val6', { 'a': 3, 'b': 3, 'c': 6 });
}
function addDocs(...docs: Document[]): Promise<void> {
return indexManager.updateIndexEntries(documentMap(...docs));
}
function addDoc(key: string, data: JsonObject<unknown>): Promise<void> {
return addDocs(doc(key, 1, data));
}
function addDocFromProto(
key: string,
data: ProtoObjectMap<ProtoValue> | undefined
): Promise<void> {
return addDocs(docFromProto(key, 1, data));
}
function docFromProto(
keyStr: string,
versionNumber: number,
data: ProtoObjectMap<ProtoValue> | undefined
): MutableDocument {
const proto: ProtoDocument = {
name: toName(serializer, key(keyStr)),
fields: data,
updateTime: toVersion(serializer, version(versionNumber)),
createTime: toVersion(serializer, version(versionNumber))
};
return fromDocument(
serializer,
proto,
/* hasCommittedMutations= */ undefined
);
}
async function verifyResults(query: Query, ...keys: string[]): Promise<void> {
const target = queryToTarget(query);
const actualResults = await indexManager.getDocumentsMatchingTarget(target);
expect(actualResults).to.not.equal(null, 'Expected successful query');
const actualKeys: string[] = [];
actualResults!.forEach(v => actualKeys.push(v.path.toString()));
expect(actualKeys).to.have.ordered.members(keys);
}
});
/**
* Defines the set of tests to run against both IndexManager implementations.
*/
function genericIndexManagerTests(
persistencePromise: () => Promise<Persistence>
): void {
addEqualityMatcher();
let indexManager: TestIndexManager;
let persistence: Persistence;
beforeEach(async () => {
persistence = await persistencePromise();
indexManager = new TestIndexManager(
persistence,
persistence.getIndexManager(User.UNAUTHENTICATED)
);
});
afterEach(async () => {
if (persistence.started) {
await persistence.shutdown();
await persistenceHelpers.clearTestPersistence();
}
});
it('can add and read collection=>parent index entries', async () => {
await indexManager.addToCollectionParentIndex(path('messages'));
await indexManager.addToCollectionParentIndex(path('messages'));
await indexManager.addToCollectionParentIndex(path('rooms/foo/messages'));
await indexManager.addToCollectionParentIndex(path('rooms/bar/messages'));
await indexManager.addToCollectionParentIndex(path('rooms/foo/messages2'));
expect(await indexManager.getCollectionParents('messages')).to.deep.equal([
path(''),
path('rooms/bar'),
path('rooms/foo')
]);
expect(await indexManager.getCollectionParents('messages2')).to.deep.equal([
path('rooms/foo')
]);
expect(await indexManager.getCollectionParents('messages3')).to.deep.equal(
[]
);
});
}