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