@minnowdb/core 0.2.1 → 0.4.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 (270) hide show
  1. package/README.md +23 -32
  2. package/dist/block-format/block.d.ts +32 -13
  3. package/dist/block-format/block.js +179 -55
  4. package/dist/block-format/checksum.d.ts +2 -1
  5. package/dist/block-format/checksum.js +5 -2
  6. package/dist/block-format/codecs.d.ts +7 -2
  7. package/dist/block-format/codecs.js +53 -12
  8. package/dist/block-format/column.d.ts +3 -2
  9. package/dist/block-format/column.js +96 -30
  10. package/dist/block-format/index.d.ts +2 -2
  11. package/dist/block-format/index.js +2 -2
  12. package/dist/block-format/physical.d.ts +8 -2
  13. package/dist/block-format/physical.js +14 -6
  14. package/dist/block-format/types.d.ts +7 -3
  15. package/dist/block-format/types.js +0 -1
  16. package/dist/block-format/unicode.d.ts +11 -0
  17. package/dist/block-format/unicode.js +46 -0
  18. package/dist/date-value.d.ts +15 -0
  19. package/dist/date-value.js +64 -0
  20. package/dist/engine/artifact-cache.d.ts +2 -1
  21. package/dist/engine/artifact-cache.js +5 -1
  22. package/dist/engine/batch.d.ts +11 -3
  23. package/dist/engine/batch.js +17 -7
  24. package/dist/engine/buffered-writer.d.ts +2 -1
  25. package/dist/engine/buffered-writer.js +34 -9
  26. package/dist/engine/cache-limits.d.ts +25 -0
  27. package/dist/engine/cache-limits.js +25 -0
  28. package/dist/engine/catalog.d.ts +24 -5
  29. package/dist/engine/catalog.js +34 -7
  30. package/dist/engine/client.d.ts +29 -7
  31. package/dist/engine/client.js +216 -40
  32. package/dist/engine/database.d.ts +224 -39
  33. package/dist/engine/database.js +7475 -1539
  34. package/dist/engine/defaults.d.ts +4 -8
  35. package/dist/engine/defaults.js +46 -22
  36. package/dist/engine/errors.d.ts +19 -1
  37. package/dist/engine/errors.js +32 -2
  38. package/dist/engine/fts.d.ts +0 -6
  39. package/dist/engine/fts.js +41 -14
  40. package/dist/engine/group-index.d.ts +0 -1
  41. package/dist/engine/group-index.js +6 -11
  42. package/dist/engine/index.d.ts +16 -10
  43. package/dist/engine/index.js +12 -9
  44. package/dist/engine/join-index.d.ts +0 -1
  45. package/dist/engine/join-index.js +2 -2
  46. package/dist/engine/keyed-live.d.ts +57 -0
  47. package/dist/engine/keyed-live.js +226 -0
  48. package/dist/engine/live-api.d.ts +4 -0
  49. package/dist/engine/live-api.js +4 -0
  50. package/dist/engine/live.d.ts +43 -26
  51. package/dist/engine/live.js +504 -126
  52. package/dist/engine/memory.d.ts +2 -1
  53. package/dist/engine/memory.js +3 -2
  54. package/dist/engine/optimizer.d.ts +0 -1
  55. package/dist/engine/optimizer.js +262 -27
  56. package/dist/engine/query-api.d.ts +3 -0
  57. package/dist/engine/query-api.js +3 -0
  58. package/dist/engine/query-cache.d.ts +1 -2
  59. package/dist/engine/query-cache.js +7 -5
  60. package/dist/engine/query.d.ts +134 -29
  61. package/dist/engine/query.js +1668 -372
  62. package/dist/engine/result-wire.d.ts +0 -1
  63. package/dist/engine/result-wire.js +2 -2
  64. package/dist/engine/schema-wire.d.ts +6 -3
  65. package/dist/engine/schema-wire.js +40 -34
  66. package/dist/engine/schema.d.ts +156 -80
  67. package/dist/engine/schema.js +570 -108
  68. package/dist/engine/sort-keys.d.ts +4 -4
  69. package/dist/engine/sort-keys.js +44 -29
  70. package/dist/engine/sql-domains.d.ts +31 -0
  71. package/dist/engine/sql-domains.js +585 -0
  72. package/dist/engine/sql-driver.d.ts +19 -0
  73. package/dist/engine/sql-driver.js +1 -0
  74. package/dist/engine/sql-json.d.ts +7 -9
  75. package/dist/engine/sql-json.js +75 -15
  76. package/dist/engine/sql-semantics.d.ts +8 -3
  77. package/dist/engine/sql-semantics.js +458 -30
  78. package/dist/engine/typed-live.d.ts +67 -0
  79. package/dist/engine/typed-live.js +349 -0
  80. package/dist/engine/vector.d.ts +12 -2
  81. package/dist/engine/vector.js +635 -119
  82. package/dist/engine/worker-host.d.ts +9 -2
  83. package/dist/engine/worker-host.js +620 -152
  84. package/dist/engine/worker.d.ts +0 -1
  85. package/dist/engine/worker.js +3 -3
  86. package/dist/engine/write-block-planner.d.ts +18 -0
  87. package/dist/engine/write-block-planner.js +112 -0
  88. package/dist/index.d.ts +0 -1
  89. package/dist/index.js +0 -1
  90. package/dist/plan/index.d.ts +3 -4
  91. package/dist/plan/index.js +3 -4
  92. package/dist/storage/index.d.ts +1 -1
  93. package/dist/storage/index.js +1 -1
  94. package/dist/storage/indexeddb.d.ts +99 -51
  95. package/dist/storage/indexeddb.js +13448 -2660
  96. package/dist/storage/memory.d.ts +64 -57
  97. package/dist/storage/memory.js +1042 -128
  98. package/dist/storage/opfs/files.d.ts +18 -7
  99. package/dist/storage/opfs/files.js +115 -28
  100. package/dist/storage/opfs/index.d.ts +1 -1
  101. package/dist/storage/opfs/index.js +1 -1
  102. package/dist/storage/opfs/leader.d.ts +409 -75
  103. package/dist/storage/opfs/leader.js +4621 -650
  104. package/dist/storage/opfs/rpc.d.ts +15 -3
  105. package/dist/storage/opfs/rpc.js +224 -2
  106. package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
  107. package/dist/storage/opfs/snapshot-ledger.js +281 -0
  108. package/dist/storage/opfs/store.d.ts +33 -99
  109. package/dist/storage/opfs/store.js +453 -364
  110. package/dist/storage/persistence.d.ts +37 -0
  111. package/dist/storage/persistence.js +78 -0
  112. package/dist/storage/snapshot-stream.d.ts +24 -0
  113. package/dist/storage/snapshot-stream.js +897 -0
  114. package/dist/storage/snapshot.d.ts +9 -85
  115. package/dist/storage/snapshot.js +33 -265
  116. package/dist/storage/toolkit/extents.d.ts +41 -7
  117. package/dist/storage/toolkit/extents.js +402 -39
  118. package/dist/storage/toolkit/index.d.ts +4 -5
  119. package/dist/storage/toolkit/index.js +28 -4
  120. package/dist/storage/toolkit/record-core.d.ts +182 -55
  121. package/dist/storage/toolkit/record-core.js +6158 -1331
  122. package/dist/storage/toolkit/sync-file.d.ts +10 -1
  123. package/dist/storage/toolkit/sync-file.js +50 -2
  124. package/dist/storage/toolkit/wal.d.ts +23 -5
  125. package/dist/storage/toolkit/wal.js +89 -27
  126. package/dist/storage/toolkit/wire.d.ts +15 -2
  127. package/dist/storage/toolkit/wire.js +199 -10
  128. package/dist/storage/types.d.ts +1439 -204
  129. package/dist/storage/types.js +1745 -141
  130. package/dist/testing/block-store-conformance.d.ts +0 -1
  131. package/dist/testing/block-store-conformance.js +1009 -119
  132. package/dist/testing/index.d.ts +46 -26
  133. package/dist/testing/index.js +112 -57
  134. package/dist/testing/opfs-shim.d.ts +13 -3
  135. package/dist/testing/opfs-shim.js +40 -6
  136. package/dist/testing/simulator.d.ts +97 -0
  137. package/dist/testing/simulator.js +591 -0
  138. package/dist/testing/sqllogictest.d.ts +89 -0
  139. package/dist/testing/sqllogictest.js +434 -0
  140. package/dist/transactions/index.d.ts +92 -16
  141. package/dist/transactions/index.js +1074 -236
  142. package/dist/worker-protocol/index.d.ts +3 -2
  143. package/dist/worker-protocol/index.js +2 -5
  144. package/package.json +53 -3
  145. package/postgres-feature-profile.json +223 -0
  146. package/sql-feature-matrix.json +172 -252
  147. package/dist/block-format/block.d.ts.map +0 -1
  148. package/dist/block-format/block.js.map +0 -1
  149. package/dist/block-format/checksum.d.ts.map +0 -1
  150. package/dist/block-format/checksum.js.map +0 -1
  151. package/dist/block-format/codecs.d.ts.map +0 -1
  152. package/dist/block-format/codecs.js.map +0 -1
  153. package/dist/block-format/column.d.ts.map +0 -1
  154. package/dist/block-format/column.js.map +0 -1
  155. package/dist/block-format/index.d.ts.map +0 -1
  156. package/dist/block-format/index.js.map +0 -1
  157. package/dist/block-format/physical.d.ts.map +0 -1
  158. package/dist/block-format/physical.js.map +0 -1
  159. package/dist/block-format/types.d.ts.map +0 -1
  160. package/dist/block-format/types.js.map +0 -1
  161. package/dist/engine/artifact-cache.d.ts.map +0 -1
  162. package/dist/engine/artifact-cache.js.map +0 -1
  163. package/dist/engine/batch.d.ts.map +0 -1
  164. package/dist/engine/batch.js.map +0 -1
  165. package/dist/engine/buffered-writer.d.ts.map +0 -1
  166. package/dist/engine/buffered-writer.js.map +0 -1
  167. package/dist/engine/catalog.d.ts.map +0 -1
  168. package/dist/engine/catalog.js.map +0 -1
  169. package/dist/engine/client.d.ts.map +0 -1
  170. package/dist/engine/client.js.map +0 -1
  171. package/dist/engine/coordinator.d.ts +0 -17
  172. package/dist/engine/coordinator.d.ts.map +0 -1
  173. package/dist/engine/coordinator.js +0 -60
  174. package/dist/engine/coordinator.js.map +0 -1
  175. package/dist/engine/database.d.ts.map +0 -1
  176. package/dist/engine/database.js.map +0 -1
  177. package/dist/engine/defaults.d.ts.map +0 -1
  178. package/dist/engine/defaults.js.map +0 -1
  179. package/dist/engine/errors.d.ts.map +0 -1
  180. package/dist/engine/errors.js.map +0 -1
  181. package/dist/engine/fts.d.ts.map +0 -1
  182. package/dist/engine/fts.js.map +0 -1
  183. package/dist/engine/group-index.d.ts.map +0 -1
  184. package/dist/engine/group-index.js.map +0 -1
  185. package/dist/engine/index.d.ts.map +0 -1
  186. package/dist/engine/index.js.map +0 -1
  187. package/dist/engine/join-index.d.ts.map +0 -1
  188. package/dist/engine/join-index.js.map +0 -1
  189. package/dist/engine/live.d.ts.map +0 -1
  190. package/dist/engine/live.js.map +0 -1
  191. package/dist/engine/memory.d.ts.map +0 -1
  192. package/dist/engine/memory.js.map +0 -1
  193. package/dist/engine/optimizer.d.ts.map +0 -1
  194. package/dist/engine/optimizer.js.map +0 -1
  195. package/dist/engine/query-cache.d.ts.map +0 -1
  196. package/dist/engine/query-cache.js.map +0 -1
  197. package/dist/engine/query.d.ts.map +0 -1
  198. package/dist/engine/query.js.map +0 -1
  199. package/dist/engine/result-wire.d.ts.map +0 -1
  200. package/dist/engine/result-wire.js.map +0 -1
  201. package/dist/engine/schema-wire.d.ts.map +0 -1
  202. package/dist/engine/schema-wire.js.map +0 -1
  203. package/dist/engine/schema.d.ts.map +0 -1
  204. package/dist/engine/schema.js.map +0 -1
  205. package/dist/engine/sort-keys.d.ts.map +0 -1
  206. package/dist/engine/sort-keys.js.map +0 -1
  207. package/dist/engine/sql-json.d.ts.map +0 -1
  208. package/dist/engine/sql-json.js.map +0 -1
  209. package/dist/engine/sql-semantics.d.ts.map +0 -1
  210. package/dist/engine/sql-semantics.js.map +0 -1
  211. package/dist/engine/vector.d.ts.map +0 -1
  212. package/dist/engine/vector.js.map +0 -1
  213. package/dist/engine/worker-host.d.ts.map +0 -1
  214. package/dist/engine/worker-host.js.map +0 -1
  215. package/dist/engine/worker.d.ts.map +0 -1
  216. package/dist/engine/worker.js.map +0 -1
  217. package/dist/index.d.ts.map +0 -1
  218. package/dist/index.js.map +0 -1
  219. package/dist/plan/index.d.ts.map +0 -1
  220. package/dist/plan/index.js.map +0 -1
  221. package/dist/storage/fixture-shape.d.ts +0 -42
  222. package/dist/storage/fixture-shape.d.ts.map +0 -1
  223. package/dist/storage/fixture-shape.js +0 -146
  224. package/dist/storage/fixture-shape.js.map +0 -1
  225. package/dist/storage/index.d.ts.map +0 -1
  226. package/dist/storage/index.js.map +0 -1
  227. package/dist/storage/indexeddb.d.ts.map +0 -1
  228. package/dist/storage/indexeddb.js.map +0 -1
  229. package/dist/storage/memory.d.ts.map +0 -1
  230. package/dist/storage/memory.js.map +0 -1
  231. package/dist/storage/opfs/files.d.ts.map +0 -1
  232. package/dist/storage/opfs/files.js.map +0 -1
  233. package/dist/storage/opfs/index.d.ts.map +0 -1
  234. package/dist/storage/opfs/index.js.map +0 -1
  235. package/dist/storage/opfs/leader.d.ts.map +0 -1
  236. package/dist/storage/opfs/leader.js.map +0 -1
  237. package/dist/storage/opfs/rpc.d.ts.map +0 -1
  238. package/dist/storage/opfs/rpc.js.map +0 -1
  239. package/dist/storage/opfs/store.d.ts.map +0 -1
  240. package/dist/storage/opfs/store.js.map +0 -1
  241. package/dist/storage/snapshot.d.ts.map +0 -1
  242. package/dist/storage/snapshot.js.map +0 -1
  243. package/dist/storage/toolkit/extents.d.ts.map +0 -1
  244. package/dist/storage/toolkit/extents.js.map +0 -1
  245. package/dist/storage/toolkit/index.d.ts.map +0 -1
  246. package/dist/storage/toolkit/index.js.map +0 -1
  247. package/dist/storage/toolkit/record-core.d.ts.map +0 -1
  248. package/dist/storage/toolkit/record-core.js.map +0 -1
  249. package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
  250. package/dist/storage/toolkit/sync-file.js.map +0 -1
  251. package/dist/storage/toolkit/wal.d.ts.map +0 -1
  252. package/dist/storage/toolkit/wal.js.map +0 -1
  253. package/dist/storage/toolkit/wire.d.ts.map +0 -1
  254. package/dist/storage/toolkit/wire.js.map +0 -1
  255. package/dist/storage/types.d.ts.map +0 -1
  256. package/dist/storage/types.js.map +0 -1
  257. package/dist/testing/block-store-conformance.d.ts.map +0 -1
  258. package/dist/testing/block-store-conformance.js.map +0 -1
  259. package/dist/testing/index.d.ts.map +0 -1
  260. package/dist/testing/index.js.map +0 -1
  261. package/dist/testing/opfs-shim.d.ts.map +0 -1
  262. package/dist/testing/opfs-shim.js.map +0 -1
  263. package/dist/testing/seeds.d.ts +0 -11
  264. package/dist/testing/seeds.d.ts.map +0 -1
  265. package/dist/testing/seeds.js +0 -50
  266. package/dist/testing/seeds.js.map +0 -1
  267. package/dist/transactions/index.d.ts.map +0 -1
  268. package/dist/transactions/index.js.map +0 -1
  269. package/dist/worker-protocol/index.d.ts.map +0 -1
  270. package/dist/worker-protocol/index.js.map +0 -1
@@ -1,11 +1,15 @@
1
+ import { copyDate, dateIsoString, dateMilliseconds, dateUtcDate, dateUtcDay, dateUtcFullYear, dateUtcHours, dateUtcMinutes, dateUtcMonth, dateUtcSeconds, setDateUtcDate, setDateUtcMonth, } from "../date-value.js";
2
+ import { assertWellFormedString, wellFormedUtf8ByteLength } from "../block-format/unicode.js";
3
+ import { MAX_SQL_NESTING_DEPTH, MAX_SQL_PARAMETERS, MAX_SQL_SCALAR_RESULT_CHARACTERS, MAX_SQL_TEXT_CHARACTERS, MAX_SQL_TOKENS, } from "./cache-limits.js";
1
4
  import { SqlCompileError } from "./errors.js";
2
5
  import { cachedQueryTerms, ftsBm25Row, FtsStatsAccumulator, ftsMatchTruth, renderDocumentValue, tokenize as ftsTokenize, validateFtsQuery, } from "./fts.js";
3
6
  import { QueryMemoryContext } from "./memory.js";
4
7
  import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
5
8
  import { stringArgument } from "./sql-semantics.js";
6
- import { jsonAtPath, jsonConstructor, jsonIsValid, parseJsonPath } from "./sql-json.js";
9
+ import { jsonAtPath, jsonConstructor, jsonIsValid, jsonValueOf, parseJsonPath, } from "./sql-json.js";
7
10
  import { optimizePlan } from "./optimizer.js";
8
- import { compareSqlValues as compareValues, compileLikePattern, encodeSqlEqualityValue, roundSqlNumber, } from "./sql-semantics.js";
11
+ import { compareSqlValues as compareValues, compileLikePattern, compileSimilarPattern, encodeSqlEqualityValue, roundSqlNumber, } from "./sql-semantics.js";
12
+ import { arrayDomainValue, boundedJsonText, collatedDomainValue, exactNumericBinary, exactNumericValue, externalSqlDomainValue, intervalDomainValue, isExactNumeric, isSqlDomainValue, jsonDomainValue, normalizeSqlDomainValue, protectedSqlTextValue, timeDomainValue, uuidDomainValue, } from "./sql-domains.js";
9
13
  import { columnarTableFromRows, prepareVectorQuery, } from "./vector.js";
