kitcn 0.17.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aggregate/index.d.ts +1 -1
- package/dist/auth/generated/index.d.ts +1 -1
- package/dist/auth/index.d.ts +12 -12
- package/dist/auth/index.js +1 -1
- package/dist/{generated-contract-disabled-CHR4NLoa.d.ts → generated-contract-disabled-LOAfma7_.d.ts} +29 -29
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/index.js +595 -143
- package/dist/{query-context-C90vNlc9.js → query-context-BzihIpnM.js} +230 -103
- package/dist/{where-clause-compiler-DVO0oBe9.d.ts → where-clause-compiler-CmBz9CWA.d.ts} +48 -9
- package/package.json +1 -1
- package/skills/kitcn/references/features/orm.md +9 -2
|
@@ -23,6 +23,27 @@ function isFieldReference(value) {
|
|
|
23
23
|
return value && typeof value === "object" && value.__brand === "FieldReference";
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
+
* The pattern is the same for every row of a scan while the value is not, so
|
|
27
|
+
* only the value genuinely has to be split per call. A small bounded cache
|
|
28
|
+
* keeps the split pattern across rows without unbounded growth on
|
|
29
|
+
* caller-supplied patterns; a query with a handful of LIKE filters still hits
|
|
30
|
+
* on every row.
|
|
31
|
+
*/
|
|
32
|
+
const LIKE_PATTERN_CACHE_MAX = 16;
|
|
33
|
+
const likePatternCache = /* @__PURE__ */ new Map();
|
|
34
|
+
function likePatternCodePoints(pattern, caseInsensitive) {
|
|
35
|
+
const key = caseInsensitive ? `i:${pattern}` : `s:${pattern}`;
|
|
36
|
+
const cached = likePatternCache.get(key);
|
|
37
|
+
if (cached) return cached;
|
|
38
|
+
const source = Array.from(caseInsensitive ? pattern.toLowerCase() : pattern);
|
|
39
|
+
if (likePatternCache.size >= LIKE_PATTERN_CACHE_MAX) for (const oldest of likePatternCache.keys()) {
|
|
40
|
+
likePatternCache.delete(oldest);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
likePatternCache.set(key, source);
|
|
44
|
+
return source;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
26
47
|
* SQL `LIKE` semantics: `%` matches any run of characters, `_` matches exactly
|
|
27
48
|
* one, everything else is literal. Wildcards work anywhere in the pattern, not
|
|
28
49
|
* only at the ends.
|
|
@@ -36,7 +57,7 @@ function isFieldReference(value) {
|
|
|
36
57
|
*/
|
|
37
58
|
function matchLikePattern(value, pattern, caseInsensitive) {
|
|
38
59
|
const target = Array.from(caseInsensitive ? value.toLowerCase() : value);
|
|
39
|
-
const source =
|
|
60
|
+
const source = likePatternCodePoints(pattern, caseInsensitive);
|
|
40
61
|
let valueIndex = 0;
|
|
41
62
|
let patternIndex = 0;
|
|
42
63
|
let wildcardPatternIndex = -1;
|
|
@@ -507,11 +528,19 @@ function getIndexFields(table, index, schema) {
|
|
|
507
528
|
fields.push("_id");
|
|
508
529
|
return fields;
|
|
509
530
|
}
|
|
510
|
-
|
|
531
|
+
/**
|
|
532
|
+
* Read the index key out of a document.
|
|
533
|
+
*
|
|
534
|
+
* Takes field paths already split on `.` rather than the field names: the field
|
|
535
|
+
* list is fixed for the life of a query, so splitting it once at `withIndex`
|
|
536
|
+
* time saves an array allocation per field per document pulled through the
|
|
537
|
+
* stream.
|
|
538
|
+
*/
|
|
539
|
+
function getIndexKey(doc, indexFieldPaths) {
|
|
511
540
|
const key = [];
|
|
512
|
-
for (const
|
|
541
|
+
for (const path of indexFieldPaths) {
|
|
513
542
|
let obj = doc;
|
|
514
|
-
for (const subfield of
|
|
543
|
+
for (const subfield of path) obj = obj[subfield];
|
|
515
544
|
key.push(obj);
|
|
516
545
|
}
|
|
517
546
|
return key;
|
|
@@ -708,17 +737,26 @@ var QueryStream = class {
|
|
|
708
737
|
}
|
|
709
738
|
[Symbol.asyncIterator]() {
|
|
710
739
|
const iterator = this.iterWithKeys()[Symbol.asyncIterator]();
|
|
711
|
-
return {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
done
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
740
|
+
return {
|
|
741
|
+
async next() {
|
|
742
|
+
const result = await iterator.next();
|
|
743
|
+
if (result.done) return {
|
|
744
|
+
done: true,
|
|
745
|
+
value: void 0
|
|
746
|
+
};
|
|
747
|
+
return {
|
|
748
|
+
done: false,
|
|
749
|
+
value: result.value[0]
|
|
750
|
+
};
|
|
751
|
+
},
|
|
752
|
+
async return() {
|
|
753
|
+
await iterator.return?.();
|
|
754
|
+
return {
|
|
755
|
+
done: true,
|
|
756
|
+
value: void 0
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
};
|
|
722
760
|
}
|
|
723
761
|
};
|
|
724
762
|
var StreamDatabaseReader = class {
|
|
@@ -837,6 +875,7 @@ var OrderedStreamQuery = class extends StreamableQuery {
|
|
|
837
875
|
table: this.parent.parent.table,
|
|
838
876
|
index: this.parent.index,
|
|
839
877
|
indexFields: this.parent.q.indexFields,
|
|
878
|
+
indexFieldPaths: this.parent.q.indexFieldPaths,
|
|
840
879
|
order: this.order,
|
|
841
880
|
bounds: {
|
|
842
881
|
lowerBound: this.parent.q.lowerBoundIndexKey ?? [],
|
|
@@ -855,7 +894,7 @@ var OrderedStreamQuery = class extends StreamableQuery {
|
|
|
855
894
|
return db.query(table).withIndex(index, indexRange).order(order);
|
|
856
895
|
}
|
|
857
896
|
iterWithKeys() {
|
|
858
|
-
const {
|
|
897
|
+
const { indexFieldPaths } = this.reflect();
|
|
859
898
|
const iterable = this.inner();
|
|
860
899
|
return { [Symbol.asyncIterator]() {
|
|
861
900
|
const iterator = iterable[Symbol.asyncIterator]();
|
|
@@ -867,7 +906,7 @@ var OrderedStreamQuery = class extends StreamableQuery {
|
|
|
867
906
|
};
|
|
868
907
|
return {
|
|
869
908
|
done: false,
|
|
870
|
-
value: [result.value, getIndexKey(result.value,
|
|
909
|
+
value: [result.value, getIndexKey(result.value, indexFieldPaths)]
|
|
871
910
|
};
|
|
872
911
|
} };
|
|
873
912
|
} };
|
|
@@ -929,8 +968,11 @@ var ReflectIndexRange = class {
|
|
|
929
968
|
upperBoundInclusive = true;
|
|
930
969
|
equalityIndexFilter = [];
|
|
931
970
|
indexFields;
|
|
971
|
+
/** `indexFields`, each split on `.` once, for `getIndexKey`. */
|
|
972
|
+
indexFieldPaths;
|
|
932
973
|
constructor(indexFields) {
|
|
933
974
|
this.indexFields = indexFields;
|
|
975
|
+
this.indexFieldPaths = indexFields.map((field) => field.split("."));
|
|
934
976
|
}
|
|
935
977
|
eq(field, value) {
|
|
936
978
|
if (!this.#canLowerBound(field) || !this.#canUpperBound(field)) throw new Error(`Cannot use eq on field '${field}'`);
|
|
@@ -1022,16 +1064,24 @@ var ReflectIndexRange = class {
|
|
|
1022
1064
|
function mergedStream(streams, orderByIndexFields) {
|
|
1023
1065
|
return new MergedStream(streams, orderByIndexFields);
|
|
1024
1066
|
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Marks sources that are already `OrderByStream`s, so `narrow` reuses them
|
|
1069
|
+
* instead of adding a redundant delegation layer per call.
|
|
1070
|
+
*
|
|
1071
|
+
* Module-private on purpose: wrapping is also where the ordering invariant is
|
|
1072
|
+
* checked, so no caller outside this file can skip it.
|
|
1073
|
+
*/
|
|
1074
|
+
const PRE_ORDERED_STREAMS = Symbol("preOrderedStreams");
|
|
1025
1075
|
var MergedStream = class MergedStream extends QueryStream {
|
|
1026
1076
|
#order;
|
|
1027
1077
|
#streams;
|
|
1028
1078
|
#equalityIndexFilter;
|
|
1029
1079
|
#indexFields;
|
|
1030
|
-
constructor(streams, orderByIndexFields) {
|
|
1080
|
+
constructor(streams, orderByIndexFields, preOrdered) {
|
|
1031
1081
|
super();
|
|
1032
1082
|
if (streams.length === 0) throw new Error("Cannot union empty array of streams");
|
|
1033
1083
|
this.#order = allSame(streams.map((stream) => stream.getOrder()), "Cannot merge streams with different orders");
|
|
1034
|
-
this.#streams = streams.map((stream) => new OrderByStream(stream, orderByIndexFields));
|
|
1084
|
+
this.#streams = preOrdered === PRE_ORDERED_STREAMS ? streams : streams.map((stream) => new OrderByStream(stream, orderByIndexFields.slice()));
|
|
1035
1085
|
this.#indexFields = allSame(this.#streams.map((stream) => stream.getIndexFields()), "Cannot merge streams with different index fields. Consider using .orderBy()");
|
|
1036
1086
|
this.#equalityIndexFilter = commonPrefix(this.#streams.map((stream) => stream.getEqualityIndexFilter()));
|
|
1037
1087
|
}
|
|
@@ -1044,40 +1094,60 @@ var MergedStream = class MergedStream extends QueryStream {
|
|
|
1044
1094
|
done: false,
|
|
1045
1095
|
value: void 0
|
|
1046
1096
|
}));
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1097
|
+
let started = false;
|
|
1098
|
+
let refillIndex;
|
|
1099
|
+
return {
|
|
1100
|
+
async next() {
|
|
1101
|
+
if (started) {
|
|
1102
|
+
if (refillIndex !== void 0) {
|
|
1103
|
+
results[refillIndex] = await iterators[refillIndex].next();
|
|
1104
|
+
refillIndex = void 0;
|
|
1105
|
+
}
|
|
1106
|
+
} else {
|
|
1107
|
+
started = true;
|
|
1108
|
+
await Promise.all(iterators.map(async (iterator, i) => {
|
|
1109
|
+
results[i] = await iterator.next();
|
|
1110
|
+
}));
|
|
1059
1111
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1112
|
+
let minIndexKeyAndIndex;
|
|
1113
|
+
for (let i = 0; i < results.length; i++) {
|
|
1114
|
+
const result = results[i];
|
|
1115
|
+
if (result.done || !result.value) continue;
|
|
1116
|
+
const [_, resultIndexKey] = result.value;
|
|
1117
|
+
if (minIndexKeyAndIndex === void 0) {
|
|
1118
|
+
minIndexKeyAndIndex = [resultIndexKey, i];
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
const [prevMin, _prevMinIndex] = minIndexKeyAndIndex;
|
|
1122
|
+
if (compareKeys({
|
|
1123
|
+
value: resultIndexKey,
|
|
1124
|
+
kind: "exact"
|
|
1125
|
+
}, {
|
|
1126
|
+
value: prevMin,
|
|
1127
|
+
kind: "exact"
|
|
1128
|
+
}) * comparisonInversion < 0) minIndexKeyAndIndex = [resultIndexKey, i];
|
|
1129
|
+
}
|
|
1130
|
+
if (minIndexKeyAndIndex === void 0) return {
|
|
1131
|
+
done: true,
|
|
1132
|
+
value: void 0
|
|
1133
|
+
};
|
|
1134
|
+
const [_, minIndex] = minIndexKeyAndIndex;
|
|
1135
|
+
const result = results[minIndex].value;
|
|
1136
|
+
results[minIndex].value = void 0;
|
|
1137
|
+
refillIndex = minIndex;
|
|
1138
|
+
return {
|
|
1139
|
+
done: false,
|
|
1140
|
+
value: result
|
|
1141
|
+
};
|
|
1142
|
+
},
|
|
1143
|
+
async return() {
|
|
1144
|
+
await Promise.all(iterators.map((iterator) => iterator.return?.()));
|
|
1145
|
+
return {
|
|
1146
|
+
done: true,
|
|
1147
|
+
value: void 0
|
|
1148
|
+
};
|
|
1068
1149
|
}
|
|
1069
|
-
|
|
1070
|
-
done: true,
|
|
1071
|
-
value: void 0
|
|
1072
|
-
};
|
|
1073
|
-
const [_, minIndex] = minIndexKeyAndIndex;
|
|
1074
|
-
const result = results[minIndex].value;
|
|
1075
|
-
results[minIndex].value = void 0;
|
|
1076
|
-
return {
|
|
1077
|
-
done: false,
|
|
1078
|
-
value: result
|
|
1079
|
-
};
|
|
1080
|
-
} };
|
|
1150
|
+
};
|
|
1081
1151
|
} };
|
|
1082
1152
|
}
|
|
1083
1153
|
getOrder() {
|
|
@@ -1090,7 +1160,7 @@ var MergedStream = class MergedStream extends QueryStream {
|
|
|
1090
1160
|
return this.#indexFields;
|
|
1091
1161
|
}
|
|
1092
1162
|
narrow(indexBounds) {
|
|
1093
|
-
return new MergedStream(this.#streams.map((stream) => stream.narrow(indexBounds)), this.#indexFields);
|
|
1163
|
+
return new MergedStream(this.#streams.map((stream) => stream.narrow(indexBounds)), this.#indexFields, PRE_ORDERED_STREAMS);
|
|
1094
1164
|
}
|
|
1095
1165
|
};
|
|
1096
1166
|
function allSame(values, errorMessage) {
|
|
@@ -1138,16 +1208,22 @@ var ConcatStreams = class ConcatStreams extends QueryStream {
|
|
|
1138
1208
|
this.#equalityIndexFilter = commonPrefix(streams.map((stream) => stream.getEqualityIndexFilter()));
|
|
1139
1209
|
}
|
|
1140
1210
|
iterWithKeys() {
|
|
1141
|
-
const
|
|
1211
|
+
const streams = this.#streams;
|
|
1142
1212
|
const comparisonInversion = this.#order === "asc" ? 1 : -1;
|
|
1143
|
-
let previousIndexKey;
|
|
1144
1213
|
return { [Symbol.asyncIterator]() {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1214
|
+
let index = 0;
|
|
1215
|
+
let iterator;
|
|
1216
|
+
let previousIndexKey;
|
|
1217
|
+
return {
|
|
1218
|
+
async next() {
|
|
1219
|
+
while (index < streams.length) {
|
|
1220
|
+
iterator ??= streams[index].iterWithKeys()[Symbol.asyncIterator]();
|
|
1221
|
+
const result = await iterator.next();
|
|
1222
|
+
if (result.done) {
|
|
1223
|
+
index++;
|
|
1224
|
+
iterator = void 0;
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1151
1227
|
const [_, indexKey] = result.value;
|
|
1152
1228
|
if (previousIndexKey !== void 0 && compareKeys({
|
|
1153
1229
|
value: previousIndexKey,
|
|
@@ -1159,12 +1235,21 @@ var ConcatStreams = class ConcatStreams extends QueryStream {
|
|
|
1159
1235
|
previousIndexKey = indexKey;
|
|
1160
1236
|
return result;
|
|
1161
1237
|
}
|
|
1238
|
+
return {
|
|
1239
|
+
done: true,
|
|
1240
|
+
value: void 0
|
|
1241
|
+
};
|
|
1242
|
+
},
|
|
1243
|
+
async return() {
|
|
1244
|
+
await iterator?.return?.();
|
|
1245
|
+
index = streams.length;
|
|
1246
|
+
iterator = void 0;
|
|
1247
|
+
return {
|
|
1248
|
+
done: true,
|
|
1249
|
+
value: void 0
|
|
1250
|
+
};
|
|
1162
1251
|
}
|
|
1163
|
-
|
|
1164
|
-
done: true,
|
|
1165
|
-
value: void 0
|
|
1166
|
-
};
|
|
1167
|
-
} };
|
|
1252
|
+
};
|
|
1168
1253
|
} };
|
|
1169
1254
|
}
|
|
1170
1255
|
getOrder() {
|
|
@@ -1237,6 +1322,18 @@ var FlatMapStreamIterator = class {
|
|
|
1237
1322
|
value: [u, [...this.#currentOuterItem.indexKey, ...indexKey]]
|
|
1238
1323
|
};
|
|
1239
1324
|
}
|
|
1325
|
+
async return() {
|
|
1326
|
+
try {
|
|
1327
|
+
await this.#currentOuterItem?.innerIterator.return?.();
|
|
1328
|
+
} finally {
|
|
1329
|
+
this.#currentOuterItem = null;
|
|
1330
|
+
await this.#outerIterator.return?.();
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
done: true,
|
|
1334
|
+
value: void 0
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1240
1337
|
};
|
|
1241
1338
|
var FlatMapStream = class FlatMapStream extends QueryStream {
|
|
1242
1339
|
#stream;
|
|
@@ -1456,15 +1553,24 @@ var OrderByStream = class OrderByStream extends QueryStream {
|
|
|
1456
1553
|
const staticFilter = this.#staticFilter;
|
|
1457
1554
|
return { [Symbol.asyncIterator]() {
|
|
1458
1555
|
const iterator = iterable[Symbol.asyncIterator]();
|
|
1459
|
-
return {
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1556
|
+
return {
|
|
1557
|
+
async next() {
|
|
1558
|
+
const result = await iterator.next();
|
|
1559
|
+
if (result.done) return result;
|
|
1560
|
+
const [doc, indexKey] = result.value;
|
|
1561
|
+
return {
|
|
1562
|
+
done: false,
|
|
1563
|
+
value: [doc, indexKey.slice(staticFilter.length)]
|
|
1564
|
+
};
|
|
1565
|
+
},
|
|
1566
|
+
async return() {
|
|
1567
|
+
await iterator.return?.();
|
|
1568
|
+
return {
|
|
1569
|
+
done: true,
|
|
1570
|
+
value: void 0
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1468
1574
|
} };
|
|
1469
1575
|
}
|
|
1470
1576
|
narrow(indexBounds) {
|
|
@@ -1495,36 +1601,57 @@ var DistinctStream = class DistinctStream extends QueryStream {
|
|
|
1495
1601
|
iterWithKeys() {
|
|
1496
1602
|
const stream = this.#stream;
|
|
1497
1603
|
const distinctIndexFieldsLength = this.#distinctIndexFieldsLength;
|
|
1604
|
+
const order = stream.getOrder();
|
|
1605
|
+
const comparisonInversion = order === "asc" ? 1 : -1;
|
|
1498
1606
|
return { [Symbol.asyncIterator]() {
|
|
1499
|
-
let
|
|
1500
|
-
let
|
|
1501
|
-
return {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
done
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1607
|
+
let currentIterator = stream.iterWithKeys()[Symbol.asyncIterator]();
|
|
1608
|
+
let previousDistinctIndexKey;
|
|
1609
|
+
return {
|
|
1610
|
+
async next() {
|
|
1611
|
+
const result = await currentIterator.next();
|
|
1612
|
+
if (result.done) return {
|
|
1613
|
+
done: true,
|
|
1614
|
+
value: void 0
|
|
1615
|
+
};
|
|
1616
|
+
const [doc, indexKey] = result.value;
|
|
1617
|
+
if (doc === null) return {
|
|
1618
|
+
done: false,
|
|
1619
|
+
value: [null, indexKey]
|
|
1620
|
+
};
|
|
1621
|
+
const distinctIndexKey = indexKey.slice(0, distinctIndexFieldsLength);
|
|
1622
|
+
if (previousDistinctIndexKey !== void 0 && compareKeys({
|
|
1623
|
+
value: previousDistinctIndexKey,
|
|
1624
|
+
kind: "exact"
|
|
1625
|
+
}, {
|
|
1626
|
+
value: distinctIndexKey,
|
|
1627
|
+
kind: "exact"
|
|
1628
|
+
}) * comparisonInversion >= 0) throw new Error(`DistinctStream in wrong order: ${JSON.stringify(previousDistinctIndexKey)}, ${JSON.stringify(distinctIndexKey)}`);
|
|
1629
|
+
previousDistinctIndexKey = distinctIndexKey;
|
|
1630
|
+
let narrowed;
|
|
1631
|
+
if (order === "asc") narrowed = stream.narrow({
|
|
1632
|
+
lowerBound: distinctIndexKey,
|
|
1633
|
+
lowerBoundInclusive: false,
|
|
1634
|
+
upperBound: [],
|
|
1635
|
+
upperBoundInclusive: true
|
|
1636
|
+
});
|
|
1637
|
+
else narrowed = stream.narrow({
|
|
1638
|
+
lowerBound: [],
|
|
1639
|
+
lowerBoundInclusive: true,
|
|
1640
|
+
upperBound: distinctIndexKey,
|
|
1641
|
+
upperBoundInclusive: false
|
|
1642
|
+
});
|
|
1643
|
+
await currentIterator.return?.();
|
|
1644
|
+
currentIterator = narrowed.iterWithKeys()[Symbol.asyncIterator]();
|
|
1645
|
+
return result;
|
|
1646
|
+
},
|
|
1647
|
+
async return() {
|
|
1648
|
+
await currentIterator.return?.();
|
|
1649
|
+
return {
|
|
1650
|
+
done: true,
|
|
1651
|
+
value: void 0
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1528
1655
|
} };
|
|
1529
1656
|
}
|
|
1530
1657
|
narrow(indexBounds) {
|
|
@@ -2470,6 +2470,11 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
|
|
|
2470
2470
|
private _orderBySpecs;
|
|
2471
2471
|
private _resolveNonPaginatedLimit;
|
|
2472
2472
|
private _compareByOrderSpecs;
|
|
2473
|
+
/**
|
|
2474
|
+
* Physical table name to relational config. Called once per row on the
|
|
2475
|
+
* relation-count path, so the linear schema scan is indexed once per schema
|
|
2476
|
+
* object rather than repeated. The schema is fixed for the process lifetime.
|
|
2477
|
+
*/
|
|
2473
2478
|
private _getTableConfigByDbName;
|
|
2474
2479
|
private _matchLike;
|
|
2475
2480
|
/**
|
|
@@ -2534,15 +2539,25 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
|
|
|
2534
2539
|
* The where-clause compiler is built from declared indexes only, and `_id` is
|
|
2535
2540
|
* never one of them, so an `id` filter can never be index-selected: it lands
|
|
2536
2541
|
* in the post-filters and the stream walks the creation-time index until it
|
|
2537
|
-
* happens on the row. `db.get()` reads exactly the rows asked for
|
|
2538
|
-
*
|
|
2539
|
-
*
|
|
2542
|
+
* happens on the row. `db.get()` reads exactly the rows asked for.
|
|
2543
|
+
*
|
|
2544
|
+
* Rows come back in the order the ids were given, one read at a time. Missing
|
|
2545
|
+
* or policy-filtered ids still cost a read, but a page does not reread the
|
|
2546
|
+
* complete list. An `orderBy` on creation time is the exception: an id carries
|
|
2547
|
+
* no creation time, so every id must be read before the first row is placed.
|
|
2540
2548
|
*
|
|
2541
2549
|
* Returns null when something else already owns the read: a pinned index, an
|
|
2542
2550
|
* index the compiler did select, a `where(predicate)`, or an `orderBy` that
|
|
2543
2551
|
* walks a different index.
|
|
2544
2552
|
*/
|
|
2545
2553
|
private _buildIdLookupStream;
|
|
2554
|
+
/**
|
|
2555
|
+
* The declared index a stream read can walk to emit `field` in order.
|
|
2556
|
+
*
|
|
2557
|
+
* A stream orders by the index it scans, so only an index that leads with
|
|
2558
|
+
* the field produces that order.
|
|
2559
|
+
*/
|
|
2560
|
+
private _findStreamOrderIndex;
|
|
2546
2561
|
private _buildBasePipelineStream;
|
|
2547
2562
|
/**
|
|
2548
2563
|
* Stream equivalent of the `db.query(...)` chain, used when a post-fetch
|
|
@@ -2616,6 +2631,16 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
|
|
|
2616
2631
|
*/
|
|
2617
2632
|
private _toConvexQuery;
|
|
2618
2633
|
private _buildRelationKey;
|
|
2634
|
+
/**
|
|
2635
|
+
* How many leading fields of the scanned index are pinned to a single value.
|
|
2636
|
+
*
|
|
2637
|
+
* `splitFilters` emits index filters in index-key order — a run of `eq`, then
|
|
2638
|
+
* at most one range on the first unpinned field — so the leading `eq` run is
|
|
2639
|
+
* the prefix Convex holds constant. A multi-probe plan carries no index
|
|
2640
|
+
* filters; each probe supplies its own bound instead, and the union is only
|
|
2641
|
+
* as pinned as its least pinned probe.
|
|
2642
|
+
*/
|
|
2643
|
+
private _indexEqPrefixCount;
|
|
2619
2644
|
private _buildIndexPredicate;
|
|
2620
2645
|
private _buildFilterPredicate;
|
|
2621
2646
|
private _queryByFields;
|
|
@@ -2660,9 +2685,7 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
|
|
|
2660
2685
|
private _createRelationCountError;
|
|
2661
2686
|
private _remapRelationCountError;
|
|
2662
2687
|
private _coerceRelationCountWhere;
|
|
2663
|
-
private _normalizeRelationCountCacheValue;
|
|
2664
2688
|
private _getRelationCountParentKey;
|
|
2665
|
-
private _buildRelationCountExecutionKey;
|
|
2666
2689
|
private _readIndexedRelationCount;
|
|
2667
2690
|
private _countRelationForRow;
|
|
2668
2691
|
private _loadRelationCounts;
|
|
@@ -2672,6 +2695,22 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
|
|
|
2672
2695
|
* M6.5 Phase 2: Added support for nested relations
|
|
2673
2696
|
*/
|
|
2674
2697
|
private _loadOneRelation;
|
|
2698
|
+
/**
|
|
2699
|
+
* Junction links per parent, stopped once each parent holds `fetchLimit`
|
|
2700
|
+
* links whose target actually reaches the page.
|
|
2701
|
+
*
|
|
2702
|
+
* A link only contributes if its target exists and survives target RLS and
|
|
2703
|
+
* the relation `where` — all of which run after the junction read. Sizing the
|
|
2704
|
+
* read on links alone therefore under-fills: three dangling or filtered links
|
|
2705
|
+
* first and `{ limit: 3 }` returns nothing. So the read is refilled in rounds,
|
|
2706
|
+
* each round resolving only the targets it newly needs and asking again for
|
|
2707
|
+
* whatever the survivors did not cover.
|
|
2708
|
+
*
|
|
2709
|
+
* Rounds are the unit rather than single links because both the target fetch
|
|
2710
|
+
* and the relation `where` de-duplicate and batch their own reads across the
|
|
2711
|
+
* parents in the round.
|
|
2712
|
+
*/
|
|
2713
|
+
private _readBoundedThroughLinks;
|
|
2675
2714
|
/**
|
|
2676
2715
|
* Load many() relation (one-to-many)
|
|
2677
2716
|
* Example: users.posts where posts.authorId → users.id
|
|
@@ -4505,22 +4544,22 @@ declare const BUILTIN_SCHEMA_EXTENSIONS: readonly [SchemaExtension<{
|
|
|
4505
4544
|
fieldName: "status";
|
|
4506
4545
|
};
|
|
4507
4546
|
};
|
|
4508
|
-
|
|
4547
|
+
cursor: ConvexTextBuilderInitial<""> & {
|
|
4509
4548
|
_: {
|
|
4510
4549
|
tableName: "migration_state";
|
|
4511
4550
|
};
|
|
4512
4551
|
} & {
|
|
4513
4552
|
_: {
|
|
4514
|
-
fieldName: "
|
|
4553
|
+
fieldName: "cursor";
|
|
4515
4554
|
};
|
|
4516
4555
|
};
|
|
4517
|
-
|
|
4556
|
+
direction: ConvexTextBuilderInitial<""> & {
|
|
4518
4557
|
_: {
|
|
4519
4558
|
tableName: "migration_state";
|
|
4520
4559
|
};
|
|
4521
4560
|
} & {
|
|
4522
4561
|
_: {
|
|
4523
|
-
fieldName: "
|
|
4562
|
+
fieldName: "direction";
|
|
4524
4563
|
};
|
|
4525
4564
|
};
|
|
4526
4565
|
updatedAt: ConvexNumberBuilderInitial<""> & {
|
package/package.json
CHANGED
|
@@ -254,6 +254,13 @@ Rules:
|
|
|
254
254
|
- `one()` with `to: ...id` → uses `db.get()` (no extra index)
|
|
255
255
|
- Missing index throws unless `allowFullScan` on parent query
|
|
256
256
|
|
|
257
|
+
Relation `limit`/`orderBy` are pushed into the relation index when the index
|
|
258
|
+
already walks in that order. After the FK, `index('by_user').on(t.userId)`
|
|
259
|
+
orders by creation time, so `with: { posts: { limit: 5, orderBy: { createdAt:
|
|
260
|
+
'desc' } } }` reads 5 posts per parent. Sorting by any other column reads the
|
|
261
|
+
parent's whole child partition and sorts in memory — put the sort column in the
|
|
262
|
+
relation index (`index('by_user_rank').on(t.userId, t.rank)`) to stay bounded.
|
|
263
|
+
|
|
257
264
|
## Schema Definition
|
|
258
265
|
|
|
259
266
|
```ts
|
|
@@ -713,8 +720,8 @@ const filtered = results.filter((a) => a.publishedAt >= startDate);
|
|
|
713
720
|
|
|
714
721
|
### Performance
|
|
715
722
|
|
|
716
|
-
1. **Index first** — constrain leading index fields. Compound indexes follow prefix rules.
|
|
717
|
-
2. **Bound scans** — use `maxScan` for predicate `where` (cursor mode only).
|
|
723
|
+
1. **Index first** — constrain leading index fields. Compound indexes follow prefix rules. Put the `orderBy` column right after the constrained prefix so the scan is already sorted and `limit` bounds the read.
|
|
724
|
+
2. **Bound scans** — use `maxScan` for predicate `where` (cursor mode only). A `.select()` ID-list query with `orderBy` needs a single field that an index leads with.
|
|
718
725
|
3. **Limit results** — always use `limit` or cursor pagination.
|
|
719
726
|
4. **Cursor stability** — keep same `where`/`orderBy` between page requests.
|
|
720
727
|
5. **`allowFullScan`** — non-cursor only. Cursor mode uses `maxScan` instead.
|