Skip to content

Commit 98ffc84

Browse files
authored
FIX tests for pubkey#4571 (pubkey#4583)
* ADD logs * FIX query planner * FIX dexie * FIX query planner * FIX
1 parent b65c4dd commit 98ffc84

File tree

5 files changed

+200
-22
lines changed

5 files changed

+200
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# RxDB Changelog
33

44
<!-- CHANGELOG NEWEST -->
5+
- FIX multiple problems in the query planner
56
- ADD 'includeWsHeaders' property for GraphQL replication [#4533](https://github.com/pubkey/rxdb/pull/4533)
67
- UPDATE [broadcast-channel](https://github.com/pubkey/broadcast-channel) to version `5.0.0`
78
<!-- ADD new changes here! -->

src/plugins/storage-dexie/dexie-query.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { INDEX_MIN } from '../../query-planner';
12
import { getQueryMatcher, getSortComparator } from '../../rx-query-helper';
23
import type {
34
DefaultPreparedQuery,
@@ -13,6 +14,13 @@ import {
1314
} from './dexie-helper';
1415
import type { RxStorageInstanceDexie } from './rx-storage-instance-dexie';
1516

17+
export function mapKeyForKeyRange(k: any) {
18+
if (k === INDEX_MIN) {
19+
return -Infinity;
20+
} else {
21+
return k;
22+
}
23+
}
1624

1725
export function getKeyRangeByQueryPlan(
1826
queryPlan: RxQueryPlan,
@@ -26,23 +34,26 @@ export function getKeyRangeByQueryPlan(
2634
}
2735
}
2836

37+
const startKeys = queryPlan.startKeys.map(mapKeyForKeyRange);
38+
const endKeys = queryPlan.endKeys.map(mapKeyForKeyRange);
39+
2940
let ret: any;
3041
/**
3142
* If index has only one field,
3243
* we have to pass the keys directly, not the key arrays.
3344
*/
3445
if (queryPlan.index.length === 1) {
35-
const equalKeys = queryPlan.startKeys[0] === queryPlan.endKeys[0];
46+
const equalKeys = startKeys[0] === endKeys[0];
3647
ret = IDBKeyRange.bound(
37-
queryPlan.startKeys[0],
38-
queryPlan.endKeys[0],
48+
startKeys[0],
49+
endKeys[0],
3950
equalKeys ? false : queryPlan.inclusiveStart,
4051
equalKeys ? false : queryPlan.inclusiveEnd
4152
);
4253
} else {
4354
ret = IDBKeyRange.bound(
44-
queryPlan.startKeys,
45-
queryPlan.endKeys,
55+
startKeys,
56+
endKeys,
4657
queryPlan.inclusiveStart,
4758
queryPlan.inclusiveEnd
4859
);

src/plugins/utils/utils-array.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,21 @@ export function arrayFilterNotEmpty<TValue>(value: TValue | null | undefined): v
7676
}
7777
return true;
7878
}
79+
80+
export function countUntilNotMatching<T>(
81+
ar: T[],
82+
matchingFn: (v: T, idx: number) => boolean
83+
): number {
84+
let count = 0;
85+
let idx = -1;
86+
for (const item of ar) {
87+
idx = idx + 1;
88+
const matching = matchingFn(item, idx);
89+
if (matching) {
90+
count = count + 1;
91+
} else {
92+
break;
93+
}
94+
}
95+
return count;
96+
}

src/query-planner.ts

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { countUntilNotMatching } from './plugins/utils';
12
import { getPrimaryFieldOfPrimaryKey } from './rx-schema-helper';
23
import type {
34
FilledMangoQuery,
@@ -10,7 +11,18 @@ import type {
1011

1112

1213
export const INDEX_MAX = String.fromCharCode(65535);
13-
export const INDEX_MIN = -Infinity;
14+
15+
/**
16+
* Do not use -Infinity here because it would be
17+
* transformed to null on JSON.stringify() which can break things
18+
* when the query plan is send to the storage as json.
19+
* @link https://stackoverflow.com/a/16644751
20+
* Notice that for IndexedDB IDBKeyRange we have
21+
* to transform the value back to -Infinity
22+
* before we can use it in IDBKeyRange.bound.
23+
*
24+
*/
25+
export const INDEX_MIN = Number.MIN_VALUE;
1426

1527
/**
1628
* Returns the query plan which contains
@@ -52,13 +64,13 @@ export function getQueryPlan<RxDocType>(
5264
const operators = matcher ? Object.keys(matcher) : [];
5365

5466
let matcherOpts: RxQueryPlanerOpts = {} as any;
55-
5667
if (
5768
!matcher ||
5869
!operators.length
5970
) {
71+
const startKey = inclusiveStart ? INDEX_MIN : INDEX_MAX;
6072
matcherOpts = {
61-
startKey: inclusiveStart ? INDEX_MIN : INDEX_MAX,
73+
startKey,
6274
endKey: inclusiveEnd ? INDEX_MAX : INDEX_MIN,
6375
inclusiveStart: true,
6476
inclusiveEnd: true
@@ -87,7 +99,6 @@ export function getQueryPlan<RxDocType>(
8799
matcherOpts.inclusiveEnd = true;
88100
}
89101

90-
91102
if (inclusiveStart && !matcherOpts.inclusiveStart) {
92103
inclusiveStart = false;
93104
}
@@ -248,22 +259,42 @@ export function rateQueryPlan<RxDocType>(
248259
queryPlan: RxQueryPlan
249260
): number {
250261
let quality: number = 0;
262+
const addQuality = (value: number) => {
263+
if (value > 0) {
264+
quality = quality + value;
265+
}
266+
};
251267

252268
const pointsPerMatchingKey = 10;
253-
const idxOfFirstMinStartKey = queryPlan.startKeys.findIndex(keyValue => keyValue === INDEX_MIN);
254-
if (idxOfFirstMinStartKey > 0) {
255-
quality = quality + (idxOfFirstMinStartKey * pointsPerMatchingKey);
256-
}
257269

258-
const idxOfFirstMaxEndKey = queryPlan.endKeys.findIndex(keyValue => keyValue === INDEX_MAX);
259-
if (idxOfFirstMaxEndKey > 0) {
260-
quality = quality + (idxOfFirstMaxEndKey * pointsPerMatchingKey);
261-
}
270+
const nonMinKeyCount = countUntilNotMatching(queryPlan.startKeys, keyValue => keyValue !== INDEX_MIN && keyValue !== INDEX_MAX);
271+
addQuality(nonMinKeyCount * pointsPerMatchingKey);
262272

263-
const pointsIfNoReSortMustBeDone = 5;
264-
if (queryPlan.sortFieldsSameAsIndexFields) {
265-
quality = quality + pointsIfNoReSortMustBeDone;
266-
}
273+
const nonMaxKeyCount = countUntilNotMatching(queryPlan.startKeys, keyValue => keyValue !== INDEX_MAX && keyValue !== INDEX_MIN);
274+
addQuality(nonMaxKeyCount * pointsPerMatchingKey);
275+
276+
const equalKeyCount = countUntilNotMatching(queryPlan.startKeys, (keyValue, idx) => {
277+
if (keyValue === queryPlan.endKeys[idx]) {
278+
return true;
279+
} else {
280+
return false;
281+
}
282+
});
283+
addQuality(equalKeyCount * pointsPerMatchingKey * 1.5);
284+
285+
const pointsIfNoReSortMustBeDone = queryPlan.sortFieldsSameAsIndexFields ? 5 : 0;
286+
addQuality(pointsIfNoReSortMustBeDone);
287+
288+
// console.log('rateQueryPlan() result:');
289+
// console.log({
290+
// query,
291+
// queryPlan,
292+
// nonMinKeyCount,
293+
// nonMaxKeyCount,
294+
// equalKeyCount,
295+
// pointsIfNoReSortMustBeDone,
296+
// quality
297+
// });
267298

268299
return quality;
269300
}

test/unit/rx-storage-query-correctness.test.ts

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ config.parallel('rx-storage-query-correctness.test.ts', () => {
112112
if (!queryData) {
113113
continue;
114114
}
115+
115116
const normalizedQuery = deepFreeze(normalizeMangoQuery(schema, queryData.query));
116117
const skip = normalizedQuery.skip ? normalizedQuery.skip : 0;
117118
const limit = normalizedQuery.limit ? normalizedQuery.limit : Infinity;
@@ -270,7 +271,7 @@ config.parallel('rx-storage-query-correctness.test.ts', () => {
270271
},
271272
sort: [{ passportId: 'asc' }]
272273
},
273-
selectorSatisfiedByIndex: false,
274+
selectorSatisfiedByIndex: true,
274275
expectedResultDocIds: [
275276
'cc-looong-id',
276277
'dd',
@@ -833,6 +834,122 @@ config.parallel('rx-storage-query-correctness.test.ts', () => {
833834
}
834835
]
835836
});
837+
/**
838+
* @link https://github.com/pubkey/rxdb/issues/4571
839+
*/
840+
testCorrectQueries({
841+
testTitle: '$eq operator with composite primary key',
842+
data: [
843+
{
844+
id: 'one',
845+
key: 'one|1|1',
846+
string: 'one',
847+
number: 1,
848+
integer: 1,
849+
},
850+
{
851+
id: 'two',
852+
key: 'two|1|1',
853+
string: 'two',
854+
number: 1,
855+
integer: 1,
856+
},
857+
{
858+
id: 'three',
859+
key: 'one|2|1',
860+
string: 'one',
861+
number: 2,
862+
integer: 1,
863+
},
864+
],
865+
schema: {
866+
version: 0,
867+
indexes: ['string', ['number', 'integer']],
868+
primaryKey: {
869+
key: 'key',
870+
fields: ['string', 'number', 'integer'],
871+
separator: '|',
872+
},
873+
type: 'object',
874+
properties: {
875+
key: {
876+
maxLength: 100,
877+
type: 'string',
878+
},
879+
id: {
880+
maxLength: 100,
881+
type: 'string',
882+
},
883+
string: {
884+
maxLength: 50,
885+
type: 'string',
886+
},
887+
number: {
888+
type: 'number',
889+
minimum: 0,
890+
maximum: 100,
891+
multipleOf: 1,
892+
},
893+
integer: {
894+
type: 'integer',
895+
minimum: 0,
896+
maximum: 100,
897+
multipleOf: 1,
898+
},
899+
},
900+
required: ['id', 'key', 'string', 'number', 'integer'],
901+
},
902+
queries: [
903+
{
904+
info: '$eq primary key',
905+
query: {
906+
selector: {
907+
id: {
908+
$eq: 'one',
909+
},
910+
},
911+
sort: [{ id: 'asc' }],
912+
},
913+
expectedResultDocIds: ['one|1|1'],
914+
},
915+
{
916+
info: '$eq by key',
917+
query: {
918+
selector: {
919+
key: {
920+
$eq: 'one|1|1',
921+
},
922+
},
923+
sort: [{ id: 'asc' }],
924+
},
925+
expectedResultDocIds: ['one|1|1'],
926+
},
927+
{
928+
info: '$eq by composite key fields',
929+
query: {
930+
selector: {
931+
$and: [
932+
{
933+
string: {
934+
$eq: 'one',
935+
},
936+
},
937+
{
938+
number: {
939+
$eq: 1,
940+
},
941+
integer: {
942+
$eq: 1,
943+
},
944+
},
945+
],
946+
},
947+
sort: [{ number: 'desc', integer: 'desc' }],
948+
},
949+
expectedResultDocIds: ['one|1|1'],
950+
},
951+
],
952+
});
836953
testCorrectQueries({
837954
testTitle: '$type',
838955
data: [

0 commit comments

Comments
 (0)