@minnowdb/core 0.6.0 → 0.6.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.
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnowdb/core",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "A columnar SQL database for the browser: PostgreSQL-style SQL over durable IndexedDB or OPFS data, with no server or WebAssembly module.",
5
5
  "license": "MIT",
6
6
  "author": "Eric Wilhite",
@@ -129,6 +129,11 @@
129
129
  "classification": "different",
130
130
  "reason": "Minnow returns JSON text; PostgreSQL returns a native JSON value, and member order is unspecified without aggregate-local ORDER BY."
131
131
  },
132
+ {
133
+ "id": "subquery.correlated-json-aggregate",
134
+ "classification": "different",
135
+ "reason": "Both engines accept the correlated JSON aggregate and agree on its JSON value, but Minnow returns JSON text while PostgreSQL returns a native JSON value."
136
+ },
132
137
  {
133
138
  "id": "type.exact-numeric",
134
139
  "classification": "different",
@@ -257,8 +257,8 @@
257
257
  {
258
258
  "id": "mutation.returning",
259
259
  "status": "supported",
260
- "example": "DELETE FROM keyed WHERE name = 'x' RETURNING name, score",
261
- "notes": "RETURNING works on INSERT, UPDATE, and DELETE; inserts echo written values, updates return post-update values, deletes the rows as read."
260
+ "example": "DELETE FROM keyed WHERE name = 'x' RETURNING keyed.name, keyed.score",
261
+ "notes": "RETURNING works on INSERT, UPDATE, and DELETE; inserts echo written values, updates return post-update values, and deletes return the rows as read. Columns and target.* may be target-qualified."
262
262
  },
263
263
  {
264
264
  "id": "mutation.upsert",
@@ -317,7 +317,7 @@
317
317
  "id": "predicate.quantified",
318
318
  "status": "supported",
319
319
  "example": "SELECT region FROM rows WHERE amount > ALL (SELECT amount FROM dims)",
320
- "notes": "ANY/SOME/ALL use full three-valued logic. Correlated forms are supported as top-level WHERE predicates. SQLite itself has no quantified comparisons."
320
+ "notes": "ANY/SOME/ALL use full three-valued logic, including when a correlated form is nested below OR, NOT, CASE, or a select expression. SQLite itself has no quantified comparisons."
321
321
  },
322
322
  {
323
323
  "id": "predicate.ilike",
@@ -400,7 +400,19 @@
400
400
  "id": "subquery.correlated-select",
401
401
  "status": "supported",
402
402
  "example": "SELECT r.region, (SELECT AVG(q.amount) FROM rows q WHERE q.region = r.region) AS regional FROM rows r",
403
- "notes": "Correlated scalar aggregates decorrelate in the select list. In a grouped query, their outer references must be GROUP BY columns and the scalar cannot sit inside an outer aggregate."
403
+ "notes": "Correlated scalar aggregates and single-row projections decorrelate in the select list. In a grouped query, their outer references must be GROUP BY columns and the scalar cannot sit inside an outer aggregate."
404
+ },
405
+ {
406
+ "id": "subquery.correlated-select-limit",
407
+ "status": "supported",
408
+ "example": "SELECT r.region, (SELECT q.amount FROM rows q WHERE q.region = r.region ORDER BY q.amount DESC LIMIT 1) AS peak FROM rows r",
409
+ "notes": "ORDER BY, LIMIT, and OFFSET apply independently to each distinct outer probe. Zero rows yield NULL and more than one unbounded row raises a scalar-cardinality error."
410
+ },
411
+ {
412
+ "id": "subquery.correlated-json-aggregate",
413
+ "status": "supported",
414
+ "example": "SELECT r.region, (SELECT JSON_ARRAYAGG(JSON_OBJECT('amount' VALUE q.amount) ORDER BY q.amount) FROM rows q WHERE q.region = r.region) AS amounts FROM rows r",
415
+ "notes": "JSON aggregate expressions use the same set-at-a-time decorrelation as numeric aggregates and preserve their JSON result domain."
404
416
  },
405
417
  {
406
418
  "id": "subquery.correlated-select-grouped",
@@ -418,7 +430,7 @@
418
430
  "id": "subquery.correlated-exists-expression",
419
431
  "status": "supported",
420
432
  "example": "SELECT r.amount FROM rows r WHERE r.amount > 100 OR EXISTS (SELECT d.region FROM dims d WHERE d.region = r.region)",
421
- "notes": "Correlated EXISTS and NOT EXISTS remain set-at-a-time when nested below OR, NOT, or CASE."
433
+ "notes": "Correlated EXISTS and NOT EXISTS remain set-at-a-time below OR, NOT, or CASE and across deeper correlated EXISTS, IN, NOT IN, or scalar blocks. Generated aliases are unique across the complete plan tree."
422
434
  },
423
435
  {
424
436
  "id": "subquery.correlated-non-equi",
@@ -438,6 +450,12 @@
438
450
  "example": "SELECT region FROM rows r WHERE region NOT IN (SELECT d.region FROM dims d WHERE d.region = r.region)",
439
451
  "notes": "Correlated NOT IN preserves empty-set and NULL semantics rather than treating it as a simple anti-join."
440
452
  },
453
+ {
454
+ "id": "subquery.correlated-membership-expression",
455
+ "status": "supported",
456
+ "example": "SELECT r.amount FROM rows r WHERE r.amount = 3 OR r.region NOT IN (SELECT q.region FROM rows q WHERE q.amount < r.amount)",
457
+ "notes": "Correlated IN and NOT IN retain true, false, and unknown results below OR, NOT, CASE, and in select expressions."
458
+ },
441
459
  {
442
460
  "id": "subquery.correlated-not-in-non-equi",
443
461
  "status": "supported",
@@ -447,8 +465,8 @@
447
465
  {
448
466
  "id": "subquery.correlated-quantified",
449
467
  "status": "supported",
450
- "example": "SELECT r.amount FROM rows r WHERE r.amount > ALL (SELECT q.amount FROM rows q WHERE q.region = r.region)",
451
- "notes": "At top-level WHERE, correlated ANY lowers to a semi-join on true comparisons and ALL to an anti-join on false or unknown comparisons."
468
+ "example": "SELECT r.amount FROM rows r WHERE r.amount = 3 OR r.amount > ALL (SELECT q.amount FROM rows q WHERE q.region = r.region)",
469
+ "notes": "Top-level WHERE uses semi/anti joins. Nested expressions group true, false, unknown, and empty-set counts per distinct outer probe tuple."
452
470
  },
453
471
  {
454
472
  "id": "cte.recursive",
@@ -1197,7 +1215,7 @@
1197
1215
  "id": "aggregate.json",
1198
1216
  "status": "supported",
1199
1217
  "example": "SELECT JSON_ARRAYAGG(JSON_OBJECT('region' VALUE region)) AS regions FROM rows",
1200
- "notes": "Supports DISTINCT, embeds JSON-producing inputs as documents, includes SQL NULL as JSON null, and returns NULL for empty input. FILTER, window use, aggregate-local ORDER BY, and explicit NULL/ABSENT clauses are not supported; input order is otherwise unspecified."
1218
+ "notes": "Supports DISTINCT and aggregate-local ORDER BY, embeds JSON-producing inputs as documents, includes SQL NULL as JSON null, and returns NULL for empty input. FILTER, window use, and explicit NULL/ABSENT clauses are not supported; input order is unspecified without ORDER BY."
1201
1219
  },
1202
1220
  {
1203
1221
  "id": "type.array",