@minnowdb/core 0.2.1 → 0.3.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.
Files changed (145) hide show
  1. package/README.md +21 -32
  2. package/dist/engine/artifact-cache.d.ts +2 -0
  3. package/dist/engine/artifact-cache.d.ts.map +1 -1
  4. package/dist/engine/artifact-cache.js +5 -0
  5. package/dist/engine/artifact-cache.js.map +1 -1
  6. package/dist/engine/batch.d.ts +6 -1
  7. package/dist/engine/batch.d.ts.map +1 -1
  8. package/dist/engine/batch.js +17 -6
  9. package/dist/engine/batch.js.map +1 -1
  10. package/dist/engine/catalog.d.ts +22 -2
  11. package/dist/engine/catalog.d.ts.map +1 -1
  12. package/dist/engine/catalog.js +30 -3
  13. package/dist/engine/catalog.js.map +1 -1
  14. package/dist/engine/client.d.ts +2 -0
  15. package/dist/engine/client.d.ts.map +1 -1
  16. package/dist/engine/client.js +20 -10
  17. package/dist/engine/client.js.map +1 -1
  18. package/dist/engine/database.d.ts +67 -5
  19. package/dist/engine/database.d.ts.map +1 -1
  20. package/dist/engine/database.js +3508 -369
  21. package/dist/engine/database.js.map +1 -1
  22. package/dist/engine/defaults.d.ts +4 -7
  23. package/dist/engine/defaults.d.ts.map +1 -1
  24. package/dist/engine/defaults.js +47 -21
  25. package/dist/engine/defaults.js.map +1 -1
  26. package/dist/engine/fts.d.ts.map +1 -1
  27. package/dist/engine/fts.js +2 -0
  28. package/dist/engine/fts.js.map +1 -1
  29. package/dist/engine/index.d.ts +1 -0
  30. package/dist/engine/index.d.ts.map +1 -1
  31. package/dist/engine/index.js +1 -0
  32. package/dist/engine/index.js.map +1 -1
  33. package/dist/engine/live.d.ts.map +1 -1
  34. package/dist/engine/live.js +10 -2
  35. package/dist/engine/live.js.map +1 -1
  36. package/dist/engine/optimizer.d.ts.map +1 -1
  37. package/dist/engine/optimizer.js +258 -23
  38. package/dist/engine/optimizer.js.map +1 -1
  39. package/dist/engine/query-cache.d.ts +1 -1
  40. package/dist/engine/query-cache.d.ts.map +1 -1
  41. package/dist/engine/query-cache.js +3 -1
  42. package/dist/engine/query-cache.js.map +1 -1
  43. package/dist/engine/query.d.ts +130 -22
  44. package/dist/engine/query.d.ts.map +1 -1
  45. package/dist/engine/query.js +1344 -241
  46. package/dist/engine/query.js.map +1 -1
  47. package/dist/engine/schema-wire.d.ts +6 -2
  48. package/dist/engine/schema-wire.d.ts.map +1 -1
  49. package/dist/engine/schema-wire.js +40 -33
  50. package/dist/engine/schema-wire.js.map +1 -1
  51. package/dist/engine/schema.d.ts +157 -76
  52. package/dist/engine/schema.d.ts.map +1 -1
  53. package/dist/engine/schema.js +548 -103
  54. package/dist/engine/schema.js.map +1 -1
  55. package/dist/engine/sort-keys.d.ts +4 -3
  56. package/dist/engine/sort-keys.d.ts.map +1 -1
  57. package/dist/engine/sort-keys.js +42 -27
  58. package/dist/engine/sort-keys.js.map +1 -1
  59. package/dist/engine/sql-domains.d.ts +26 -0
  60. package/dist/engine/sql-domains.d.ts.map +1 -0
  61. package/dist/engine/sql-domains.js +421 -0
  62. package/dist/engine/sql-domains.js.map +1 -0
  63. package/dist/engine/sql-driver.d.ts +19 -0
  64. package/dist/engine/sql-driver.d.ts.map +1 -0
  65. package/dist/engine/sql-driver.js +2 -0
  66. package/dist/engine/sql-driver.js.map +1 -0
  67. package/dist/engine/sql-json.d.ts +7 -8
  68. package/dist/engine/sql-json.d.ts.map +1 -1
  69. package/dist/engine/sql-json.js +28 -14
  70. package/dist/engine/sql-json.js.map +1 -1
  71. package/dist/engine/sql-semantics.d.ts +2 -0
  72. package/dist/engine/sql-semantics.d.ts.map +1 -1
  73. package/dist/engine/sql-semantics.js +69 -3
  74. package/dist/engine/sql-semantics.js.map +1 -1
  75. package/dist/engine/vector.d.ts +5 -1
  76. package/dist/engine/vector.d.ts.map +1 -1
  77. package/dist/engine/vector.js +433 -61
  78. package/dist/engine/vector.js.map +1 -1
  79. package/dist/engine/worker-host.d.ts +1 -0
  80. package/dist/engine/worker-host.d.ts.map +1 -1
  81. package/dist/engine/worker-host.js +4 -0
  82. package/dist/engine/worker-host.js.map +1 -1
  83. package/dist/plan/index.d.ts +3 -3
  84. package/dist/plan/index.js +3 -3
  85. package/dist/storage/indexeddb.d.ts +44 -5
  86. package/dist/storage/indexeddb.d.ts.map +1 -1
  87. package/dist/storage/indexeddb.js +738 -175
  88. package/dist/storage/indexeddb.js.map +1 -1
  89. package/dist/storage/memory.d.ts +27 -10
  90. package/dist/storage/memory.d.ts.map +1 -1
  91. package/dist/storage/memory.js +67 -18
  92. package/dist/storage/memory.js.map +1 -1
  93. package/dist/storage/opfs/leader.d.ts +28 -6
  94. package/dist/storage/opfs/leader.d.ts.map +1 -1
  95. package/dist/storage/opfs/leader.js +301 -34
  96. package/dist/storage/opfs/leader.js.map +1 -1
  97. package/dist/storage/opfs/rpc.d.ts.map +1 -1
  98. package/dist/storage/opfs/rpc.js +2 -1
  99. package/dist/storage/opfs/rpc.js.map +1 -1
  100. package/dist/storage/opfs/store.d.ts +34 -5
  101. package/dist/storage/opfs/store.d.ts.map +1 -1
  102. package/dist/storage/opfs/store.js +41 -0
  103. package/dist/storage/opfs/store.js.map +1 -1
  104. package/dist/storage/snapshot.d.ts +14 -0
  105. package/dist/storage/snapshot.d.ts.map +1 -1
  106. package/dist/storage/snapshot.js +44 -0
  107. package/dist/storage/snapshot.js.map +1 -1
  108. package/dist/storage/toolkit/index.d.ts +1 -1
  109. package/dist/storage/toolkit/index.d.ts.map +1 -1
  110. package/dist/storage/toolkit/index.js +1 -1
  111. package/dist/storage/toolkit/index.js.map +1 -1
  112. package/dist/storage/toolkit/record-core.d.ts +19 -5
  113. package/dist/storage/toolkit/record-core.d.ts.map +1 -1
  114. package/dist/storage/toolkit/record-core.js +353 -78
  115. package/dist/storage/toolkit/record-core.js.map +1 -1
  116. package/dist/storage/toolkit/wire.d.ts +15 -0
  117. package/dist/storage/toolkit/wire.d.ts.map +1 -1
  118. package/dist/storage/toolkit/wire.js +134 -0
  119. package/dist/storage/toolkit/wire.js.map +1 -1
  120. package/dist/storage/types.d.ts +213 -31
  121. package/dist/storage/types.d.ts.map +1 -1
  122. package/dist/storage/types.js +291 -20
  123. package/dist/storage/types.js.map +1 -1
  124. package/dist/testing/block-store-conformance.d.ts.map +1 -1
  125. package/dist/testing/block-store-conformance.js +153 -2
  126. package/dist/testing/block-store-conformance.js.map +1 -1
  127. package/dist/testing/index.d.ts +9 -0
  128. package/dist/testing/index.d.ts.map +1 -1
  129. package/dist/testing/index.js +23 -0
  130. package/dist/testing/index.js.map +1 -1
  131. package/dist/testing/simulator.d.ts +98 -0
  132. package/dist/testing/simulator.d.ts.map +1 -0
  133. package/dist/testing/simulator.js +560 -0
  134. package/dist/testing/simulator.js.map +1 -0
  135. package/dist/testing/sqllogictest.d.ts +90 -0
  136. package/dist/testing/sqllogictest.d.ts.map +1 -0
  137. package/dist/testing/sqllogictest.js +435 -0
  138. package/dist/testing/sqllogictest.js.map +1 -0
  139. package/dist/transactions/index.d.ts +18 -3
  140. package/dist/transactions/index.d.ts.map +1 -1
  141. package/dist/transactions/index.js +64 -8
  142. package/dist/transactions/index.js.map +1 -1
  143. package/package.json +5 -3
  144. package/postgres-feature-profile.json +223 -0
  145. package/sql-feature-matrix.json +172 -252
