mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
Implementation of Firestore OR Queries without the public API. The public API will be released later. Several minor API changes are included in this release: Functions in the Firestore package that return QueryConstraints (for example: where(...), limit(...), and orderBy(...)) now return a more specific type, which extends QueryConstraint. This is a non-breaking change that will support OR Queries.
462 lines
14 KiB
TypeScript
462 lines
14 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2022 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 { Bound } from '../../../src/core/bound';
|
|
import {
|
|
queryToTarget,
|
|
queryWithEndAt,
|
|
queryWithStartAt
|
|
} from '../../../src/core/query';
|
|
import {
|
|
targetGetUpperBound,
|
|
targetGetLowerBound,
|
|
targetGetArrayValues
|
|
} from '../../../src/core/target';
|
|
import { IndexKind } from '../../../src/model/field_index';
|
|
import {
|
|
canonicalId,
|
|
MAX_VALUE,
|
|
MIN_VALUE,
|
|
valueEquals
|
|
} from '../../../src/model/values';
|
|
import {
|
|
blob,
|
|
bound,
|
|
fieldIndex,
|
|
filter,
|
|
orderBy,
|
|
query,
|
|
wrap
|
|
} from '../../util/helpers';
|
|
|
|
describe('Target Bounds', () => {
|
|
// TODO(indexing): Consider adding more test helpers to reduce the boilerplate
|
|
// in these tests.
|
|
|
|
it('empty query', () => {
|
|
const target = queryToTarget(query('c'));
|
|
const index = fieldIndex('c');
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true);
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true);
|
|
});
|
|
|
|
it('equals query with ascending index', () => {
|
|
const target = queryToTarget(query('c', filter('foo', '==', 'bar')));
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.ASCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, 'bar');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true, 'bar');
|
|
});
|
|
|
|
it('equals query with descending index', () => {
|
|
const target = queryToTarget(query('c', filter('foo', '==', 'bar')));
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.DESCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, 'bar');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true, 'bar');
|
|
});
|
|
|
|
it('less than query with ascending index', () => {
|
|
const target = queryToTarget(query('c', filter('foo', '<', 'bar')));
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.ASCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, '');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, false, 'bar');
|
|
});
|
|
|
|
it('less than query with descending index', () => {
|
|
const target = queryToTarget(query('c', filter('foo', '<', 'bar')));
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.DESCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, false, 'bar');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true, '');
|
|
});
|
|
|
|
it('less than or equals query with ascending index', () => {
|
|
const target = queryToTarget(query('c', filter('foo', '<=', 'bar')));
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.ASCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, '');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true, 'bar');
|
|
});
|
|
|
|
it('less than or equals query with descending index', () => {
|
|
const target = queryToTarget(query('c', filter('foo', '<=', 'bar')));
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.DESCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, 'bar');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true, '');
|
|
});
|
|
|
|
it('greater than query with ascending index', () => {
|
|
const target = queryToTarget(query('c', filter('foo', '>', 'bar')));
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.ASCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, false, 'bar');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, false, blob());
|
|
});
|
|
|
|
it('greater than query with descending index', () => {
|
|
const target = queryToTarget(query('c', filter('foo', '>', 'bar')));
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.DESCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, false, blob());
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, false, 'bar');
|
|
});
|
|
|
|
it('greater than or equals query with ascending index', () => {
|
|
const target = queryToTarget(query('c', filter('foo', '>=', 'bar')));
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.ASCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, 'bar');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, false, blob());
|
|
});
|
|
|
|
it('greater than or equals query with descending index', () => {
|
|
const target = queryToTarget(query('c', filter('foo', '>=', 'bar')));
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.DESCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, false, blob());
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true, 'bar');
|
|
});
|
|
|
|
it('arrayContains query', () => {
|
|
const target = queryToTarget(
|
|
query('c', filter('foo', 'array-contains', 'bar'))
|
|
);
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.CONTAINS]] });
|
|
|
|
const arrayValues = targetGetArrayValues(target, index);
|
|
expect(arrayValues).to.deep.equal([wrap('bar')]);
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true);
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true);
|
|
});
|
|
|
|
it('arrayContainsAny query', () => {
|
|
const target = queryToTarget(
|
|
query('c', filter('foo', 'array-contains-any', ['bar', 'baz']))
|
|
);
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.CONTAINS]] });
|
|
|
|
const arrayValues = targetGetArrayValues(target, index);
|
|
expect(arrayValues).to.deep.equal(wrap(['bar', 'baz']).arrayValue!.values);
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true);
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true);
|
|
});
|
|
|
|
it('orderBy query', () => {
|
|
const target = queryToTarget(query('c', orderBy('foo')));
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.ASCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
expect(lowerBound?.position[0]).to.equal(MIN_VALUE);
|
|
expect(lowerBound?.inclusive).to.be.true;
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
expect(upperBound?.position[0]).to.equal(MAX_VALUE);
|
|
expect(upperBound?.inclusive).to.be.true;
|
|
});
|
|
|
|
it('orderBy query with filter', () => {
|
|
const target = queryToTarget(
|
|
query('c', filter('foo', '>', 'bar'), orderBy('foo'))
|
|
);
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.ASCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, false, 'bar');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, false, blob());
|
|
});
|
|
|
|
it('startAt query', () => {
|
|
const target = queryToTarget(
|
|
queryWithStartAt(
|
|
query('c', orderBy('foo')),
|
|
bound(['bar'], /* inclusive= */ true)
|
|
)
|
|
);
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.ASCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, 'bar');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
expect(upperBound?.position[0]).to.equal(MAX_VALUE);
|
|
expect(upperBound?.inclusive).to.be.true;
|
|
});
|
|
|
|
it('startAt query with filter', () => {
|
|
// Tests that the startAt and the filter get merged to form a narrow bound
|
|
const target = queryToTarget(
|
|
queryWithStartAt(
|
|
query(
|
|
'c',
|
|
filter('a', '>=', 'a1'),
|
|
filter('b', '==', 'b1'),
|
|
orderBy('a'),
|
|
orderBy('b')
|
|
),
|
|
bound(['a1', 'b1'], /* inclusive= */ true)
|
|
)
|
|
);
|
|
const index = fieldIndex('c', {
|
|
fields: [
|
|
['a', IndexKind.ASCENDING],
|
|
['b', IndexKind.ASCENDING]
|
|
]
|
|
});
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, 'a1', 'b1');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, false, blob(), 'b1');
|
|
});
|
|
|
|
it('startAfter query with filter', () => {
|
|
const target = queryToTarget(
|
|
queryWithStartAt(
|
|
query(
|
|
'c',
|
|
filter('a', '>=', 'a1'),
|
|
filter('b', '==', 'b1'),
|
|
orderBy('a'),
|
|
orderBy('b')
|
|
),
|
|
bound(['a2', 'b1'], /* inclusive= */ false)
|
|
)
|
|
);
|
|
const index = fieldIndex('c', {
|
|
fields: [
|
|
['a', IndexKind.ASCENDING],
|
|
['b', IndexKind.ASCENDING]
|
|
]
|
|
});
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, false, 'a2', 'b1');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, false, blob(), 'b1');
|
|
});
|
|
|
|
it('startAfter does not change bound if not applicable', () => {
|
|
const target = queryToTarget(
|
|
queryWithStartAt(
|
|
query(
|
|
'c',
|
|
filter('a', '>=', 'a2'),
|
|
filter('b', '==', 'b2'),
|
|
orderBy('a'),
|
|
orderBy('b')
|
|
),
|
|
bound(['a1', 'b1'], /* inclusive= */ false)
|
|
)
|
|
);
|
|
const index = fieldIndex('c', {
|
|
fields: [
|
|
['a', IndexKind.ASCENDING],
|
|
['b', IndexKind.ASCENDING]
|
|
]
|
|
});
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, 'a2', 'b2');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, false, blob(), 'b2');
|
|
});
|
|
|
|
it('endAt query', () => {
|
|
const target = queryToTarget(
|
|
queryWithEndAt(
|
|
query('c', orderBy('foo')),
|
|
bound(['bar'], /* inclusive= */ true)
|
|
)
|
|
);
|
|
const index = fieldIndex('c', { fields: [['foo', IndexKind.ASCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
expect(lowerBound?.position[0]).to.equal(MIN_VALUE);
|
|
expect(lowerBound?.inclusive).to.be.true;
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true, 'bar');
|
|
});
|
|
|
|
it('endAt query with filter', () => {
|
|
// Tests that the endAt and the filter get merged to form a narrow bound
|
|
const target = queryToTarget(
|
|
queryWithEndAt(
|
|
query(
|
|
'c',
|
|
filter('a', '<=', 'a2'),
|
|
filter('b', '==', 'b2'),
|
|
orderBy('a'),
|
|
orderBy('b')
|
|
),
|
|
bound(['a1', 'b1'], /* inclusive= */ true)
|
|
)
|
|
);
|
|
const index = fieldIndex('c', {
|
|
fields: [
|
|
['a', IndexKind.ASCENDING],
|
|
['b', IndexKind.ASCENDING]
|
|
]
|
|
});
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, '', 'b2');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true, 'a1', 'b1');
|
|
});
|
|
|
|
it('endBefore query with filter', () => {
|
|
const target = queryToTarget(
|
|
queryWithEndAt(
|
|
query(
|
|
'c',
|
|
filter('a', '<=', 'a2'),
|
|
filter('b', '==', 'b2'),
|
|
orderBy('a'),
|
|
orderBy('b')
|
|
),
|
|
bound(['a1', 'b1'], /* inclusive= */ false)
|
|
)
|
|
);
|
|
const index = fieldIndex('c', {
|
|
fields: [
|
|
['a', IndexKind.ASCENDING],
|
|
['b', IndexKind.ASCENDING]
|
|
]
|
|
});
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, '', 'b2');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, false, 'a1', 'b1');
|
|
});
|
|
|
|
it('endBefore does not change bound if not applicable', () => {
|
|
const target = queryToTarget(
|
|
queryWithEndAt(
|
|
query(
|
|
'c',
|
|
filter('a', '<=', 'a1'),
|
|
filter('b', '==', 'b1'),
|
|
orderBy('a'),
|
|
orderBy('b')
|
|
),
|
|
bound(['a2', 'b2'], /* inclusive= */ false)
|
|
)
|
|
);
|
|
const index = fieldIndex('c', {
|
|
fields: [
|
|
['a', IndexKind.ASCENDING],
|
|
['b', IndexKind.ASCENDING]
|
|
]
|
|
});
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, '', 'b1');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true, 'a1', 'b1');
|
|
});
|
|
|
|
it('partial index match with bound', () => {
|
|
const target = queryToTarget(
|
|
query('c', filter('a', '==', 'a'), filter('b', '==', 'b'))
|
|
);
|
|
const index = fieldIndex('c', { fields: [['a', IndexKind.ASCENDING]] });
|
|
|
|
const lowerBound = targetGetLowerBound(target, index);
|
|
verifyBound(lowerBound, true, 'a');
|
|
|
|
const upperBound = targetGetUpperBound(target, index);
|
|
verifyBound(upperBound, true, 'a');
|
|
});
|
|
|
|
function verifyBound(
|
|
bound: Bound | null,
|
|
inclusive: boolean,
|
|
...values: unknown[]
|
|
): void {
|
|
expect(bound).to.not.be.null;
|
|
expect(bound!.inclusive).to.equal(inclusive, 'inclusive');
|
|
const position = bound!.position;
|
|
expect(position.length).to.equal(values.length, 'size');
|
|
for (let i = 0; i < values.length; ++i) {
|
|
const expectedValue = wrap(values[i]);
|
|
expect(
|
|
valueEquals(position[i], expectedValue),
|
|
`Values should be equal: Expected: ${canonicalId(
|
|
expectedValue
|
|
)}, Actual: ${canonicalId(position[i])}`
|
|
);
|
|
}
|
|
}
|
|
});
|