@minnowdb/core 0.2.0 → 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 (146) 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 +3 -0
  15. package/dist/engine/client.d.ts.map +1 -1
  16. package/dist/engine/client.js +21 -10
  17. package/dist/engine/client.js.map +1 -1
  18. package/dist/engine/database.d.ts +71 -5
  19. package/dist/engine/database.d.ts.map +1 -1
  20. package/dist/engine/database.js +3619 -421
  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 +6 -2
  34. package/dist/engine/live.d.ts.map +1 -1
  35. package/dist/engine/live.js +10 -2
  36. package/dist/engine/live.js.map +1 -1
  37. package/dist/engine/optimizer.d.ts.map +1 -1
  38. package/dist/engine/optimizer.js +258 -23
  39. package/dist/engine/optimizer.js.map +1 -1
  40. package/dist/engine/query-cache.d.ts +1 -1
  41. package/dist/engine/query-cache.d.ts.map +1 -1
  42. package/dist/engine/query-cache.js +3 -1
  43. package/dist/engine/query-cache.js.map +1 -1
  44. package/dist/engine/query.d.ts +132 -22
  45. package/dist/engine/query.d.ts.map +1 -1
  46. package/dist/engine/query.js +1368 -246
  47. package/dist/engine/query.js.map +1 -1
  48. package/dist/engine/schema-wire.d.ts +6 -2
  49. package/dist/engine/schema-wire.d.ts.map +1 -1
  50. package/dist/engine/schema-wire.js +40 -33
  51. package/dist/engine/schema-wire.js.map +1 -1
  52. package/dist/engine/schema.d.ts +157 -76
  53. package/dist/engine/schema.d.ts.map +1 -1
  54. package/dist/engine/schema.js +548 -103
  55. package/dist/engine/schema.js.map +1 -1
  56. package/dist/engine/sort-keys.d.ts +4 -3
  57. package/dist/engine/sort-keys.d.ts.map +1 -1
  58. package/dist/engine/sort-keys.js +42 -27
  59. package/dist/engine/sort-keys.js.map +1 -1
  60. package/dist/engine/sql-domains.d.ts +26 -0
  61. package/dist/engine/sql-domains.d.ts.map +1 -0
  62. package/dist/engine/sql-domains.js +421 -0
  63. package/dist/engine/sql-domains.js.map +1 -0
  64. package/dist/engine/sql-driver.d.ts +19 -0
  65. package/dist/engine/sql-driver.d.ts.map +1 -0
  66. package/dist/engine/sql-driver.js +2 -0
  67. package/dist/engine/sql-driver.js.map +1 -0
  68. package/dist/engine/sql-json.d.ts +7 -8
  69. package/dist/engine/sql-json.d.ts.map +1 -1
  70. package/dist/engine/sql-json.js +28 -14
  71. package/dist/engine/sql-json.js.map +1 -1
  72. package/dist/engine/sql-semantics.d.ts +2 -0
  73. package/dist/engine/sql-semantics.d.ts.map +1 -1
  74. package/dist/engine/sql-semantics.js +69 -3
  75. package/dist/engine/sql-semantics.js.map +1 -1
  76. package/dist/engine/vector.d.ts +5 -1
  77. package/dist/engine/vector.d.ts.map +1 -1
  78. package/dist/engine/vector.js +433 -61
  79. package/dist/engine/vector.js.map +1 -1
  80. package/dist/engine/worker-host.d.ts +1 -0
  81. package/dist/engine/worker-host.d.ts.map +1 -1
  82. package/dist/engine/worker-host.js +8 -0
  83. package/dist/engine/worker-host.js.map +1 -1
  84. package/dist/plan/index.d.ts +3 -3
  85. package/dist/plan/index.js +3 -3
  86. package/dist/storage/indexeddb.d.ts +44 -5
  87. package/dist/storage/indexeddb.d.ts.map +1 -1
  88. package/dist/storage/indexeddb.js +738 -175
  89. package/dist/storage/indexeddb.js.map +1 -1
  90. package/dist/storage/memory.d.ts +27 -10
  91. package/dist/storage/memory.d.ts.map +1 -1
  92. package/dist/storage/memory.js +67 -18
  93. package/dist/storage/memory.js.map +1 -1
  94. package/dist/storage/opfs/leader.d.ts +28 -6
  95. package/dist/storage/opfs/leader.d.ts.map +1 -1
  96. package/dist/storage/opfs/leader.js +301 -34
  97. package/dist/storage/opfs/leader.js.map +1 -1
  98. package/dist/storage/opfs/rpc.d.ts.map +1 -1
  99. package/dist/storage/opfs/rpc.js +2 -1
  100. package/dist/storage/opfs/rpc.js.map +1 -1
  101. package/dist/storage/opfs/store.d.ts +34 -5
  102. package/dist/storage/opfs/store.d.ts.map +1 -1
  103. package/dist/storage/opfs/store.js +41 -0
  104. package/dist/storage/opfs/store.js.map +1 -1
  105. package/dist/storage/snapshot.d.ts +14 -0
  106. package/dist/storage/snapshot.d.ts.map +1 -1
  107. package/dist/storage/snapshot.js +44 -0
  108. package/dist/storage/snapshot.js.map +1 -1
  109. package/dist/storage/toolkit/index.d.ts +1 -1
  110. package/dist/storage/toolkit/index.d.ts.map +1 -1
  111. package/dist/storage/toolkit/index.js +1 -1
  112. package/dist/storage/toolkit/index.js.map +1 -1
  113. package/dist/storage/toolkit/record-core.d.ts +19 -5
  114. package/dist/storage/toolkit/record-core.d.ts.map +1 -1
  115. package/dist/storage/toolkit/record-core.js +353 -78
  116. package/dist/storage/toolkit/record-core.js.map +1 -1
  117. package/dist/storage/toolkit/wire.d.ts +15 -0
  118. package/dist/storage/toolkit/wire.d.ts.map +1 -1
  119. package/dist/storage/toolkit/wire.js +134 -0
  120. package/dist/storage/toolkit/wire.js.map +1 -1
  121. package/dist/storage/types.d.ts +213 -31
  122. package/dist/storage/types.d.ts.map +1 -1
  123. package/dist/storage/types.js +291 -20
  124. package/dist/storage/types.js.map +1 -1
  125. package/dist/testing/block-store-conformance.d.ts.map +1 -1
  126. package/dist/testing/block-store-conformance.js +153 -2
  127. package/dist/testing/block-store-conformance.js.map +1 -1
  128. package/dist/testing/index.d.ts +9 -0
  129. package/dist/testing/index.d.ts.map +1 -1
  130. package/dist/testing/index.js +23 -0
  131. package/dist/testing/index.js.map +1 -1
  132. package/dist/testing/simulator.d.ts +98 -0
  133. package/dist/testing/simulator.d.ts.map +1 -0
  134. package/dist/testing/simulator.js +560 -0
  135. package/dist/testing/simulator.js.map +1 -0
  136. package/dist/testing/sqllogictest.d.ts +90 -0
  137. package/dist/testing/sqllogictest.d.ts.map +1 -0
  138. package/dist/testing/sqllogictest.js +435 -0
  139. package/dist/testing/sqllogictest.js.map +1 -0
  140. package/dist/transactions/index.d.ts +18 -3
  141. package/dist/transactions/index.d.ts.map +1 -1
  142. package/dist/transactions/index.js +64 -8
  143. package/dist/transactions/index.js.map +1 -1
  144. package/package.json +5 -3
  145. package/postgres-feature-profile.json +223 -0
  146. package/sql-feature-matrix.json +179 -251
