@minnowdb/core 0.6.0 → 0.6.2

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.
@@ -0,0 +1,57 @@
1
+ import type { CompiledQuery, QueryValue } from "../plan/model.js";
2
+ export type PointReadValue = boolean | number | string | Date;
3
+ export interface PointReadEquality {
4
+ column: string;
5
+ value: PointReadValue;
6
+ }
7
+ export interface PointReadShape {
8
+ table: string;
9
+ /** Conjunctive equalities, in predicate order; may repeat a column. */
10
+ equalities: PointReadEquality[];
11
+ /** Plain column projections, in select order. */
12
+ select: Array<{
13
+ column: string;
14
+ alias: string;
15
+ }>;
16
+ }
17
+ /** The statement-shaped half of the analysis, computed once per cached plan. */
18
+ interface PointReadTemplate {
19
+ table: string;
20
+ equalities: Array<{
21
+ column: string;
22
+ value: PointReadValue;
23
+ } | {
24
+ column: string;
25
+ parameter: number;
26
+ }>;
27
+ select: Array<{
28
+ column: string;
29
+ alias: string;
30
+ }>;
31
+ }
32
+ /**
33
+ * Test-only escape hatch and counters. Not exported from any public entry point: in-repo
34
+ * differential suites import this module directly to force the ordinary executor and to
35
+ * assert the fast path actually served eligible statements.
36
+ */
37
+ export declare const pointReadTestHooks: {
38
+ disabled: boolean;
39
+ attempted: number;
40
+ served: number;
41
+ };
42
+ /** The template for a cached compiled plan, analyzed once per statement. */
43
+ export declare function cachedPointReadTemplate(plan: CompiledQuery): PointReadTemplate | null;
44
+ /**
45
+ * Substitutes this call's parameters into the statement template. Undefined means a parameter
46
+ * carries a value the fast path cannot compare exactly (NULL, a non-finite number, an invalid
47
+ * Date, or a non-storage value), and the ordinary executor must decide what it means.
48
+ */
49
+ export declare function resolvePointReadShape(template: PointReadTemplate, params: readonly QueryValue[]): PointReadShape | undefined;
50
+ /** Whether the array is non-strictly ascending; memoized per immutable decoded array. */
51
+ export declare function valuesAreAscending(values: Float64Array): boolean;
52
+ /** The [begin, end) run of slots equal to `target` over an ascending array. */
53
+ export declare function equalRunRange(values: Float64Array, target: number): {
54
+ begin: number;
55
+ end: number;
56
+ };
57
+ export {};
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Shape analysis for the keyed point-read fast path: a single-table conjunction of
3
+ * column-equals-literal predicates that covers the table's unique key, projecting plain
4
+ * columns. Such a statement addresses at most one row, so execution can skip parameter
5
+ * binding, plan cloning, streamed-view construction, and the vector pipeline entirely and
6
+ * answer from cached decoded blocks. Anything this module cannot prove eligible falls back to
7
+ * the ordinary executor, which stays the authority on errors and semantics.
8
+ */
9
+ import { dateMilliseconds } from "../date-value.js";
10
+ /**
11
+ * Test-only escape hatch and counters. Not exported from any public entry point: in-repo
12
+ * differential suites import this module directly to force the ordinary executor and to
13
+ * assert the fast path actually served eligible statements.
14
+ */
15
+ export const pointReadTestHooks = {
16
+ disabled: false,
17
+ attempted: 0,
18
+ served: 0,
19
+ };
20
+ const DUAL_TABLE = "(dual)";
21
+ /** A base-table column reference: bare, or qualified by the base source's alias. */
22
+ function baseColumnReference(expression, alias) {
23
+ if (expression.kind !== "column")
24
+ return undefined;
25
+ const reference = expression.reference;
26
+ const dot = reference.indexOf(".");
27
+ if (dot < 0)
28
+ return reference;
29
+ if (reference.slice(0, dot) !== alias)
30
+ return undefined;
31
+ const column = reference.slice(dot + 1);
32
+ // A nested qualifier ("a.b.c") is not a base-table column.
33
+ return column.includes(".") ? undefined : column;
34
+ }
35
+ /** A usable equality operand: a plain non-null literal of a storage type, verified exactly. */
36
+ function equalityValue(value) {
37
+ if (value === null)
38
+ return undefined;
39
+ if (typeof value === "number" && !Number.isFinite(value))
40
+ return undefined;
41
+ // Strings in the engine's protected NUL namespace are wrapped at the write boundary, so a
42
+ // raw comparison against stored bytes would not reproduce the ordinary path's answer.
43
+ if (typeof value === "string" && value.startsWith("\u0000"))
44
+ return undefined;
45
+ // The internal-slot read never invokes caller-controlled Date methods.
46
+ if (value instanceof Date && Number.isNaN(dateMilliseconds(value)))
47
+ return undefined;
48
+ return value;
49
+ }
50
+ function templateEquality(expression) {
51
+ if (expression.kind === "parameter")
52
+ return { parameter: expression.index };
53
+ if (expression.kind !== "literal")
54
+ return undefined;
55
+ // Tagged internal values and logical-domain literals compare under domain rules the fast
56
+ // path does not implement; a NULL literal never equals anything.
57
+ if (expression.internalSqlValue === true || expression.sqlDomain !== undefined)
58
+ return undefined;
59
+ const value = equalityValue(expression.value);
60
+ return value === undefined ? undefined : { value };
61
+ }
62
+ /**
63
+ * Recognizes the eligible statement shape. Purely syntactic: catalog checks (column existence,
64
+ * types, key coverage, view expansion, physical history) belong to the caller, which falls
65
+ * back to the ordinary executor whenever they cannot be proven.
66
+ */
67
+ function pointReadTemplate(plan) {
68
+ const base = plan.base;
69
+ if (base.table === DUAL_TABLE ||
70
+ base.derived !== undefined ||
71
+ base.union !== undefined ||
72
+ base.recursive !== undefined ||
73
+ base.windowed !== undefined ||
74
+ base.lateral === true ||
75
+ base.columnAliases !== undefined ||
76
+ plan.joins.length > 0 ||
77
+ plan.groupBy.length > 0 ||
78
+ plan.having.length > 0 ||
79
+ plan.orderBy.length > 0 ||
80
+ plan.select.length === 0 ||
81
+ plan.predicates.length === 0 ||
82
+ plan.limit !== undefined ||
83
+ plan.offset !== undefined ||
84
+ plan.limitParameter !== undefined ||
85
+ plan.offsetParameter !== undefined ||
86
+ (plan.limitValidationParameters?.length ?? 0) > 0 ||
87
+ (plan.offsetValidationParameters?.length ?? 0) > 0 ||
88
+ plan.limitWithTies === true ||
89
+ plan.distinctWildcard === true ||
90
+ plan.usesStatementDatetime === true ||
91
+ plan.usesSequenceCalls === true ||
92
+ plan.usesVolatileFunctions === true) {
93
+ return undefined;
94
+ }
95
+ const select = [];
96
+ for (const item of plan.select) {
97
+ const column = baseColumnReference(item.expression, base.alias);
98
+ if (column === undefined)
99
+ return undefined;
100
+ select.push({ column, alias: item.alias });
101
+ }
102
+ const equalities = [];
103
+ for (const predicate of plan.predicates) {
104
+ if (predicate.operator !== "=" || predicate.escape !== undefined)
105
+ return undefined;
106
+ const leftColumn = baseColumnReference(predicate.left, base.alias);
107
+ const rightColumn = baseColumnReference(predicate.right, base.alias);
108
+ const operand = leftColumn !== undefined && rightColumn === undefined
109
+ ? templateEquality(predicate.right)
110
+ : rightColumn !== undefined && leftColumn === undefined
111
+ ? templateEquality(predicate.left)
112
+ : undefined;
113
+ const column = leftColumn ?? rightColumn;
114
+ if (operand === undefined || column === undefined)
115
+ return undefined;
116
+ equalities.push({ column, ...operand });
117
+ }
118
+ return { table: base.table, equalities, select };
119
+ }
120
+ const templates = new WeakMap();
121
+ /** The template for a cached compiled plan, analyzed once per statement. */
122
+ export function cachedPointReadTemplate(plan) {
123
+ const cached = templates.get(plan);
124
+ if (cached !== undefined)
125
+ return cached;
126
+ const template = pointReadTemplate(plan) ?? null;
127
+ templates.set(plan, template);
128
+ return template;
129
+ }
130
+ /**
131
+ * Substitutes this call's parameters into the statement template. Undefined means a parameter
132
+ * carries a value the fast path cannot compare exactly (NULL, a non-finite number, an invalid
133
+ * Date, or a non-storage value), and the ordinary executor must decide what it means.
134
+ */
135
+ export function resolvePointReadShape(template, params) {
136
+ const equalities = [];
137
+ for (const entry of template.equalities) {
138
+ if ("value" in entry) {
139
+ equalities.push(entry);
140
+ continue;
141
+ }
142
+ const raw = params[entry.parameter];
143
+ if (raw === undefined)
144
+ return undefined;
145
+ const value = equalityValue(raw);
146
+ if (value === undefined)
147
+ return undefined;
148
+ equalities.push({ column: entry.column, value });
149
+ }
150
+ return { table: template.table, equalities, select: template.select };
151
+ }
152
+ const ascendingRuns = new WeakMap();
153
+ /** Whether the array is non-strictly ascending; memoized per immutable decoded array. */
154
+ export function valuesAreAscending(values) {
155
+ const cached = ascendingRuns.get(values);
156
+ if (cached !== undefined)
157
+ return cached;
158
+ let ascending = true;
159
+ for (let index = 1; index < values.length; index += 1) {
160
+ if ((values[index] ?? 0) < (values[index - 1] ?? 0)) {
161
+ ascending = false;
162
+ break;
163
+ }
164
+ }
165
+ ascendingRuns.set(values, ascending);
166
+ return ascending;
167
+ }
168
+ /** The [begin, end) run of slots equal to `target` over an ascending array. */
169
+ export function equalRunRange(values, target) {
170
+ let low = 0;
171
+ let high = values.length;
172
+ while (low < high) {
173
+ const middle = (low + high) >>> 1;
174
+ if ((values[middle] ?? 0) < target)
175
+ low = middle + 1;
176
+ else
177
+ high = middle;
178
+ }
179
+ const begin = low;
180
+ high = values.length;
181
+ while (low < high) {
182
+ const middle = (low + high) >>> 1;
183
+ if ((values[middle] ?? 0) <= target)
184
+ low = middle + 1;
185
+ else
186
+ high = middle;
187
+ }
188
+ return { begin, end: low };
189
+ }
@@ -583,6 +583,12 @@ export declare function distinctFromComparison(left: unknown, right: unknown): b
583
583
  export declare function evaluateBooleanExpression(expression: Expression, evaluateValue: (expression: Expression) => unknown): boolean | null;
