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/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-C1DyktHe.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-DdSg3fuk.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-DbPcDW-N.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-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 `in` list still worth turning into an index union when it is the only
707
- * indexable term. Each value becomes its own index range, so past this width
708
- * the fan-out costs more than the single scan it would replace.
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 MAX_PROMOTED_PROBES = 64;
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 > MAX_PROMOTED_PROBES) continue;
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
- const orderField = this.orderFields[0];
980
- const serving = candidates.find((index) => index.indexFields.length > 1 && index.indexFields[1] === orderField);
981
- if (serving) return serving;
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
- return indexFields[pinnedLength] === this.orderFields[0] ? INDEX_ORDER_BONUS : 0;
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
- if (aVal < bVal) return order.direction === "asc" ? -1 : 1;
1748
- if (aVal > bVal) return order.direction === "asc" ? 1 : -1;
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
- _buildBasePipelineStream(queryConfig, wherePredicate, configuredIndex) {
2679
- const schemaDefinition = this._getSchemaDefinitionOrThrow();
2680
- let streamQuery = stream(this.db, schemaDefinition).query(this.tableConfig.name);
2681
- const primaryOrder = queryConfig.order?.[0];
2682
- const primaryOrderDirection = primaryOrder?.direction ?? "asc";
2683
- if (queryConfig.index) streamQuery = streamQuery.withIndex(queryConfig.index.name, (q) => {
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 (primaryOrder && primaryOrder.field !== INTERNAL_CREATION_TIME_FIELD) {
2690
- const orderIndex = this._findStreamOrderIndex(primaryOrder.field);
2691
- if (orderIndex) streamQuery = streamQuery.withIndex(orderIndex.name, (q) => q);
2692
- }
2693
- streamQuery = streamQuery.order(primaryOrderDirection);
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 streamQuery;
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 = stream(this.db, schemaDefinition).query(this.tableConfig.name);
2719
- if (queryConfig.index) streamQuery = streamQuery.withIndex(queryConfig.index.name, (q) => {
2720
- let indexQuery = q;
2721
- for (const filter of queryConfig.index.filters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
2722
- return indexQuery;
2723
- });
2724
- else if (configuredIndex?.name) streamQuery = streamQuery.withIndex(configuredIndex.name, configuredIndex.range ? configuredIndex.range : (q) => q);
2725
- else if (orderIndexName) streamQuery = streamQuery.withIndex(orderIndexName, (q) => q);
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
- streamQuery = await this._buildIdLookupStream({
4047
+ const idLookupStream = await this._buildIdLookupStream({
3896
4048
  configuredIndex,
3897
4049
  idLookup,
3898
4050
  order: fallbackOrder,
3899
4051
  queryConfig,
3900
4052
  wherePredicate
3901
- }) ?? this._buildBasePipelineStream(queryConfig, wherePredicate, configuredIndex);
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 pushdownDirection = resolveIndexOrderPushdown({
4026
- indexFields: indexConfig.fields,
4027
- pinnedEqCount: this._indexEqPrefixCount(queryConfig),
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 pushdownDirection = resolveIndexOrderPushdown({
4040
- indexFields: configuredIndexFields,
4041
- pinnedEqCount: countConfiguredIndexEqPrefix(configuredIndexFields, rangeOperations),
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 === "_creationTime") query = query.order(primaryOrder.direction);
4050
- else {
4051
- const orderIndex = getIndexes(this.tableConfig.table).find((idx) => idx.fields[0] === orderField) ?? this.edgeMetadata.find((idx) => idx.indexFields[0] === orderField);
4052
- if (orderIndex) {
4053
- orderIndexName = "indexName" in orderIndex ? orderIndex.indexName : orderIndex.name;
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 = needsPostFetchSortForPrimary || hasSecondaryOrders;
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 (hasSecondaryOrders) console.warn("Pagination: Only the first orderBy field is used for cursor ordering. Secondary orderBy fields are applied per page and may be unstable across pages.");
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 = primaryOrder ? resolveIndexOrderPushdown({
4120
- indexFields: queryConfig.index.fields,
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
- if (queryConfig.strategy === "multiProbe") if (maxScan === void 0) {
4153
- if (strict) throw new Error("Pagination with multi-probe index-union filters requires maxScan when strict=true. Add maxScan or make the query indexable.");
4154
- console.warn("Pagination with multi-probe index-union filters is running without maxScan because strict: false.");
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
- if (!schemaDefinition) throw new Error("Pagination with maxScan requires defineSchema(). Ensure defineSchema(tables) was used with the same tables object passed to defineRelations.");
4158
- let streamQuery = stream(this.db, schemaDefinition).query(this.tableConfig.name);
4159
- if (queryConfig.order && primaryOrder) {
4160
- if (needsPostFetchSortForPrimary) {
4161
- if (strict) throw new Error(`Pagination: Field '${primaryOrder.field}' has no index. Add an index or disable strict.`);
4162
- console.warn(`Pagination: Field '${primaryOrder.field}' has no index. Falling back to _creationTime ordering.`);
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 (hasSecondaryOrders) console.warn("Pagination: Only the first orderBy field is used for cursor ordering. Secondary orderBy fields are applied per page and may be unstable across pages.");
4165
- streamQuery = streamQuery.order(primaryOrder.direction);
4166
- } else streamQuery = streamQuery.order("desc");
4167
- if (queryConfig.postFilters.length > 0) streamQuery = streamQuery.filterWith(async (row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
4168
- const paginationResult = await streamQuery.paginate({
4169
- cursor: cursor ?? null,
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 (hasSecondaryOrders) console.warn("Pagination: Only the first orderBy field is used for cursor ordering. Secondary orderBy fields are applied per page and may be unstable across pages.");
4197
- streamQuery = streamQuery.order(primaryOrder.direction);
4198
- } else streamQuery = streamQuery.order("desc");
4199
- streamQuery = streamQuery.filterWith(async (row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
4200
- const paginationResult = await streamQuery.paginate({
4201
- cursor: cursor ?? null,
4202
- limit: config.limit,
4203
- maxScan
4204
- });
4205
- let pageRows = paginationResult.page;
4206
- pageRows = await this._applyRlsSelectFilter(pageRows, this.tableConfig);
4207
- if (whereFilter) pageRows = await this._applyRelationsFilterToRows(pageRows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with);
4208
- return {
4209
- page: await this._finalizeRows(pageRows),
4210
- continueCursor: paginationResult.continueCursor,
4211
- isDone: paginationResult.isDone,
4212
- pageStatus: paginationResult.pageStatus,
4213
- splitCursor: paginationResult.splitCursor
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 (hasSecondaryOrders) console.warn("Pagination: Only the first orderBy field is used for cursor ordering. Secondary orderBy fields are applied per page and may be unstable across pages.");
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 (hasSecondaryOrders) console.warn("Pagination: Only the first orderBy field is used for cursor ordering. Secondary orderBy fields are applied per page and may be unstable across pages.");
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;