kitcn 0.31.1 → 0.32.1
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 +88 -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-Ctcw2VRq.d.ts} +52 -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 +435 -151
- 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-BHGHbfb9.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
|
|
@@ -1279,6 +1322,20 @@ const DEFAULT_RELATION_FAN_OUT_MAX_KEYS = 1e3;
|
|
|
1279
1322
|
* satisfies the limit.
|
|
1280
1323
|
*/
|
|
1281
1324
|
const RELATION_FILTER_STREAM_CHUNK = 32;
|
|
1325
|
+
/**
|
|
1326
|
+
* Marks a relation config that only exists to answer "does a matching child
|
|
1327
|
+
* exist?" for a `where` clause. The lowered load may stop at the first
|
|
1328
|
+
* surviving child, but it must not widen the window it walks to find one --
|
|
1329
|
+
* that window is what decides the answer, so changing it would change results
|
|
1330
|
+
* rather than just their cost.
|
|
1331
|
+
*
|
|
1332
|
+
* A symbol, not a field: the emitted object is fed to the same relation loader
|
|
1333
|
+
* that serves a caller's `with`, and nothing a caller can write should be able
|
|
1334
|
+
* to claim this.
|
|
1335
|
+
*/
|
|
1336
|
+
const RELATION_EXISTENCE_PROBE = Symbol("kitcn.relationExistenceProbe");
|
|
1337
|
+
/** Lowering a subtree with this set emits no existence probes anywhere in it. */
|
|
1338
|
+
const NO_PROBE_RELATIONS = /* @__PURE__ */ new Set();
|
|
1282
1339
|
const DEFAULT_AGGREGATE_CARTESIAN_MAX_KEYS = 4096;
|
|
1283
1340
|
const DEFAULT_AGGREGATE_WORK_BUDGET = 16384;
|
|
1284
1341
|
const PUBLIC_ID_FIELD = "id";
|
|
@@ -1290,6 +1347,22 @@ const RELATION_COUNT_ERROR = {
|
|
|
1290
1347
|
};
|
|
1291
1348
|
const RELATION_DEPTH_ERROR = "RELATION_DEPTH_EXCEEDED";
|
|
1292
1349
|
/**
|
|
1350
|
+
* Whether a compiled column validator admits an absent or null value.
|
|
1351
|
+
*
|
|
1352
|
+
* `v.optional(...)` is how a non-`.notNull()` column is emitted, and absent is
|
|
1353
|
+
* a legal stored value for every document written before the column existed, so
|
|
1354
|
+
* both halves matter. Unrecognized shapes answer true: the only caller uses
|
|
1355
|
+
* this to decide whether it may skip a sort, and guessing "never null" there
|
|
1356
|
+
* silently reorders rows.
|
|
1357
|
+
*/
|
|
1358
|
+
const validatorAcceptsMissingOrNull = (validator) => {
|
|
1359
|
+
if (!validator) return true;
|
|
1360
|
+
if (validator.isOptional === "optional") return true;
|
|
1361
|
+
if (validator.kind === "null" || validator.kind === "any") return true;
|
|
1362
|
+
if (validator.kind === "union") return (validator.members ?? []).some(validatorAcceptsMissingOrNull);
|
|
1363
|
+
return false;
|
|
1364
|
+
};
|
|
1365
|
+
/**
|
|
1293
1366
|
* How many levels of `with` the relation loader will follow. A `with` config is
|
|
1294
1367
|
* a finite object the caller wrote, so its own nesting is the real bound; this
|
|
1295
1368
|
* ceiling only stops a self-referential config from recursing forever. Reaching
|
|
@@ -1712,18 +1785,41 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
1712
1785
|
return (Array.isArray(orderBy) ? orderBy : [orderBy]).filter((item) => item !== void 0).map((item) => this._normalizeOrderByValue(item));
|
|
1713
1786
|
}
|
|
1714
1787
|
_orderBySpecs(orderBy, tableConfig = this.tableConfig) {
|
|
1788
|
+
const withNullability = (spec) => ({
|
|
1789
|
+
...spec,
|
|
1790
|
+
nullable: this._isNullableOrderField(spec.field, tableConfig)
|
|
1791
|
+
});
|
|
1715
1792
|
if (orderBy && typeof orderBy === "object" && !Array.isArray(orderBy) && !this._isOrderByClause(orderBy) && !this._isColumnBuilder(orderBy)) return Object.entries(orderBy).map(([field, direction]) => ({
|
|
1716
1793
|
field: this._normalizePublicFieldName(field, tableConfig),
|
|
1717
1794
|
direction
|
|
1718
|
-
})).filter((entry) => entry.direction === "asc" || entry.direction === "desc").map((entry) => ({
|
|
1795
|
+
})).filter((entry) => entry.direction === "asc" || entry.direction === "desc").map((entry) => withNullability({
|
|
1719
1796
|
field: entry.field,
|
|
1720
1797
|
direction: entry.direction
|
|
1721
1798
|
}));
|
|
1722
|
-
return this._normalizeOrderBy(orderBy).map((clause) => ({
|
|
1799
|
+
return this._normalizeOrderBy(orderBy).map((clause) => withNullability({
|
|
1723
1800
|
field: clause.column.columnName,
|
|
1724
1801
|
direction: clause.direction
|
|
1725
1802
|
}));
|
|
1726
1803
|
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Whether rows can be missing this sort column or hold null in it.
|
|
1806
|
+
*
|
|
1807
|
+
* Only the declared validator can answer: a column that is not `.notNull()`
|
|
1808
|
+
* compiles to `v.optional(v.union(v.null(), T))`, so both absent and null are
|
|
1809
|
+
* legal stored values — and `timestamp().notNull().defaultNow()` on
|
|
1810
|
+
* `createdAt` is deliberately emitted as optional for migration safety, so
|
|
1811
|
+
* reading `config.notNull` alone would answer wrong for it. Anything the
|
|
1812
|
+
* lookup cannot resolve counts as nullable, which only costs a pushdown.
|
|
1813
|
+
*/
|
|
1814
|
+
_isNullableOrderField(field, tableConfig) {
|
|
1815
|
+
if (field === INTERNAL_ID_FIELD || field === INTERNAL_CREATION_TIME_FIELD) return false;
|
|
1816
|
+
try {
|
|
1817
|
+
const builder = this._getColumns(tableConfig)[field];
|
|
1818
|
+
return validatorAcceptsMissingOrNull(builder?.build?.());
|
|
1819
|
+
} catch {
|
|
1820
|
+
return true;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1727
1823
|
_resolveNonPaginatedLimit(config) {
|
|
1728
1824
|
const explicitLimit = config.limit;
|
|
1729
1825
|
const defaultLimit = getOrmContext(this.db)?.resolvedDefaults?.defaultLimit ?? this.tableConfig.defaults?.defaultLimit;
|
|
@@ -1744,8 +1840,8 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
1744
1840
|
return 1;
|
|
1745
1841
|
}
|
|
1746
1842
|
if (bVal === null || bVal === void 0) return -1;
|
|
1747
|
-
|
|
1748
|
-
if (
|
|
1843
|
+
const comparison = compareValues(aVal, bVal);
|
|
1844
|
+
if (comparison !== 0) return order.direction === "asc" ? comparison : -comparison;
|
|
1749
1845
|
}
|
|
1750
1846
|
return 0;
|
|
1751
1847
|
}
|
|
@@ -2309,7 +2405,47 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2309
2405
|
}
|
|
2310
2406
|
}
|
|
2311
2407
|
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Relation keys the filter tree mentions exactly once, across every OR/AND/NOT
|
|
2410
|
+
* branch at this table level.
|
|
2411
|
+
*
|
|
2412
|
+
* `_mergeWithConfig` collapses the whole tree into one load per relation key,
|
|
2413
|
+
* so a key two branches disagree about has to be loaded the way both branches
|
|
2414
|
+
* can read. Only a key with a single occurrence has one predicate to satisfy,
|
|
2415
|
+
* and only then can that predicate be pushed into the read plan.
|
|
2416
|
+
*
|
|
2417
|
+
* Relation values are not descended into: they belong to the target table and
|
|
2418
|
+
* get their own count when that level is lowered.
|
|
2419
|
+
*/
|
|
2420
|
+
_collectSingleOccurrenceRelations(filter, tableConfig) {
|
|
2421
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2422
|
+
const walk = (node) => {
|
|
2423
|
+
if (!this._isRecord(node)) return;
|
|
2424
|
+
for (const [key, value] of Object.entries(node)) {
|
|
2425
|
+
if (value === void 0) continue;
|
|
2426
|
+
if (key === "OR" || key === "AND") {
|
|
2427
|
+
if (!Array.isArray(value)) continue;
|
|
2428
|
+
for (const sub of value) walk(sub);
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
if (key === "NOT") {
|
|
2432
|
+
walk(value);
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2435
|
+
if (!tableConfig.relations[key]) continue;
|
|
2436
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2437
|
+
}
|
|
2438
|
+
};
|
|
2439
|
+
walk(filter);
|
|
2440
|
+
const single = /* @__PURE__ */ new Set();
|
|
2441
|
+
for (const [key, count] of counts) if (count === 1) single.add(key);
|
|
2442
|
+
return single;
|
|
2443
|
+
}
|
|
2312
2444
|
_buildFilterWithConfig(filter, tableConfig) {
|
|
2445
|
+
if (!this._isRecord(filter)) return {};
|
|
2446
|
+
return this._buildFilterWithConfigForLevel(filter, tableConfig, this._collectSingleOccurrenceRelations(filter, tableConfig));
|
|
2447
|
+
}
|
|
2448
|
+
_buildFilterWithConfigForLevel(filter, tableConfig, probeEligible) {
|
|
2313
2449
|
if (!this._isRecord(filter)) return {};
|
|
2314
2450
|
const result = {};
|
|
2315
2451
|
const entries = Object.entries(filter);
|
|
@@ -2319,27 +2455,36 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2319
2455
|
if (key === "OR" || key === "AND") {
|
|
2320
2456
|
if (!Array.isArray(value) || value.length === 0) continue;
|
|
2321
2457
|
for (const sub of value) {
|
|
2322
|
-
const nested = this.
|
|
2458
|
+
const nested = this._buildFilterWithConfigForLevel(sub, tableConfig, probeEligible);
|
|
2323
2459
|
this._mergeWithConfig(result, nested);
|
|
2324
2460
|
}
|
|
2325
2461
|
continue;
|
|
2326
2462
|
}
|
|
2327
2463
|
if (key === "NOT") {
|
|
2328
|
-
const nested = this.
|
|
2464
|
+
const nested = this._buildFilterWithConfigForLevel(value, tableConfig, probeEligible);
|
|
2329
2465
|
this._mergeWithConfig(result, nested);
|
|
2330
2466
|
continue;
|
|
2331
2467
|
}
|
|
2332
2468
|
this._assertNoLegacyPublicFieldName(key);
|
|
2333
2469
|
const relation = tableConfig.relations[key];
|
|
2334
2470
|
if (!relation) continue;
|
|
2471
|
+
const probe = relation.relationType !== "one" && probeEligible.has(key) ? { [RELATION_EXISTENCE_PROBE]: true } : void 0;
|
|
2335
2472
|
if (typeof value === "boolean") {
|
|
2336
|
-
result[key] = true;
|
|
2473
|
+
result[key] = probe ?? true;
|
|
2337
2474
|
continue;
|
|
2338
2475
|
}
|
|
2339
2476
|
const targetTableConfig = this._getTableConfigByDbName(relation.targetTableName);
|
|
2340
2477
|
if (!targetTableConfig) continue;
|
|
2341
|
-
const nested = this.
|
|
2342
|
-
|
|
2478
|
+
const nested = this._buildFilterWithConfigForLevel(value, targetTableConfig, probe ? this._collectSingleOccurrenceRelations(value, targetTableConfig) : NO_PROBE_RELATIONS);
|
|
2479
|
+
const hasNested = Object.keys(nested).length > 0;
|
|
2480
|
+
if (!probe) {
|
|
2481
|
+
result[key] = hasNested ? { with: nested } : true;
|
|
2482
|
+
continue;
|
|
2483
|
+
}
|
|
2484
|
+
const emitted = { ...probe };
|
|
2485
|
+
if (Object.keys(value).length > 0) emitted.where = value;
|
|
2486
|
+
if (hasNested) emitted.with = nested;
|
|
2487
|
+
result[key] = emitted;
|
|
2343
2488
|
}
|
|
2344
2489
|
return result;
|
|
2345
2490
|
}
|
|
@@ -2675,28 +2820,99 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2675
2820
|
_findStreamOrderIndex(field) {
|
|
2676
2821
|
return getIndexes(this.tableConfig.table).find((index) => index.fields[0] === field);
|
|
2677
2822
|
}
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2823
|
+
/**
|
|
2824
|
+
* The compiled index union, as one ordered stream.
|
|
2825
|
+
*
|
|
2826
|
+
* Each probe is its own index range, so the union is only usable where the
|
|
2827
|
+
* merged order is the order the read has to produce. `mergedStream` orders by
|
|
2828
|
+
* a *suffix* of the index key, and a suffix may only drop key components the
|
|
2829
|
+
* probes all pin to a single value — so the requested field has to sit inside
|
|
2830
|
+
* the pinned run or immediately after it. Returns null when it does not, and
|
|
2831
|
+
* the caller falls back to the plan's plain index range or a bounded scan.
|
|
2832
|
+
*/
|
|
2833
|
+
_buildProbeUnionStream(params) {
|
|
2834
|
+
const { probeFilters } = params;
|
|
2835
|
+
if (probeFilters.length === 0 || probeFilters.length > MAX_INDEX_UNION_PROBES) return null;
|
|
2836
|
+
const streamIndexFields = getIndexFields(this.tableConfig.name, params.indexName, params.schemaDefinition);
|
|
2837
|
+
const pinned = this._indexEqPrefixCount({ probeFilters });
|
|
2838
|
+
let mergeOffset = -1;
|
|
2839
|
+
for (let i = 0; i <= pinned && i < streamIndexFields.length; i += 1) if (streamIndexFields[i] === params.orderField) {
|
|
2840
|
+
mergeOffset = i;
|
|
2841
|
+
break;
|
|
2842
|
+
}
|
|
2843
|
+
if (mergeOffset === -1) return null;
|
|
2844
|
+
return mergedStream(probeFilters.map((filters) => stream(this.db, params.schemaDefinition).query(this.tableConfig.name).withIndex(params.indexName, (q) => {
|
|
2845
|
+
let indexQuery = q;
|
|
2846
|
+
for (const filter of filters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
2847
|
+
return indexQuery;
|
|
2848
|
+
}).order(params.order)), streamIndexFields.slice(mergeOffset));
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* The read the compiled plan describes, as a stream, with nothing filtered
|
|
2852
|
+
* yet.
|
|
2853
|
+
*
|
|
2854
|
+
* Precedence: the compiled index union, then the compiled index range, then
|
|
2855
|
+
* the caller's pinned `.withIndex(...)`, then an index that supplies the
|
|
2856
|
+
* requested order, then a full scan. The first two rungs can only ever refine
|
|
2857
|
+
* what the caller pinned — `_toConvexQuery` discards a compiled plan that
|
|
2858
|
+
* would displace a caller's index or its bounds before it gets here.
|
|
2859
|
+
*
|
|
2860
|
+
* `probeUnion` tells the caller the read is bounded by index ranges rather
|
|
2861
|
+
* than by scan length, which is what makes a scan budget unnecessary. A
|
|
2862
|
+
* rejected union stays unanchored; its original predicate remains in
|
|
2863
|
+
* `queryConfig.postFilters` for the caller to apply while pulling the scan.
|
|
2864
|
+
*/
|
|
2865
|
+
_buildPlanStream(params) {
|
|
2866
|
+
const { queryConfig, configuredIndex, order, primaryOrder } = params;
|
|
2867
|
+
const probeIndex = queryConfig.probeFilters.length > 0 ? queryConfig.index : void 0;
|
|
2868
|
+
const hasProbeUnionPlan = !!probeIndex;
|
|
2869
|
+
if (hasProbeUnionPlan) {
|
|
2870
|
+
const probeUnion = this._buildProbeUnionStream({
|
|
2871
|
+
schemaDefinition: params.schemaDefinition,
|
|
2872
|
+
indexName: probeIndex.name,
|
|
2873
|
+
probeFilters: queryConfig.probeFilters,
|
|
2874
|
+
order,
|
|
2875
|
+
orderField: primaryOrder?.field ?? probeIndex.fields[0]
|
|
2876
|
+
});
|
|
2877
|
+
if (probeUnion) return {
|
|
2878
|
+
stream: probeUnion,
|
|
2879
|
+
probeUnion: true
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
let streamQuery = stream(this.db, params.schemaDefinition).query(this.tableConfig.name);
|
|
2883
|
+
if (queryConfig.index && !hasProbeUnionPlan) streamQuery = streamQuery.withIndex(queryConfig.index.name, (q) => {
|
|
2684
2884
|
let indexQuery = q;
|
|
2685
2885
|
for (const filter of queryConfig.index.filters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
2686
2886
|
return indexQuery;
|
|
2687
2887
|
});
|
|
2688
|
-
else if (configuredIndex?.name) streamQuery = streamQuery.withIndex(configuredIndex.name, configuredIndex.range ? configuredIndex.range : (q) => q);
|
|
2689
|
-
else if (
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2888
|
+
else if (!hasProbeUnionPlan && configuredIndex?.name) streamQuery = streamQuery.withIndex(configuredIndex.name, configuredIndex.range ? configuredIndex.range : (q) => q);
|
|
2889
|
+
else if (!hasProbeUnionPlan && params.orderIndexName) streamQuery = streamQuery.withIndex(params.orderIndexName, (q) => q);
|
|
2890
|
+
return {
|
|
2891
|
+
stream: streamQuery.order(order),
|
|
2892
|
+
probeUnion: false
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
_buildBasePipelineStream(queryConfig, wherePredicate, configuredIndex, fallbackOrder) {
|
|
2896
|
+
const schemaDefinition = this._getSchemaDefinitionOrThrow();
|
|
2897
|
+
const primaryOrder = queryConfig.order?.[0];
|
|
2898
|
+
const planStream = this._buildPlanStream({
|
|
2899
|
+
queryConfig,
|
|
2900
|
+
configuredIndex,
|
|
2901
|
+
order: primaryOrder?.direction ?? fallbackOrder,
|
|
2902
|
+
primaryOrder,
|
|
2903
|
+
orderIndexName: primaryOrder && primaryOrder.field !== INTERNAL_CREATION_TIME_FIELD ? this._findStreamOrderIndex(primaryOrder.field)?.name ?? null : null,
|
|
2904
|
+
schemaDefinition
|
|
2905
|
+
});
|
|
2906
|
+
let streamQuery = planStream.stream;
|
|
2694
2907
|
if (queryConfig.postFilters.length > 0 || wherePredicate) streamQuery = streamQuery.filterWith(async (row) => {
|
|
2695
2908
|
for (const filter of queryConfig.postFilters) if (!this._evaluatePostFetchFilter(row, filter)) return false;
|
|
2696
2909
|
if (wherePredicate) return await wherePredicate(row);
|
|
2697
2910
|
return true;
|
|
2698
2911
|
});
|
|
2699
|
-
return
|
|
2912
|
+
return {
|
|
2913
|
+
stream: streamQuery,
|
|
2914
|
+
probeUnion: planStream.probeUnion
|
|
2915
|
+
};
|
|
2700
2916
|
}
|
|
2701
2917
|
/**
|
|
2702
2918
|
* Stream equivalent of the `db.query(...)` chain, used when a post-fetch
|
|
@@ -2714,16 +2930,15 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2714
2930
|
_buildResidualFilterStream(params) {
|
|
2715
2931
|
const schemaDefinition = this.schema[OrmSchemaDefinition];
|
|
2716
2932
|
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);
|
|
2933
|
+
const { queryConfig, configuredIndex, membershipFilter, orderIndexName, primaryOrder, pushdownDirection, fallbackOrder } = params;
|
|
2934
|
+
let streamQuery = this._buildPlanStream({
|
|
2935
|
+
queryConfig,
|
|
2936
|
+
configuredIndex,
|
|
2937
|
+
order: pushdownDirection ?? (primaryOrder ? primaryOrder.direction : fallbackOrder),
|
|
2938
|
+
primaryOrder,
|
|
2939
|
+
orderIndexName,
|
|
2940
|
+
schemaDefinition
|
|
2941
|
+
}).stream;
|
|
2727
2942
|
streamQuery = streamQuery.filterWith(async (row) => {
|
|
2728
2943
|
for (const filter of queryConfig.postFilters) if (!this._evaluatePostFetchFilter(row, filter)) return false;
|
|
2729
2944
|
return membershipFilter ? await membershipFilter(row) : true;
|
|
@@ -3876,7 +4091,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
3876
4091
|
if (endCursor !== void 0 && searchConfig) throw new Error("endCursor is not supported with search in findMany().");
|
|
3877
4092
|
if (endCursor !== void 0 && vectorSearchConfig) throw new Error("endCursor is not supported with vectorSearch in findMany().");
|
|
3878
4093
|
if (useAdvancedStreamPath) {
|
|
3879
|
-
const fallbackOrder = (queryConfig.order?.[0])?.direction ?? "asc";
|
|
4094
|
+
const fallbackOrder = (queryConfig.order?.[0])?.direction ?? (endCursor !== void 0 && !pipeline ? "desc" : "asc");
|
|
3880
4095
|
let streamQuery;
|
|
3881
4096
|
const unionSources = pipeline?.union ?? [];
|
|
3882
4097
|
if (unionSources.length > 0) {
|
|
@@ -3892,13 +4107,22 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
3892
4107
|
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
4108
|
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
4109
|
}
|
|
3895
|
-
|
|
4110
|
+
const idLookupStream = await this._buildIdLookupStream({
|
|
3896
4111
|
configuredIndex,
|
|
3897
4112
|
idLookup,
|
|
3898
4113
|
order: fallbackOrder,
|
|
3899
4114
|
queryConfig,
|
|
3900
4115
|
wherePredicate
|
|
3901
|
-
})
|
|
4116
|
+
});
|
|
4117
|
+
if (idLookupStream) streamQuery = idLookupStream;
|
|
4118
|
+
else {
|
|
4119
|
+
const baseStream = this._buildBasePipelineStream(queryConfig, wherePredicate, configuredIndex, fallbackOrder);
|
|
4120
|
+
if (isCursorPaginated && queryConfig.strategy === "multiProbe" && !!queryConfig.index && !baseStream.probeUnion && maxScan === void 0) {
|
|
4121
|
+
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.");
|
|
4122
|
+
console.warn("Pagination with multi-probe index-union filters is running without maxScan because strict: false.");
|
|
4123
|
+
}
|
|
4124
|
+
streamQuery = baseStream.stream;
|
|
4125
|
+
}
|
|
3902
4126
|
}
|
|
3903
4127
|
const rootRlsEnabled = isRlsEnabled(this.tableConfig.table);
|
|
3904
4128
|
if (rootRlsEnabled) {
|
|
@@ -4014,6 +4238,37 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4014
4238
|
const primaryOrder = postFetchOrders[0];
|
|
4015
4239
|
const hasSecondaryOrders = postFetchOrders.length > 1;
|
|
4016
4240
|
let orderIndexName = null;
|
|
4241
|
+
/**
|
|
4242
|
+
* Direction the scanned index can be walked in to produce the *whole*
|
|
4243
|
+
* requested sort, or null when some of it still has to run in JavaScript.
|
|
4244
|
+
* This is the read bound: a non-null answer means `take(limit)` is the
|
|
4245
|
+
* exact page.
|
|
4246
|
+
*/
|
|
4247
|
+
let orderPushdownDirection = null;
|
|
4248
|
+
/**
|
|
4249
|
+
* Ask the one owner both questions an index has to answer.
|
|
4250
|
+
*
|
|
4251
|
+
* `whole` decides the read bound. `primary` is narrower and only feeds the
|
|
4252
|
+
* cursor-pagination diagnostics, which are about the leading sort field
|
|
4253
|
+
* having an index at all — an index that anchors the primary but not the
|
|
4254
|
+
* tie-break is still the right index to scan, and must not start reporting
|
|
4255
|
+
* itself as unindexed.
|
|
4256
|
+
*/
|
|
4257
|
+
const resolveOrderForIndex = (indexFields, pinnedEqCount) => {
|
|
4258
|
+
const whole = resolveIndexOrderPushdown({
|
|
4259
|
+
indexFields,
|
|
4260
|
+
pinnedEqCount,
|
|
4261
|
+
orderSpecs: postFetchOrders
|
|
4262
|
+
});
|
|
4263
|
+
return {
|
|
4264
|
+
whole,
|
|
4265
|
+
primary: whole ?? (primaryOrder ? resolveIndexOrderPushdown({
|
|
4266
|
+
indexFields,
|
|
4267
|
+
pinnedEqCount,
|
|
4268
|
+
orderSpecs: [primaryOrder]
|
|
4269
|
+
}) : null)
|
|
4270
|
+
};
|
|
4271
|
+
};
|
|
4017
4272
|
if (queryConfig.index) {
|
|
4018
4273
|
const indexConfig = queryConfig.index;
|
|
4019
4274
|
query = query.withIndex(indexConfig.name, (q) => {
|
|
@@ -4022,12 +4277,9 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4022
4277
|
return indexQuery;
|
|
4023
4278
|
});
|
|
4024
4279
|
if (primaryOrder) {
|
|
4025
|
-
const
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
orderSpecs: [primaryOrder]
|
|
4029
|
-
});
|
|
4030
|
-
if (pushdownDirection) query = query.order(pushdownDirection);
|
|
4280
|
+
const resolved = resolveOrderForIndex(indexConfig.fields, this._indexEqPrefixCount(queryConfig));
|
|
4281
|
+
orderPushdownDirection = resolved.whole;
|
|
4282
|
+
if (resolved.primary) query = query.order(resolved.primary);
|
|
4031
4283
|
else needsPostFetchSortForPrimary = true;
|
|
4032
4284
|
}
|
|
4033
4285
|
} else if (configuredIndex?.name) {
|
|
@@ -4036,27 +4288,51 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4036
4288
|
const configuredRange = configuredIndex.range;
|
|
4037
4289
|
query = query.withIndex(configuredIndex.name, configuredRange ? (q) => configuredRange(observeConfiguredIndexRange(q, rangeOperations)) : (q) => q);
|
|
4038
4290
|
if (primaryOrder) {
|
|
4039
|
-
const
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
orderSpecs: [primaryOrder]
|
|
4043
|
-
});
|
|
4044
|
-
if (pushdownDirection) query = query.order(pushdownDirection);
|
|
4291
|
+
const resolved = resolveOrderForIndex(configuredIndexFields, countConfiguredIndexEqPrefix(configuredIndexFields, rangeOperations));
|
|
4292
|
+
orderPushdownDirection = resolved.whole;
|
|
4293
|
+
if (resolved.primary) query = query.order(resolved.primary);
|
|
4045
4294
|
else needsPostFetchSortForPrimary = true;
|
|
4046
4295
|
}
|
|
4047
4296
|
} else if (queryConfig.order && primaryOrder) {
|
|
4048
4297
|
const orderField = primaryOrder.field;
|
|
4049
|
-
if (orderField ===
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4298
|
+
if (orderField === INTERNAL_CREATION_TIME_FIELD) {
|
|
4299
|
+
query = query.order(primaryOrder.direction);
|
|
4300
|
+
orderPushdownDirection = resolveIndexOrderPushdown({
|
|
4301
|
+
indexFields: [],
|
|
4302
|
+
pinnedEqCount: 0,
|
|
4303
|
+
orderSpecs: postFetchOrders
|
|
4304
|
+
});
|
|
4305
|
+
} else {
|
|
4306
|
+
const candidates = [...getIndexes(this.tableConfig.table).map((idx) => ({
|
|
4307
|
+
fields: idx.fields,
|
|
4308
|
+
name: idx.name
|
|
4309
|
+
})), ...this.edgeMetadata.map((idx) => ({
|
|
4310
|
+
fields: idx.indexFields,
|
|
4311
|
+
name: idx.indexName
|
|
4312
|
+
}))];
|
|
4313
|
+
let selected;
|
|
4314
|
+
for (const candidate of candidates) {
|
|
4315
|
+
const direction = resolveIndexOrderPushdown({
|
|
4316
|
+
indexFields: candidate.fields,
|
|
4317
|
+
pinnedEqCount: 0,
|
|
4318
|
+
orderSpecs: postFetchOrders
|
|
4319
|
+
});
|
|
4320
|
+
if (direction) {
|
|
4321
|
+
selected = candidate;
|
|
4322
|
+
orderPushdownDirection = direction;
|
|
4323
|
+
break;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
selected ??= candidates.find((candidate) => candidate.fields[0] === orderField);
|
|
4327
|
+
if (selected) {
|
|
4328
|
+
orderIndexName = selected.name;
|
|
4054
4329
|
query = query.withIndex(orderIndexName, (q) => q);
|
|
4055
|
-
query = query.order(primaryOrder.direction);
|
|
4330
|
+
query = query.order(orderPushdownDirection ?? primaryOrder.direction);
|
|
4056
4331
|
} else needsPostFetchSortForPrimary = true;
|
|
4057
4332
|
}
|
|
4058
4333
|
}
|
|
4059
|
-
usePostFetchSort =
|
|
4334
|
+
usePostFetchSort = postFetchOrders.length > 0 && orderPushdownDirection === null;
|
|
4335
|
+
const hasUnservedSecondaryOrders = hasSecondaryOrders && orderPushdownDirection === null;
|
|
4060
4336
|
if (wherePredicate) {
|
|
4061
4337
|
const predicateIndex = configuredIndex;
|
|
4062
4338
|
if (!predicateIndex?.name) throw new Error("predicate(...) requires .withIndex(name, range?) on the query.");
|
|
@@ -4068,9 +4344,9 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4068
4344
|
if (strict) throw new Error(`Pagination: Field '${primaryOrder?.field}' has no index. Add an index or disable strict.`);
|
|
4069
4345
|
console.warn(`Pagination: Field '${primaryOrder?.field}' has no index. Falling back to _creationTime ordering.`);
|
|
4070
4346
|
}
|
|
4071
|
-
if (
|
|
4347
|
+
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
4348
|
}
|
|
4073
|
-
if (primaryOrder && !needsPostFetchSortForPrimary) streamQuery = streamQuery.order(primaryOrder.direction);
|
|
4349
|
+
if (primaryOrder && !needsPostFetchSortForPrimary) streamQuery = streamQuery.order(orderPushdownDirection ?? primaryOrder.direction);
|
|
4074
4350
|
else if (isCursorPaginated) streamQuery = streamQuery.order("desc");
|
|
4075
4351
|
streamQuery = streamQuery.filterWith(async (row) => {
|
|
4076
4352
|
for (const filter of queryConfig.postFilters) if (!this._evaluatePostFetchFilter(row, filter)) return false;
|
|
@@ -4116,12 +4392,8 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4116
4392
|
const convexProbeFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
|
|
4117
4393
|
const probeHasResidualFilter = convexProbeFilters.length !== queryConfig.postFilters.length;
|
|
4118
4394
|
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;
|
|
4395
|
+
const probeOrderDirection = orderPushdownDirection;
|
|
4396
|
+
const probeBound = probeLimit !== void 0 && !probeHasResidualFilter && !probeHasPostFetchMembership && (postFetchOrders.length === 0 || probeOrderDirection !== null) ? probeOffset + probeLimit : void 0;
|
|
4125
4397
|
const probeRows = await Promise.all(queryConfig.probeFilters.map(async (probeFilters) => {
|
|
4126
4398
|
let probeQuery = this.db.query(queryConfig.table).withIndex(queryConfig.index.name, (q) => {
|
|
4127
4399
|
let indexQuery = q;
|
|
@@ -4149,69 +4421,53 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4149
4421
|
return (await this._applyRelationsFilterToRows(visibleRows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with)).length > 0;
|
|
4150
4422
|
};
|
|
4151
4423
|
if (isCursorPaginated) {
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
} else {
|
|
4424
|
+
const isMultiProbePlan = queryConfig.strategy === "multiProbe" && !!queryConfig.index;
|
|
4425
|
+
const isScanFallbackPlan = !queryConfig.index && queryConfig.postFilters.length > 0;
|
|
4426
|
+
if (isMultiProbePlan || isScanFallbackPlan) {
|
|
4156
4427
|
const schemaDefinition = this.schema[OrmSchemaDefinition];
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4428
|
+
const probeUnion = isMultiProbePlan && schemaDefinition ? this._buildProbeUnionStream({
|
|
4429
|
+
schemaDefinition,
|
|
4430
|
+
indexName: queryConfig.index.name,
|
|
4431
|
+
probeFilters: queryConfig.probeFilters,
|
|
4432
|
+
order: primaryOrder?.direction ?? "desc",
|
|
4433
|
+
orderField: primaryOrder?.field ?? queryConfig.index.fields[0]
|
|
4434
|
+
}) : null;
|
|
4435
|
+
if (!probeUnion && maxScan === void 0) {
|
|
4436
|
+
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.");
|
|
4437
|
+
if (isMultiProbePlan) console.warn("Pagination with multi-probe index-union filters is running without maxScan because strict: false.");
|
|
4438
|
+
}
|
|
4439
|
+
if (probeUnion || maxScan !== void 0) {
|
|
4440
|
+
if (!schemaDefinition) throw new Error("Pagination with maxScan requires defineSchema(). Ensure defineSchema(tables) was used with the same tables object passed to defineRelations.");
|
|
4441
|
+
let streamQuery = probeUnion;
|
|
4442
|
+
if (!streamQuery) {
|
|
4443
|
+
streamQuery = stream(this.db, schemaDefinition).query(this.tableConfig.name);
|
|
4444
|
+
if (isScanFallbackPlan && configuredIndex?.name) streamQuery = streamQuery.withIndex(configuredIndex.name, configuredIndex.range ? configuredIndex.range : (q) => q);
|
|
4163
4445
|
}
|
|
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.`);
|
|
4446
|
+
if (queryConfig.order && primaryOrder) {
|
|
4447
|
+
if (needsPostFetchSortForPrimary) {
|
|
4448
|
+
if (strict) throw new Error(`Pagination: Field '${primaryOrder.field}' has no index. Add an index or disable strict.`);
|
|
4449
|
+
console.warn(`Pagination: Field '${primaryOrder.field}' has no index. Falling back to _creationTime ordering.`);
|
|
4450
|
+
}
|
|
4451
|
+
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
4452
|
}
|
|
4196
|
-
if (
|
|
4197
|
-
streamQuery = streamQuery.
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
};
|
|
4453
|
+
if (!probeUnion) streamQuery = streamQuery.order(queryConfig.order && primaryOrder ? primaryOrder.direction : "desc");
|
|
4454
|
+
if (queryConfig.postFilters.length > 0) streamQuery = streamQuery.filterWith(async (row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
|
|
4455
|
+
const paginationResult = await streamQuery.paginate({
|
|
4456
|
+
cursor: cursor ?? null,
|
|
4457
|
+
limit: config.limit,
|
|
4458
|
+
maxScan
|
|
4459
|
+
});
|
|
4460
|
+
let pageRows = paginationResult.page;
|
|
4461
|
+
pageRows = await this._applyRlsSelectFilter(pageRows, this.tableConfig);
|
|
4462
|
+
if (whereFilter) pageRows = await this._applyRelationsFilterToRows(pageRows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with);
|
|
4463
|
+
return {
|
|
4464
|
+
page: await this._finalizeRows(pageRows),
|
|
4465
|
+
continueCursor: paginationResult.continueCursor,
|
|
4466
|
+
isDone: paginationResult.isDone,
|
|
4467
|
+
pageStatus: paginationResult.pageStatus,
|
|
4468
|
+
splitCursor: paginationResult.splitCursor
|
|
4469
|
+
};
|
|
4470
|
+
}
|
|
4215
4471
|
}
|
|
4216
4472
|
const convexPostFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
|
|
4217
4473
|
const hasResidualPostFilter = convexPostFilters.length !== queryConfig.postFilters.length;
|
|
@@ -4221,6 +4477,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4221
4477
|
membershipFilter: matchesPostFetchMembership,
|
|
4222
4478
|
orderIndexName,
|
|
4223
4479
|
primaryOrder,
|
|
4480
|
+
pushdownDirection: orderPushdownDirection,
|
|
4224
4481
|
fallbackOrder: "desc"
|
|
4225
4482
|
}) : null;
|
|
4226
4483
|
if (hasResidualPostFilter && !residualPageStream) this._getSchemaDefinitionOrThrow();
|
|
@@ -4228,7 +4485,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4228
4485
|
if (queryConfig.order && primaryOrder) {
|
|
4229
4486
|
if (needsPostFetchSortForPrimary && strict) throw new Error(`Pagination: Field '${primaryOrder.field}' has no index. Add an index or disable strict.`);
|
|
4230
4487
|
if (needsPostFetchSortForPrimary) console.warn(`Pagination: Field '${primaryOrder.field}' has no index. Falling back to _creationTime ordering.`);
|
|
4231
|
-
if (
|
|
4488
|
+
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
4489
|
}
|
|
4233
4490
|
const paginationResult = await residualPageStream.paginate({
|
|
4234
4491
|
cursor: cursor ?? null,
|
|
@@ -4251,7 +4508,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4251
4508
|
console.warn(`Pagination: Field '${primaryOrder.field}' has no index. Falling back to _creationTime ordering.`);
|
|
4252
4509
|
query = query.order(primaryOrder.direction === "asc" ? "asc" : "desc");
|
|
4253
4510
|
}
|
|
4254
|
-
if (
|
|
4511
|
+
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
4512
|
} else query = query.order("desc");
|
|
4256
4513
|
const paginationResult = await query.paginate({
|
|
4257
4514
|
cursor: cursor ?? null,
|
|
@@ -4281,6 +4538,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4281
4538
|
membershipFilter: matchesPostFetchMembership,
|
|
4282
4539
|
orderIndexName,
|
|
4283
4540
|
primaryOrder,
|
|
4541
|
+
pushdownDirection: orderPushdownDirection,
|
|
4284
4542
|
fallbackOrder: "asc"
|
|
4285
4543
|
}) : null;
|
|
4286
4544
|
let rows;
|
|
@@ -4760,9 +5018,10 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4760
5018
|
* parents in the round.
|
|
4761
5019
|
*/
|
|
4762
5020
|
async _readBoundedThroughLinks(params) {
|
|
4763
|
-
const {
|
|
5021
|
+
const { applyAmbientTargetFilters, applyConfigTargetFilter, edge, enforceTargetKeyCap, entries, fetchLimit, fetchTargets, scanLimit, targetFields, throughIndexName, throughTableConfig } = params;
|
|
4764
5022
|
const throughTargetFields = edge.through.targetFields;
|
|
4765
5023
|
const cursors = entries.map(([key, values]) => ({
|
|
5024
|
+
ambientLinks: 0,
|
|
4766
5025
|
buffered: [],
|
|
4767
5026
|
exhausted: false,
|
|
4768
5027
|
iterator: this._queryByFields(this.db.query(edge.through.table), edge.through.sourceFields, values, throughIndexName)[Symbol.asyncIterator](),
|
|
@@ -4789,10 +5048,12 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4789
5048
|
};
|
|
4790
5049
|
/** Target key -> the surviving document, absent when it did not survive. */
|
|
4791
5050
|
const survivorByKey = /* @__PURE__ */ new Map();
|
|
5051
|
+
/** Target keys that cleared the ambient filters, survivors or not. */
|
|
5052
|
+
const ambientKeys = /* @__PURE__ */ new Set();
|
|
4792
5053
|
const resolvedKeys = /* @__PURE__ */ new Set();
|
|
4793
5054
|
const survivors = [];
|
|
4794
5055
|
while (true) {
|
|
4795
|
-
const active = cursors.filter((cursor) => cursor.links.length < fetchLimit && !(cursor.exhausted && cursor.buffered.length === 0));
|
|
5056
|
+
const active = cursors.filter((cursor) => cursor.links.length < fetchLimit && (scanLimit === void 0 || cursor.ambientLinks < scanLimit) && !(cursor.exhausted && cursor.buffered.length === 0));
|
|
4796
5057
|
if (active.length === 0) break;
|
|
4797
5058
|
const candidatesPerCursor = await this._mapWithConcurrency(active, async (cursor) => {
|
|
4798
5059
|
const need = fetchLimit - cursor.links.length;
|
|
@@ -4811,7 +5072,12 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4811
5072
|
enforceTargetKeyCap(resolvedKeys.size + newKeys.size);
|
|
4812
5073
|
const fetched = await fetchTargets(Array.from(newKeys.entries()));
|
|
4813
5074
|
for (const key of newKeys.keys()) resolvedKeys.add(key);
|
|
4814
|
-
const
|
|
5075
|
+
const ambientTargets = await applyAmbientTargetFilters(fetched.map((entry) => entry.target).filter((target) => !!target));
|
|
5076
|
+
for (const target of ambientTargets) {
|
|
5077
|
+
const key = this._buildRelationKey(target, targetFields);
|
|
5078
|
+
if (key) ambientKeys.add(key);
|
|
5079
|
+
}
|
|
5080
|
+
const surviving = await applyConfigTargetFilter(ambientTargets);
|
|
4815
5081
|
for (const target of surviving) {
|
|
4816
5082
|
const key = this._buildRelationKey(target, targetFields);
|
|
4817
5083
|
if (!key || survivorByKey.has(key)) continue;
|
|
@@ -4823,8 +5089,11 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4823
5089
|
const cursor = active[i];
|
|
4824
5090
|
for (const link of candidatesPerCursor[i]) {
|
|
4825
5091
|
if (cursor.links.length >= fetchLimit) break;
|
|
5092
|
+
if (scanLimit !== void 0 && cursor.ambientLinks >= scanLimit) break;
|
|
4826
5093
|
const key = this._buildRelationKey(link, throughTargetFields);
|
|
4827
|
-
if (!key
|
|
5094
|
+
if (!key) continue;
|
|
5095
|
+
if (ambientKeys.has(key)) cursor.ambientLinks += 1;
|
|
5096
|
+
if (!survivorByKey.has(key)) continue;
|
|
4828
5097
|
cursor.links.push(link);
|
|
4829
5098
|
}
|
|
4830
5099
|
}
|
|
@@ -4877,6 +5146,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4877
5146
|
const relationDefinition = tableConfig.relations[relationName];
|
|
4878
5147
|
const strict = tableConfig.strict !== false;
|
|
4879
5148
|
const hasPostFetchTargetFilter = this.rls?.mode !== "skip" && isRlsEnabled(targetTableConfig.table) || Boolean(relationDefinition?.where) || Boolean(relationConfig && typeof relationConfig === "object" && "where" in relationConfig && relationConfig.where);
|
|
5149
|
+
const isExistenceProbe = Boolean(relationConfig) && typeof relationConfig === "object" && relationConfig[RELATION_EXISTENCE_PROBE] === true;
|
|
4880
5150
|
let orderSpecs = [];
|
|
4881
5151
|
if (relationConfig && typeof relationConfig === "object" && "orderBy" in relationConfig) {
|
|
4882
5152
|
let orderByValue = relationConfig.orderBy;
|
|
@@ -4891,15 +5161,22 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4891
5161
|
if (effectivePerParentLimit === void 0 && !this.allowFullScan) throw new Error(`Relation "${tableConfig.name}.${relationName}" requires limit, allowFullScan: true, or defineSchema(..., { defaults: { defaultLimit } }).`);
|
|
4892
5162
|
const perParentOffset = relationConfig && typeof relationConfig === "object" && "offset" in relationConfig ? relationConfig.offset : void 0;
|
|
4893
5163
|
if (perParentOffset !== void 0 && typeof perParentOffset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
|
|
5164
|
+
const probeFetchLimit = isExistenceProbe ? 1 : void 0;
|
|
5165
|
+
const probeScanLimit = isExistenceProbe ? effectivePerParentLimit : void 0;
|
|
5166
|
+
const canBoundPerParentRead = effectivePerParentLimit !== void 0 || isExistenceProbe;
|
|
4894
5167
|
const applyOffsetAndLimit = (items) => {
|
|
4895
5168
|
let result = items;
|
|
4896
5169
|
if (perParentOffset !== void 0 && perParentOffset > 0) result = result.slice(perParentOffset);
|
|
4897
5170
|
if (effectivePerParentLimit !== void 0) result = result.slice(0, effectivePerParentLimit);
|
|
4898
5171
|
return result;
|
|
4899
5172
|
};
|
|
4900
|
-
const
|
|
4901
|
-
|
|
4902
|
-
if (relationDefinition?.where)
|
|
5173
|
+
const applyAmbientTargetFilters = async (candidateTargets) => {
|
|
5174
|
+
const visibleTargets = await this._applyRlsSelectFilter(candidateTargets, targetTableConfig);
|
|
5175
|
+
if (!relationDefinition?.where) return visibleTargets;
|
|
5176
|
+
return visibleTargets.filter((target) => this._evaluateTableFilter(target, targetTableConfig, relationDefinition.where));
|
|
5177
|
+
};
|
|
5178
|
+
const applyConfigTargetFilter = async (ambientTargets) => {
|
|
5179
|
+
let filteredTargets = ambientTargets;
|
|
4903
5180
|
if (relationConfig && typeof relationConfig === "object" && "where" in relationConfig) {
|
|
4904
5181
|
const whereFilter = relationConfig.where;
|
|
4905
5182
|
if (typeof whereFilter === "function") {
|
|
@@ -4912,6 +5189,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4912
5189
|
}
|
|
4913
5190
|
return filteredTargets;
|
|
4914
5191
|
};
|
|
5192
|
+
const applyPostFetchTargetFilters = async (candidateTargets) => applyConfigTargetFilter(await applyAmbientTargetFilters(candidateTargets));
|
|
4915
5193
|
let targets = [];
|
|
4916
5194
|
let throughBySourceKey;
|
|
4917
5195
|
let targetFiltersApplied = false;
|
|
@@ -4946,14 +5224,16 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4946
5224
|
};
|
|
4947
5225
|
});
|
|
4948
5226
|
};
|
|
4949
|
-
if (orderSpecs.length === 0 &&
|
|
5227
|
+
if (orderSpecs.length === 0 && canBoundPerParentRead) {
|
|
4950
5228
|
const bounded = await this._readBoundedThroughLinks({
|
|
4951
|
-
|
|
5229
|
+
applyAmbientTargetFilters,
|
|
5230
|
+
applyConfigTargetFilter,
|
|
4952
5231
|
edge,
|
|
4953
5232
|
enforceTargetKeyCap,
|
|
4954
5233
|
entries,
|
|
4955
|
-
fetchLimit: Math.max(perParentOffset ?? 0, 0) + effectivePerParentLimit,
|
|
5234
|
+
fetchLimit: probeFetchLimit ?? Math.max(perParentOffset ?? 0, 0) + effectivePerParentLimit,
|
|
4956
5235
|
fetchTargets: fetchThroughTargets,
|
|
5236
|
+
scanLimit: probeScanLimit,
|
|
4957
5237
|
targetFields,
|
|
4958
5238
|
throughIndexName,
|
|
4959
5239
|
throughTableConfig
|
|
@@ -4993,22 +5273,25 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4993
5273
|
});
|
|
4994
5274
|
const orderServedByIndex = orderSpecs.length === 0 || orderPushdownDirection !== null;
|
|
4995
5275
|
const applyPushdownOrder = (query) => orderPushdownDirection ? query.order(orderPushdownDirection) : query;
|
|
4996
|
-
const streamPostFetchTargetFilters = orderServedByIndex && hasPostFetchTargetFilter &&
|
|
5276
|
+
const streamPostFetchTargetFilters = orderServedByIndex && hasPostFetchTargetFilter && canBoundPerParentRead;
|
|
4997
5277
|
targetFiltersApplied = streamPostFetchTargetFilters;
|
|
4998
5278
|
targets = (await this._mapWithConcurrency(entries, async ([, values]) => {
|
|
4999
5279
|
const query = this._queryByFields(this.db.query(edge.targetTable), targetFields, values, indexName);
|
|
5000
|
-
if (orderServedByIndex && !hasPostFetchTargetFilter &&
|
|
5001
|
-
const fetchLimit = (perParentOffset ?? 0) + (effectivePerParentLimit ?? 0);
|
|
5280
|
+
if (orderServedByIndex && !hasPostFetchTargetFilter && canBoundPerParentRead) {
|
|
5281
|
+
const fetchLimit = probeFetchLimit ?? (perParentOffset ?? 0) + (effectivePerParentLimit ?? 0);
|
|
5002
5282
|
return await applyPushdownOrder(query).take(fetchLimit);
|
|
5003
5283
|
}
|
|
5004
5284
|
if (streamPostFetchTargetFilters) {
|
|
5005
5285
|
const visibleTargets = [];
|
|
5006
|
-
const fetchLimit = Math.max(perParentOffset ?? 0, 0) + (effectivePerParentLimit ?? 0);
|
|
5286
|
+
const fetchLimit = probeFetchLimit ?? Math.max(perParentOffset ?? 0, 0) + (effectivePerParentLimit ?? 0);
|
|
5007
5287
|
let batch = [];
|
|
5288
|
+
let ambientSeen = 0;
|
|
5008
5289
|
const drain = async () => {
|
|
5009
5290
|
if (batch.length === 0) return;
|
|
5010
|
-
const
|
|
5291
|
+
const ambientTargets = await applyAmbientTargetFilters(batch);
|
|
5011
5292
|
batch = [];
|
|
5293
|
+
ambientSeen += ambientTargets.length;
|
|
5294
|
+
const filtered = await applyConfigTargetFilter(ambientTargets);
|
|
5012
5295
|
for (const row of filtered) {
|
|
5013
5296
|
if (visibleTargets.length >= fetchLimit) return;
|
|
5014
5297
|
visibleTargets.push(row);
|
|
@@ -5020,6 +5303,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
5020
5303
|
if (batch.length < chunk) continue;
|
|
5021
5304
|
await drain();
|
|
5022
5305
|
if (visibleTargets.length >= fetchLimit) break;
|
|
5306
|
+
if (probeScanLimit !== void 0 && ambientSeen >= probeScanLimit) break;
|
|
5023
5307
|
}
|
|
5024
5308
|
if (visibleTargets.length < fetchLimit) await drain();
|
|
5025
5309
|
return visibleTargets;
|