584
584
  export declare function comparisonHolds(operator: PredicateOperator, leftValue: unknown, rightValue: unknown): boolean;
585
585
  export declare function hasAggregate(expression: Expression): boolean;
586
+ /** One root aggregate call, using the same canonical set as parsing and execution. */
587
+ export declare function isAggregateCall(expression: Expression): expression is Extract<Expression, {
588
+ kind: "call";
589
+ }> & {
590
+ name: AggregateName;
591
+ };
586
592
  /** The column references inside one expression; a subquery contributes none (its own scope). */
587
593
  export declare function expressionColumnNames(expression: Expression): string[];
588
594
  export declare function expressionColumns(expression: Expression): string[];
@@ -800,6 +800,7 @@ const aggregateNames = new Set([
800
800
  "MAX",
801
801
  "JSON_ARRAYAGG",
802
802
  "STRING_AGG",
803
+ "MINNOW_SINGLE_VALUE",
803
804
  ]);
804
805
  /** Set functions the parser builds from COUNT/SUM rather than from their own accumulator. */
805
806
  const statisticalAggregates = new Set([
@@ -1502,8 +1503,12 @@ export function containsParameter(expression) {
1502
1503
  return childExpressions(expression).some(containsParameter);
1503
1504
  }
1504
1505
  export function blockHasParameters(block) {
1505
- if (block.limitParameter !== undefined || block.offsetParameter !== undefined)
1506
+ if (block.limitParameter !== undefined ||
1507
+ block.offsetParameter !== undefined ||
1508
+ (block.limitValidationParameters?.length ?? 0) > 0 ||
1509
+ (block.offsetValidationParameters?.length ?? 0) > 0) {
1506
1510
  return true;
1511
+ }
1507
1512
  const expressions = [];
1508
1513
  forEachBlockExpression(block, (expression) => expressions.push(expression));
1509
1514
  return (expressions.some(containsParameter) ||
@@ -1608,6 +1613,14 @@ function bindBlock(block, values) {
1608
1613
  block.offset = validateOffset(numeric(values[block.offsetParameter] ?? null));
1609
1614
  delete block.offsetParameter;
1610
1615
  }
1616
+ for (const index of block.limitValidationParameters ?? []) {
1617
+ validateLimit(numeric(values[index] ?? null));
1618
+ }
1619
+ for (const index of block.offsetValidationParameters ?? []) {
1620
+ validateOffset(numeric(values[index] ?? null));
1621
+ }
1622
+ delete block.limitValidationParameters;
1623
+ delete block.offsetValidationParameters;
1611
1624
  for (const item of block.select)
1612
1625
  item.expression = bindExpression(item.expression, values);
1613
1626
  for (const predicate of [...block.predicates, ...block.having]) {
@@ -1892,6 +1905,7 @@ export function inferBlockSchema(plan, schemas) {
1892
1905
  expression.name === "AVG" ||
1893
1906
  expression.name === "MIN" ||
1894
1907
  expression.name === "MAX" ||
1908
+ expression.name === "MINNOW_SINGLE_VALUE" ||
1895
1909
  expression.name === "COALESCE" ||
1896
1910
  expression.name === "NULLIF" ||
1897
1911
  expression.name === "GREATEST" ||
@@ -3790,6 +3804,13 @@ function evaluate(expression, context, group) {
3790
3804
  if (group === undefined)
3791
3805
  throw new TypeError(`${expression.name} requires grouped execution`);
3792
3806
  const argument = expression.arguments[0] ?? { kind: "wildcard" };
3807
+ if (expression.name === "MINNOW_SINGLE_VALUE") {
3808
+ if (group.length > 1) {
3809
+ throw new TypeError(`A scalar subquery returned ${String(group.length)} rows`);
3810
+ }
3811
+ const row = group[0];
3812
+ return row === undefined ? null : evaluate(argument, row);
3813
+ }
3793
3814
  if (expression.name === "STRING_AGG") {
3794
3815
  const delimiter = expression.arguments[1];
3795
3816
  if (delimiter === undefined)
@@ -3847,12 +3868,44 @@ function evaluate(expression, context, group) {
3847
3868
  String(externalSqlDomainValue(member.value)))
3848
3869
  .join(""));
3849
3870
  }
3871
+ if (expression.name === "JSON_ARRAYAGG") {
3872
+ let members = group.map((row) => ({
3873
+ value: argument.kind === "wildcard" ? 1 : evaluate(argument, row),
3874
+ order: (expression.aggregateOrderBy ?? []).map((item) => evaluate(item.expression, row)),
3875
+ }));
3876
+ if (expression.distinct === true) {
3877
+ const seen = new Set();
3878
+ members = members.filter(({ value }) => {
3879
+ const key = value instanceof Date ? ` d${String(dateMilliseconds(value))}` : value;
3880
+ if (seen.has(key))
3881
+ return false;
3882
+ seen.add(key);
3883
+ return true;
3884
+ });
3885
+ }
3886
+ if ((expression.aggregateOrderBy?.length ?? 0) > 0) {
3887
+ members.sort((left, right) => {
3888
+ for (const [index, order] of (expression.aggregateOrderBy ?? []).entries()) {
3889
+ const a = left.order[index];
3890
+ const b = right.order[index];
3891
+ const placed = nullOrder(a, b, order.nulls, order.direction);
3892
+ if (placed !== undefined && placed !== 0)
3893
+ return placed;
3894
+ const compared = compareValues(a, b);
3895
+ if (compared !== 0)
3896
+ return order.direction === "desc" ? -compared : compared;
3897
+ }
3898
+ return 0;
3899
+ });
3900
+ }
3901
+ return members.length === 0
3902
+ ? null
3903
+ : preservedJsonDomainValue(jsonConstructor("JSON_ARRAY", members.map(({ value }) => value)));
3904
+ }
3850
3905
  let values = argument.kind === "wildcard"
3851
3906
  ? group.map(() => 1)
3852
3907
  : group.map((row) => evaluate(argument, row));
3853
- if (expression.name !== "JSON_ARRAYAGG") {
3854
- values = values.filter((value) => value !== null && value !== undefined);
3855
- }
3908
+ values = values.filter((value) => value !== null && value !== undefined);
3856
3909
  if (expression.distinct === true) {
3857
3910
  const seen = new Set();
3858
3911
  values = values.filter((value) => {
@@ -3874,11 +3927,6 @@ function evaluate(expression, context, group) {
3874
3927
  const sum = sumNumericValues(values);
3875
3928
  return exactNumericBinary("/", sum, values.length) ?? numeric(sum) / values.length;
3876
3929
  })();
3877
- if (expression.name === "JSON_ARRAYAGG") {
3878
- return values.length === 0
3879
- ? null
3880
- : preservedJsonDomainValue(jsonConstructor("JSON_ARRAY", values));
3881
- }
3882
3930
  if (expression.name === "MIN")
3883
3931
  return values.reduce((best, value) => (best === undefined || compareValues(value, best) < 0 ? value : best), undefined);
3884
3932
  return values.reduce((best, value) => (best === undefined || compareValues(value, best) > 0 ? value : best), undefined);
@@ -4330,6 +4378,10 @@ export function hasAggregate(expression) {
4330
4378
  }
4331
4379
  return childExpressions(expression).some(hasAggregate);
4332
4380
  }
4381
+ /** One root aggregate call, using the same canonical set as parsing and execution. */
4382
+ export function isAggregateCall(expression) {
4383
+ return expression.kind === "call" && aggregateNames.has(expression.name);
4384
+ }
4333
4385
  function validateGrouping(plan) {
4334
4386
  const grouped = plan.groupBy.length > 0 || plan.select.some((item) => hasAggregate(item.expression));
4335
4387
  if (!grouped)
@@ -5336,7 +5388,7 @@ class Parser {
5336
5388
  rows: [[]],
5337
5389
  defaultValues: true,
5338
5390
  ...this.#onConflictClause(table),
5339
- ...this.#returningClause(),
5391
+ ...this.#returningClause(table),
5340
5392
  };
5341
5393
  }
5342
5394
  if (this.#isKeyword("SELECT")) {
@@ -5353,7 +5405,7 @@ class Parser {
5353
5405
  columns,
5354
5406
  rows: [],
5355
5407
  query,
5356
- ...this.#returningClause(),
5408
+ ...this.#returningClause(table),
5357
5409
  };
5358
5410
  }
5359
5411
  this.#keyword("VALUES");
@@ -5380,7 +5432,7 @@ class Parser {
5380
5432
  columns,
5381
5433
  rows,
5382
5434
  ...this.#onConflictClause(table),
5383
- ...this.#returningClause(),
5435
+ ...this.#returningClause(table),
5384
5436
  };
5385
5437
  }
5386
5438
  #onConflictClause(table) {
@@ -5447,8 +5499,8 @@ class Parser {
5447
5499
  },
5448
5500
  };
5449
5501
  }
5450
- /** RETURNING * or RETURNING col, ... — the engine's runStatement implements the semantics. */
5451
- #returningClause() {
5502
+ /** RETURNING *, target.*, or [target.]col, ... — execution owns the row semantics. */
5503
+ #returningClause(table) {
5452
5504
  if (!this.#isKeyword("RETURNING"))
5453
5505
  return {};
5454
5506
  this.#keyword("RETURNING");
@@ -5458,7 +5510,23 @@ class Parser {
5458
5510
  }
5459
5511
  const columns = [];
5460
5512
  for (;;) {
5461
- columns.push(this.#identifier());
5513
+ const first = this.#identifier();
5514
+ if (this.#punctuation(".")) {
5515
+ if (first !== table) {
5516
+ throw new TypeError(`RETURNING qualifier must name the target table: ${table}`);
5517
+ }
5518
+ if (this.#peek().text === "*") {
5519
+ this.#index += 1;
5520
+ if (columns.length > 0 || this.#peek().text === ",") {
5521
+ throw new TypeError("RETURNING target.* must be the only returned item");
5522
+ }
5523
+ return { returning: "*" };
5524
+ }
5525
+ columns.push(this.#identifier());
5526
+ }
5527
+ else {
5528
+ columns.push(first);
5529
+ }
5462
5530
  if (!this.#punctuation(","))
5463
5531
  break;
5464
5532
  }
@@ -5609,14 +5677,14 @@ class Parser {
5609
5677
  throw new TypeError("UPDATE assignments must set each column once");
5610
5678
  }
5611
5679
  const predicates = this.#mutationPredicates();
5612
- return { kind: "update", table, assignments, predicates, ...this.#returningClause() };
5680
+ return { kind: "update", table, assignments, predicates, ...this.#returningClause(table) };
5613
5681
  }
5614
5682
  #deleteStatement() {
5615
5683
  this.#keyword("DELETE");
5616
5684
  this.#keyword("FROM");
5617
5685
  const table = this.#identifier();
5618
5686
  const predicates = this.#mutationPredicates();
5619
- return { kind: "delete", table, predicates, ...this.#returningClause() };
5687
+ return { kind: "delete", table, predicates, ...this.#returningClause(table) };
5620
5688
  }
5621
5689
  #mutationPredicates() {
5622
5690
  const predicates = [];
@@ -6791,10 +6859,18 @@ class Parser {
6791
6859
  const all = this.#isKeyword("ALL");
6792
6860
  this.#keyword(this.#isKeyword("ANY") ? "ANY" : all ? "ALL" : "SOME");
6793
6861
  this.#expectPunctuation("(");
6862
+ // Kysely's fn.any(subquery) emits PostgreSQL's valid ANY((SELECT ...)) spelling.
6863
+ let wrapped = 0;
6864
+ while (this.#punctuation("("))
6865
+ wrapped += 1;
6794
6866
  if (!this.#isKeyword("SELECT")) {
6795
6867
  throw new TypeError("ANY/ALL take a subquery");
6796
6868
  }
6797
6869
  const block = this.#selectBlock("(quantified subquery)");
6870
+ while (wrapped > 0) {
6871
+ this.#expectPunctuation(")");
6872
+ wrapped -= 1;
6873
+ }
6798
6874
  this.#expectPunctuation(")");
6799
6875
  return {
6800
6876
  kind: "condition",
@@ -7296,7 +7372,9 @@ class Parser {
7296
7372
  // canonical plan names. ANY_VALUE picks an implementation-dependent row of the group
7297
7373
  // (T626); MIN is one such choice and reuses its accumulator exactly.
7298
7374
  const name = (upper === "ANY_VALUE" ? "MIN" : (functionSpellings.get(upper) ?? upper));
7299
- if (name === "MINNOW_TUPLE_KEY" || name === "MINNOW_COLLATE") {
7375
+ if (name === "MINNOW_TUPLE_KEY" ||
7376
+ name === "MINNOW_COLLATE" ||
7377
+ name === "MINNOW_SINGLE_VALUE") {
7300
7378
  throw new TypeError(`Unsupported function: ${identifier}`);
7301
7379
  }
7302
7380
  if (!aggregateNames.has(name) && !scalarFunctionNames.has(name))
@@ -7327,6 +7405,11 @@ class Parser {
7327
7405
  if (this.#isKeyword("ORDER"))
7328
7406
  aggregateOrderBy = this.#orderByClause();
7329
7407
  }
7408
+ else if (name === "JSON_ARRAYAGG") {
7409
+ args.push(this.#expression());
7410
+ if (this.#isKeyword("ORDER"))
7411
+ aggregateOrderBy = this.#orderByClause();
7412
+ }
7330
7413
  else {
7331
7414
  args.push(...this.#expressionList());
7332
7415
  }
@@ -864,8 +864,11 @@ function bindExpression(expression, sources, aggregateSpecs, aggregateIndexes, m
864
864
  argument.vector.kind === "datetime"
865
865
  ? { source: argument.source, vector: argument.vector }
866
866
  : undefined;
867
- const rawNumber = expression.name !== "JSON_ARRAYAGG" &&
868
- expression.name !== "STRING_AGG" &&
867
+ const rawNumber = (expression.name === "COUNT" ||
868
+ expression.name === "SUM" ||
869
+ expression.name === "AVG" ||
870
+ expression.name === "MIN" ||
871
+ expression.name === "MAX") &&
869
872
  argument.kind === "column" &&
870
873
  argument.vector.kind === "number"
871
874
  ? { source: argument.source, vector: argument.vector }
@@ -1536,6 +1539,12 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
1536
1539
  (spec.orderBy ?? []).map((order) => encodeSpillValue(asQueryValue(evaluateBatchExpression(plan, order.expression, batch, row)))),
1537
1540
  ]);
1538
1541
  }
1542
+ else if (spec.name === "JSON_ARRAYAGG" && (spec.orderBy?.length ?? 0) > 0) {
1543
+ spillRow[`a${String(index)}`] = JSON.stringify([
1544
+ encodeSpillValue(asQueryValue(raw ?? null)),
1545
+ (spec.orderBy ?? []).map((order) => encodeSpillValue(asQueryValue(evaluateBatchExpression(plan, order.expression, batch, row)))),
1546
+ ]);
1547
+ }
1539
1548
  else {
1540
1549
  spillRow[`a${String(index)}`] =
1541
1550
  raw === null || raw === undefined ? null : asQueryValue(raw);
@@ -3763,12 +3772,30 @@ function updateAggregatesFromValues(plan, state, values, memory) {
3763
3772
  throw new Error("Spilled STRING_AGG order is invalid");
3764
3773
  applyAggregateValue(spec, state, index, decoded[0], memory, decoded[1], encodedOrder.map(decodeSpillValue));
3765
3774
  }
3775
+ else if (spec.name === "JSON_ARRAYAGG" &&
3776
+ (spec.orderBy?.length ?? 0) > 0 &&
3777
+ typeof value === "string") {
3778
+ const decoded = JSON.parse(value);
3779
+ if (!Array.isArray(decoded) || decoded.length !== 2 || !Array.isArray(decoded[1])) {
3780
+ throw new Error("Spilled JSON_ARRAYAGG input is invalid");
3781
+ }
3782
+ applyAggregateValue(spec, state, index, decodeSpillValue(decoded[0]), memory, undefined, decoded[1].map(decodeSpillValue));
3783
+ }
3766
3784
  else {
3767
3785
  applyAggregateValue(spec, state, index, value, memory);
3768
3786
  }
3769
3787
  }
3770
3788
  }
3771
3789
  function applyAggregateValue(spec, state, index, value, memory, delimiter, orderValues = []) {
3790
+ if (spec.name === "MINNOW_SINGLE_VALUE") {
3791
+ const count = (state.counts[index] ?? 0) + 1;
3792
+ state.counts[index] = count;
3793
+ if (count > 1) {
3794
+ throw new TypeError(`A scalar subquery returned ${String(count)} rows`);
3795
+ }
3796
+ replaceAggregateValue(state, index, asQueryValue(value ?? null), "Scalar subquery value", memory);
3797
+ return;
3798
+ }
3772
3799
  if (spec.name === "JSON_ARRAYAGG") {
3773
3800
  const member = asQueryValue(value ?? null);
3774
3801
  if (spec.distinct === true && !firstOfItsKind(state, index, member, memory))
@@ -3780,8 +3807,14 @@ function applyAggregateValue(spec, state, index, value, memory, delimiter, order
3780
3807
  list = [];
3781
3808
  lists[index] = list;
3782
3809
  }
3783
- list.push(member);
3784
- memory.tally(safeMemorySum(QUERY_REFERENCE_BYTES, queryValuePayloadBytes(member), "JSON_ARRAYAGG member"), "JSON_ARRAYAGG member");
3810
+ const retained = (spec.orderBy?.length ?? 0) === 0
3811
+ ? member
3812
+ : JSON.stringify([
3813
+ encodeSpillValue(member),
3814
+ orderValues.map((orderValue) => encodeSpillValue(orderValue)),
3815
+ ]);
3816
+ list.push(retained);
3817
+ memory.tally(safeMemorySum(QUERY_REFERENCE_BYTES, queryValuePayloadBytes(retained), "JSON_ARRAYAGG member"), "JSON_ARRAYAGG member");
3785
3818
  return;
3786
3819
  }
3787
3820
  if (spec.name === "STRING_AGG") {
@@ -3921,7 +3954,38 @@ function evaluateFinalExpression(plan, expression, group) {
3921
3954
  if (count === 0)
3922
3955
  return null;
3923
3956
  if (expression.name === "JSON_ARRAYAGG") {
3924
- return preservedJsonDomainValue(jsonConstructor("JSON_ARRAY", required(group.lists, "JSON aggregate list state is missing")[aggregateIndex] ?? []));
3957
+ const retained = required(group.lists, "JSON aggregate list state is missing")[aggregateIndex] ?? [];
3958
+ const orderBy = plan.aggregates[aggregateIndex]?.orderBy ?? [];
3959
+ const members = retained.map((encoded) => {
3960
+ if (orderBy.length === 0)
3961
+ return { value: encoded, order: [] };
3962
+ if (typeof encoded !== "string")
3963
+ throw new Error("JSON_ARRAYAGG member is invalid");
3964
+ const pair = JSON.parse(encoded);
3965
+ if (!Array.isArray(pair) || pair.length !== 2 || !Array.isArray(pair[1])) {
3966
+ throw new Error("JSON_ARRAYAGG member is invalid");
3967
+ }
3968
+ return {
3969
+ value: decodeSpillValue(pair[0]),
3970
+ order: pair[1].map(decodeSpillValue),
3971
+ };
3972
+ });
3973
+ if (orderBy.length > 0) {
3974
+ members.sort((left, right) => {
3975
+ for (const [index, order] of orderBy.entries()) {
3976
+ const a = left.order[index];
3977
+ const b = right.order[index];
3978
+ const placed = nullOrder(a, b, order.nulls, order.direction);
3979
+ if (placed !== undefined && placed !== 0)
3980
+ return placed;
3981
+ const compared = compareValues(a, b);
3982
+ if (compared !== 0)
3983
+ return order.direction === "desc" ? -compared : compared;
3984
+ }
3985
+ return 0;
3986
+ });
3987
+ }
3988
+ return preservedJsonDomainValue(jsonConstructor("JSON_ARRAY", members.map(({ value }) => value)));
3925
3989
  }
3926
3990
  if (expression.name === "STRING_AGG") {
3927
3991
  const members = required(group.lists, "STRING_AGG list state is missing")[aggregateIndex] ?? [];
@@ -14,7 +14,9 @@ export interface QueryResult {
14
14
  }
15
15
  export type BinaryOperator = "+" | "-" | "*" | "/" | "%" | "||";
16
16
  export type ComparisonOperator = "=" | "!=" | "<>" | ">" | ">=" | "<" | "<=";
17
- export type AggregateName = "COUNT" | "SUM" | "AVG" | "MIN" | "MAX" | "JSON_ARRAYAGG" | "STRING_AGG";
17
+ export type AggregateName = "COUNT" | "SUM" | "AVG" | "MIN" | "MAX" | "JSON_ARRAYAGG" | "STRING_AGG"
18
+ /** Optimizer-only aggregate that enforces scalar-subquery cardinality. */
19
+ | "MINNOW_SINGLE_VALUE";
18
20
  export type ScalarFunctionName = "ROUND" | "COALESCE" | "DATE_TRUNC" | "DATE_ADD" | "UPPER" | "LOWER" | "LENGTH" | "ABS" | "TRIM" | "LTRIM" | "RTRIM" | "SUBSTR" | "REPLACE" | "INSTR" | "NULLIF" | "GREATEST" | "LEAST" | "FLOOR" | "CEIL" | "MOD" | "POWER" | "SQRT" | "EXTRACT" | "CAST" | "OCTET_LENGTH" | "LPAD" | "RPAD" | "OVERLAY" | "CURRENT_DATE" | "CURRENT_TIMESTAMP" | "LOCALTIME" | "GROUPING" | "JSON_VALUE" | "JSON_QUERY" | "JSON_EXISTS" | "JSON_OBJECT" | "JSON_ARRAY" | "IS_JSON" | "ARRAY"
19
21
  /** Optimizer-only, prefix-free equality key for hashable multi-column decorrelation. */
20
22
  | "MINNOW_TUPLE_KEY"
@@ -201,6 +203,10 @@ export interface CompiledQuery {
201
203
  distinctWildcard?: boolean;
202
204
  limitParameter?: number;
203
205
  offsetParameter?: number;
206
+ /** Optimizer-relocated LIMIT placeholders that still need bind-time numeric/range validation. */
207
+ limitValidationParameters?: number[];
208
+ /** Optimizer-relocated OFFSET placeholders that still need bind-time numeric/range validation. */
209
+ offsetValidationParameters?: number[];
204
210
  limitWithTies?: boolean;
205
211
  parameterCount?: number;
206
212
  usesStatementDatetime?: boolean;