@@ -3,9 +3,10 @@ import { cachedQueryTerms, ftsBm25Row, FtsStatsAccumulator, ftsMatchTruth, rende
3
3
  import { QueryMemoryContext } from "./memory.js";
4
4
  import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
5
5
  import { stringArgument } from "./sql-semantics.js";
6
- import { jsonAtPath, jsonConstructor, jsonIsValid, parseJsonPath } from "./sql-json.js";
6
+ import { jsonAtPath, jsonConstructor, jsonIsValid, jsonValueOf, parseJsonPath, } from "./sql-json.js";
7
7
  import { optimizePlan } from "./optimizer.js";
8
- import { compareSqlValues as compareValues, compileLikePattern, encodeSqlEqualityValue, roundSqlNumber, } from "./sql-semantics.js";
8
+ import { compareSqlValues as compareValues, compileLikePattern, compileSimilarPattern, encodeSqlEqualityValue, roundSqlNumber, } from "./sql-semantics.js";
9
+ import { arrayDomainValue, collatedDomainValue, exactNumericBinary, exactNumericValue, externalSqlDomainValue, intervalDomainValue, isExactNumeric, isSqlDomainValue, jsonDomainValue, normalizeSqlDomainValue, protectedSqlTextValue, timeDomainValue, uuidDomainValue, } from "./sql-domains.js";
9
10
  import { columnarTableFromRows, prepareVectorQuery, } from "./vector.js";
10
11
  export const scalarFunctionNames = new Set([
11
12
  "ROUND",
@@ -46,6 +47,18 @@ export const scalarFunctionNames = new Set([
46
47
  "JSON_OBJECT",
47
48
  "JSON_ARRAY",
48
49
  "IS_JSON",
50
+ "ARRAY",
51
+ "MINNOW_TUPLE_KEY",
52
+ "MINNOW_COLLATE",
53
+ "NEXTVAL",
54
+ "CURRVAL",
55
+ "RANDOM",
56
+ "GEN_RANDOM_UUID",
57
+ ]);
58
+ /** Functions whose answer can change without any catalog or input-row change. */
59
+ export const volatileScalarFunctionNames = new Set([
60
+ "RANDOM",
61
+ "GEN_RANDOM_UUID",
49
62
  ]);
50
63
  /**
51
64
  * The niladic datetime functions (F051-06/07/08). Their value is the statement's own clock
@@ -103,7 +116,7 @@ function trimEnds(name, value, characters, side) {
103
116
  end -= unit.length;
104
117
  }
105
118
  }
106
- return text.slice(start, end);
119
+ return protectedSqlTextValue(text.slice(start, end));
107
120
  }
108
121
  /**
109
122
  * CAST conversions between the four logical types, matching the strict common ground of
@@ -112,15 +125,30 @@ function trimEnds(name, value, characters, side) {
112
125
  * number cast to datetime reads as milliseconds since the epoch.
113
126
  */
114
127
  function castValue(value, target) {
128
+ if (target.startsWith("numeric")) {
129
+ const [, precision, scale] = target.split(":");
130
+ return exactNumericValue(value, precision === undefined || precision === "" ? undefined : Number(precision), scale === undefined || scale === "" ? undefined : Number(scale));
131
+ }
132
+ if (target === "json")
133
+ return jsonDomainValue(value, false);
134
+ if (target === "jsonb")
135
+ return jsonDomainValue(value, true);
136
+ if (target === "uuid")
137
+ return uuidDomainValue(value);
138
+ if (target === "time")
139
+ return timeDomainValue(value);
140
+ if (target === "interval")
141
+ return intervalDomainValue(value);
115
142
  if (target === "string") {
116
- if (typeof value === "string")
117
- return value;
143
+ const external = externalSqlDomainValue(value);
144
+ if (typeof external === "string")
145
+ return protectedSqlTextValue(external);
118
146
  if (typeof value === "number")
119
- return String(value);
147
+ return protectedSqlTextValue(String(value));
120
148
  if (typeof value === "boolean")
121
- return value ? "true" : "false";
149
+ return protectedSqlTextValue(value ? "true" : "false");
122
150
  if (value instanceof Date)
123
- return value.toISOString();
151
+ return protectedSqlTextValue(value.toISOString());
124
152
  }
125
153
  if (target === "number" || target === "number-integer") {
126
154
  let parsed;
@@ -136,8 +164,15 @@ function castValue(value, target) {
136
164
  }
137
165
  parsed = candidate;
138
166
  }
139
- if (parsed !== undefined)
140
- return target === "number-integer" ? Math.trunc(parsed) : parsed;
167
+ if (parsed !== undefined) {
168
+ if (target !== "number-integer")
169
+ return parsed;
170
+ const integer = Math.trunc(parsed);
171
+ if (!Number.isSafeInteger(integer)) {
172
+ throw new RangeError(`Integer cast is outside the exact safe range: ${String(value)}`);
173
+ }
174
+ return integer;
175
+ }
141
176
  }
142
177
  if (target === "boolean") {
143
178
  if (typeof value === "boolean")
@@ -189,6 +224,10 @@ export function scalarFunctionValue(name, values) {
189
224
  // means a call survived that pass, which would silently give one statement two clocks.
190
225
  throw new TypeError(`${name} must be resolved before execution`);
191
226
  }
227
+ if (name === "RANDOM")
228
+ return Math.random();
229
+ if (name === "GEN_RANDOM_UUID")
230
+ return globalThis.crypto.randomUUID();
192
231
  if (name === "DATE_TRUNC")
193
232
  return dateTruncValue(values[0], values[1]);
194
233
  if (name === "DATE_ADD")
@@ -210,6 +249,21 @@ export function scalarFunctionValue(name, values) {
210
249
  // These build from every argument, so a NULL first one is data, not an early exit.
211
250
  return jsonConstructor(name, values);
212
251
  }
252
+ if (name === "ARRAY")
253
+ return arrayDomainValue(values);
254
+ if (name === "MINNOW_COLLATE")
255
+ return collatedDomainValue(values[0], values[1]);
256
+ if (name === "NEXTVAL" || name === "CURRVAL") {
257
+ throw new TypeError(`${name} must be resolved by the database catalog`);
258
+ }
259
+ if (name === "MINNOW_TUPLE_KEY") {
260
+ // SQL equality cannot match a tuple containing NULL. JSON stringification of the tagged
261
+ // scalar equality encodings is prefix-free and keeps strings, numbers, booleans, and
262
+ // datetimes distinct, so the ordinary single-key hash join can safely carry a composite.
263
+ if (values.some((value) => value === null || value === undefined))
264
+ return null;
265
+ return JSON.stringify(values.map(encodeSqlEqualityValue));
266
+ }
213
267
  const first = values[0];
214
268
  if (first === null || first === undefined)
215
269
  return null;
@@ -256,9 +310,7 @@ export function scalarFunctionValue(name, values) {
256
310
  const search = stringArgument("REPLACE", values[1]);
257
311
  if (search === "")
258
312
  return stringArgument("REPLACE", first);
259
- return stringArgument("REPLACE", first)
260
- .split(search)
261
- .join(stringArgument("REPLACE", values[2]));
313
+ return protectedSqlTextValue(stringArgument("REPLACE", first).split(search).join(stringArgument("REPLACE", values[2])));
262
314
  }
263
315
  case "INSTR": {
264
316
  if (values[1] === null || values[1] === undefined)
@@ -280,9 +332,9 @@ export function scalarFunctionValue(name, values) {
280
332
  case "ABS":
281
333
  return Math.abs(numeric(first));
282
334
  case "UPPER":
283
- return stringArgument("UPPER", first).toUpperCase();
335
+ return protectedSqlTextValue(stringArgument("UPPER", first).toUpperCase());
284
336
  case "LOWER":
285
- return stringArgument("LOWER", first).toLowerCase();
337
+ return protectedSqlTextValue(stringArgument("LOWER", first).toLowerCase());
286
338
  case "TRIM":
287
339
  // SQL TRIM removes spaces, not general whitespace.
288
340
  return trimEnds("TRIM", first, values[1], "both");
@@ -306,7 +358,7 @@ export function scalarFunctionValue(name, values) {
306
358
  return null;
307
359
  if (typeof value === "object")
308
360
  return null;
309
- return typeof value === "string" ? value : JSON.stringify(value);
361
+ return protectedSqlTextValue(typeof value === "string" ? value : JSON.stringify(value));
310
362
  }
311
363
  case "JSON_QUERY": {
312
364
  const found = jsonAtPath(first, values[1], "JSON_QUERY");
@@ -335,12 +387,12 @@ export function scalarFunctionValue(name, values) {
335
387
  const filler = Array.from(fill);
336
388
  // An empty fill cannot pad, so the value passes through, matching PostgreSQL.
337
389
  if (filler.length === 0)
338
- return text.join("");
390
+ return protectedSqlTextValue(text.join(""));
339
391
  const padding = [];
340
392
  while (padding.length < width - text.length) {
341
393
  padding.push(filler[padding.length % filler.length] ?? "");
342
394
  }
343
- return name === "LPAD" ? padding.join("") + text.join("") : text.join("") + padding.join("");
395
+ return protectedSqlTextValue(name === "LPAD" ? padding.join("") + text.join("") : text.join("") + padding.join(""));
344
396
  }
345
397
  case "OVERLAY": {
346
398
  // OVERLAY(s PLACING r FROM start [FOR length]) replaces `length` characters of `s`
@@ -360,16 +412,16 @@ export function scalarFunctionValue(name, values) {
360
412
  if (!Number.isInteger(span) || span < 0) {
361
413
  throw new TypeError("OVERLAY length must be a non-negative integer");
362
414
  }
363
- return [
415
+ return protectedSqlTextValue([
364
416
  ...text.slice(0, start - 1),
365
417
  ...replacement,
366
418
  ...text.slice(Math.min(start - 1 + span, text.length)),
367
- ].join("");
419
+ ].join(""));
368
420
  }
369
421
  case "CAST":
370
422
  return castValue(first, typeof values[1] === "string" ? values[1] : "");
371
423
  case "SUBSTR": {
372
- // SQL:2023 6.32: the result is the characters whose positions fall in both the requested
424
+ // PostgreSQL SUBSTRING returns the characters whose positions fall in both the requested
373
425
  // window and the string, so a start before 1 shortens the result instead of shifting it,
374
426
  // and a window entirely off the string is empty rather than an error.
375
427
  const text = Array.from(stringArgument("SUBSTR", first));
@@ -390,7 +442,7 @@ export function scalarFunctionValue(name, values) {
390
442
  }
391
443
  const from = Math.max(start, 1);
392
444
  const to = Math.min(until, text.length + 1);
393
- return to <= from ? "" : text.slice(from - 1, to - 1).join("");
445
+ return protectedSqlTextValue(to <= from ? "" : text.slice(from - 1, to - 1).join(""));
394
446
  }
395
447
  }
396
448
  }
@@ -512,8 +564,32 @@ export function windowOutputType(window, innerSchema) {
512
564
  if (carries && window.argumentAlias !== undefined) {
513
565
  return innerSchema.find(({ name }) => name === window.argumentAlias)?.type ?? "number";
514
566
  }
567
+ if ((window.name === "SUM" || window.name === "AVG") &&
568
+ window.argumentAlias !== undefined &&
569
+ innerSchema.find(({ name }) => name === window.argumentAlias)?.sqlDomain?.kind === "numeric") {
570
+ return "string";
571
+ }
515
572
  return "number";
516
573
  }
574
+ /** Logical domain carried or produced by a window result, when its physical type is not enough. */
575
+ export function windowOutputDomain(window, innerSchema) {
576
+ if (window.argumentAlias === undefined)
577
+ return undefined;
578
+ const domain = innerSchema.find(({ name }) => name === window.argumentAlias)?.sqlDomain;
579
+ if (domain === undefined)
580
+ return undefined;
581
+ if (window.name === "MIN" ||
582
+ window.name === "MAX" ||
583
+ window.name === "LAG" ||
584
+ window.name === "LEAD" ||
585
+ window.name === "FIRST_VALUE" ||
586
+ window.name === "LAST_VALUE" ||
587
+ window.name === "NTH_VALUE" ||
588
+ ((window.name === "SUM" || window.name === "AVG") && domain.kind === "numeric")) {
589
+ return domain;
590
+ }
591
+ return undefined;
592
+ }
517
593
  const MAX_RECURSIVE_ITERATIONS = 10_000;
518
594
  const MAX_RECURSIVE_ROWS = 1_000_000;
519
595
  /**
@@ -535,8 +611,6 @@ const createTableTypeNames = new Map([
535
611
  ["SMALLINT", "number"],
536
612
  ["REAL", "number"],
537
613
  ["FLOAT", "number"],
538
- ["NUMERIC", "number"],
539
- ["DECIMAL", "number"],
540
614
  ["TEXT", "string"],
541
615
  ["VARCHAR", "string"],
542
616
  ["CHAR", "string"],
@@ -569,7 +643,15 @@ const clauseKeywords = new Set([
569
643
  "UNION",
570
644
  "RETURNING",
571
645
  ]);
572
- const aggregateNames = new Set(["COUNT", "SUM", "AVG", "MIN", "MAX"]);
646
+ const aggregateNames = new Set([
647
+ "COUNT",
648
+ "SUM",
649
+ "AVG",
650
+ "MIN",
651
+ "MAX",
652
+ "JSON_ARRAYAGG",
653
+ "STRING_AGG",
654
+ ]);
573
655
  /** Set functions the parser builds from COUNT/SUM rather than from their own accumulator. */
574
656
  const statisticalAggregates = new Set([
575
657
  "VAR_POP",
@@ -638,26 +720,29 @@ export function compileQuery(sql, options = {}) {
638
720
  compiled.parameterCount = parser.parameterCount;
639
721
  if (parser.usesStatementDatetime)
640
722
  compiled.usesStatementDatetime = true;
723
+ if (parser.usesSequenceCalls)
724
+ compiled.usesSequenceCalls = true;
725
+ if (parser.usesVolatileFunctions)
726
+ compiled.usesVolatileFunctions = true;
641
727
  return compiled;
642
728
  }
643
729
  /**
644
- * Reads a column DEFAULT into the catalog's own representation: a constant, or the
645
- * CURRENT_TIMESTAMP family, which the catalog records as "now" and fills per inserted row.
730
+ * Reads a column DEFAULT into the catalog's own representation. Literal nodes stay structured;
731
+ * every other variable-free SQL expression is preserved as authored for catalog introspection.
646
732
  */
647
- function columnDefaultFor(expression) {
733
+ function columnDefaultFor(expression, sql) {
648
734
  if (expression.kind === "literal") {
649
735
  const value = expression.value;
650
736
  if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
651
737
  return { kind: "literal", value };
652
738
  }
653
739
  if (value instanceof Date)
654
- return { kind: "literal", value: value.toISOString() };
655
- }
656
- if (expression.kind === "call" &&
657
- (expression.name === "CURRENT_TIMESTAMP" || expression.name === "CURRENT_DATE")) {
658
- return { kind: "now" };
740
+ return { kind: "literal", value: new Date(value.getTime()) };
659
741
  }
660
- throw new TypeError("DEFAULT takes a constant or CURRENT_TIMESTAMP");
742
+ return { kind: "expression", sql };
743
+ }
744
+ export function isDefaultInsertValue(value) {
745
+ return (typeof value === "object" && value !== null && !(value instanceof Date) && "default" in value);
661
746
  }
662
747
  /**
663
748
  * Parses CREATE TRIGGER name AFTER INSERT|UPDATE|DELETE ON table [FOR EACH ROW]
@@ -674,6 +759,37 @@ function columnDefaultFor(expression) {
674
759
  * mode a transaction could relax into.
675
760
  */
676
761
  function parseTransactionStatement(keyword, tokens) {
762
+ const identifierAt = (index) => {
763
+ const token = tokens[index];
764
+ if (token?.kind !== "identifier") {
765
+ throw new TypeError(`Expected savepoint name, found ${token?.text ?? "end of input"}`);
766
+ }
767
+ return token.text;
768
+ };
769
+ if (keyword === "SAVEPOINT") {
770
+ const name = identifierAt(1);
771
+ if (tokens[2]?.kind !== "eof")
772
+ throw new TypeError("Unexpected input after SAVEPOINT name");
773
+ return { kind: "transaction", action: "savepoint", name };
774
+ }
775
+ if (keyword === "RELEASE") {
776
+ const hasNoise = tokens[1]?.kind === "identifier" && tokens[1].text.toUpperCase() === "SAVEPOINT";
777
+ const name = identifierAt(hasNoise ? 2 : 1);
778
+ if (tokens[hasNoise ? 3 : 2]?.kind !== "eof") {
779
+ throw new TypeError("Unexpected input after RELEASE name");
780
+ }
781
+ return { kind: "transaction", action: "release", name };
782
+ }
783
+ if (keyword === "ROLLBACK" &&
784
+ tokens[1]?.kind === "identifier" &&
785
+ tokens[1].text.toUpperCase() === "TO") {
786
+ const hasNoise = tokens[2]?.kind === "identifier" && tokens[2].text.toUpperCase() === "SAVEPOINT";
787
+ const name = identifierAt(hasNoise ? 3 : 2);
788
+ if (tokens[hasNoise ? 4 : 3]?.kind !== "eof") {
789
+ throw new TypeError("Unexpected input after ROLLBACK TO name");
790
+ }
791
+ return { kind: "transaction", action: "rollback-to", name };
792
+ }
677
793
  const words = tokens
678
794
  .filter((token) => token.kind === "identifier")
679
795
  .map((token) => token.text.toUpperCase());
@@ -702,6 +818,50 @@ export function compileCheckExpression(sql, name) {
702
818
  }
703
819
  return expression;
704
820
  }
821
+ /**
822
+ * Parses and type-checks one catalog default. PostgreSQL defaults are variable-free scalar
823
+ * expressions: functions and operators are allowed, while row references, parameters,
824
+ * aggregates, windows, and subqueries are not.
825
+ */
826
+ export function validateDefaultExpression(sql, target) {
827
+ const parser = new Parser(tokenize(sql), sql);
828
+ const expression = parser.parseExpression();
829
+ const hasSubquery = (value) => value.kind === "subquery" ||
830
+ value.kind === "exists" ||
831
+ childExpressions(value).some(hasSubquery);
832
+ if (expressionColumns(expression).length > 0 ||
833
+ containsParameter(expression) ||
834
+ hasAggregate(expression) ||
835
+ containsWindow(expression) ||
836
+ hasSubquery(expression)) {
837
+ throw new TypeError(`DEFAULT ${target.name} takes a variable-free scalar SQL expression`);
838
+ }
839
+ const plan = {
840
+ sql: `(default ${target.name})`,
841
+ base: { table: DUAL_TABLE, alias: DUAL_TABLE },
842
+ joins: [],
843
+ select: [{ expression, alias: "value" }],
844
+ predicates: [],
845
+ groupBy: [],
846
+ having: [],
847
+ orderBy: [],
848
+ };
849
+ let inferred;
850
+ try {
851
+ inferred = inferBlockSchema(plan, new Map())[0]?.type;
852
+ }
853
+ catch (error) {
854
+ if (!(error instanceof TypeError) || !error.message.startsWith("Cannot infer a column type")) {
855
+ throw error;
856
+ }
857
+ }
858
+ // A bare NULL is polymorphic in PostgreSQL; the destination column supplies its type.
859
+ if (inferred !== undefined &&
860
+ inferred !== target.type &&
861
+ !(target.sqlDomain?.kind === "numeric" && inferred === "number")) {
862
+ throw new TypeError(`DEFAULT ${target.name} produces ${inferred}, but the column is ${target.type}`);
863
+ }
864
+ }
705
865
  /**
706
866
  * CREATE [OR REPLACE] VIEW name AS <query>. The body is kept as the author's own text rather
707
867
  * than a re-rendered plan: the catalog stores what was written, and every read compiles it
@@ -872,7 +1032,9 @@ export function compileStatement(sql) {
872
1032
  if (keyword === "BEGIN" ||
873
1033
  keyword === "START" ||
874
1034
  keyword === "COMMIT" ||
875
- keyword === "ROLLBACK") {
1035
+ keyword === "ROLLBACK" ||
1036
+ keyword === "SAVEPOINT" ||
1037
+ keyword === "RELEASE") {
876
1038
  return parseTransactionStatement(keyword, tokens);
877
1039
  }
878
1040
  if (keyword === "MERGE") {
@@ -892,18 +1054,62 @@ export function compileStatement(sql) {
892
1054
  if (keyword === "CREATE") {
893
1055
  if (isTriggerDdl)
894
1056
  return parseCreateTrigger(text, tokens);
1057
+ if (second?.kind === "identifier" && second.text.toUpperCase() === "TYPE") {
1058
+ const name = tokens[2];
1059
+ if (name?.kind !== "identifier")
1060
+ throw new TypeError("CREATE TYPE needs a name");
1061
+ const as = tokens[3];
1062
+ const enumToken = tokens[4];
1063
+ if (as?.kind !== "identifier" ||
1064
+ as.text.toUpperCase() !== "AS" ||
1065
+ enumToken?.kind !== "identifier" ||
1066
+ enumToken.text.toUpperCase() !== "ENUM" ||
1067
+ tokens[5]?.text !== "(") {
1068
+ throw new TypeError("CREATE TYPE supports AS ENUM (...)");
1069
+ }
1070
+ const values = [];
1071
+ let index = 6;
1072
+ for (;;) {
1073
+ const value = tokens[index];
1074
+ if (value?.kind !== "string")
1075
+ throw new TypeError("ENUM values must be string literals");
1076
+ values.push(value.text);
1077
+ index += 1;
1078
+ if (tokens[index]?.text === ")")
1079
+ break;
1080
+ if (tokens[index]?.text !== ",")
1081
+ throw new TypeError("Expected comma in ENUM values");
1082
+ index += 1;
1083
+ }
1084
+ if (tokens[index + 1]?.kind !== "eof")
1085
+ throw new TypeError("Unexpected input after ENUM");
1086
+ return { kind: "create-enum", name: name.text, values };
1087
+ }
1088
+ if (second?.kind === "identifier" && second.text.toUpperCase() === "SEQUENCE") {
1089
+ const name = tokens[2];
1090
+ if (name?.kind !== "identifier")
1091
+ throw new TypeError("CREATE SEQUENCE needs a name");
1092
+ if (tokens[3]?.kind !== "eof") {
1093
+ throw new TypeError("CREATE SEQUENCE options are not supported yet");
1094
+ }
1095
+ return { kind: "create-sequence", name: name.text };
1096
+ }
895
1097
  const viewAt = second?.text.toUpperCase() === "OR" ? 3 : 1;
896
1098
  if (tokens[viewAt]?.kind === "identifier" && tokens[viewAt].text.toUpperCase() === "VIEW") {
897
1099
  return parseCreateView(text, tokens, viewAt === 3);
898
1100
  }
899
1101
  parser = new Parser(tokens, text);
1102
+ if (second?.kind === "identifier" &&
1103
+ (second.text.toUpperCase() === "INDEX" || second.text.toUpperCase() === "UNIQUE")) {
1104
+ return parser.parseCreateIndex();
1105
+ }
900
1106
  const statement = parser.parseCreateTable();
901
1107
  if (parser.parameterCount > 0)
902
1108
  statement.parameterCount = parser.parameterCount;
903
1109
  return statement;
904
1110
  }
905
1111
  if (keyword === "ALTER") {
906
- parser = new Parser(tokens);
1112
+ parser = new Parser(tokens, text);
907
1113
  return parser.parseAlterTable();
908
1114
  }
909
1115
  if (keyword === "DROP") {
@@ -919,7 +1125,11 @@ export function compileStatement(sql) {
919
1125
  parser = new Parser(tokens);
920
1126
  return parser.parseDropTable();
921
1127
  }
922
- throw new TypeError("DROP supports: DROP TABLE name, DROP TRIGGER name");
1128
+ if (second?.kind === "identifier" && second.text.toUpperCase() === "INDEX") {
1129
+ parser = new Parser(tokens);
1130
+ return parser.parseDropIndex();
1131
+ }
1132
+ throw new TypeError("DROP supports: DROP TABLE, DROP VIEW, DROP INDEX, DROP TRIGGER");
923
1133
  }
924
1134
  if (keyword === "WITH") {
925
1135
  parser = new Parser(tokens);
@@ -1203,6 +1413,9 @@ function bindExpression(expression, values) {
1203
1413
  }
1204
1414
  if (expression.kind === "call") {
1205
1415
  expression.arguments = expression.arguments.map((argument) => bindExpression(argument, values));
1416
+ for (const order of expression.aggregateOrderBy ?? []) {
1417
+ order.expression = bindExpression(order.expression, values);
1418
+ }
1206
1419
  return expression;
1207
1420
  }
1208
1421
  if (expression.kind === "list") {
@@ -1320,7 +1533,7 @@ export function bindPlanParameters(plan, params) {
1320
1533
  return clone;
1321
1534
  }
1322
1535
  function isParameterSlot(value) {
1323
- return typeof value === "object" && value !== null && !(value instanceof Date);
1536
+ return (typeof value === "object" && value !== null && !(value instanceof Date) && "parameter" in value);
1324
1537
  }
1325
1538
  /** The mutation-statement counterpart of bindPlanParameters; SELECTs bind through their plan. */
1326
1539
  export function bindStatementParameters(statement, params) {
@@ -1332,6 +1545,8 @@ export function bindStatementParameters(statement, params) {
1332
1545
  throw new TypeError("SELECT parameters bind through the query pipeline, not the statement");
1333
1546
  }
1334
1547
  if (statement.kind === "create-table" ||
1548
+ statement.kind === "create-enum" ||
1549
+ statement.kind === "create-sequence" ||
1335
1550
  statement.kind === "create-trigger" ||
1336
1551
  statement.kind === "drop-trigger") {
1337
1552
  throw new TypeError("DDL statements take no parameters");
@@ -1343,6 +1558,14 @@ export function bindStatementParameters(statement, params) {
1343
1558
  if (clone.query !== undefined)
1344
1559
  bindBlock(clone.query, values);
1345
1560
  clone.rows = clone.rows.map((row) => row.map((value) => (isParameterSlot(value) ? (values[value.parameter] ?? null) : value)));
1561
+ if (clone.onConflict?.assignments !== undefined) {
1562
+ for (const assignment of clone.onConflict.assignments) {
1563
+ assignment.expression = bindExpression(assignment.expression, values);
1564
+ }
1565
+ }
1566
+ if (clone.onConflict?.where !== undefined) {
1567
+ clone.onConflict.where = bindExpression(clone.onConflict.where, values);
1568
+ }
1346
1569
  return clone;
1347
1570
  }
1348
1571
  if (clone.kind === "drop-table" ||
@@ -1392,9 +1615,13 @@ export function bindStatementParameters(statement, params) {
1392
1615
  export function inferBlockSchema(plan, schemas) {
1393
1616
  const sources = [plan.base, ...plan.joins];
1394
1617
  const multipleSources = sources.length > 1;
1395
- const wildcardSchema = (source) => (schemas.get(source.table) ?? []).map((column) => ({
1618
+ const wildcardSchema = (source) => (schemas.get(source.table) ?? [])
1619
+ .filter((column) => !column.name.startsWith("\0"))
1620
+ .map((column) => ({
1396
1621
  name: multipleSources ? `${source.alias}.${column.name}` : column.name,
1397
1622
  type: column.type,
1623
+ ...(column.integer === true ? { integer: true } : {}),
1624
+ ...(column.sqlDomain === undefined ? {} : { sqlDomain: column.sqlDomain }),
1398
1625
  }));
1399
1626
  if (plan.select[0]?.expression.kind === "wildcard" &&
1400
1627
  plan.select[0].expression.table === undefined)
@@ -1417,6 +1644,84 @@ export function inferBlockSchema(plan, schemas) {
1417
1644
  throw new TypeError(`Ambiguous or missing column: ${reference}`);
1418
1645
  return matches[0] ?? "string";
1419
1646
  };
1647
+ const resolveColumnInteger = (reference) => {
1648
+ const parts = reference.split(".");
1649
+ if (parts.length === 2) {
1650
+ const source = sources.find(({ alias }) => alias === parts[0]);
1651
+ return (source !== undefined &&
1652
+ (schemas.get(source.table) ?? []).find(({ name }) => name === parts[1])?.integer === true);
1653
+ }
1654
+ const matches = sources.flatMap((source) => (schemas.get(source.table) ?? []).filter(({ name }) => name === parts[0]));
1655
+ return matches.length === 1 && matches[0]?.integer === true;
1656
+ };
1657
+ const resolveColumnDomain = (reference) => {
1658
+ const parts = reference.split(".");
1659
+ if (parts.length === 2) {
1660
+ const source = sources.find(({ alias }) => alias === parts[0]);
1661
+ return source === undefined
1662
+ ? undefined
1663
+ : (schemas.get(source.table) ?? []).find(({ name }) => name === parts[1])?.sqlDomain;
1664
+ }
1665
+ const matches = sources.flatMap((source) => (schemas.get(source.table) ?? []).filter(({ name }) => name === parts[0]));
1666
+ return matches.length === 1 ? matches[0]?.sqlDomain : undefined;
1667
+ };
1668
+ const castDomain = (target) => {
1669
+ if (target.startsWith("numeric:")) {
1670
+ const [, precisionWord = "", scaleWord = ""] = target.split(":");
1671
+ const precision = precisionWord === "" ? undefined : Number(precisionWord);
1672
+ const scale = scaleWord === "" ? undefined : Number(scaleWord);
1673
+ return {
1674
+ kind: "numeric",
1675
+ ...(precision === undefined ? {} : { precision }),
1676
+ ...(scale === undefined ? {} : { scale }),
1677
+ };
1678
+ }
1679
+ if (target === "json" || target === "jsonb" || target === "uuid" || target === "time") {
1680
+ return { kind: target };
1681
+ }
1682
+ if (target === "interval")
1683
+ return { kind: "interval" };
1684
+ return undefined;
1685
+ };
1686
+ const inferDomain = (expression) => {
1687
+ if (expression.kind === "literal")
1688
+ return expression.sqlDomain;
1689
+ if (expression.kind === "column")
1690
+ return resolveColumnDomain(expression.reference);
1691
+ if (expression.kind === "binary") {
1692
+ const left = inferDomain(expression.left);
1693
+ const right = inferDomain(expression.right);
1694
+ return left?.kind === "numeric" || right?.kind === "numeric"
1695
+ ? { kind: "numeric" }
1696
+ : undefined;
1697
+ }
1698
+ if (expression.kind === "case") {
1699
+ const outcomes = [
1700
+ ...expression.branches.map((branch) => branch.then),
1701
+ ...(expression.otherwise === undefined ? [] : [expression.otherwise]),
1702
+ ];
1703
+ return outcomes.map(inferDomain).find((domain) => domain !== undefined);
1704
+ }
1705
+ if (expression.kind !== "call")
1706
+ return undefined;
1707
+ if (expression.name === "CAST") {
1708
+ const target = expression.arguments[1];
1709
+ return target?.kind === "literal" && typeof target.value === "string"
1710
+ ? castDomain(target.value)
1711
+ : undefined;
1712
+ }
1713
+ if (expression.name === "SUM" ||
1714
+ expression.name === "AVG" ||
1715
+ expression.name === "MIN" ||
1716
+ expression.name === "MAX" ||
1717
+ expression.name === "COALESCE" ||
1718
+ expression.name === "NULLIF" ||
1719
+ expression.name === "GREATEST" ||
1720
+ expression.name === "LEAST") {
1721
+ return expression.arguments.map(inferDomain).find((domain) => domain !== undefined);
1722
+ }
1723
+ return undefined;
1724
+ };
1420
1725
  const infer = (expression) => {
1421
1726
  if (expression.kind === "subquery" || expression.kind === "list") {
1422
1727
  throw new TypeError("Subqueries must be resolved before schema inference");
@@ -1489,17 +1794,21 @@ export function inferBlockSchema(plan, schemas) {
1489
1794
  }
1490
1795
  return "string";
1491
1796
  }
1797
+ const exactNumeric = inferDomain(expression)?.kind === "numeric";
1492
1798
  for (const side of [expression.left, expression.right]) {
1493
1799
  const type = infer(side);
1494
- if (type === "string" || type === "boolean") {
1800
+ if (type === "boolean" || (type === "string" && inferDomain(side)?.kind !== "numeric")) {
1495
1801
  throw new TypeError(`Arithmetic requires numeric operands: ${plan.sql}`);
1496
1802
  }
1497
1803
  }
1498
- return "number";
1804
+ return exactNumeric ? "string" : "number";
1499
1805
  }
1500
- if (expression.name === "COUNT" || expression.name === "SUM" || expression.name === "AVG") {
1806
+ if (expression.name === "COUNT") {
1501
1807
  return "number";
1502
1808
  }
1809
+ if (expression.name === "SUM" || expression.name === "AVG") {
1810
+ return inferDomain(expression)?.kind === "numeric" ? "string" : "number";
1811
+ }
1503
1812
  if (expression.name === "ROUND" ||
1504
1813
  expression.name === "LENGTH" ||
1505
1814
  expression.name === "ABS" ||
@@ -1511,13 +1820,22 @@ export function inferBlockSchema(plan, schemas) {
1511
1820
  expression.name === "INSTR" ||
1512
1821
  expression.name === "EXTRACT" ||
1513
1822
  expression.name === "OCTET_LENGTH" ||
1514
- expression.name === "GROUPING") {
1823
+ expression.name === "GROUPING" ||
1824
+ expression.name === "NEXTVAL" ||
1825
+ expression.name === "CURRVAL" ||
1826
+ expression.name === "RANDOM") {
1515
1827
  return "number";
1516
1828
  }
1517
- if (expression.name === "JSON_VALUE" ||
1829
+ if (expression.name === "JSON_ARRAYAGG" ||
1830
+ expression.name === "STRING_AGG" ||
1831
+ expression.name === "JSON_VALUE" ||
1518
1832
  expression.name === "JSON_QUERY" ||
1519
1833
  expression.name === "JSON_OBJECT" ||
1520
- expression.name === "JSON_ARRAY") {
1834
+ expression.name === "JSON_ARRAY" ||
1835
+ expression.name === "ARRAY" ||
1836
+ expression.name === "MINNOW_TUPLE_KEY" ||
1837
+ expression.name === "MINNOW_COLLATE" ||
1838
+ expression.name === "GEN_RANDOM_UUID") {
1521
1839
  return "string";
1522
1840
  }
1523
1841
  if (expression.name === "JSON_EXISTS" || expression.name === "IS_JSON")
@@ -1527,8 +1845,8 @@ export function inferBlockSchema(plan, schemas) {
1527
1845
  if (expression.name === "CURRENT_DATE" || expression.name === "CURRENT_TIMESTAMP") {
1528
1846
  return "datetime";
1529
1847
  }
1530
- // The engine has no TIME type, so LOCALTIME reads as an 'HH:MM:SS' string, like SQLite's
1531
- // CURRENT_TIME.
1848
+ // LOCALTIME is the statement clock's canonical 'HH:MM:SS' text; explicit TIME values use
1849
+ // the same public representation while carrying a logical domain internally.
1532
1850
  if (expression.name === "LOCALTIME")
1533
1851
  return "string";
1534
1852
  if (expression.name === "NULLIF" ||
@@ -1552,6 +1870,8 @@ export function inferBlockSchema(plan, schemas) {
1552
1870
  const word = target?.kind === "literal" && typeof target.value === "string" ? target.value : "";
1553
1871
  if (word === "number-integer")
1554
1872
  return "number";
1873
+ if (castDomain(word) !== undefined)
1874
+ return "string";
1555
1875
  if (word === "number" || word === "string" || word === "boolean" || word === "datetime") {
1556
1876
  return word;
1557
1877
  }
@@ -1604,9 +1924,60 @@ export function inferBlockSchema(plan, schemas) {
1604
1924
  if (type === "null") {
1605
1925
  throw new TypeError(`Cannot infer a column type for output ${item.alias}`);
1606
1926
  }
1607
- return [{ name: item.alias, type }];
1927
+ const integer = (item.expression.kind === "column" && resolveColumnInteger(item.expression.reference)) ||
1928
+ (item.expression.kind === "call" &&
1929
+ item.expression.name === "CAST" &&
1930
+ item.expression.arguments[1]?.kind === "literal" &&
1931
+ item.expression.arguments[1].value === "number-integer") ||
1932
+ (item.expression.kind === "call" && item.expression.name === "COUNT");
1933
+ const sqlDomain = inferDomain(item.expression);
1934
+ return [
1935
+ {
1936
+ name: item.alias,
1937
+ type,
1938
+ ...(integer ? { integer: true } : {}),
1939
+ ...(sqlDomain === undefined ? {} : { sqlDomain }),
1940
+ },
1941
+ ];
1608
1942
  });
1609
1943
  }
1944
+ /** Whether a final result can contain one of the engine's tagged string-domain values. */
1945
+ export function queryResultNeedsExternalization(plan, schemas) {
1946
+ // This is only a result-boundary optimization flag, not a second typecheck. In particular,
1947
+ // PostgreSQL permits an untyped NULL output and Minnow historically permits heterogeneous CASE
1948
+ // results, so calling inferBlockSchema here would make execution stricter just because tagged
1949
+ // domains exist elsewhere in the engine. Be conservative instead: externalization is a cheap
1950
+ // prefix check, and only domain-bearing schemas or expressions pay it.
1951
+ const createsDomain = (expression) => {
1952
+ if (expression.kind === "literal" && expression.internalSqlValue === true)
1953
+ return true;
1954
+ if (expression.kind === "call") {
1955
+ if (expression.name === "ARRAY" || expression.name === "MINNOW_COLLATE")
1956
+ return true;
1957
+ if (expression.name === "CAST") {
1958
+ const target = expression.arguments[1];
1959
+ if (target?.kind === "literal" &&
1960
+ typeof target.value === "string" &&
1961
+ (target.value.startsWith("numeric:") ||
1962
+ ["json", "jsonb", "uuid", "time", "interval"].includes(target.value))) {
1963
+ return true;
1964
+ }
1965
+ }
1966
+ }
1967
+ return childExpressions(expression).some(createsDomain);
1968
+ };
1969
+ try {
1970
+ // Plain TEXT uses a protected execution-time wrapper when it happens to share the internal
1971
+ // domain namespace, so string outputs cross the same boundary as logical SQL domains.
1972
+ return inferBlockSchema(plan, schemas).some((column) => column.type === "string" || column.sqlDomain !== undefined);
1973
+ }
1974
+ catch {
1975
+ // This flag must not make execution stricter for historically accepted shapes such as an
1976
+ // untyped NULL or heterogeneous CASE. When exact inference cannot answer, scan if any source
1977
+ // or expression could carry an internal string; externalization itself remains copy-on-change.
1978
+ return ([...schemas.values()].some((columns) => columns.some((column) => column.type === "string" || column.sqlDomain !== undefined)) || plan.select.some(({ expression }) => createsDomain(expression)));
1979
+ }
1980
+ }
1610
1981
  export function referencedColumns(plan, schemas) {
1611
1982
  const sources = [plan.base, ...plan.joins];
1612
1983
  const sourceAliases = sources.map((source) => source.alias);
@@ -1656,8 +2027,13 @@ export function referencedColumns(plan, schemas) {
1656
2027
  for (const source of sources) {
1657
2028
  if (named !== undefined && source.alias !== named)
1658
2029
  continue;
1659
- for (const column of schemas.get(source.table) ?? [])
2030
+ for (const column of schemas.get(source.table) ?? []) {
2031
+ // Storage may carry unspellable NUL-prefixed locator columns for composite identity.
2032
+ // Explicit internal references can request one; SQL wildcards are public schema only.
2033
+ if (column.startsWith("\u0000"))
2034
+ continue;
1660
2035
  requested.get(source.table)?.add(column);
2036
+ }
1661
2037
  }
1662
2038
  }
1663
2039
  };
@@ -1746,12 +2122,16 @@ export function createPreparedQuery(plan, tables, options = {}) {
1746
2122
  const ties = withTiesPlan(plan);
1747
2123
  if (ties.plan !== plan)
1748
2124
  return trimPreparedResults(createPreparedQuery(ties.plan, tables, options), ties.trim);
2125
+ // Rows passed to this low-level public entry point are public SQL values. Escape the private
2126
+ // string-domain namespace before derived blocks add their own already-internal results. This
2127
+ // comes after WITH TIES lowering because that lowering recursively prepares the same input.
2128
+ tables = cloneRowTables(tables, true);
1749
2129
  // Every engine entry expands MATCH(*) exactly once, here against the row tables' own
1750
2130
  // columns; past this point no executor sees the "*" sentinel.
1751
2131
  plan = expandFtsColumns(plan, rowTableSearchableColumns(tables));
1752
2132
  const resolution = subqueryResolutionSteps(plan);
1753
2133
  for (const step of resolution.steps)
1754
- step.substitute(executeRowQuery(step.block, tables));
2134
+ step.substitute(executeRowQueryInternal(step.block, tables));
1755
2135
  plan = resolution.plan;
1756
2136
  tables = resolveDerivedRowTables(plan, tables);
1757
2137
  plan = expandDistinctWildcard(plan, wildcardRowColumns(tables));
@@ -1764,7 +2144,7 @@ export function createPreparedQuery(plan, tables, options = {}) {
1764
2144
  return createPreparedRowQuery(plan, tables, memory);
1765
2145
  }
1766
2146
  try {
1767
- return createPreparedColumnarQuery(plan, normalizeColumnarTables(plan, tables), memory);
2147
+ return trimPreparedResults(createPreparedColumnarQuery(plan, normalizeColumnarTables(plan, tables), memory), externalizeQueryResult);
1768
2148
  }
1769
2149
  catch (error) {
1770
2150
  memory.close();
@@ -1799,6 +2179,7 @@ export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemo
1799
2179
  memory.close();
1800
2180
  throw error;
1801
2181
  }
2182
+ const outputNeedsExternalization = options.outputNeedsExternalization;
1802
2183
  return {
1803
2184
  sql: plan.sql,
1804
2185
  tables: [plan.base.table, ...plan.joins.map((join) => join.table)],
@@ -1808,12 +2189,12 @@ export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemo
1808
2189
  execute() {
1809
2190
  if (closed || prepared === undefined)
1810
2191
  throw new Error("Prepared query is closed");
1811
- return prepared.execute();
2192
+ return markExternalizationState(prepared.execute(), outputNeedsExternalization);
1812
2193
  },
1813
2194
  async executeAsync(options) {
1814
2195
  if (closed || prepared === undefined)
1815
2196
  throw new Error("Prepared query is closed");
1816
- return prepared.executeAsync(options);
2197
+ return markExternalizationState(await prepared.executeAsync(options), outputNeedsExternalization);
1817
2198
  },
1818
2199
  close() {
1819
2200
  if (closed)
@@ -1837,12 +2218,12 @@ function createPreparedRowQuery(plan, tables, memory) {
1837
2218
  execute() {
1838
2219
  if (closed || rows === undefined)
1839
2220
  throw new Error("Prepared query is closed");
1840
- return executeRowQuery(plan, cloneRowTables(rows));
2221
+ return externalizeQueryResult(executeRowQueryInternal(plan, cloneRowTables(rows)));
1841
2222
  },
1842
2223
  async executeAsync() {
1843
2224
  if (closed || rows === undefined)
1844
2225
  throw new Error("Prepared query is closed");
1845
- return executeRowQuery(plan, cloneRowTables(rows));
2226
+ return externalizeQueryResult(executeRowQueryInternal(plan, cloneRowTables(rows)));
1846
2227
  },
1847
2228
  close() {
1848
2229
  closed = true;
@@ -1851,19 +2232,23 @@ function createPreparedRowQuery(plan, tables, memory) {
1851
2232
  },
1852
2233
  };
1853
2234
  }
1854
- function cloneRowTables(tables) {
2235
+ function cloneRowTables(tables, protectText = false) {
1855
2236
  return new Map([...tables].map(([name, rows]) => [
1856
2237
  name,
1857
2238
  rows.map((row) => Object.fromEntries(Object.entries(row).map(([column, value]) => [
1858
2239
  column,
1859
- value instanceof Date ? new Date(value.getTime()) : value,
2240
+ value instanceof Date
2241
+ ? new Date(value.getTime())
2242
+ : protectText && typeof value === "string"
2243
+ ? protectedSqlTextValue(value)
2244
+ : value,
1860
2245
  ]))),
1861
2246
  ]));
1862
2247
  }
1863
2248
  export function executeQuery(plan, tables, options = {}) {
1864
2249
  const prepared = createPreparedQuery(plan, tables, options);
1865
2250
  try {
1866
- return prepared.execute();
2251
+ return externalizeQueryResult(prepared.execute());
1867
2252
  }
1868
2253
  finally {
1869
2254
  prepared.close();
@@ -1993,8 +2378,12 @@ export function createRecursiveCteState(base, all) {
1993
2378
  export function childExpressions(expression) {
1994
2379
  if (expression.kind === "binary")
1995
2380
  return [expression.left, expression.right];
1996
- if (expression.kind === "call")
1997
- return [...expression.arguments];
2381
+ if (expression.kind === "call") {
2382
+ return [
2383
+ ...expression.arguments,
2384
+ ...(expression.aggregateOrderBy ?? []).map((order) => order.expression),
2385
+ ];
2386
+ }
1998
2387
  if (expression.kind === "list")
1999
2388
  return [...expression.items];
2000
2389
  if (expression.kind === "condition" || expression.kind === "logical") {
@@ -2035,7 +2424,18 @@ export function mapChildExpressions(expression, map) {
2035
2424
  if (expression.kind === "not")
2036
2425
  return { ...expression, operand: map(expression.operand) };
2037
2426
  if (expression.kind === "call") {
2038
- return { ...expression, arguments: expression.arguments.map(map) };
2427
+ return {
2428
+ ...expression,
2429
+ arguments: expression.arguments.map(map),
2430
+ ...(expression.aggregateOrderBy === undefined
2431
+ ? {}
2432
+ : {
2433
+ aggregateOrderBy: expression.aggregateOrderBy.map((order) => ({
2434
+ ...order,
2435
+ expression: map(order.expression),
2436
+ })),
2437
+ }),
2438
+ };
2039
2439
  }
2040
2440
  if (expression.kind === "list")
2041
2441
  return { ...expression, items: expression.items.map(map) };
@@ -2284,6 +2684,21 @@ function aggregateWindowMembers(window, values, members) {
2284
2684
  if (present.length === 0)
2285
2685
  return null;
2286
2686
  if (window.name === "SUM" || window.name === "AVG") {
2687
+ const exact = present.some((member) => isExactNumeric(values[member]));
2688
+ if (exact) {
2689
+ let total = exactNumericValue(0);
2690
+ for (const member of present) {
2691
+ const value = values[member];
2692
+ if (!isExactNumeric(value)) {
2693
+ throw new TypeError("Exact NUMERIC window input mixed with an approximate number");
2694
+ }
2695
+ const next = exactNumericBinary("+", total, value);
2696
+ if (next === null || next === undefined)
2697
+ throw new Error("Exact NUMERIC sum disappeared");
2698
+ total = next;
2699
+ }
2700
+ return window.name === "SUM" ? total : exactNumericBinary("/", total, present.length);
2701
+ }
2287
2702
  const total = present.reduce((sum, member) => sum + numeric(values[member]), 0);
2288
2703
  return window.name === "SUM" ? total : total / present.length;
2289
2704
  }
@@ -2342,17 +2757,39 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
2342
2757
  }
2343
2758
  }
2344
2759
  const values = [];
2345
- const prefixNonNull = new Float64Array(size + 1);
2346
- const prefixSum = new Float64Array(size + 1);
2347
2760
  const sums = window.name === "SUM" || window.name === "AVG";
2348
2761
  for (let position = 0; position < size; position += 1) {
2349
2762
  const value = window.argumentAlias === undefined
2350
2763
  ? undefined
2351
2764
  : (rows[indexes[start + position] ?? -1]?.[window.argumentAlias] ?? null);
2352
2765
  values.push(value);
2766
+ }
2767
+ const exactSums = sums && values.some((value) => isExactNumeric(value));
2768
+ const prefixNonNull = new Float64Array(size + 1);
2769
+ const prefixSum = exactSums ? undefined : new Float64Array(size + 1);
2770
+ const prefixExact = exactSums ? new Array(size + 1) : undefined;
2771
+ const exactZero = exactNumericValue(0);
2772
+ if (exactZero === null)
2773
+ throw new Error("Exact NUMERIC zero disappeared");
2774
+ if (prefixExact !== undefined)
2775
+ prefixExact[0] = exactZero;
2776
+ for (let position = 0; position < size; position += 1) {
2777
+ const value = values[position];
2353
2778
  const nonNull = window.argumentAlias !== undefined && value !== null && value !== undefined;
2354
2779
  prefixNonNull[position + 1] = (prefixNonNull[position] ?? 0) + (nonNull ? 1 : 0);
2355
- prefixSum[position + 1] = (prefixSum[position] ?? 0) + (sums && nonNull ? numeric(value) : 0);
2780
+ if (prefixExact !== undefined) {
2781
+ if (nonNull && !isExactNumeric(value)) {
2782
+ throw new TypeError("Exact NUMERIC window input mixed with an approximate number");
2783
+ }
2784
+ const previous = prefixExact[position] ?? exactZero;
2785
+ const next = nonNull ? exactNumericBinary("+", previous, value) : previous;
2786
+ if (next === null || next === undefined)
2787
+ throw new Error("Exact NUMERIC sum disappeared");
2788
+ prefixExact[position + 1] = next;
2789
+ }
2790
+ else if (prefixSum !== undefined) {
2791
+ prefixSum[position + 1] = (prefixSum[position] ?? 0) + (sums && nonNull ? numeric(value) : 0);
2792
+ }
2356
2793
  }
2357
2794
  // GROUPS frames count peer groups rather than rows, so each position needs its group's
2358
2795
  // ordinal and the group boundaries to translate an offset back into row positions.
@@ -2459,8 +2896,17 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
2459
2896
  }
2460
2897
  else if (sums) {
2461
2898
  const nonNull = (prefixNonNull[high] ?? 0) - (prefixNonNull[low] ?? 0);
2462
- const total = (prefixSum[high] ?? 0) - (prefixSum[low] ?? 0);
2463
- value = nonNull === 0 ? null : window.name === "SUM" ? total : total / nonNull;
2899
+ if (nonNull === 0) {
2900
+ value = null;
2901
+ }
2902
+ else if (prefixExact !== undefined) {
2903
+ const total = exactNumericBinary("-", prefixExact[high] ?? exactZero, prefixExact[low] ?? exactZero);
2904
+ value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull);
2905
+ }
2906
+ else {
2907
+ const total = (prefixSum?.[high] ?? 0) - (prefixSum?.[low] ?? 0);
2908
+ value = window.name === "SUM" ? total : total / nonNull;
2909
+ }
2464
2910
  }
2465
2911
  else {
2466
2912
  let best;
@@ -2681,28 +3127,28 @@ function resolveDerivedRowTables(plan, tables) {
2681
3127
  for (const source of sources) {
2682
3128
  if (source.recursive !== undefined) {
2683
3129
  const { reference, base, step, all } = source.recursive;
2684
- const state = createRecursiveCteState(executeRowQuery(base, tables), all);
3130
+ const state = createRecursiveCteState(executeRowQueryInternal(base, tables), all);
2685
3131
  while (state.frontier.length > 0) {
2686
3132
  const stepTables = new Map(tables);
2687
3133
  stepTables.set(reference, state.frontier);
2688
- state.absorb(executeRowQuery(step, stepTables));
3134
+ state.absorb(executeRowQueryInternal(step, stepTables));
2689
3135
  }
2690
3136
  resolved.set(source.table, state.rows);
2691
3137
  continue;
2692
3138
  }
2693
3139
  if (source.union !== undefined) {
2694
- const results = source.union.blocks.map((block) => executeRowQuery(block, tables));
3140
+ const results = source.union.blocks.map((block) => executeRowQueryInternal(block, tables));
2695
3141
  resolved.set(source.table, combineUnionResults(results, source.union.ops).rows);
2696
3142
  continue;
2697
3143
  }
2698
3144
  if (source.windowed !== undefined) {
2699
- const inner = executeRowQuery(source.windowed.block, tables);
3145
+ const inner = executeRowQueryInternal(source.windowed.block, tables);
2700
3146
  resolved.set(source.table, applyWindowFunctions(inner, source.windowed.windows).rows);
2701
3147
  continue;
2702
3148
  }
2703
3149
  if (source.derived === undefined)
2704
3150
  continue;
2705
- resolved.set(source.table, executeRowQuery(source.derived, tables).rows);
3151
+ resolved.set(source.table, executeRowQueryInternal(source.derived, tables).rows);
2706
3152
  }
2707
3153
  return resolved;
2708
3154
  }
@@ -2717,11 +3163,14 @@ function normalizeColumnarTables(plan, tables) {
2717
3163
  .filter(([name]) => requiredTables.has(name))
2718
3164
  .map(([name, table]) => [
2719
3165
  name,
2720
- columnarTableFromRows(name, table, requestedColumns.get(name) ?? []),
3166
+ columnarTableFromRows(name, table, requestedColumns.get(name) ?? [], false),
2721
3167
  ]));
2722
3168
  }
2723
3169
  /** Correctness reference retained while the vector executor matures. */
2724
3170
  export function executeRowQuery(plan, tables) {
3171
+ return externalizeQueryResult(executeRowQueryInternal(plan, cloneRowTables(tables, true)));
3172
+ }
3173
+ function executeRowQueryInternal(plan, tables) {
2725
3174
  assertTailParametersBound(plan);
2726
3175
  validateGrouping(plan);
2727
3176
  plan = resolveStatementDatetimes(plan);
@@ -2732,8 +3181,9 @@ export function executeRowQuery(plan, tables) {
2732
3181
  plan = ties.plan;
2733
3182
  plan = expandFtsColumns(plan, rowTableSearchableColumns(tables));
2734
3183
  const resolution = subqueryResolutionSteps(plan);
2735
- for (const step of resolution.steps)
2736
- step.substitute(executeRowQuery(step.block, tables));
3184
+ for (const step of resolution.steps) {
3185
+ step.substitute(executeRowQueryInternal(step.block, tables));
3186
+ }
2737
3187
  // Stats annotation writes into the plan; when resolution had nothing to clone, the plan is
2738
3188
  // still the caller's (possibly cached) object, and frozen statistics would survive into later
2739
3189
  // executions against different rows.
@@ -2785,12 +3235,13 @@ export function executeRowQuery(plan, tables) {
2785
3235
  : [];
2786
3236
  const sortColumns = plan.orderBy.map(({ expression, direction, nulls }) => ({
2787
3237
  outputName: orderOutputName(expression, plan.select, orderSources),
3238
+ direction,
2788
3239
  multiplier: direction === "desc" ? -1 : 1,
2789
3240
  nulls,
2790
3241
  }));
2791
3242
  rows.sort((left, right) => {
2792
- for (const { outputName, multiplier, nulls } of sortColumns) {
2793
- const placed = explicitNullOrder(left[outputName], right[outputName], nulls);
3243
+ for (const { outputName, direction, multiplier, nulls } of sortColumns) {
3244
+ const placed = nullOrder(left[outputName], right[outputName], nulls, direction);
2794
3245
  if (placed !== undefined && placed !== 0)
2795
3246
  return placed;
2796
3247
  const comparison = compareValues(left[outputName], right[outputName]);
@@ -2876,11 +3327,15 @@ function executeJoin(contexts, join, rows) {
2876
3327
  const candidate = { ...context, [join.alias]: row };
2877
3328
  if (evaluateBooleanExpression(condition, (nested) => evaluate(nested, candidate)) === true) {
2878
3329
  matched = true;
2879
- joined.push(candidate);
3330
+ if (join.kind !== "anti")
3331
+ joined.push(join.kind === "semi" ? context : candidate);
3332
+ if (join.kind === "semi" || join.kind === "anti")
3333
+ break;
2880
3334
  }
2881
3335
  }
2882
- if (!matched && join.kind === "left")
2883
- joined.push({ ...context, [join.alias]: undefined });
3336
+ if (!matched && (join.kind === "left" || join.kind === "anti")) {
3337
+ joined.push(join.kind === "anti" ? context : { ...context, [join.alias]: undefined });
3338
+ }
2884
3339
  }
2885
3340
  return joined;
2886
3341
  }
@@ -2905,11 +3360,16 @@ function executeJoin(contexts, join, rows) {
2905
3360
  for (const context of contexts) {
2906
3361
  const leftKey = comparable(evaluate(leftExpression, context));
2907
3362
  const matches = isSqlJoinKey(leftKey) ? (index.get(leftKey) ?? []) : [];
2908
- if (matches.length === 0 && join.kind === "left")
2909
- joined.push({ ...context, [join.alias]: undefined });
2910
- else
3363
+ if (matches.length === 0 && (join.kind === "left" || join.kind === "anti")) {
3364
+ joined.push(join.kind === "anti" ? context : { ...context, [join.alias]: undefined });
3365
+ }
3366
+ else if (matches.length > 0 && join.kind === "semi") {
3367
+ joined.push(context);
3368
+ }
3369
+ else if (join.kind !== "anti") {
2911
3370
  for (const row of matches)
2912
3371
  joined.push({ ...context, [join.alias]: row });
3372
+ }
2913
3373
  }
2914
3374
  return joined;
2915
3375
  }
@@ -2929,7 +3389,9 @@ function project(select, context, group) {
2929
3389
  function evaluate(expression, context, group) {
2930
3390
  switch (expression.kind) {
2931
3391
  case "literal":
2932
- return expression.value;
3392
+ return typeof expression.value === "string" && expression.internalSqlValue !== true
3393
+ ? protectedSqlTextValue(expression.value)
3394
+ : expression.value;
2933
3395
  case "parameter":
2934
3396
  throw new TypeError(`Placeholder $${String(expression.index + 1)} is unbound; pass parameters when executing`);
2935
3397
  case "wildcard":
@@ -2981,8 +3443,11 @@ function evaluate(expression, context, group) {
2981
3443
  if (typeof left !== "string" || typeof right !== "string") {
2982
3444
  throw new TypeError("|| requires string operands");
2983
3445
  }
2984
- return left + right;
3446
+ return protectedSqlTextValue(String(externalSqlDomainValue(left)) + String(externalSqlDomainValue(right)));
2985
3447
  }
3448
+ const exact = exactNumericBinary(expression.operator, left, right);
3449
+ if (exact !== undefined)
3450
+ return exact;
2986
3451
  const a = numeric(left);
2987
3452
  const b = numeric(right);
2988
3453
  if (expression.operator === "+")
@@ -3001,11 +3466,69 @@ function evaluate(expression, context, group) {
3001
3466
  if (group === undefined)
3002
3467
  throw new TypeError(`${expression.name} requires grouped execution`);
3003
3468
  const argument = expression.arguments[0] ?? { kind: "wildcard" };
3469
+ if (expression.name === "STRING_AGG") {
3470
+ const delimiter = expression.arguments[1];
3471
+ if (delimiter === undefined)
3472
+ throw new TypeError("STRING_AGG requires a delimiter");
3473
+ let members = group.flatMap((row) => {
3474
+ const value = evaluate(argument, row);
3475
+ if (value === null || value === undefined)
3476
+ return [];
3477
+ if (typeof value !== "string") {
3478
+ throw new TypeError("STRING_AGG value must be a string");
3479
+ }
3480
+ const separator = evaluate(delimiter, row);
3481
+ if (separator !== null && separator !== undefined && typeof separator !== "string") {
3482
+ throw new TypeError("STRING_AGG delimiter must be a string");
3483
+ }
3484
+ return [
3485
+ {
3486
+ value,
3487
+ delimiter: separator ?? "",
3488
+ order: (expression.aggregateOrderBy ?? []).map((item) => evaluate(item.expression, row)),
3489
+ },
3490
+ ];
3491
+ });
3492
+ if (expression.distinct === true) {
3493
+ const seen = new Set();
3494
+ members = members.filter((member) => {
3495
+ const key = JSON.stringify([member.value, member.delimiter]);
3496
+ if (seen.has(key))
3497
+ return false;
3498
+ seen.add(key);
3499
+ return true;
3500
+ });
3501
+ }
3502
+ if ((expression.aggregateOrderBy?.length ?? 0) > 0) {
3503
+ members.sort((left, right) => {
3504
+ for (const [index, order] of (expression.aggregateOrderBy ?? []).entries()) {
3505
+ const a = left.order[index];
3506
+ const b = right.order[index];
3507
+ const placed = nullOrder(a, b, order.nulls, order.direction);
3508
+ if (placed !== undefined && placed !== 0)
3509
+ return placed;
3510
+ const compared = compareValues(a, b);
3511
+ if (compared !== 0)
3512
+ return order.direction === "desc" ? -compared : compared;
3513
+ }
3514
+ return 0;
3515
+ });
3516
+ }
3517
+ if (members.length === 0)
3518
+ return null;
3519
+ return protectedSqlTextValue(members
3520
+ .map((member, index) => index === 0
3521
+ ? String(externalSqlDomainValue(member.value))
3522
+ : String(externalSqlDomainValue(member.delimiter)) +
3523
+ String(externalSqlDomainValue(member.value)))
3524
+ .join(""));
3525
+ }
3004
3526
  let values = argument.kind === "wildcard"
3005
3527
  ? group.map(() => 1)
3006
- : group
3007
- .map((row) => evaluate(argument, row))
3008
- .filter((value) => value !== null && value !== undefined);
3528
+ : group.map((row) => evaluate(argument, row));
3529
+ if (expression.name !== "JSON_ARRAYAGG") {
3530
+ values = values.filter((value) => value !== null && value !== undefined);
3531
+ }
3009
3532
  if (expression.distinct === true) {
3010
3533
  const seen = new Set();
3011
3534
  values = values.filter((value) => {
@@ -3019,13 +3542,19 @@ function evaluate(expression, context, group) {
3019
3542
  if (expression.name === "COUNT")
3020
3543
  return values.length;
3021
3544
  if (expression.name === "SUM")
3545
+ return values.length === 0 ? null : sumNumericValues(values);
3546
+ if (expression.name === "AVG")
3022
3547
  return values.length === 0
3023
3548
  ? null
3024
- : values.reduce((sum, value) => sum + numeric(value), 0);
3025
- if (expression.name === "AVG")
3549
+ : (() => {
3550
+ const sum = sumNumericValues(values);
3551
+ return exactNumericBinary("/", sum, values.length) ?? numeric(sum) / values.length;
3552
+ })();
3553
+ if (expression.name === "JSON_ARRAYAGG") {
3026
3554
  return values.length === 0
3027
3555
  ? null
3028
- : values.reduce((sum, value) => sum + numeric(value), 0) / values.length;
3556
+ : JSON.stringify(values.map((value) => jsonValueOf(value ?? null)));
3557
+ }
3029
3558
  if (expression.name === "MIN")
3030
3559
  return values.reduce((best, value) => (best === undefined || compareValues(value, best) < 0 ? value : best), undefined);
3031
3560
  return values.reduce((best, value) => (best === undefined || compareValues(value, best) > 0 ? value : best), undefined);
@@ -3052,7 +3581,9 @@ function evaluatePredicate(predicate, context) {
3052
3581
  if (predicate.operator === "LIKE" ||
3053
3582
  predicate.operator === "NOT LIKE" ||
3054
3583
  predicate.operator === "ILIKE" ||
3055
- predicate.operator === "NOT ILIKE") {
3584
+ predicate.operator === "NOT ILIKE" ||
3585
+ predicate.operator === "SIMILAR TO" ||
3586
+ predicate.operator === "NOT SIMILAR TO") {
3056
3587
  return (evaluateBooleanExpression({
3057
3588
  kind: "condition",
3058
3589
  operator: predicate.operator,
@@ -3271,7 +3802,7 @@ export function distinctFromComparison(left, right) {
3271
3802
  const rightNull = right === null || right === undefined;
3272
3803
  if (leftNull || rightNull)
3273
3804
  return leftNull !== rightNull;
3274
- return comparable(left) !== comparable(right);
3805
+ return compareValues(comparable(left), comparable(right)) !== 0;
3275
3806
  }
3276
3807
  /**
3277
3808
  * Evaluates a boolean expression tree with SQL three-valued logic: comparisons over NULL are
@@ -3344,17 +3875,24 @@ export function evaluateBooleanExpression(expression, evaluateValue) {
3344
3875
  if (operator === "LIKE" ||
3345
3876
  operator === "NOT LIKE" ||
3346
3877
  operator === "ILIKE" ||
3347
- operator === "NOT ILIKE") {
3348
- const value = evaluateValue(expression.left);
3349
- const pattern = evaluateValue(expression.right);
3878
+ operator === "NOT ILIKE" ||
3879
+ operator === "SIMILAR TO" ||
3880
+ operator === "NOT SIMILAR TO") {
3881
+ const value = externalSqlDomainValue(evaluateValue(expression.left));
3882
+ const pattern = externalSqlDomainValue(evaluateValue(expression.right));
3350
3883
  if (value === null || value === undefined || pattern === null || pattern === undefined) {
3351
3884
  return null;
3352
3885
  }
3353
3886
  if (typeof value !== "string" || typeof pattern !== "string") {
3354
3887
  throw new TypeError("LIKE requires string operands");
3355
3888
  }
3356
- const matched = likeMatches(pattern, value, operator === "ILIKE" || operator === "NOT ILIKE", expression.escape);
3357
- return operator === "LIKE" || operator === "ILIKE" ? matched : !matched;
3889
+ const similar = operator === "SIMILAR TO" || operator === "NOT SIMILAR TO";
3890
+ const matched = similar
3891
+ ? compileSimilarPattern(pattern, expression.escape ?? "\\").test(value)
3892
+ : likeMatches(pattern, value, operator === "ILIKE" || operator === "NOT ILIKE", expression.escape);
3893
+ return operator === "LIKE" || operator === "ILIKE" || operator === "SIMILAR TO"
3894
+ ? matched
3895
+ : !matched;
3358
3896
  }
3359
3897
  if (operator === "IS DISTINCT FROM" || operator === "IS NOT DISTINCT FROM") {
3360
3898
  const distinct = distinctFromComparison(evaluateValue(expression.left), evaluateValue(expression.right));
@@ -3367,9 +3905,9 @@ export function evaluateBooleanExpression(expression, evaluateValue) {
3367
3905
  const a = comparable(left);
3368
3906
  const b = comparable(right);
3369
3907
  if (operator === "=")
3370
- return a === b;
3908
+ return compareValues(a, b) === 0;
3371
3909
  if (operator === "!=" || operator === "<>")
3372
- return a !== b;
3910
+ return compareValues(a, b) !== 0;
3373
3911
  const comparison = compareValues(a, b);
3374
3912
  if (operator === ">")
3375
3913
  return comparison > 0;
@@ -3401,7 +3939,11 @@ function comparisonHolds(operator, leftValue, rightValue) {
3401
3939
  if (operator === "LIKE" ||
3402
3940
  operator === "NOT LIKE" ||
3403
3941
  operator === "ILIKE" ||
3404
- operator === "NOT ILIKE") {
3942
+ operator === "NOT ILIKE" ||
3943
+ operator === "SIMILAR TO" ||
3944
+ operator === "NOT SIMILAR TO") {
3945
+ leftValue = externalSqlDomainValue(leftValue);
3946
+ rightValue = externalSqlDomainValue(rightValue);
3405
3947
  if (leftValue === null ||
3406
3948
  leftValue === undefined ||
3407
3949
  rightValue === null ||
@@ -3411,8 +3953,13 @@ function comparisonHolds(operator, leftValue, rightValue) {
3411
3953
  if (typeof leftValue !== "string" || typeof rightValue !== "string") {
3412
3954
  throw new TypeError("LIKE requires string operands");
3413
3955
  }
3414
- const matched = likeMatches(rightValue, leftValue, operator === "ILIKE" || operator === "NOT ILIKE");
3415
- return operator === "LIKE" || operator === "ILIKE" ? matched : !matched;
3956
+ const similar = operator === "SIMILAR TO" || operator === "NOT SIMILAR TO";
3957
+ const matched = similar
3958
+ ? compileSimilarPattern(rightValue).test(leftValue)
3959
+ : likeMatches(rightValue, leftValue, operator === "ILIKE" || operator === "NOT ILIKE");
3960
+ return operator === "LIKE" || operator === "ILIKE" || operator === "SIMILAR TO"
3961
+ ? matched
3962
+ : !matched;
3416
3963
  }
3417
3964
  if (leftValue === null ||
3418
3965
  leftValue === undefined ||
@@ -3422,9 +3969,9 @@ function comparisonHolds(operator, leftValue, rightValue) {
3422
3969
  const left = comparable(leftValue);
3423
3970
  const right = comparable(rightValue);
3424
3971
  if (operator === "=")
3425
- return left === right;
3972
+ return compareValues(left, right) === 0;
3426
3973
  if (operator === "!=" || operator === "<>")
3427
- return left !== right;
3974
+ return compareValues(left, right) !== 0;
3428
3975
  const comparison = compareValues(left, right);
3429
3976
  if (operator === ">")
3430
3977
  return comparison > 0;
@@ -3503,7 +4050,7 @@ function validateGrouping(plan) {
3503
4050
  for (const item of plan.select) {
3504
4051
  if (hasAggregate(item.expression))
3505
4052
  continue;
3506
- // A constant expression is the same for every group, as standard SQL allows. A full-text
4053
+ // A constant expression is the same for every group, as PostgreSQL allows. A full-text
3507
4054
  // node is never constant — MATCH(*) carries no column children before expansion, but it
3508
4055
  // reads every searchable column of its row.
3509
4056
  if (expressionColumns(item.expression).length === 0 &&
@@ -3543,27 +4090,37 @@ function comparable(value) {
3543
4090
  return value instanceof Date ? value.getTime() : value;
3544
4091
  }
3545
4092
  /**
3546
- * Resolves an explicit NULLS FIRST/LAST placement for one order term. Returns undefined when
3547
- * no explicit placement applies (either none was requested or neither side is NULL); otherwise
3548
- * the signed placement, which is absolute direction negation must not apply to it. Two NULLs
3549
- * return 0 so the comparison falls through to the next term.
4093
+ * Resolves NULL placement for one order term. An omitted placement follows PostgreSQL: NULLS LAST
4094
+ * for ASC and NULLS FIRST for DESC. Returns undefined when neither side is NULL; otherwise the
4095
+ * signed placement is absolute, so direction negation must not apply to it. Two NULLs return 0
4096
+ * so comparison falls through to the next term.
3550
4097
  */
3551
- export function explicitNullOrder(left, right, nulls) {
3552
- if (nulls === undefined)
3553
- return undefined;
4098
+ export function nullOrder(left, right, nulls, direction) {
3554
4099
  const leftNull = left === null || left === undefined;
3555
4100
  const rightNull = right === null || right === undefined;
3556
4101
  if (!leftNull && !rightNull)
3557
4102
  return undefined;
3558
4103
  if (leftNull && rightNull)
3559
4104
  return 0;
3560
- return (leftNull ? -1 : 1) * (nulls === "first" ? 1 : -1);
4105
+ const placement = nulls ?? (direction === "desc" ? "first" : "last");
4106
+ return (leftNull ? -1 : 1) * (placement === "first" ? 1 : -1);
3561
4107
  }
3562
4108
  function numeric(value) {
3563
4109
  if (typeof value !== "number")
3564
4110
  throw new TypeError("Arithmetic and numeric aggregates require numbers");
3565
4111
  return value;
3566
4112
  }
4113
+ function sumNumericValues(values) {
4114
+ let total = values[0];
4115
+ if (total === undefined)
4116
+ return null;
4117
+ for (let index = 1; index < values.length; index += 1) {
4118
+ const value = values[index];
4119
+ const exact = exactNumericBinary("+", total, value);
4120
+ total = exact === undefined ? numeric(total) + numeric(value) : exact;
4121
+ }
4122
+ return total;
4123
+ }
3567
4124
  function asQueryValue(value) {
3568
4125
  if (value === null ||
3569
4126
  typeof value === "boolean" ||
@@ -3575,6 +4132,83 @@ function asQueryValue(value) {
3575
4132
  return null;
3576
4133
  throw new TypeError("Query produced an unsupported value");
3577
4134
  }
4135
+ function asExternalQueryValue(value) {
4136
+ return asQueryValue(externalSqlDomainValue(value));
4137
+ }
4138
+ const alreadyExternalResults = new WeakSet();
4139
+ function markExternalizationState(result, outputNeedsExternalization) {
4140
+ if (outputNeedsExternalization === false)
4141
+ alreadyExternalResults.add(result);
4142
+ return result;
4143
+ }
4144
+ /** Carries the internal no-conversion proof across a defensive result copy. */
4145
+ export function copyQueryResultExternalization(source, copy) {
4146
+ if (alreadyExternalResults.has(source))
4147
+ alreadyExternalResults.add(copy);
4148
+ return copy;
4149
+ }
4150
+ /** Removes internal domain tags only after every comparison, group, join, and sort is complete. */
4151
+ export function externalizeQueryResult(result) {
4152
+ if (alreadyExternalResults.has(result))
4153
+ return result;
4154
+ let changed = false;
4155
+ const rows = new Array(result.rows.length);
4156
+ for (let rowIndex = 0; rowIndex < result.rows.length; rowIndex += 1) {
4157
+ const row = result.rows[rowIndex] ?? {};
4158
+ let output = row;
4159
+ // Ordinary primitive results are already public values. Most queries never touch one of the
4160
+ // tagged PostgreSQL domains, so keep their row objects and avoid rebuilding a large result
4161
+ // set merely to discover that every value is unchanged.
4162
+ for (const name of result.columns) {
4163
+ const value = row[name];
4164
+ if (value !== undefined && !isSqlDomainValue(value))
4165
+ continue;
4166
+ const external = asExternalQueryValue(value);
4167
+ if (external === value)
4168
+ continue;
4169
+ if (output === row)
4170
+ output = { ...row };
4171
+ output[name] = external;
4172
+ changed = true;
4173
+ }
4174
+ rows[rowIndex] = output;
4175
+ }
4176
+ const externalized = changed ? { columns: [...result.columns], rows } : result;
4177
+ // Public TEXT is allowed to begin with the private tag prefix, so externalization is not
4178
+ // byte-idempotent. Record the boundary crossing: a wrapper such as executeQuery(query()) must
4179
+ // not interpret the now-public bytes a second time.
4180
+ alreadyExternalResults.add(externalized);
4181
+ return externalized;
4182
+ }
4183
+ /** Gives bare conflict-update columns the target-row meaning SQL assigns them. */
4184
+ function rewriteUpsertExpression(expression, table) {
4185
+ if (hasAggregate(expression)) {
4186
+ throw new TypeError("ON CONFLICT DO UPDATE expressions cannot contain aggregates");
4187
+ }
4188
+ const rewrite = (node) => {
4189
+ if (node.kind === "column") {
4190
+ const parts = node.reference.split(".");
4191
+ if (parts.length === 1)
4192
+ return { ...node, reference: `${table}.${node.reference}` };
4193
+ if (parts.length === 2 && parts[0]?.toUpperCase() === "EXCLUDED") {
4194
+ return { ...node, reference: `EXCLUDED.${parts[1] ?? ""}` };
4195
+ }
4196
+ if (parts.length === 2 && parts[0]?.toUpperCase() === table.toUpperCase()) {
4197
+ return { ...node, reference: `${table}.${parts[1] ?? ""}` };
4198
+ }
4199
+ return node;
4200
+ }
4201
+ if (node.kind === "subquery" ||
4202
+ node.kind === "exists" ||
4203
+ node.kind === "window" ||
4204
+ node.kind === "fts" ||
4205
+ node.kind === "wildcard") {
4206
+ throw new TypeError("ON CONFLICT DO UPDATE expressions use target and EXCLUDED scalar values only");
4207
+ }
4208
+ return mapChildExpressions(node, rewrite);
4209
+ };
4210
+ return rewrite(expression);
4211
+ }
3578
4212
  class Parser {
3579
4213
  tokens;
3580
4214
  /** The statement text the tokens came from, for the clauses stored verbatim (CHECK bodies). */
@@ -3586,6 +4220,10 @@ class Parser {
3586
4220
  #highestNumberedParameter = 0;
3587
4221
  /** Set when the statement names CURRENT_DATE, CURRENT_TIMESTAMP, or LOCALTIME. */
3588
4222
  usesStatementDatetime = false;
4223
+ /** Set when the statement names NEXTVAL or CURRVAL. */
4224
+ usesSequenceCalls = false;
4225
+ /** Set when the statement names a volatile scalar function. */
4226
+ usesVolatileFunctions = false;
3589
4227
  /** Total parameter slots the statement expects; 0 when it has no placeholders. */
3590
4228
  get parameterCount() {
3591
4229
  return Math.max(this.#positionalParameters, this.#highestNumberedParameter);
@@ -3697,7 +4335,7 @@ class Parser {
3697
4335
  this.#ctes.set(name, block);
3698
4336
  }
3699
4337
  }
3700
- // INTERSECT binds tighter than UNION and EXCEPT, per the SQL standard.
4338
+ // INTERSECT binds tighter than UNION and EXCEPT, matching PostgreSQL.
3701
4339
  const firstTerm = this.#setTerm(sql);
3702
4340
  let plan = firstTerm.block;
3703
4341
  if (this.#isKeyword("UNION") || this.#isKeyword("EXCEPT")) {
@@ -3755,7 +4393,7 @@ class Parser {
3755
4393
  throw new TypeError("ORDER BY or LIMIT in a UNION member requires parentheses");
3756
4394
  }
3757
4395
  }
3758
- // Standard SQL assigns a trailing ORDER BY or LIMIT to the whole compound. After an
4396
+ // PostgreSQL assigns a trailing ORDER BY or LIMIT to the whole compound. After an
3759
4397
  // unparenthesized last member the clause was greedily parsed into that member and lifts
3760
4398
  // out; after a parenthesized member it is still unparsed.
3761
4399
  const last = members[members.length - 1];
@@ -3784,10 +4422,53 @@ class Parser {
3784
4422
  }
3785
4423
  return compoundSelectBlock(sql, members.map((member) => member.block), ops, tail, this.nextDerivedSequence);
3786
4424
  }
4425
+ /** CREATE [UNIQUE] INDEX name ON table(column [ASC|DESC], ...). */
4426
+ parseCreateIndex() {
4427
+ this.#keyword("CREATE");
4428
+ const unique = this.#isKeyword("UNIQUE");
4429
+ if (unique)
4430
+ this.#keyword("UNIQUE");
4431
+ this.#keyword("INDEX");
4432
+ let ifNotExists = false;
4433
+ if (this.#isKeyword("IF")) {
4434
+ this.#keyword("IF");
4435
+ this.#keyword("NOT");
4436
+ this.#keyword("EXISTS");
4437
+ ifNotExists = true;
4438
+ }
4439
+ const index = this.#identifier();
4440
+ this.#keyword("ON");
4441
+ const table = this.#identifier();
4442
+ this.#expectPunctuation("(");
4443
+ const columns = [];
4444
+ do {
4445
+ if (columns.length > 0)
4446
+ this.#expectPunctuation(",");
4447
+ const name = this.#identifier();
4448
+ let direction = "asc";
4449
+ if (this.#isKeyword("ASC"))
4450
+ this.#keyword("ASC");
4451
+ else if (this.#isKeyword("DESC")) {
4452
+ this.#keyword("DESC");
4453
+ direction = "desc";
4454
+ }
4455
+ columns.push({ name, direction });
4456
+ } while (this.#peek().text === ",");
4457
+ this.#expectPunctuation(")");
4458
+ this.#take("eof");
4459
+ return {
4460
+ kind: "create-index",
4461
+ index,
4462
+ table,
4463
+ columns,
4464
+ ...(unique ? { unique: true } : {}),
4465
+ ...(ifNotExists ? { ifNotExists: true } : {}),
4466
+ };
4467
+ }
3787
4468
  /**
3788
4469
  * CREATE TABLE name (col TYPE [PRIMARY KEY | UNIQUE] [NOT NULL | NULL], ...). Standard type
3789
- * names map onto the engine's four logical types; widths in parentheses parse and are
3790
- * ignored, because numeric widths and encodings are the engine's job, not schema choices.
4470
+ * names map onto the engine's four logical types. Integer spellings retain an exact-domain
4471
+ * catalog guard; exact NUMERIC/DECIMAL is rejected until it has a distinct physical type.
3791
4472
  */
3792
4473
  parseCreateTable() {
3793
4474
  this.#keyword("CREATE");
@@ -3816,9 +4497,28 @@ class Parser {
3816
4497
  const columns = [];
3817
4498
  const checks = [];
3818
4499
  const foreignKeys = [];
3819
- let uniqueKey;
4500
+ const uniqueConstraints = [];
4501
+ let primaryKey;
4502
+ const constraintColumns = () => {
4503
+ this.#expectPunctuation("(");
4504
+ const names = [];
4505
+ for (;;) {
4506
+ names.push(this.#identifier());
4507
+ if (!this.#punctuation(","))
4508
+ break;
4509
+ }
4510
+ this.#expectPunctuation(")");
4511
+ if (new Set(names).size !== names.length) {
4512
+ throw new TypeError("Constraint columns must be unique");
4513
+ }
4514
+ return names;
4515
+ };
3820
4516
  for (;;) {
3821
- if (this.#isKeyword("CONSTRAINT") || this.#isKeyword("CHECK") || this.#isKeyword("FOREIGN")) {
4517
+ if (this.#isKeyword("CONSTRAINT") ||
4518
+ this.#isKeyword("CHECK") ||
4519
+ this.#isKeyword("FOREIGN") ||
4520
+ this.#isKeyword("PRIMARY") ||
4521
+ this.#isKeyword("UNIQUE")) {
3822
4522
  // A table-level constraint, named or not.
3823
4523
  let constraintName;
3824
4524
  if (this.#isKeyword("CONSTRAINT")) {
@@ -3828,59 +4528,50 @@ class Parser {
3828
4528
  if (this.#isKeyword("FOREIGN")) {
3829
4529
  this.#keyword("FOREIGN");
3830
4530
  this.#keyword("KEY");
3831
- this.#expectPunctuation("(");
3832
- const column = this.#identifier();
3833
- if (this.#peek().text === ",") {
3834
- throw new TypeError("FOREIGN KEY supports one column, the parent's unique key");
3835
- }
3836
- this.#expectPunctuation(")");
3837
- foreignKeys.push(this.#references(constraintName ?? `${table}_${column}_fkey`, column));
4531
+ const names = constraintColumns();
4532
+ foreignKeys.push(this.#references(constraintName ?? `${table}_${names.join("_")}_fkey`, names));
3838
4533
  if (!this.#punctuation(","))
3839
4534
  break;
3840
4535
  continue;
3841
4536
  }
3842
- if (!this.#isKeyword("CHECK")) {
3843
- throw new TypeError("Table constraints are CHECK and FOREIGN KEY");
3844
- }
3845
- checks.push(this.#checkConstraint(constraintName ?? `${table}_check_${String(checks.length + 1)}`));
3846
- if (!this.#punctuation(","))
3847
- break;
3848
- continue;
3849
- }
3850
- if (this.#isKeyword("PRIMARY") || this.#isKeyword("UNIQUE")) {
3851
- // E141-08: the table-level spelling of the same single-column key.
3852
4537
  if (this.#isKeyword("PRIMARY")) {
3853
4538
  this.#keyword("PRIMARY");
3854
4539
  this.#keyword("KEY");
4540
+ if (primaryKey !== undefined)
4541
+ throw new TypeError("CREATE TABLE has two PRIMARY KEYs");
4542
+ primaryKey = constraintColumns();
4543
+ if (!this.#punctuation(","))
4544
+ break;
4545
+ continue;
3855
4546
  }
3856
- else
4547
+ if (this.#isKeyword("UNIQUE")) {
3857
4548
  this.#keyword("UNIQUE");
3858
- this.#expectPunctuation("(");
3859
- const keyColumn = this.#identifier();
3860
- if (this.#peek().text === ",") {
3861
- throw new TypeError("CREATE TABLE supports one unique key column");
4549
+ const names = constraintColumns();
4550
+ uniqueConstraints.push({
4551
+ name: constraintName ?? `${table}_${names.join("_")}_key`,
4552
+ columns: names,
4553
+ });
4554
+ if (!this.#punctuation(","))
4555
+ break;
4556
+ continue;
3862
4557
  }
3863
- this.#expectPunctuation(")");
3864
- if (uniqueKey !== undefined && uniqueKey !== keyColumn) {
3865
- throw new TypeError("CREATE TABLE supports one unique key column");
4558
+ if (!this.#isKeyword("CHECK")) {
4559
+ throw new TypeError("Expected PRIMARY KEY, UNIQUE, CHECK, or FOREIGN KEY");
3866
4560
  }
3867
- uniqueKey = keyColumn;
4561
+ checks.push(this.#checkConstraint(constraintName ?? `${table}_check_${String(checks.length + 1)}`));
3868
4562
  if (!this.#punctuation(","))
3869
4563
  break;
3870
4564
  continue;
3871
4565
  }
3872
4566
  const name = this.#identifier();
3873
- const type = this.#columnType();
4567
+ const columnType = this.#columnType();
3874
4568
  let nullable = true;
3875
- let explicitlyNullable = false;
3876
4569
  let defaultValue;
3877
4570
  for (;;) {
3878
4571
  if (this.#isKeyword("DEFAULT")) {
3879
- // E141-07. The catalog fills defaults at insert time, so they are constants: a
3880
- // literal, or the CURRENT_TIMESTAMP family, which it stores as "now".
4572
+ // PostgreSQL-compatible variable-free scalar expression, retained in the catalog.
3881
4573
  this.#keyword("DEFAULT");
3882
- const expression = this.#expression();
3883
- defaultValue = columnDefaultFor(expression);
4574
+ defaultValue = this.#columnDefault();
3884
4575
  continue;
3885
4576
  }
3886
4577
  if (this.#isKeyword("CHECK")) {
@@ -3888,22 +4579,22 @@ class Parser {
3888
4579
  continue;
3889
4580
  }
3890
4581
  if (this.#isKeyword("REFERENCES")) {
3891
- foreignKeys.push(this.#references(`${table}_${name}_fkey`, name));
4582
+ foreignKeys.push(this.#references(`${table}_${name}_fkey`, [name]));
3892
4583
  continue;
3893
4584
  }
3894
4585
  if (this.#isKeyword("PRIMARY") || this.#isKeyword("UNIQUE")) {
3895
4586
  if (this.#isKeyword("PRIMARY")) {
3896
4587
  this.#keyword("PRIMARY");
3897
4588
  this.#keyword("KEY");
4589
+ if (primaryKey !== undefined)
4590
+ throw new TypeError("CREATE TABLE has two PRIMARY KEYs");
4591
+ primaryKey = [name];
4592
+ nullable = false;
3898
4593
  }
3899
4594
  else {
3900
4595
  this.#keyword("UNIQUE");
4596
+ uniqueConstraints.push({ name: `${table}_${name}_key`, columns: [name] });
3901
4597
  }
3902
- if (uniqueKey !== undefined) {
3903
- throw new TypeError("CREATE TABLE supports one unique key column");
3904
- }
3905
- uniqueKey = name;
3906
- nullable = false;
3907
4598
  continue;
3908
4599
  }
3909
4600
  if (this.#isKeyword("NOT")) {
@@ -3919,19 +4610,13 @@ class Parser {
3919
4610
  if (this.#isKeyword("NULL")) {
3920
4611
  this.#keyword("NULL");
3921
4612
  nullable = true;
3922
- explicitlyNullable = true;
3923
4613
  continue;
3924
4614
  }
3925
4615
  break;
3926
4616
  }
3927
- // A default answers what an absent value means, so the column cannot also be nullable
3928
- // unless the author says so — and then the engine rejects the pair, since NULL and the
3929
- // default would both claim the same slot.
3930
- if (defaultValue !== undefined && !explicitlyNullable)
3931
- nullable = false;
3932
4617
  columns.push({
3933
4618
  name,
3934
- type,
4619
+ ...columnType,
3935
4620
  ...(nullable ? { nullable: true } : {}),
3936
4621
  ...(defaultValue === undefined ? {} : { defaultValue }),
3937
4622
  });
@@ -3943,10 +4628,14 @@ class Parser {
3943
4628
  if (new Set(columns.map((column) => column.name)).size !== columns.length) {
3944
4629
  throw new TypeError("CREATE TABLE column names must be unique");
3945
4630
  }
3946
- if (uniqueKey !== undefined && !columns.some((column) => column.name === uniqueKey)) {
3947
- throw new TypeError(`CREATE TABLE key column is not declared: ${uniqueKey}`);
3948
- }
3949
4631
  const declared = new Set(columns.map((column) => column.name));
4632
+ for (const name of [
4633
+ ...(primaryKey ?? []),
4634
+ ...uniqueConstraints.flatMap((key) => key.columns),
4635
+ ]) {
4636
+ if (!declared.has(name))
4637
+ throw new TypeError(`Constraint column is not declared: ${name}`);
4638
+ }
3950
4639
  for (const check of checks) {
3951
4640
  for (const reference of expressionColumnNames(compileCheckExpression(check.sql, check.name))) {
3952
4641
  const column = reference.split(".").at(-1) ?? reference;
@@ -3955,11 +4644,24 @@ class Parser {
3955
4644
  }
3956
4645
  }
3957
4646
  }
4647
+ // Preserve the released single-UNIQUE row-addressing behavior when no PRIMARY KEY was
4648
+ // declared. Additional UNIQUE constraints remain independently enforced secondary keys.
4649
+ const promotedUnique = primaryKey === undefined && uniqueConstraints[0]?.columns.length === 1
4650
+ ? uniqueConstraints.shift()
4651
+ : undefined;
4652
+ const uniqueKey = primaryKey?.length === 1 ? primaryKey[0] : promotedUnique?.columns[0];
4653
+ const primaryNames = new Set(primaryKey ?? []);
3958
4654
  return {
3959
4655
  kind: "create-table",
3960
4656
  table,
3961
- columns: columns.map((column) => column.name === uniqueKey ? { ...column, nullable: false } : column),
4657
+ columns: columns.map((column) => column.name === uniqueKey || primaryNames.has(column.name)
4658
+ ? { ...column, nullable: false }
4659
+ : column),
3962
4660
  ...(uniqueKey === undefined ? {} : { uniqueKey }),
4661
+ ...(primaryKey === undefined || primaryKey.length < 2
4662
+ ? {}
4663
+ : { compositePrimaryKey: primaryKey }),
4664
+ ...(uniqueConstraints.length === 0 ? {} : { uniqueConstraints }),
3963
4665
  ...(checks.length === 0 ? {} : { checks }),
3964
4666
  ...(foreignKeys.length === 0 ? {} : { foreignKeys }),
3965
4667
  ...(ifNotExists ? { ifNotExists: true } : {}),
@@ -3970,14 +4672,22 @@ class Parser {
3970
4672
  * key cannot change in this engine, so ON UPDATE has nothing to act on and only the
3971
4673
  * no-op actions parse; ON DELETE takes the three the engine can carry out.
3972
4674
  */
3973
- #references(name, column) {
4675
+ #references(name, columns) {
3974
4676
  this.#keyword("REFERENCES");
3975
4677
  const parentTable = this.#identifier();
3976
- let parentColumn;
4678
+ let parentColumns;
3977
4679
  if (this.#punctuation("(")) {
3978
- parentColumn = this.#identifier();
4680
+ parentColumns = [];
4681
+ for (;;) {
4682
+ parentColumns.push(this.#identifier());
4683
+ if (!this.#punctuation(","))
4684
+ break;
4685
+ }
3979
4686
  this.#expectPunctuation(")");
3980
4687
  }
4688
+ if (parentColumns !== undefined && parentColumns.length !== columns.length) {
4689
+ throw new TypeError("FOREIGN KEY and REFERENCES must name the same number of columns");
4690
+ }
3981
4691
  let onDelete = "restrict";
3982
4692
  while (this.#isKeyword("ON")) {
3983
4693
  this.#keyword("ON");
@@ -3994,9 +4704,10 @@ class Parser {
3994
4704
  }
3995
4705
  return {
3996
4706
  name,
3997
- column,
4707
+ column: columns[0] ?? "",
4708
+ ...(columns.length === 1 ? {} : { columns }),
3998
4709
  parentTable,
3999
- ...(parentColumn === undefined ? {} : { parentColumn }),
4710
+ ...(parentColumns === undefined ? {} : { parentColumn: parentColumns[0], parentColumns }),
4000
4711
  onDelete,
4001
4712
  };
4002
4713
  }
@@ -4046,6 +4757,15 @@ class Parser {
4046
4757
  this.#take("eof");
4047
4758
  return expression;
4048
4759
  }
4760
+ /** Reads one column DEFAULT while preserving its authored SQL for catalog introspection. */
4761
+ #columnDefault() {
4762
+ const start = this.#peek().start;
4763
+ const expression = this.#expression();
4764
+ const sql = this.text.slice(start, this.#peek().start).trim();
4765
+ if (sql.length === 0)
4766
+ throw new TypeError("DEFAULT requires an expression");
4767
+ return columnDefaultFor(expression, sql);
4768
+ }
4049
4769
  /** DROP VIEW [IF EXISTS] name (F031-16). */
4050
4770
  parseDropView() {
4051
4771
  this.#keyword("DROP");
@@ -4062,6 +4782,20 @@ class Parser {
4062
4782
  this.#take("eof");
4063
4783
  return { kind: "drop-view", view, ...(ifExists ? { ifExists: true } : {}) };
4064
4784
  }
4785
+ /** DROP INDEX [IF EXISTS] name. */
4786
+ parseDropIndex() {
4787
+ this.#keyword("DROP");
4788
+ this.#keyword("INDEX");
4789
+ let ifExists = false;
4790
+ if (this.#isKeyword("IF")) {
4791
+ this.#keyword("IF");
4792
+ this.#keyword("EXISTS");
4793
+ ifExists = true;
4794
+ }
4795
+ const index = this.#identifier();
4796
+ this.#take("eof");
4797
+ return { kind: "drop-index", index, ...(ifExists ? { ifExists: true } : {}) };
4798
+ }
4065
4799
  /** DROP TABLE [IF EXISTS] name (F031-13). */
4066
4800
  parseDropTable() {
4067
4801
  this.#keyword("DROP");
@@ -4083,22 +4817,46 @@ class Parser {
4083
4817
  this.#take("eof");
4084
4818
  return { kind: "drop-table", table, ...(ifExists ? { ifExists: true } : {}) };
4085
4819
  }
4086
- /** ALTER TABLE name ADD [COLUMN] col TYPE [NOT NULL] [DEFAULT v] (F031-04). */
4820
+ /** ALTER TABLE name ADD/DROP [COLUMN] (F031). */
4087
4821
  parseAlterTable() {
4088
4822
  this.#keyword("ALTER");
4089
4823
  this.#keyword("TABLE");
4090
4824
  const table = this.#identifier();
4825
+ if (this.#isKeyword("DROP")) {
4826
+ this.#keyword("DROP");
4827
+ if (this.#isKeyword("COLUMN"))
4828
+ this.#keyword("COLUMN");
4829
+ let ifExists = false;
4830
+ if (this.#isKeyword("IF")) {
4831
+ this.#keyword("IF");
4832
+ this.#keyword("EXISTS");
4833
+ ifExists = true;
4834
+ }
4835
+ const column = this.#identifier();
4836
+ if (this.#isKeyword("RESTRICT"))
4837
+ this.#keyword("RESTRICT");
4838
+ else if (this.#isKeyword("CASCADE")) {
4839
+ throw new TypeError("ALTER TABLE DROP COLUMN CASCADE is not supported; drop dependents explicitly");
4840
+ }
4841
+ this.#take("eof");
4842
+ return {
4843
+ kind: "drop-column",
4844
+ table,
4845
+ column,
4846
+ ...(ifExists ? { ifExists: true } : {}),
4847
+ };
4848
+ }
4091
4849
  this.#keyword("ADD");
4092
4850
  if (this.#isKeyword("COLUMN"))
4093
4851
  this.#keyword("COLUMN");
4094
4852
  const name = this.#identifier();
4095
- const type = this.#columnType();
4853
+ const columnType = this.#columnType();
4096
4854
  let nullable = true;
4097
4855
  let defaultValue;
4098
4856
  for (;;) {
4099
4857
  if (this.#isKeyword("DEFAULT")) {
4100
4858
  this.#keyword("DEFAULT");
4101
- defaultValue = columnDefaultFor(this.#expression());
4859
+ defaultValue = this.#columnDefault();
4102
4860
  continue;
4103
4861
  }
4104
4862
  if (this.#isKeyword("NOT")) {
@@ -4120,7 +4878,7 @@ class Parser {
4120
4878
  table,
4121
4879
  column: {
4122
4880
  name,
4123
- type,
4881
+ ...columnType,
4124
4882
  ...(nullable ? { nullable: true } : {}),
4125
4883
  ...(defaultValue === undefined ? {} : { defaultValue }),
4126
4884
  },
@@ -4129,6 +4887,22 @@ class Parser {
4129
4887
  /** A CAST target: the SqlColumnType, or "number-integer" for the truncating integer names. */
4130
4888
  #castTarget() {
4131
4889
  const word = this.#identifier().toUpperCase();
4890
+ if (word === "NUMERIC" || word === "DECIMAL") {
4891
+ let precision;
4892
+ let scale;
4893
+ if (this.#punctuation("(")) {
4894
+ precision = Number(this.#take("number").text);
4895
+ if (this.#punctuation(","))
4896
+ scale = Number(this.#take("number").text);
4897
+ else
4898
+ scale = 0;
4899
+ this.#expectPunctuation(")");
4900
+ }
4901
+ return `numeric:${precision === undefined ? "" : String(precision)}:${scale === undefined ? "" : String(scale)}`;
4902
+ }
4903
+ if (["JSON", "JSONB", "UUID", "TIME", "INTERVAL"].includes(word)) {
4904
+ return word.toLowerCase();
4905
+ }
4132
4906
  if (word === "DOUBLE") {
4133
4907
  this.#keyword("PRECISION");
4134
4908
  return "number";
@@ -4146,22 +4920,50 @@ class Parser {
4146
4920
  return integer ? "number-integer" : mapped;
4147
4921
  }
4148
4922
  #columnType() {
4149
- const word = this.#identifier().toUpperCase();
4923
+ const declared = this.#identifier();
4924
+ const word = declared.toUpperCase();
4925
+ if (word === "NUMERIC" || word === "DECIMAL") {
4926
+ let precision;
4927
+ let scale;
4928
+ if (this.#punctuation("(")) {
4929
+ precision = Number(this.#take("number").text);
4930
+ scale = this.#punctuation(",") ? Number(this.#take("number").text) : 0;
4931
+ this.#expectPunctuation(")");
4932
+ }
4933
+ return {
4934
+ type: "string",
4935
+ sqlDomain: {
4936
+ kind: "numeric",
4937
+ ...(precision === undefined ? {} : { precision }),
4938
+ ...(scale === undefined ? {} : { scale }),
4939
+ },
4940
+ };
4941
+ }
4942
+ if (["JSON", "JSONB", "UUID", "TIME", "INTERVAL"].includes(word)) {
4943
+ return { type: "string", sqlDomain: { kind: word.toLowerCase() } };
4944
+ }
4150
4945
  if (word === "DOUBLE") {
4151
4946
  this.#keyword("PRECISION");
4152
- return "number";
4947
+ return { type: "number" };
4153
4948
  }
4154
4949
  const mapped = createTableTypeNames.get(word);
4155
- if (mapped === undefined)
4156
- throw new TypeError(`Unsupported column type: ${word}`);
4157
- // A width like VARCHAR(80) or NUMERIC(10, 2) parses and is discarded.
4950
+ if (mapped === undefined) {
4951
+ return { type: "string", sqlDomain: { kind: "enum", name: declared, values: [] } };
4952
+ }
4953
+ // Character widths document intent and do not truncate values.
4158
4954
  if (this.#punctuation("(")) {
4159
4955
  this.#take("number");
4160
- if (this.#punctuation(","))
4161
- this.#take("number");
4956
+ if (this.#punctuation(",")) {
4957
+ throw new TypeError(`${word} takes one width`);
4958
+ }
4162
4959
  this.#expectPunctuation(")");
4163
4960
  }
4164
- return mapped;
4961
+ if (this.#punctuation("[")) {
4962
+ this.#expectPunctuation("]");
4963
+ return { type: "string", sqlDomain: { kind: "array", element: word } };
4964
+ }
4965
+ const integer = word === "INTEGER" || word === "INT" || word === "BIGINT" || word === "SMALLINT";
4966
+ return { type: mapped, ...(integer ? { integer: true } : {}) };
4165
4967
  }
4166
4968
  parseMutation(keyword) {
4167
4969
  const statement = keyword === "INSERT"
@@ -4176,23 +4978,37 @@ class Parser {
4176
4978
  this.#keyword("INSERT");
4177
4979
  this.#keyword("INTO");
4178
4980
  const table = this.#identifier();
4179
- this.#expectPunctuation("(");
4180
4981
  const columns = [];
4181
- for (;;) {
4182
- columns.push(this.#identifier());
4183
- if (!this.#punctuation(","))
4184
- break;
4982
+ if (this.#punctuation("(")) {
4983
+ for (;;) {
4984
+ columns.push(this.#identifier());
4985
+ if (!this.#punctuation(","))
4986
+ break;
4987
+ }
4988
+ this.#expectPunctuation(")");
4185
4989
  }
4186
- this.#expectPunctuation(")");
4187
4990
  if (new Set(columns).size !== columns.length) {
4188
4991
  throw new TypeError("INSERT columns must be unique");
4189
4992
  }
4993
+ if (this.#isKeyword("DEFAULT")) {
4994
+ this.#keyword("DEFAULT");
4995
+ this.#keyword("VALUES");
4996
+ return {
4997
+ kind: "insert",
4998
+ table,
4999
+ columns,
5000
+ rows: [[]],
5001
+ defaultValues: true,
5002
+ ...this.#onConflictClause(table),
5003
+ ...this.#returningClause(),
5004
+ };
5005
+ }
4190
5006
  if (this.#isKeyword("SELECT")) {
4191
5007
  const query = optimizePlan(this.#selectBlock("(insert select)"));
4192
5008
  if (query.select.some((item) => item.expression.kind === "wildcard")) {
4193
5009
  throw new TypeError("INSERT ... SELECT requires an explicit select list");
4194
5010
  }
4195
- if (query.select.length !== columns.length) {
5011
+ if (columns.length > 0 && query.select.length !== columns.length) {
4196
5012
  throw new TypeError("INSERT ... SELECT must produce exactly the insert column count");
4197
5013
  }
4198
5014
  return {
@@ -4215,7 +5031,7 @@ class Parser {
4215
5031
  break;
4216
5032
  }
4217
5033
  this.#expectPunctuation(")");
4218
- if (values.length !== columns.length) {
5034
+ if (columns.length > 0 && values.length !== columns.length) {
4219
5035
  throw new TypeError("Each INSERT row must match the column list length");
4220
5036
  }
4221
5037
  rows.push(values);
@@ -4227,55 +5043,72 @@ class Parser {
4227
5043
  table,
4228
5044
  columns,
4229
5045
  rows,
4230
- ...this.#onConflictClause(columns),
5046
+ ...this.#onConflictClause(table),
4231
5047
  ...this.#returningClause(),
4232
5048
  };
4233
5049
  }
4234
- #onConflictClause(columns) {
5050
+ #onConflictClause(table) {
4235
5051
  if (!this.#isKeyword("ON"))
4236
5052
  return {};
4237
5053
  this.#keyword("ON");
4238
5054
  this.#keyword("CONFLICT");
4239
5055
  this.#expectPunctuation("(");
4240
- const column = this.#identifier();
5056
+ const columns = [];
5057
+ for (;;) {
5058
+ columns.push(this.#identifier());
5059
+ if (!this.#punctuation(","))
5060
+ break;
5061
+ }
4241
5062
  this.#expectPunctuation(")");
5063
+ const column = columns[0];
5064
+ if (column === undefined)
5065
+ throw new TypeError("ON CONFLICT needs at least one column");
5066
+ const target = columns.length === 1 ? {} : { columns };
4242
5067
  this.#keyword("DO");
4243
5068
  if (this.#isKeyword("NOTHING")) {
4244
5069
  this.#keyword("NOTHING");
4245
- return { onConflict: { column, action: "nothing" } };
5070
+ return { onConflict: { column, ...target, action: "nothing" } };
5071
+ }
5072
+ // Minnow's concise whole-row upsert spelling. Unlike DO UPDATE SET, it also has an exact
5073
+ // meaning for a table whose unique key is its only column.
5074
+ if (this.#isKeyword("REPLACE")) {
5075
+ this.#keyword("REPLACE");
5076
+ return { onConflict: { column, ...target, action: "replace" } };
4246
5077
  }
4247
5078
  this.#keyword("UPDATE");
4248
5079
  this.#keyword("SET");
4249
5080
  const assigned = new Set();
5081
+ const assignments = [];
4250
5082
  for (;;) {
4251
5083
  const target = this.#identifier();
4252
5084
  this.#operator("=");
4253
- const value = this.#expression();
4254
- const parts = value.kind === "column" ? value.reference.split(".") : [];
4255
- if (parts.length !== 2 || parts[0]?.toUpperCase() !== "EXCLUDED" || parts[1] !== target) {
4256
- throw new TypeError("ON CONFLICT DO UPDATE supports assignments of the form column = EXCLUDED.column");
4257
- }
4258
5085
  if (assigned.has(target)) {
4259
5086
  throw new TypeError(`ON CONFLICT DO UPDATE sets a column twice: ${target}`);
4260
5087
  }
4261
- if (target === column) {
5088
+ if (columns.includes(target)) {
4262
5089
  throw new TypeError("ON CONFLICT DO UPDATE cannot reassign the conflict key");
4263
5090
  }
4264
5091
  assigned.add(target);
5092
+ assignments.push({
5093
+ column: target,
5094
+ expression: rewriteUpsertExpression(this.#expression(), table),
5095
+ });
4265
5096
  if (!this.#punctuation(","))
4266
5097
  break;
4267
5098
  }
4268
- for (const name of assigned) {
4269
- if (!columns.includes(name)) {
4270
- throw new TypeError(`ON CONFLICT DO UPDATE sets a column that is not inserted: ${name}`);
4271
- }
5099
+ let where;
5100
+ if (this.#isKeyword("WHERE")) {
5101
+ this.#keyword("WHERE");
5102
+ where = rewriteUpsertExpression(this.#expression(), table);
4272
5103
  }
4273
- // Every inserted column assigned is a whole-row upsert; a subset merges into existing rows.
4274
- const complete = columns.every((name) => name === column || assigned.has(name));
4275
5104
  return {
4276
- onConflict: complete
4277
- ? { column, action: "replace" }
4278
- : { column, action: "update", columns: [...assigned] },
5105
+ onConflict: {
5106
+ column,
5107
+ ...target,
5108
+ action: "update",
5109
+ assignments,
5110
+ ...(where === undefined ? {} : { where }),
5111
+ },
4279
5112
  };
4280
5113
  }
4281
5114
  /** RETURNING * or RETURNING col, ... — the engine's runStatement implements the semantics. */
@@ -4463,6 +5296,10 @@ class Parser {
4463
5296
  return predicates;
4464
5297
  }
4465
5298
  #insertValue(label) {
5299
+ if (this.#isKeyword("DEFAULT")) {
5300
+ this.#keyword("DEFAULT");
5301
+ return { default: true };
5302
+ }
4466
5303
  const expression = this.#expression();
4467
5304
  // A bare placeholder stays a slot; the batch write binds it later. Placeholders nested in
4468
5305
  // arithmetic would need expression retention through the batch path, so they stay rejected.
@@ -4907,14 +5744,18 @@ class Parser {
4907
5744
  }
4908
5745
  #selectList() {
4909
5746
  const items = [];
5747
+ const explicitAliases = [];
4910
5748
  for (;;) {
4911
5749
  const expression = this.#expression();
4912
5750
  let alias = defaultAlias(expression);
5751
+ let explicitAlias = false;
4913
5752
  if (this.#isKeyword("AS")) {
4914
5753
  this.#keyword("AS");
4915
5754
  alias = this.#identifier();
5755
+ explicitAlias = true;
4916
5756
  }
4917
5757
  items.push({ expression, alias });
5758
+ explicitAliases.push(explicitAlias);
4918
5759
  if (!this.#punctuation(","))
4919
5760
  break;
4920
5761
  }
@@ -4923,15 +5764,23 @@ class Parser {
4923
5764
  throw new TypeError("SELECT * cannot be mixed with other expressions");
4924
5765
  }
4925
5766
  const aliases = new Set();
4926
- for (const item of items) {
5767
+ for (const [index, item] of items.entries()) {
4927
5768
  if (item.expression.kind === "list") {
4928
5769
  throw new TypeError("A row constructor is only allowed in a comparison or IN list");
4929
5770
  }
4930
5771
  // A qualified wildcard has no single output name until expansion resolves its columns.
4931
5772
  if (item.expression.kind === "wildcard" && item.expression.table !== undefined)
4932
5773
  continue;
4933
- if (aliases.has(item.alias))
4934
- throw new TypeError(`Duplicate output column: ${item.alias}`);
5774
+ if (aliases.has(item.alias)) {
5775
+ if (explicitAliases[index] === true) {
5776
+ throw new TypeError(`Duplicate output column: ${item.alias}`);
5777
+ }
5778
+ const base = item.alias;
5779
+ let suffix = 2;
5780
+ while (aliases.has(`${base}_${String(suffix)}`))
5781
+ suffix++;
5782
+ item.alias = `${base}_${String(suffix)}`;
5783
+ }
4935
5784
  aliases.add(item.alias);
4936
5785
  }
4937
5786
  return items;
@@ -5087,15 +5936,126 @@ class Parser {
5087
5936
  return this.#derivedSource(derived, alias);
5088
5937
  }
5089
5938
  const table = this.#identifier();
5939
+ if (table.toUpperCase() === "LATERAL") {
5940
+ this.#expectPunctuation("(");
5941
+ const derived = this.#queryExpression("(lateral)");
5942
+ this.#expectPunctuation(")");
5943
+ const alias = this.#sourceAlias();
5944
+ if (alias === undefined)
5945
+ throw new TypeError("A LATERAL derived table requires an alias");
5946
+ if (this.#punctuation("("))
5947
+ this.#applyColumnAliases(derived);
5948
+ return { ...this.#derivedSource(derived, alias), lateral: true };
5949
+ }
5090
5950
  if (this.#peek().text === "(") {
5091
5951
  // Two row-producing sources the executors have no operator for; both parse far enough to
5092
5952
  // say so, rather than failing on the punctuation that follows.
5093
5953
  const upper = table.toUpperCase();
5094
- if (upper === "LATERAL") {
5095
- throw new TypeError("LATERAL sources are not supported");
5096
- }
5097
5954
  if (upper === "JSON_TABLE") {
5098
- throw new TypeError("JSON_TABLE is not supported; JSON_VALUE and JSON_QUERY read one value");
5955
+ this.#expectPunctuation("(");
5956
+ const document = this.#expression();
5957
+ if (containsParameter(document) ||
5958
+ expressionColumns(document).length > 0 ||
5959
+ hasAggregate(document)) {
5960
+ throw new TypeError("JSON_TABLE currently requires a constant document");
5961
+ }
5962
+ this.#expectPunctuation(",");
5963
+ const rowPath = this.#take("string").text;
5964
+ this.#keyword("COLUMNS");
5965
+ this.#expectPunctuation("(");
5966
+ const columns = [];
5967
+ for (;;) {
5968
+ const name = this.#identifier();
5969
+ const columnType = this.#columnType();
5970
+ this.#keyword("PATH");
5971
+ columns.push({ name, ...columnType, path: this.#take("string").text });
5972
+ if (!this.#punctuation(","))
5973
+ break;
5974
+ }
5975
+ this.#expectPunctuation(")");
5976
+ this.#expectPunctuation(")");
5977
+ const alias = this.#sourceAlias();
5978
+ if (alias === undefined)
5979
+ throw new TypeError("JSON_TABLE requires an alias");
5980
+ const raw = externalSqlDomainValue(evaluate(document, {}));
5981
+ if (typeof raw !== "string")
5982
+ throw new TypeError("JSON_TABLE document must be JSON text");
5983
+ let parsed;
5984
+ try {
5985
+ parsed = JSON.parse(raw);
5986
+ }
5987
+ catch {
5988
+ throw new TypeError("JSON_TABLE document is invalid JSON");
5989
+ }
5990
+ const members = rowPath === "$"
5991
+ ? [parsed]
5992
+ : rowPath === "$[*]" && Array.isArray(parsed)
5993
+ ? parsed
5994
+ : (() => {
5995
+ throw new TypeError("JSON_TABLE row path supports $ and $[*]");
5996
+ })();
5997
+ const blockFor = (member, empty = false) => ({
5998
+ sql: "(json table row)",
5999
+ base: { table: DUAL_TABLE, alias: DUAL_TABLE },
6000
+ joins: [],
6001
+ select: columns.map((column) => {
6002
+ const selected = jsonAtPath(JSON.stringify(member), column.path, "JSON_TABLE");
6003
+ const value = empty
6004
+ ? column.sqlDomain?.kind === "numeric"
6005
+ ? jsonTableColumnValue(column, 0)
6006
+ : column.type === "number"
6007
+ ? 0
6008
+ : column.type === "boolean"
6009
+ ? false
6010
+ : column.type === "datetime"
6011
+ ? new Date(0)
6012
+ : ""
6013
+ : jsonTableColumnValue(column, selected.found ? selected.value : null);
6014
+ return {
6015
+ expression: {
6016
+ kind: "literal",
6017
+ value,
6018
+ ...(column.sqlDomain === undefined
6019
+ ? {}
6020
+ : { internalSqlValue: true, sqlDomain: column.sqlDomain }),
6021
+ },
6022
+ alias: column.name,
6023
+ };
6024
+ }),
6025
+ predicates: empty
6026
+ ? [
6027
+ {
6028
+ left: { kind: "literal", value: false },
6029
+ operator: "IS TRUE",
6030
+ right: { kind: "literal", value: true },
6031
+ },
6032
+ ]
6033
+ : [],
6034
+ groupBy: [],
6035
+ having: [],
6036
+ orderBy: [],
6037
+ });
6038
+ const blocks = members.length === 0 ? [blockFor(null, true)] : members.map((member) => blockFor(member));
6039
+ const firstBlock = blocks[0];
6040
+ if (firstBlock === undefined)
6041
+ throw new TypeError("JSON_TABLE requires a row source");
6042
+ const derived = blocks.length === 1
6043
+ ? firstBlock
6044
+ : {
6045
+ sql: "(json table)",
6046
+ base: {
6047
+ table: `(json table ${String(this.nextDerivedSequence())})`,
6048
+ alias: "json_table",
6049
+ union: { blocks, ops: blocks.slice(1).map(() => "union all") },
6050
+ },
6051
+ joins: [],
6052
+ select: [{ expression: { kind: "wildcard" }, alias: "*" }],
6053
+ predicates: [],
6054
+ groupBy: [],
6055
+ having: [],
6056
+ orderBy: [],
6057
+ };
6058
+ return this.#derivedSource(derived, alias);
5099
6059
  }
5100
6060
  }
5101
6061
  const candidate = this.#recursiveCandidate;
@@ -5385,14 +6345,13 @@ class Parser {
5385
6345
  // After a value expression, NOT can only introduce NOT BETWEEN / NOT IN / NOT LIKE.
5386
6346
  let negated = false;
5387
6347
  if (this.#isKeyword("NOT")) {
6348
+ const next = this.tokens[this.#index + 1];
6349
+ const nextWord = next?.kind === "identifier" && next.quoted !== true ? next.text.toUpperCase() : "";
6350
+ // In a column definition, `DEFAULT <expression> NOT NULL` ends the expression here.
6351
+ if (!["BETWEEN", "IN", "LIKE", "ILIKE", "SIMILAR"].includes(nextWord))
6352
+ return left;
5388
6353
  this.#keyword("NOT");
5389
6354
  negated = true;
5390
- if (!this.#isKeyword("BETWEEN") &&
5391
- !this.#isKeyword("IN") &&
5392
- !this.#isKeyword("LIKE") &&
5393
- !this.#isKeyword("ILIKE")) {
5394
- throw new TypeError(`Expected BETWEEN, IN, LIKE, or ILIKE after NOT`);
5395
- }
5396
6355
  }
5397
6356
  if (this.#isKeyword("BETWEEN")) {
5398
6357
  this.#keyword("BETWEEN");
@@ -5469,6 +6428,26 @@ class Parser {
5469
6428
  ...(escape === undefined ? {} : { escape }),
5470
6429
  };
5471
6430
  }
6431
+ if (this.#isKeyword("SIMILAR")) {
6432
+ this.#keyword("SIMILAR");
6433
+ this.#keyword("TO");
6434
+ const pattern = this.#additive();
6435
+ let escape;
6436
+ if (this.#isKeyword("ESCAPE")) {
6437
+ this.#keyword("ESCAPE");
6438
+ escape = this.#take("string").text;
6439
+ if (Array.from(escape).length !== 1) {
6440
+ throw new TypeError("SIMILAR TO ESCAPE takes a single character");
6441
+ }
6442
+ }
6443
+ return {
6444
+ kind: "condition",
6445
+ operator: negated ? "NOT SIMILAR TO" : "SIMILAR TO",
6446
+ left,
6447
+ right: pattern,
6448
+ ...(escape === undefined ? {} : { escape }),
6449
+ };
6450
+ }
5472
6451
  const token = this.#peek();
5473
6452
  if (token.kind === "operator" && ["=", "!=", "<>", ">", ">=", "<", "<="].includes(token.text)) {
5474
6453
  const operator = this.#comparison();
@@ -5495,6 +6474,15 @@ class Parser {
5495
6474
  #additive(minimumPrecedence = 0) {
5496
6475
  let left = this.#primary();
5497
6476
  for (;;) {
6477
+ if (this.#isKeyword("COLLATE")) {
6478
+ this.#keyword("COLLATE");
6479
+ left = {
6480
+ kind: "call",
6481
+ name: "MINNOW_COLLATE",
6482
+ arguments: [left, { kind: "literal", value: this.#identifier() }],
6483
+ };
6484
+ continue;
6485
+ }
5498
6486
  const operator = this.#peek().text;
5499
6487
  // || binds loosest, matching PostgreSQL: concatenation applies to whole arithmetic terms.
5500
6488
  const precedence = operator === "*" || operator === "/" || operator === "%"
@@ -5559,7 +6547,11 @@ class Parser {
5559
6547
  return this.#parameterExpression();
5560
6548
  if (token.kind === "number") {
5561
6549
  this.#index += 1;
5562
- return { kind: "literal", value: Number(token.text) };
6550
+ const value = Number(token.text);
6551
+ if (!token.text.includes(".") && !Number.isSafeInteger(value)) {
6552
+ throw new SqlCompileError(`Integer literal is outside the exact safe range: ${token.text}`, token.start, token.end - token.start);
6553
+ }
6554
+ return { kind: "literal", value };
5563
6555
  }
5564
6556
  if (token.kind === "string") {
5565
6557
  this.#index += 1;
@@ -5804,6 +6796,30 @@ class Parser {
5804
6796
  if ((upper === "TIMESTAMP" || upper === "DATETIME") && this.#peek().kind === "string") {
5805
6797
  return { kind: "literal", value: timestampLiteral(this.#take("string").text) };
5806
6798
  }
6799
+ if (upper === "TIME" && this.#peek().kind === "string") {
6800
+ return {
6801
+ kind: "literal",
6802
+ value: timeDomainValue(this.#take("string").text),
6803
+ internalSqlValue: true,
6804
+ sqlDomain: { kind: "time" },
6805
+ };
6806
+ }
6807
+ if (upper === "INTERVAL" && this.#peek().kind === "string") {
6808
+ return {
6809
+ kind: "literal",
6810
+ value: intervalDomainValue(this.#take("string").text),
6811
+ internalSqlValue: true,
6812
+ sqlDomain: { kind: "interval" },
6813
+ };
6814
+ }
6815
+ if (upper === "ARRAY" && this.#punctuation("[")) {
6816
+ const arguments_ = [];
6817
+ if (!this.#punctuation("]")) {
6818
+ arguments_.push(...this.#expressionList());
6819
+ this.#expectPunctuation("]");
6820
+ }
6821
+ return { kind: "call", name: "ARRAY", arguments: arguments_ };
6822
+ }
5807
6823
  if (this.#punctuation("(")) {
5808
6824
  if (upper === "ROW_NUMBER" ||
5809
6825
  upper === "RANK" ||
@@ -5936,8 +6952,15 @@ class Parser {
5936
6952
  // canonical plan names. ANY_VALUE picks an implementation-dependent row of the group
5937
6953
  // (T626); MIN is one such choice and reuses its accumulator exactly.
5938
6954
  const name = (upper === "ANY_VALUE" ? "MIN" : (functionSpellings.get(upper) ?? upper));
6955
+ if (name === "MINNOW_TUPLE_KEY" || name === "MINNOW_COLLATE") {
6956
+ throw new TypeError(`Unsupported function: ${identifier}`);
6957
+ }
5939
6958
  if (!aggregateNames.has(name) && !scalarFunctionNames.has(name))
5940
6959
  throw new TypeError(`Unsupported function: ${identifier}`);
6960
+ if (name === "NEXTVAL" || name === "CURRVAL")
6961
+ this.usesSequenceCalls = true;
6962
+ if (volatileScalarFunctionNames.has(name))
6963
+ this.usesVolatileFunctions = true;
5941
6964
  let distinct = false;
5942
6965
  if (this.#isKeyword("DISTINCT")) {
5943
6966
  if (!aggregateNames.has(name)) {
@@ -5951,15 +6974,32 @@ class Parser {
5951
6974
  this.#keyword("ALL");
5952
6975
  }
5953
6976
  const args = [];
6977
+ let aggregateOrderBy;
5954
6978
  if (!this.#punctuation(")")) {
5955
- args.push(...this.#expressionList());
6979
+ if (name === "STRING_AGG") {
6980
+ args.push(this.#expression());
6981
+ this.#expectPunctuation(",");
6982
+ args.push(this.#expression());
6983
+ if (this.#isKeyword("ORDER"))
6984
+ aggregateOrderBy = this.#orderByClause();
6985
+ }
6986
+ else {
6987
+ args.push(...this.#expressionList());
6988
+ }
5956
6989
  this.#expectPunctuation(")");
5957
6990
  }
5958
- if (distinct && (args.length !== 1 || args[0]?.kind === "wildcard")) {
6991
+ const distinctArity = name === "STRING_AGG" ? 2 : 1;
6992
+ if (distinct && (args.length !== distinctArity || args[0]?.kind === "wildcard")) {
5959
6993
  throw new TypeError(`${name}(DISTINCT) requires exactly one scalar argument`);
5960
6994
  }
5961
- if (aggregateNames.has(name) && args.length !== 1)
6995
+ if (name === "STRING_AGG" && args.length !== 2) {
6996
+ throw new TypeError("STRING_AGG requires a value and delimiter");
6997
+ }
6998
+ if (aggregateNames.has(name) && name !== "STRING_AGG" && args.length !== 1)
5962
6999
  throw new TypeError(`${name} requires exactly one argument`);
7000
+ if (name === "JSON_ARRAYAGG" && args[0]?.kind === "wildcard") {
7001
+ throw new TypeError("JSON_ARRAYAGG requires a scalar value expression");
7002
+ }
5963
7003
  if (name === "ROUND" && (args.length < 1 || args.length > 2))
5964
7004
  throw new TypeError("ROUND requires one or two arguments");
5965
7005
  if (name === "COALESCE" && args.length < 1)
@@ -5988,6 +7028,9 @@ class Parser {
5988
7028
  if (statementDatetimeNames.has(name) && args.length !== 0) {
5989
7029
  throw new TypeError(`${name} takes no arguments`);
5990
7030
  }
7031
+ if (volatileScalarFunctionNames.has(name) && args.length !== 0) {
7032
+ throw new TypeError(`${name} takes no arguments`);
7033
+ }
5991
7034
  if ((name === "JSON_VALUE" || name === "JSON_QUERY" || name === "JSON_EXISTS") &&
5992
7035
  args.length !== 2) {
5993
7036
  throw new TypeError(`${name} requires a JSON document and a path`);
@@ -6028,6 +7071,9 @@ class Parser {
6028
7071
  }
6029
7072
  }
6030
7073
  if (aggregateNames.has(name) && this.#isKeyword("FILTER")) {
7074
+ if (name === "JSON_ARRAYAGG") {
7075
+ throw new TypeError("JSON_ARRAYAGG FILTER is not supported; filter rows in WHERE");
7076
+ }
6031
7077
  // FILTER (WHERE cond) desugars into the aggregate's argument: rows failing the filter
6032
7078
  // contribute NULL, which every aggregate skips — COUNT(*) counts a CASE over 1.
6033
7079
  this.#keyword("FILTER");
@@ -6048,6 +7094,12 @@ class Parser {
6048
7094
  });
6049
7095
  }
6050
7096
  if (aggregateNames.has(name) && this.#isKeyword("OVER")) {
7097
+ if (name === "JSON_ARRAYAGG") {
7098
+ throw new TypeError("JSON_ARRAYAGG window use is not supported");
7099
+ }
7100
+ if (name === "STRING_AGG") {
7101
+ throw new TypeError("STRING_AGG window use is not supported");
7102
+ }
6051
7103
  if (distinct)
6052
7104
  throw new TypeError("DISTINCT window aggregates are not supported");
6053
7105
  this.#keyword("OVER");
@@ -6065,7 +7117,13 @@ class Parser {
6065
7117
  ...(frame === undefined ? {} : { frame }),
6066
7118
  };
6067
7119
  }
6068
- return { kind: "call", name, arguments: args, ...(distinct ? { distinct: true } : {}) };
7120
+ return {
7121
+ kind: "call",
7122
+ name,
7123
+ arguments: args,
7124
+ ...(distinct ? { distinct: true } : {}),
7125
+ ...(aggregateOrderBy === undefined ? {} : { aggregateOrderBy }),
7126
+ };
6069
7127
  }
6070
7128
  let reference = identifier;
6071
7129
  if (this.#punctuation(".")) {
@@ -6655,7 +7713,9 @@ export function expandDistinctWildcard(plan, columnsOf) {
6655
7713
  if (columns === undefined || columns.length === 0) {
6656
7714
  throw new TypeError(`SELECT DISTINCT * requires known columns for: ${source.table}`);
6657
7715
  }
6658
- return columns.map((name) => {
7716
+ return columns
7717
+ .filter((name) => !name.startsWith("\0"))
7718
+ .map((name) => {
6659
7719
  const output = multiple ? `${source.alias}.${name}` : name;
6660
7720
  return { expression: { kind: "column", reference: output }, alias: output };
6661
7721
  });
@@ -6669,16 +7729,20 @@ export function expandDistinctWildcard(plan, columnsOf) {
6669
7729
  * table's columns positionally (E051-09). The table's own column order is only known here, so
6670
7730
  * the parser records the names and this pass — one per execution entry — applies them.
6671
7731
  */
6672
- /** Whether any block of the plan reads a table name the catalog answers with a view. */
6673
- export function planReadsViews(plan, isView) {
6674
- if ([plan.base, ...plan.joins].some((source) => source.derived === undefined && isView(source.table)))
7732
+ /** Whether any block of the plan reads a table accepted by the supplied catalog predicate. */
7733
+ export function planReadsTable(plan, matches) {
7734
+ if ([plan.base, ...plan.joins].some((source) => source.derived === undefined && matches(source.table)))
6675
7735
  return true;
6676
7736
  let nested = false;
6677
7737
  forEachNestedBlock(plan, (inner) => {
6678
- nested ||= planReadsViews(inner, isView);
7738
+ nested ||= planReadsTable(inner, matches);
6679
7739
  });
6680
7740
  return nested;
6681
7741
  }
7742
+ /** Whether any block of the plan reads a table name the catalog answers with a view. */
7743
+ export function planReadsViews(plan, isView) {
7744
+ return planReadsTable(plan, isView);
7745
+ }
6682
7746
  /**
6683
7747
  * Replaces every reference to a view with the query it stands for, as a derived table under the
6684
7748
  * reference's own alias (F031-02). A view whose body reads another view expands too, up to a
@@ -6778,11 +7842,13 @@ export function withTiesPlan(plan) {
6778
7842
  */
6779
7843
  function sourceWildcardColumns(source, columnsOf) {
6780
7844
  if (source.derived !== undefined)
6781
- return source.derived.select.map((item) => item.alias);
7845
+ return source.derived.select.map((item) => item.alias).filter((name) => !name.startsWith("\0"));
6782
7846
  if (source.union !== undefined) {
6783
- return source.union.blocks[0]?.select.map((item) => item.alias);
7847
+ return source.union.blocks[0]?.select
7848
+ .map((item) => item.alias)
7849
+ .filter((name) => !name.startsWith("\0"));
6784
7850
  }
6785
- return columnsOf(source.table);
7851
+ return columnsOf(source.table)?.filter((name) => !name.startsWith("\0"));
6786
7852
  }
6787
7853
  /** Whether any block of the plan still carries an unresolved NATURAL join marker. */
6788
7854
  export function planHasNaturalJoins(plan) {
@@ -7104,7 +8170,7 @@ export function transparentProjectionSource(plan) {
7104
8170
  }
7105
8171
  /** Projects a result to a wrapper's visible aliases, preserving row order. */
7106
8172
  export function projectResultColumns(result, aliases) {
7107
- return {
8173
+ return copyQueryResultExternalization(result, {
7108
8174
  columns: [...aliases],
7109
8175
  rows: result.rows.map((row) => {
7110
8176
  const projected = {};
@@ -7112,7 +8178,7 @@ export function projectResultColumns(result, aliases) {
7112
8178
  projected[alias] = row[alias] ?? null;
7113
8179
  return projected;
7114
8180
  }),
7115
- };
8181
+ });
7116
8182
  }
7117
8183
  /** Names a derived (subquery or expanded CTE) source under the shared sequence. */
7118
8184
  export function derivedTableSource(derived, alias, nextSequence) {
@@ -7146,6 +8212,43 @@ function renameBlockOutputs(block, columns, name) {
7146
8212
  * writes a T and leaves the time off entirely for midnight. A literal without a zone is UTC, the
7147
8213
  * same reading `DATE` already takes and the same one every datetime in a Minnow database has.
7148
8214
  */
8215
+ function jsonTableColumnValue(column, value) {
8216
+ if (value === null || value === undefined)
8217
+ return null;
8218
+ if (column.sqlDomain !== undefined) {
8219
+ const input = column.sqlDomain.kind === "array" && typeof value !== "string"
8220
+ ? JSON.stringify(value)
8221
+ : value;
8222
+ return normalizeSqlDomainValue(column.sqlDomain, input);
8223
+ }
8224
+ if (column.type === "string") {
8225
+ return typeof value === "string" ? value : JSON.stringify(value);
8226
+ }
8227
+ if (column.type === "boolean") {
8228
+ if (typeof value !== "boolean")
8229
+ throw new TypeError("JSON_TABLE boolean column needs a boolean");
8230
+ return value;
8231
+ }
8232
+ if (column.type === "number") {
8233
+ const number = typeof value === "number" ? value : Number(value);
8234
+ if (!Number.isFinite(number) || (column.integer === true && !Number.isSafeInteger(number))) {
8235
+ throw new TypeError("JSON_TABLE numeric column has an invalid value");
8236
+ }
8237
+ return number;
8238
+ }
8239
+ let date;
8240
+ if (value instanceof Date)
8241
+ date = value;
8242
+ else if (typeof value === "string")
8243
+ date = new Date(value);
8244
+ else if (typeof value === "number")
8245
+ date = new Date(value);
8246
+ else
8247
+ throw new TypeError("JSON_TABLE datetime is invalid");
8248
+ if (!Number.isFinite(date.getTime()))
8249
+ throw new TypeError("JSON_TABLE datetime is invalid");
8250
+ return date;
8251
+ }
7149
8252
  export function timestampLiteral(text) {
7150
8253
  const trimmed = text.trim();
7151
8254
  const match = /^(\d{4}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?))?(Z|[+-]\d{2}:?\d{2})?$/.exec(trimmed);
@@ -7465,7 +8568,7 @@ function tokenize(sql) {
7465
8568
  }
7466
8569
  if (["+", "-", "*", "/", "%", "=", ">", "<"].includes(character))
7467
8570
  tokens.push({ kind: "operator", text: character, start: index, end: index + 1 });
7468
- else if (["(", ")", ",", "."].includes(character))
8571
+ else if (["(", ")", "[", "]", ",", "."].includes(character))
7469
8572
  tokens.push({ kind: "punctuation", text: character, start: index, end: index + 1 });
7470
8573
  else
7471
8574
  throw new SqlCompileError(`Unsupported SQL character: ${character}`, index, 1);