10
14
  export const scalarFunctionNames = new Set([
11
15
  "ROUND",
@@ -46,6 +50,18 @@ export const scalarFunctionNames = new Set([
46
50
  "JSON_OBJECT",
47
51
  "JSON_ARRAY",
48
52
  "IS_JSON",
53
+ "ARRAY",
54
+ "MINNOW_TUPLE_KEY",
55
+ "MINNOW_COLLATE",
56
+ "NEXTVAL",
57
+ "CURRVAL",
58
+ "RANDOM",
59
+ "GEN_RANDOM_UUID",
60
+ ]);
61
+ /** Functions whose answer can change without any catalog or input-row change. */
62
+ export const volatileScalarFunctionNames = new Set([
63
+ "RANDOM",
64
+ "GEN_RANDOM_UUID",
49
65
  ]);
50
66
  /**
51
67
  * The niladic datetime functions (F051-06/07/08). Their value is the statement's own clock
@@ -103,7 +119,7 @@ function trimEnds(name, value, characters, side) {
103
119
  end -= unit.length;
104
120
  }
105
121
  }
106
- return text.slice(start, end);
122
+ return protectedSqlTextValue(text.slice(start, end));
107
123
  }
108
124
  /**
109
125
  * CAST conversions between the four logical types, matching the strict common ground of
@@ -112,15 +128,30 @@ function trimEnds(name, value, characters, side) {
112
128
  * number cast to datetime reads as milliseconds since the epoch.
113
129
  */
114
130
  function castValue(value, target) {
131
+ if (target.startsWith("numeric")) {
132
+ const [, precision, scale] = target.split(":");
133
+ return exactNumericValue(value, precision === undefined || precision === "" ? undefined : Number(precision), scale === undefined || scale === "" ? undefined : Number(scale));
134
+ }
135
+ if (target === "json")
136
+ return jsonDomainValue(value, false);
137
+ if (target === "jsonb")
138
+ return jsonDomainValue(value, true);
139
+ if (target === "uuid")
140
+ return uuidDomainValue(value);
141
+ if (target === "time")
142
+ return timeDomainValue(value);
143
+ if (target === "interval")
144
+ return intervalDomainValue(value);
115
145
  if (target === "string") {
116
- if (typeof value === "string")
117
- return value;
146
+ const external = externalSqlDomainValue(value);
147
+ if (typeof external === "string")
148
+ return protectedSqlTextValue(external);
118
149
  if (typeof value === "number")
119
- return String(value);
150
+ return protectedSqlTextValue(String(value));
120
151
  if (typeof value === "boolean")
121
- return value ? "true" : "false";
152
+ return protectedSqlTextValue(value ? "true" : "false");
122
153
  if (value instanceof Date)
123
- return value.toISOString();
154
+ return protectedSqlTextValue(dateIsoString(value));
124
155
  }
125
156
  if (target === "number" || target === "number-integer") {
126
157
  let parsed;
@@ -136,8 +167,15 @@ function castValue(value, target) {
136
167
  }
137
168
  parsed = candidate;
138
169
  }
139
- if (parsed !== undefined)
140
- return target === "number-integer" ? Math.trunc(parsed) : parsed;
170
+ if (parsed !== undefined) {
171
+ if (target !== "number-integer")
172
+ return parsed;
173
+ const integer = Math.trunc(parsed);
174
+ if (!Number.isSafeInteger(integer)) {
175
+ throw new RangeError(`Integer cast is outside the exact safe range: ${String(value)}`);
176
+ }
177
+ return integer;
178
+ }
141
179
  }
142
180
  if (target === "boolean") {
143
181
  if (typeof value === "boolean")
@@ -163,7 +201,7 @@ function castValue(value, target) {
163
201
  return value;
164
202
  if (typeof value === "string" || typeof value === "number") {
165
203
  const parsed = new Date(value);
166
- if (Number.isFinite(parsed.getTime()))
204
+ if (Number.isFinite(dateMilliseconds(parsed)))
167
205
  return parsed;
168
206
  throw new TypeError(`Cannot cast this value to a datetime: ${String(value)}`);
169
207
  }
@@ -189,6 +227,10 @@ export function scalarFunctionValue(name, values) {
189
227
  // means a call survived that pass, which would silently give one statement two clocks.
190
228
  throw new TypeError(`${name} must be resolved before execution`);
191
229
  }
230
+ if (name === "RANDOM")
231
+ return Math.random();
232
+ if (name === "GEN_RANDOM_UUID")
233
+ return globalThis.crypto.randomUUID();
192
234
  if (name === "DATE_TRUNC")
193
235
  return dateTruncValue(values[0], values[1]);
194
236
  if (name === "DATE_ADD")
@@ -210,6 +252,21 @@ export function scalarFunctionValue(name, values) {
210
252
  // These build from every argument, so a NULL first one is data, not an early exit.
211
253
  return jsonConstructor(name, values);
212
254
  }
255
+ if (name === "ARRAY")
256
+ return arrayDomainValue(values);
257
+ if (name === "MINNOW_COLLATE")
258
+ return collatedDomainValue(values[0], values[1]);
259
+ if (name === "NEXTVAL" || name === "CURRVAL") {
260
+ throw new TypeError(`${name} must be resolved by the database catalog`);
261
+ }
262
+ if (name === "MINNOW_TUPLE_KEY") {
263
+ // SQL equality cannot match a tuple containing NULL. JSON stringification of the tagged
264
+ // scalar equality encodings is prefix-free and keeps strings, numbers, booleans, and
265
+ // datetimes distinct, so the ordinary single-key hash join can safely carry a composite.
266
+ if (values.some((value) => value === null || value === undefined))
267
+ return null;
268
+ return boundedJsonText(values.map(encodeSqlEqualityValue), false, "Composite key");
269
+ }
213
270
  const first = values[0];
214
271
  if (first === null || first === undefined)
215
272
  return null;
@@ -253,12 +310,16 @@ export function scalarFunctionValue(name, values) {
253
310
  return null;
254
311
  if (values[2] === null || values[2] === undefined)
255
312
  return null;
313
+ const source = stringArgument("REPLACE", first);
256
314
  const search = stringArgument("REPLACE", values[1]);
315
+ const replacement = stringArgument("REPLACE", values[2]);
316
+ assertScalarInputLength(source, "REPLACE source");
317
+ assertScalarInputLength(search, "REPLACE search text");
318
+ assertScalarInputLength(replacement, "REPLACE replacement text");
257
319
  if (search === "")
258
- return stringArgument("REPLACE", first);
259
- return stringArgument("REPLACE", first)
260
- .split(search)
261
- .join(stringArgument("REPLACE", values[2]));
320
+ return source;
321
+ assertReplacementResultLength(source, search, replacement);
322
+ return protectedSqlTextValue(source.split(search).join(replacement));
262
323
  }
263
324
  case "INSTR": {
264
325
  if (values[1] === null || values[1] === undefined)
@@ -267,7 +328,7 @@ export function scalarFunctionValue(name, values) {
267
328
  const needle = stringArgument("INSTR", values[1]);
268
329
  const index = haystack.indexOf(needle);
269
330
  // 1-based character position, 0 when absent, counting codepoints like LENGTH.
270
- return index === -1 ? 0 : Array.from(haystack.slice(0, index)).length + 1;
331
+ return index === -1 ? 0 : codePointLength(haystack, index) + 1;
271
332
  }
272
333
  case "EXTRACT":
273
334
  return extractDatePart(typeof first === "string" ? first : "", values[1]);
@@ -279,17 +340,23 @@ export function scalarFunctionValue(name, values) {
279
340
  }
280
341
  case "ABS":
281
342
  return Math.abs(numeric(first));
282
- case "UPPER":
283
- return stringArgument("UPPER", first).toUpperCase();
284
- case "LOWER":
285
- return stringArgument("LOWER", first).toLowerCase();
343
+ case "UPPER": {
344
+ const source = stringArgument("UPPER", first);
345
+ assertScalarInputLength(source, "UPPER input");
346
+ return boundedScalarResult(source.toUpperCase(), "UPPER result");
347
+ }
348
+ case "LOWER": {
349
+ const source = stringArgument("LOWER", first);
350
+ assertScalarInputLength(source, "LOWER input");
351
+ return boundedScalarResult(source.toLowerCase(), "LOWER result");
352
+ }
286
353
  case "TRIM":
287
354
  // SQL TRIM removes spaces, not general whitespace.
288
355
  return trimEnds("TRIM", first, values[1], "both");
289
356
  case "LENGTH":
290
- return Array.from(stringArgument("LENGTH", first)).length;
357
+ return codePointLength(stringArgument("LENGTH", first));
291
358
  case "OCTET_LENGTH":
292
- return new TextEncoder().encode(stringArgument("OCTET_LENGTH", first)).length;
359
+ return wellFormedUtf8ByteLength(stringArgument("OCTET_LENGTH", first), "OCTET_LENGTH input");
293
360
  case "IS_JSON": {
294
361
  const kind = values[1];
295
362
  return jsonIsValid(first, typeof kind === "string" ? kind : "value");
@@ -306,7 +373,7 @@ export function scalarFunctionValue(name, values) {
306
373
  return null;
307
374
  if (typeof value === "object")
308
375
  return null;
309
- return typeof value === "string" ? value : JSON.stringify(value);
376
+ return protectedSqlTextValue(typeof value === "string" ? value : JSON.stringify(value));
310
377
  }
311
378
  case "JSON_QUERY": {
312
379
  const found = jsonAtPath(first, values[1], "JSON_QUERY");
@@ -323,24 +390,24 @@ export function scalarFunctionValue(name, values) {
323
390
  if (!Number.isInteger(width) || width < 0) {
324
391
  throw new TypeError(`${name} length must be a non-negative integer`);
325
392
  }
326
- const text = Array.from(stringArgument(name, first));
327
- if (text.length >= width)
328
- return text.slice(0, width).join("");
393
+ if (width > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
394
+ throw new RangeError(`${name} result exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
395
+ }
396
+ const source = stringArgument(name, first);
397
+ const text = codePointPrefix(source, width);
398
+ if (text.count >= width)
399
+ return boundedScalarResult(text.text, `${name} result`);
329
400
  let fill = " ";
330
401
  if (values.length > 2) {
331
402
  if (values[2] === null || values[2] === undefined)
332
403
  return null;
333
404
  fill = stringArgument(name, values[2]);
334
405
  }
335
- const filler = Array.from(fill);
336
406
  // An empty fill cannot pad, so the value passes through, matching PostgreSQL.
337
- if (filler.length === 0)
338
- return text.join("");
339
- const padding = [];
340
- while (padding.length < width - text.length) {
341
- padding.push(filler[padding.length % filler.length] ?? "");
342
- }
343
- return name === "LPAD" ? padding.join("") + text.join("") : text.join("") + padding.join("");
407
+ const padding = repeatedCodePointPrefix(fill, width - text.count);
408
+ if (padding === "")
409
+ return protectedSqlTextValue(text.text);
410
+ return boundedScalarResult(name === "LPAD" ? padding + text.text : text.text + padding, `${name} result`);
344
411
  }
345
412
  case "OVERLAY": {
346
413
  // OVERLAY(s PLACING r FROM start [FOR length]) replaces `length` characters of `s`
@@ -350,8 +417,12 @@ export function scalarFunctionValue(name, values) {
350
417
  return null;
351
418
  }
352
419
  }
353
- const text = Array.from(stringArgument("OVERLAY", first));
354
- const replacement = Array.from(stringArgument("OVERLAY", values[1]));
420
+ const sourceText = stringArgument("OVERLAY", first);
421
+ const replacementText = stringArgument("OVERLAY", values[1]);
422
+ assertScalarInputLength(sourceText, "OVERLAY source");
423
+ assertScalarInputLength(replacementText, "OVERLAY replacement");
424
+ const text = Array.from(sourceText);
425
+ const replacement = Array.from(replacementText);
355
426
  const start = numeric(values[2]);
356
427
  if (!Number.isInteger(start) || start < 1) {
357
428
  throw new TypeError("OVERLAY start must be a positive integer");
@@ -360,25 +431,25 @@ export function scalarFunctionValue(name, values) {
360
431
  if (!Number.isInteger(span) || span < 0) {
361
432
  throw new TypeError("OVERLAY length must be a non-negative integer");
362
433
  }
363
- return [
434
+ return boundedScalarResult([
364
435
  ...text.slice(0, start - 1),
365
436
  ...replacement,
366
437
  ...text.slice(Math.min(start - 1 + span, text.length)),
367
- ].join("");
438
+ ].join(""), "OVERLAY result");
368
439
  }
369
440
  case "CAST":
370
441
  return castValue(first, typeof values[1] === "string" ? values[1] : "");
371
442
  case "SUBSTR": {
372
- // SQL:2023 6.32: the result is the characters whose positions fall in both the requested
443
+ // PostgreSQL SUBSTRING returns the characters whose positions fall in both the requested
373
444
  // window and the string, so a start before 1 shortens the result instead of shifting it,
374
445
  // and a window entirely off the string is empty rather than an error.
375
- const text = Array.from(stringArgument("SUBSTR", first));
446
+ const source = stringArgument("SUBSTR", first);
376
447
  if (values[1] === null || values[1] === undefined)
377
448
  return null;
378
449
  const start = numeric(values[1]);
379
450
  if (!Number.isInteger(start))
380
451
  throw new TypeError("SUBSTR start must be an integer");
381
- let until = text.length + 1;
452
+ let until = Number.POSITIVE_INFINITY;
382
453
  if (values.length > 2) {
383
454
  if (values[2] === null || values[2] === undefined)
384
455
  return null;
@@ -389,9 +460,109 @@ export function scalarFunctionValue(name, values) {
389
460
  until = start + length;
390
461
  }
391
462
  const from = Math.max(start, 1);
392
- const to = Math.min(until, text.length + 1);
393
- return to <= from ? "" : text.slice(from - 1, to - 1).join("");
463
+ if (until <= from)
464
+ return "";
465
+ return boundedScalarResult(codePointSlice(source, from - 1, until - 1), "SUBSTR result");
466
+ }
467
+ }
468
+ }
469
+ function assertScalarInputLength(value, label) {
470
+ if (value.length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
471
+ throw new RangeError(`${label} exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
472
+ }
473
+ }
474
+ function boundedScalarResult(value, label) {
475
+ if (value.length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
476
+ throw new RangeError(`${label} exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
477
+ }
478
+ return protectedSqlTextValue(value);
479
+ }
480
+ function codePointLength(value, end = value.length) {
481
+ let count = 0;
482
+ for (let index = 0; index < end; index += 1) {
483
+ const first = value.charCodeAt(index);
484
+ if (first >= 0xd800 && first <= 0xdbff && index + 1 < end) {
485
+ const second = value.charCodeAt(index + 1);
486
+ if (second >= 0xdc00 && second <= 0xdfff)
487
+ index += 1;
394
488
  }
489
+ count += 1;
490
+ }
491
+ return count;
492
+ }
493
+ function codePointPrefix(value, count) {
494
+ let index = 0;
495
+ let found = 0;
496
+ while (index < value.length && found < count) {
497
+ const first = value.charCodeAt(index);
498
+ index +=
499
+ first >= 0xd800 &&
500
+ first <= 0xdbff &&
501
+ index + 1 < value.length &&
502
+ value.charCodeAt(index + 1) >= 0xdc00 &&
503
+ value.charCodeAt(index + 1) <= 0xdfff
504
+ ? 2
505
+ : 1;
506
+ found += 1;
507
+ }
508
+ return { text: value.slice(0, index), count: found };
509
+ }
510
+ function repeatedCodePointPrefix(value, count) {
511
+ if (count <= 0 || value.length === 0)
512
+ return "";
513
+ const first = codePointPrefix(value, count);
514
+ if (first.count >= count)
515
+ return first.text;
516
+ const copies = Math.floor(count / first.count);
517
+ const remainder = count % first.count;
518
+ return first.text.repeat(copies) + codePointPrefix(first.text, remainder).text;
519
+ }
520
+ function codePointSlice(value, from, to) {
521
+ let codePoint = 0;
522
+ let index = 0;
523
+ let start = value.length;
524
+ let end = value.length;
525
+ while (index < value.length) {
526
+ if (codePoint === from)
527
+ start = index;
528
+ if (codePoint === to) {
529
+ end = index;
530
+ break;
531
+ }
532
+ const first = value.charCodeAt(index);
533
+ index +=
534
+ first >= 0xd800 &&
535
+ first <= 0xdbff &&
536
+ index + 1 < value.length &&
537
+ value.charCodeAt(index + 1) >= 0xdc00 &&
538
+ value.charCodeAt(index + 1) <= 0xdfff
539
+ ? 2
540
+ : 1;
541
+ codePoint += 1;
542
+ }
543
+ if (codePoint < from)
544
+ return "";
545
+ if (to === Number.POSITIVE_INFINITY)
546
+ end = value.length;
547
+ const length = end - start;
548
+ if (length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
549
+ throw new RangeError(`SUBSTR result exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
550
+ }
551
+ return value.slice(start, end);
552
+ }
553
+ function assertReplacementResultLength(source, search, replacement) {
554
+ let matches = 0;
555
+ let from = 0;
556
+ for (;;) {
557
+ const index = source.indexOf(search, from);
558
+ if (index < 0)
559
+ break;
560
+ matches += 1;
561
+ from = index + search.length;
562
+ }
563
+ const resultLength = source.length + matches * (replacement.length - search.length);
564
+ if (!Number.isSafeInteger(resultLength) || resultLength > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
565
+ throw new RangeError(`REPLACE result exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
395
566
  }
396
567
  }
397
568
  export const dateTruncUnits = new Set([
@@ -456,15 +627,15 @@ export function dateAddValue(value, months, milliseconds) {
456
627
  if (!(value instanceof Date))
457
628
  throw new TypeError("Date arithmetic requires a datetime value");
458
629
  const monthCount = Number(months ?? 0);
459
- const shifted = new Date(value.getTime());
630
+ const shifted = copyDate(value);
460
631
  if (monthCount !== 0) {
461
- const day = shifted.getUTCDate();
462
- shifted.setUTCDate(1);
463
- shifted.setUTCMonth(shifted.getUTCMonth() + monthCount);
464
- const lastDay = new Date(Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth() + 1, 0)).getUTCDate();
465
- shifted.setUTCDate(Math.min(day, lastDay));
632
+ const day = dateUtcDate(shifted);
633
+ setDateUtcDate(shifted, 1);
634
+ setDateUtcMonth(shifted, dateUtcMonth(shifted) + monthCount);
635
+ const lastDay = new Date(Date.UTC(dateUtcFullYear(shifted), dateUtcMonth(shifted) + 1, 0));
636
+ setDateUtcDate(shifted, Math.min(day, dateUtcDate(lastDay)));
466
637
  }
467
- return new Date(shifted.getTime() + Number(milliseconds ?? 0));
638
+ return new Date(dateMilliseconds(shifted) + Number(milliseconds ?? 0));
468
639
  }
469
640
  export function dateTruncValue(unit, value) {
470
641
  if (typeof unit !== "string" || !dateTruncUnits.has(unit.toLowerCase())) {
@@ -475,9 +646,9 @@ export function dateTruncValue(unit, value) {
475
646
  if (!(value instanceof Date))
476
647
  throw new TypeError("DATE_TRUNC requires a datetime value");
477
648
  const normalized = unit.toLowerCase();
478
- const year = value.getUTCFullYear();
479
- const month = value.getUTCMonth();
480
- const day = value.getUTCDate();
649
+ const year = dateUtcFullYear(value);
650
+ const month = dateUtcMonth(value);
651
+ const day = dateUtcDate(value);
481
652
  switch (normalized) {
482
653
  case "year":
483
654
  return new Date(Date.UTC(year, 0, 1));
@@ -487,17 +658,17 @@ export function dateTruncValue(unit, value) {
487
658
  return new Date(Date.UTC(year, month, 1));
488
659
  case "week": {
489
660
  const start = new Date(Date.UTC(year, month, day));
490
- start.setUTCDate(start.getUTCDate() - ((start.getUTCDay() + 6) % 7));
661
+ setDateUtcDate(start, dateUtcDate(start) - ((dateUtcDay(start) + 6) % 7));
491
662
  return start;
492
663
  }
493
664
  case "day":
494
665
  return new Date(Date.UTC(year, month, day));
495
666
  case "hour":
496
- return new Date(Date.UTC(year, month, day, value.getUTCHours()));
667
+ return new Date(Date.UTC(year, month, day, dateUtcHours(value)));
497
668
  case "minute":
498
- return new Date(Date.UTC(year, month, day, value.getUTCHours(), value.getUTCMinutes()));
669
+ return new Date(Date.UTC(year, month, day, dateUtcHours(value), dateUtcMinutes(value)));
499
670
  default:
500
- return new Date(Date.UTC(year, month, day, value.getUTCHours(), value.getUTCMinutes(), value.getUTCSeconds()));
671
+ return new Date(Date.UTC(year, month, day, dateUtcHours(value), dateUtcMinutes(value), dateUtcSeconds(value)));
501
672
  }
502
673
  }
503
674
  /** The output column type of one window: rankings and most aggregates count, MIN/MAX carry. */
@@ -512,8 +683,32 @@ export function windowOutputType(window, innerSchema) {
512
683
  if (carries && window.argumentAlias !== undefined) {
513
684
  return innerSchema.find(({ name }) => name === window.argumentAlias)?.type ?? "number";
514
685
  }
686
+ if ((window.name === "SUM" || window.name === "AVG") &&
687
+ window.argumentAlias !== undefined &&
688
+ innerSchema.find(({ name }) => name === window.argumentAlias)?.sqlDomain?.kind === "numeric") {
689
+ return "string";
690
+ }
515
691
  return "number";
516
692
  }
693
+ /** Logical domain carried or produced by a window result, when its physical type is not enough. */
694
+ export function windowOutputDomain(window, innerSchema) {
695
+ if (window.argumentAlias === undefined)
696
+ return undefined;
697
+ const domain = innerSchema.find(({ name }) => name === window.argumentAlias)?.sqlDomain;
698
+ if (domain === undefined)
699
+ return undefined;
700
+ if (window.name === "MIN" ||
701
+ window.name === "MAX" ||
702
+ window.name === "LAG" ||
703
+ window.name === "LEAD" ||
704
+ window.name === "FIRST_VALUE" ||
705
+ window.name === "LAST_VALUE" ||
706
+ window.name === "NTH_VALUE" ||
707
+ ((window.name === "SUM" || window.name === "AVG") && domain.kind === "numeric")) {
708
+ return domain;
709
+ }
710
+ return undefined;
711
+ }
517
712
  const MAX_RECURSIVE_ITERATIONS = 10_000;
518
713
  const MAX_RECURSIVE_ROWS = 1_000_000;
519
714
  /**
@@ -535,8 +730,6 @@ const createTableTypeNames = new Map([
535
730
  ["SMALLINT", "number"],
536
731
  ["REAL", "number"],
537
732
  ["FLOAT", "number"],
538
- ["NUMERIC", "number"],
539
- ["DECIMAL", "number"],
540
733
  ["TEXT", "string"],
541
734
  ["VARCHAR", "string"],
542
735
  ["CHAR", "string"],
@@ -569,7 +762,15 @@ const clauseKeywords = new Set([
569
762
  "UNION",
570
763
  "RETURNING",
571
764
  ]);
572
- const aggregateNames = new Set(["COUNT", "SUM", "AVG", "MIN", "MAX"]);
765
+ const aggregateNames = new Set([
766
+ "COUNT",
767
+ "SUM",
768
+ "AVG",
769
+ "MIN",
770
+ "MAX",
771
+ "JSON_ARRAYAGG",
772
+ "STRING_AGG",
773
+ ]);
573
774
  /** Set functions the parser builds from COUNT/SUM rather than from their own accumulator. */
574
775
  const statisticalAggregates = new Set([
575
776
  "VAR_POP",
@@ -611,6 +812,7 @@ function throwLocated(error, offset, span) {
611
812
  throw error;
612
813
  }
613
814
  export function compileQuery(sql, options = {}) {
815
+ validateSqlSource(sql);
614
816
  const { text, offset } = normalizeSql(sql);
615
817
  if (text.length === 0)
616
818
  throw new SqlCompileError("Enter a SELECT query", offset, 0);
@@ -638,26 +840,29 @@ export function compileQuery(sql, options = {}) {
638
840
  compiled.parameterCount = parser.parameterCount;
639
841
  if (parser.usesStatementDatetime)
640
842
  compiled.usesStatementDatetime = true;
843
+ if (parser.usesSequenceCalls)
844
+ compiled.usesSequenceCalls = true;
845
+ if (parser.usesVolatileFunctions)
846
+ compiled.usesVolatileFunctions = true;
641
847
  return compiled;
642
848
  }
643
849
  /**
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.
850
+ * Reads a column DEFAULT into the catalog's own representation. Literal nodes stay structured;
851
+ * every other variable-free SQL expression is preserved as authored for catalog introspection.
646
852
  */
647
- function columnDefaultFor(expression) {
853
+ function columnDefaultFor(expression, sql) {
648
854
  if (expression.kind === "literal") {
649
855
  const value = expression.value;
650
856
  if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
651
857
  return { kind: "literal", value };
652
858
  }
653
859
  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" };
860
+ return { kind: "literal", value: copyDate(value) };
659
861
  }
660
- throw new TypeError("DEFAULT takes a constant or CURRENT_TIMESTAMP");
862
+ return { kind: "expression", sql };
863
+ }
864
+ export function isDefaultInsertValue(value) {
865
+ return (typeof value === "object" && value !== null && !(value instanceof Date) && "default" in value);
661
866
  }
662
867
  /**
663
868
  * Parses CREATE TRIGGER name AFTER INSERT|UPDATE|DELETE ON table [FOR EACH ROW]
@@ -674,6 +879,37 @@ function columnDefaultFor(expression) {
674
879
  * mode a transaction could relax into.
675
880
  */
676
881
  function parseTransactionStatement(keyword, tokens) {
882
+ const identifierAt = (index) => {
883
+ const token = tokens[index];
884
+ if (token?.kind !== "identifier") {
885
+ throw new TypeError(`Expected savepoint name, found ${token?.text ?? "end of input"}`);
886
+ }
887
+ return token.text;
888
+ };
889
+ if (keyword === "SAVEPOINT") {
890
+ const name = identifierAt(1);
891
+ if (tokens[2]?.kind !== "eof")
892
+ throw new TypeError("Unexpected input after SAVEPOINT name");
893
+ return { kind: "transaction", action: "savepoint", name };
894
+ }
895
+ if (keyword === "RELEASE") {
896
+ const hasNoise = tokens[1]?.kind === "identifier" && tokens[1].text.toUpperCase() === "SAVEPOINT";
897
+ const name = identifierAt(hasNoise ? 2 : 1);
898
+ if (tokens[hasNoise ? 3 : 2]?.kind !== "eof") {
899
+ throw new TypeError("Unexpected input after RELEASE name");
900
+ }
901
+ return { kind: "transaction", action: "release", name };
902
+ }
903
+ if (keyword === "ROLLBACK" &&
904
+ tokens[1]?.kind === "identifier" &&
905
+ tokens[1].text.toUpperCase() === "TO") {
906
+ const hasNoise = tokens[2]?.kind === "identifier" && tokens[2].text.toUpperCase() === "SAVEPOINT";
907
+ const name = identifierAt(hasNoise ? 3 : 2);
908
+ if (tokens[hasNoise ? 4 : 3]?.kind !== "eof") {
909
+ throw new TypeError("Unexpected input after ROLLBACK TO name");
910
+ }
911
+ return { kind: "transaction", action: "rollback-to", name };
912
+ }
677
913
  const words = tokens
678
914
  .filter((token) => token.kind === "identifier")
679
915
  .map((token) => token.text.toUpperCase());
@@ -702,6 +938,50 @@ export function compileCheckExpression(sql, name) {
702
938
  }
703
939
  return expression;
704
940
  }
941
+ /**
942
+ * Parses and type-checks one catalog default. PostgreSQL defaults are variable-free scalar
943
+ * expressions: functions and operators are allowed, while row references, parameters,
944
+ * aggregates, windows, and subqueries are not.
945
+ */
946
+ export function validateDefaultExpression(sql, target) {
947
+ const parser = new Parser(tokenize(sql), sql);
948
+ const expression = parser.parseExpression();
949
+ const hasSubquery = (value) => value.kind === "subquery" ||
950
+ value.kind === "exists" ||
951
+ childExpressions(value).some(hasSubquery);
952
+ if (expressionColumns(expression).length > 0 ||
953
+ containsParameter(expression) ||
954
+ hasAggregate(expression) ||
955
+ containsWindow(expression) ||
956
+ hasSubquery(expression)) {
957
+ throw new TypeError(`DEFAULT ${target.name} takes a variable-free scalar SQL expression`);
958
+ }
959
+ const plan = {
960
+ sql: `(default ${target.name})`,
961
+ base: { table: DUAL_TABLE, alias: DUAL_TABLE },
962
+ joins: [],
963
+ select: [{ expression, alias: "value" }],
964
+ predicates: [],
965
+ groupBy: [],
966
+ having: [],
967
+ orderBy: [],
968
+ };
969
+ let inferred;
970
+ try {
971
+ inferred = inferBlockSchema(plan, new Map())[0]?.type;
972
+ }
973
+ catch (error) {
974
+ if (!(error instanceof TypeError) || !error.message.startsWith("Cannot infer a column type")) {
975
+ throw error;
976
+ }
977
+ }
978
+ // A bare NULL is polymorphic in PostgreSQL; the destination column supplies its type.
979
+ if (inferred !== undefined &&
980
+ inferred !== target.type &&
981
+ !(target.sqlDomain?.kind === "numeric" && inferred === "number")) {
982
+ throw new TypeError(`DEFAULT ${target.name} produces ${inferred}, but the column is ${target.type}`);
983
+ }
984
+ }
705
985
  /**
706
986
  * CREATE [OR REPLACE] VIEW name AS <query>. The body is kept as the author's own text rather
707
987
  * than a re-rendered plan: the catalog stores what was written, and every read compiles it
@@ -836,7 +1116,7 @@ function parseCreateTrigger(text, tokens) {
836
1116
  if (compiled.kind === "insert" && compiled.onConflict !== undefined) {
837
1117
  throw new TypeError("Trigger body INSERTs cannot carry ON CONFLICT");
838
1118
  }
839
- if (compiled.kind !== "insert" && compiled.returning !== undefined) {
1119
+ if (compiled.returning !== undefined) {
840
1120
  throw new TypeError("Trigger bodies cannot carry RETURNING");
841
1121
  }
842
1122
  if ((compiled.parameterCount ?? 0) !== bindings.length) {
@@ -855,6 +1135,7 @@ function parseCreateTrigger(text, tokens) {
855
1135
  * leading keyword fails explicitly.
856
1136
  */
857
1137
  export function compileStatement(sql) {
1138
+ validateSqlSource(sql);
858
1139
  const { text, offset } = normalizeSql(sql);
859
1140
  if (text.length === 0)
860
1141
  throw new SqlCompileError("Enter a SQL statement", offset, 0);
@@ -872,7 +1153,9 @@ export function compileStatement(sql) {
872
1153
  if (keyword === "BEGIN" ||
873
1154
  keyword === "START" ||
874
1155
  keyword === "COMMIT" ||
875
- keyword === "ROLLBACK") {
1156
+ keyword === "ROLLBACK" ||
1157
+ keyword === "SAVEPOINT" ||
1158
+ keyword === "RELEASE") {
876
1159
  return parseTransactionStatement(keyword, tokens);
877
1160
  }
878
1161
  if (keyword === "MERGE") {
@@ -892,18 +1175,62 @@ export function compileStatement(sql) {
892
1175
  if (keyword === "CREATE") {
893
1176
  if (isTriggerDdl)
894
1177
  return parseCreateTrigger(text, tokens);
1178
+ if (second?.kind === "identifier" && second.text.toUpperCase() === "TYPE") {
1179
+ const name = tokens[2];
1180
+ if (name?.kind !== "identifier")
1181
+ throw new TypeError("CREATE TYPE needs a name");
1182
+ const as = tokens[3];
1183
+ const enumToken = tokens[4];
1184
+ if (as?.kind !== "identifier" ||
1185
+ as.text.toUpperCase() !== "AS" ||
1186
+ enumToken?.kind !== "identifier" ||
1187
+ enumToken.text.toUpperCase() !== "ENUM" ||
1188
+ tokens[5]?.text !== "(") {
1189
+ throw new TypeError("CREATE TYPE supports AS ENUM (...)");
1190
+ }
1191
+ const values = [];
1192
+ let index = 6;
1193
+ for (;;) {
1194
+ const value = tokens[index];
1195
+ if (value?.kind !== "string")
1196
+ throw new TypeError("ENUM values must be string literals");
1197
+ values.push(value.text);
1198
+ index += 1;
1199
+ if (tokens[index]?.text === ")")
1200
+ break;
1201
+ if (tokens[index]?.text !== ",")
1202
+ throw new TypeError("Expected comma in ENUM values");
1203
+ index += 1;
1204
+ }
1205
+ if (tokens[index + 1]?.kind !== "eof")
1206
+ throw new TypeError("Unexpected input after ENUM");
1207
+ return { kind: "create-enum", name: name.text, values };
1208
+ }
1209
+ if (second?.kind === "identifier" && second.text.toUpperCase() === "SEQUENCE") {
1210
+ const name = tokens[2];
1211
+ if (name?.kind !== "identifier")
1212
+ throw new TypeError("CREATE SEQUENCE needs a name");
1213
+ if (tokens[3]?.kind !== "eof") {
1214
+ throw new TypeError("CREATE SEQUENCE options are not supported yet");
1215
+ }
1216
+ return { kind: "create-sequence", name: name.text };
1217
+ }
895
1218
  const viewAt = second?.text.toUpperCase() === "OR" ? 3 : 1;
896
1219
  if (tokens[viewAt]?.kind === "identifier" && tokens[viewAt].text.toUpperCase() === "VIEW") {
897
1220
  return parseCreateView(text, tokens, viewAt === 3);
898
1221
  }
899
1222
  parser = new Parser(tokens, text);
1223
+ if (second?.kind === "identifier" &&
1224
+ (second.text.toUpperCase() === "INDEX" || second.text.toUpperCase() === "UNIQUE")) {
1225
+ return parser.parseCreateIndex();
1226
+ }
900
1227
  const statement = parser.parseCreateTable();
901
1228
  if (parser.parameterCount > 0)
902
1229
  statement.parameterCount = parser.parameterCount;
903
1230
  return statement;
904
1231
  }
905
1232
  if (keyword === "ALTER") {
906
- parser = new Parser(tokens);
1233
+ parser = new Parser(tokens, text);
907
1234
  return parser.parseAlterTable();
908
1235
  }
909
1236
  if (keyword === "DROP") {
@@ -919,7 +1246,11 @@ export function compileStatement(sql) {
919
1246
  parser = new Parser(tokens);
920
1247
  return parser.parseDropTable();
921
1248
  }
922
- throw new TypeError("DROP supports: DROP TABLE name, DROP TRIGGER name");
1249
+ if (second?.kind === "identifier" && second.text.toUpperCase() === "INDEX") {
1250
+ parser = new Parser(tokens);
1251
+ return parser.parseDropIndex();
1252
+ }
1253
+ throw new TypeError("DROP supports: DROP TABLE, DROP VIEW, DROP INDEX, DROP TRIGGER");
923
1254
  }
924
1255
  if (keyword === "WITH") {
925
1256
  parser = new Parser(tokens);
@@ -1168,7 +1499,7 @@ function validateParameters(count, params) {
1168
1499
  return;
1169
1500
  }
1170
1501
  if (value instanceof Date) {
1171
- if (!Number.isFinite(value.getTime())) {
1502
+ if (!Number.isFinite(dateMilliseconds(value))) {
1172
1503
  throw new TypeError(`Parameter ${label} must be a valid date`);
1173
1504
  }
1174
1505
  return;
@@ -1203,6 +1534,9 @@ function bindExpression(expression, values) {
1203
1534
  }
1204
1535
  if (expression.kind === "call") {
1205
1536
  expression.arguments = expression.arguments.map((argument) => bindExpression(argument, values));
1537
+ for (const order of expression.aggregateOrderBy ?? []) {
1538
+ order.expression = bindExpression(order.expression, values);
1539
+ }
1206
1540
  return expression;
1207
1541
  }
1208
1542
  if (expression.kind === "list") {
@@ -1287,7 +1621,7 @@ function cloneTreeValue(value) {
1287
1621
  if (typeof value !== "object" || value === null)
1288
1622
  return value;
1289
1623
  if (value instanceof Date)
1290
- return new Date(value.getTime());
1624
+ return copyDate(value);
1291
1625
  if (Array.isArray(value)) {
1292
1626
  const copy = new Array(value.length);
1293
1627
  for (let index = 0; index < value.length; index += 1)
@@ -1320,7 +1654,7 @@ export function bindPlanParameters(plan, params) {
1320
1654
  return clone;
1321
1655
  }
1322
1656
  function isParameterSlot(value) {
1323
- return typeof value === "object" && value !== null && !(value instanceof Date);
1657
+ return (typeof value === "object" && value !== null && !(value instanceof Date) && "parameter" in value);
1324
1658
  }
1325
1659
  /** The mutation-statement counterpart of bindPlanParameters; SELECTs bind through their plan. */
1326
1660
  export function bindStatementParameters(statement, params) {
@@ -1332,6 +1666,8 @@ export function bindStatementParameters(statement, params) {
1332
1666
  throw new TypeError("SELECT parameters bind through the query pipeline, not the statement");
1333
1667
  }
1334
1668
  if (statement.kind === "create-table" ||
1669
+ statement.kind === "create-enum" ||
1670
+ statement.kind === "create-sequence" ||
1335
1671
  statement.kind === "create-trigger" ||
1336
1672
  statement.kind === "drop-trigger") {
1337
1673
  throw new TypeError("DDL statements take no parameters");
@@ -1343,6 +1679,14 @@ export function bindStatementParameters(statement, params) {
1343
1679
  if (clone.query !== undefined)
1344
1680
  bindBlock(clone.query, values);
1345
1681
  clone.rows = clone.rows.map((row) => row.map((value) => (isParameterSlot(value) ? (values[value.parameter] ?? null) : value)));
1682
+ if (clone.onConflict?.assignments !== undefined) {
1683
+ for (const assignment of clone.onConflict.assignments) {
1684
+ assignment.expression = bindExpression(assignment.expression, values);
1685
+ }
1686
+ }
1687
+ if (clone.onConflict?.where !== undefined) {
1688
+ clone.onConflict.where = bindExpression(clone.onConflict.where, values);
1689
+ }
1346
1690
  return clone;
1347
1691
  }
1348
1692
  if (clone.kind === "drop-table" ||
@@ -1392,9 +1736,13 @@ export function bindStatementParameters(statement, params) {
1392
1736
  export function inferBlockSchema(plan, schemas) {
1393
1737
  const sources = [plan.base, ...plan.joins];
1394
1738
  const multipleSources = sources.length > 1;
1395
- const wildcardSchema = (source) => (schemas.get(source.table) ?? []).map((column) => ({
1739
+ const wildcardSchema = (source) => (schemas.get(source.table) ?? [])
1740
+ .filter((column) => !column.name.startsWith("\0"))
1741
+ .map((column) => ({
1396
1742
  name: multipleSources ? `${source.alias}.${column.name}` : column.name,
1397
1743
  type: column.type,
1744
+ ...(column.integer === true ? { integer: true } : {}),
1745
+ ...(column.sqlDomain === undefined ? {} : { sqlDomain: column.sqlDomain }),
1398
1746
  }));
1399
1747
  if (plan.select[0]?.expression.kind === "wildcard" &&
1400
1748
  plan.select[0].expression.table === undefined)
@@ -1417,6 +1765,84 @@ export function inferBlockSchema(plan, schemas) {
1417
1765
  throw new TypeError(`Ambiguous or missing column: ${reference}`);
1418
1766
  return matches[0] ?? "string";
1419
1767
  };
1768
+ const resolveColumnInteger = (reference) => {
1769
+ const parts = reference.split(".");
1770
+ if (parts.length === 2) {
1771
+ const source = sources.find(({ alias }) => alias === parts[0]);
1772
+ return (source !== undefined &&
1773
+ (schemas.get(source.table) ?? []).find(({ name }) => name === parts[1])?.integer === true);
1774
+ }
1775
+ const matches = sources.flatMap((source) => (schemas.get(source.table) ?? []).filter(({ name }) => name === parts[0]));
1776
+ return matches.length === 1 && matches[0]?.integer === true;
1777
+ };
1778
+ const resolveColumnDomain = (reference) => {
1779
+ const parts = reference.split(".");
1780
+ if (parts.length === 2) {
1781
+ const source = sources.find(({ alias }) => alias === parts[0]);
1782
+ return source === undefined
1783
+ ? undefined
1784
+ : (schemas.get(source.table) ?? []).find(({ name }) => name === parts[1])?.sqlDomain;
1785
+ }
1786
+ const matches = sources.flatMap((source) => (schemas.get(source.table) ?? []).filter(({ name }) => name === parts[0]));
1787
+ return matches.length === 1 ? matches[0]?.sqlDomain : undefined;
1788
+ };
1789
+ const castDomain = (target) => {
1790
+ if (target.startsWith("numeric:")) {
1791
+ const [, precisionWord = "", scaleWord = ""] = target.split(":");
1792
+ const precision = precisionWord === "" ? undefined : Number(precisionWord);
1793
+ const scale = scaleWord === "" ? undefined : Number(scaleWord);
1794
+ return {
1795
+ kind: "numeric",
1796
+ ...(precision === undefined ? {} : { precision }),
1797
+ ...(scale === undefined ? {} : { scale }),
1798
+ };
1799
+ }
1800
+ if (target === "json" || target === "jsonb" || target === "uuid" || target === "time") {
1801
+ return { kind: target };
1802
+ }
1803
+ if (target === "interval")
1804
+ return { kind: "interval" };
1805
+ return undefined;
1806
+ };
1807
+ const inferDomain = (expression) => {
1808
+ if (expression.kind === "literal")
1809
+ return expression.sqlDomain;
1810
+ if (expression.kind === "column")
1811
+ return resolveColumnDomain(expression.reference);
1812
+ if (expression.kind === "binary") {
1813
+ const left = inferDomain(expression.left);
1814
+ const right = inferDomain(expression.right);
1815
+ return left?.kind === "numeric" || right?.kind === "numeric"
1816
+ ? { kind: "numeric" }
1817
+ : undefined;
1818
+ }
1819
+ if (expression.kind === "case") {
1820
+ const outcomes = [
1821
+ ...expression.branches.map((branch) => branch.then),
1822
+ ...(expression.otherwise === undefined ? [] : [expression.otherwise]),
1823
+ ];
1824
+ return outcomes.map(inferDomain).find((domain) => domain !== undefined);
1825
+ }
1826
+ if (expression.kind !== "call")
1827
+ return undefined;
1828
+ if (expression.name === "CAST") {
1829
+ const target = expression.arguments[1];
1830
+ return target?.kind === "literal" && typeof target.value === "string"
1831
+ ? castDomain(target.value)
1832
+ : undefined;
1833
+ }
1834
+ if (expression.name === "SUM" ||
1835
+ expression.name === "AVG" ||
1836
+ expression.name === "MIN" ||
1837
+ expression.name === "MAX" ||
1838
+ expression.name === "COALESCE" ||
1839
+ expression.name === "NULLIF" ||
1840
+ expression.name === "GREATEST" ||
1841
+ expression.name === "LEAST") {
1842
+ return expression.arguments.map(inferDomain).find((domain) => domain !== undefined);
1843
+ }
1844
+ return undefined;
1845
+ };
1420
1846
  const infer = (expression) => {
1421
1847
  if (expression.kind === "subquery" || expression.kind === "list") {
1422
1848
  throw new TypeError("Subqueries must be resolved before schema inference");
@@ -1489,17 +1915,21 @@ export function inferBlockSchema(plan, schemas) {
1489
1915
  }
1490
1916
  return "string";
1491
1917
  }
1918
+ const exactNumeric = inferDomain(expression)?.kind === "numeric";
1492
1919
  for (const side of [expression.left, expression.right]) {
1493
1920
  const type = infer(side);
1494
- if (type === "string" || type === "boolean") {
1921
+ if (type === "boolean" || (type === "string" && inferDomain(side)?.kind !== "numeric")) {
1495
1922
  throw new TypeError(`Arithmetic requires numeric operands: ${plan.sql}`);
1496
1923
  }
1497
1924
  }
1498
- return "number";
1925
+ return exactNumeric ? "string" : "number";
1499
1926
  }
1500
- if (expression.name === "COUNT" || expression.name === "SUM" || expression.name === "AVG") {
1927
+ if (expression.name === "COUNT") {
1501
1928
  return "number";
1502
1929
  }
1930
+ if (expression.name === "SUM" || expression.name === "AVG") {
1931
+ return inferDomain(expression)?.kind === "numeric" ? "string" : "number";
1932
+ }
1503
1933
  if (expression.name === "ROUND" ||
1504
1934
  expression.name === "LENGTH" ||
1505
1935
  expression.name === "ABS" ||
@@ -1511,13 +1941,22 @@ export function inferBlockSchema(plan, schemas) {
1511
1941
  expression.name === "INSTR" ||
1512
1942
  expression.name === "EXTRACT" ||
1513
1943
  expression.name === "OCTET_LENGTH" ||
1514
- expression.name === "GROUPING") {
1944
+ expression.name === "GROUPING" ||
1945
+ expression.name === "NEXTVAL" ||
1946
+ expression.name === "CURRVAL" ||
1947
+ expression.name === "RANDOM") {
1515
1948
  return "number";
1516
1949
  }
1517
- if (expression.name === "JSON_VALUE" ||
1950
+ if (expression.name === "JSON_ARRAYAGG" ||
1951
+ expression.name === "STRING_AGG" ||
1952
+ expression.name === "JSON_VALUE" ||
1518
1953
  expression.name === "JSON_QUERY" ||
1519
1954
  expression.name === "JSON_OBJECT" ||
1520
- expression.name === "JSON_ARRAY") {
1955
+ expression.name === "JSON_ARRAY" ||
1956
+ expression.name === "ARRAY" ||
1957
+ expression.name === "MINNOW_TUPLE_KEY" ||
1958
+ expression.name === "MINNOW_COLLATE" ||
1959
+ expression.name === "GEN_RANDOM_UUID") {
1521
1960
  return "string";
1522
1961
  }
1523
1962
  if (expression.name === "JSON_EXISTS" || expression.name === "IS_JSON")
@@ -1527,8 +1966,8 @@ export function inferBlockSchema(plan, schemas) {
1527
1966
  if (expression.name === "CURRENT_DATE" || expression.name === "CURRENT_TIMESTAMP") {
1528
1967
  return "datetime";
1529
1968
  }
1530
- // The engine has no TIME type, so LOCALTIME reads as an 'HH:MM:SS' string, like SQLite's
1531
- // CURRENT_TIME.
1969
+ // LOCALTIME is the statement clock's canonical 'HH:MM:SS' text; explicit TIME values use
1970
+ // the same public representation while carrying a logical domain internally.
1532
1971
  if (expression.name === "LOCALTIME")
1533
1972
  return "string";
1534
1973
  if (expression.name === "NULLIF" ||
@@ -1552,6 +1991,8 @@ export function inferBlockSchema(plan, schemas) {
1552
1991
  const word = target?.kind === "literal" && typeof target.value === "string" ? target.value : "";
1553
1992
  if (word === "number-integer")
1554
1993
  return "number";
1994
+ if (castDomain(word) !== undefined)
1995
+ return "string";
1555
1996
  if (word === "number" || word === "string" || word === "boolean" || word === "datetime") {
1556
1997
  return word;
1557
1998
  }
@@ -1604,9 +2045,60 @@ export function inferBlockSchema(plan, schemas) {
1604
2045
  if (type === "null") {
1605
2046
  throw new TypeError(`Cannot infer a column type for output ${item.alias}`);
1606
2047
  }
1607
- return [{ name: item.alias, type }];
2048
+ const integer = (item.expression.kind === "column" && resolveColumnInteger(item.expression.reference)) ||
2049
+ (item.expression.kind === "call" &&
2050
+ item.expression.name === "CAST" &&
2051
+ item.expression.arguments[1]?.kind === "literal" &&
2052
+ item.expression.arguments[1].value === "number-integer") ||
2053
+ (item.expression.kind === "call" && item.expression.name === "COUNT");
2054
+ const sqlDomain = inferDomain(item.expression);
2055
+ return [
2056
+ {
2057
+ name: item.alias,
2058
+ type,
2059
+ ...(integer ? { integer: true } : {}),
2060
+ ...(sqlDomain === undefined ? {} : { sqlDomain }),
2061
+ },
2062
+ ];
1608
2063
  });
1609
2064
  }
2065
+ /** Whether a final result can contain one of the engine's tagged string-domain values. */
2066
+ export function queryResultNeedsExternalization(plan, schemas) {
2067
+ // This is only a result-boundary optimization flag, not a second typecheck. In particular,
2068
+ // PostgreSQL permits an untyped NULL output and Minnow historically permits heterogeneous CASE
2069
+ // results, so calling inferBlockSchema here would make execution stricter just because tagged
2070
+ // domains exist elsewhere in the engine. Be conservative instead: externalization is a cheap
2071
+ // prefix check, and only domain-bearing schemas or expressions pay it.
2072
+ const createsDomain = (expression) => {
2073
+ if (expression.kind === "literal" && expression.internalSqlValue === true)
2074
+ return true;
2075
+ if (expression.kind === "call") {
2076
+ if (expression.name === "ARRAY" || expression.name === "MINNOW_COLLATE")
2077
+ return true;
2078
+ if (expression.name === "CAST") {
2079
+ const target = expression.arguments[1];
2080
+ if (target?.kind === "literal" &&
2081
+ typeof target.value === "string" &&
2082
+ (target.value.startsWith("numeric:") ||
2083
+ ["json", "jsonb", "uuid", "time", "interval"].includes(target.value))) {
2084
+ return true;
2085
+ }
2086
+ }
2087
+ }
2088
+ return childExpressions(expression).some(createsDomain);
2089
+ };
2090
+ try {
2091
+ // Plain TEXT uses a protected execution-time wrapper when it happens to share the internal
2092
+ // domain namespace, so string outputs cross the same boundary as logical SQL domains.
2093
+ return inferBlockSchema(plan, schemas).some((column) => column.type === "string" || column.sqlDomain !== undefined);
2094
+ }
2095
+ catch {
2096
+ // This flag must not make execution stricter for historically accepted shapes such as an
2097
+ // untyped NULL or heterogeneous CASE. When exact inference cannot answer, scan if any source
2098
+ // or expression could carry an internal string; externalization itself remains copy-on-change.
2099
+ return ([...schemas.values()].some((columns) => columns.some((column) => column.type === "string" || column.sqlDomain !== undefined)) || plan.select.some(({ expression }) => createsDomain(expression)));
2100
+ }
2101
+ }
1610
2102
  export function referencedColumns(plan, schemas) {
1611
2103
  const sources = [plan.base, ...plan.joins];
1612
2104
  const sourceAliases = sources.map((source) => source.alias);
@@ -1656,8 +2148,13 @@ export function referencedColumns(plan, schemas) {
1656
2148
  for (const source of sources) {
1657
2149
  if (named !== undefined && source.alias !== named)
1658
2150
  continue;
1659
- for (const column of schemas.get(source.table) ?? [])
2151
+ for (const column of schemas.get(source.table) ?? []) {
2152
+ // Storage may carry unspellable NUL-prefixed locator columns for composite identity.
2153
+ // Explicit internal references can request one; SQL wildcards are public schema only.
2154
+ if (column.startsWith("\u0000"))
2155
+ continue;
1660
2156
  requested.get(source.table)?.add(column);
2157
+ }
1661
2158
  }
1662
2159
  }
1663
2160
  };
@@ -1729,6 +2226,7 @@ function trimPreparedResults(prepared, trim) {
1729
2226
  },
1730
2227
  execute: () => trim(prepared.execute()),
1731
2228
  executeAsync: async (options) => trim(await prepared.executeAsync(options)),
2229
+ executeBatches: (options, consume) => prepared.executeBatches(options, (batch) => consume(trim(batch))),
1732
2230
  close: () => {
1733
2231
  prepared.close();
1734
2232
  },
@@ -1746,25 +2244,33 @@ export function createPreparedQuery(plan, tables, options = {}) {
1746
2244
  const ties = withTiesPlan(plan);
1747
2245
  if (ties.plan !== plan)
1748
2246
  return trimPreparedResults(createPreparedQuery(ties.plan, tables, options), ties.trim);
2247
+ // Rows passed to this low-level public entry point are public SQL values. Escape the private
2248
+ // string-domain namespace before derived blocks add their own already-internal results. This
2249
+ // comes after WITH TIES lowering because that lowering recursively prepares the same input.
2250
+ tables = cloneRowTables(tables, true);
1749
2251
  // Every engine entry expands MATCH(*) exactly once, here against the row tables' own
1750
2252
  // columns; past this point no executor sees the "*" sentinel.
1751
2253
  plan = expandFtsColumns(plan, rowTableSearchableColumns(tables));
1752
- const resolution = subqueryResolutionSteps(plan);
1753
- for (const step of resolution.steps)
1754
- step.substitute(executeRowQuery(step.block, tables));
1755
- plan = resolution.plan;
1756
- tables = resolveDerivedRowTables(plan, tables);
1757
- plan = expandDistinctWildcard(plan, wildcardRowColumns(tables));
1758
2254
  const memory = new QueryMemoryContext(options.executionMemoryBudgetBytes);
1759
- if ([...tables.values()].some((rows) => rows.length === 0)) {
1760
- if (options.executionMemoryBudgetBytes !== undefined) {
1761
- memory.close();
1762
- throw new TypeError("Query memory budgets require typed columnar schemas when an input table is empty");
1763
- }
1764
- return createPreparedRowQuery(plan, tables, memory);
1765
- }
1766
2255
  try {
1767
- return createPreparedColumnarQuery(plan, normalizeColumnarTables(plan, tables), memory);
2256
+ // Scalar subqueries and derived row sources are materialized before the vector plan is
2257
+ // prepared. They must share the same context: otherwise a tiny (or the finite default)
2258
+ // budget would apply only after an arbitrarily large intermediate had already escaped the
2259
+ // memory model.
2260
+ const resolution = subqueryResolutionSteps(plan);
2261
+ for (const step of resolution.steps) {
2262
+ step.substitute(executeRowQueryInternal(step.block, tables, memory));
2263
+ }
2264
+ plan = resolution.plan;
2265
+ tables = resolveDerivedRowTables(plan, tables, memory);
2266
+ plan = expandDistinctWildcard(plan, wildcardRowColumns(tables));
2267
+ if ([...tables.values()].some((rows) => rows.length === 0)) {
2268
+ if (options.executionMemoryBudgetBytes !== undefined) {
2269
+ throw new TypeError("Query memory budgets require typed columnar schemas when an input table is empty");
2270
+ }
2271
+ return createPreparedRowQuery(plan, tables, memory);
2272
+ }
2273
+ return trimPreparedResults(createPreparedColumnarQuery(plan, normalizeColumnarTables(plan, tables), memory), externalizeQueryResult);
1768
2274
  }
1769
2275
  catch (error) {
1770
2276
  memory.close();
@@ -1799,6 +2305,7 @@ export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemo
1799
2305
  memory.close();
1800
2306
  throw error;
1801
2307
  }
2308
+ const outputNeedsExternalization = options.outputNeedsExternalization;
1802
2309
  return {
1803
2310
  sql: plan.sql,
1804
2311
  tables: [plan.base.table, ...plan.joins.map((join) => join.table)],
@@ -1808,12 +2315,17 @@ export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemo
1808
2315
  execute() {
1809
2316
  if (closed || prepared === undefined)
1810
2317
  throw new Error("Prepared query is closed");
1811
- return prepared.execute();
2318
+ return markExternalizationState(prepared.execute(), outputNeedsExternalization);
1812
2319
  },
1813
2320
  async executeAsync(options) {
1814
2321
  if (closed || prepared === undefined)
1815
2322
  throw new Error("Prepared query is closed");
1816
- return prepared.executeAsync(options);
2323
+ return markExternalizationState(await prepared.executeAsync(options), outputNeedsExternalization);
2324
+ },
2325
+ executeBatches(options, consume) {
2326
+ if (closed || prepared === undefined)
2327
+ throw new Error("Prepared query is closed");
2328
+ return prepared.executeBatches(options, (batch) => consume(markExternalizationState(batch, outputNeedsExternalization)));
1817
2329
  },
1818
2330
  close() {
1819
2331
  if (closed)
@@ -1837,12 +2349,47 @@ function createPreparedRowQuery(plan, tables, memory) {
1837
2349
  execute() {
1838
2350
  if (closed || rows === undefined)
1839
2351
  throw new Error("Prepared query is closed");
1840
- return executeRowQuery(plan, cloneRowTables(rows));
2352
+ const executionMemory = memory.createChild();
2353
+ try {
2354
+ return externalizeQueryResult(executeRowQueryInternal(plan, cloneRowTables(rows), executionMemory));
2355
+ }
2356
+ finally {
2357
+ executionMemory.close();
2358
+ }
1841
2359
  },
1842
2360
  async executeAsync() {
1843
2361
  if (closed || rows === undefined)
1844
2362
  throw new Error("Prepared query is closed");
1845
- return executeRowQuery(plan, cloneRowTables(rows));
2363
+ const executionMemory = memory.createChild();
2364
+ try {
2365
+ return externalizeQueryResult(executeRowQueryInternal(plan, cloneRowTables(rows), executionMemory));
2366
+ }
2367
+ finally {
2368
+ executionMemory.close();
2369
+ }
2370
+ },
2371
+ async executeBatches(options, consume) {
2372
+ if (closed || rows === undefined)
2373
+ throw new Error("Prepared query is closed");
2374
+ if (!Number.isSafeInteger(options.batchRows) || options.batchRows <= 0) {
2375
+ throw new RangeError("Query batch rows must be a positive whole number");
2376
+ }
2377
+ const executionMemory = memory.createChild();
2378
+ let result;
2379
+ try {
2380
+ result = externalizeQueryResult(executeRowQueryInternal(plan, cloneRowTables(rows), executionMemory));
2381
+ }
2382
+ finally {
2383
+ executionMemory.close();
2384
+ }
2385
+ for (let start = 0; start < result.rows.length; start += options.batchRows) {
2386
+ options.signal?.throwIfAborted();
2387
+ await consume({
2388
+ columns: [...result.columns],
2389
+ rows: result.rows.slice(start, start + options.batchRows),
2390
+ });
2391
+ }
2392
+ return result.columns;
1846
2393
  },
1847
2394
  close() {
1848
2395
  closed = true;
@@ -1851,19 +2398,23 @@ function createPreparedRowQuery(plan, tables, memory) {
1851
2398
  },
1852
2399
  };
1853
2400
  }
1854
- function cloneRowTables(tables) {
2401
+ function cloneRowTables(tables, protectText = false) {
1855
2402
  return new Map([...tables].map(([name, rows]) => [
1856
2403
  name,
1857
2404
  rows.map((row) => Object.fromEntries(Object.entries(row).map(([column, value]) => [
1858
2405
  column,
1859
- value instanceof Date ? new Date(value.getTime()) : value,
2406
+ value instanceof Date
2407
+ ? copyDate(value)
2408
+ : protectText && typeof value === "string"
2409
+ ? protectedSqlTextValue(value)
2410
+ : value,
1860
2411
  ]))),
1861
2412
  ]));
1862
2413
  }
1863
2414
  export function executeQuery(plan, tables, options = {}) {
1864
2415
  const prepared = createPreparedQuery(plan, tables, options);
1865
2416
  try {
1866
- return prepared.execute();
2417
+ return externalizeQueryResult(prepared.execute());
1867
2418
  }
1868
2419
  finally {
1869
2420
  prepared.close();
@@ -1993,8 +2544,12 @@ export function createRecursiveCteState(base, all) {
1993
2544
  export function childExpressions(expression) {
1994
2545
  if (expression.kind === "binary")
1995
2546
  return [expression.left, expression.right];
1996
- if (expression.kind === "call")
1997
- return [...expression.arguments];
2547
+ if (expression.kind === "call") {
2548
+ return [
2549
+ ...expression.arguments,
2550
+ ...(expression.aggregateOrderBy ?? []).map((order) => order.expression),
2551
+ ];
2552
+ }
1998
2553
  if (expression.kind === "list")
1999
2554
  return [...expression.items];
2000
2555
  if (expression.kind === "condition" || expression.kind === "logical") {
@@ -2035,7 +2590,18 @@ export function mapChildExpressions(expression, map) {
2035
2590
  if (expression.kind === "not")
2036
2591
  return { ...expression, operand: map(expression.operand) };
2037
2592
  if (expression.kind === "call") {
2038
- return { ...expression, arguments: expression.arguments.map(map) };
2593
+ return {
2594
+ ...expression,
2595
+ arguments: expression.arguments.map(map),
2596
+ ...(expression.aggregateOrderBy === undefined
2597
+ ? {}
2598
+ : {
2599
+ aggregateOrderBy: expression.aggregateOrderBy.map((order) => ({
2600
+ ...order,
2601
+ expression: map(order.expression),
2602
+ })),
2603
+ }),
2604
+ };
2039
2605
  }
2040
2606
  if (expression.kind === "list")
2041
2607
  return { ...expression, items: expression.items.map(map) };
@@ -2119,10 +2685,10 @@ export function mapBlockExpressions(block, map) {
2119
2685
  export function resolveStatementDatetimes(plan, now = new Date()) {
2120
2686
  if (plan.usesStatementDatetime !== true)
2121
2687
  return plan;
2122
- const iso = now.toISOString();
2688
+ const iso = dateIsoString(now);
2123
2689
  const values = new Map([
2124
2690
  ["CURRENT_DATE", new Date(`${iso.slice(0, 10)}T00:00:00.000Z`)],
2125
- ["CURRENT_TIMESTAMP", new Date(now.getTime())],
2691
+ ["CURRENT_TIMESTAMP", copyDate(now)],
2126
2692
  ["LOCALTIME", iso.slice(11, 19)],
2127
2693
  ]);
2128
2694
  const resolved = clonePlanTree(plan);
@@ -2284,6 +2850,21 @@ function aggregateWindowMembers(window, values, members) {
2284
2850
  if (present.length === 0)
2285
2851
  return null;
2286
2852
  if (window.name === "SUM" || window.name === "AVG") {
2853
+ const exact = present.some((member) => isExactNumeric(values[member]));
2854
+ if (exact) {
2855
+ let total = exactNumericValue(0);
2856
+ for (const member of present) {
2857
+ const value = values[member];
2858
+ if (!isExactNumeric(value)) {
2859
+ throw new TypeError("Exact NUMERIC window input mixed with an approximate number");
2860
+ }
2861
+ const next = exactNumericBinary("+", total, value);
2862
+ if (next === null || next === undefined)
2863
+ throw new Error("Exact NUMERIC sum disappeared");
2864
+ total = next;
2865
+ }
2866
+ return window.name === "SUM" ? total : exactNumericBinary("/", total, present.length);
2867
+ }
2287
2868
  const total = present.reduce((sum, member) => sum + numeric(values[member]), 0);
2288
2869
  return window.name === "SUM" ? total : total / present.length;
2289
2870
  }
@@ -2342,17 +2923,39 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
2342
2923
  }
2343
2924
  }
2344
2925
  const values = [];
2345
- const prefixNonNull = new Float64Array(size + 1);
2346
- const prefixSum = new Float64Array(size + 1);
2347
2926
  const sums = window.name === "SUM" || window.name === "AVG";
2348
2927
  for (let position = 0; position < size; position += 1) {
2349
2928
  const value = window.argumentAlias === undefined
2350
2929
  ? undefined
2351
2930
  : (rows[indexes[start + position] ?? -1]?.[window.argumentAlias] ?? null);
2352
2931
  values.push(value);
2932
+ }
2933
+ const exactSums = sums && values.some((value) => isExactNumeric(value));
2934
+ const prefixNonNull = new Float64Array(size + 1);
2935
+ const prefixSum = exactSums ? undefined : new Float64Array(size + 1);
2936
+ const prefixExact = exactSums ? new Array(size + 1) : undefined;
2937
+ const exactZero = exactNumericValue(0);
2938
+ if (exactZero === null)
2939
+ throw new Error("Exact NUMERIC zero disappeared");
2940
+ if (prefixExact !== undefined)
2941
+ prefixExact[0] = exactZero;
2942
+ for (let position = 0; position < size; position += 1) {
2943
+ const value = values[position];
2353
2944
  const nonNull = window.argumentAlias !== undefined && value !== null && value !== undefined;
2354
2945
  prefixNonNull[position + 1] = (prefixNonNull[position] ?? 0) + (nonNull ? 1 : 0);
2355
- prefixSum[position + 1] = (prefixSum[position] ?? 0) + (sums && nonNull ? numeric(value) : 0);
2946
+ if (prefixExact !== undefined) {
2947
+ if (nonNull && !isExactNumeric(value)) {
2948
+ throw new TypeError("Exact NUMERIC window input mixed with an approximate number");
2949
+ }
2950
+ const previous = prefixExact[position] ?? exactZero;
2951
+ const next = nonNull ? exactNumericBinary("+", previous, value) : previous;
2952
+ if (next === null || next === undefined)
2953
+ throw new Error("Exact NUMERIC sum disappeared");
2954
+ prefixExact[position + 1] = next;
2955
+ }
2956
+ else if (prefixSum !== undefined) {
2957
+ prefixSum[position + 1] = (prefixSum[position] ?? 0) + (sums && nonNull ? numeric(value) : 0);
2958
+ }
2356
2959
  }
2357
2960
  // GROUPS frames count peer groups rather than rows, so each position needs its group's
2358
2961
  // ordinal and the group boundaries to translate an offset back into row positions.
@@ -2459,8 +3062,17 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
2459
3062
  }
2460
3063
  else if (sums) {
2461
3064
  const nonNull = (prefixNonNull[high] ?? 0) - (prefixNonNull[low] ?? 0);
2462
- const total = (prefixSum[high] ?? 0) - (prefixSum[low] ?? 0);
2463
- value = nonNull === 0 ? null : window.name === "SUM" ? total : total / nonNull;
3065
+ if (nonNull === 0) {
3066
+ value = null;
3067
+ }
3068
+ else if (prefixExact !== undefined) {
3069
+ const total = exactNumericBinary("-", prefixExact[high] ?? exactZero, prefixExact[low] ?? exactZero);
3070
+ value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull);
3071
+ }
3072
+ else {
3073
+ const total = (prefixSum?.[high] ?? 0) - (prefixSum?.[low] ?? 0);
3074
+ value = window.name === "SUM" ? total : total / nonNull;
3075
+ }
2464
3076
  }
2465
3077
  else {
2466
3078
  let best;
@@ -2665,7 +3277,7 @@ export function applyWindowFunctions(result, windows, options = {}) {
2665
3277
  };
2666
3278
  }
2667
3279
  /** Executes each derived or set-operation source with the row reference. */
2668
- function resolveDerivedRowTables(plan, tables) {
3280
+ function resolveDerivedRowTables(plan, tables, memory) {
2669
3281
  const sources = [plan.base, ...plan.joins];
2670
3282
  const needsDual = sources.some((source) => source.derived === undefined && source.table === DUAL_TABLE);
2671
3283
  if (!needsDual &&
@@ -2681,28 +3293,28 @@ function resolveDerivedRowTables(plan, tables) {
2681
3293
  for (const source of sources) {
2682
3294
  if (source.recursive !== undefined) {
2683
3295
  const { reference, base, step, all } = source.recursive;
2684
- const state = createRecursiveCteState(executeRowQuery(base, tables), all);
3296
+ const state = createRecursiveCteState(executeRowQueryInternal(base, tables, memory), all);
2685
3297
  while (state.frontier.length > 0) {
2686
3298
  const stepTables = new Map(tables);
2687
3299
  stepTables.set(reference, state.frontier);
2688
- state.absorb(executeRowQuery(step, stepTables));
3300
+ state.absorb(executeRowQueryInternal(step, stepTables, memory));
2689
3301
  }
2690
3302
  resolved.set(source.table, state.rows);
2691
3303
  continue;
2692
3304
  }
2693
3305
  if (source.union !== undefined) {
2694
- const results = source.union.blocks.map((block) => executeRowQuery(block, tables));
3306
+ const results = source.union.blocks.map((block) => executeRowQueryInternal(block, tables, memory));
2695
3307
  resolved.set(source.table, combineUnionResults(results, source.union.ops).rows);
2696
3308
  continue;
2697
3309
  }
2698
3310
  if (source.windowed !== undefined) {
2699
- const inner = executeRowQuery(source.windowed.block, tables);
3311
+ const inner = executeRowQueryInternal(source.windowed.block, tables, memory);
2700
3312
  resolved.set(source.table, applyWindowFunctions(inner, source.windowed.windows).rows);
2701
3313
  continue;
2702
3314
  }
2703
3315
  if (source.derived === undefined)
2704
3316
  continue;
2705
- resolved.set(source.table, executeRowQuery(source.derived, tables).rows);
3317
+ resolved.set(source.table, executeRowQueryInternal(source.derived, tables, memory).rows);
2706
3318
  }
2707
3319
  return resolved;
2708
3320
  }
@@ -2717,11 +3329,14 @@ function normalizeColumnarTables(plan, tables) {
2717
3329
  .filter(([name]) => requiredTables.has(name))
2718
3330
  .map(([name, table]) => [
2719
3331
  name,
2720
- columnarTableFromRows(name, table, requestedColumns.get(name) ?? []),
3332
+ columnarTableFromRows(name, table, requestedColumns.get(name) ?? [], false),
2721
3333
  ]));
2722
3334
  }
2723
3335
  /** Correctness reference retained while the vector executor matures. */
2724
3336
  export function executeRowQuery(plan, tables) {
3337
+ return externalizeQueryResult(executeRowQueryInternal(plan, cloneRowTables(tables, true)));
3338
+ }
3339
+ function executeRowQueryInternal(plan, tables, memory) {
2725
3340
  assertTailParametersBound(plan);
2726
3341
  validateGrouping(plan);
2727
3342
  plan = resolveStatementDatetimes(plan);
@@ -2732,8 +3347,9 @@ export function executeRowQuery(plan, tables) {
2732
3347
  plan = ties.plan;
2733
3348
  plan = expandFtsColumns(plan, rowTableSearchableColumns(tables));
2734
3349
  const resolution = subqueryResolutionSteps(plan);
2735
- for (const step of resolution.steps)
2736
- step.substitute(executeRowQuery(step.block, tables));
3350
+ for (const step of resolution.steps) {
3351
+ step.substitute(executeRowQueryInternal(step.block, tables, memory));
3352
+ }
2737
3353
  // Stats annotation writes into the plan; when resolution had nothing to clone, the plan is
2738
3354
  // still the caller's (possibly cached) object, and frozen statistics would survive into later
2739
3355
  // executions against different rows.
@@ -2741,7 +3357,7 @@ export function executeRowQuery(plan, tables) {
2741
3357
  resolution.plan === plan && planContainsFts(plan, "bm25")
2742
3358
  ? structuredClone(plan)
2743
3359
  : resolution.plan;
2744
- tables = resolveDerivedRowTables(plan, tables);
3360
+ tables = resolveDerivedRowTables(plan, tables, memory);
2745
3361
  plan = expandDistinctWildcard(plan, wildcardRowColumns(tables));
2746
3362
  annotateRowFtsStats(plan, tables);
2747
3363
  let contexts = (tables.get(plan.base.table) ?? []).map((row) => ({
@@ -2762,17 +3378,28 @@ export function executeRowQuery(plan, tables) {
2762
3378
  group.push(context);
2763
3379
  groups.set(key, group);
2764
3380
  }
2765
- rows = [...groups.values()]
2766
- .filter((group) => plan.having.every((predicate) => evaluateBooleanExpression({
2767
- kind: "condition",
2768
- operator: predicate.operator,
2769
- left: predicate.left,
2770
- right: predicate.right,
2771
- }, (nested) => evaluate(nested, group[0] ?? {}, group)) === true))
2772
- .map((group) => project(plan.select, group[0] ?? {}, group));
3381
+ rows = [];
3382
+ for (const group of groups.values()) {
3383
+ if (!plan.having.every((predicate) => evaluateBooleanExpression({
3384
+ kind: "condition",
3385
+ operator: predicate.operator,
3386
+ left: predicate.left,
3387
+ right: predicate.right,
3388
+ }, (nested) => evaluate(nested, group[0] ?? {}, group)) === true)) {
3389
+ continue;
3390
+ }
3391
+ const row = project(plan.select, group[0] ?? {}, group);
3392
+ memory?.tally(rowQueryPayloadBytes(row), "Row query result");
3393
+ rows.push(row);
3394
+ }
2773
3395
  }
2774
3396
  else {
2775
- rows = contexts.map((context) => project(plan.select, context));
3397
+ rows = [];
3398
+ for (const context of contexts) {
3399
+ const row = project(plan.select, context);
3400
+ memory?.tally(rowQueryPayloadBytes(row), "Row query result");
3401
+ rows.push(row);
3402
+ }
2776
3403
  }
2777
3404
  if (plan.orderBy.length > 0) {
2778
3405
  // Only a wildcard select needs the source shapes: every other select resolves against its
@@ -2785,12 +3412,13 @@ export function executeRowQuery(plan, tables) {
2785
3412
  : [];
2786
3413
  const sortColumns = plan.orderBy.map(({ expression, direction, nulls }) => ({
2787
3414
  outputName: orderOutputName(expression, plan.select, orderSources),
3415
+ direction,
2788
3416
  multiplier: direction === "desc" ? -1 : 1,
2789
3417
  nulls,
2790
3418
  }));
2791
3419
  rows.sort((left, right) => {
2792
- for (const { outputName, multiplier, nulls } of sortColumns) {
2793
- const placed = explicitNullOrder(left[outputName], right[outputName], nulls);
3420
+ for (const { outputName, direction, multiplier, nulls } of sortColumns) {
3421
+ const placed = nullOrder(left[outputName], right[outputName], nulls, direction);
2794
3422
  if (placed !== undefined && placed !== 0)
2795
3423
  return placed;
2796
3424
  const comparison = compareValues(left[outputName], right[outputName]);
@@ -2809,6 +3437,25 @@ export function executeRowQuery(plan, tables) {
2809
3437
  : plan.select.map((item) => item.alias);
2810
3438
  return ties.trim({ columns, rows });
2811
3439
  }
3440
+ /** Matches the vector executor's deliberately inexpensive modeled result-payload accounting. */
3441
+ function rowQueryPayloadBytes(row) {
3442
+ let total = 8;
3443
+ for (const key in row) {
3444
+ const value = row[key] ?? null;
3445
+ total +=
3446
+ value === null
3447
+ ? 1
3448
+ : typeof value === "boolean"
3449
+ ? 2
3450
+ : typeof value === "number" || value instanceof Date
3451
+ ? 9
3452
+ : 1 + value.length;
3453
+ }
3454
+ if (!Number.isSafeInteger(total) || total < 0) {
3455
+ throw new RangeError("Row query result payload exceeds the safe integer range");
3456
+ }
3457
+ return total;
3458
+ }
2812
3459
  /**
2813
3460
  * Resolves one ORDER BY reference to the output column that carries its values, throwing when
2814
3461
  * nothing matches. Dropping an unresolved sort key silently would return rows in an arbitrary
@@ -2876,11 +3523,15 @@ function executeJoin(contexts, join, rows) {
2876
3523
  const candidate = { ...context, [join.alias]: row };
2877
3524
  if (evaluateBooleanExpression(condition, (nested) => evaluate(nested, candidate)) === true) {
2878
3525
  matched = true;
2879
- joined.push(candidate);
3526
+ if (join.kind !== "anti")
3527
+ joined.push(join.kind === "semi" ? context : candidate);
3528
+ if (join.kind === "semi" || join.kind === "anti")
3529
+ break;
2880
3530
  }
2881
3531
  }
2882
- if (!matched && join.kind === "left")
2883
- joined.push({ ...context, [join.alias]: undefined });
3532
+ if (!matched && (join.kind === "left" || join.kind === "anti")) {
3533
+ joined.push(join.kind === "anti" ? context : { ...context, [join.alias]: undefined });
3534
+ }
2884
3535
  }
2885
3536
  return joined;
2886
3537
  }
@@ -2905,11 +3556,16 @@ function executeJoin(contexts, join, rows) {
2905
3556
  for (const context of contexts) {
2906
3557
  const leftKey = comparable(evaluate(leftExpression, context));
2907
3558
  const matches = isSqlJoinKey(leftKey) ? (index.get(leftKey) ?? []) : [];
2908
- if (matches.length === 0 && join.kind === "left")
2909
- joined.push({ ...context, [join.alias]: undefined });
2910
- else
3559
+ if (matches.length === 0 && (join.kind === "left" || join.kind === "anti")) {
3560
+ joined.push(join.kind === "anti" ? context : { ...context, [join.alias]: undefined });
3561
+ }
3562
+ else if (matches.length > 0 && join.kind === "semi") {
3563
+ joined.push(context);
3564
+ }
3565
+ else if (join.kind !== "anti") {
2911
3566
  for (const row of matches)
2912
3567
  joined.push({ ...context, [join.alias]: row });
3568
+ }
2913
3569
  }
2914
3570
  return joined;
2915
3571
  }
@@ -2929,7 +3585,9 @@ function project(select, context, group) {
2929
3585
  function evaluate(expression, context, group) {
2930
3586
  switch (expression.kind) {
2931
3587
  case "literal":
2932
- return expression.value;
3588
+ return typeof expression.value === "string" && expression.internalSqlValue !== true
3589
+ ? protectedSqlTextValue(expression.value)
3590
+ : expression.value;
2933
3591
  case "parameter":
2934
3592
  throw new TypeError(`Placeholder $${String(expression.index + 1)} is unbound; pass parameters when executing`);
2935
3593
  case "wildcard":
@@ -2981,8 +3639,11 @@ function evaluate(expression, context, group) {
2981
3639
  if (typeof left !== "string" || typeof right !== "string") {
2982
3640
  throw new TypeError("|| requires string operands");
2983
3641
  }
2984
- return left + right;
3642
+ return protectedSqlTextValue(String(externalSqlDomainValue(left)) + String(externalSqlDomainValue(right)));
2985
3643
  }
3644
+ const exact = exactNumericBinary(expression.operator, left, right);
3645
+ if (exact !== undefined)
3646
+ return exact;
2986
3647
  const a = numeric(left);
2987
3648
  const b = numeric(right);
2988
3649
  if (expression.operator === "+")
@@ -3001,15 +3662,73 @@ function evaluate(expression, context, group) {
3001
3662
  if (group === undefined)
3002
3663
  throw new TypeError(`${expression.name} requires grouped execution`);
3003
3664
  const argument = expression.arguments[0] ?? { kind: "wildcard" };
3665
+ if (expression.name === "STRING_AGG") {
3666
+ const delimiter = expression.arguments[1];
3667
+ if (delimiter === undefined)
3668
+ throw new TypeError("STRING_AGG requires a delimiter");
3669
+ let members = group.flatMap((row) => {
3670
+ const value = evaluate(argument, row);
3671
+ if (value === null || value === undefined)
3672
+ return [];
3673
+ if (typeof value !== "string") {
3674
+ throw new TypeError("STRING_AGG value must be a string");
3675
+ }
3676
+ const separator = evaluate(delimiter, row);
3677
+ if (separator !== null && separator !== undefined && typeof separator !== "string") {
3678
+ throw new TypeError("STRING_AGG delimiter must be a string");
3679
+ }
3680
+ return [
3681
+ {
3682
+ value,
3683
+ delimiter: separator ?? "",
3684
+ order: (expression.aggregateOrderBy ?? []).map((item) => evaluate(item.expression, row)),
3685
+ },
3686
+ ];
3687
+ });
3688
+ if (expression.distinct === true) {
3689
+ const seen = new Set();
3690
+ members = members.filter((member) => {
3691
+ const key = JSON.stringify([member.value, member.delimiter]);
3692
+ if (seen.has(key))
3693
+ return false;
3694
+ seen.add(key);
3695
+ return true;
3696
+ });
3697
+ }
3698
+ if ((expression.aggregateOrderBy?.length ?? 0) > 0) {
3699
+ members.sort((left, right) => {
3700
+ for (const [index, order] of (expression.aggregateOrderBy ?? []).entries()) {
3701
+ const a = left.order[index];
3702
+ const b = right.order[index];
3703
+ const placed = nullOrder(a, b, order.nulls, order.direction);
3704
+ if (placed !== undefined && placed !== 0)
3705
+ return placed;
3706
+ const compared = compareValues(a, b);
3707
+ if (compared !== 0)
3708
+ return order.direction === "desc" ? -compared : compared;
3709
+ }
3710
+ return 0;
3711
+ });
3712
+ }
3713
+ if (members.length === 0)
3714
+ return null;
3715
+ return protectedSqlTextValue(members
3716
+ .map((member, index) => index === 0
3717
+ ? String(externalSqlDomainValue(member.value))
3718
+ : String(externalSqlDomainValue(member.delimiter)) +
3719
+ String(externalSqlDomainValue(member.value)))
3720
+ .join(""));
3721
+ }
3004
3722
  let values = argument.kind === "wildcard"
3005
3723
  ? group.map(() => 1)
3006
- : group
3007
- .map((row) => evaluate(argument, row))
3008
- .filter((value) => value !== null && value !== undefined);
3724
+ : group.map((row) => evaluate(argument, row));
3725
+ if (expression.name !== "JSON_ARRAYAGG") {
3726
+ values = values.filter((value) => value !== null && value !== undefined);
3727
+ }
3009
3728
  if (expression.distinct === true) {
3010
3729
  const seen = new Set();
3011
3730
  values = values.filter((value) => {
3012
- const key = value instanceof Date ? ` d${String(value.getTime())}` : value;
3731
+ const key = value instanceof Date ? ` d${String(dateMilliseconds(value))}` : value;
3013
3732
  if (seen.has(key))
3014
3733
  return false;
3015
3734
  seen.add(key);
@@ -3019,13 +3738,19 @@ function evaluate(expression, context, group) {
3019
3738
  if (expression.name === "COUNT")
3020
3739
  return values.length;
3021
3740
  if (expression.name === "SUM")
3741
+ return values.length === 0 ? null : sumNumericValues(values);
3742
+ if (expression.name === "AVG")
3022
3743
  return values.length === 0
3023
3744
  ? null
3024
- : values.reduce((sum, value) => sum + numeric(value), 0);
3025
- if (expression.name === "AVG")
3745
+ : (() => {
3746
+ const sum = sumNumericValues(values);
3747
+ return exactNumericBinary("/", sum, values.length) ?? numeric(sum) / values.length;
3748
+ })();
3749
+ if (expression.name === "JSON_ARRAYAGG") {
3026
3750
  return values.length === 0
3027
3751
  ? null
3028
- : values.reduce((sum, value) => sum + numeric(value), 0) / values.length;
3752
+ : JSON.stringify(values.map((value) => jsonValueOf(value ?? null)));
3753
+ }
3029
3754
  if (expression.name === "MIN")
3030
3755
  return values.reduce((best, value) => (best === undefined || compareValues(value, best) < 0 ? value : best), undefined);
3031
3756
  return values.reduce((best, value) => (best === undefined || compareValues(value, best) > 0 ? value : best), undefined);
@@ -3052,7 +3777,9 @@ function evaluatePredicate(predicate, context) {
3052
3777
  if (predicate.operator === "LIKE" ||
3053
3778
  predicate.operator === "NOT LIKE" ||
3054
3779
  predicate.operator === "ILIKE" ||
3055
- predicate.operator === "NOT ILIKE") {
3780
+ predicate.operator === "NOT ILIKE" ||
3781
+ predicate.operator === "SIMILAR TO" ||
3782
+ predicate.operator === "NOT SIMILAR TO") {
3056
3783
  return (evaluateBooleanExpression({
3057
3784
  kind: "condition",
3058
3785
  operator: predicate.operator,
@@ -3133,10 +3860,6 @@ export function cachedListMembership(node, items) {
3133
3860
  }
3134
3861
  return cached;
3135
3862
  }
3136
- /** Compiles a LIKE pattern (% = any run, _ = any character) to an anchored RegExp, cached. */
3137
- export function likeRegExp(pattern, caseInsensitive = false, escape) {
3138
- return compileLikePattern(pattern, caseInsensitive, escape);
3139
- }
3140
3863
  const extractFields = new Set([
3141
3864
  "year",
3142
3865
  "quarter",
@@ -3164,70 +3887,38 @@ function extractDatePart(field, value) {
3164
3887
  throw new TypeError("EXTRACT requires a datetime value");
3165
3888
  switch (normalized) {
3166
3889
  case "year":
3167
- return value.getUTCFullYear();
3890
+ return dateUtcFullYear(value);
3168
3891
  case "quarter":
3169
- return Math.floor(value.getUTCMonth() / 3) + 1;
3892
+ return Math.floor(dateUtcMonth(value) / 3) + 1;
3170
3893
  case "month":
3171
- return value.getUTCMonth() + 1;
3894
+ return dateUtcMonth(value) + 1;
3172
3895
  case "week": {
3173
- const date = new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()));
3896
+ const date = new Date(Date.UTC(dateUtcFullYear(value), dateUtcMonth(value), dateUtcDate(value)));
3174
3897
  // ISO week: shift to the Thursday of this week, then count weeks from January 1st.
3175
- date.setUTCDate(date.getUTCDate() + 4 - (date.getUTCDay() || 7));
3176
- const yearStart = Date.UTC(date.getUTCFullYear(), 0, 1);
3177
- return Math.ceil(((date.getTime() - yearStart) / 86_400_000 + 1) / 7);
3898
+ setDateUtcDate(date, dateUtcDate(date) + 4 - (dateUtcDay(date) || 7));
3899
+ const yearStart = Date.UTC(dateUtcFullYear(date), 0, 1);
3900
+ return Math.ceil(((dateMilliseconds(date) - yearStart) / 86_400_000 + 1) / 7);
3178
3901
  }
3179
3902
  case "day":
3180
- return value.getUTCDate();
3903
+ return dateUtcDate(value);
3181
3904
  case "hour":
3182
- return value.getUTCHours();
3905
+ return dateUtcHours(value);
3183
3906
  case "minute":
3184
- return value.getUTCMinutes();
3907
+ return dateUtcMinutes(value);
3185
3908
  case "second":
3186
- return value.getUTCSeconds();
3909
+ return dateUtcSeconds(value);
3187
3910
  case "epoch":
3188
- return value.getTime() / 1000;
3911
+ return dateMilliseconds(value) / 1000;
3189
3912
  default:
3190
- return value.getUTCDay();
3913
+ return dateUtcDay(value);
3191
3914
  }
3192
3915
  }
3193
- const likeMatcherCache = new Map();
3194
3916
  /**
3195
- * A compiled LIKE matcher. Patterns shaped `abc%`, `%abc`, `%abc%`, and `abc` skip the regular
3196
- * expression entirely prefix/suffix/containment string scans are several times faster and
3197
- * dominate real workloads — and everything else falls back to the anchored RegExp.
3917
+ * Shared bounded LIKE matcher. Compilation and the common literal prefix/suffix/containment
3918
+ * fast paths are cached in sql-semantics; complex wildcard shapes have deterministic work caps.
3198
3919
  */
3199
3920
  export function likeMatches(pattern, value, caseInsensitive = false, escape) {
3200
- const key = `${caseInsensitive ? "i" : "s"}${escape ?? ""}\0${pattern}`;
3201
- let matcher = likeMatcherCache.get(key);
3202
- if (matcher === undefined) {
3203
- matcher = buildLikeMatcher(pattern, caseInsensitive, escape);
3204
- if (likeMatcherCache.size >= 128)
3205
- likeMatcherCache.clear();
3206
- likeMatcherCache.set(key, matcher);
3207
- }
3208
- return matcher(value);
3209
- }
3210
- function buildLikeMatcher(pattern, caseInsensitive, escape) {
3211
- if (escape === undefined && !pattern.includes("_")) {
3212
- const leading = pattern.startsWith("%");
3213
- const trailing = pattern.endsWith("%");
3214
- const body = pattern.slice(leading ? 1 : 0, trailing ? pattern.length - 1 : undefined);
3215
- if (!body.includes("%")) {
3216
- const needle = caseInsensitive ? body.toLowerCase() : body;
3217
- const fold = caseInsensitive
3218
- ? (value) => value.toLowerCase()
3219
- : (value) => value;
3220
- if (leading && trailing)
3221
- return (value) => fold(value).includes(needle);
3222
- if (trailing)
3223
- return (value) => fold(value).startsWith(needle);
3224
- if (leading)
3225
- return (value) => fold(value).endsWith(needle);
3226
- return (value) => fold(value) === needle;
3227
- }
3228
- }
3229
- const regExp = likeRegExp(pattern, caseInsensitive, escape);
3230
- return (value) => regExp.test(value);
3921
+ return compileLikePattern(pattern, caseInsensitive, escape).test(value);
3231
3922
  }
3232
3923
  /** Splits a quantified operator like "> ANY" into its comparison and quantifier. */
3233
3924
  export function parseQuantified(operator) {
@@ -3271,7 +3962,7 @@ export function distinctFromComparison(left, right) {
3271
3962
  const rightNull = right === null || right === undefined;
3272
3963
  if (leftNull || rightNull)
3273
3964
  return leftNull !== rightNull;
3274
- return comparable(left) !== comparable(right);
3965
+ return compareValues(comparable(left), comparable(right)) !== 0;
3275
3966
  }
3276
3967
  /**
3277
3968
  * Evaluates a boolean expression tree with SQL three-valued logic: comparisons over NULL are
@@ -3344,17 +4035,24 @@ export function evaluateBooleanExpression(expression, evaluateValue) {
3344
4035
  if (operator === "LIKE" ||
3345
4036
  operator === "NOT LIKE" ||
3346
4037
  operator === "ILIKE" ||
3347
- operator === "NOT ILIKE") {
3348
- const value = evaluateValue(expression.left);
3349
- const pattern = evaluateValue(expression.right);
4038
+ operator === "NOT ILIKE" ||
4039
+ operator === "SIMILAR TO" ||
4040
+ operator === "NOT SIMILAR TO") {
4041
+ const value = externalSqlDomainValue(evaluateValue(expression.left));
4042
+ const pattern = externalSqlDomainValue(evaluateValue(expression.right));
3350
4043
  if (value === null || value === undefined || pattern === null || pattern === undefined) {
3351
4044
  return null;
3352
4045
  }
3353
4046
  if (typeof value !== "string" || typeof pattern !== "string") {
3354
4047
  throw new TypeError("LIKE requires string operands");
3355
4048
  }
3356
- const matched = likeMatches(pattern, value, operator === "ILIKE" || operator === "NOT ILIKE", expression.escape);
3357
- return operator === "LIKE" || operator === "ILIKE" ? matched : !matched;
4049
+ const similar = operator === "SIMILAR TO" || operator === "NOT SIMILAR TO";
4050
+ const matched = similar
4051
+ ? compileSimilarPattern(pattern, expression.escape ?? "\\").test(value)
4052
+ : likeMatches(pattern, value, operator === "ILIKE" || operator === "NOT ILIKE", expression.escape);
4053
+ return operator === "LIKE" || operator === "ILIKE" || operator === "SIMILAR TO"
4054
+ ? matched
4055
+ : !matched;
3358
4056
  }
3359
4057
  if (operator === "IS DISTINCT FROM" || operator === "IS NOT DISTINCT FROM") {
3360
4058
  const distinct = distinctFromComparison(evaluateValue(expression.left), evaluateValue(expression.right));
@@ -3367,9 +4065,9 @@ export function evaluateBooleanExpression(expression, evaluateValue) {
3367
4065
  const a = comparable(left);
3368
4066
  const b = comparable(right);
3369
4067
  if (operator === "=")
3370
- return a === b;
4068
+ return compareValues(a, b) === 0;
3371
4069
  if (operator === "!=" || operator === "<>")
3372
- return a !== b;
4070
+ return compareValues(a, b) !== 0;
3373
4071
  const comparison = compareValues(a, b);
3374
4072
  if (operator === ">")
3375
4073
  return comparison > 0;
@@ -3401,7 +4099,11 @@ function comparisonHolds(operator, leftValue, rightValue) {
3401
4099
  if (operator === "LIKE" ||
3402
4100
  operator === "NOT LIKE" ||
3403
4101
  operator === "ILIKE" ||
3404
- operator === "NOT ILIKE") {
4102
+ operator === "NOT ILIKE" ||
4103
+ operator === "SIMILAR TO" ||
4104
+ operator === "NOT SIMILAR TO") {
4105
+ leftValue = externalSqlDomainValue(leftValue);
4106
+ rightValue = externalSqlDomainValue(rightValue);
3405
4107
  if (leftValue === null ||
3406
4108
  leftValue === undefined ||
3407
4109
  rightValue === null ||
@@ -3411,8 +4113,13 @@ function comparisonHolds(operator, leftValue, rightValue) {
3411
4113
  if (typeof leftValue !== "string" || typeof rightValue !== "string") {
3412
4114
  throw new TypeError("LIKE requires string operands");
3413
4115
  }
3414
- const matched = likeMatches(rightValue, leftValue, operator === "ILIKE" || operator === "NOT ILIKE");
3415
- return operator === "LIKE" || operator === "ILIKE" ? matched : !matched;
4116
+ const similar = operator === "SIMILAR TO" || operator === "NOT SIMILAR TO";
4117
+ const matched = similar
4118
+ ? compileSimilarPattern(rightValue).test(leftValue)
4119
+ : likeMatches(rightValue, leftValue, operator === "ILIKE" || operator === "NOT ILIKE");
4120
+ return operator === "LIKE" || operator === "ILIKE" || operator === "SIMILAR TO"
4121
+ ? matched
4122
+ : !matched;
3416
4123
  }
3417
4124
  if (leftValue === null ||
3418
4125
  leftValue === undefined ||
@@ -3422,9 +4129,9 @@ function comparisonHolds(operator, leftValue, rightValue) {
3422
4129
  const left = comparable(leftValue);
3423
4130
  const right = comparable(rightValue);
3424
4131
  if (operator === "=")
3425
- return left === right;
4132
+ return compareValues(left, right) === 0;
3426
4133
  if (operator === "!=" || operator === "<>")
3427
- return left !== right;
4134
+ return compareValues(left, right) !== 0;
3428
4135
  const comparison = compareValues(left, right);
3429
4136
  if (operator === ">")
3430
4137
  return comparison > 0;
@@ -3503,7 +4210,7 @@ function validateGrouping(plan) {
3503
4210
  for (const item of plan.select) {
3504
4211
  if (hasAggregate(item.expression))
3505
4212
  continue;
3506
- // A constant expression is the same for every group, as standard SQL allows. A full-text
4213
+ // A constant expression is the same for every group, as PostgreSQL allows. A full-text
3507
4214
  // node is never constant — MATCH(*) carries no column children before expansion, but it
3508
4215
  // reads every searchable column of its row.
3509
4216
  if (expressionColumns(item.expression).length === 0 &&
@@ -3540,30 +4247,40 @@ export function expressionAliases(expression) {
3540
4247
  return new Set(expressionColumns(expression).flatMap((reference) => reference.includes(".") ? [reference.split(".")[0] ?? ""] : []));
3541
4248
  }
3542
4249
  function comparable(value) {
3543
- return value instanceof Date ? value.getTime() : value;
4250
+ return value instanceof Date ? dateMilliseconds(value) : value;
3544
4251
  }
3545
4252
  /**
3546
- * Resolves an explicit NULLS FIRST/LAST placement for one order term. Returns undefined when
3547
- * no explicit placement applies (either none was requested or neither side is NULL); otherwise
3548
- * the signed placement, which is absolute direction negation must not apply to it. Two NULLs
3549
- * return 0 so the comparison falls through to the next term.
4253
+ * Resolves NULL placement for one order term. An omitted placement follows PostgreSQL: NULLS LAST
4254
+ * for ASC and NULLS FIRST for DESC. Returns undefined when neither side is NULL; otherwise the
4255
+ * signed placement is absolute, so direction negation must not apply to it. Two NULLs return 0
4256
+ * so comparison falls through to the next term.
3550
4257
  */
3551
- export function explicitNullOrder(left, right, nulls) {
3552
- if (nulls === undefined)
3553
- return undefined;
4258
+ export function nullOrder(left, right, nulls, direction) {
3554
4259
  const leftNull = left === null || left === undefined;
3555
4260
  const rightNull = right === null || right === undefined;
3556
4261
  if (!leftNull && !rightNull)
3557
4262
  return undefined;
3558
4263
  if (leftNull && rightNull)
3559
4264
  return 0;
3560
- return (leftNull ? -1 : 1) * (nulls === "first" ? 1 : -1);
4265
+ const placement = nulls ?? (direction === "desc" ? "first" : "last");
4266
+ return (leftNull ? -1 : 1) * (placement === "first" ? 1 : -1);
3561
4267
  }
3562
4268
  function numeric(value) {
3563
4269
  if (typeof value !== "number")
3564
4270
  throw new TypeError("Arithmetic and numeric aggregates require numbers");
3565
4271
  return value;
3566
4272
  }
4273
+ function sumNumericValues(values) {
4274
+ let total = values[0];
4275
+ if (total === undefined)
4276
+ return null;
4277
+ for (let index = 1; index < values.length; index += 1) {
4278
+ const value = values[index];
4279
+ const exact = exactNumericBinary("+", total, value);
4280
+ total = exact === undefined ? numeric(total) + numeric(value) : exact;
4281
+ }
4282
+ return total;
4283
+ }
3567
4284
  function asQueryValue(value) {
3568
4285
  if (value === null ||
3569
4286
  typeof value === "boolean" ||
@@ -3575,6 +4292,83 @@ function asQueryValue(value) {
3575
4292
  return null;
3576
4293
  throw new TypeError("Query produced an unsupported value");
3577
4294
  }
4295
+ function asExternalQueryValue(value) {
4296
+ return asQueryValue(externalSqlDomainValue(value));
4297
+ }
4298
+ const alreadyExternalResults = new WeakSet();
4299
+ function markExternalizationState(result, outputNeedsExternalization) {
4300
+ if (outputNeedsExternalization === false)
4301
+ alreadyExternalResults.add(result);
4302
+ return result;
4303
+ }
4304
+ /** Carries the internal no-conversion proof across a defensive result copy. */
4305
+ export function copyQueryResultExternalization(source, copy) {
4306
+ if (alreadyExternalResults.has(source))
4307
+ alreadyExternalResults.add(copy);
4308
+ return copy;
4309
+ }
4310
+ /** Removes internal domain tags only after every comparison, group, join, and sort is complete. */
4311
+ export function externalizeQueryResult(result) {
4312
+ if (alreadyExternalResults.has(result))
4313
+ return result;
4314
+ let changed = false;
4315
+ const rows = new Array(result.rows.length);
4316
+ for (let rowIndex = 0; rowIndex < result.rows.length; rowIndex += 1) {
4317
+ const row = result.rows[rowIndex] ?? {};
4318
+ let output = row;
4319
+ // Ordinary primitive results are already public values. Most queries never touch one of the
4320
+ // tagged PostgreSQL domains, so keep their row objects and avoid rebuilding a large result
4321
+ // set merely to discover that every value is unchanged.
4322
+ for (const name of result.columns) {
4323
+ const value = row[name];
4324
+ if (value !== undefined && !isSqlDomainValue(value))
4325
+ continue;
4326
+ const external = asExternalQueryValue(value);
4327
+ if (external === value)
4328
+ continue;
4329
+ if (output === row)
4330
+ output = { ...row };
4331
+ output[name] = external;
4332
+ changed = true;
4333
+ }
4334
+ rows[rowIndex] = output;
4335
+ }
4336
+ const externalized = changed ? { columns: [...result.columns], rows } : result;
4337
+ // Public TEXT is allowed to begin with the private tag prefix, so externalization is not
4338
+ // byte-idempotent. Record the boundary crossing: a wrapper such as executeQuery(query()) must
4339
+ // not interpret the now-public bytes a second time.
4340
+ alreadyExternalResults.add(externalized);
4341
+ return externalized;
4342
+ }
4343
+ /** Gives bare conflict-update columns the target-row meaning SQL assigns them. */
4344
+ function rewriteUpsertExpression(expression, table) {
4345
+ if (hasAggregate(expression)) {
4346
+ throw new TypeError("ON CONFLICT DO UPDATE expressions cannot contain aggregates");
4347
+ }
4348
+ const rewrite = (node) => {
4349
+ if (node.kind === "column") {
4350
+ const parts = node.reference.split(".");
4351
+ if (parts.length === 1)
4352
+ return { ...node, reference: `${table}.${node.reference}` };
4353
+ if (parts.length === 2 && parts[0]?.toUpperCase() === "EXCLUDED") {
4354
+ return { ...node, reference: `EXCLUDED.${parts[1] ?? ""}` };
4355
+ }
4356
+ if (parts.length === 2 && parts[0]?.toUpperCase() === table.toUpperCase()) {
4357
+ return { ...node, reference: `${table}.${parts[1] ?? ""}` };
4358
+ }
4359
+ return node;
4360
+ }
4361
+ if (node.kind === "subquery" ||
4362
+ node.kind === "exists" ||
4363
+ node.kind === "window" ||
4364
+ node.kind === "fts" ||
4365
+ node.kind === "wildcard") {
4366
+ throw new TypeError("ON CONFLICT DO UPDATE expressions use target and EXCLUDED scalar values only");
4367
+ }
4368
+ return mapChildExpressions(node, rewrite);
4369
+ };
4370
+ return rewrite(expression);
4371
+ }
3578
4372
  class Parser {
3579
4373
  tokens;
3580
4374
  /** The statement text the tokens came from, for the clauses stored verbatim (CHECK bodies). */
@@ -3586,6 +4380,10 @@ class Parser {
3586
4380
  #highestNumberedParameter = 0;
3587
4381
  /** Set when the statement names CURRENT_DATE, CURRENT_TIMESTAMP, or LOCALTIME. */
3588
4382
  usesStatementDatetime = false;
4383
+ /** Set when the statement names NEXTVAL or CURRVAL. */
4384
+ usesSequenceCalls = false;
4385
+ /** Set when the statement names a volatile scalar function. */
4386
+ usesVolatileFunctions = false;
3589
4387
  /** Total parameter slots the statement expects; 0 when it has no placeholders. */
3590
4388
  get parameterCount() {
3591
4389
  return Math.max(this.#positionalParameters, this.#highestNumberedParameter);
@@ -3697,7 +4495,7 @@ class Parser {
3697
4495
  this.#ctes.set(name, block);
3698
4496
  }
3699
4497
  }
3700
- // INTERSECT binds tighter than UNION and EXCEPT, per the SQL standard.
4498
+ // INTERSECT binds tighter than UNION and EXCEPT, matching PostgreSQL.
3701
4499
  const firstTerm = this.#setTerm(sql);
3702
4500
  let plan = firstTerm.block;
3703
4501
  if (this.#isKeyword("UNION") || this.#isKeyword("EXCEPT")) {
@@ -3755,7 +4553,7 @@ class Parser {
3755
4553
  throw new TypeError("ORDER BY or LIMIT in a UNION member requires parentheses");
3756
4554
  }
3757
4555
  }
3758
- // Standard SQL assigns a trailing ORDER BY or LIMIT to the whole compound. After an
4556
+ // PostgreSQL assigns a trailing ORDER BY or LIMIT to the whole compound. After an
3759
4557
  // unparenthesized last member the clause was greedily parsed into that member and lifts
3760
4558
  // out; after a parenthesized member it is still unparsed.
3761
4559
  const last = members[members.length - 1];
@@ -3784,10 +4582,53 @@ class Parser {
3784
4582
  }
3785
4583
  return compoundSelectBlock(sql, members.map((member) => member.block), ops, tail, this.nextDerivedSequence);
3786
4584
  }
4585
+ /** CREATE [UNIQUE] INDEX name ON table(column [ASC|DESC], ...). */
4586
+ parseCreateIndex() {
4587
+ this.#keyword("CREATE");
4588
+ const unique = this.#isKeyword("UNIQUE");
4589
+ if (unique)
4590
+ this.#keyword("UNIQUE");
4591
+ this.#keyword("INDEX");
4592
+ let ifNotExists = false;
4593
+ if (this.#isKeyword("IF")) {
4594
+ this.#keyword("IF");
4595
+ this.#keyword("NOT");
4596
+ this.#keyword("EXISTS");
4597
+ ifNotExists = true;
4598
+ }
4599
+ const index = this.#identifier();
4600
+ this.#keyword("ON");
4601
+ const table = this.#identifier();
4602
+ this.#expectPunctuation("(");
4603
+ const columns = [];
4604
+ do {
4605
+ if (columns.length > 0)
4606
+ this.#expectPunctuation(",");
4607
+ const name = this.#identifier();
4608
+ let direction = "asc";
4609
+ if (this.#isKeyword("ASC"))
4610
+ this.#keyword("ASC");
4611
+ else if (this.#isKeyword("DESC")) {
4612
+ this.#keyword("DESC");
4613
+ direction = "desc";
4614
+ }
4615
+ columns.push({ name, direction });
4616
+ } while (this.#peek().text === ",");
4617
+ this.#expectPunctuation(")");
4618
+ this.#take("eof");
4619
+ return {
4620
+ kind: "create-index",
4621
+ index,
4622
+ table,
4623
+ columns,
4624
+ ...(unique ? { unique: true } : {}),
4625
+ ...(ifNotExists ? { ifNotExists: true } : {}),
4626
+ };
4627
+ }
3787
4628
  /**
3788
4629
  * CREATE TABLE name (col TYPE [PRIMARY KEY | UNIQUE] [NOT NULL | NULL], ...). Standard type
3789
- * names map onto the engine's four logical types; widths in parentheses parse and are
3790
- * ignored, because numeric widths and encodings are the engine's job, not schema choices.
4630
+ * names map onto the engine's four logical types. Integer spellings retain an exact-domain
4631
+ * catalog guard; exact NUMERIC/DECIMAL is rejected until it has a distinct physical type.
3791
4632
  */
3792
4633
  parseCreateTable() {
3793
4634
  this.#keyword("CREATE");
@@ -3816,9 +4657,28 @@ class Parser {
3816
4657
  const columns = [];
3817
4658
  const checks = [];
3818
4659
  const foreignKeys = [];
3819
- let uniqueKey;
4660
+ const uniqueConstraints = [];
4661
+ let primaryKey;
4662
+ const constraintColumns = () => {
4663
+ this.#expectPunctuation("(");
4664
+ const names = [];
4665
+ for (;;) {
4666
+ names.push(this.#identifier());
4667
+ if (!this.#punctuation(","))
4668
+ break;
4669
+ }
4670
+ this.#expectPunctuation(")");
4671
+ if (new Set(names).size !== names.length) {
4672
+ throw new TypeError("Constraint columns must be unique");
4673
+ }
4674
+ return names;
4675
+ };
3820
4676
  for (;;) {
3821
- if (this.#isKeyword("CONSTRAINT") || this.#isKeyword("CHECK") || this.#isKeyword("FOREIGN")) {
4677
+ if (this.#isKeyword("CONSTRAINT") ||
4678
+ this.#isKeyword("CHECK") ||
4679
+ this.#isKeyword("FOREIGN") ||
4680
+ this.#isKeyword("PRIMARY") ||
4681
+ this.#isKeyword("UNIQUE")) {
3822
4682
  // A table-level constraint, named or not.
3823
4683
  let constraintName;
3824
4684
  if (this.#isKeyword("CONSTRAINT")) {
@@ -3828,59 +4688,50 @@ class Parser {
3828
4688
  if (this.#isKeyword("FOREIGN")) {
3829
4689
  this.#keyword("FOREIGN");
3830
4690
  this.#keyword("KEY");
3831
- this.#expectPunctuation("(");
3832
- const column = this.#identifier();
3833
- if (this.#peek().text === ",") {
3834
- throw new TypeError("FOREIGN KEY supports one column, the parent's unique key");
3835
- }
3836
- this.#expectPunctuation(")");
3837
- foreignKeys.push(this.#references(constraintName ?? `${table}_${column}_fkey`, column));
4691
+ const names = constraintColumns();
4692
+ foreignKeys.push(this.#references(constraintName ?? `${table}_${names.join("_")}_fkey`, names));
3838
4693
  if (!this.#punctuation(","))
3839
4694
  break;
3840
4695
  continue;
3841
4696
  }
3842
- if (!this.#isKeyword("CHECK")) {
3843
- throw new TypeError("Table constraints are CHECK and FOREIGN KEY");
3844
- }
3845
- checks.push(this.#checkConstraint(constraintName ?? `${table}_check_${String(checks.length + 1)}`));
3846
- if (!this.#punctuation(","))
3847
- break;
3848
- continue;
3849
- }
3850
- if (this.#isKeyword("PRIMARY") || this.#isKeyword("UNIQUE")) {
3851
- // E141-08: the table-level spelling of the same single-column key.
3852
4697
  if (this.#isKeyword("PRIMARY")) {
3853
4698
  this.#keyword("PRIMARY");
3854
4699
  this.#keyword("KEY");
4700
+ if (primaryKey !== undefined)
4701
+ throw new TypeError("CREATE TABLE has two PRIMARY KEYs");
4702
+ primaryKey = constraintColumns();
4703
+ if (!this.#punctuation(","))
4704
+ break;
4705
+ continue;
3855
4706
  }
3856
- else
4707
+ if (this.#isKeyword("UNIQUE")) {
3857
4708
  this.#keyword("UNIQUE");
3858
- this.#expectPunctuation("(");
3859
- const keyColumn = this.#identifier();
3860
- if (this.#peek().text === ",") {
3861
- throw new TypeError("CREATE TABLE supports one unique key column");
4709
+ const names = constraintColumns();
4710
+ uniqueConstraints.push({
4711
+ name: constraintName ?? `${table}_${names.join("_")}_key`,
4712
+ columns: names,
4713
+ });
4714
+ if (!this.#punctuation(","))
4715
+ break;
4716
+ continue;
3862
4717
  }
3863
- this.#expectPunctuation(")");
3864
- if (uniqueKey !== undefined && uniqueKey !== keyColumn) {
3865
- throw new TypeError("CREATE TABLE supports one unique key column");
4718
+ if (!this.#isKeyword("CHECK")) {
4719
+ throw new TypeError("Expected PRIMARY KEY, UNIQUE, CHECK, or FOREIGN KEY");
3866
4720
  }
3867
- uniqueKey = keyColumn;
4721
+ checks.push(this.#checkConstraint(constraintName ?? `${table}_check_${String(checks.length + 1)}`));
3868
4722
  if (!this.#punctuation(","))
3869
4723
  break;
3870
4724
  continue;
3871
4725
  }
3872
4726
  const name = this.#identifier();
3873
- const type = this.#columnType();
4727
+ const columnType = this.#columnType();
3874
4728
  let nullable = true;
3875
- let explicitlyNullable = false;
3876
4729
  let defaultValue;
3877
4730
  for (;;) {
3878
4731
  if (this.#isKeyword("DEFAULT")) {
3879
- // E141-07. The catalog fills defaults at insert time, so they are constants: a
3880
- // literal, or the CURRENT_TIMESTAMP family, which it stores as "now".
4732
+ // PostgreSQL-compatible variable-free scalar expression, retained in the catalog.
3881
4733
  this.#keyword("DEFAULT");
3882
- const expression = this.#expression();
3883
- defaultValue = columnDefaultFor(expression);
4734
+ defaultValue = this.#columnDefault();
3884
4735
  continue;
3885
4736
  }
3886
4737
  if (this.#isKeyword("CHECK")) {
@@ -3888,22 +4739,22 @@ class Parser {
3888
4739
  continue;
3889
4740
  }
3890
4741
  if (this.#isKeyword("REFERENCES")) {
3891
- foreignKeys.push(this.#references(`${table}_${name}_fkey`, name));
4742
+ foreignKeys.push(this.#references(`${table}_${name}_fkey`, [name]));
3892
4743
  continue;
3893
4744
  }
3894
4745
  if (this.#isKeyword("PRIMARY") || this.#isKeyword("UNIQUE")) {
3895
4746
  if (this.#isKeyword("PRIMARY")) {
3896
4747
  this.#keyword("PRIMARY");
3897
4748
  this.#keyword("KEY");
4749
+ if (primaryKey !== undefined)
4750
+ throw new TypeError("CREATE TABLE has two PRIMARY KEYs");
4751
+ primaryKey = [name];
4752
+ nullable = false;
3898
4753
  }
3899
4754
  else {
3900
4755
  this.#keyword("UNIQUE");
4756
+ uniqueConstraints.push({ name: `${table}_${name}_key`, columns: [name] });
3901
4757
  }
3902
- if (uniqueKey !== undefined) {
3903
- throw new TypeError("CREATE TABLE supports one unique key column");
3904
- }
3905
- uniqueKey = name;
3906
- nullable = false;
3907
4758
  continue;
3908
4759
  }
3909
4760
  if (this.#isKeyword("NOT")) {
@@ -3919,19 +4770,13 @@ class Parser {
3919
4770
  if (this.#isKeyword("NULL")) {
3920
4771
  this.#keyword("NULL");
3921
4772
  nullable = true;
3922
- explicitlyNullable = true;
3923
4773
  continue;
3924
4774
  }
3925
4775
  break;
3926
4776
  }
3927
- // A default answers what an absent value means, so the column cannot also be nullable
3928
- // unless the author says so — and then the engine rejects the pair, since NULL and the
3929
- // default would both claim the same slot.
3930
- if (defaultValue !== undefined && !explicitlyNullable)
3931
- nullable = false;
3932
4777
  columns.push({
3933
4778
  name,
3934
- type,
4779
+ ...columnType,
3935
4780
  ...(nullable ? { nullable: true } : {}),
3936
4781
  ...(defaultValue === undefined ? {} : { defaultValue }),
3937
4782
  });
@@ -3943,10 +4788,14 @@ class Parser {
3943
4788
  if (new Set(columns.map((column) => column.name)).size !== columns.length) {
3944
4789
  throw new TypeError("CREATE TABLE column names must be unique");
3945
4790
  }
3946
- if (uniqueKey !== undefined && !columns.some((column) => column.name === uniqueKey)) {
3947
- throw new TypeError(`CREATE TABLE key column is not declared: ${uniqueKey}`);
3948
- }
3949
4791
  const declared = new Set(columns.map((column) => column.name));
4792
+ for (const name of [
4793
+ ...(primaryKey ?? []),
4794
+ ...uniqueConstraints.flatMap((key) => key.columns),
4795
+ ]) {
4796
+ if (!declared.has(name))
4797
+ throw new TypeError(`Constraint column is not declared: ${name}`);
4798
+ }
3950
4799
  for (const check of checks) {
3951
4800
  for (const reference of expressionColumnNames(compileCheckExpression(check.sql, check.name))) {
3952
4801
  const column = reference.split(".").at(-1) ?? reference;
@@ -3955,11 +4804,24 @@ class Parser {
3955
4804
  }
3956
4805
  }
3957
4806
  }
4807
+ // Preserve the released single-UNIQUE row-addressing behavior when no PRIMARY KEY was
4808
+ // declared. Additional UNIQUE constraints remain independently enforced secondary keys.
4809
+ const promotedUnique = primaryKey === undefined && uniqueConstraints[0]?.columns.length === 1
4810
+ ? uniqueConstraints.shift()
4811
+ : undefined;
4812
+ const uniqueKey = primaryKey?.length === 1 ? primaryKey[0] : promotedUnique?.columns[0];
4813
+ const primaryNames = new Set(primaryKey ?? []);
3958
4814
  return {
3959
4815
  kind: "create-table",
3960
4816
  table,
3961
- columns: columns.map((column) => column.name === uniqueKey ? { ...column, nullable: false } : column),
4817
+ columns: columns.map((column) => column.name === uniqueKey || primaryNames.has(column.name)
4818
+ ? { ...column, nullable: false }
4819
+ : column),
3962
4820
  ...(uniqueKey === undefined ? {} : { uniqueKey }),
4821
+ ...(primaryKey === undefined || primaryKey.length < 2
4822
+ ? {}
4823
+ : { compositePrimaryKey: primaryKey }),
4824
+ ...(uniqueConstraints.length === 0 ? {} : { uniqueConstraints }),
3963
4825
  ...(checks.length === 0 ? {} : { checks }),
3964
4826
  ...(foreignKeys.length === 0 ? {} : { foreignKeys }),
3965
4827
  ...(ifNotExists ? { ifNotExists: true } : {}),
@@ -3970,14 +4832,22 @@ class Parser {
3970
4832
  * key cannot change in this engine, so ON UPDATE has nothing to act on and only the
3971
4833
  * no-op actions parse; ON DELETE takes the three the engine can carry out.
3972
4834
  */
3973
- #references(name, column) {
4835
+ #references(name, columns) {
3974
4836
  this.#keyword("REFERENCES");
3975
4837
  const parentTable = this.#identifier();
3976
- let parentColumn;
4838
+ let parentColumns;
3977
4839
  if (this.#punctuation("(")) {
3978
- parentColumn = this.#identifier();
4840
+ parentColumns = [];
4841
+ for (;;) {
4842
+ parentColumns.push(this.#identifier());
4843
+ if (!this.#punctuation(","))
4844
+ break;
4845
+ }
3979
4846
  this.#expectPunctuation(")");
3980
4847
  }
4848
+ if (parentColumns !== undefined && parentColumns.length !== columns.length) {
4849
+ throw new TypeError("FOREIGN KEY and REFERENCES must name the same number of columns");
4850
+ }
3981
4851
  let onDelete = "restrict";
3982
4852
  while (this.#isKeyword("ON")) {
3983
4853
  this.#keyword("ON");
@@ -3994,9 +4864,10 @@ class Parser {
3994
4864
  }
3995
4865
  return {
3996
4866
  name,
3997
- column,
4867
+ column: columns[0] ?? "",
4868
+ ...(columns.length === 1 ? {} : { columns }),
3998
4869
  parentTable,
3999
- ...(parentColumn === undefined ? {} : { parentColumn }),
4870
+ ...(parentColumns === undefined ? {} : { parentColumn: parentColumns[0], parentColumns }),
4000
4871
  onDelete,
4001
4872
  };
4002
4873
  }
@@ -4046,6 +4917,15 @@ class Parser {
4046
4917
  this.#take("eof");
4047
4918
  return expression;
4048
4919
  }
4920
+ /** Reads one column DEFAULT while preserving its authored SQL for catalog introspection. */
4921
+ #columnDefault() {
4922
+ const start = this.#peek().start;
4923
+ const expression = this.#expression();
4924
+ const sql = this.text.slice(start, this.#peek().start).trim();
4925
+ if (sql.length === 0)
4926
+ throw new TypeError("DEFAULT requires an expression");
4927
+ return columnDefaultFor(expression, sql);
4928
+ }
4049
4929
  /** DROP VIEW [IF EXISTS] name (F031-16). */
4050
4930
  parseDropView() {
4051
4931
  this.#keyword("DROP");
@@ -4062,6 +4942,20 @@ class Parser {
4062
4942
  this.#take("eof");
4063
4943
  return { kind: "drop-view", view, ...(ifExists ? { ifExists: true } : {}) };
4064
4944
  }
4945
+ /** DROP INDEX [IF EXISTS] name. */
4946
+ parseDropIndex() {
4947
+ this.#keyword("DROP");
4948
+ this.#keyword("INDEX");
4949
+ let ifExists = false;
4950
+ if (this.#isKeyword("IF")) {
4951
+ this.#keyword("IF");
4952
+ this.#keyword("EXISTS");
4953
+ ifExists = true;
4954
+ }
4955
+ const index = this.#identifier();
4956
+ this.#take("eof");
4957
+ return { kind: "drop-index", index, ...(ifExists ? { ifExists: true } : {}) };
4958
+ }
4065
4959
  /** DROP TABLE [IF EXISTS] name (F031-13). */
4066
4960
  parseDropTable() {
4067
4961
  this.#keyword("DROP");
@@ -4083,22 +4977,46 @@ class Parser {
4083
4977
  this.#take("eof");
4084
4978
  return { kind: "drop-table", table, ...(ifExists ? { ifExists: true } : {}) };
4085
4979
  }
4086
- /** ALTER TABLE name ADD [COLUMN] col TYPE [NOT NULL] [DEFAULT v] (F031-04). */
4980
+ /** ALTER TABLE name ADD/DROP [COLUMN] (F031). */
4087
4981
  parseAlterTable() {
4088
4982
  this.#keyword("ALTER");
4089
4983
  this.#keyword("TABLE");
4090
4984
  const table = this.#identifier();
4985
+ if (this.#isKeyword("DROP")) {
4986
+ this.#keyword("DROP");
4987
+ if (this.#isKeyword("COLUMN"))
4988
+ this.#keyword("COLUMN");
4989
+ let ifExists = false;
4990
+ if (this.#isKeyword("IF")) {
4991
+ this.#keyword("IF");
4992
+ this.#keyword("EXISTS");
4993
+ ifExists = true;
4994
+ }
4995
+ const column = this.#identifier();
4996
+ if (this.#isKeyword("RESTRICT"))
4997
+ this.#keyword("RESTRICT");
4998
+ else if (this.#isKeyword("CASCADE")) {
4999
+ throw new TypeError("ALTER TABLE DROP COLUMN CASCADE is not supported; drop dependents explicitly");
5000
+ }
5001
+ this.#take("eof");
5002
+ return {
5003
+ kind: "drop-column",
5004
+ table,
5005
+ column,
5006
+ ...(ifExists ? { ifExists: true } : {}),
5007
+ };
5008
+ }
4091
5009
  this.#keyword("ADD");
4092
5010
  if (this.#isKeyword("COLUMN"))
4093
5011
  this.#keyword("COLUMN");
4094
5012
  const name = this.#identifier();
4095
- const type = this.#columnType();
5013
+ const columnType = this.#columnType();
4096
5014
  let nullable = true;
4097
5015
  let defaultValue;
4098
5016
  for (;;) {
4099
5017
  if (this.#isKeyword("DEFAULT")) {
4100
5018
  this.#keyword("DEFAULT");
4101
- defaultValue = columnDefaultFor(this.#expression());
5019
+ defaultValue = this.#columnDefault();
4102
5020
  continue;
4103
5021
  }
4104
5022
  if (this.#isKeyword("NOT")) {
@@ -4120,7 +5038,7 @@ class Parser {
4120
5038
  table,
4121
5039
  column: {
4122
5040
  name,
4123
- type,
5041
+ ...columnType,
4124
5042
  ...(nullable ? { nullable: true } : {}),
4125
5043
  ...(defaultValue === undefined ? {} : { defaultValue }),
4126
5044
  },
@@ -4129,6 +5047,22 @@ class Parser {
4129
5047
  /** A CAST target: the SqlColumnType, or "number-integer" for the truncating integer names. */
4130
5048
  #castTarget() {
4131
5049
  const word = this.#identifier().toUpperCase();
5050
+ if (word === "NUMERIC" || word === "DECIMAL") {
5051
+ let precision;
5052
+ let scale;
5053
+ if (this.#punctuation("(")) {
5054
+ precision = Number(this.#take("number").text);
5055
+ if (this.#punctuation(","))
5056
+ scale = Number(this.#take("number").text);
5057
+ else
5058
+ scale = 0;
5059
+ this.#expectPunctuation(")");
5060
+ }
5061
+ return `numeric:${precision === undefined ? "" : String(precision)}:${scale === undefined ? "" : String(scale)}`;
5062
+ }
5063
+ if (["JSON", "JSONB", "UUID", "TIME", "INTERVAL"].includes(word)) {
5064
+ return word.toLowerCase();
5065
+ }
4132
5066
  if (word === "DOUBLE") {
4133
5067
  this.#keyword("PRECISION");
4134
5068
  return "number";
@@ -4146,22 +5080,50 @@ class Parser {
4146
5080
  return integer ? "number-integer" : mapped;
4147
5081
  }
4148
5082
  #columnType() {
4149
- const word = this.#identifier().toUpperCase();
5083
+ const declared = this.#identifier();
5084
+ const word = declared.toUpperCase();
5085
+ if (word === "NUMERIC" || word === "DECIMAL") {
5086
+ let precision;
5087
+ let scale;
5088
+ if (this.#punctuation("(")) {
5089
+ precision = Number(this.#take("number").text);
5090
+ scale = this.#punctuation(",") ? Number(this.#take("number").text) : 0;
5091
+ this.#expectPunctuation(")");
5092
+ }
5093
+ return {
5094
+ type: "string",
5095
+ sqlDomain: {
5096
+ kind: "numeric",
5097
+ ...(precision === undefined ? {} : { precision }),
5098
+ ...(scale === undefined ? {} : { scale }),
5099
+ },
5100
+ };
5101
+ }
5102
+ if (["JSON", "JSONB", "UUID", "TIME", "INTERVAL"].includes(word)) {
5103
+ return { type: "string", sqlDomain: { kind: word.toLowerCase() } };
5104
+ }
4150
5105
  if (word === "DOUBLE") {
4151
5106
  this.#keyword("PRECISION");
4152
- return "number";
5107
+ return { type: "number" };
4153
5108
  }
4154
5109
  const mapped = createTableTypeNames.get(word);
4155
- if (mapped === undefined)
4156
- throw new TypeError(`Unsupported column type: ${word}`);
4157
- // A width like VARCHAR(80) or NUMERIC(10, 2) parses and is discarded.
5110
+ if (mapped === undefined) {
5111
+ return { type: "string", sqlDomain: { kind: "enum", name: declared, values: [] } };
5112
+ }
5113
+ // Character widths document intent and do not truncate values.
4158
5114
  if (this.#punctuation("(")) {
4159
5115
  this.#take("number");
4160
- if (this.#punctuation(","))
4161
- this.#take("number");
5116
+ if (this.#punctuation(",")) {
5117
+ throw new TypeError(`${word} takes one width`);
5118
+ }
4162
5119
  this.#expectPunctuation(")");
4163
5120
  }
4164
- return mapped;
5121
+ if (this.#punctuation("[")) {
5122
+ this.#expectPunctuation("]");
5123
+ return { type: "string", sqlDomain: { kind: "array", element: word } };
5124
+ }
5125
+ const integer = word === "INTEGER" || word === "INT" || word === "BIGINT" || word === "SMALLINT";
5126
+ return { type: mapped, ...(integer ? { integer: true } : {}) };
4165
5127
  }
4166
5128
  parseMutation(keyword) {
4167
5129
  const statement = keyword === "INSERT"
@@ -4176,23 +5138,37 @@ class Parser {
4176
5138
  this.#keyword("INSERT");
4177
5139
  this.#keyword("INTO");
4178
5140
  const table = this.#identifier();
4179
- this.#expectPunctuation("(");
4180
5141
  const columns = [];
4181
- for (;;) {
4182
- columns.push(this.#identifier());
4183
- if (!this.#punctuation(","))
4184
- break;
5142
+ if (this.#punctuation("(")) {
5143
+ for (;;) {
5144
+ columns.push(this.#identifier());
5145
+ if (!this.#punctuation(","))
5146
+ break;
5147
+ }
5148
+ this.#expectPunctuation(")");
4185
5149
  }
4186
- this.#expectPunctuation(")");
4187
5150
  if (new Set(columns).size !== columns.length) {
4188
5151
  throw new TypeError("INSERT columns must be unique");
4189
5152
  }
5153
+ if (this.#isKeyword("DEFAULT")) {
5154
+ this.#keyword("DEFAULT");
5155
+ this.#keyword("VALUES");
5156
+ return {
5157
+ kind: "insert",
5158
+ table,
5159
+ columns,
5160
+ rows: [[]],
5161
+ defaultValues: true,
5162
+ ...this.#onConflictClause(table),
5163
+ ...this.#returningClause(),
5164
+ };
5165
+ }
4190
5166
  if (this.#isKeyword("SELECT")) {
4191
5167
  const query = optimizePlan(this.#selectBlock("(insert select)"));
4192
5168
  if (query.select.some((item) => item.expression.kind === "wildcard")) {
4193
5169
  throw new TypeError("INSERT ... SELECT requires an explicit select list");
4194
5170
  }
4195
- if (query.select.length !== columns.length) {
5171
+ if (columns.length > 0 && query.select.length !== columns.length) {
4196
5172
  throw new TypeError("INSERT ... SELECT must produce exactly the insert column count");
4197
5173
  }
4198
5174
  return {
@@ -4215,7 +5191,7 @@ class Parser {
4215
5191
  break;
4216
5192
  }
4217
5193
  this.#expectPunctuation(")");
4218
- if (values.length !== columns.length) {
5194
+ if (columns.length > 0 && values.length !== columns.length) {
4219
5195
  throw new TypeError("Each INSERT row must match the column list length");
4220
5196
  }
4221
5197
  rows.push(values);
@@ -4227,55 +5203,72 @@ class Parser {
4227
5203
  table,
4228
5204
  columns,
4229
5205
  rows,
4230
- ...this.#onConflictClause(columns),
5206
+ ...this.#onConflictClause(table),
4231
5207
  ...this.#returningClause(),
4232
5208
  };
4233
5209
  }
4234
- #onConflictClause(columns) {
5210
+ #onConflictClause(table) {
4235
5211
  if (!this.#isKeyword("ON"))
4236
5212
  return {};
4237
5213
  this.#keyword("ON");
4238
5214
  this.#keyword("CONFLICT");
4239
5215
  this.#expectPunctuation("(");
4240
- const column = this.#identifier();
5216
+ const columns = [];
5217
+ for (;;) {
5218
+ columns.push(this.#identifier());
5219
+ if (!this.#punctuation(","))
5220
+ break;
5221
+ }
4241
5222
  this.#expectPunctuation(")");
5223
+ const column = columns[0];
5224
+ if (column === undefined)
5225
+ throw new TypeError("ON CONFLICT needs at least one column");
5226
+ const target = columns.length === 1 ? {} : { columns };
4242
5227
  this.#keyword("DO");
4243
5228
  if (this.#isKeyword("NOTHING")) {
4244
5229
  this.#keyword("NOTHING");
4245
- return { onConflict: { column, action: "nothing" } };
5230
+ return { onConflict: { column, ...target, action: "nothing" } };
5231
+ }
5232
+ // Minnow's concise whole-row upsert spelling. Unlike DO UPDATE SET, it also has an exact
5233
+ // meaning for a table whose unique key is its only column.
5234
+ if (this.#isKeyword("REPLACE")) {
5235
+ this.#keyword("REPLACE");
5236
+ return { onConflict: { column, ...target, action: "replace" } };
4246
5237
  }
4247
5238
  this.#keyword("UPDATE");
4248
5239
  this.#keyword("SET");
4249
5240
  const assigned = new Set();
5241
+ const assignments = [];
4250
5242
  for (;;) {
4251
5243
  const target = this.#identifier();
4252
5244
  this.#operator("=");
4253
- const value = this.#expression();
4254
- const parts = value.kind === "column" ? value.reference.split(".") : [];
4255
- if (parts.length !== 2 || parts[0]?.toUpperCase() !== "EXCLUDED" || parts[1] !== target) {
4256
- throw new TypeError("ON CONFLICT DO UPDATE supports assignments of the form column = EXCLUDED.column");
4257
- }
4258
5245
  if (assigned.has(target)) {
4259
5246
  throw new TypeError(`ON CONFLICT DO UPDATE sets a column twice: ${target}`);
4260
5247
  }
4261
- if (target === column) {
5248
+ if (columns.includes(target)) {
4262
5249
  throw new TypeError("ON CONFLICT DO UPDATE cannot reassign the conflict key");
4263
5250
  }
4264
5251
  assigned.add(target);
5252
+ assignments.push({
5253
+ column: target,
5254
+ expression: rewriteUpsertExpression(this.#expression(), table),
5255
+ });
4265
5256
  if (!this.#punctuation(","))
4266
5257
  break;
4267
5258
  }
4268
- for (const name of assigned) {
4269
- if (!columns.includes(name)) {
4270
- throw new TypeError(`ON CONFLICT DO UPDATE sets a column that is not inserted: ${name}`);
4271
- }
5259
+ let where;
5260
+ if (this.#isKeyword("WHERE")) {
5261
+ this.#keyword("WHERE");
5262
+ where = rewriteUpsertExpression(this.#expression(), table);
4272
5263
  }
4273
- // Every inserted column assigned is a whole-row upsert; a subset merges into existing rows.
4274
- const complete = columns.every((name) => name === column || assigned.has(name));
4275
5264
  return {
4276
- onConflict: complete
4277
- ? { column, action: "replace" }
4278
- : { column, action: "update", columns: [...assigned] },
5265
+ onConflict: {
5266
+ column,
5267
+ ...target,
5268
+ action: "update",
5269
+ assignments,
5270
+ ...(where === undefined ? {} : { where }),
5271
+ },
4279
5272
  };
4280
5273
  }
4281
5274
  /** RETURNING * or RETURNING col, ... — the engine's runStatement implements the semantics. */
@@ -4463,6 +5456,10 @@ class Parser {
4463
5456
  return predicates;
4464
5457
  }
4465
5458
  #insertValue(label) {
5459
+ if (this.#isKeyword("DEFAULT")) {
5460
+ this.#keyword("DEFAULT");
5461
+ return { default: true };
5462
+ }
4466
5463
  const expression = this.#expression();
4467
5464
  // A bare placeholder stays a slot; the batch write binds it later. Placeholders nested in
4468
5465
  // arithmetic would need expression retention through the batch path, so they stay rejected.
@@ -4907,14 +5904,18 @@ class Parser {
4907
5904
  }
4908
5905
  #selectList() {
4909
5906
  const items = [];
5907
+ const explicitAliases = [];
4910
5908
  for (;;) {
4911
5909
  const expression = this.#expression();
4912
5910
  let alias = defaultAlias(expression);
5911
+ let explicitAlias = false;
4913
5912
  if (this.#isKeyword("AS")) {
4914
5913
  this.#keyword("AS");
4915
5914
  alias = this.#identifier();
5915
+ explicitAlias = true;
4916
5916
  }
4917
5917
  items.push({ expression, alias });
5918
+ explicitAliases.push(explicitAlias);
4918
5919
  if (!this.#punctuation(","))
4919
5920
  break;
4920
5921
  }
@@ -4923,15 +5924,23 @@ class Parser {
4923
5924
  throw new TypeError("SELECT * cannot be mixed with other expressions");
4924
5925
  }
4925
5926
  const aliases = new Set();
4926
- for (const item of items) {
5927
+ for (const [index, item] of items.entries()) {
4927
5928
  if (item.expression.kind === "list") {
4928
5929
  throw new TypeError("A row constructor is only allowed in a comparison or IN list");
4929
5930
  }
4930
5931
  // A qualified wildcard has no single output name until expansion resolves its columns.
4931
5932
  if (item.expression.kind === "wildcard" && item.expression.table !== undefined)
4932
5933
  continue;
4933
- if (aliases.has(item.alias))
4934
- throw new TypeError(`Duplicate output column: ${item.alias}`);
5934
+ if (aliases.has(item.alias)) {
5935
+ if (explicitAliases[index] === true) {
5936
+ throw new TypeError(`Duplicate output column: ${item.alias}`);
5937
+ }
5938
+ const base = item.alias;
5939
+ let suffix = 2;
5940
+ while (aliases.has(`${base}_${String(suffix)}`))
5941
+ suffix++;
5942
+ item.alias = `${base}_${String(suffix)}`;
5943
+ }
4935
5944
  aliases.add(item.alias);
4936
5945
  }
4937
5946
  return items;
@@ -5087,15 +6096,126 @@ class Parser {
5087
6096
  return this.#derivedSource(derived, alias);
5088
6097
  }
5089
6098
  const table = this.#identifier();
6099
+ if (table.toUpperCase() === "LATERAL") {
6100
+ this.#expectPunctuation("(");
6101
+ const derived = this.#queryExpression("(lateral)");
6102
+ this.#expectPunctuation(")");
6103
+ const alias = this.#sourceAlias();
6104
+ if (alias === undefined)
6105
+ throw new TypeError("A LATERAL derived table requires an alias");
6106
+ if (this.#punctuation("("))
6107
+ this.#applyColumnAliases(derived);
6108
+ return { ...this.#derivedSource(derived, alias), lateral: true };
6109
+ }
5090
6110
  if (this.#peek().text === "(") {
5091
6111
  // Two row-producing sources the executors have no operator for; both parse far enough to
5092
6112
  // say so, rather than failing on the punctuation that follows.
5093
6113
  const upper = table.toUpperCase();
5094
- if (upper === "LATERAL") {
5095
- throw new TypeError("LATERAL sources are not supported");
5096
- }
5097
6114
  if (upper === "JSON_TABLE") {
5098
- throw new TypeError("JSON_TABLE is not supported; JSON_VALUE and JSON_QUERY read one value");
6115
+ this.#expectPunctuation("(");
6116
+ const document = this.#expression();
6117
+ if (containsParameter(document) ||
6118
+ expressionColumns(document).length > 0 ||
6119
+ hasAggregate(document)) {
6120
+ throw new TypeError("JSON_TABLE currently requires a constant document");
6121
+ }
6122
+ this.#expectPunctuation(",");
6123
+ const rowPath = this.#take("string").text;
6124
+ this.#keyword("COLUMNS");
6125
+ this.#expectPunctuation("(");
6126
+ const columns = [];
6127
+ for (;;) {
6128
+ const name = this.#identifier();
6129
+ const columnType = this.#columnType();
6130
+ this.#keyword("PATH");
6131
+ columns.push({ name, ...columnType, path: this.#take("string").text });
6132
+ if (!this.#punctuation(","))
6133
+ break;
6134
+ }
6135
+ this.#expectPunctuation(")");
6136
+ this.#expectPunctuation(")");
6137
+ const alias = this.#sourceAlias();
6138
+ if (alias === undefined)
6139
+ throw new TypeError("JSON_TABLE requires an alias");
6140
+ const raw = externalSqlDomainValue(evaluate(document, {}));
6141
+ if (typeof raw !== "string")
6142
+ throw new TypeError("JSON_TABLE document must be JSON text");
6143
+ let parsed;
6144
+ try {
6145
+ parsed = JSON.parse(raw);
6146
+ }
6147
+ catch {
6148
+ throw new TypeError("JSON_TABLE document is invalid JSON");
6149
+ }
6150
+ const members = rowPath === "$"
6151
+ ? [parsed]
6152
+ : rowPath === "$[*]" && Array.isArray(parsed)
6153
+ ? parsed
6154
+ : (() => {
6155
+ throw new TypeError("JSON_TABLE row path supports $ and $[*]");
6156
+ })();
6157
+ const blockFor = (member, empty = false) => ({
6158
+ sql: "(json table row)",
6159
+ base: { table: DUAL_TABLE, alias: DUAL_TABLE },
6160
+ joins: [],
6161
+ select: columns.map((column) => {
6162
+ const selected = jsonAtPath(JSON.stringify(member), column.path, "JSON_TABLE");
6163
+ const value = empty
6164
+ ? column.sqlDomain?.kind === "numeric"
6165
+ ? jsonTableColumnValue(column, 0)
6166
+ : column.type === "number"
6167
+ ? 0
6168
+ : column.type === "boolean"
6169
+ ? false
6170
+ : column.type === "datetime"
6171
+ ? new Date(0)
6172
+ : ""
6173
+ : jsonTableColumnValue(column, selected.found ? selected.value : null);
6174
+ return {
6175
+ expression: {
6176
+ kind: "literal",
6177
+ value,
6178
+ ...(column.sqlDomain === undefined
6179
+ ? {}
6180
+ : { internalSqlValue: true, sqlDomain: column.sqlDomain }),
6181
+ },
6182
+ alias: column.name,
6183
+ };
6184
+ }),
6185
+ predicates: empty
6186
+ ? [
6187
+ {
6188
+ left: { kind: "literal", value: false },
6189
+ operator: "IS TRUE",
6190
+ right: { kind: "literal", value: true },
6191
+ },
6192
+ ]
6193
+ : [],
6194
+ groupBy: [],
6195
+ having: [],
6196
+ orderBy: [],
6197
+ });
6198
+ const blocks = members.length === 0 ? [blockFor(null, true)] : members.map((member) => blockFor(member));
6199
+ const firstBlock = blocks[0];
6200
+ if (firstBlock === undefined)
6201
+ throw new TypeError("JSON_TABLE requires a row source");
6202
+ const derived = blocks.length === 1
6203
+ ? firstBlock
6204
+ : {
6205
+ sql: "(json table)",
6206
+ base: {
6207
+ table: `(json table ${String(this.nextDerivedSequence())})`,
6208
+ alias: "json_table",
6209
+ union: { blocks, ops: blocks.slice(1).map(() => "union all") },
6210
+ },
6211
+ joins: [],
6212
+ select: [{ expression: { kind: "wildcard" }, alias: "*" }],
6213
+ predicates: [],
6214
+ groupBy: [],
6215
+ having: [],
6216
+ orderBy: [],
6217
+ };
6218
+ return this.#derivedSource(derived, alias);
5099
6219
  }
5100
6220
  }
5101
6221
  const candidate = this.#recursiveCandidate;
@@ -5385,14 +6505,13 @@ class Parser {
5385
6505
  // After a value expression, NOT can only introduce NOT BETWEEN / NOT IN / NOT LIKE.
5386
6506
  let negated = false;
5387
6507
  if (this.#isKeyword("NOT")) {
6508
+ const next = this.tokens[this.#index + 1];
6509
+ const nextWord = next?.kind === "identifier" && next.quoted !== true ? next.text.toUpperCase() : "";
6510
+ // In a column definition, `DEFAULT <expression> NOT NULL` ends the expression here.
6511
+ if (!["BETWEEN", "IN", "LIKE", "ILIKE", "SIMILAR"].includes(nextWord))
6512
+ return left;
5388
6513
  this.#keyword("NOT");
5389
6514
  negated = true;
5390
- if (!this.#isKeyword("BETWEEN") &&
5391
- !this.#isKeyword("IN") &&
5392
- !this.#isKeyword("LIKE") &&
5393
- !this.#isKeyword("ILIKE")) {
5394
- throw new TypeError(`Expected BETWEEN, IN, LIKE, or ILIKE after NOT`);
5395
- }
5396
6515
  }
5397
6516
  if (this.#isKeyword("BETWEEN")) {
5398
6517
  this.#keyword("BETWEEN");
@@ -5469,6 +6588,26 @@ class Parser {
5469
6588
  ...(escape === undefined ? {} : { escape }),
5470
6589
  };
5471
6590
  }
6591
+ if (this.#isKeyword("SIMILAR")) {
6592
+ this.#keyword("SIMILAR");
6593
+ this.#keyword("TO");
6594
+ const pattern = this.#additive();
6595
+ let escape;
6596
+ if (this.#isKeyword("ESCAPE")) {
6597
+ this.#keyword("ESCAPE");
6598
+ escape = this.#take("string").text;
6599
+ if (Array.from(escape).length !== 1) {
6600
+ throw new TypeError("SIMILAR TO ESCAPE takes a single character");
6601
+ }
6602
+ }
6603
+ return {
6604
+ kind: "condition",
6605
+ operator: negated ? "NOT SIMILAR TO" : "SIMILAR TO",
6606
+ left,
6607
+ right: pattern,
6608
+ ...(escape === undefined ? {} : { escape }),
6609
+ };
6610
+ }
5472
6611
  const token = this.#peek();
5473
6612
  if (token.kind === "operator" && ["=", "!=", "<>", ">", ">=", "<", "<="].includes(token.text)) {
5474
6613
  const operator = this.#comparison();
@@ -5495,6 +6634,15 @@ class Parser {
5495
6634
  #additive(minimumPrecedence = 0) {
5496
6635
  let left = this.#primary();
5497
6636
  for (;;) {
6637
+ if (this.#isKeyword("COLLATE")) {
6638
+ this.#keyword("COLLATE");
6639
+ left = {
6640
+ kind: "call",
6641
+ name: "MINNOW_COLLATE",
6642
+ arguments: [left, { kind: "literal", value: this.#identifier() }],
6643
+ };
6644
+ continue;
6645
+ }
5498
6646
  const operator = this.#peek().text;
5499
6647
  // || binds loosest, matching PostgreSQL: concatenation applies to whole arithmetic terms.
5500
6648
  const precedence = operator === "*" || operator === "/" || operator === "%"
@@ -5541,6 +6689,9 @@ class Parser {
5541
6689
  throw new TypeError("Use either ? or $n placeholders in one statement, not both");
5542
6690
  }
5543
6691
  this.#positionalParameters += 1;
6692
+ if (this.#positionalParameters > MAX_SQL_PARAMETERS) {
6693
+ throw new RangeError(`A SQL statement cannot exceed ${String(MAX_SQL_PARAMETERS)} parameters`);
6694
+ }
5544
6695
  return { kind: "parameter", index: this.#positionalParameters - 1 };
5545
6696
  }
5546
6697
  if (this.#positionalParameters > 0) {
@@ -5550,6 +6701,9 @@ class Parser {
5550
6701
  if (!Number.isInteger(number) || number < 1) {
5551
6702
  throw new TypeError(`Parameter numbers start at $1: $${token.text}`);
5552
6703
  }
6704
+ if (number > MAX_SQL_PARAMETERS) {
6705
+ throw new RangeError(`A SQL statement cannot exceed ${String(MAX_SQL_PARAMETERS)} parameters`);
6706
+ }
5553
6707
  this.#highestNumberedParameter = Math.max(this.#highestNumberedParameter, number);
5554
6708
  return { kind: "parameter", index: number - 1 };
5555
6709
  }
@@ -5559,7 +6713,11 @@ class Parser {
5559
6713
  return this.#parameterExpression();
5560
6714
  if (token.kind === "number") {
5561
6715
  this.#index += 1;
5562
- return { kind: "literal", value: Number(token.text) };
6716
+ const value = Number(token.text);
6717
+ if (!token.text.includes(".") && !Number.isSafeInteger(value)) {
6718
+ throw new SqlCompileError(`Integer literal is outside the exact safe range: ${token.text}`, token.start, token.end - token.start);
6719
+ }
6720
+ return { kind: "literal", value };
5563
6721
  }
5564
6722
  if (token.kind === "string") {
5565
6723
  this.#index += 1;
@@ -5797,13 +6955,37 @@ class Parser {
5797
6955
  }
5798
6956
  if (upper === "DATE" && this.#peek().kind === "string") {
5799
6957
  const date = new Date(`${this.#take("string").text}T00:00:00.000Z`);
5800
- if (!Number.isFinite(date.getTime()))
6958
+ if (!Number.isFinite(dateMilliseconds(date)))
5801
6959
  throw new TypeError("Invalid DATE literal");
5802
6960
  return { kind: "literal", value: date };
5803
6961
  }
5804
6962
  if ((upper === "TIMESTAMP" || upper === "DATETIME") && this.#peek().kind === "string") {
5805
6963
  return { kind: "literal", value: timestampLiteral(this.#take("string").text) };
5806
6964
  }
6965
+ if (upper === "TIME" && this.#peek().kind === "string") {
6966
+ return {
6967
+ kind: "literal",
6968
+ value: timeDomainValue(this.#take("string").text),
6969
+ internalSqlValue: true,
6970
+ sqlDomain: { kind: "time" },
6971
+ };
6972
+ }
6973
+ if (upper === "INTERVAL" && this.#peek().kind === "string") {
6974
+ return {
6975
+ kind: "literal",
6976
+ value: intervalDomainValue(this.#take("string").text),
6977
+ internalSqlValue: true,
6978
+ sqlDomain: { kind: "interval" },
6979
+ };
6980
+ }
6981
+ if (upper === "ARRAY" && this.#punctuation("[")) {
6982
+ const arguments_ = [];
6983
+ if (!this.#punctuation("]")) {
6984
+ arguments_.push(...this.#expressionList());
6985
+ this.#expectPunctuation("]");
6986
+ }
6987
+ return { kind: "call", name: "ARRAY", arguments: arguments_ };
6988
+ }
5807
6989
  if (this.#punctuation("(")) {
5808
6990
  if (upper === "ROW_NUMBER" ||
5809
6991
  upper === "RANK" ||
@@ -5936,8 +7118,15 @@ class Parser {
5936
7118
  // canonical plan names. ANY_VALUE picks an implementation-dependent row of the group
5937
7119
  // (T626); MIN is one such choice and reuses its accumulator exactly.
5938
7120
  const name = (upper === "ANY_VALUE" ? "MIN" : (functionSpellings.get(upper) ?? upper));
7121
+ if (name === "MINNOW_TUPLE_KEY" || name === "MINNOW_COLLATE") {
7122
+ throw new TypeError(`Unsupported function: ${identifier}`);
7123
+ }
5939
7124
  if (!aggregateNames.has(name) && !scalarFunctionNames.has(name))
5940
7125
  throw new TypeError(`Unsupported function: ${identifier}`);
7126
+ if (name === "NEXTVAL" || name === "CURRVAL")
7127
+ this.usesSequenceCalls = true;
7128
+ if (volatileScalarFunctionNames.has(name))
7129
+ this.usesVolatileFunctions = true;
5941
7130
  let distinct = false;
5942
7131
  if (this.#isKeyword("DISTINCT")) {
5943
7132
  if (!aggregateNames.has(name)) {
@@ -5951,15 +7140,32 @@ class Parser {
5951
7140
  this.#keyword("ALL");
5952
7141
  }
5953
7142
  const args = [];
7143
+ let aggregateOrderBy;
5954
7144
  if (!this.#punctuation(")")) {
5955
- args.push(...this.#expressionList());
7145
+ if (name === "STRING_AGG") {
7146
+ args.push(this.#expression());
7147
+ this.#expectPunctuation(",");
7148
+ args.push(this.#expression());
7149
+ if (this.#isKeyword("ORDER"))
7150
+ aggregateOrderBy = this.#orderByClause();
7151
+ }
7152
+ else {
7153
+ args.push(...this.#expressionList());
7154
+ }
5956
7155
  this.#expectPunctuation(")");
5957
7156
  }
5958
- if (distinct && (args.length !== 1 || args[0]?.kind === "wildcard")) {
7157
+ const distinctArity = name === "STRING_AGG" ? 2 : 1;
7158
+ if (distinct && (args.length !== distinctArity || args[0]?.kind === "wildcard")) {
5959
7159
  throw new TypeError(`${name}(DISTINCT) requires exactly one scalar argument`);
5960
7160
  }
5961
- if (aggregateNames.has(name) && args.length !== 1)
7161
+ if (name === "STRING_AGG" && args.length !== 2) {
7162
+ throw new TypeError("STRING_AGG requires a value and delimiter");
7163
+ }
7164
+ if (aggregateNames.has(name) && name !== "STRING_AGG" && args.length !== 1)
5962
7165
  throw new TypeError(`${name} requires exactly one argument`);
7166
+ if (name === "JSON_ARRAYAGG" && args[0]?.kind === "wildcard") {
7167
+ throw new TypeError("JSON_ARRAYAGG requires a scalar value expression");
7168
+ }
5963
7169
  if (name === "ROUND" && (args.length < 1 || args.length > 2))
5964
7170
  throw new TypeError("ROUND requires one or two arguments");
5965
7171
  if (name === "COALESCE" && args.length < 1)
@@ -5988,6 +7194,9 @@ class Parser {
5988
7194
  if (statementDatetimeNames.has(name) && args.length !== 0) {
5989
7195
  throw new TypeError(`${name} takes no arguments`);
5990
7196
  }
7197
+ if (volatileScalarFunctionNames.has(name) && args.length !== 0) {
7198
+ throw new TypeError(`${name} takes no arguments`);
7199
+ }
5991
7200
  if ((name === "JSON_VALUE" || name === "JSON_QUERY" || name === "JSON_EXISTS") &&
5992
7201
  args.length !== 2) {
5993
7202
  throw new TypeError(`${name} requires a JSON document and a path`);
@@ -6028,6 +7237,9 @@ class Parser {
6028
7237
  }
6029
7238
  }
6030
7239
  if (aggregateNames.has(name) && this.#isKeyword("FILTER")) {
7240
+ if (name === "JSON_ARRAYAGG") {
7241
+ throw new TypeError("JSON_ARRAYAGG FILTER is not supported; filter rows in WHERE");
7242
+ }
6031
7243
  // FILTER (WHERE cond) desugars into the aggregate's argument: rows failing the filter
6032
7244
  // contribute NULL, which every aggregate skips — COUNT(*) counts a CASE over 1.
6033
7245
  this.#keyword("FILTER");
@@ -6048,6 +7260,12 @@ class Parser {
6048
7260
  });
6049
7261
  }
6050
7262
  if (aggregateNames.has(name) && this.#isKeyword("OVER")) {
7263
+ if (name === "JSON_ARRAYAGG") {
7264
+ throw new TypeError("JSON_ARRAYAGG window use is not supported");
7265
+ }
7266
+ if (name === "STRING_AGG") {
7267
+ throw new TypeError("STRING_AGG window use is not supported");
7268
+ }
6051
7269
  if (distinct)
6052
7270
  throw new TypeError("DISTINCT window aggregates are not supported");
6053
7271
  this.#keyword("OVER");
@@ -6065,7 +7283,13 @@ class Parser {
6065
7283
  ...(frame === undefined ? {} : { frame }),
6066
7284
  };
6067
7285
  }
6068
- return { kind: "call", name, arguments: args, ...(distinct ? { distinct: true } : {}) };
7286
+ return {
7287
+ kind: "call",
7288
+ name,
7289
+ arguments: args,
7290
+ ...(distinct ? { distinct: true } : {}),
7291
+ ...(aggregateOrderBy === undefined ? {} : { aggregateOrderBy }),
7292
+ };
6069
7293
  }
6070
7294
  let reference = identifier;
6071
7295
  if (this.#punctuation(".")) {
@@ -6655,7 +7879,9 @@ export function expandDistinctWildcard(plan, columnsOf) {
6655
7879
  if (columns === undefined || columns.length === 0) {
6656
7880
  throw new TypeError(`SELECT DISTINCT * requires known columns for: ${source.table}`);
6657
7881
  }
6658
- return columns.map((name) => {
7882
+ return columns
7883
+ .filter((name) => !name.startsWith("\0"))
7884
+ .map((name) => {
6659
7885
  const output = multiple ? `${source.alias}.${name}` : name;
6660
7886
  return { expression: { kind: "column", reference: output }, alias: output };
6661
7887
  });
@@ -6669,16 +7895,20 @@ export function expandDistinctWildcard(plan, columnsOf) {
6669
7895
  * table's columns positionally (E051-09). The table's own column order is only known here, so
6670
7896
  * the parser records the names and this pass — one per execution entry — applies them.
6671
7897
  */
6672
- /** Whether any block of the plan reads a table name the catalog answers with a view. */
6673
- export function planReadsViews(plan, isView) {
6674
- if ([plan.base, ...plan.joins].some((source) => source.derived === undefined && isView(source.table)))
7898
+ /** Whether any block of the plan reads a table accepted by the supplied catalog predicate. */
7899
+ export function planReadsTable(plan, matches) {
7900
+ if ([plan.base, ...plan.joins].some((source) => source.derived === undefined && matches(source.table)))
6675
7901
  return true;
6676
7902
  let nested = false;
6677
7903
  forEachNestedBlock(plan, (inner) => {
6678
- nested ||= planReadsViews(inner, isView);
7904
+ nested ||= planReadsTable(inner, matches);
6679
7905
  });
6680
7906
  return nested;
6681
7907
  }
7908
+ /** Whether any block of the plan reads a table name the catalog answers with a view. */
7909
+ export function planReadsViews(plan, isView) {
7910
+ return planReadsTable(plan, isView);
7911
+ }
6682
7912
  /**
6683
7913
  * Replaces every reference to a view with the query it stands for, as a derived table under the
6684
7914
  * reference's own alias (F031-02). A view whose body reads another view expands too, up to a
@@ -6778,11 +8008,13 @@ export function withTiesPlan(plan) {
6778
8008
  */
6779
8009
  function sourceWildcardColumns(source, columnsOf) {
6780
8010
  if (source.derived !== undefined)
6781
- return source.derived.select.map((item) => item.alias);
8011
+ return source.derived.select.map((item) => item.alias).filter((name) => !name.startsWith("\0"));
6782
8012
  if (source.union !== undefined) {
6783
- return source.union.blocks[0]?.select.map((item) => item.alias);
8013
+ return source.union.blocks[0]?.select
8014
+ .map((item) => item.alias)
8015
+ .filter((name) => !name.startsWith("\0"));
6784
8016
  }
6785
- return columnsOf(source.table);
8017
+ return columnsOf(source.table)?.filter((name) => !name.startsWith("\0"));
6786
8018
  }
6787
8019
  /** Whether any block of the plan still carries an unresolved NATURAL join marker. */
6788
8020
  export function planHasNaturalJoins(plan) {
@@ -7104,7 +8336,7 @@ export function transparentProjectionSource(plan) {
7104
8336
  }
7105
8337
  /** Projects a result to a wrapper's visible aliases, preserving row order. */
7106
8338
  export function projectResultColumns(result, aliases) {
7107
- return {
8339
+ return copyQueryResultExternalization(result, {
7108
8340
  columns: [...aliases],
7109
8341
  rows: result.rows.map((row) => {
7110
8342
  const projected = {};
@@ -7112,7 +8344,7 @@ export function projectResultColumns(result, aliases) {
7112
8344
  projected[alias] = row[alias] ?? null;
7113
8345
  return projected;
7114
8346
  }),
7115
- };
8347
+ });
7116
8348
  }
7117
8349
  /** Names a derived (subquery or expanded CTE) source under the shared sequence. */
7118
8350
  export function derivedTableSource(derived, alias, nextSequence) {
@@ -7146,6 +8378,44 @@ function renameBlockOutputs(block, columns, name) {
7146
8378
  * writes a T and leaves the time off entirely for midnight. A literal without a zone is UTC, the
7147
8379
  * same reading `DATE` already takes and the same one every datetime in a Minnow database has.
7148
8380
  */
8381
+ function jsonTableColumnValue(column, value) {
8382
+ if (value === null || value === undefined)
8383
+ return null;
8384
+ if (column.sqlDomain !== undefined) {
8385
+ const input = column.sqlDomain.kind === "array" && typeof value !== "string"
8386
+ ? JSON.stringify(value)
8387
+ : value;
8388
+ return normalizeSqlDomainValue(column.sqlDomain, input);
8389
+ }
8390
+ if (column.type === "string") {
8391
+ return typeof value === "string" ? value : JSON.stringify(value);
8392
+ }
8393
+ if (column.type === "boolean") {
8394
+ if (typeof value !== "boolean")
8395
+ throw new TypeError("JSON_TABLE boolean column needs a boolean");
8396
+ return value;
8397
+ }
8398
+ if (column.type === "number") {
8399
+ const number = typeof value === "number" ? value : Number(value);
8400
+ if (!Number.isFinite(number) || (column.integer === true && !Number.isSafeInteger(number))) {
8401
+ throw new TypeError("JSON_TABLE numeric column has an invalid value");
8402
+ }
8403
+ return number;
8404
+ }
8405
+ let date;
8406
+ if (value instanceof Date)
8407
+ date = value;
8408
+ else if (typeof value === "string")
8409
+ date = new Date(value);
8410
+ else if (typeof value === "number")
8411
+ date = new Date(value);
8412
+ else
8413
+ throw new TypeError("JSON_TABLE datetime is invalid");
8414
+ if (!Number.isFinite(dateMilliseconds(date))) {
8415
+ throw new TypeError("JSON_TABLE datetime is invalid");
8416
+ }
8417
+ return date;
8418
+ }
7149
8419
  export function timestampLiteral(text) {
7150
8420
  const trimmed = text.trim();
7151
8421
  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);
@@ -7154,8 +8424,9 @@ export function timestampLiteral(text) {
7154
8424
  const [, day, time = "00:00:00", zone = "Z"] = match;
7155
8425
  const seconds = time.length === 5 ? `${time}:00` : time;
7156
8426
  const date = new Date(`${String(day)}T${seconds}${zone === "Z" ? "Z" : zone}`);
7157
- if (!Number.isFinite(date.getTime()))
8427
+ if (!Number.isFinite(dateMilliseconds(date))) {
7158
8428
  throw new TypeError(`Invalid TIMESTAMP literal: ${text}`);
8429
+ }
7159
8430
  return date;
7160
8431
  }
7161
8432
  /** The parser's OFFSET range contract, shared with the typed builder. */
@@ -7330,6 +8601,24 @@ function validNumericLiteral(text, radix) {
7330
8601
  }
7331
8602
  function tokenize(sql) {
7332
8603
  const tokens = [];
8604
+ let nestingDepth = 0;
8605
+ const push = (token) => {
8606
+ if (tokens.length >= MAX_SQL_TOKENS) {
8607
+ throw new SqlCompileError(`A SQL statement cannot exceed ${String(MAX_SQL_TOKENS)} tokens`, token.start, Math.max(token.end - token.start, 1));
8608
+ }
8609
+ if (token.kind === "punctuation") {
8610
+ if (token.text === "(" || token.text === "[") {
8611
+ nestingDepth += 1;
8612
+ if (nestingDepth > MAX_SQL_NESTING_DEPTH) {
8613
+ throw new SqlCompileError(`SQL nesting cannot exceed ${String(MAX_SQL_NESTING_DEPTH)} levels`, token.start, 1);
8614
+ }
8615
+ }
8616
+ else if ((token.text === ")" || token.text === "]") && nestingDepth > 0) {
8617
+ nestingDepth -= 1;
8618
+ }
8619
+ }
8620
+ tokens.push(token);
8621
+ };
7333
8622
  let index = 0;
7334
8623
  while (index < sql.length) {
7335
8624
  const character = sql[index] ?? "";
@@ -7341,7 +8630,7 @@ function tokenize(sql) {
7341
8630
  const start = index++;
7342
8631
  while (index < sql.length && /[A-Za-z0-9_]/.test(sql[index] ?? ""))
7343
8632
  index += 1;
7344
- tokens.push({ kind: "identifier", text: sql.slice(start, index), start, end: index });
8633
+ push({ kind: "identifier", text: sql.slice(start, index), start, end: index });
7345
8634
  continue;
7346
8635
  }
7347
8636
  if (/\d/.test(character)) {
@@ -7358,7 +8647,7 @@ function tokenize(sql) {
7358
8647
  if (!validNumericLiteral(digits, radix) || !Number.isSafeInteger(value)) {
7359
8648
  throw new SqlCompileError(`Invalid number: ${sql.slice(start, index)}`, start, index - start);
7360
8649
  }
7361
- tokens.push({ kind: "number", text: String(value), start, end: index });
8650
+ push({ kind: "number", text: String(value), start, end: index });
7362
8651
  continue;
7363
8652
  }
7364
8653
  index += 1;
@@ -7368,7 +8657,7 @@ function tokenize(sql) {
7368
8657
  const text = sql.slice(start, index);
7369
8658
  if (!validNumericLiteral(text, 10))
7370
8659
  throw new SqlCompileError(`Invalid number: ${text}`, start, index - start);
7371
- tokens.push({ kind: "number", text: text.replaceAll("_", ""), start, end: index });
8660
+ push({ kind: "number", text: text.replaceAll("_", ""), start, end: index });
7372
8661
  continue;
7373
8662
  }
7374
8663
  if (character === "'") {
@@ -7390,7 +8679,7 @@ function tokenize(sql) {
7390
8679
  }
7391
8680
  if (!closed)
7392
8681
  throw new SqlCompileError("Unterminated string literal", start, sql.length - start);
7393
- tokens.push({ kind: "string", text: value, start, end: index });
8682
+ push({ kind: "string", text: value, start, end: index });
7394
8683
  continue;
7395
8684
  }
7396
8685
  if (character === '"') {
@@ -7416,11 +8705,11 @@ function tokenize(sql) {
7416
8705
  if (value.length === 0) {
7417
8706
  throw new SqlCompileError("Quoted identifiers cannot be empty", start, index - start);
7418
8707
  }
7419
- tokens.push({ kind: "identifier", text: value, quoted: true, start, end: index });
8708
+ push({ kind: "identifier", text: value, quoted: true, start, end: index });
7420
8709
  continue;
7421
8710
  }
7422
8711
  if (character === "?") {
7423
- tokens.push({ kind: "parameter", text: "", start: index, end: index + 1 });
8712
+ push({ kind: "parameter", text: "", start: index, end: index + 1 });
7424
8713
  index += 1;
7425
8714
  continue;
7426
8715
  }
@@ -7432,7 +8721,7 @@ function tokenize(sql) {
7432
8721
  if (digits.length === 0) {
7433
8722
  throw new SqlCompileError("Expected a parameter number after $", start, 1);
7434
8723
  }
7435
- tokens.push({ kind: "parameter", text: digits, start, end: index });
8724
+ push({ kind: "parameter", text: digits, start, end: index });
7436
8725
  continue;
7437
8726
  }
7438
8727
  const pair = sql.slice(index, index + 2);
@@ -7454,19 +8743,19 @@ function tokenize(sql) {
7454
8743
  if (character === ";") {
7455
8744
  // Statement separators lex normally; the routers reject them everywhere except inside
7456
8745
  // a CREATE TRIGGER body, which is the one multi-statement construct.
7457
- tokens.push({ kind: "punctuation", text: ";", start: index, end: index + 1 });
8746
+ push({ kind: "punctuation", text: ";", start: index, end: index + 1 });
7458
8747
  index += 1;
7459
8748
  continue;
7460
8749
  }
7461
8750
  if ([">=", "<=", "!=", "<>", "||"].includes(pair)) {
7462
- tokens.push({ kind: "operator", text: pair, start: index, end: index + 2 });
8751
+ push({ kind: "operator", text: pair, start: index, end: index + 2 });
7463
8752
  index += 2;
7464
8753
  continue;
7465
8754
  }
7466
8755
  if (["+", "-", "*", "/", "%", "=", ">", "<"].includes(character))
7467
- tokens.push({ kind: "operator", text: character, start: index, end: index + 1 });
7468
- else if (["(", ")", ",", "."].includes(character))
7469
- tokens.push({ kind: "punctuation", text: character, start: index, end: index + 1 });
8756
+ push({ kind: "operator", text: character, start: index, end: index + 1 });
8757
+ else if (["(", ")", "[", "]", ",", "."].includes(character))
8758
+ push({ kind: "punctuation", text: character, start: index, end: index + 1 });
7470
8759
  else
7471
8760
  throw new SqlCompileError(`Unsupported SQL character: ${character}`, index, 1);
7472
8761
  index += 1;
@@ -7474,4 +8763,11 @@ function tokenize(sql) {
7474
8763
  tokens.push({ kind: "eof", text: "", start: sql.length, end: sql.length });
7475
8764
  return tokens;
7476
8765
  }
7477
- //# sourceMappingURL=query.js.map
8766
+ function validateSqlSource(sql) {
8767
+ if (typeof sql !== "string")
8768
+ throw new TypeError("SQL must be a string");
8769
+ if (sql.length > MAX_SQL_TEXT_CHARACTERS) {
8770
+ throw new RangeError(`SQL text cannot exceed ${String(MAX_SQL_TEXT_CHARACTERS)} characters`);
8771
+ }
8772
+ assertWellFormedString(sql, "SQL text");
8773
+ }