@@ -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);
@@ -1180,6 +1390,16 @@ function bindExpression(expression, values) {
1180
1390
  if (expression.kind === "parameter") {
1181
1391
  return { kind: "literal", value: values[expression.index] ?? null };
1182
1392
  }
1393
+ if (expression.kind === "fts" && expression.queryParameter !== undefined) {
1394
+ const value = values[expression.queryParameter] ?? null;
1395
+ if (typeof value !== "string") {
1396
+ throw new TypeError("A full-text search query parameter must be a string");
1397
+ }
1398
+ validateFtsQuery(value);
1399
+ expression.query = value;
1400
+ delete expression.queryParameter;
1401
+ return expression;
1402
+ }
1183
1403
  if (expression.kind === "subquery" || expression.kind === "exists") {
1184
1404
  bindBlock(expression.block, values);
1185
1405
  return expression;
@@ -1193,6 +1413,9 @@ function bindExpression(expression, values) {
1193
1413
  }
1194
1414
  if (expression.kind === "call") {
1195
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
+ }
1196
1419
  return expression;
1197
1420
  }
1198
1421
  if (expression.kind === "list") {
@@ -1310,7 +1533,7 @@ export function bindPlanParameters(plan, params) {
1310
1533
  return clone;
1311
1534
  }
1312
1535
  function isParameterSlot(value) {
1313
- return typeof value === "object" && value !== null && !(value instanceof Date);
1536
+ return (typeof value === "object" && value !== null && !(value instanceof Date) && "parameter" in value);
1314
1537
  }
1315
1538
  /** The mutation-statement counterpart of bindPlanParameters; SELECTs bind through their plan. */
1316
1539
  export function bindStatementParameters(statement, params) {
@@ -1322,6 +1545,8 @@ export function bindStatementParameters(statement, params) {
1322
1545
  throw new TypeError("SELECT parameters bind through the query pipeline, not the statement");
1323
1546
  }
1324
1547
  if (statement.kind === "create-table" ||
1548
+ statement.kind === "create-enum" ||
1549
+ statement.kind === "create-sequence" ||
1325
1550
  statement.kind === "create-trigger" ||
1326
1551
  statement.kind === "drop-trigger") {
1327
1552
  throw new TypeError("DDL statements take no parameters");
@@ -1333,6 +1558,14 @@ export function bindStatementParameters(statement, params) {
1333
1558
  if (clone.query !== undefined)
1334
1559
  bindBlock(clone.query, values);
1335
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
+ }
1336
1569
  return clone;
1337
1570
  }
1338
1571
  if (clone.kind === "drop-table" ||
@@ -1382,9 +1615,13 @@ export function bindStatementParameters(statement, params) {
1382
1615
  export function inferBlockSchema(plan, schemas) {
1383
1616
  const sources = [plan.base, ...plan.joins];
1384
1617
  const multipleSources = sources.length > 1;
1385
- 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) => ({
1386
1621
  name: multipleSources ? `${source.alias}.${column.name}` : column.name,
1387
1622
  type: column.type,
1623
+ ...(column.integer === true ? { integer: true } : {}),
1624
+ ...(column.sqlDomain === undefined ? {} : { sqlDomain: column.sqlDomain }),
1388
1625
  }));
1389
1626
  if (plan.select[0]?.expression.kind === "wildcard" &&
1390
1627
  plan.select[0].expression.table === undefined)
@@ -1407,6 +1644,84 @@ export function inferBlockSchema(plan, schemas) {
1407
1644
  throw new TypeError(`Ambiguous or missing column: ${reference}`);
1408
1645
  return matches[0] ?? "string";
1409
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
+ };
1410
1725
  const infer = (expression) => {
1411
1726
  if (expression.kind === "subquery" || expression.kind === "list") {
1412
1727
  throw new TypeError("Subqueries must be resolved before schema inference");
@@ -1479,17 +1794,21 @@ export function inferBlockSchema(plan, schemas) {
1479
1794
  }
1480
1795
  return "string";
1481
1796
  }
1797
+ const exactNumeric = inferDomain(expression)?.kind === "numeric";
1482
1798
  for (const side of [expression.left, expression.right]) {
1483
1799
  const type = infer(side);
1484
- if (type === "string" || type === "boolean") {
1800
+ if (type === "boolean" || (type === "string" && inferDomain(side)?.kind !== "numeric")) {
1485
1801
  throw new TypeError(`Arithmetic requires numeric operands: ${plan.sql}`);
1486
1802
  }
1487
1803
  }
1488
- return "number";
1804
+ return exactNumeric ? "string" : "number";
1489
1805
  }
1490
- if (expression.name === "COUNT" || expression.name === "SUM" || expression.name === "AVG") {
1806
+ if (expression.name === "COUNT") {
1491
1807
  return "number";
1492
1808
  }
1809
+ if (expression.name === "SUM" || expression.name === "AVG") {
1810
+ return inferDomain(expression)?.kind === "numeric" ? "string" : "number";
1811
+ }
1493
1812
  if (expression.name === "ROUND" ||
1494
1813
  expression.name === "LENGTH" ||
1495
1814
  expression.name === "ABS" ||
@@ -1501,13 +1820,22 @@ export function inferBlockSchema(plan, schemas) {
1501
1820
  expression.name === "INSTR" ||
1502
1821
  expression.name === "EXTRACT" ||
1503
1822
  expression.name === "OCTET_LENGTH" ||
1504
- expression.name === "GROUPING") {
1823
+ expression.name === "GROUPING" ||
1824
+ expression.name === "NEXTVAL" ||
1825
+ expression.name === "CURRVAL" ||
1826
+ expression.name === "RANDOM") {
1505
1827
  return "number";
1506
1828
  }
1507
- if (expression.name === "JSON_VALUE" ||
1829
+ if (expression.name === "JSON_ARRAYAGG" ||
1830
+ expression.name === "STRING_AGG" ||
1831
+ expression.name === "JSON_VALUE" ||
1508
1832
  expression.name === "JSON_QUERY" ||
1509
1833
  expression.name === "JSON_OBJECT" ||
1510
- 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") {
1511
1839
  return "string";
1512
1840
  }
1513
1841
  if (expression.name === "JSON_EXISTS" || expression.name === "IS_JSON")
@@ -1517,8 +1845,8 @@ export function inferBlockSchema(plan, schemas) {
1517
1845
  if (expression.name === "CURRENT_DATE" || expression.name === "CURRENT_TIMESTAMP") {
1518
1846
  return "datetime";
1519
1847
  }
1520
- // The engine has no TIME type, so LOCALTIME reads as an 'HH:MM:SS' string, like SQLite's
1521
- // 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.
1522
1850
  if (expression.name === "LOCALTIME")
1523
1851
  return "string";
1524
1852
  if (expression.name === "NULLIF" ||
@@ -1542,6 +1870,8 @@ export function inferBlockSchema(plan, schemas) {
1542
1870
  const word = target?.kind === "literal" && typeof target.value === "string" ? target.value : "";
1543
1871
  if (word === "number-integer")
1544
1872
  return "number";
1873
+ if (castDomain(word) !== undefined)
1874
+ return "string";
1545
1875
  if (word === "number" || word === "string" || word === "boolean" || word === "datetime") {
1546
1876
  return word;
1547
1877
  }
@@ -1594,9 +1924,60 @@ export function inferBlockSchema(plan, schemas) {
1594
1924
  if (type === "null") {
1595
1925
  throw new TypeError(`Cannot infer a column type for output ${item.alias}`);
1596
1926
  }
1597
- 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
+ ];
1598
1942
  });
1599
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
+ }
1600
1981
  export function referencedColumns(plan, schemas) {
1601
1982
  const sources = [plan.base, ...plan.joins];
1602
1983
  const sourceAliases = sources.map((source) => source.alias);
@@ -1646,8 +2027,13 @@ export function referencedColumns(plan, schemas) {
1646
2027
  for (const source of sources) {
1647
2028
  if (named !== undefined && source.alias !== named)
1648
2029
  continue;
1649
- 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;
1650
2035
  requested.get(source.table)?.add(column);
2036
+ }
1651
2037
  }
1652
2038
  }
1653
2039
  };
@@ -1736,12 +2122,16 @@ export function createPreparedQuery(plan, tables, options = {}) {
1736
2122
  const ties = withTiesPlan(plan);
1737
2123
  if (ties.plan !== plan)
1738
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);
1739
2129
  // Every engine entry expands MATCH(*) exactly once, here against the row tables' own
1740
2130
  // columns; past this point no executor sees the "*" sentinel.
1741
2131
  plan = expandFtsColumns(plan, rowTableSearchableColumns(tables));
1742
2132
  const resolution = subqueryResolutionSteps(plan);
1743
2133
  for (const step of resolution.steps)
1744
- step.substitute(executeRowQuery(step.block, tables));
2134
+ step.substitute(executeRowQueryInternal(step.block, tables));
1745
2135
  plan = resolution.plan;
1746
2136
  tables = resolveDerivedRowTables(plan, tables);
1747
2137
  plan = expandDistinctWildcard(plan, wildcardRowColumns(tables));
@@ -1754,7 +2144,7 @@ export function createPreparedQuery(plan, tables, options = {}) {
1754
2144
  return createPreparedRowQuery(plan, tables, memory);
1755
2145
  }
1756
2146
  try {
1757
- return createPreparedColumnarQuery(plan, normalizeColumnarTables(plan, tables), memory);
2147
+ return trimPreparedResults(createPreparedColumnarQuery(plan, normalizeColumnarTables(plan, tables), memory), externalizeQueryResult);
1758
2148
  }
1759
2149
  catch (error) {
1760
2150
  memory.close();
@@ -1789,6 +2179,7 @@ export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemo
1789
2179
  memory.close();
1790
2180
  throw error;
1791
2181
  }
2182
+ const outputNeedsExternalization = options.outputNeedsExternalization;
1792
2183
  return {
1793
2184
  sql: plan.sql,
1794
2185
  tables: [plan.base.table, ...plan.joins.map((join) => join.table)],
@@ -1798,12 +2189,12 @@ export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemo
1798
2189
  execute() {
1799
2190
  if (closed || prepared === undefined)
1800
2191
  throw new Error("Prepared query is closed");
1801
- return prepared.execute();
2192
+ return markExternalizationState(prepared.execute(), outputNeedsExternalization);
1802
2193
  },
1803
2194
  async executeAsync(options) {
1804
2195
  if (closed || prepared === undefined)
1805
2196
  throw new Error("Prepared query is closed");
1806
- return prepared.executeAsync(options);
2197
+ return markExternalizationState(await prepared.executeAsync(options), outputNeedsExternalization);
1807
2198
  },
1808
2199
  close() {
1809
2200
  if (closed)
@@ -1827,12 +2218,12 @@ function createPreparedRowQuery(plan, tables, memory) {
1827
2218
  execute() {
1828
2219
  if (closed || rows === undefined)
1829
2220
  throw new Error("Prepared query is closed");
1830
- return executeRowQuery(plan, cloneRowTables(rows));
2221
+ return externalizeQueryResult(executeRowQueryInternal(plan, cloneRowTables(rows)));
1831
2222
  },
1832
2223
  async executeAsync() {
1833
2224
  if (closed || rows === undefined)
1834
2225
  throw new Error("Prepared query is closed");
1835
- return executeRowQuery(plan, cloneRowTables(rows));
2226
+ return externalizeQueryResult(executeRowQueryInternal(plan, cloneRowTables(rows)));
1836
2227
  },
1837
2228
  close() {
1838
2229
  closed = true;
@@ -1841,19 +2232,23 @@ function createPreparedRowQuery(plan, tables, memory) {
1841
2232
  },
1842
2233
  };
1843
2234
  }
1844
- function cloneRowTables(tables) {
2235
+ function cloneRowTables(tables, protectText = false) {
1845
2236
  return new Map([...tables].map(([name, rows]) => [
1846
2237
  name,
1847
2238
  rows.map((row) => Object.fromEntries(Object.entries(row).map(([column, value]) => [
1848
2239
  column,
1849
- 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,
1850
2245
  ]))),
1851
2246
  ]));
1852
2247
  }
1853
2248
  export function executeQuery(plan, tables, options = {}) {
1854
2249
  const prepared = createPreparedQuery(plan, tables, options);
1855
2250
  try {
1856
- return prepared.execute();
2251
+ return externalizeQueryResult(prepared.execute());
1857
2252
  }
1858
2253
  finally {
1859
2254
  prepared.close();
@@ -1983,8 +2378,12 @@ export function createRecursiveCteState(base, all) {
1983
2378
  export function childExpressions(expression) {
1984
2379
  if (expression.kind === "binary")
1985
2380
  return [expression.left, expression.right];
1986
- if (expression.kind === "call")
1987
- return [...expression.arguments];
2381
+ if (expression.kind === "call") {
2382
+ return [
2383
+ ...expression.arguments,
2384
+ ...(expression.aggregateOrderBy ?? []).map((order) => order.expression),
2385
+ ];
2386
+ }
1988
2387
  if (expression.kind === "list")
1989
2388
  return [...expression.items];
1990
2389
  if (expression.kind === "condition" || expression.kind === "logical") {
@@ -2025,7 +2424,18 @@ export function mapChildExpressions(expression, map) {
2025
2424
  if (expression.kind === "not")
2026
2425
  return { ...expression, operand: map(expression.operand) };
2027
2426
  if (expression.kind === "call") {
2028
- 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
+ };
2029
2439
  }
2030
2440
  if (expression.kind === "list")
2031
2441
  return { ...expression, items: expression.items.map(map) };
@@ -2274,6 +2684,21 @@ function aggregateWindowMembers(window, values, members) {
2274
2684
  if (present.length === 0)
2275
2685
  return null;
2276
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
+ }
2277
2702
  const total = present.reduce((sum, member) => sum + numeric(values[member]), 0);
2278
2703
  return window.name === "SUM" ? total : total / present.length;
2279
2704
  }
@@ -2332,17 +2757,39 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
2332
2757
  }
2333
2758
  }
2334
2759
  const values = [];
2335
- const prefixNonNull = new Float64Array(size + 1);
2336
- const prefixSum = new Float64Array(size + 1);
2337
2760
  const sums = window.name === "SUM" || window.name === "AVG";
2338
2761
  for (let position = 0; position < size; position += 1) {
2339
2762
  const value = window.argumentAlias === undefined
2340
2763
  ? undefined
2341
2764
  : (rows[indexes[start + position] ?? -1]?.[window.argumentAlias] ?? null);
2342
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];
2343
2778
  const nonNull = window.argumentAlias !== undefined && value !== null && value !== undefined;
2344
2779
  prefixNonNull[position + 1] = (prefixNonNull[position] ?? 0) + (nonNull ? 1 : 0);
2345
- 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
+ }
2346
2793
  }
2347
2794
  // GROUPS frames count peer groups rather than rows, so each position needs its group's
2348
2795
  // ordinal and the group boundaries to translate an offset back into row positions.
@@ -2449,8 +2896,17 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
2449
2896
  }
2450
2897
  else if (sums) {
2451
2898
  const nonNull = (prefixNonNull[high] ?? 0) - (prefixNonNull[low] ?? 0);
2452
- const total = (prefixSum[high] ?? 0) - (prefixSum[low] ?? 0);
2453
- 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
+ }
2454
2910
  }
2455
2911
  else {
2456
2912
  let best;
@@ -2671,28 +3127,28 @@ function resolveDerivedRowTables(plan, tables) {
2671
3127
  for (const source of sources) {
2672
3128
  if (source.recursive !== undefined) {
2673
3129
  const { reference, base, step, all } = source.recursive;
2674
- const state = createRecursiveCteState(executeRowQuery(base, tables), all);
3130
+ const state = createRecursiveCteState(executeRowQueryInternal(base, tables), all);
2675
3131
  while (state.frontier.length > 0) {
2676
3132
  const stepTables = new Map(tables);
2677
3133
  stepTables.set(reference, state.frontier);
2678
- state.absorb(executeRowQuery(step, stepTables));
3134
+ state.absorb(executeRowQueryInternal(step, stepTables));
2679
3135
  }
2680
3136
  resolved.set(source.table, state.rows);
2681
3137
  continue;
2682
3138
  }
2683
3139
  if (source.union !== undefined) {
2684
- const results = source.union.blocks.map((block) => executeRowQuery(block, tables));
3140
+ const results = source.union.blocks.map((block) => executeRowQueryInternal(block, tables));
2685
3141
  resolved.set(source.table, combineUnionResults(results, source.union.ops).rows);
2686
3142
  continue;
2687
3143
  }
2688
3144
  if (source.windowed !== undefined) {
2689
- const inner = executeRowQuery(source.windowed.block, tables);
3145
+ const inner = executeRowQueryInternal(source.windowed.block, tables);
2690
3146
  resolved.set(source.table, applyWindowFunctions(inner, source.windowed.windows).rows);
2691
3147
  continue;
2692
3148
  }
2693
3149
  if (source.derived === undefined)
2694
3150
  continue;
2695
- resolved.set(source.table, executeRowQuery(source.derived, tables).rows);
3151
+ resolved.set(source.table, executeRowQueryInternal(source.derived, tables).rows);
2696
3152
  }
2697
3153
  return resolved;
2698
3154
  }
@@ -2707,11 +3163,14 @@ function normalizeColumnarTables(plan, tables) {
2707
3163
  .filter(([name]) => requiredTables.has(name))
2708
3164
  .map(([name, table]) => [
2709
3165
  name,
2710
- columnarTableFromRows(name, table, requestedColumns.get(name) ?? []),
3166
+ columnarTableFromRows(name, table, requestedColumns.get(name) ?? [], false),
2711
3167
  ]));
2712
3168
  }
2713
3169
  /** Correctness reference retained while the vector executor matures. */
2714
3170
  export function executeRowQuery(plan, tables) {
3171
+ return externalizeQueryResult(executeRowQueryInternal(plan, cloneRowTables(tables, true)));
3172
+ }
3173
+ function executeRowQueryInternal(plan, tables) {
2715
3174
  assertTailParametersBound(plan);
2716
3175
  validateGrouping(plan);
2717
3176
  plan = resolveStatementDatetimes(plan);
@@ -2722,8 +3181,9 @@ export function executeRowQuery(plan, tables) {
2722
3181
  plan = ties.plan;
2723
3182
  plan = expandFtsColumns(plan, rowTableSearchableColumns(tables));
2724
3183
  const resolution = subqueryResolutionSteps(plan);
2725
- for (const step of resolution.steps)
2726
- step.substitute(executeRowQuery(step.block, tables));
3184
+ for (const step of resolution.steps) {
3185
+ step.substitute(executeRowQueryInternal(step.block, tables));
3186
+ }
2727
3187
  // Stats annotation writes into the plan; when resolution had nothing to clone, the plan is
2728
3188
  // still the caller's (possibly cached) object, and frozen statistics would survive into later
2729
3189
  // executions against different rows.
@@ -2775,12 +3235,13 @@ export function executeRowQuery(plan, tables) {
2775
3235
  : [];
2776
3236
  const sortColumns = plan.orderBy.map(({ expression, direction, nulls }) => ({
2777
3237
  outputName: orderOutputName(expression, plan.select, orderSources),
3238
+ direction,
2778
3239
  multiplier: direction === "desc" ? -1 : 1,
2779
3240
  nulls,
2780
3241
  }));
2781
3242
  rows.sort((left, right) => {
2782
- for (const { outputName, multiplier, nulls } of sortColumns) {
2783
- 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);
2784
3245
  if (placed !== undefined && placed !== 0)
2785
3246
  return placed;
2786
3247
  const comparison = compareValues(left[outputName], right[outputName]);
@@ -2866,11 +3327,15 @@ function executeJoin(contexts, join, rows) {
2866
3327
  const candidate = { ...context, [join.alias]: row };
2867
3328
  if (evaluateBooleanExpression(condition, (nested) => evaluate(nested, candidate)) === true) {
2868
3329
  matched = true;
2869
- 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;
2870
3334
  }
2871
3335
  }
2872
- if (!matched && join.kind === "left")
2873
- 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
+ }
2874
3339
  }
2875
3340
  return joined;
2876
3341
  }
@@ -2895,11 +3360,16 @@ function executeJoin(contexts, join, rows) {
2895
3360
  for (const context of contexts) {
2896
3361
  const leftKey = comparable(evaluate(leftExpression, context));
2897
3362
  const matches = isSqlJoinKey(leftKey) ? (index.get(leftKey) ?? []) : [];
2898
- if (matches.length === 0 && join.kind === "left")
2899
- joined.push({ ...context, [join.alias]: undefined });
2900
- 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") {
2901
3370
  for (const row of matches)
2902
3371
  joined.push({ ...context, [join.alias]: row });
3372
+ }
2903
3373
  }
2904
3374
  return joined;
2905
3375
  }
@@ -2919,7 +3389,9 @@ function project(select, context, group) {
2919
3389
  function evaluate(expression, context, group) {
2920
3390
  switch (expression.kind) {
2921
3391
  case "literal":
2922
- return expression.value;
3392
+ return typeof expression.value === "string" && expression.internalSqlValue !== true
3393
+ ? protectedSqlTextValue(expression.value)
3394
+ : expression.value;
2923
3395
  case "parameter":
2924
3396
  throw new TypeError(`Placeholder $${String(expression.index + 1)} is unbound; pass parameters when executing`);
2925
3397
  case "wildcard":
@@ -2971,8 +3443,11 @@ function evaluate(expression, context, group) {
2971
3443
  if (typeof left !== "string" || typeof right !== "string") {
2972
3444
  throw new TypeError("|| requires string operands");
2973
3445
  }
2974
- return left + right;
3446
+ return protectedSqlTextValue(String(externalSqlDomainValue(left)) + String(externalSqlDomainValue(right)));
2975
3447
  }
3448
+ const exact = exactNumericBinary(expression.operator, left, right);
3449
+ if (exact !== undefined)
3450
+ return exact;
2976
3451
  const a = numeric(left);
2977
3452
  const b = numeric(right);
2978
3453
  if (expression.operator === "+")
@@ -2991,11 +3466,69 @@ function evaluate(expression, context, group) {
2991
3466
  if (group === undefined)
2992
3467
  throw new TypeError(`${expression.name} requires grouped execution`);
2993
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
+ }
2994
3526
  let values = argument.kind === "wildcard"
2995
3527
  ? group.map(() => 1)
2996
- : group
2997
- .map((row) => evaluate(argument, row))
2998
- .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
+ }
2999
3532
  if (expression.distinct === true) {
3000
3533
  const seen = new Set();
3001
3534
  values = values.filter((value) => {
@@ -3009,13 +3542,19 @@ function evaluate(expression, context, group) {
3009
3542
  if (expression.name === "COUNT")
3010
3543
  return values.length;
3011
3544
  if (expression.name === "SUM")
3545
+ return values.length === 0 ? null : sumNumericValues(values);
3546
+ if (expression.name === "AVG")
3012
3547
  return values.length === 0
3013
3548
  ? null
3014
- : values.reduce((sum, value) => sum + numeric(value), 0);
3015
- 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") {
3016
3554
  return values.length === 0
3017
3555
  ? null
3018
- : values.reduce((sum, value) => sum + numeric(value), 0) / values.length;
3556
+ : JSON.stringify(values.map((value) => jsonValueOf(value ?? null)));
3557
+ }
3019
3558
  if (expression.name === "MIN")
3020
3559
  return values.reduce((best, value) => (best === undefined || compareValues(value, best) < 0 ? value : best), undefined);
3021
3560
  return values.reduce((best, value) => (best === undefined || compareValues(value, best) > 0 ? value : best), undefined);
@@ -3042,7 +3581,9 @@ function evaluatePredicate(predicate, context) {
3042
3581
  if (predicate.operator === "LIKE" ||
3043
3582
  predicate.operator === "NOT LIKE" ||
3044
3583
  predicate.operator === "ILIKE" ||
3045
- predicate.operator === "NOT ILIKE") {
3584
+ predicate.operator === "NOT ILIKE" ||
3585
+ predicate.operator === "SIMILAR TO" ||
3586
+ predicate.operator === "NOT SIMILAR TO") {
3046
3587
  return (evaluateBooleanExpression({
3047
3588
  kind: "condition",
3048
3589
  operator: predicate.operator,
@@ -3261,7 +3802,7 @@ export function distinctFromComparison(left, right) {
3261
3802
  const rightNull = right === null || right === undefined;
3262
3803
  if (leftNull || rightNull)
3263
3804
  return leftNull !== rightNull;
3264
- return comparable(left) !== comparable(right);
3805
+ return compareValues(comparable(left), comparable(right)) !== 0;
3265
3806
  }
3266
3807
  /**
3267
3808
  * Evaluates a boolean expression tree with SQL three-valued logic: comparisons over NULL are
@@ -3334,17 +3875,24 @@ export function evaluateBooleanExpression(expression, evaluateValue) {
3334
3875
  if (operator === "LIKE" ||
3335
3876
  operator === "NOT LIKE" ||
3336
3877
  operator === "ILIKE" ||
3337
- operator === "NOT ILIKE") {
3338
- const value = evaluateValue(expression.left);
3339
- 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));
3340
3883
  if (value === null || value === undefined || pattern === null || pattern === undefined) {
3341
3884
  return null;
3342
3885
  }
3343
3886
  if (typeof value !== "string" || typeof pattern !== "string") {
3344
3887
  throw new TypeError("LIKE requires string operands");
3345
3888
  }
3346
- const matched = likeMatches(pattern, value, operator === "ILIKE" || operator === "NOT ILIKE", expression.escape);
3347
- 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;
3348
3896
  }
3349
3897
  if (operator === "IS DISTINCT FROM" || operator === "IS NOT DISTINCT FROM") {
3350
3898
  const distinct = distinctFromComparison(evaluateValue(expression.left), evaluateValue(expression.right));
@@ -3357,9 +3905,9 @@ export function evaluateBooleanExpression(expression, evaluateValue) {
3357
3905
  const a = comparable(left);
3358
3906
  const b = comparable(right);
3359
3907
  if (operator === "=")
3360
- return a === b;
3908
+ return compareValues(a, b) === 0;
3361
3909
  if (operator === "!=" || operator === "<>")
3362
- return a !== b;
3910
+ return compareValues(a, b) !== 0;
3363
3911
  const comparison = compareValues(a, b);
3364
3912
  if (operator === ">")
3365
3913
  return comparison > 0;
@@ -3391,7 +3939,11 @@ function comparisonHolds(operator, leftValue, rightValue) {
3391
3939
  if (operator === "LIKE" ||
3392
3940
  operator === "NOT LIKE" ||
3393
3941
  operator === "ILIKE" ||
3394
- operator === "NOT ILIKE") {
3942
+ operator === "NOT ILIKE" ||
3943
+ operator === "SIMILAR TO" ||
3944
+ operator === "NOT SIMILAR TO") {
3945
+ leftValue = externalSqlDomainValue(leftValue);
3946
+ rightValue = externalSqlDomainValue(rightValue);
3395
3947
  if (leftValue === null ||
3396
3948
  leftValue === undefined ||
3397
3949
  rightValue === null ||
@@ -3401,8 +3953,13 @@ function comparisonHolds(operator, leftValue, rightValue) {
3401
3953
  if (typeof leftValue !== "string" || typeof rightValue !== "string") {
3402
3954
  throw new TypeError("LIKE requires string operands");
3403
3955
  }
3404
- const matched = likeMatches(rightValue, leftValue, operator === "ILIKE" || operator === "NOT ILIKE");
3405
- 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;
3406
3963
  }
3407
3964
  if (leftValue === null ||
3408
3965
  leftValue === undefined ||
@@ -3412,9 +3969,9 @@ function comparisonHolds(operator, leftValue, rightValue) {
3412
3969
  const left = comparable(leftValue);
3413
3970
  const right = comparable(rightValue);
3414
3971
  if (operator === "=")
3415
- return left === right;
3972
+ return compareValues(left, right) === 0;
3416
3973
  if (operator === "!=" || operator === "<>")
3417
- return left !== right;
3974
+ return compareValues(left, right) !== 0;
3418
3975
  const comparison = compareValues(left, right);
3419
3976
  if (operator === ">")
3420
3977
  return comparison > 0;
@@ -3493,7 +4050,7 @@ function validateGrouping(plan) {
3493
4050
  for (const item of plan.select) {
3494
4051
  if (hasAggregate(item.expression))
3495
4052
  continue;
3496
- // 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
3497
4054
  // node is never constant — MATCH(*) carries no column children before expansion, but it
3498
4055
  // reads every searchable column of its row.
3499
4056
  if (expressionColumns(item.expression).length === 0 &&
@@ -3533,27 +4090,37 @@ function comparable(value) {
3533
4090
  return value instanceof Date ? value.getTime() : value;
3534
4091
  }
3535
4092
  /**
3536
- * Resolves an explicit NULLS FIRST/LAST placement for one order term. Returns undefined when
3537
- * no explicit placement applies (either none was requested or neither side is NULL); otherwise
3538
- * the signed placement, which is absolute direction negation must not apply to it. Two NULLs
3539
- * 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.
3540
4097
  */
3541
- export function explicitNullOrder(left, right, nulls) {
3542
- if (nulls === undefined)
3543
- return undefined;
4098
+ export function nullOrder(left, right, nulls, direction) {
3544
4099
  const leftNull = left === null || left === undefined;
3545
4100
  const rightNull = right === null || right === undefined;
3546
4101
  if (!leftNull && !rightNull)
3547
4102
  return undefined;
3548
4103
  if (leftNull && rightNull)
3549
4104
  return 0;
3550
- 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);
3551
4107
  }
3552
4108
  function numeric(value) {
3553
4109
  if (typeof value !== "number")
3554
4110
  throw new TypeError("Arithmetic and numeric aggregates require numbers");
3555
4111
  return value;
3556
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
+ }
3557
4124
  function asQueryValue(value) {
3558
4125
  if (value === null ||
3559
4126
  typeof value === "boolean" ||
@@ -3565,6 +4132,83 @@ function asQueryValue(value) {
3565
4132
  return null;
3566
4133
  throw new TypeError("Query produced an unsupported value");
3567
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
+ }
3568
4212
  class Parser {
3569
4213
  tokens;
3570
4214
  /** The statement text the tokens came from, for the clauses stored verbatim (CHECK bodies). */
@@ -3576,6 +4220,10 @@ class Parser {
3576
4220
  #highestNumberedParameter = 0;
3577
4221
  /** Set when the statement names CURRENT_DATE, CURRENT_TIMESTAMP, or LOCALTIME. */
3578
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;
3579
4227
  /** Total parameter slots the statement expects; 0 when it has no placeholders. */
3580
4228
  get parameterCount() {
3581
4229
  return Math.max(this.#positionalParameters, this.#highestNumberedParameter);
@@ -3687,7 +4335,7 @@ class Parser {
3687
4335
  this.#ctes.set(name, block);
3688
4336
  }
3689
4337
  }
3690
- // INTERSECT binds tighter than UNION and EXCEPT, per the SQL standard.
4338
+ // INTERSECT binds tighter than UNION and EXCEPT, matching PostgreSQL.
3691
4339
  const firstTerm = this.#setTerm(sql);
3692
4340
  let plan = firstTerm.block;
3693
4341
  if (this.#isKeyword("UNION") || this.#isKeyword("EXCEPT")) {
@@ -3745,7 +4393,7 @@ class Parser {
3745
4393
  throw new TypeError("ORDER BY or LIMIT in a UNION member requires parentheses");
3746
4394
  }
3747
4395
  }
3748
- // 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
3749
4397
  // unparenthesized last member the clause was greedily parsed into that member and lifts
3750
4398
  // out; after a parenthesized member it is still unparsed.
3751
4399
  const last = members[members.length - 1];
@@ -3774,10 +4422,53 @@ class Parser {
3774
4422
  }
3775
4423
  return compoundSelectBlock(sql, members.map((member) => member.block), ops, tail, this.nextDerivedSequence);
3776
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
+ }
3777
4468
  /**
3778
4469
  * CREATE TABLE name (col TYPE [PRIMARY KEY | UNIQUE] [NOT NULL | NULL], ...). Standard type
3779
- * names map onto the engine's four logical types; widths in parentheses parse and are
3780
- * 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.
3781
4472
  */
3782
4473
  parseCreateTable() {
3783
4474
  this.#keyword("CREATE");
@@ -3806,9 +4497,28 @@ class Parser {
3806
4497
  const columns = [];
3807
4498
  const checks = [];
3808
4499
  const foreignKeys = [];
3809
- 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
+ };
3810
4516
  for (;;) {
3811
- 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")) {
3812
4522
  // A table-level constraint, named or not.
3813
4523
  let constraintName;
3814
4524
  if (this.#isKeyword("CONSTRAINT")) {
@@ -3818,59 +4528,50 @@ class Parser {
3818
4528
  if (this.#isKeyword("FOREIGN")) {
3819
4529
  this.#keyword("FOREIGN");
3820
4530
  this.#keyword("KEY");
3821
- this.#expectPunctuation("(");
3822
- const column = this.#identifier();
3823
- if (this.#peek().text === ",") {
3824
- throw new TypeError("FOREIGN KEY supports one column, the parent's unique key");
3825
- }
3826
- this.#expectPunctuation(")");
3827
- foreignKeys.push(this.#references(constraintName ?? `${table}_${column}_fkey`, column));
4531
+ const names = constraintColumns();
4532
+ foreignKeys.push(this.#references(constraintName ?? `${table}_${names.join("_")}_fkey`, names));
3828
4533
  if (!this.#punctuation(","))
3829
4534
  break;
3830
4535
  continue;
3831
4536
  }
3832
- if (!this.#isKeyword("CHECK")) {
3833
- throw new TypeError("Table constraints are CHECK and FOREIGN KEY");
3834
- }
3835
- checks.push(this.#checkConstraint(constraintName ?? `${table}_check_${String(checks.length + 1)}`));
3836
- if (!this.#punctuation(","))
3837
- break;
3838
- continue;
3839
- }
3840
- if (this.#isKeyword("PRIMARY") || this.#isKeyword("UNIQUE")) {
3841
- // E141-08: the table-level spelling of the same single-column key.
3842
4537
  if (this.#isKeyword("PRIMARY")) {
3843
4538
  this.#keyword("PRIMARY");
3844
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;
3845
4546
  }
3846
- else
4547
+ if (this.#isKeyword("UNIQUE")) {
3847
4548
  this.#keyword("UNIQUE");
3848
- this.#expectPunctuation("(");
3849
- const keyColumn = this.#identifier();
3850
- if (this.#peek().text === ",") {
3851
- 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;
3852
4557
  }
3853
- this.#expectPunctuation(")");
3854
- if (uniqueKey !== undefined && uniqueKey !== keyColumn) {
3855
- 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");
3856
4560
  }
3857
- uniqueKey = keyColumn;
4561
+ checks.push(this.#checkConstraint(constraintName ?? `${table}_check_${String(checks.length + 1)}`));
3858
4562
  if (!this.#punctuation(","))
3859
4563
  break;
3860
4564
  continue;
3861
4565
  }
3862
4566
  const name = this.#identifier();
3863
- const type = this.#columnType();
4567
+ const columnType = this.#columnType();
3864
4568
  let nullable = true;
3865
- let explicitlyNullable = false;
3866
4569
  let defaultValue;
3867
4570
  for (;;) {
3868
4571
  if (this.#isKeyword("DEFAULT")) {
3869
- // E141-07. The catalog fills defaults at insert time, so they are constants: a
3870
- // literal, or the CURRENT_TIMESTAMP family, which it stores as "now".
4572
+ // PostgreSQL-compatible variable-free scalar expression, retained in the catalog.
3871
4573
  this.#keyword("DEFAULT");
3872
- const expression = this.#expression();
3873
- defaultValue = columnDefaultFor(expression);
4574
+ defaultValue = this.#columnDefault();
3874
4575
  continue;
3875
4576
  }
3876
4577
  if (this.#isKeyword("CHECK")) {
@@ -3878,22 +4579,22 @@ class Parser {
3878
4579
  continue;
3879
4580
  }
3880
4581
  if (this.#isKeyword("REFERENCES")) {
3881
- foreignKeys.push(this.#references(`${table}_${name}_fkey`, name));
4582
+ foreignKeys.push(this.#references(`${table}_${name}_fkey`, [name]));
3882
4583
  continue;
3883
4584
  }
3884
4585
  if (this.#isKeyword("PRIMARY") || this.#isKeyword("UNIQUE")) {
3885
4586
  if (this.#isKeyword("PRIMARY")) {
3886
4587
  this.#keyword("PRIMARY");
3887
4588
  this.#keyword("KEY");
4589
+ if (primaryKey !== undefined)
4590
+ throw new TypeError("CREATE TABLE has two PRIMARY KEYs");
4591
+ primaryKey = [name];
4592
+ nullable = false;
3888
4593
  }
3889
4594
  else {
3890
4595
  this.#keyword("UNIQUE");
4596
+ uniqueConstraints.push({ name: `${table}_${name}_key`, columns: [name] });
3891
4597
  }
3892
- if (uniqueKey !== undefined) {
3893
- throw new TypeError("CREATE TABLE supports one unique key column");
3894
- }
3895
- uniqueKey = name;
3896
- nullable = false;
3897
4598
  continue;
3898
4599
  }
3899
4600
  if (this.#isKeyword("NOT")) {
@@ -3909,19 +4610,13 @@ class Parser {
3909
4610
  if (this.#isKeyword("NULL")) {
3910
4611
  this.#keyword("NULL");
3911
4612
  nullable = true;
3912
- explicitlyNullable = true;
3913
4613
  continue;
3914
4614
  }
3915
4615
  break;
3916
4616
  }
3917
- // A default answers what an absent value means, so the column cannot also be nullable
3918
- // unless the author says so — and then the engine rejects the pair, since NULL and the
3919
- // default would both claim the same slot.
3920
- if (defaultValue !== undefined && !explicitlyNullable)
3921
- nullable = false;
3922
4617
  columns.push({
3923
4618
  name,
3924
- type,
4619
+ ...columnType,
3925
4620
  ...(nullable ? { nullable: true } : {}),
3926
4621
  ...(defaultValue === undefined ? {} : { defaultValue }),
3927
4622
  });
@@ -3933,10 +4628,14 @@ class Parser {
3933
4628
  if (new Set(columns.map((column) => column.name)).size !== columns.length) {
3934
4629
  throw new TypeError("CREATE TABLE column names must be unique");
3935
4630
  }
3936
- if (uniqueKey !== undefined && !columns.some((column) => column.name === uniqueKey)) {
3937
- throw new TypeError(`CREATE TABLE key column is not declared: ${uniqueKey}`);
3938
- }
3939
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
+ }
3940
4639
  for (const check of checks) {
3941
4640
  for (const reference of expressionColumnNames(compileCheckExpression(check.sql, check.name))) {
3942
4641
  const column = reference.split(".").at(-1) ?? reference;
@@ -3945,11 +4644,24 @@ class Parser {
3945
4644
  }
3946
4645
  }
3947
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 ?? []);
3948
4654
  return {
3949
4655
  kind: "create-table",
3950
4656
  table,
3951
- 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),
3952
4660
  ...(uniqueKey === undefined ? {} : { uniqueKey }),
4661
+ ...(primaryKey === undefined || primaryKey.length < 2
4662
+ ? {}
4663
+ : { compositePrimaryKey: primaryKey }),
4664
+ ...(uniqueConstraints.length === 0 ? {} : { uniqueConstraints }),
3953
4665
  ...(checks.length === 0 ? {} : { checks }),
3954
4666
  ...(foreignKeys.length === 0 ? {} : { foreignKeys }),
3955
4667
  ...(ifNotExists ? { ifNotExists: true } : {}),
@@ -3960,14 +4672,22 @@ class Parser {
3960
4672
  * key cannot change in this engine, so ON UPDATE has nothing to act on and only the
3961
4673
  * no-op actions parse; ON DELETE takes the three the engine can carry out.
3962
4674
  */
3963
- #references(name, column) {
4675
+ #references(name, columns) {
3964
4676
  this.#keyword("REFERENCES");
3965
4677
  const parentTable = this.#identifier();
3966
- let parentColumn;
4678
+ let parentColumns;
3967
4679
  if (this.#punctuation("(")) {
3968
- parentColumn = this.#identifier();
4680
+ parentColumns = [];
4681
+ for (;;) {
4682
+ parentColumns.push(this.#identifier());
4683
+ if (!this.#punctuation(","))
4684
+ break;
4685
+ }
3969
4686
  this.#expectPunctuation(")");
3970
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
+ }
3971
4691
  let onDelete = "restrict";
3972
4692
  while (this.#isKeyword("ON")) {
3973
4693
  this.#keyword("ON");
@@ -3984,9 +4704,10 @@ class Parser {
3984
4704
  }
3985
4705
  return {
3986
4706
  name,
3987
- column,
4707
+ column: columns[0] ?? "",
4708
+ ...(columns.length === 1 ? {} : { columns }),
3988
4709
  parentTable,
3989
- ...(parentColumn === undefined ? {} : { parentColumn }),
4710
+ ...(parentColumns === undefined ? {} : { parentColumn: parentColumns[0], parentColumns }),
3990
4711
  onDelete,
3991
4712
  };
3992
4713
  }
@@ -4036,6 +4757,15 @@ class Parser {
4036
4757
  this.#take("eof");
4037
4758
  return expression;
4038
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
+ }
4039
4769
  /** DROP VIEW [IF EXISTS] name (F031-16). */
4040
4770
  parseDropView() {
4041
4771
  this.#keyword("DROP");
@@ -4052,6 +4782,20 @@ class Parser {
4052
4782
  this.#take("eof");
4053
4783
  return { kind: "drop-view", view, ...(ifExists ? { ifExists: true } : {}) };
4054
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
+ }
4055
4799
  /** DROP TABLE [IF EXISTS] name (F031-13). */
4056
4800
  parseDropTable() {
4057
4801
  this.#keyword("DROP");
@@ -4073,22 +4817,46 @@ class Parser {
4073
4817
  this.#take("eof");
4074
4818
  return { kind: "drop-table", table, ...(ifExists ? { ifExists: true } : {}) };
4075
4819
  }
4076
- /** ALTER TABLE name ADD [COLUMN] col TYPE [NOT NULL] [DEFAULT v] (F031-04). */
4820
+ /** ALTER TABLE name ADD/DROP [COLUMN] (F031). */
4077
4821
  parseAlterTable() {
4078
4822
  this.#keyword("ALTER");
4079
4823
  this.#keyword("TABLE");
4080
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
+ }
4081
4849
  this.#keyword("ADD");
4082
4850
  if (this.#isKeyword("COLUMN"))
4083
4851
  this.#keyword("COLUMN");
4084
4852
  const name = this.#identifier();
4085
- const type = this.#columnType();
4853
+ const columnType = this.#columnType();
4086
4854
  let nullable = true;
4087
4855
  let defaultValue;
4088
4856
  for (;;) {
4089
4857
  if (this.#isKeyword("DEFAULT")) {
4090
4858
  this.#keyword("DEFAULT");
4091
- defaultValue = columnDefaultFor(this.#expression());
4859
+ defaultValue = this.#columnDefault();
4092
4860
  continue;
4093
4861
  }
4094
4862
  if (this.#isKeyword("NOT")) {
@@ -4110,7 +4878,7 @@ class Parser {
4110
4878
  table,
4111
4879
  column: {
4112
4880
  name,
4113
- type,
4881
+ ...columnType,
4114
4882
  ...(nullable ? { nullable: true } : {}),
4115
4883
  ...(defaultValue === undefined ? {} : { defaultValue }),
4116
4884
  },
@@ -4119,6 +4887,22 @@ class Parser {
4119
4887
  /** A CAST target: the SqlColumnType, or "number-integer" for the truncating integer names. */
4120
4888
  #castTarget() {
4121
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
+ }
4122
4906
  if (word === "DOUBLE") {
4123
4907
  this.#keyword("PRECISION");
4124
4908
  return "number";
@@ -4136,22 +4920,50 @@ class Parser {
4136
4920
  return integer ? "number-integer" : mapped;
4137
4921
  }
4138
4922
  #columnType() {
4139
- 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
+ }
4140
4945
  if (word === "DOUBLE") {
4141
4946
  this.#keyword("PRECISION");
4142
- return "number";
4947
+ return { type: "number" };
4143
4948
  }
4144
4949
  const mapped = createTableTypeNames.get(word);
4145
- if (mapped === undefined)
4146
- throw new TypeError(`Unsupported column type: ${word}`);
4147
- // 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.
4148
4954
  if (this.#punctuation("(")) {
4149
4955
  this.#take("number");
4150
- if (this.#punctuation(","))
4151
- this.#take("number");
4956
+ if (this.#punctuation(",")) {
4957
+ throw new TypeError(`${word} takes one width`);
4958
+ }
4152
4959
  this.#expectPunctuation(")");
4153
4960
  }
4154
- 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 } : {}) };
4155
4967
  }
4156
4968
  parseMutation(keyword) {
4157
4969
  const statement = keyword === "INSERT"
@@ -4166,23 +4978,37 @@ class Parser {
4166
4978
  this.#keyword("INSERT");
4167
4979
  this.#keyword("INTO");
4168
4980
  const table = this.#identifier();
4169
- this.#expectPunctuation("(");
4170
4981
  const columns = [];
4171
- for (;;) {
4172
- columns.push(this.#identifier());
4173
- if (!this.#punctuation(","))
4174
- break;
4982
+ if (this.#punctuation("(")) {
4983
+ for (;;) {
4984
+ columns.push(this.#identifier());
4985
+ if (!this.#punctuation(","))
4986
+ break;
4987
+ }
4988
+ this.#expectPunctuation(")");
4175
4989
  }
4176
- this.#expectPunctuation(")");
4177
4990
  if (new Set(columns).size !== columns.length) {
4178
4991
  throw new TypeError("INSERT columns must be unique");
4179
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
+ }
4180
5006
  if (this.#isKeyword("SELECT")) {
4181
5007
  const query = optimizePlan(this.#selectBlock("(insert select)"));
4182
5008
  if (query.select.some((item) => item.expression.kind === "wildcard")) {
4183
5009
  throw new TypeError("INSERT ... SELECT requires an explicit select list");
4184
5010
  }
4185
- if (query.select.length !== columns.length) {
5011
+ if (columns.length > 0 && query.select.length !== columns.length) {
4186
5012
  throw new TypeError("INSERT ... SELECT must produce exactly the insert column count");
4187
5013
  }
4188
5014
  return {
@@ -4205,7 +5031,7 @@ class Parser {
4205
5031
  break;
4206
5032
  }
4207
5033
  this.#expectPunctuation(")");
4208
- if (values.length !== columns.length) {
5034
+ if (columns.length > 0 && values.length !== columns.length) {
4209
5035
  throw new TypeError("Each INSERT row must match the column list length");
4210
5036
  }
4211
5037
  rows.push(values);
@@ -4217,55 +5043,72 @@ class Parser {
4217
5043
  table,
4218
5044
  columns,
4219
5045
  rows,
4220
- ...this.#onConflictClause(columns),
5046
+ ...this.#onConflictClause(table),
4221
5047
  ...this.#returningClause(),
4222
5048
  };
4223
5049
  }
4224
- #onConflictClause(columns) {
5050
+ #onConflictClause(table) {
4225
5051
  if (!this.#isKeyword("ON"))
4226
5052
  return {};
4227
5053
  this.#keyword("ON");
4228
5054
  this.#keyword("CONFLICT");
4229
5055
  this.#expectPunctuation("(");
4230
- const column = this.#identifier();
5056
+ const columns = [];
5057
+ for (;;) {
5058
+ columns.push(this.#identifier());
5059
+ if (!this.#punctuation(","))
5060
+ break;
5061
+ }
4231
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 };
4232
5067
  this.#keyword("DO");
4233
5068
  if (this.#isKeyword("NOTHING")) {
4234
5069
  this.#keyword("NOTHING");
4235
- 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" } };
4236
5077
  }
4237
5078
  this.#keyword("UPDATE");
4238
5079
  this.#keyword("SET");
4239
5080
  const assigned = new Set();
5081
+ const assignments = [];
4240
5082
  for (;;) {
4241
5083
  const target = this.#identifier();
4242
5084
  this.#operator("=");
4243
- const value = this.#expression();
4244
- const parts = value.kind === "column" ? value.reference.split(".") : [];
4245
- if (parts.length !== 2 || parts[0]?.toUpperCase() !== "EXCLUDED" || parts[1] !== target) {
4246
- throw new TypeError("ON CONFLICT DO UPDATE supports assignments of the form column = EXCLUDED.column");
4247
- }
4248
5085
  if (assigned.has(target)) {
4249
5086
  throw new TypeError(`ON CONFLICT DO UPDATE sets a column twice: ${target}`);
4250
5087
  }
4251
- if (target === column) {
5088
+ if (columns.includes(target)) {
4252
5089
  throw new TypeError("ON CONFLICT DO UPDATE cannot reassign the conflict key");
4253
5090
  }
4254
5091
  assigned.add(target);
5092
+ assignments.push({
5093
+ column: target,
5094
+ expression: rewriteUpsertExpression(this.#expression(), table),
5095
+ });
4255
5096
  if (!this.#punctuation(","))
4256
5097
  break;
4257
5098
  }
4258
- for (const name of assigned) {
4259
- if (!columns.includes(name)) {
4260
- throw new TypeError(`ON CONFLICT DO UPDATE sets a column that is not inserted: ${name}`);
4261
- }
5099
+ let where;
5100
+ if (this.#isKeyword("WHERE")) {
5101
+ this.#keyword("WHERE");
5102
+ where = rewriteUpsertExpression(this.#expression(), table);
4262
5103
  }
4263
- // Every inserted column assigned is a whole-row upsert; a subset merges into existing rows.
4264
- const complete = columns.every((name) => name === column || assigned.has(name));
4265
5104
  return {
4266
- onConflict: complete
4267
- ? { column, action: "replace" }
4268
- : { column, action: "update", columns: [...assigned] },
5105
+ onConflict: {
5106
+ column,
5107
+ ...target,
5108
+ action: "update",
5109
+ assignments,
5110
+ ...(where === undefined ? {} : { where }),
5111
+ },
4269
5112
  };
4270
5113
  }
4271
5114
  /** RETURNING * or RETURNING col, ... — the engine's runStatement implements the semantics. */
@@ -4453,6 +5296,10 @@ class Parser {
4453
5296
  return predicates;
4454
5297
  }
4455
5298
  #insertValue(label) {
5299
+ if (this.#isKeyword("DEFAULT")) {
5300
+ this.#keyword("DEFAULT");
5301
+ return { default: true };
5302
+ }
4456
5303
  const expression = this.#expression();
4457
5304
  // A bare placeholder stays a slot; the batch write binds it later. Placeholders nested in
4458
5305
  // arithmetic would need expression retention through the batch path, so they stay rejected.
@@ -4515,7 +5362,7 @@ class Parser {
4515
5362
  this.#keyword("LIMIT");
4516
5363
  if (this.#peek().kind === "parameter") {
4517
5364
  const parameter = this.#parameterExpression();
4518
- return parameter.kind === "parameter" ? { limitParameter: parameter.index } : {};
5365
+ return { limitParameter: parameter.index };
4519
5366
  }
4520
5367
  return { limit: validateLimit(Number(this.#take("number").text)) };
4521
5368
  }
@@ -4533,8 +5380,7 @@ class Parser {
4533
5380
  let result = { limit: 1 };
4534
5381
  if (this.#peek().kind === "parameter") {
4535
5382
  const parameter = this.#parameterExpression();
4536
- if (parameter.kind === "parameter")
4537
- result = { limitParameter: parameter.index };
5383
+ result = { limitParameter: parameter.index };
4538
5384
  }
4539
5385
  else if (this.#peek().kind === "number") {
4540
5386
  result = { limit: validateLimit(Number(this.#take("number").text)) };
@@ -4559,7 +5405,7 @@ class Parser {
4559
5405
  let result;
4560
5406
  if (this.#peek().kind === "parameter") {
4561
5407
  const parameter = this.#parameterExpression();
4562
- result = parameter.kind === "parameter" ? { offsetParameter: parameter.index } : {};
5408
+ result = { offsetParameter: parameter.index };
4563
5409
  }
4564
5410
  else {
4565
5411
  result = { offset: validateOffset(Number(this.#take("number").text)) };
@@ -4898,14 +5744,18 @@ class Parser {
4898
5744
  }
4899
5745
  #selectList() {
4900
5746
  const items = [];
5747
+ const explicitAliases = [];
4901
5748
  for (;;) {
4902
5749
  const expression = this.#expression();
4903
5750
  let alias = defaultAlias(expression);
5751
+ let explicitAlias = false;
4904
5752
  if (this.#isKeyword("AS")) {
4905
5753
  this.#keyword("AS");
4906
5754
  alias = this.#identifier();
5755
+ explicitAlias = true;
4907
5756
  }
4908
5757
  items.push({ expression, alias });
5758
+ explicitAliases.push(explicitAlias);
4909
5759
  if (!this.#punctuation(","))
4910
5760
  break;
4911
5761
  }
@@ -4914,15 +5764,23 @@ class Parser {
4914
5764
  throw new TypeError("SELECT * cannot be mixed with other expressions");
4915
5765
  }
4916
5766
  const aliases = new Set();
4917
- for (const item of items) {
5767
+ for (const [index, item] of items.entries()) {
4918
5768
  if (item.expression.kind === "list") {
4919
5769
  throw new TypeError("A row constructor is only allowed in a comparison or IN list");
4920
5770
  }
4921
5771
  // A qualified wildcard has no single output name until expansion resolves its columns.
4922
5772
  if (item.expression.kind === "wildcard" && item.expression.table !== undefined)
4923
5773
  continue;
4924
- if (aliases.has(item.alias))
4925
- 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
+ }
4926
5784
  aliases.add(item.alias);
4927
5785
  }
4928
5786
  return items;
@@ -5078,15 +5936,126 @@ class Parser {
5078
5936
  return this.#derivedSource(derived, alias);
5079
5937
  }
5080
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
+ }
5081
5950
  if (this.#peek().text === "(") {
5082
5951
  // Two row-producing sources the executors have no operator for; both parse far enough to
5083
5952
  // say so, rather than failing on the punctuation that follows.
5084
5953
  const upper = table.toUpperCase();
5085
- if (upper === "LATERAL") {
5086
- throw new TypeError("LATERAL sources are not supported");
5087
- }
5088
5954
  if (upper === "JSON_TABLE") {
5089
- 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);
5090
6059
  }
5091
6060
  }
5092
6061
  const candidate = this.#recursiveCandidate;
@@ -5376,14 +6345,13 @@ class Parser {
5376
6345
  // After a value expression, NOT can only introduce NOT BETWEEN / NOT IN / NOT LIKE.
5377
6346
  let negated = false;
5378
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;
5379
6353
  this.#keyword("NOT");
5380
6354
  negated = true;
5381
- if (!this.#isKeyword("BETWEEN") &&
5382
- !this.#isKeyword("IN") &&
5383
- !this.#isKeyword("LIKE") &&
5384
- !this.#isKeyword("ILIKE")) {
5385
- throw new TypeError(`Expected BETWEEN, IN, LIKE, or ILIKE after NOT`);
5386
- }
5387
6355
  }
5388
6356
  if (this.#isKeyword("BETWEEN")) {
5389
6357
  this.#keyword("BETWEEN");
@@ -5460,6 +6428,26 @@ class Parser {
5460
6428
  ...(escape === undefined ? {} : { escape }),
5461
6429
  };
5462
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
+ }
5463
6451
  const token = this.#peek();
5464
6452
  if (token.kind === "operator" && ["=", "!=", "<>", ">", ">=", "<", "<="].includes(token.text)) {
5465
6453
  const operator = this.#comparison();
@@ -5486,6 +6474,15 @@ class Parser {
5486
6474
  #additive(minimumPrecedence = 0) {
5487
6475
  let left = this.#primary();
5488
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
+ }
5489
6486
  const operator = this.#peek().text;
5490
6487
  // || binds loosest, matching PostgreSQL: concatenation applies to whole arithmetic terms.
5491
6488
  const precedence = operator === "*" || operator === "/" || operator === "%"
@@ -5550,7 +6547,11 @@ class Parser {
5550
6547
  return this.#parameterExpression();
5551
6548
  if (token.kind === "number") {
5552
6549
  this.#index += 1;
5553
- 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 };
5554
6555
  }
5555
6556
  if (token.kind === "string") {
5556
6557
  this.#index += 1;
@@ -5646,8 +6647,18 @@ class Parser {
5646
6647
  const columns = this.#ftsColumns();
5647
6648
  this.#keyword("AGAINST");
5648
6649
  const queryToken = this.#peek();
6650
+ if (queryToken.kind === "parameter") {
6651
+ const parameter = this.#parameterExpression();
6652
+ return {
6653
+ kind: "fts",
6654
+ op: upper === "MATCH" ? "match" : "bm25",
6655
+ columns,
6656
+ query: "",
6657
+ queryParameter: parameter.index,
6658
+ };
6659
+ }
5649
6660
  if (queryToken.kind !== "string") {
5650
- throw new TypeError(`${upper} ... AGAINST requires a string literal query`);
6661
+ throw new TypeError(`${upper} ... AGAINST requires a string literal or parameter query`);
5651
6662
  }
5652
6663
  this.#index += 1;
5653
6664
  validateFtsQuery(queryToken.text);
@@ -5785,6 +6796,30 @@ class Parser {
5785
6796
  if ((upper === "TIMESTAMP" || upper === "DATETIME") && this.#peek().kind === "string") {
5786
6797
  return { kind: "literal", value: timestampLiteral(this.#take("string").text) };
5787
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
+ }
5788
6823
  if (this.#punctuation("(")) {
5789
6824
  if (upper === "ROW_NUMBER" ||
5790
6825
  upper === "RANK" ||
@@ -5917,8 +6952,15 @@ class Parser {
5917
6952
  // canonical plan names. ANY_VALUE picks an implementation-dependent row of the group
5918
6953
  // (T626); MIN is one such choice and reuses its accumulator exactly.
5919
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
+ }
5920
6958
  if (!aggregateNames.has(name) && !scalarFunctionNames.has(name))
5921
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;
5922
6964
  let distinct = false;
5923
6965
  if (this.#isKeyword("DISTINCT")) {
5924
6966
  if (!aggregateNames.has(name)) {
@@ -5932,15 +6974,32 @@ class Parser {
5932
6974
  this.#keyword("ALL");
5933
6975
  }
5934
6976
  const args = [];
6977
+ let aggregateOrderBy;
5935
6978
  if (!this.#punctuation(")")) {
5936
- 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
+ }
5937
6989
  this.#expectPunctuation(")");
5938
6990
  }
5939
- 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")) {
5940
6993
  throw new TypeError(`${name}(DISTINCT) requires exactly one scalar argument`);
5941
6994
  }
5942
- 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)
5943
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
+ }
5944
7003
  if (name === "ROUND" && (args.length < 1 || args.length > 2))
5945
7004
  throw new TypeError("ROUND requires one or two arguments");
5946
7005
  if (name === "COALESCE" && args.length < 1)
@@ -5969,6 +7028,9 @@ class Parser {
5969
7028
  if (statementDatetimeNames.has(name) && args.length !== 0) {
5970
7029
  throw new TypeError(`${name} takes no arguments`);
5971
7030
  }
7031
+ if (volatileScalarFunctionNames.has(name) && args.length !== 0) {
7032
+ throw new TypeError(`${name} takes no arguments`);
7033
+ }
5972
7034
  if ((name === "JSON_VALUE" || name === "JSON_QUERY" || name === "JSON_EXISTS") &&
5973
7035
  args.length !== 2) {
5974
7036
  throw new TypeError(`${name} requires a JSON document and a path`);
@@ -6009,6 +7071,9 @@ class Parser {
6009
7071
  }
6010
7072
  }
6011
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
+ }
6012
7077
  // FILTER (WHERE cond) desugars into the aggregate's argument: rows failing the filter
6013
7078
  // contribute NULL, which every aggregate skips — COUNT(*) counts a CASE over 1.
6014
7079
  this.#keyword("FILTER");
@@ -6029,6 +7094,12 @@ class Parser {
6029
7094
  });
6030
7095
  }
6031
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
+ }
6032
7103
  if (distinct)
6033
7104
  throw new TypeError("DISTINCT window aggregates are not supported");
6034
7105
  this.#keyword("OVER");
@@ -6046,7 +7117,13 @@ class Parser {
6046
7117
  ...(frame === undefined ? {} : { frame }),
6047
7118
  };
6048
7119
  }
6049
- 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
+ };
6050
7127
  }
6051
7128
  let reference = identifier;
6052
7129
  if (this.#punctuation(".")) {
@@ -6636,7 +7713,9 @@ export function expandDistinctWildcard(plan, columnsOf) {
6636
7713
  if (columns === undefined || columns.length === 0) {
6637
7714
  throw new TypeError(`SELECT DISTINCT * requires known columns for: ${source.table}`);
6638
7715
  }
6639
- return columns.map((name) => {
7716
+ return columns
7717
+ .filter((name) => !name.startsWith("\0"))
7718
+ .map((name) => {
6640
7719
  const output = multiple ? `${source.alias}.${name}` : name;
6641
7720
  return { expression: { kind: "column", reference: output }, alias: output };
6642
7721
  });
@@ -6650,16 +7729,20 @@ export function expandDistinctWildcard(plan, columnsOf) {
6650
7729
  * table's columns positionally (E051-09). The table's own column order is only known here, so
6651
7730
  * the parser records the names and this pass — one per execution entry — applies them.
6652
7731
  */
6653
- /** Whether any block of the plan reads a table name the catalog answers with a view. */
6654
- export function planReadsViews(plan, isView) {
6655
- 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)))
6656
7735
  return true;
6657
7736
  let nested = false;
6658
7737
  forEachNestedBlock(plan, (inner) => {
6659
- nested ||= planReadsViews(inner, isView);
7738
+ nested ||= planReadsTable(inner, matches);
6660
7739
  });
6661
7740
  return nested;
6662
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
+ }
6663
7746
  /**
6664
7747
  * Replaces every reference to a view with the query it stands for, as a derived table under the
6665
7748
  * reference's own alias (F031-02). A view whose body reads another view expands too, up to a
@@ -6759,11 +7842,13 @@ export function withTiesPlan(plan) {
6759
7842
  */
6760
7843
  function sourceWildcardColumns(source, columnsOf) {
6761
7844
  if (source.derived !== undefined)
6762
- return source.derived.select.map((item) => item.alias);
7845
+ return source.derived.select.map((item) => item.alias).filter((name) => !name.startsWith("\0"));
6763
7846
  if (source.union !== undefined) {
6764
- 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"));
6765
7850
  }
6766
- return columnsOf(source.table);
7851
+ return columnsOf(source.table)?.filter((name) => !name.startsWith("\0"));
6767
7852
  }
6768
7853
  /** Whether any block of the plan still carries an unresolved NATURAL join marker. */
6769
7854
  export function planHasNaturalJoins(plan) {
@@ -7085,7 +8170,7 @@ export function transparentProjectionSource(plan) {
7085
8170
  }
7086
8171
  /** Projects a result to a wrapper's visible aliases, preserving row order. */
7087
8172
  export function projectResultColumns(result, aliases) {
7088
- return {
8173
+ return copyQueryResultExternalization(result, {
7089
8174
  columns: [...aliases],
7090
8175
  rows: result.rows.map((row) => {
7091
8176
  const projected = {};
@@ -7093,7 +8178,7 @@ export function projectResultColumns(result, aliases) {
7093
8178
  projected[alias] = row[alias] ?? null;
7094
8179
  return projected;
7095
8180
  }),
7096
- };
8181
+ });
7097
8182
  }
7098
8183
  /** Names a derived (subquery or expanded CTE) source under the shared sequence. */
7099
8184
  export function derivedTableSource(derived, alias, nextSequence) {
@@ -7127,6 +8212,43 @@ function renameBlockOutputs(block, columns, name) {
7127
8212
  * writes a T and leaves the time off entirely for midnight. A literal without a zone is UTC, the
7128
8213
  * same reading `DATE` already takes and the same one every datetime in a Minnow database has.
7129
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
+ }
7130
8252
  export function timestampLiteral(text) {
7131
8253
  const trimmed = text.trim();
7132
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);
@@ -7446,7 +8568,7 @@ function tokenize(sql) {
7446
8568
  }
7447
8569
  if (["+", "-", "*", "/", "%", "=", ">", "<"].includes(character))
7448
8570
  tokens.push({ kind: "operator", text: character, start: index, end: index + 1 });
7449
- else if (["(", ")", ",", "."].includes(character))
8571
+ else if (["(", ")", "[", "]", ",", "."].includes(character))
7450
8572
  tokens.push({ kind: "punctuation", text: character, start: index, end: index + 1 });
7451
8573
  else
7452
8574
  throw new SqlCompileError(`Unsupported SQL character: ${character}`, index, 1);