kitcn 0.31.1 → 0.32.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/CHANGELOG.md +68 -0
- package/dist/aggregate/index.d.ts +2 -2
- package/dist/auth/generated/index.d.ts +1 -1
- package/dist/auth/index.d.ts +11 -11
- package/dist/auth/index.js +2 -2
- package/dist/{capabilities-CG-oIMyR.d.ts → capabilities-CD-Ij91k.d.ts} +38 -0
- package/dist/{generated-contract-disabled-BKvk4lFx.d.ts → generated-contract-disabled-PdNGvNYP.d.ts} +10 -10
- package/dist/{index-utils-C1DyktHe.js → index-utils-DvK7P6Q1.js} +32 -9
- package/dist/orm/aggregate-index/index.d.ts +1 -1
- package/dist/orm/aggregate-index/index.js +2 -2
- package/dist/orm/index.d.ts +2 -2
- package/dist/orm/index.js +326 -131
- package/dist/orm/migrations/index.d.ts +2 -2
- package/dist/{query-context-DdSg3fuk.js → query-context-D4z1CnDH.js} +12 -3
- package/dist/{schema-DbPcDW-N.js → schema-Bh7AmJwY.js} +1 -1
- package/dist/{where-clause-compiler-Bgkzm3Y-.d.ts → where-clause-compiler-FDJTqLDC.d.ts} +56 -56
- package/package.json +1 -1
- package/skills/kitcn/references/features/orm.md +34 -4
package/dist/orm/index.js
CHANGED
|
@@ -3,9 +3,9 @@ import { d as boolean, i as detectMigrationDrift, l as migrationExtension, n as
|
|
|
3
3
|
import { a as pretendRequired, i as pretend, n as deprecated } from "../validators-CIoUYCqO.js";
|
|
4
4
|
import { o as id } from "../schema-BFP_awgP.js";
|
|
5
5
|
import { a as OrmSchemaDefinition, c as OrmSchemaExtensionTriggers, d as OrmSchemaRelations, f as OrmSchemaTriggers, g as TablePolymorphic, h as TableName, i as OrmContext, l as OrmSchemaExtensions, n as Columns, o as OrmSchemaExtensionRelations, p as RlsPolicies, r as EnableRLS, s as OrmSchemaExtensionTables, t as Brand, u as OrmSchemaOptions } from "../symbols-DDNAddkd.js";
|
|
6
|
-
import { A as inArray, B as notBetween, C as eq, D as gt, E as filterValuesEqual, F as lt, G as startsWith, H as notInArray, I as lte, L as matchLikePattern, M as isNotNull, N as isNull, O as gte, P as like, R as ne, S as endsWith, T as filterValueInList, U as notLike, V as notIlike, W as or, _ as arrayContains, a as findVectorIndexByName, b as column, c as getRankIndexes, d as INTERNAL_CREATION_TIME_FIELD, f as PUBLIC_CREATED_AT_FIELD, g as arrayContained, h as and, i as findSearchIndexByName, j as isFieldReference, k as ilike, l as resolveIndexOrderPushdown, m as usesSystemCreatedAtAlias, n as findIndexForColumns, o as getAggregateIndexes, r as findRelationIndex, s as getIndexes, t as findExactIndexForColumns, u as CREATED_AT_MIGRATION_MESSAGE, v as arrayOverlaps, w as fieldRef, x as contains, y as between, z as not } from "../index-utils-
|
|
7
|
-
import { a as indexKeyWithinBounds, c as streamIndexRange, i as getIndexFields, l as isUnsetToken, n as EmptyStream, o as mergedStream, r as QueryStream, s as stream, t as getByIdWithOrmQueryFallback, u as unsetToken } from "../query-context-
|
|
8
|
-
import { $ as patchReferencingRows, A as enforcePolymorphicWrite, B as getForeignKeys, C as collectPrimaryIdLookupRows, D as encodeUndefinedDeep, E as deserializeFilterExpression, F as evaluateCheckConstraintTriState, G as getTableColumns$2, H as getMutationCollectionLimits, I as evaluateFilter, J as getUniqueIndexes, K as getTableDeleteConfig, L as extractPrimaryIdLookup, M as ensureDefaultColumns, N as ensureNonNullValues, O as enforceCheckConstraints, P as ensureNullableColumns, Q as normalizeTemporalComparableValue, R as getChecks, S as collectMutationRowsBounded, T as decodeUndefinedDeep, U as getMutationExecutionMode, V as getMutationAsyncDelayMs, W as getOrmContext, X as hydrateDateFieldsForRead, Y as hardDeleteRow, Z as normalizeDateFieldsForWrite, _ as applyDefaults, at as splitReturningSelection, b as buildForeignKeyGraph, c as getAggregateIndexDefinitions, ct as toConvexFilter, d as COUNT_ERROR, dt as markLifecycleHookedTables, et as resolveOrmRuntimeDefaults, f as createAggregateError, ft as compileConvexFilter, g as ensureCountAllowedForRls, h as ensureAggregateAllowedForRls, ht as mapWithConcurrency, it as softDeleteRow, j as enforceUniqueIndexes, k as enforceForeignKeys, l as getRankIndexDefinitions, lt as unsetFieldsOf, mt as isConvexEnforceableFilter, nt as selectReturningRowWithHydration, o as aggregateExtension, ot as stripUnsetFields, p as createCountError, pt as convexAnd, q as getTableName, rt as serializeFilterExpression, st as takeRowsWithinByteBudget, tt as returningSelectionReadsCreationTime, u as AGGREGATE_ERROR, ut as hasLifecycleHooks, v as applyIncomingForeignKeyActionsOnDelete, w as createForeignKeyProbeMemo, x as canUsePrimaryIdLookupCursor, y as applyIncomingForeignKeyActionsOnUpdate, z as getColumnName$1 } from "../schema-
|
|
6
|
+
import { A as inArray, B as notBetween, C as eq, D as gt, E as filterValuesEqual, F as lt, G as startsWith, H as notInArray, I as lte, L as matchLikePattern, M as isNotNull, N as isNull, O as gte, P as like, R as ne, S as endsWith, T as filterValueInList, U as notLike, V as notIlike, W as or, _ as arrayContains, a as findVectorIndexByName, b as column, c as getRankIndexes, d as INTERNAL_CREATION_TIME_FIELD, f as PUBLIC_CREATED_AT_FIELD, g as arrayContained, h as and, i as findSearchIndexByName, j as isFieldReference, k as ilike, l as resolveIndexOrderPushdown, m as usesSystemCreatedAtAlias, n as findIndexForColumns, o as getAggregateIndexes, r as findRelationIndex, s as getIndexes, t as findExactIndexForColumns, u as CREATED_AT_MIGRATION_MESSAGE, v as arrayOverlaps, w as fieldRef, x as contains, y as between, z as not } from "../index-utils-DvK7P6Q1.js";
|
|
7
|
+
import { a as indexKeyWithinBounds, c as streamIndexRange, i as getIndexFields, l as isUnsetToken, n as EmptyStream, o as mergedStream, r as QueryStream, s as stream, t as getByIdWithOrmQueryFallback, u as unsetToken } from "../query-context-D4z1CnDH.js";
|
|
8
|
+
import { $ as patchReferencingRows, A as enforcePolymorphicWrite, B as getForeignKeys, C as collectPrimaryIdLookupRows, D as encodeUndefinedDeep, E as deserializeFilterExpression, F as evaluateCheckConstraintTriState, G as getTableColumns$2, H as getMutationCollectionLimits, I as evaluateFilter, J as getUniqueIndexes, K as getTableDeleteConfig, L as extractPrimaryIdLookup, M as ensureDefaultColumns, N as ensureNonNullValues, O as enforceCheckConstraints, P as ensureNullableColumns, Q as normalizeTemporalComparableValue, R as getChecks, S as collectMutationRowsBounded, T as decodeUndefinedDeep, U as getMutationExecutionMode, V as getMutationAsyncDelayMs, W as getOrmContext, X as hydrateDateFieldsForRead, Y as hardDeleteRow, Z as normalizeDateFieldsForWrite, _ as applyDefaults, at as splitReturningSelection, b as buildForeignKeyGraph, c as getAggregateIndexDefinitions, ct as toConvexFilter, d as COUNT_ERROR, dt as markLifecycleHookedTables, et as resolveOrmRuntimeDefaults, f as createAggregateError, ft as compileConvexFilter, g as ensureCountAllowedForRls, h as ensureAggregateAllowedForRls, ht as mapWithConcurrency, it as softDeleteRow, j as enforceUniqueIndexes, k as enforceForeignKeys, l as getRankIndexDefinitions, lt as unsetFieldsOf, mt as isConvexEnforceableFilter, nt as selectReturningRowWithHydration, o as aggregateExtension, ot as stripUnsetFields, p as createCountError, pt as convexAnd, q as getTableName, rt as serializeFilterExpression, st as takeRowsWithinByteBudget, tt as returningSelectionReadsCreationTime, u as AGGREGATE_ERROR, ut as hasLifecycleHooks, v as applyIncomingForeignKeyActionsOnDelete, w as createForeignKeyProbeMemo, x as canUsePrimaryIdLookupCursor, y as applyIncomingForeignKeyActionsOnUpdate, z as getColumnName$1 } from "../schema-Bh7AmJwY.js";
|
|
9
9
|
import { t as defineSchemaExtension } from "../extensions-Bp0XHanR.js";
|
|
10
10
|
import { compareValues, convexToJson, jsonToConvex, v } from "convex/values";
|
|
11
11
|
import { defineSchema as defineSchema$1, internalActionGeneric, internalMutationGeneric, internalQueryGeneric } from "convex/server";
|
|
@@ -703,11 +703,15 @@ async function filterSelectRows(options) {
|
|
|
703
703
|
*/
|
|
704
704
|
const INDEX_ORDER_BONUS = 30;
|
|
705
705
|
/**
|
|
706
|
-
* Widest
|
|
707
|
-
*
|
|
708
|
-
*
|
|
706
|
+
* Widest index union worth opening. Each probe becomes its own index range, and
|
|
707
|
+
* a streamed union holds every one of them open at once, so past this width the
|
|
708
|
+
* fan-out costs more than the single scan it would replace.
|
|
709
|
+
*
|
|
710
|
+
* Owns both ends of that decision: whether to promote an `in` inside an AND to
|
|
711
|
+
* a union here, and whether the executor may read a compiled union as a merged
|
|
712
|
+
* stream instead of a bounded scan.
|
|
709
713
|
*/
|
|
710
|
-
const
|
|
714
|
+
const MAX_INDEX_UNION_PROBES = 64;
|
|
711
715
|
var WhereClauseCompiler = class {
|
|
712
716
|
/**
|
|
713
717
|
* Requested sort fields for the compile in flight. Index choice has to see
|
|
@@ -785,7 +789,7 @@ var WhereClauseCompiler = class {
|
|
|
785
789
|
for (const term of terms) {
|
|
786
790
|
if (term.type !== "binary" || term.operator !== "inArray") continue;
|
|
787
791
|
const probePlan = this.tryCompileInArray(term);
|
|
788
|
-
if (!probePlan || probePlan.probeFilters.length >
|
|
792
|
+
if (!probePlan || probePlan.probeFilters.length > MAX_INDEX_UNION_PROBES) continue;
|
|
789
793
|
return {
|
|
790
794
|
...probePlan,
|
|
791
795
|
postFilters: [expression]
|
|
@@ -976,9 +980,16 @@ var WhereClauseCompiler = class {
|
|
|
976
980
|
findLeadingIndex(fieldName) {
|
|
977
981
|
const candidates = this.availableIndexes.filter((index) => index.indexFields[0] === fieldName).sort((a, b) => a.indexFields.length - b.indexFields.length);
|
|
978
982
|
if (this.orderFields.length > 0) {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
983
|
+
let best = null;
|
|
984
|
+
let bestServed = 0;
|
|
985
|
+
for (const index of candidates) {
|
|
986
|
+
const served = this.servedOrderFieldCount(index.indexFields, 1);
|
|
987
|
+
if (served > bestServed) {
|
|
988
|
+
best = index;
|
|
989
|
+
bestServed = served;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (best) return best;
|
|
982
993
|
}
|
|
983
994
|
return candidates[0] ?? null;
|
|
984
995
|
}
|
|
@@ -1155,10 +1166,42 @@ var WhereClauseCompiler = class {
|
|
|
1155
1166
|
* Reward an index that also supplies the requested order. Large enough to
|
|
1156
1167
|
* clear the 25-point exact-over-prefix premium, so `(orgId, createdAt)` beats
|
|
1157
1168
|
* `(orgId)` for `where { orgId } orderBy { createdAt }`.
|
|
1169
|
+
*
|
|
1170
|
+
* The reward grows with how much of the sort the index carries, so
|
|
1171
|
+
* `(orgId, createdAt, title)` beats `(orgId, createdAt)` for a two-field
|
|
1172
|
+
* sort: only the longer one lets the scan produce the whole tuple, and
|
|
1173
|
+
* picking the shorter one costs the entire read bound. The extra point per
|
|
1174
|
+
* field is deliberately small — order coverage breaks ties between indexes
|
|
1175
|
+
* that already serve the filter equally well, it does not outrank filtering.
|
|
1158
1176
|
*/
|
|
1159
1177
|
indexOrderBonus(indexFields, pinnedLength) {
|
|
1160
1178
|
if (this.orderFields.length === 0 || pinnedLength >= indexFields.length) return 0;
|
|
1161
|
-
|
|
1179
|
+
const served = this.servedOrderFieldCount(indexFields, pinnedLength);
|
|
1180
|
+
return served === 0 ? 0 : INDEX_ORDER_BONUS + (served - 1);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* How many moving sort fields this index would produce for free. Sort fields
|
|
1184
|
+
* inside the pinned prefix are constant and can appear anywhere in the
|
|
1185
|
+
* request; every other field must match the remaining index keys in order.
|
|
1186
|
+
*/
|
|
1187
|
+
servedOrderFieldCount(indexFields, pinnedLength) {
|
|
1188
|
+
const pinnedFields = new Set(indexFields.slice(0, pinnedLength));
|
|
1189
|
+
let indexOffset = pinnedLength;
|
|
1190
|
+
let orderOffset = 0;
|
|
1191
|
+
let served = 0;
|
|
1192
|
+
while (orderOffset < this.orderFields.length) {
|
|
1193
|
+
const orderField = this.orderFields[orderOffset];
|
|
1194
|
+
if (pinnedFields.has(orderField)) {
|
|
1195
|
+
orderOffset += 1;
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (indexFields[indexOffset] !== orderField) break;
|
|
1199
|
+
served += 1;
|
|
1200
|
+
indexOffset += 1;
|
|
1201
|
+
orderOffset += 1;
|
|
1202
|
+
}
|
|
1203
|
+
if (this.orderFields.length > 1 && orderOffset === this.orderFields.length && indexOffset < indexFields.length) return 0;
|
|
1204
|
+
return served;
|
|
1162
1205
|
}
|
|
1163
1206
|
/**
|
|
1164
1207
|
* Check if referenced fields exactly match index fields in order
|
|
@@ -1290,6 +1333,22 @@ const RELATION_COUNT_ERROR = {
|
|
|
1290
1333
|
};
|
|
1291
1334
|
const RELATION_DEPTH_ERROR = "RELATION_DEPTH_EXCEEDED";
|
|
1292
1335
|
/**
|
|
1336
|
+
* Whether a compiled column validator admits an absent or null value.
|
|
1337
|
+
*
|
|
1338
|
+
* `v.optional(...)` is how a non-`.notNull()` column is emitted, and absent is
|
|
1339
|
+
* a legal stored value for every document written before the column existed, so
|
|
1340
|
+
* both halves matter. Unrecognized shapes answer true: the only caller uses
|
|
1341
|
+
* this to decide whether it may skip a sort, and guessing "never null" there
|
|
1342
|
+
* silently reorders rows.
|
|
1343
|
+
*/
|
|
1344
|
+
const validatorAcceptsMissingOrNull = (validator) => {
|
|
1345
|
+
if (!validator) return true;
|
|
1346
|
+
if (validator.isOptional === "optional") return true;
|
|
1347
|
+
if (validator.kind === "null" || validator.kind === "any") return true;
|
|
1348
|
+
if (validator.kind === "union") return (validator.members ?? []).some(validatorAcceptsMissingOrNull);
|
|
1349
|
+
return false;
|
|
1350
|
+
};
|
|
1351
|
+
/**
|
|
1293
1352
|
* How many levels of `with` the relation loader will follow. A `with` config is
|
|
1294
1353
|
* a finite object the caller wrote, so its own nesting is the real bound; this
|
|
1295
1354
|
* ceiling only stops a self-referential config from recursing forever. Reaching
|
|
@@ -1712,18 +1771,41 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
1712
1771
|
return (Array.isArray(orderBy) ? orderBy : [orderBy]).filter((item) => item !== void 0).map((item) => this._normalizeOrderByValue(item));
|
|
1713
1772
|
}
|
|
1714
1773
|
_orderBySpecs(orderBy, tableConfig = this.tableConfig) {
|
|
1774
|
+
const withNullability = (spec) => ({
|
|
1775
|
+
...spec,
|
|
1776
|
+
nullable: this._isNullableOrderField(spec.field, tableConfig)
|
|
1777
|
+
});
|
|
1715
1778
|
if (orderBy && typeof orderBy === "object" && !Array.isArray(orderBy) && !this._isOrderByClause(orderBy) && !this._isColumnBuilder(orderBy)) return Object.entries(orderBy).map(([field, direction]) => ({
|
|
1716
1779
|
field: this._normalizePublicFieldName(field, tableConfig),
|
|
1717
1780
|
direction
|
|
1718
|
-
})).filter((entry) => entry.direction === "asc" || entry.direction === "desc").map((entry) => ({
|
|
1781
|
+
})).filter((entry) => entry.direction === "asc" || entry.direction === "desc").map((entry) => withNullability({
|
|
1719
1782
|
field: entry.field,
|
|
1720
1783
|
direction: entry.direction
|
|
1721
1784
|
}));
|
|
1722
|
-
return this._normalizeOrderBy(orderBy).map((clause) => ({
|
|
1785
|
+
return this._normalizeOrderBy(orderBy).map((clause) => withNullability({
|
|
1723
1786
|
field: clause.column.columnName,
|
|
1724
1787
|
direction: clause.direction
|
|
1725
1788
|
}));
|
|
1726
1789
|
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Whether rows can be missing this sort column or hold null in it.
|
|
1792
|
+
*
|
|
1793
|
+
* Only the declared validator can answer: a column that is not `.notNull()`
|
|
1794
|
+
* compiles to `v.optional(v.union(v.null(), T))`, so both absent and null are
|
|
1795
|
+
* legal stored values — and `timestamp().notNull().defaultNow()` on
|
|
1796
|
+
* `createdAt` is deliberately emitted as optional for migration safety, so
|
|
1797
|
+
* reading `config.notNull` alone would answer wrong for it. Anything the
|
|
1798
|
+
* lookup cannot resolve counts as nullable, which only costs a pushdown.
|
|
1799
|
+
*/
|
|
1800
|
+
_isNullableOrderField(field, tableConfig) {
|
|
1801
|
+
if (field === INTERNAL_ID_FIELD || field === INTERNAL_CREATION_TIME_FIELD) return false;
|
|
1802
|
+
try {
|
|
1803
|
+
const builder = this._getColumns(tableConfig)[field];
|
|
1804
|
+
return validatorAcceptsMissingOrNull(builder?.build?.());
|
|
1805
|
+
} catch {
|
|
1806
|
+
return true;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1727
1809
|
_resolveNonPaginatedLimit(config) {
|
|
1728
1810
|
const explicitLimit = config.limit;
|
|
1729
1811
|
const defaultLimit = getOrmContext(this.db)?.resolvedDefaults?.defaultLimit ?? this.tableConfig.defaults?.defaultLimit;
|
|
@@ -1744,8 +1826,8 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
1744
1826
|
return 1;
|
|
1745
1827
|
}
|
|
1746
1828
|
if (bVal === null || bVal === void 0) return -1;
|
|
1747
|
-
|
|
1748
|
-
if (
|
|
1829
|
+
const comparison = compareValues(aVal, bVal);
|
|
1830
|
+
if (comparison !== 0) return order.direction === "asc" ? comparison : -comparison;
|
|
1749
1831
|
}
|
|
1750
1832
|
return 0;
|
|
1751
1833
|
}
|
|
@@ -2675,28 +2757,99 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2675
2757
|
_findStreamOrderIndex(field) {
|
|
2676
2758
|
return getIndexes(this.tableConfig.table).find((index) => index.fields[0] === field);
|
|
2677
2759
|
}
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2760
|
+
/**
|
|
2761
|
+
* The compiled index union, as one ordered stream.
|
|
2762
|
+
*
|
|
2763
|
+
* Each probe is its own index range, so the union is only usable where the
|
|
2764
|
+
* merged order is the order the read has to produce. `mergedStream` orders by
|
|
2765
|
+
* a *suffix* of the index key, and a suffix may only drop key components the
|
|
2766
|
+
* probes all pin to a single value — so the requested field has to sit inside
|
|
2767
|
+
* the pinned run or immediately after it. Returns null when it does not, and
|
|
2768
|
+
* the caller falls back to the plan's plain index range or a bounded scan.
|
|
2769
|
+
*/
|
|
2770
|
+
_buildProbeUnionStream(params) {
|
|
2771
|
+
const { probeFilters } = params;
|
|
2772
|
+
if (probeFilters.length === 0 || probeFilters.length > MAX_INDEX_UNION_PROBES) return null;
|
|
2773
|
+
const streamIndexFields = getIndexFields(this.tableConfig.name, params.indexName, params.schemaDefinition);
|
|
2774
|
+
const pinned = this._indexEqPrefixCount({ probeFilters });
|
|
2775
|
+
let mergeOffset = -1;
|
|
2776
|
+
for (let i = 0; i <= pinned && i < streamIndexFields.length; i += 1) if (streamIndexFields[i] === params.orderField) {
|
|
2777
|
+
mergeOffset = i;
|
|
2778
|
+
break;
|
|
2779
|
+
}
|
|
2780
|
+
if (mergeOffset === -1) return null;
|
|
2781
|
+
return mergedStream(probeFilters.map((filters) => stream(this.db, params.schemaDefinition).query(this.tableConfig.name).withIndex(params.indexName, (q) => {
|
|
2782
|
+
let indexQuery = q;
|
|
2783
|
+
for (const filter of filters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
2784
|
+
return indexQuery;
|
|
2785
|
+
}).order(params.order)), streamIndexFields.slice(mergeOffset));
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* The read the compiled plan describes, as a stream, with nothing filtered
|
|
2789
|
+
* yet.
|
|
2790
|
+
*
|
|
2791
|
+
* Precedence: the compiled index union, then the compiled index range, then
|
|
2792
|
+
* the caller's pinned `.withIndex(...)`, then an index that supplies the
|
|
2793
|
+
* requested order, then a full scan. The first two rungs can only ever refine
|
|
2794
|
+
* what the caller pinned — `_toConvexQuery` discards a compiled plan that
|
|
2795
|
+
* would displace a caller's index or its bounds before it gets here.
|
|
2796
|
+
*
|
|
2797
|
+
* `probeUnion` tells the caller the read is bounded by index ranges rather
|
|
2798
|
+
* than by scan length, which is what makes a scan budget unnecessary. A
|
|
2799
|
+
* rejected union stays unanchored; its original predicate remains in
|
|
2800
|
+
* `queryConfig.postFilters` for the caller to apply while pulling the scan.
|
|
2801
|
+
*/
|
|
2802
|
+
_buildPlanStream(params) {
|
|
2803
|
+
const { queryConfig, configuredIndex, order, primaryOrder } = params;
|
|
2804
|
+
const probeIndex = queryConfig.probeFilters.length > 0 ? queryConfig.index : void 0;
|
|
2805
|
+
const hasProbeUnionPlan = !!probeIndex;
|
|
2806
|
+
if (hasProbeUnionPlan) {
|
|
2807
|
+
const probeUnion = this._buildProbeUnionStream({
|
|
2808
|
+
schemaDefinition: params.schemaDefinition,
|
|
2809
|
+
indexName: probeIndex.name,
|
|
2810
|
+
probeFilters: queryConfig.probeFilters,
|
|
2811
|
+
order,
|
|
2812
|
+
orderField: primaryOrder?.field ?? probeIndex.fields[0]
|
|
2813
|
+
});
|
|
2814
|
+
if (probeUnion) return {
|
|
2815
|
+
stream: probeUnion,
|
|
2816
|
+
probeUnion: true
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
let streamQuery = stream(this.db, params.schemaDefinition).query(this.tableConfig.name);
|
|
2820
|
+
if (queryConfig.index && !hasProbeUnionPlan) streamQuery = streamQuery.withIndex(queryConfig.index.name, (q) => {
|
|
2684
2821
|
let indexQuery = q;
|
|
2685
2822
|
for (const filter of queryConfig.index.filters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
2686
2823
|
return indexQuery;
|
|
2687
2824
|
});
|
|
2688
|
-
else if (configuredIndex?.name) streamQuery = streamQuery.withIndex(configuredIndex.name, configuredIndex.range ? configuredIndex.range : (q) => q);
|
|
2689
|
-
else if (
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2825
|
+
else if (!hasProbeUnionPlan && configuredIndex?.name) streamQuery = streamQuery.withIndex(configuredIndex.name, configuredIndex.range ? configuredIndex.range : (q) => q);
|
|
2826
|
+
else if (!hasProbeUnionPlan && params.orderIndexName) streamQuery = streamQuery.withIndex(params.orderIndexName, (q) => q);
|
|
2827
|
+
return {
|
|
2828
|
+
stream: streamQuery.order(order),
|
|
2829
|
+
probeUnion: false
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
_buildBasePipelineStream(queryConfig, wherePredicate, configuredIndex, fallbackOrder) {
|
|
2833
|
+
const schemaDefinition = this._getSchemaDefinitionOrThrow();
|
|
2834
|
+
const primaryOrder = queryConfig.order?.[0];
|
|
2835
|
+
const planStream = this._buildPlanStream({
|
|
2836
|
+
queryConfig,
|
|
2837
|
+
configuredIndex,
|
|
2838
|
+
order: primaryOrder?.direction ?? fallbackOrder,
|
|
2839
|
+
primaryOrder,
|
|
2840
|
+
orderIndexName: primaryOrder && primaryOrder.field !== INTERNAL_CREATION_TIME_FIELD ? this._findStreamOrderIndex(primaryOrder.field)?.name ?? null : null,
|
|
2841
|
+
schemaDefinition
|
|
2842
|
+
});
|
|
2843
|
+
let streamQuery = planStream.stream;
|
|
2694
2844
|
if (queryConfig.postFilters.length > 0 || wherePredicate) streamQuery = streamQuery.filterWith(async (row) => {
|
|
2695
2845
|
for (const filter of queryConfig.postFilters) if (!this._evaluatePostFetchFilter(row, filter)) return false;
|
|
2696
2846
|
if (wherePredicate) return await wherePredicate(row);
|
|
2697
2847
|
return true;
|
|
2698
2848
|
});
|
|
2699
|
-
return
|
|
2849
|
+
return {
|
|
2850
|
+
stream: streamQuery,
|
|
2851
|
+
probeUnion: planStream.probeUnion
|
|
2852
|
+
};
|
|
2700
2853
|
}
|
|
2701
2854
|
/**
|
|
2702
2855
|
* Stream equivalent of the `db.query(...)` chain, used when a post-fetch
|
|
@@ -2714,16 +2867,15 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2714
2867
|
_buildResidualFilterStream(params) {
|
|
2715
2868
|
const schemaDefinition = this.schema[OrmSchemaDefinition];
|
|
2716
2869
|
if (!schemaDefinition) return null;
|
|
2717
|
-
const { queryConfig, configuredIndex, membershipFilter, orderIndexName, primaryOrder, fallbackOrder } = params;
|
|
2718
|
-
let streamQuery =
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
streamQuery = streamQuery.order(primaryOrder ? primaryOrder.direction : fallbackOrder);
|
|
2870
|
+
const { queryConfig, configuredIndex, membershipFilter, orderIndexName, primaryOrder, pushdownDirection, fallbackOrder } = params;
|
|
2871
|
+
let streamQuery = this._buildPlanStream({
|
|
2872
|
+
queryConfig,
|
|
2873
|
+
configuredIndex,
|
|
2874
|
+
order: pushdownDirection ?? (primaryOrder ? primaryOrder.direction : fallbackOrder),
|
|
2875
|
+
primaryOrder,
|
|
2876
|
+
orderIndexName,
|
|
2877
|
+
schemaDefinition
|
|
2878
|
+
}).stream;
|
|
2727
2879
|
streamQuery = streamQuery.filterWith(async (row) => {
|
|
2728
2880
|
for (const filter of queryConfig.postFilters) if (!this._evaluatePostFetchFilter(row, filter)) return false;
|
|
2729
2881
|
return membershipFilter ? await membershipFilter(row) : true;
|
|
@@ -3876,7 +4028,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
3876
4028
|
if (endCursor !== void 0 && searchConfig) throw new Error("endCursor is not supported with search in findMany().");
|
|
3877
4029
|
if (endCursor !== void 0 && vectorSearchConfig) throw new Error("endCursor is not supported with vectorSearch in findMany().");
|
|
3878
4030
|
if (useAdvancedStreamPath) {
|
|
3879
|
-
const fallbackOrder = (queryConfig.order?.[0])?.direction ?? "asc";
|
|
4031
|
+
const fallbackOrder = (queryConfig.order?.[0])?.direction ?? (endCursor !== void 0 && !pipeline ? "desc" : "asc");
|
|
3880
4032
|
let streamQuery;
|
|
3881
4033
|
const unionSources = pipeline?.union ?? [];
|
|
3882
4034
|
if (unionSources.length > 0) {
|
|
@@ -3892,13 +4044,22 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
3892
4044
|
if (orderedIdList.field === INTERNAL_CREATION_TIME_FIELD) throw new Error("An id IN pipeline cannot combine orderBy on createdAt with maxScan, because ordering an id list by creation time requires reading every id in the list. Drop maxScan, or drop orderBy to page in id-list order at one read per row.");
|
|
3893
4045
|
if (queryConfig.order?.length !== 1 || configuredIndex?.name || queryConfig.index || !this._findStreamOrderIndex(orderedIdList.field)) throw new Error(`An id IN pipeline cannot combine orderBy on ${this._toPublicFilterFieldName(orderedIdList.field)} with maxScan, because the scan cannot produce that order. It needs a single orderBy field that an index leads with, and no index pinned by withIndex().`);
|
|
3894
4046
|
}
|
|
3895
|
-
|
|
4047
|
+
const idLookupStream = await this._buildIdLookupStream({
|
|
3896
4048
|
configuredIndex,
|
|
3897
4049
|
idLookup,
|
|
3898
4050
|
order: fallbackOrder,
|
|
3899
4051
|
queryConfig,
|
|
3900
4052
|
wherePredicate
|
|
3901
|
-
})
|
|
4053
|
+
});
|
|
4054
|
+
if (idLookupStream) streamQuery = idLookupStream;
|
|
4055
|
+
else {
|
|
4056
|
+
const baseStream = this._buildBasePipelineStream(queryConfig, wherePredicate, configuredIndex, fallbackOrder);
|
|
4057
|
+
if (isCursorPaginated && queryConfig.strategy === "multiProbe" && !!queryConfig.index && !baseStream.probeUnion && maxScan === void 0) {
|
|
4058
|
+
if (strict) throw new Error("Pagination with multi-probe index-union filters requires maxScan when strict=true. Add maxScan, order by a field the union can serve, or make the query indexable.");
|
|
4059
|
+
console.warn("Pagination with multi-probe index-union filters is running without maxScan because strict: false.");
|
|
4060
|
+
}
|
|
4061
|
+
streamQuery = baseStream.stream;
|
|
4062
|
+
}
|
|
3902
4063
|
}
|
|
3903
4064
|
const rootRlsEnabled = isRlsEnabled(this.tableConfig.table);
|
|
3904
4065
|
if (rootRlsEnabled) {
|
|
@@ -4014,6 +4175,37 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4014
4175
|
const primaryOrder = postFetchOrders[0];
|
|
4015
4176
|
const hasSecondaryOrders = postFetchOrders.length > 1;
|
|
4016
4177
|
let orderIndexName = null;
|
|
4178
|
+
/**
|
|
4179
|
+
* Direction the scanned index can be walked in to produce the *whole*
|
|
4180
|
+
* requested sort, or null when some of it still has to run in JavaScript.
|
|
4181
|
+
* This is the read bound: a non-null answer means `take(limit)` is the
|
|
4182
|
+
* exact page.
|
|
4183
|
+
*/
|
|
4184
|
+
let orderPushdownDirection = null;
|
|
4185
|
+
/**
|
|
4186
|
+
* Ask the one owner both questions an index has to answer.
|
|
4187
|
+
*
|
|
4188
|
+
* `whole` decides the read bound. `primary` is narrower and only feeds the
|
|
4189
|
+
* cursor-pagination diagnostics, which are about the leading sort field
|
|
4190
|
+
* having an index at all — an index that anchors the primary but not the
|
|
4191
|
+
* tie-break is still the right index to scan, and must not start reporting
|
|
4192
|
+
* itself as unindexed.
|
|
4193
|
+
*/
|
|
4194
|
+
const resolveOrderForIndex = (indexFields, pinnedEqCount) => {
|
|
4195
|
+
const whole = resolveIndexOrderPushdown({
|
|
4196
|
+
indexFields,
|
|
4197
|
+
pinnedEqCount,
|
|
4198
|
+
orderSpecs: postFetchOrders
|
|
4199
|
+
});
|
|
4200
|
+
return {
|
|
4201
|
+
whole,
|
|
4202
|
+
primary: whole ?? (primaryOrder ? resolveIndexOrderPushdown({
|
|
4203
|
+
indexFields,
|
|
4204
|
+
pinnedEqCount,
|
|
4205
|
+
orderSpecs: [primaryOrder]
|
|
4206
|
+
}) : null)
|
|
4207
|
+
};
|
|
4208
|
+
};
|
|
4017
4209
|
if (queryConfig.index) {
|
|
4018
4210
|
const indexConfig = queryConfig.index;
|
|
4019
4211
|
query = query.withIndex(indexConfig.name, (q) => {
|
|
@@ -4022,12 +4214,9 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4022
4214
|
return indexQuery;
|
|
4023
4215
|
});
|
|
4024
4216
|
if (primaryOrder) {
|
|
4025
|
-
const
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
orderSpecs: [primaryOrder]
|
|
4029
|
-
});
|
|
4030
|
-
if (pushdownDirection) query = query.order(pushdownDirection);
|
|
4217
|
+
const resolved = resolveOrderForIndex(indexConfig.fields, this._indexEqPrefixCount(queryConfig));
|
|
4218
|
+
orderPushdownDirection = resolved.whole;
|
|
4219
|
+
if (resolved.primary) query = query.order(resolved.primary);
|
|
4031
4220
|
else needsPostFetchSortForPrimary = true;
|
|
4032
4221
|
}
|
|
4033
4222
|
} else if (configuredIndex?.name) {
|
|
@@ -4036,27 +4225,51 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4036
4225
|
const configuredRange = configuredIndex.range;
|
|
4037
4226
|
query = query.withIndex(configuredIndex.name, configuredRange ? (q) => configuredRange(observeConfiguredIndexRange(q, rangeOperations)) : (q) => q);
|
|
4038
4227
|
if (primaryOrder) {
|
|
4039
|
-
const
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
orderSpecs: [primaryOrder]
|
|
4043
|
-
});
|
|
4044
|
-
if (pushdownDirection) query = query.order(pushdownDirection);
|
|
4228
|
+
const resolved = resolveOrderForIndex(configuredIndexFields, countConfiguredIndexEqPrefix(configuredIndexFields, rangeOperations));
|
|
4229
|
+
orderPushdownDirection = resolved.whole;
|
|
4230
|
+
if (resolved.primary) query = query.order(resolved.primary);
|
|
4045
4231
|
else needsPostFetchSortForPrimary = true;
|
|
4046
4232
|
}
|
|
4047
4233
|
} else if (queryConfig.order && primaryOrder) {
|
|
4048
4234
|
const orderField = primaryOrder.field;
|
|
4049
|
-
if (orderField ===
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4235
|
+
if (orderField === INTERNAL_CREATION_TIME_FIELD) {
|
|
4236
|
+
query = query.order(primaryOrder.direction);
|
|
4237
|
+
orderPushdownDirection = resolveIndexOrderPushdown({
|
|
4238
|
+
indexFields: [],
|
|
4239
|
+
pinnedEqCount: 0,
|
|
4240
|
+
orderSpecs: postFetchOrders
|
|
4241
|
+
});
|
|
4242
|
+
} else {
|
|
4243
|
+
const candidates = [...getIndexes(this.tableConfig.table).map((idx) => ({
|
|
4244
|
+
fields: idx.fields,
|
|
4245
|
+
name: idx.name
|
|
4246
|
+
})), ...this.edgeMetadata.map((idx) => ({
|
|
4247
|
+
fields: idx.indexFields,
|
|
4248
|
+
name: idx.indexName
|
|
4249
|
+
}))];
|
|
4250
|
+
let selected;
|
|
4251
|
+
for (const candidate of candidates) {
|
|
4252
|
+
const direction = resolveIndexOrderPushdown({
|
|
4253
|
+
indexFields: candidate.fields,
|
|
4254
|
+
pinnedEqCount: 0,
|
|
4255
|
+
orderSpecs: postFetchOrders
|
|
4256
|
+
});
|
|
4257
|
+
if (direction) {
|
|
4258
|
+
selected = candidate;
|
|
4259
|
+
orderPushdownDirection = direction;
|
|
4260
|
+
break;
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
selected ??= candidates.find((candidate) => candidate.fields[0] === orderField);
|
|
4264
|
+
if (selected) {
|
|
4265
|
+
orderIndexName = selected.name;
|
|
4054
4266
|
query = query.withIndex(orderIndexName, (q) => q);
|
|
4055
|
-
query = query.order(primaryOrder.direction);
|
|
4267
|
+
query = query.order(orderPushdownDirection ?? primaryOrder.direction);
|
|
4056
4268
|
} else needsPostFetchSortForPrimary = true;
|
|
4057
4269
|
}
|
|
4058
4270
|
}
|
|
4059
|
-
usePostFetchSort =
|
|
4271
|
+
usePostFetchSort = postFetchOrders.length > 0 && orderPushdownDirection === null;
|
|
4272
|
+
const hasUnservedSecondaryOrders = hasSecondaryOrders && orderPushdownDirection === null;
|
|
4060
4273
|
if (wherePredicate) {
|
|
4061
4274
|
const predicateIndex = configuredIndex;
|
|
4062
4275
|
if (!predicateIndex?.name) throw new Error("predicate(...) requires .withIndex(name, range?) on the query.");
|
|
@@ -4068,9 +4281,9 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4068
4281
|
if (strict) throw new Error(`Pagination: Field '${primaryOrder?.field}' has no index. Add an index or disable strict.`);
|
|
4069
4282
|
console.warn(`Pagination: Field '${primaryOrder?.field}' has no index. Falling back to _creationTime ordering.`);
|
|
4070
4283
|
}
|
|
4071
|
-
if (
|
|
4284
|
+
if (hasUnservedSecondaryOrders) console.warn("Pagination: Only the first orderBy field is used for cursor ordering. Secondary orderBy fields are ignored because no index serves the whole sort.");
|
|
4072
4285
|
}
|
|
4073
|
-
if (primaryOrder && !needsPostFetchSortForPrimary) streamQuery = streamQuery.order(primaryOrder.direction);
|
|
4286
|
+
if (primaryOrder && !needsPostFetchSortForPrimary) streamQuery = streamQuery.order(orderPushdownDirection ?? primaryOrder.direction);
|
|
4074
4287
|
else if (isCursorPaginated) streamQuery = streamQuery.order("desc");
|
|
4075
4288
|
streamQuery = streamQuery.filterWith(async (row) => {
|
|
4076
4289
|
for (const filter of queryConfig.postFilters) if (!this._evaluatePostFetchFilter(row, filter)) return false;
|
|
@@ -4116,12 +4329,8 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4116
4329
|
const convexProbeFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
|
|
4117
4330
|
const probeHasResidualFilter = convexProbeFilters.length !== queryConfig.postFilters.length;
|
|
4118
4331
|
const probeHasPostFetchMembership = this._hasSearchDisallowedRelationFilter(whereFilter, this.tableConfig) || this.rls?.mode !== "skip" && isRlsEnabled(this.tableConfig.table);
|
|
4119
|
-
const probeOrderDirection =
|
|
4120
|
-
|
|
4121
|
-
pinnedEqCount: this._indexEqPrefixCount(queryConfig),
|
|
4122
|
-
orderSpecs: [primaryOrder]
|
|
4123
|
-
}) : null;
|
|
4124
|
-
const probeBound = probeLimit !== void 0 && !probeHasResidualFilter && !probeHasPostFetchMembership && (postFetchOrders.length === 0 || probeOrderDirection !== null && !hasSecondaryOrders) ? probeOffset + probeLimit : void 0;
|
|
4332
|
+
const probeOrderDirection = orderPushdownDirection;
|
|
4333
|
+
const probeBound = probeLimit !== void 0 && !probeHasResidualFilter && !probeHasPostFetchMembership && (postFetchOrders.length === 0 || probeOrderDirection !== null) ? probeOffset + probeLimit : void 0;
|
|
4125
4334
|
const probeRows = await Promise.all(queryConfig.probeFilters.map(async (probeFilters) => {
|
|
4126
4335
|
let probeQuery = this.db.query(queryConfig.table).withIndex(queryConfig.index.name, (q) => {
|
|
4127
4336
|
let indexQuery = q;
|
|
@@ -4149,69 +4358,53 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4149
4358
|
return (await this._applyRelationsFilterToRows(visibleRows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with)).length > 0;
|
|
4150
4359
|
};
|
|
4151
4360
|
if (isCursorPaginated) {
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
} else {
|
|
4361
|
+
const isMultiProbePlan = queryConfig.strategy === "multiProbe" && !!queryConfig.index;
|
|
4362
|
+
const isScanFallbackPlan = !queryConfig.index && queryConfig.postFilters.length > 0;
|
|
4363
|
+
if (isMultiProbePlan || isScanFallbackPlan) {
|
|
4156
4364
|
const schemaDefinition = this.schema[OrmSchemaDefinition];
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4365
|
+
const probeUnion = isMultiProbePlan && schemaDefinition ? this._buildProbeUnionStream({
|
|
4366
|
+
schemaDefinition,
|
|
4367
|
+
indexName: queryConfig.index.name,
|
|
4368
|
+
probeFilters: queryConfig.probeFilters,
|
|
4369
|
+
order: primaryOrder?.direction ?? "desc",
|
|
4370
|
+
orderField: primaryOrder?.field ?? queryConfig.index.fields[0]
|
|
4371
|
+
}) : null;
|
|
4372
|
+
if (!probeUnion && maxScan === void 0) {
|
|
4373
|
+
if (strict) throw new Error(isMultiProbePlan ? "Pagination with multi-probe index-union filters requires maxScan when strict=true. Add maxScan, order by a field the union can serve, or make the query indexable." : "Cursor pagination with scan fallback requires maxScan when strict=true. Add maxScan or make the query indexable.");
|
|
4374
|
+
if (isMultiProbePlan) console.warn("Pagination with multi-probe index-union filters is running without maxScan because strict: false.");
|
|
4375
|
+
}
|
|
4376
|
+
if (probeUnion || maxScan !== void 0) {
|
|
4377
|
+
if (!schemaDefinition) throw new Error("Pagination with maxScan requires defineSchema(). Ensure defineSchema(tables) was used with the same tables object passed to defineRelations.");
|
|
4378
|
+
let streamQuery = probeUnion;
|
|
4379
|
+
if (!streamQuery) {
|
|
4380
|
+
streamQuery = stream(this.db, schemaDefinition).query(this.tableConfig.name);
|
|
4381
|
+
if (isScanFallbackPlan && configuredIndex?.name) streamQuery = streamQuery.withIndex(configuredIndex.name, configuredIndex.range ? configuredIndex.range : (q) => q);
|
|
4163
4382
|
}
|
|
4164
|
-
if (
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
limit: config.limit,
|
|
4171
|
-
maxScan
|
|
4172
|
-
});
|
|
4173
|
-
let pageRows = paginationResult.page;
|
|
4174
|
-
pageRows = await this._applyRlsSelectFilter(pageRows, this.tableConfig);
|
|
4175
|
-
if (whereFilter) pageRows = await this._applyRelationsFilterToRows(pageRows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with);
|
|
4176
|
-
return {
|
|
4177
|
-
page: await this._finalizeRows(pageRows),
|
|
4178
|
-
continueCursor: paginationResult.continueCursor,
|
|
4179
|
-
isDone: paginationResult.isDone,
|
|
4180
|
-
pageStatus: paginationResult.pageStatus,
|
|
4181
|
-
splitCursor: paginationResult.splitCursor
|
|
4182
|
-
};
|
|
4183
|
-
}
|
|
4184
|
-
if (!queryConfig.index && queryConfig.postFilters.length > 0) if (maxScan === void 0) {
|
|
4185
|
-
if (strict) throw new Error("Cursor pagination with scan fallback requires maxScan when strict=true. Add maxScan or make the query indexable.");
|
|
4186
|
-
} else {
|
|
4187
|
-
const schemaDefinition = this.schema[OrmSchemaDefinition];
|
|
4188
|
-
if (!schemaDefinition) throw new Error("Pagination with maxScan requires defineSchema(). Ensure defineSchema(tables) was used with the same tables object passed to defineRelations.");
|
|
4189
|
-
let streamQuery = stream(this.db, schemaDefinition).query(this.tableConfig.name);
|
|
4190
|
-
if (configuredIndex?.name) streamQuery = streamQuery.withIndex(configuredIndex.name, configuredIndex.range ? configuredIndex.range : (q) => q);
|
|
4191
|
-
if (queryConfig.order && primaryOrder) {
|
|
4192
|
-
if (needsPostFetchSortForPrimary) {
|
|
4193
|
-
if (strict) throw new Error(`Pagination: Field '${primaryOrder.field}' has no index. Add an index or disable strict.`);
|
|
4194
|
-
console.warn(`Pagination: Field '${primaryOrder.field}' has no index. Falling back to _creationTime ordering.`);
|
|
4383
|
+
if (queryConfig.order && primaryOrder) {
|
|
4384
|
+
if (needsPostFetchSortForPrimary) {
|
|
4385
|
+
if (strict) throw new Error(`Pagination: Field '${primaryOrder.field}' has no index. Add an index or disable strict.`);
|
|
4386
|
+
console.warn(`Pagination: Field '${primaryOrder.field}' has no index. Falling back to _creationTime ordering.`);
|
|
4387
|
+
}
|
|
4388
|
+
if (hasSecondaryOrders) console.warn("Pagination: Only the first orderBy field is used for cursor ordering. Secondary orderBy fields are ignored because this page is not read through an index that orders by them.");
|
|
4195
4389
|
}
|
|
4196
|
-
if (
|
|
4197
|
-
streamQuery = streamQuery.
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
};
|
|
4390
|
+
if (!probeUnion) streamQuery = streamQuery.order(queryConfig.order && primaryOrder ? primaryOrder.direction : "desc");
|
|
4391
|
+
if (queryConfig.postFilters.length > 0) streamQuery = streamQuery.filterWith(async (row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
|
|
4392
|
+
const paginationResult = await streamQuery.paginate({
|
|
4393
|
+
cursor: cursor ?? null,
|
|
4394
|
+
limit: config.limit,
|
|
4395
|
+
maxScan
|
|
4396
|
+
});
|
|
4397
|
+
let pageRows = paginationResult.page;
|
|
4398
|
+
pageRows = await this._applyRlsSelectFilter(pageRows, this.tableConfig);
|
|
4399
|
+
if (whereFilter) pageRows = await this._applyRelationsFilterToRows(pageRows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with);
|
|
4400
|
+
return {
|
|
4401
|
+
page: await this._finalizeRows(pageRows),
|
|
4402
|
+
continueCursor: paginationResult.continueCursor,
|
|
4403
|
+
isDone: paginationResult.isDone,
|
|
4404
|
+
pageStatus: paginationResult.pageStatus,
|
|
4405
|
+
splitCursor: paginationResult.splitCursor
|
|
4406
|
+
};
|
|
4407
|
+
}
|
|
4215
4408
|
}
|
|
4216
4409
|
const convexPostFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
|
|
4217
4410
|
const hasResidualPostFilter = convexPostFilters.length !== queryConfig.postFilters.length;
|
|
@@ -4221,6 +4414,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4221
4414
|
membershipFilter: matchesPostFetchMembership,
|
|
4222
4415
|
orderIndexName,
|
|
4223
4416
|
primaryOrder,
|
|
4417
|
+
pushdownDirection: orderPushdownDirection,
|
|
4224
4418
|
fallbackOrder: "desc"
|
|
4225
4419
|
}) : null;
|
|
4226
4420
|
if (hasResidualPostFilter && !residualPageStream) this._getSchemaDefinitionOrThrow();
|
|
@@ -4228,7 +4422,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4228
4422
|
if (queryConfig.order && primaryOrder) {
|
|
4229
4423
|
if (needsPostFetchSortForPrimary && strict) throw new Error(`Pagination: Field '${primaryOrder.field}' has no index. Add an index or disable strict.`);
|
|
4230
4424
|
if (needsPostFetchSortForPrimary) console.warn(`Pagination: Field '${primaryOrder.field}' has no index. Falling back to _creationTime ordering.`);
|
|
4231
|
-
if (
|
|
4425
|
+
if (hasUnservedSecondaryOrders) console.warn("Pagination: Only the first orderBy field is used for cursor ordering. Secondary orderBy fields are ignored because no index serves the whole sort.");
|
|
4232
4426
|
}
|
|
4233
4427
|
const paginationResult = await residualPageStream.paginate({
|
|
4234
4428
|
cursor: cursor ?? null,
|
|
@@ -4251,7 +4445,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4251
4445
|
console.warn(`Pagination: Field '${primaryOrder.field}' has no index. Falling back to _creationTime ordering.`);
|
|
4252
4446
|
query = query.order(primaryOrder.direction === "asc" ? "asc" : "desc");
|
|
4253
4447
|
}
|
|
4254
|
-
if (
|
|
4448
|
+
if (hasUnservedSecondaryOrders) console.warn("Pagination: Only the first orderBy field is used for cursor ordering. Secondary orderBy fields are ignored because no index serves the whole sort.");
|
|
4255
4449
|
} else query = query.order("desc");
|
|
4256
4450
|
const paginationResult = await query.paginate({
|
|
4257
4451
|
cursor: cursor ?? null,
|
|
@@ -4281,6 +4475,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4281
4475
|
membershipFilter: matchesPostFetchMembership,
|
|
4282
4476
|
orderIndexName,
|
|
4283
4477
|
primaryOrder,
|
|
4478
|
+
pushdownDirection: orderPushdownDirection,
|
|
4284
4479
|
fallbackOrder: "asc"
|
|
4285
4480
|
}) : null;
|
|
4286
4481
|
let rows;
|