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