@minnowdb/core 0.6.8 → 0.7.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 +3 -2
- package/dist/date-value.d.ts +5 -0
- package/dist/date-value.js +41 -0
- package/dist/engine/database.js +1085 -81
- package/dist/engine/optimizer.js +446 -18
- package/dist/engine/point-read.d.ts +4 -2
- package/dist/engine/point-read.js +16 -6
- package/dist/engine/query.d.ts +53 -1
- package/dist/engine/query.js +1160 -239
- package/dist/engine/sql-domains.d.ts +7 -1
- package/dist/engine/sql-domains.js +38 -1
- package/dist/engine/sql-functions.d.ts +11 -0
- package/dist/engine/sql-functions.js +1186 -0
- package/dist/engine/sql-semantics.d.ts +25 -4
- package/dist/engine/sql-semantics.js +134 -1
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +718 -124
- package/dist/plan/model.d.ts +21 -1
- package/dist/storage/types.js +44 -18
- package/package.json +1 -1
- package/postgres-feature-profile.json +15 -5
- package/sql-feature-matrix.json +277 -6
package/dist/engine/query.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { copyDate, dateIsoString, dateMilliseconds,
|
|
1
|
+
import { civilFromDays, copyDate, dateIsoString, dateMilliseconds, daysFromCivil, epochDays, dateUtcDate, dateUtcFullYear, dateUtcMonth, setDateUtcDate, setDateUtcMonth, } from "../date-value.js";
|
|
2
2
|
import { crossJoinPlan } from "../plan/model.js";
|
|
3
3
|
import { assertWellFormedString, wellFormedUtf8ByteLength } from "../block-format/unicode.js";
|
|
4
4
|
import { MAX_SQL_NESTING_DEPTH, MAX_SQL_PARAMETERS, MAX_SQL_SCALAR_RESULT_CHARACTERS, MAX_SQL_TEXT_CHARACTERS, MAX_SQL_TOKENS, } from "./cache-limits.js";
|
|
@@ -6,7 +6,8 @@ import { SqlCompileError } from "./errors.js";
|
|
|
6
6
|
import { cachedQueryTerms, ftsBm25Row, FtsStatsAccumulator, ftsMatchTruth, renderDocumentValue, tokenize as ftsTokenize, validateFtsQuery, } from "./fts.js";
|
|
7
7
|
import { QueryMemoryContext } from "./memory.js";
|
|
8
8
|
import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
|
|
9
|
-
import { stringArgument } from "./sql-semantics.js";
|
|
9
|
+
import { coerceComparisonOperands, coercedComparable, parseSqlTimestampText, stringArgument, } from "./sql-semantics.js";
|
|
10
|
+
import { simpleScalarFunctions } from "./sql-functions.js";
|
|
10
11
|
import { jsonArrowStep, jsonAtPath, jsonConstructor, jsonIsValid, parseJsonPath, } from "./sql-json.js";
|
|
11
12
|
import { optimizePlan } from "./optimizer.js";
|
|
12
13
|
import { compareSqlValues as compareValues, compileLikePattern, compileSimilarPattern, encodeSqlEqualityValue, roundSqlNumber, } from "./sql-semantics.js";
|
|
@@ -71,6 +72,7 @@ export const scalarFunctionNames = new Set([
|
|
|
71
72
|
"CURRVAL",
|
|
72
73
|
"RANDOM",
|
|
73
74
|
"GEN_RANDOM_UUID",
|
|
75
|
+
...Array.from(simpleScalarFunctions.keys()),
|
|
74
76
|
]);
|
|
75
77
|
/** Functions whose answer can change without any catalog or input-row change. */
|
|
76
78
|
export const volatileScalarFunctionNames = new Set([
|
|
@@ -95,6 +97,8 @@ const statementDatetimeNames = new Set([
|
|
|
95
97
|
/** Standard function spellings that share one canonical plan name. */
|
|
96
98
|
const functionSpellings = new Map([
|
|
97
99
|
["SUBSTRING", "SUBSTR"],
|
|
100
|
+
// DATE_PART('field', value) is EXTRACT(field FROM value) with the field as a string.
|
|
101
|
+
["DATE_PART", "EXTRACT"],
|
|
98
102
|
["CEILING", "CEIL"],
|
|
99
103
|
["CHAR_LENGTH", "LENGTH"],
|
|
100
104
|
["CHARACTER_LENGTH", "LENGTH"],
|
|
@@ -103,6 +107,8 @@ const statementDatetimeAliases = new Map([
|
|
|
103
107
|
["CURRENT_DATE", "CURRENT_DATE"],
|
|
104
108
|
["CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP"],
|
|
105
109
|
["LOCALTIMESTAMP", "CURRENT_TIMESTAMP"],
|
|
110
|
+
// PostgreSQL's now() is transaction_timestamp(): one reading per statement, like the rest.
|
|
111
|
+
["NOW", "CURRENT_TIMESTAMP"],
|
|
106
112
|
["CURRENT_TIME", "LOCALTIME"],
|
|
107
113
|
["LOCALTIME", "LOCALTIME"],
|
|
108
114
|
]);
|
|
@@ -190,7 +196,10 @@ function castValue(value, target) {
|
|
|
190
196
|
if (parsed !== undefined) {
|
|
191
197
|
if (target !== "number-integer")
|
|
192
198
|
return parsed;
|
|
193
|
-
|
|
199
|
+
// PostgreSQL rounds a double precision value cast to an integer type to the nearest
|
|
200
|
+
// integer, ties to even (2.5 -> 2, 3.5 -> 4, -2.5 -> -2); SQLite truncates. A stored
|
|
201
|
+
// number is double precision, so the engine follows PostgreSQL's float8 cast.
|
|
202
|
+
const integer = roundHalfToEven(parsed);
|
|
194
203
|
if (!Number.isSafeInteger(integer)) {
|
|
195
204
|
throw new RangeError(`Integer cast is outside the exact safe range: ${String(value)}`);
|
|
196
205
|
}
|
|
@@ -222,7 +231,7 @@ function castValue(value, target) {
|
|
|
222
231
|
return value;
|
|
223
232
|
const external = externalSqlDomainValue(value);
|
|
224
233
|
if (typeof external === "string" || typeof external === "number") {
|
|
225
|
-
const parsed = new Date(external);
|
|
234
|
+
const parsed = typeof external === "string" ? datetimeText(external) : new Date(external);
|
|
226
235
|
if (Number.isFinite(dateMilliseconds(parsed)))
|
|
227
236
|
return parsed;
|
|
228
237
|
throw new TypeError(`Cannot cast this value to a datetime: ${String(value)}`);
|
|
@@ -230,6 +239,16 @@ function castValue(value, target) {
|
|
|
230
239
|
}
|
|
231
240
|
throw new TypeError(`Unsupported CAST: ${typeof value} to ${target}`);
|
|
232
241
|
}
|
|
242
|
+
/** Nearest integer with ties to even, the rounding PostgreSQL applies to float8 -> integer. */
|
|
243
|
+
function roundHalfToEven(value) {
|
|
244
|
+
const floor = Math.floor(value);
|
|
245
|
+
const fraction = value - floor;
|
|
246
|
+
if (fraction < 0.5)
|
|
247
|
+
return floor;
|
|
248
|
+
if (fraction > 0.5)
|
|
249
|
+
return floor + 1;
|
|
250
|
+
return floor % 2 === 0 ? floor : floor + 1;
|
|
251
|
+
}
|
|
233
252
|
/**
|
|
234
253
|
* Evaluates one scalar function over already-evaluated argument values. Every executor calls
|
|
235
254
|
* through here, so a function behaves identically in the row executor, the vectorized executor,
|
|
@@ -238,6 +257,97 @@ function castValue(value, target) {
|
|
|
238
257
|
* SUBSTR count characters, not UTF-16 units, matching SQLite and PostgreSQL.
|
|
239
258
|
*/
|
|
240
259
|
export function scalarFunctionValue(name, values) {
|
|
260
|
+
return scalarFunctionEvaluator(name)(values);
|
|
261
|
+
}
|
|
262
|
+
const scalarFunctionEvaluators = new Map();
|
|
263
|
+
/**
|
|
264
|
+
* The evaluator for one function name, resolved once. A call site that keeps the evaluator
|
|
265
|
+
* skips the name dispatch on every row; the registry functions and the hottest built-ins get
|
|
266
|
+
* a closure of their own, everything else a closure over the general dispatch.
|
|
267
|
+
*/
|
|
268
|
+
export function scalarFunctionEvaluator(name) {
|
|
269
|
+
let evaluator = scalarFunctionEvaluators.get(name);
|
|
270
|
+
if (evaluator === undefined) {
|
|
271
|
+
evaluator = buildScalarFunctionEvaluator(name);
|
|
272
|
+
scalarFunctionEvaluators.set(name, evaluator);
|
|
273
|
+
}
|
|
274
|
+
return evaluator;
|
|
275
|
+
}
|
|
276
|
+
function anyNull(values) {
|
|
277
|
+
for (const value of values)
|
|
278
|
+
if (value === null || value === undefined)
|
|
279
|
+
return true;
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
function buildScalarFunctionEvaluator(name) {
|
|
283
|
+
const simple = simpleScalarFunctions.get(name);
|
|
284
|
+
if (simple !== undefined) {
|
|
285
|
+
return simple.nullOnNull === false
|
|
286
|
+
? simple.evaluate
|
|
287
|
+
: (values) => (anyNull(values) ? null : simple.evaluate(values));
|
|
288
|
+
}
|
|
289
|
+
// The specialised closures below repeat the general dispatch's rules for their name: a NULL
|
|
290
|
+
// first argument is NULL, then the case body. Names are disjoint, so the earlier name checks
|
|
291
|
+
// in scalarFunctionValueGeneric can never claim one of these.
|
|
292
|
+
switch (name) {
|
|
293
|
+
case "DATE_TRUNC":
|
|
294
|
+
return (values) => dateTruncValue(values[0], values[1]);
|
|
295
|
+
case "EXTRACT":
|
|
296
|
+
return (values) => {
|
|
297
|
+
const first = values[0];
|
|
298
|
+
if (first === null || first === undefined)
|
|
299
|
+
return null;
|
|
300
|
+
return extractDatePart(typeof first === "string" ? first : "", values[1]);
|
|
301
|
+
};
|
|
302
|
+
case "ROUND":
|
|
303
|
+
return (values) => {
|
|
304
|
+
const first = values[0];
|
|
305
|
+
if (first === null || first === undefined)
|
|
306
|
+
return null;
|
|
307
|
+
if (values.length > 1 && (values[1] === null || values[1] === undefined))
|
|
308
|
+
return null;
|
|
309
|
+
const digits = values.length > 1 ? numeric(values[1]) : 0;
|
|
310
|
+
if (isExactNumeric(first) && Number.isInteger(digits) && digits >= 0) {
|
|
311
|
+
return exactNumericValue(first, undefined, digits);
|
|
312
|
+
}
|
|
313
|
+
return roundSqlNumber(numeric(first), digits);
|
|
314
|
+
};
|
|
315
|
+
case "FLOOR":
|
|
316
|
+
return (values) => values[0] === null || values[0] === undefined ? null : Math.floor(numeric(values[0]));
|
|
317
|
+
case "CEIL":
|
|
318
|
+
return (values) => values[0] === null || values[0] === undefined ? null : Math.ceil(numeric(values[0]));
|
|
319
|
+
case "ABS":
|
|
320
|
+
return (values) => values[0] === null || values[0] === undefined ? null : Math.abs(numeric(values[0]));
|
|
321
|
+
case "UPPER":
|
|
322
|
+
return (values) => {
|
|
323
|
+
const first = values[0];
|
|
324
|
+
if (first === null || first === undefined)
|
|
325
|
+
return null;
|
|
326
|
+
const source = stringArgument("UPPER", first);
|
|
327
|
+
assertScalarInputLength(source, "UPPER input");
|
|
328
|
+
return boundedScalarResult(source.toUpperCase(), "UPPER result");
|
|
329
|
+
};
|
|
330
|
+
case "LOWER":
|
|
331
|
+
return (values) => {
|
|
332
|
+
const first = values[0];
|
|
333
|
+
if (first === null || first === undefined)
|
|
334
|
+
return null;
|
|
335
|
+
const source = stringArgument("LOWER", first);
|
|
336
|
+
assertScalarInputLength(source, "LOWER input");
|
|
337
|
+
return boundedScalarResult(source.toLowerCase(), "LOWER result");
|
|
338
|
+
};
|
|
339
|
+
case "LENGTH":
|
|
340
|
+
return (values) => {
|
|
341
|
+
const first = values[0];
|
|
342
|
+
if (first === null || first === undefined)
|
|
343
|
+
return null;
|
|
344
|
+
return codePointLength(stringArgument("LENGTH", first));
|
|
345
|
+
};
|
|
346
|
+
default:
|
|
347
|
+
return (values) => scalarFunctionValueGeneric(name, values);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function scalarFunctionValueGeneric(name, values) {
|
|
241
351
|
if (name === "GROUPING") {
|
|
242
352
|
// T433. The grouping-sets desugar knows which columns each member aggregates away and
|
|
243
353
|
// replaces every GROUPING call with its constant; reaching here means there was no
|
|
@@ -281,6 +391,14 @@ export function scalarFunctionValue(name, values) {
|
|
|
281
391
|
if (name === "NEXTVAL" || name === "CURRVAL") {
|
|
282
392
|
throw new TypeError(`${name} must be resolved by the database catalog`);
|
|
283
393
|
}
|
|
394
|
+
const simple = simpleScalarFunctions.get(name);
|
|
395
|
+
if (simple !== undefined) {
|
|
396
|
+
if (simple.nullOnNull !== false &&
|
|
397
|
+
values.some((value) => value === null || value === undefined)) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
return simple.evaluate(values);
|
|
401
|
+
}
|
|
284
402
|
if (name === "MINNOW_TUPLE_KEY") {
|
|
285
403
|
// SQL equality cannot match a tuple containing NULL. JSON stringification of the tagged
|
|
286
404
|
// scalar equality encodings is prefix-free and keeps strings, numbers, booleans, and
|
|
@@ -358,6 +476,11 @@ export function scalarFunctionValue(name, values) {
|
|
|
358
476
|
if (values.length > 1 && (values[1] === null || values[1] === undefined))
|
|
359
477
|
return null;
|
|
360
478
|
const digits = values.length > 1 ? numeric(values[1]) : 0;
|
|
479
|
+
// An exact NUMERIC rounds exactly, to the requested scale, half away from zero as
|
|
480
|
+
// PostgreSQL's numeric ROUND does; a double takes the float path.
|
|
481
|
+
if (isExactNumeric(first) && Number.isInteger(digits) && digits >= 0) {
|
|
482
|
+
return exactNumericValue(first, undefined, digits);
|
|
483
|
+
}
|
|
361
484
|
return roundSqlNumber(numeric(first), digits);
|
|
362
485
|
}
|
|
363
486
|
case "ABS":
|
|
@@ -691,45 +814,47 @@ function dateAddValue(value, months, milliseconds) {
|
|
|
691
814
|
? dateDomainValue(dateIsoString(result).slice(0, 10))
|
|
692
815
|
: result;
|
|
693
816
|
}
|
|
694
|
-
function dateTruncValue(unit, value) {
|
|
817
|
+
export function dateTruncValue(unit, value) {
|
|
695
818
|
if (typeof unit !== "string" || !dateTruncUnits.has(unit.toLowerCase())) {
|
|
696
819
|
throw new TypeError("DATE_TRUNC requires a unit of year, quarter, month, week, day, hour, minute, or second");
|
|
697
820
|
}
|
|
698
821
|
if (value === null || value === undefined)
|
|
699
822
|
return null;
|
|
700
823
|
const external = externalSqlDomainValue(value);
|
|
701
|
-
const
|
|
702
|
-
? value
|
|
824
|
+
const milliseconds = value instanceof Date
|
|
825
|
+
? dateMilliseconds(value)
|
|
703
826
|
: isDateDomainValue(value) && typeof external === "string"
|
|
704
|
-
?
|
|
827
|
+
? Date.parse(`${external}T00:00:00.000Z`)
|
|
705
828
|
: undefined;
|
|
706
|
-
if (
|
|
829
|
+
if (milliseconds === undefined)
|
|
707
830
|
throw new TypeError("DATE_TRUNC requires a date or datetime value");
|
|
831
|
+
if (!Number.isFinite(milliseconds))
|
|
832
|
+
return new Date(Number.NaN);
|
|
833
|
+
// Everything is UTC arithmetic on the epoch value: no Date is built until the answer, and the
|
|
834
|
+
// calendar fields come from the civil-date conversion rather than intrinsic getters.
|
|
708
835
|
const normalized = unit.toLowerCase();
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
return new Date(Date.UTC(year, month, day, dateUtcHours(input), dateUtcMinutes(input), dateUtcSeconds(input)));
|
|
732
|
-
}
|
|
836
|
+
const day = 86_400_000;
|
|
837
|
+
if (normalized === "second")
|
|
838
|
+
return new Date(Math.floor(milliseconds / 1000) * 1000);
|
|
839
|
+
if (normalized === "minute")
|
|
840
|
+
return new Date(Math.floor(milliseconds / 60_000) * 60_000);
|
|
841
|
+
if (normalized === "hour")
|
|
842
|
+
return new Date(Math.floor(milliseconds / 3_600_000) * 3_600_000);
|
|
843
|
+
const days = epochDays(milliseconds);
|
|
844
|
+
if (normalized === "day")
|
|
845
|
+
return new Date(days * day);
|
|
846
|
+
if (normalized === "week") {
|
|
847
|
+
// 1970-01-01 was a Thursday; weeks start on Monday.
|
|
848
|
+
const weekday = (((days + 3) % 7) + 7) % 7;
|
|
849
|
+
return new Date((days - weekday) * day);
|
|
850
|
+
}
|
|
851
|
+
const [year, month] = civilFromDays(days);
|
|
852
|
+
if (normalized === "month")
|
|
853
|
+
return new Date(daysFromCivil(year, month, 1) * day);
|
|
854
|
+
if (normalized === "quarter") {
|
|
855
|
+
return new Date(daysFromCivil(year, Math.floor(month / 3) * 3, 1) * day);
|
|
856
|
+
}
|
|
857
|
+
return new Date(daysFromCivil(year, 0, 1) * day);
|
|
733
858
|
}
|
|
734
859
|
/** The output column type of one window: rankings and most aggregates count, MIN/MAX carry. */
|
|
735
860
|
export function windowOutputType(window, innerSchema) {
|
|
@@ -890,7 +1015,11 @@ export function compileQuery(sql, options = {}) {
|
|
|
890
1015
|
let compiled;
|
|
891
1016
|
try {
|
|
892
1017
|
resolvePlanExactNumericConstants(plan);
|
|
893
|
-
|
|
1018
|
+
if (options.optimize === false && planHasPendingSelectShapes(plan)) {
|
|
1019
|
+
plan.preserveUnoptimizedShape = true;
|
|
1020
|
+
}
|
|
1021
|
+
compiled =
|
|
1022
|
+
options.optimize === false || planHasPendingSelectShapes(plan) ? plan : optimizePlan(plan);
|
|
894
1023
|
}
|
|
895
1024
|
catch (error) {
|
|
896
1025
|
// Compile-time rewrites (for example decorrelation) reject unsupported shapes; those
|
|
@@ -899,12 +1028,18 @@ export function compileQuery(sql, options = {}) {
|
|
|
899
1028
|
}
|
|
900
1029
|
if (parser.parameterCount > 0)
|
|
901
1030
|
compiled.parameterCount = parser.parameterCount;
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1031
|
+
// The ORDER-BY-expression desugar wraps the parsed block in a projection, and execution paths
|
|
1032
|
+
// run that inner block on its own. The flags have to travel with it, or CURRENT_TIMESTAMP in a
|
|
1033
|
+
// WHERE clause would reach the executor unresolved whenever the query sorts by a hidden column.
|
|
1034
|
+
const flagged = [compiled, transparentProjectionSource(compiled)?.inner].filter((block) => block !== undefined);
|
|
1035
|
+
for (const block of flagged) {
|
|
1036
|
+
if (parser.usesStatementDatetime)
|
|
1037
|
+
block.usesStatementDatetime = true;
|
|
1038
|
+
if (parser.usesSequenceCalls)
|
|
1039
|
+
block.usesSequenceCalls = true;
|
|
1040
|
+
if (parser.usesVolatileFunctions)
|
|
1041
|
+
block.usesVolatileFunctions = true;
|
|
1042
|
+
}
|
|
908
1043
|
return compiled;
|
|
909
1044
|
}
|
|
910
1045
|
/**
|
|
@@ -925,6 +1060,9 @@ function columnDefaultFor(expression, sql) {
|
|
|
925
1060
|
export function isDefaultInsertValue(value) {
|
|
926
1061
|
return (typeof value === "object" && value !== null && !(value instanceof Date) && "default" in value);
|
|
927
1062
|
}
|
|
1063
|
+
export function isDeferredInsertExpression(value) {
|
|
1064
|
+
return (typeof value === "object" && value !== null && !(value instanceof Date) && "expression" in value);
|
|
1065
|
+
}
|
|
928
1066
|
/**
|
|
929
1067
|
* Parses CREATE TRIGGER name AFTER INSERT|UPDATE|DELETE ON table [FOR EACH ROW]
|
|
930
1068
|
* BEGIN insert; ... END. Body statements are INSERT ... VALUES with NEW.col / OLD.col
|
|
@@ -1914,6 +2052,11 @@ export function inferBlockSchema(plan, schemas) {
|
|
|
1914
2052
|
return undefined;
|
|
1915
2053
|
if (expression.name === "CURRENT_DATE")
|
|
1916
2054
|
return { kind: "date" };
|
|
2055
|
+
const simple = simpleScalarFunctions.get(expression.name);
|
|
2056
|
+
if (simple?.returns === "date")
|
|
2057
|
+
return { kind: "date" };
|
|
2058
|
+
if (simple?.returns === "interval")
|
|
2059
|
+
return { kind: "interval" };
|
|
1917
2060
|
if (expression.name === "CAST") {
|
|
1918
2061
|
const target = expression.arguments[1];
|
|
1919
2062
|
return target?.kind === "literal" && typeof target.value === "string"
|
|
@@ -2016,11 +2159,10 @@ export function inferBlockSchema(plan, schemas) {
|
|
|
2016
2159
|
}
|
|
2017
2160
|
if (expression.kind === "binary") {
|
|
2018
2161
|
if (expression.operator === "||") {
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
}
|
|
2162
|
+
// PostgreSQL's text || anynonarray: one side must be text, the other renders as text.
|
|
2163
|
+
const types = [expression.left, expression.right].map(infer);
|
|
2164
|
+
if (!types.some((type) => type === "string" || type === "null")) {
|
|
2165
|
+
throw new TypeError("|| requires a string operand");
|
|
2024
2166
|
}
|
|
2025
2167
|
return "string";
|
|
2026
2168
|
}
|
|
@@ -2072,6 +2214,15 @@ export function inferBlockSchema(plan, schemas) {
|
|
|
2072
2214
|
}
|
|
2073
2215
|
if (expression.name === "JSON_EXISTS" || expression.name === "IS_JSON")
|
|
2074
2216
|
return "boolean";
|
|
2217
|
+
const simple = simpleScalarFunctions.get(expression.name);
|
|
2218
|
+
if (simple !== undefined) {
|
|
2219
|
+
if (simple.returns === "argument") {
|
|
2220
|
+
const argument = expression.arguments[0];
|
|
2221
|
+
return argument === undefined ? "null" : infer(argument);
|
|
2222
|
+
}
|
|
2223
|
+
// DATE and INTERVAL results are logical domains carried as strings.
|
|
2224
|
+
return simple.returns === "date" || simple.returns === "interval" ? "string" : simple.returns;
|
|
2225
|
+
}
|
|
2075
2226
|
if (expression.name === "DATE_TRUNC")
|
|
2076
2227
|
return "datetime";
|
|
2077
2228
|
if (expression.name === "DATE_ADD") {
|
|
@@ -2365,8 +2516,9 @@ function trimPreparedResults(prepared, trim) {
|
|
|
2365
2516
|
}
|
|
2366
2517
|
export function createPreparedQuery(plan, tables, options = {}) {
|
|
2367
2518
|
assertTailParametersBound(plan);
|
|
2368
|
-
validateGrouping(plan);
|
|
2369
2519
|
plan = resolveStatementDatetimes(plan);
|
|
2520
|
+
plan = bindPendingSelectShapes(plan, wildcardRowColumns(tables));
|
|
2521
|
+
validateGrouping(plan);
|
|
2370
2522
|
// The schema-dependent rewrites run before derived sources materialize, because both can
|
|
2371
2523
|
// turn a scanned table into one more derived block.
|
|
2372
2524
|
plan = expandSourceColumnAliases(plan, wildcardRowColumns(tables));
|
|
@@ -2411,16 +2563,17 @@ export function createPreparedQuery(plan, tables, options = {}) {
|
|
|
2411
2563
|
/** Internal columnar entry point used after MinnowDatabase materializes a stable snapshot. */
|
|
2412
2564
|
export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemoryContext(), preparedOptions = {}) {
|
|
2413
2565
|
plan = resolveStatementDatetimes(plan);
|
|
2414
|
-
const ties = withTiesPlan(plan);
|
|
2415
|
-
if (ties.plan !== plan) {
|
|
2416
|
-
return trimPreparedResults(createPreparedColumnarQuery(ties.plan, tables, memory, preparedOptions), ties.trim);
|
|
2417
|
-
}
|
|
2418
2566
|
const columnarColumns = (tableName) => {
|
|
2419
2567
|
const table = tables.get(tableName);
|
|
2420
2568
|
return table === undefined
|
|
2421
2569
|
? undefined
|
|
2422
2570
|
: [...table.columns.keys()].filter((name) => !name.startsWith("\0"));
|
|
2423
2571
|
};
|
|
2572
|
+
plan = bindPendingSelectShapes(plan, columnarColumns);
|
|
2573
|
+
const ties = withTiesPlan(plan);
|
|
2574
|
+
if (ties.plan !== plan) {
|
|
2575
|
+
return trimPreparedResults(createPreparedColumnarQuery(ties.plan, tables, memory, preparedOptions), ties.trim);
|
|
2576
|
+
}
|
|
2424
2577
|
plan = expandSourceColumnAliases(plan, columnarColumns);
|
|
2425
2578
|
plan = expandNaturalJoins(plan, columnarColumns);
|
|
2426
2579
|
plan = expandQualifiedWildcards(plan, columnarColumns);
|
|
@@ -3691,8 +3844,9 @@ export function executeRowQuery(plan, tables) {
|
|
|
3691
3844
|
}
|
|
3692
3845
|
function executeRowQueryInternal(plan, tables, memory) {
|
|
3693
3846
|
assertTailParametersBound(plan);
|
|
3694
|
-
validateGrouping(plan);
|
|
3695
3847
|
plan = resolveStatementDatetimes(plan);
|
|
3848
|
+
plan = bindPendingSelectShapes(plan, wildcardRowColumns(tables));
|
|
3849
|
+
validateGrouping(plan);
|
|
3696
3850
|
plan = expandSourceColumnAliases(plan, wildcardRowColumns(tables));
|
|
3697
3851
|
plan = expandNaturalJoins(plan, wildcardRowColumns(tables));
|
|
3698
3852
|
plan = expandQualifiedWildcards(plan, wildcardRowColumns(tables));
|
|
@@ -4004,12 +4158,8 @@ function evaluate(expression, context, group) {
|
|
|
4004
4158
|
const right = evaluate(expression.right, context, group);
|
|
4005
4159
|
if (left === null || left === undefined || right === null || right === undefined)
|
|
4006
4160
|
return null;
|
|
4007
|
-
if (expression.operator === "||")
|
|
4008
|
-
if (typeof left !== "string" || typeof right !== "string") {
|
|
4009
|
-
throw new TypeError("|| requires string operands");
|
|
4010
|
-
}
|
|
4161
|
+
if (expression.operator === "||")
|
|
4011
4162
|
return concatenatedSqlValue(left, right);
|
|
4012
|
-
}
|
|
4013
4163
|
const exact = exactNumericBinary(expression.operator, left, right);
|
|
4014
4164
|
if (exact !== undefined)
|
|
4015
4165
|
return exact;
|
|
@@ -4202,6 +4352,11 @@ function evaluatePredicate(predicate, context) {
|
|
|
4202
4352
|
return false;
|
|
4203
4353
|
if (membership.set.has(comparable(value)))
|
|
4204
4354
|
return predicate.operator === "IN";
|
|
4355
|
+
// A typed value beside text members reads them in its own type (`id IN ('1', '2')`),
|
|
4356
|
+
// which the hashed set cannot see; the per-item path coerces as comparisons do.
|
|
4357
|
+
if (typeof value !== "string" && membership.hasText) {
|
|
4358
|
+
return inListHolds(predicate.operator, value, predicate.right.items.map((item) => evaluate(item, context)));
|
|
4359
|
+
}
|
|
4205
4360
|
return predicate.operator === "NOT IN" && !membership.hasNull;
|
|
4206
4361
|
}
|
|
4207
4362
|
return inListHolds(predicate.operator, evaluate(predicate.left, context), predicate.right.items.map((item) => evaluate(item, context)));
|
|
@@ -4230,7 +4385,8 @@ function inListHolds(operator, value, items) {
|
|
|
4230
4385
|
hasNull = true;
|
|
4231
4386
|
continue;
|
|
4232
4387
|
}
|
|
4233
|
-
|
|
4388
|
+
const member = typeof item === "string" && typeof value !== "string" ? coercedComparable(item, value) : item;
|
|
4389
|
+
if (comparable(value) === comparable(member))
|
|
4234
4390
|
return operator === "IN";
|
|
4235
4391
|
}
|
|
4236
4392
|
return operator === "NOT IN" && !hasNull;
|
|
@@ -4249,13 +4405,17 @@ export function cachedListMembership(node, items) {
|
|
|
4249
4405
|
if (items.every((item) => item.kind === "literal")) {
|
|
4250
4406
|
const set = new Set();
|
|
4251
4407
|
let hasNull = false;
|
|
4408
|
+
let hasText = false;
|
|
4252
4409
|
for (const item of items) {
|
|
4253
4410
|
if (item.value === null || item.value === undefined)
|
|
4254
4411
|
hasNull = true;
|
|
4255
|
-
else
|
|
4412
|
+
else {
|
|
4413
|
+
if (typeof item.value === "string")
|
|
4414
|
+
hasText = true;
|
|
4256
4415
|
set.add(comparable(item.value));
|
|
4416
|
+
}
|
|
4257
4417
|
}
|
|
4258
|
-
cached = { set, hasNull };
|
|
4418
|
+
cached = { set, hasNull, hasText };
|
|
4259
4419
|
}
|
|
4260
4420
|
else {
|
|
4261
4421
|
cached = null;
|
|
@@ -4275,6 +4435,14 @@ const extractFields = new Set([
|
|
|
4275
4435
|
"second",
|
|
4276
4436
|
"epoch",
|
|
4277
4437
|
"dow",
|
|
4438
|
+
"doy",
|
|
4439
|
+
"isodow",
|
|
4440
|
+
"isoyear",
|
|
4441
|
+
"decade",
|
|
4442
|
+
"century",
|
|
4443
|
+
"millennium",
|
|
4444
|
+
"milliseconds",
|
|
4445
|
+
"microseconds",
|
|
4278
4446
|
]);
|
|
4279
4447
|
/**
|
|
4280
4448
|
* EXTRACT(field FROM datetime), in UTC. `week` is the ISO 8601 week number and `dow` counts
|
|
@@ -4287,34 +4455,73 @@ function extractDatePart(field, value) {
|
|
|
4287
4455
|
}
|
|
4288
4456
|
if (value === null || value === undefined)
|
|
4289
4457
|
return null;
|
|
4290
|
-
|
|
4291
|
-
|
|
4458
|
+
let milliseconds;
|
|
4459
|
+
if (value instanceof Date)
|
|
4460
|
+
milliseconds = dateMilliseconds(value);
|
|
4461
|
+
else {
|
|
4462
|
+
// A DATE value is a midnight instant for every field; other text is not a datetime.
|
|
4463
|
+
const external = isDateDomainValue(value) ? externalSqlDomainValue(value) : undefined;
|
|
4464
|
+
if (typeof external !== "string")
|
|
4465
|
+
throw new TypeError("EXTRACT requires a datetime value");
|
|
4466
|
+
milliseconds = Date.parse(`${external}T00:00:00.000Z`);
|
|
4467
|
+
}
|
|
4468
|
+
if (!Number.isFinite(milliseconds))
|
|
4469
|
+
return Number.NaN;
|
|
4470
|
+
if (normalized === "epoch")
|
|
4471
|
+
return milliseconds / 1000;
|
|
4472
|
+
// Time-of-day fields are modular arithmetic on the epoch value; calendar fields come from the
|
|
4473
|
+
// civil-date conversion of the day count. No Date is allocated on this path.
|
|
4474
|
+
const day = 86_400_000;
|
|
4475
|
+
const days = epochDays(milliseconds);
|
|
4476
|
+
const timeOfDay = milliseconds - days * day;
|
|
4292
4477
|
switch (normalized) {
|
|
4293
|
-
case "year":
|
|
4294
|
-
return dateUtcFullYear(value);
|
|
4295
|
-
case "quarter":
|
|
4296
|
-
return Math.floor(dateUtcMonth(value) / 3) + 1;
|
|
4297
|
-
case "month":
|
|
4298
|
-
return dateUtcMonth(value) + 1;
|
|
4299
|
-
case "week": {
|
|
4300
|
-
const date = new Date(Date.UTC(dateUtcFullYear(value), dateUtcMonth(value), dateUtcDate(value)));
|
|
4301
|
-
// ISO week: shift to the Thursday of this week, then count weeks from January 1st.
|
|
4302
|
-
setDateUtcDate(date, dateUtcDate(date) + 4 - (dateUtcDay(date) || 7));
|
|
4303
|
-
const yearStart = Date.UTC(dateUtcFullYear(date), 0, 1);
|
|
4304
|
-
return Math.ceil(((dateMilliseconds(date) - yearStart) / 86_400_000 + 1) / 7);
|
|
4305
|
-
}
|
|
4306
|
-
case "day":
|
|
4307
|
-
return dateUtcDate(value);
|
|
4308
4478
|
case "hour":
|
|
4309
|
-
return
|
|
4479
|
+
return Math.floor(timeOfDay / 3_600_000);
|
|
4310
4480
|
case "minute":
|
|
4311
|
-
return
|
|
4481
|
+
return Math.floor(timeOfDay / 60_000) % 60;
|
|
4312
4482
|
case "second":
|
|
4313
|
-
return
|
|
4314
|
-
case "
|
|
4315
|
-
return
|
|
4483
|
+
return Math.floor(timeOfDay / 1000) % 60;
|
|
4484
|
+
case "milliseconds":
|
|
4485
|
+
return timeOfDay % 60_000;
|
|
4486
|
+
case "microseconds":
|
|
4487
|
+
return (timeOfDay % 60_000) * 1000;
|
|
4488
|
+
case "dow":
|
|
4489
|
+
return (((days + 4) % 7) + 7) % 7;
|
|
4490
|
+
case "isodow": {
|
|
4491
|
+
const dow = (((days + 4) % 7) + 7) % 7;
|
|
4492
|
+
return dow === 0 ? 7 : dow;
|
|
4493
|
+
}
|
|
4316
4494
|
default:
|
|
4317
|
-
|
|
4495
|
+
break;
|
|
4496
|
+
}
|
|
4497
|
+
const [year, month, dayOfMonth] = civilFromDays(days);
|
|
4498
|
+
switch (normalized) {
|
|
4499
|
+
case "year":
|
|
4500
|
+
return year;
|
|
4501
|
+
case "month":
|
|
4502
|
+
return month + 1;
|
|
4503
|
+
case "day":
|
|
4504
|
+
return dayOfMonth;
|
|
4505
|
+
case "quarter":
|
|
4506
|
+
return Math.floor(month / 3) + 1;
|
|
4507
|
+
case "doy":
|
|
4508
|
+
return days - daysFromCivil(year, 0, 1) + 1;
|
|
4509
|
+
case "decade":
|
|
4510
|
+
return Math.floor(year / 10);
|
|
4511
|
+
case "century":
|
|
4512
|
+
return Math.ceil(year / 100);
|
|
4513
|
+
case "millennium":
|
|
4514
|
+
return Math.ceil(year / 1000);
|
|
4515
|
+
default: {
|
|
4516
|
+
// ISO week and ISO year: the Thursday of this week decides both.
|
|
4517
|
+
const dow = (((days + 4) % 7) + 7) % 7;
|
|
4518
|
+
const thursday = days + 4 - (dow || 7);
|
|
4519
|
+
const [isoYear] = civilFromDays(thursday);
|
|
4520
|
+
if (normalized === "isoyear")
|
|
4521
|
+
return isoYear;
|
|
4522
|
+
const yearStart = daysFromCivil(isoYear, 0, 1);
|
|
4523
|
+
return Math.ceil((thursday - yearStart + 1) / 7);
|
|
4524
|
+
}
|
|
4318
4525
|
}
|
|
4319
4526
|
}
|
|
4320
4527
|
/**
|
|
@@ -4530,6 +4737,9 @@ export function comparisonHolds(operator, leftValue, rightValue) {
|
|
|
4530
4737
|
rightValue === null ||
|
|
4531
4738
|
rightValue === undefined)
|
|
4532
4739
|
return false;
|
|
4740
|
+
// An untyped string beside a datetime, number, or boolean reads in that value's type, as
|
|
4741
|
+
// PostgreSQL types an unknown-typed literal by its context (`joined >= '2026-01-01'`).
|
|
4742
|
+
[leftValue, rightValue] = coerceComparisonOperands(leftValue, rightValue);
|
|
4533
4743
|
const left = comparable(leftValue);
|
|
4534
4744
|
const right = comparable(rightValue);
|
|
4535
4745
|
if (operator === "=")
|
|
@@ -4615,6 +4825,23 @@ function hasWindow(expression) {
|
|
|
4615
4825
|
export function isAggregateCall(expression) {
|
|
4616
4826
|
return expression.kind === "call" && aggregateNames.has(expression.name);
|
|
4617
4827
|
}
|
|
4828
|
+
/**
|
|
4829
|
+
* Whether a non-aggregate select expression is a function of the grouping expressions: it is one
|
|
4830
|
+
* of them, or every column it reads sits inside a subexpression that is one of them. PostgreSQL
|
|
4831
|
+
* accepts `FLOOR(amount / 25) * 25` grouped by `FLOOR(amount / 25)` on exactly this rule.
|
|
4832
|
+
*/
|
|
4833
|
+
function groupedExpression(expression, grouped) {
|
|
4834
|
+
if (grouped.has(JSON.stringify(expression)))
|
|
4835
|
+
return true;
|
|
4836
|
+
// A full-text node reads its row's searchable columns even when MATCH(*) lists none, so it
|
|
4837
|
+
// is only grouped when the GROUP BY names it outright.
|
|
4838
|
+
if (expression.kind === "column" || containsFtsExpression(expression))
|
|
4839
|
+
return false;
|
|
4840
|
+
const children = childExpressions(expression);
|
|
4841
|
+
if (children.length === 0)
|
|
4842
|
+
return expressionColumns(expression).length === 0;
|
|
4843
|
+
return children.every((child) => expressionColumns(child).length === 0 || groupedExpression(child, grouped));
|
|
4844
|
+
}
|
|
4618
4845
|
function validateGrouping(plan) {
|
|
4619
4846
|
const grouped = plan.groupBy.length > 0 || plan.select.some((item) => hasAggregate(item.expression));
|
|
4620
4847
|
if (!grouped)
|
|
@@ -4630,7 +4857,7 @@ function validateGrouping(plan) {
|
|
|
4630
4857
|
!containsFtsExpression(item.expression)) {
|
|
4631
4858
|
continue;
|
|
4632
4859
|
}
|
|
4633
|
-
if (!
|
|
4860
|
+
if (!groupedExpression(item.expression, groupExpressions)) {
|
|
4634
4861
|
throw new TypeError(`Selected column must appear in GROUP BY: ${item.alias}`);
|
|
4635
4862
|
}
|
|
4636
4863
|
}
|
|
@@ -4797,6 +5024,12 @@ class Parser {
|
|
|
4797
5024
|
/** Placeholders seen so far: positional `?` count, and the highest `$n` number. */
|
|
4798
5025
|
#positionalParameters = 0;
|
|
4799
5026
|
#highestNumberedParameter = 0;
|
|
5027
|
+
/**
|
|
5028
|
+
* Set while a set-operation member's SELECT parses. The member leaves a trailing ORDER BY,
|
|
5029
|
+
* LIMIT, OFFSET, or FETCH unparsed: PostgreSQL applies that tail to the whole compound, and a
|
|
5030
|
+
* member that swallowed it would resolve the ordering against its own select list instead.
|
|
5031
|
+
*/
|
|
5032
|
+
#compoundMember = false;
|
|
4800
5033
|
/** Set when the statement names CURRENT_DATE, CURRENT_TIMESTAMP, or LOCALTIME. */
|
|
4801
5034
|
usesStatementDatetime = false;
|
|
4802
5035
|
/** Set when the statement names NEXTVAL or CURRVAL. */
|
|
@@ -4915,7 +5148,7 @@ class Parser {
|
|
|
4915
5148
|
}
|
|
4916
5149
|
}
|
|
4917
5150
|
// INTERSECT binds tighter than UNION and EXCEPT, matching PostgreSQL.
|
|
4918
|
-
const firstTerm = this.#setTerm(sql);
|
|
5151
|
+
const firstTerm = this.#setTerm(sql, false);
|
|
4919
5152
|
let plan = firstTerm.block;
|
|
4920
5153
|
if (this.#isKeyword("UNION") || this.#isKeyword("EXCEPT")) {
|
|
4921
5154
|
const members = [firstTerm];
|
|
@@ -4939,14 +5172,38 @@ class Parser {
|
|
|
4939
5172
|
else
|
|
4940
5173
|
ops.push("except");
|
|
4941
5174
|
}
|
|
4942
|
-
members.push(this.#setTerm("(union member)"));
|
|
5175
|
+
members.push(this.#setTerm("(union member)", true));
|
|
5176
|
+
}
|
|
5177
|
+
// PostgreSQL assigns a trailing ORDER BY or LIMIT to the whole compound, so every member
|
|
5178
|
+
// after the first parsed without one and the tail is still unparsed here.
|
|
5179
|
+
plan = this.#compoundBlock(sql, members, ops, this.#compoundTail());
|
|
5180
|
+
}
|
|
5181
|
+
else if (firstTerm.values === true && !firstTerm.parenthesized) {
|
|
5182
|
+
// A bare VALUES list desugars into a UNION ALL of one-row selects; a trailing ORDER BY
|
|
5183
|
+
// or LIMIT applies to that compound, with ordinals resolving against the row shape.
|
|
5184
|
+
const tail = this.#compoundTail();
|
|
5185
|
+
if (tail.orderBy.length > 0 || tail.limit !== undefined || tail.offset !== undefined) {
|
|
5186
|
+
const union = plan.base.union;
|
|
5187
|
+
plan =
|
|
5188
|
+
union === undefined
|
|
5189
|
+
? compoundSelectBlock(sql, [plan], [], tail, this.nextDerivedSequence)
|
|
5190
|
+
: compoundSelectBlock(sql, union.blocks, union.ops, tail, this.nextDerivedSequence);
|
|
4943
5191
|
}
|
|
4944
|
-
plan = this.#compoundBlock(sql, members, ops);
|
|
4945
5192
|
}
|
|
4946
5193
|
return plan;
|
|
4947
5194
|
}
|
|
4948
|
-
|
|
4949
|
-
|
|
5195
|
+
/** The clauses that close a query expression: ORDER BY, then LIMIT/OFFSET or OFFSET/FETCH. */
|
|
5196
|
+
#compoundTail() {
|
|
5197
|
+
return { orderBy: this.#orderByClause(), ...this.#tailClauses() };
|
|
5198
|
+
}
|
|
5199
|
+
/**
|
|
5200
|
+
* One UNION/EXCEPT term: a member, or an INTERSECT chain of members. `member` marks a term
|
|
5201
|
+
* that follows a set operator, whose trailing clauses belong to the enclosing compound; the
|
|
5202
|
+
* first term of a statement parses its own tail, which is where a lone INTERSECT chain's
|
|
5203
|
+
* ORDER BY lands.
|
|
5204
|
+
*/
|
|
5205
|
+
#setTerm(sql, member) {
|
|
5206
|
+
const first = this.#unionMember(sql, member);
|
|
4950
5207
|
if (!this.#isKeyword("INTERSECT"))
|
|
4951
5208
|
return first;
|
|
4952
5209
|
const members = [first];
|
|
@@ -4959,46 +5216,20 @@ class Parser {
|
|
|
4959
5216
|
}
|
|
4960
5217
|
else
|
|
4961
5218
|
ops.push("intersect");
|
|
4962
|
-
members.push(this.#unionMember("(intersect member)"));
|
|
5219
|
+
members.push(this.#unionMember("(intersect member)", true));
|
|
4963
5220
|
}
|
|
4964
|
-
|
|
5221
|
+
const tail = member ? { orderBy: [] } : this.#compoundTail();
|
|
5222
|
+
return { block: this.#compoundBlock(sql, members, ops, tail), parenthesized: false };
|
|
4965
5223
|
}
|
|
4966
|
-
#compoundBlock(sql, members, ops) {
|
|
4967
|
-
|
|
4968
|
-
|
|
5224
|
+
#compoundBlock(sql, members, ops, tail) {
|
|
5225
|
+
// Only the first member can have parsed a tail of its own (later members skip theirs), and
|
|
5226
|
+
// a member-level ORDER BY or LIMIT is legal only inside parentheses.
|
|
5227
|
+
for (const member of members) {
|
|
4969
5228
|
if (!member.parenthesized &&
|
|
4970
|
-
!last &&
|
|
4971
5229
|
(member.block.orderBy.length > 0 || member.block.limit !== undefined)) {
|
|
4972
5230
|
throw new TypeError("ORDER BY or LIMIT in a UNION member requires parentheses");
|
|
4973
5231
|
}
|
|
4974
5232
|
}
|
|
4975
|
-
// PostgreSQL assigns a trailing ORDER BY or LIMIT to the whole compound. After an
|
|
4976
|
-
// unparenthesized last member the clause was greedily parsed into that member and lifts
|
|
4977
|
-
// out; after a parenthesized member it is still unparsed.
|
|
4978
|
-
const last = members[members.length - 1];
|
|
4979
|
-
let tail;
|
|
4980
|
-
if (last !== undefined && !last.parenthesized) {
|
|
4981
|
-
tail = {
|
|
4982
|
-
orderBy: last.block.orderBy,
|
|
4983
|
-
...(last.block.limit === undefined ? {} : { limit: last.block.limit }),
|
|
4984
|
-
...(last.block.offset === undefined ? {} : { offset: last.block.offset }),
|
|
4985
|
-
...(last.block.limitWithTies === true ? { limitWithTies: true } : {}),
|
|
4986
|
-
...(last.block.limitParameter === undefined
|
|
4987
|
-
? {}
|
|
4988
|
-
: { limitParameter: last.block.limitParameter }),
|
|
4989
|
-
...(last.block.offsetParameter === undefined
|
|
4990
|
-
? {}
|
|
4991
|
-
: { offsetParameter: last.block.offsetParameter }),
|
|
4992
|
-
};
|
|
4993
|
-
last.block.orderBy = [];
|
|
4994
|
-
delete last.block.limit;
|
|
4995
|
-
delete last.block.offset;
|
|
4996
|
-
delete last.block.limitParameter;
|
|
4997
|
-
delete last.block.offsetParameter;
|
|
4998
|
-
}
|
|
4999
|
-
else {
|
|
5000
|
-
tail = { orderBy: this.#orderByClause(), ...this.#tailClauses() };
|
|
5001
|
-
}
|
|
5002
5233
|
return compoundSelectBlock(sql, members.map((member) => member.block), ops, tail, this.nextDerivedSequence);
|
|
5003
5234
|
}
|
|
5004
5235
|
/** CREATE [UNIQUE] INDEX name ON table(column [ASC|DESC], ...). */
|
|
@@ -5143,9 +5374,9 @@ class Parser {
|
|
|
5143
5374
|
continue;
|
|
5144
5375
|
}
|
|
5145
5376
|
const name = this.#identifier();
|
|
5146
|
-
const columnType = this.#columnType();
|
|
5147
|
-
let nullable = true;
|
|
5148
|
-
let defaultValue;
|
|
5377
|
+
const { serial, ...columnType } = this.#columnType();
|
|
5378
|
+
let nullable = serial !== true;
|
|
5379
|
+
let defaultValue = serial === true ? { kind: "autoincrement" } : undefined;
|
|
5149
5380
|
let generatedValue;
|
|
5150
5381
|
for (;;) {
|
|
5151
5382
|
if (this.#isKeyword("DEFAULT")) {
|
|
@@ -5155,9 +5386,29 @@ class Parser {
|
|
|
5155
5386
|
continue;
|
|
5156
5387
|
}
|
|
5157
5388
|
if (this.#isKeyword("GENERATED")) {
|
|
5389
|
+
if (this.#identityClause()) {
|
|
5390
|
+
// GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY: PostgreSQL's identity column, the
|
|
5391
|
+
// same auto-increment default a SERIAL column carries.
|
|
5392
|
+
if (columnType.type !== "number" || columnType.integer !== true) {
|
|
5393
|
+
throw new TypeError(`Identity column ${name} must be an integer type`);
|
|
5394
|
+
}
|
|
5395
|
+
defaultValue = { kind: "autoincrement" };
|
|
5396
|
+
nullable = false;
|
|
5397
|
+
continue;
|
|
5398
|
+
}
|
|
5158
5399
|
generatedValue = this.#generatedColumn();
|
|
5159
5400
|
continue;
|
|
5160
5401
|
}
|
|
5402
|
+
if (this.#isKeyword("AUTOINCREMENT")) {
|
|
5403
|
+
// SQLite's spelling, after PRIMARY KEY.
|
|
5404
|
+
this.#keyword("AUTOINCREMENT");
|
|
5405
|
+
if (columnType.type !== "number" || columnType.integer !== true) {
|
|
5406
|
+
throw new TypeError(`AUTOINCREMENT column ${name} must be an integer type`);
|
|
5407
|
+
}
|
|
5408
|
+
defaultValue = { kind: "autoincrement" };
|
|
5409
|
+
nullable = false;
|
|
5410
|
+
continue;
|
|
5411
|
+
}
|
|
5161
5412
|
if (this.#isKeyword("CHECK")) {
|
|
5162
5413
|
checks.push(this.#checkConstraint(`${table}_${name}_check`));
|
|
5163
5414
|
continue;
|
|
@@ -5369,6 +5620,44 @@ class Parser {
|
|
|
5369
5620
|
return columnDefaultFor(expression, sql);
|
|
5370
5621
|
}
|
|
5371
5622
|
/** GENERATED [ALWAYS] AS (expression) STORED. */
|
|
5623
|
+
/**
|
|
5624
|
+
* Consumes `GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY` and answers true; leaves the
|
|
5625
|
+
* tokens alone and answers false for the stored generated-column form.
|
|
5626
|
+
*/
|
|
5627
|
+
#identityClause() {
|
|
5628
|
+
const start = this.#index;
|
|
5629
|
+
this.#keyword("GENERATED");
|
|
5630
|
+
if (this.#isKeyword("BY")) {
|
|
5631
|
+
this.#keyword("BY");
|
|
5632
|
+
this.#keyword("DEFAULT");
|
|
5633
|
+
}
|
|
5634
|
+
else if (this.#isKeyword("ALWAYS")) {
|
|
5635
|
+
this.#keyword("ALWAYS");
|
|
5636
|
+
}
|
|
5637
|
+
if (this.#isKeyword("AS")) {
|
|
5638
|
+
this.#keyword("AS");
|
|
5639
|
+
if (this.#isKeyword("IDENTITY")) {
|
|
5640
|
+
this.#keyword("IDENTITY");
|
|
5641
|
+
// Sequence options in parentheses are accepted and ignored; the counter starts at 1.
|
|
5642
|
+
if (this.#punctuation("(")) {
|
|
5643
|
+
let depth = 1;
|
|
5644
|
+
while (depth > 0) {
|
|
5645
|
+
const token = this.#peek();
|
|
5646
|
+
if (token.kind === "eof")
|
|
5647
|
+
throw new TypeError("Unterminated identity options");
|
|
5648
|
+
this.#index += 1;
|
|
5649
|
+
if (token.kind === "punctuation" && token.text === "(")
|
|
5650
|
+
depth += 1;
|
|
5651
|
+
if (token.kind === "punctuation" && token.text === ")")
|
|
5652
|
+
depth -= 1;
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
return true;
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
this.#index = start;
|
|
5659
|
+
return false;
|
|
5660
|
+
}
|
|
5372
5661
|
#generatedColumn() {
|
|
5373
5662
|
this.#keyword("GENERATED");
|
|
5374
5663
|
if (this.#isKeyword("ALWAYS"))
|
|
@@ -5492,6 +5781,10 @@ class Parser {
|
|
|
5492
5781
|
break;
|
|
5493
5782
|
}
|
|
5494
5783
|
this.#take("eof");
|
|
5784
|
+
// PostgreSQL fills existing rows with a constant DEFAULT when the column is added, which is
|
|
5785
|
+
// also what lets the column be NOT NULL. An expression default (NOW(), nextval) is evaluated
|
|
5786
|
+
// per write, so it fills only the rows written afterwards.
|
|
5787
|
+
const backfill = defaultValue?.kind === "literal" ? defaultValue.value : undefined;
|
|
5495
5788
|
return {
|
|
5496
5789
|
kind: "add-column",
|
|
5497
5790
|
table,
|
|
@@ -5500,7 +5793,9 @@ class Parser {
|
|
|
5500
5793
|
...columnType,
|
|
5501
5794
|
...(nullable ? { nullable: true } : {}),
|
|
5502
5795
|
...(defaultValue === undefined ? {} : { defaultValue }),
|
|
5796
|
+
...(backfill === undefined ? {} : { backfill }),
|
|
5503
5797
|
},
|
|
5798
|
+
...(backfill === undefined ? {} : { allowNonNullableWithBackfill: true }),
|
|
5504
5799
|
};
|
|
5505
5800
|
}
|
|
5506
5801
|
/** A type name's precision, scale, or width: plain digits, as PostgreSQL requires there. */
|
|
@@ -5549,6 +5844,10 @@ class Parser {
|
|
|
5549
5844
|
#columnType() {
|
|
5550
5845
|
const declared = this.#identifier();
|
|
5551
5846
|
const word = declared.toUpperCase();
|
|
5847
|
+
if (word === "SERIAL" || word === "BIGSERIAL" || word === "SMALLSERIAL") {
|
|
5848
|
+
// PostgreSQL's serial pseudo-types: an integer column fed by an auto-increment default.
|
|
5849
|
+
return { type: "number", integer: true, serial: true };
|
|
5850
|
+
}
|
|
5552
5851
|
if (word === "NUMERIC" || word === "DECIMAL") {
|
|
5553
5852
|
let precision;
|
|
5554
5853
|
let scale;
|
|
@@ -5631,14 +5930,20 @@ class Parser {
|
|
|
5631
5930
|
...this.#returningClause(table),
|
|
5632
5931
|
};
|
|
5633
5932
|
}
|
|
5634
|
-
if (this.#isKeyword("SELECT")) {
|
|
5635
|
-
|
|
5933
|
+
if (this.#isKeyword("SELECT") || this.#isKeyword("WITH") || this.#peek().text === "(") {
|
|
5934
|
+
// Any query expression feeds INSERT: a plain SELECT, a WITH, a set operation, or a
|
|
5935
|
+
// parenthesized member; ON CONFLICT then applies to the produced rows as it does to VALUES.
|
|
5936
|
+
const insertSource = this.#queryExpression("(insert select)");
|
|
5636
5937
|
resolvePlanExactNumericConstants(insertSource);
|
|
5637
|
-
const query =
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5938
|
+
const query = planHasPendingSelectShapes(insertSource)
|
|
5939
|
+
? insertSource
|
|
5940
|
+
: optimizePlan(insertSource);
|
|
5941
|
+
// A set operation or VALUES source projects `*` over its members until execution; the
|
|
5942
|
+
// materialized result is checked against the column list then, like every other source.
|
|
5943
|
+
if (!planHasPendingSelectShapes(query) &&
|
|
5944
|
+
!query.select.some((item) => item.expression.kind === "wildcard") &&
|
|
5945
|
+
columns.length > 0 &&
|
|
5946
|
+
query.select.length !== columns.length) {
|
|
5642
5947
|
throw new TypeError("INSERT ... SELECT must produce exactly the insert column count");
|
|
5643
5948
|
}
|
|
5644
5949
|
return {
|
|
@@ -5647,6 +5952,7 @@ class Parser {
|
|
|
5647
5952
|
columns,
|
|
5648
5953
|
rows: [],
|
|
5649
5954
|
query,
|
|
5955
|
+
...this.#onConflictClause(table),
|
|
5650
5956
|
...this.#returningClause(table),
|
|
5651
5957
|
};
|
|
5652
5958
|
}
|
|
@@ -5682,6 +5988,13 @@ class Parser {
|
|
|
5682
5988
|
return {};
|
|
5683
5989
|
this.#keyword("ON");
|
|
5684
5990
|
this.#keyword("CONFLICT");
|
|
5991
|
+
if (this.#isKeyword("DO")) {
|
|
5992
|
+
// PostgreSQL lets DO NOTHING omit the conflict target (any unique key); DO UPDATE has to
|
|
5993
|
+
// name one. Kysely's `onConflict((oc) => oc.doNothing())` emits exactly this spelling.
|
|
5994
|
+
this.#keyword("DO");
|
|
5995
|
+
this.#keyword("NOTHING");
|
|
5996
|
+
return { onConflict: { column: "", anyTarget: true, action: "nothing" } };
|
|
5997
|
+
}
|
|
5685
5998
|
this.#expectPunctuation("(");
|
|
5686
5999
|
const columns = [];
|
|
5687
6000
|
for (;;) {
|
|
@@ -5742,7 +6055,13 @@ class Parser {
|
|
|
5742
6055
|
};
|
|
5743
6056
|
}
|
|
5744
6057
|
/** RETURNING *, target.*, or [target.]col, ... — execution owns the row semantics. */
|
|
5745
|
-
|
|
6058
|
+
/**
|
|
6059
|
+
* RETURNING: `*`, `target.*`, or a list of items. A list of plain (optionally target-qualified)
|
|
6060
|
+
* columns keeps the column form; any other item — an expression, a function call, an
|
|
6061
|
+
* aliased column — is carried as an expression list that the executor evaluates over the
|
|
6062
|
+
* affected rows through an ordinary SELECT, with the statement text of each item.
|
|
6063
|
+
*/
|
|
6064
|
+
#returningClause(table, alias) {
|
|
5746
6065
|
if (!this.#isKeyword("RETURNING"))
|
|
5747
6066
|
return {};
|
|
5748
6067
|
this.#keyword("RETURNING");
|
|
@@ -5750,29 +6069,67 @@ class Parser {
|
|
|
5750
6069
|
this.#index += 1;
|
|
5751
6070
|
return { returning: "*" };
|
|
5752
6071
|
}
|
|
5753
|
-
const
|
|
6072
|
+
const items = [];
|
|
6073
|
+
let plain = true;
|
|
5754
6074
|
for (;;) {
|
|
5755
|
-
const
|
|
5756
|
-
if (
|
|
5757
|
-
|
|
5758
|
-
|
|
6075
|
+
const start = this.#peek();
|
|
6076
|
+
if (start.kind === "identifier" &&
|
|
6077
|
+
this.tokens[this.#index + 1]?.text === "." &&
|
|
6078
|
+
this.tokens[this.#index + 2]?.text === "*") {
|
|
6079
|
+
this.#index += 3;
|
|
6080
|
+
if (start.text !== table && start.text !== alias) {
|
|
6081
|
+
throw new TypeError(`RETURNING qualifier must name the target table: ${alias ?? table}`);
|
|
5759
6082
|
}
|
|
5760
|
-
if (this.#peek().text === "
|
|
5761
|
-
|
|
5762
|
-
if (columns.length > 0 || this.#peek().text === ",") {
|
|
5763
|
-
throw new TypeError("RETURNING target.* must be the only returned item");
|
|
5764
|
-
}
|
|
5765
|
-
return { returning: "*" };
|
|
6083
|
+
if (items.length > 0 || this.#peek().text === ",") {
|
|
6084
|
+
throw new TypeError("RETURNING target.* must be the only returned item");
|
|
5766
6085
|
}
|
|
5767
|
-
|
|
6086
|
+
return { returning: "*" };
|
|
5768
6087
|
}
|
|
5769
|
-
|
|
5770
|
-
|
|
6088
|
+
const expression = this.#expression();
|
|
6089
|
+
const end = this.tokens[this.#index - 1] ?? start;
|
|
6090
|
+
let outputAlias = defaultAlias(expression);
|
|
6091
|
+
let explicit = false;
|
|
6092
|
+
if (this.#isKeyword("AS")) {
|
|
6093
|
+
this.#keyword("AS");
|
|
6094
|
+
outputAlias = this.#identifier();
|
|
6095
|
+
explicit = true;
|
|
6096
|
+
}
|
|
6097
|
+
let column;
|
|
6098
|
+
if (expression.kind === "column") {
|
|
6099
|
+
const parts = expression.reference.split(".");
|
|
6100
|
+
if (parts.length === 1)
|
|
6101
|
+
column = parts[0];
|
|
6102
|
+
else if (parts.length === 2 && (parts[0] === table || parts[0] === alias))
|
|
6103
|
+
column = parts[1];
|
|
6104
|
+
else {
|
|
6105
|
+
throw new TypeError(`RETURNING qualifier must name the target table: ${alias ?? table}`);
|
|
6106
|
+
}
|
|
5771
6107
|
}
|
|
6108
|
+
if (column === undefined || explicit)
|
|
6109
|
+
plain = false;
|
|
6110
|
+
items.push({
|
|
6111
|
+
sql: this.text.slice(start.start, end.end),
|
|
6112
|
+
alias: outputAlias,
|
|
6113
|
+
expression,
|
|
6114
|
+
...(column === undefined ? {} : { column }),
|
|
6115
|
+
});
|
|
5772
6116
|
if (!this.#punctuation(","))
|
|
5773
6117
|
break;
|
|
5774
6118
|
}
|
|
5775
|
-
|
|
6119
|
+
if (plain)
|
|
6120
|
+
return { returning: items.map((item) => item.column ?? item.alias) };
|
|
6121
|
+
for (const item of items) {
|
|
6122
|
+
if (hasAggregate(item.expression)) {
|
|
6123
|
+
throw new TypeError("RETURNING cannot use aggregate functions");
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
const aliases = new Set();
|
|
6127
|
+
for (const item of items) {
|
|
6128
|
+
if (aliases.has(item.alias))
|
|
6129
|
+
throw new TypeError(`Duplicate output column: ${item.alias}`);
|
|
6130
|
+
aliases.add(item.alias);
|
|
6131
|
+
}
|
|
6132
|
+
return { returningItems: items };
|
|
5776
6133
|
}
|
|
5777
6134
|
/**
|
|
5778
6135
|
* MERGE INTO target USING source ON condition WHEN [NOT] MATCHED [AND …] THEN … (F312).
|
|
@@ -5904,15 +6261,19 @@ class Parser {
|
|
|
5904
6261
|
#updateStatement() {
|
|
5905
6262
|
this.#keyword("UPDATE");
|
|
5906
6263
|
const table = this.#identifier();
|
|
6264
|
+
const alias = this.#mutationAlias("SET");
|
|
5907
6265
|
this.#keyword("SET");
|
|
5908
6266
|
const assignments = [];
|
|
5909
6267
|
for (;;) {
|
|
5910
|
-
const column = this.#
|
|
6268
|
+
const column = this.#mutationTargetColumn(table, alias);
|
|
5911
6269
|
this.#operator("=");
|
|
5912
6270
|
const expression = this.#expression();
|
|
5913
6271
|
if (hasAggregate(expression)) {
|
|
5914
6272
|
throw new TypeError("Aggregate functions are not allowed in UPDATE assignments");
|
|
5915
6273
|
}
|
|
6274
|
+
if (containsWindow(expression)) {
|
|
6275
|
+
throw new TypeError("Window functions are not allowed in UPDATE assignments");
|
|
6276
|
+
}
|
|
5916
6277
|
assignments.push({ column, expression });
|
|
5917
6278
|
if (!this.#punctuation(","))
|
|
5918
6279
|
break;
|
|
@@ -5921,14 +6282,54 @@ class Parser {
|
|
|
5921
6282
|
throw new TypeError("UPDATE assignments must set each column once");
|
|
5922
6283
|
}
|
|
5923
6284
|
const predicates = this.#mutationPredicates();
|
|
5924
|
-
return {
|
|
6285
|
+
return {
|
|
6286
|
+
kind: "update",
|
|
6287
|
+
table,
|
|
6288
|
+
...(alias === undefined ? {} : { alias }),
|
|
6289
|
+
assignments,
|
|
6290
|
+
predicates,
|
|
6291
|
+
...this.#returningClause(table, alias),
|
|
6292
|
+
};
|
|
5925
6293
|
}
|
|
5926
6294
|
#deleteStatement() {
|
|
5927
6295
|
this.#keyword("DELETE");
|
|
5928
6296
|
this.#keyword("FROM");
|
|
5929
6297
|
const table = this.#identifier();
|
|
6298
|
+
const alias = this.#mutationAlias("WHERE", "RETURNING");
|
|
5930
6299
|
const predicates = this.#mutationPredicates();
|
|
5931
|
-
return {
|
|
6300
|
+
return {
|
|
6301
|
+
kind: "delete",
|
|
6302
|
+
table,
|
|
6303
|
+
...(alias === undefined ? {} : { alias }),
|
|
6304
|
+
predicates,
|
|
6305
|
+
...this.#returningClause(table, alias),
|
|
6306
|
+
};
|
|
6307
|
+
}
|
|
6308
|
+
/**
|
|
6309
|
+
* `UPDATE t AS u` / `DELETE FROM t u`: PostgreSQL's mutation alias. A bare identifier that is
|
|
6310
|
+
* not one of the clause keywords that may follow the table is the alias.
|
|
6311
|
+
*/
|
|
6312
|
+
#mutationAlias(...clauses) {
|
|
6313
|
+
if (this.#isKeyword("AS")) {
|
|
6314
|
+
this.#keyword("AS");
|
|
6315
|
+
return this.#identifier();
|
|
6316
|
+
}
|
|
6317
|
+
const token = this.#peek();
|
|
6318
|
+
if (token.kind !== "identifier")
|
|
6319
|
+
return undefined;
|
|
6320
|
+
if (token.quoted !== true && clauses.includes(token.text.toUpperCase()))
|
|
6321
|
+
return undefined;
|
|
6322
|
+
return this.#identifier();
|
|
6323
|
+
}
|
|
6324
|
+
/** An assignment target, optionally qualified by the table or its alias (`SET u.total = …`). */
|
|
6325
|
+
#mutationTargetColumn(table, alias) {
|
|
6326
|
+
const first = this.#identifier();
|
|
6327
|
+
if (!this.#punctuation("."))
|
|
6328
|
+
return first;
|
|
6329
|
+
if (first !== table && first !== alias) {
|
|
6330
|
+
throw new TypeError(`SET qualifier must name the target table: ${alias ?? table}`);
|
|
6331
|
+
}
|
|
6332
|
+
return this.#identifier();
|
|
5932
6333
|
}
|
|
5933
6334
|
#mutationPredicates() {
|
|
5934
6335
|
const predicates = [];
|
|
@@ -5959,17 +6360,30 @@ class Parser {
|
|
|
5959
6360
|
if (hasAggregate(expression) || expressionColumns(expression).length > 0) {
|
|
5960
6361
|
throw new TypeError(`${label} must be constant expressions`);
|
|
5961
6362
|
}
|
|
6363
|
+
// A scalar subquery is a value the statement reads when it runs, like the statement clock.
|
|
6364
|
+
const needsExecution = (value) => value.kind === "subquery" ||
|
|
6365
|
+
value.kind === "exists" ||
|
|
6366
|
+
(value.kind === "call" &&
|
|
6367
|
+
(statementDatetimeNames.has(value.name) ||
|
|
6368
|
+
value.name === "NEXTVAL" ||
|
|
6369
|
+
value.name === "CURRVAL" ||
|
|
6370
|
+
volatileScalarFunctionNames.has(value.name))) ||
|
|
6371
|
+
childExpressions(value).some(needsExecution);
|
|
6372
|
+
if (needsExecution(expression)) {
|
|
6373
|
+
return { expression: resolveExactNumericConstants(expression) };
|
|
6374
|
+
}
|
|
5962
6375
|
return asQueryValue(evaluate(resolveExactNumericConstants(expression), {}));
|
|
5963
6376
|
}
|
|
5964
|
-
#unionMember(sql) {
|
|
6377
|
+
#unionMember(sql, member) {
|
|
5965
6378
|
if (this.#punctuation("(")) {
|
|
5966
6379
|
const block = this.#isKeyword("VALUES") ? this.#valuesBlock() : this.#queryExpression(sql);
|
|
5967
6380
|
this.#expectPunctuation(")");
|
|
5968
6381
|
return { block, parenthesized: true };
|
|
5969
6382
|
}
|
|
5970
6383
|
if (this.#isKeyword("VALUES")) {
|
|
5971
|
-
return { block: this.#valuesBlock(), parenthesized: false };
|
|
6384
|
+
return { block: this.#valuesBlock(), parenthesized: false, values: true };
|
|
5972
6385
|
}
|
|
6386
|
+
this.#compoundMember = member;
|
|
5973
6387
|
return { block: this.#selectBlock(sql), parenthesized: false };
|
|
5974
6388
|
}
|
|
5975
6389
|
#orderByClause() {
|
|
@@ -6146,6 +6560,10 @@ class Parser {
|
|
|
6146
6560
|
return windows;
|
|
6147
6561
|
}
|
|
6148
6562
|
#selectBlockBody(sql) {
|
|
6563
|
+
// Captured and cleared first, so a derived table or subquery inside this block parses its
|
|
6564
|
+
// own tail: only the member's outermost SELECT leaves the compound's clauses alone.
|
|
6565
|
+
const compoundMember = this.#compoundMember;
|
|
6566
|
+
this.#compoundMember = false;
|
|
6149
6567
|
this.#keyword("SELECT");
|
|
6150
6568
|
let distinct = false;
|
|
6151
6569
|
if (this.#isKeyword("DISTINCT")) {
|
|
@@ -6302,8 +6720,6 @@ class Parser {
|
|
|
6302
6720
|
const having = [];
|
|
6303
6721
|
if (this.#isKeyword("HAVING")) {
|
|
6304
6722
|
this.#keyword("HAVING");
|
|
6305
|
-
if (distinct)
|
|
6306
|
-
throw new TypeError("SELECT DISTINCT cannot be combined with HAVING");
|
|
6307
6723
|
having.push(...splitCondition(this.#expression()));
|
|
6308
6724
|
}
|
|
6309
6725
|
if (this.#isKeyword("WINDOW")) {
|
|
@@ -6318,7 +6734,7 @@ class Parser {
|
|
|
6318
6734
|
break;
|
|
6319
6735
|
}
|
|
6320
6736
|
}
|
|
6321
|
-
const orderBy = this.#orderByClause();
|
|
6737
|
+
const orderBy = compoundMember ? [] : this.#orderByClause();
|
|
6322
6738
|
return assembleSelectBlock({
|
|
6323
6739
|
sql,
|
|
6324
6740
|
base,
|
|
@@ -6329,7 +6745,7 @@ class Parser {
|
|
|
6329
6745
|
groupBy,
|
|
6330
6746
|
having,
|
|
6331
6747
|
orderBy,
|
|
6332
|
-
...this.#tailClauses(),
|
|
6748
|
+
...(compoundMember ? {} : this.#tailClauses()),
|
|
6333
6749
|
...(groupingSets === undefined ? {} : { groupingSets }),
|
|
6334
6750
|
}, this.nextDerivedSequence);
|
|
6335
6751
|
}
|
|
@@ -6394,10 +6810,6 @@ class Parser {
|
|
|
6394
6810
|
if (!this.#punctuation(","))
|
|
6395
6811
|
break;
|
|
6396
6812
|
}
|
|
6397
|
-
if (items.some((item) => item.expression.kind === "wildcard" && item.expression.table === undefined) &&
|
|
6398
|
-
items.length > 1) {
|
|
6399
|
-
throw new TypeError("SELECT * cannot be mixed with other expressions");
|
|
6400
|
-
}
|
|
6401
6813
|
const aliases = new Set();
|
|
6402
6814
|
for (const [index, item] of items.entries()) {
|
|
6403
6815
|
if (item.expression.kind === "list") {
|
|
@@ -6515,18 +6927,7 @@ class Parser {
|
|
|
6515
6927
|
break;
|
|
6516
6928
|
}
|
|
6517
6929
|
this.#expectPunctuation(")");
|
|
6518
|
-
|
|
6519
|
-
const target = derived.base.union !== undefined && derived.select[0]?.expression.kind === "wildcard"
|
|
6520
|
-
? derived.base.union.blocks[0]
|
|
6521
|
-
: derived;
|
|
6522
|
-
if (target?.select.length !== names.length) {
|
|
6523
|
-
throw new TypeError("Column alias list must match the derived table's column count");
|
|
6524
|
-
}
|
|
6525
|
-
names.forEach((name, index) => {
|
|
6526
|
-
const item = target.select[index];
|
|
6527
|
-
if (item !== undefined)
|
|
6528
|
-
item.alias = name;
|
|
6529
|
-
});
|
|
6930
|
+
renameBlockOutputs(derived, names, "derived table");
|
|
6530
6931
|
}
|
|
6531
6932
|
/**
|
|
6532
6933
|
* `JOIN t USING (a, b)`: the named columns must exist on both sides, and the join condition
|
|
@@ -7134,16 +7535,48 @@ class Parser {
|
|
|
7134
7535
|
}
|
|
7135
7536
|
// || and the JSON arrows share PostgreSQL's loosest "any other operator" level, applying
|
|
7136
7537
|
// left-to-right to whole arithmetic terms: `'a' || d ->> 'k'` is `('a' || d) ->> 'k'`.
|
|
7137
|
-
const
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7538
|
+
const regexOperator = operator === "~" || operator === "~*" || operator === "!~" || operator === "!~*";
|
|
7539
|
+
const precedence = operator === "::"
|
|
7540
|
+
? 30
|
|
7541
|
+
: operator === "^"
|
|
7542
|
+
? 25
|
|
7543
|
+
: operator === "*" || operator === "/" || operator === "%"
|
|
7544
|
+
? 20
|
|
7545
|
+
: operator === "+" || operator === "-"
|
|
7546
|
+
? 10
|
|
7547
|
+
: operator === "||" || operator === "->" || operator === "->>" || regexOperator
|
|
7548
|
+
? 5
|
|
7549
|
+
: -1;
|
|
7144
7550
|
if (precedence < minimumPrecedence)
|
|
7145
7551
|
break;
|
|
7146
7552
|
this.#index += 1;
|
|
7553
|
+
if (operator === "^") {
|
|
7554
|
+
// Exponentiation binds above * and / and, as in PostgreSQL, associates to the left:
|
|
7555
|
+
// 2 ^ 3 ^ 2 is (2 ^ 3) ^ 2.
|
|
7556
|
+
const exponent = this.#additive(precedence + 1);
|
|
7557
|
+
left = { kind: "call", name: "POWER", arguments: [left, exponent] };
|
|
7558
|
+
continue;
|
|
7559
|
+
}
|
|
7560
|
+
if (regexOperator) {
|
|
7561
|
+
const pattern = this.#additive(precedence + 1);
|
|
7562
|
+
const match = {
|
|
7563
|
+
kind: "call",
|
|
7564
|
+
name: "MINNOW_REGEX_MATCH",
|
|
7565
|
+
arguments: [left, pattern, { kind: "literal", value: operator.endsWith("*") ? "i" : "" }],
|
|
7566
|
+
};
|
|
7567
|
+
left = operator.startsWith("!") ? { kind: "not", operand: match } : match;
|
|
7568
|
+
continue;
|
|
7569
|
+
}
|
|
7570
|
+
if (operator === "::") {
|
|
7571
|
+
// PostgreSQL's postfix cast binds tighter than every binary operator, so it applies to
|
|
7572
|
+
// the term just parsed and rides the same CAST call as the standard spelling.
|
|
7573
|
+
left = {
|
|
7574
|
+
kind: "call",
|
|
7575
|
+
name: "CAST",
|
|
7576
|
+
arguments: [left, { kind: "literal", value: this.#castTarget() }],
|
|
7577
|
+
};
|
|
7578
|
+
continue;
|
|
7579
|
+
}
|
|
7147
7580
|
if (operator === "->" || operator === "->>") {
|
|
7148
7581
|
const key = this.#additive(precedence + 1);
|
|
7149
7582
|
// A key fixed at compile time fails here rather than per row, like SQL/JSON paths.
|
|
@@ -7656,7 +8089,8 @@ class Parser {
|
|
|
7656
8089
|
name === "MINNOW_COLLATE" ||
|
|
7657
8090
|
name === "MINNOW_SINGLE_VALUE" ||
|
|
7658
8091
|
name === "MINNOW_JSON_GET" ||
|
|
7659
|
-
name === "MINNOW_JSON_GET_TEXT"
|
|
8092
|
+
name === "MINNOW_JSON_GET_TEXT" ||
|
|
8093
|
+
name === "MINNOW_REGEX_MATCH") {
|
|
7660
8094
|
throw new TypeError(`Unsupported function: ${identifier}`);
|
|
7661
8095
|
}
|
|
7662
8096
|
if (!aggregateNames.has(name) && !scalarFunctionNames.has(name))
|
|
@@ -7709,6 +8143,25 @@ class Parser {
|
|
|
7709
8143
|
if (name === "JSON_ARRAYAGG" && args[0]?.kind === "wildcard") {
|
|
7710
8144
|
throw new TypeError("JSON_ARRAYAGG requires a scalar value expression");
|
|
7711
8145
|
}
|
|
8146
|
+
const simple = simpleScalarFunctions.get(name);
|
|
8147
|
+
if (simple !== undefined) {
|
|
8148
|
+
if (name === "AGE" && args.length === 1) {
|
|
8149
|
+
// AGE(x) is AGE(CURRENT_DATE, x): the statement clock, resolved once per statement.
|
|
8150
|
+
this.usesStatementDatetime = true;
|
|
8151
|
+
args.unshift({ kind: "call", name: "CURRENT_DATE", arguments: [] });
|
|
8152
|
+
}
|
|
8153
|
+
if (args.length < simple.minArgs || args.length > simple.maxArgs) {
|
|
8154
|
+
const range = simple.minArgs === simple.maxArgs
|
|
8155
|
+
? `exactly ${String(simple.minArgs)}`
|
|
8156
|
+
: simple.maxArgs === Number.POSITIVE_INFINITY
|
|
8157
|
+
? `at least ${String(simple.minArgs)}`
|
|
8158
|
+
: `${String(simple.minArgs)} to ${String(simple.maxArgs)}`;
|
|
8159
|
+
throw new TypeError(`${name} takes ${range} argument${simple.maxArgs === 1 ? "" : "s"}`);
|
|
8160
|
+
}
|
|
8161
|
+
if (args.some((argument) => argument.kind === "wildcard")) {
|
|
8162
|
+
throw new TypeError(`${name} takes scalar arguments`);
|
|
8163
|
+
}
|
|
8164
|
+
}
|
|
7712
8165
|
if (name === "ROUND" && (args.length < 1 || args.length > 2))
|
|
7713
8166
|
throw new TypeError("ROUND requires one or two arguments");
|
|
7714
8167
|
if (name === "COALESCE" && args.length < 1)
|
|
@@ -8202,23 +8655,33 @@ function desugarGroupingSets(parts, nextSequence) {
|
|
|
8202
8655
|
if (hasAggregate(item.expression))
|
|
8203
8656
|
return item;
|
|
8204
8657
|
const signature = signatureOf(item.expression);
|
|
8205
|
-
if (setSignatures.has(signature) ||
|
|
8658
|
+
if (setSignatures.has(signature) || expressionColumns(item.expression).length === 0) {
|
|
8659
|
+
return item;
|
|
8660
|
+
}
|
|
8661
|
+
// A grouping expression this set aggregated away is NULL in every one of its groups.
|
|
8662
|
+
// NULLIF(e, e) is that NULL typed like e, and it substitutes for the expression wherever
|
|
8663
|
+
// it appears: bare (`region`) or inside a wider select expression
|
|
8664
|
+
// (`COALESCE(region, 'all')`, the usual rollup label). MIN over the result keeps the
|
|
8665
|
+
// member block grouped; the value is the same for every row of the group.
|
|
8666
|
+
const away = { count: 0 };
|
|
8667
|
+
const nullify = (expression) => {
|
|
8668
|
+
const inner = signatureOf(expression);
|
|
8669
|
+
if (universe.has(inner) && !setSignatures.has(inner)) {
|
|
8670
|
+
away.count += 1;
|
|
8671
|
+
return {
|
|
8672
|
+
kind: "call",
|
|
8673
|
+
name: "NULLIF",
|
|
8674
|
+
arguments: [structuredClone(expression), structuredClone(expression)],
|
|
8675
|
+
};
|
|
8676
|
+
}
|
|
8677
|
+
return mapChildExpressions(expression, nullify);
|
|
8678
|
+
};
|
|
8679
|
+
const nullified = nullify(structuredClone(item.expression));
|
|
8680
|
+
if (away.count === 0)
|
|
8206
8681
|
return item;
|
|
8207
|
-
// MIN over an always-NULL argument: legal in a grouped select, NULL in every group, and
|
|
8208
|
-
// typed like the original expression through MIN's carry and NULLIF's first argument.
|
|
8209
8682
|
return {
|
|
8210
8683
|
alias: item.alias,
|
|
8211
|
-
expression: {
|
|
8212
|
-
kind: "call",
|
|
8213
|
-
name: "MIN",
|
|
8214
|
-
arguments: [
|
|
8215
|
-
{
|
|
8216
|
-
kind: "call",
|
|
8217
|
-
name: "NULLIF",
|
|
8218
|
-
arguments: [structuredClone(item.expression), structuredClone(item.expression)],
|
|
8219
|
-
},
|
|
8220
|
-
],
|
|
8221
|
-
},
|
|
8684
|
+
expression: { kind: "call", name: "MIN", arguments: [nullified] },
|
|
8222
8685
|
};
|
|
8223
8686
|
});
|
|
8224
8687
|
return assembleSelectBlock({
|
|
@@ -8258,6 +8721,80 @@ function resolveOrderByOrdinals(orderBy, select) {
|
|
|
8258
8721
|
return { ...order, expression: { kind: "column", reference: item.alias } };
|
|
8259
8722
|
});
|
|
8260
8723
|
}
|
|
8724
|
+
/**
|
|
8725
|
+
* `SELECT id FROM people AS p ORDER BY p.id`: a qualified sort key that names the same source
|
|
8726
|
+
* column the select list carries unqualified sorts by that output. Only a qualifier that is one
|
|
8727
|
+
* of the block's own sources counts, and only when exactly one selected column matches, so an
|
|
8728
|
+
* ambiguous or misspelled reference still fails in resolution.
|
|
8729
|
+
*/
|
|
8730
|
+
function resolveOrderQualifiers(orderBy, select, sourceAliases) {
|
|
8731
|
+
return orderBy.map((order) => {
|
|
8732
|
+
const expression = order.expression;
|
|
8733
|
+
if (expression.kind !== "column")
|
|
8734
|
+
return order;
|
|
8735
|
+
const separator = expression.reference.indexOf(".");
|
|
8736
|
+
if (separator === -1)
|
|
8737
|
+
return order;
|
|
8738
|
+
// A selected column referenced exactly as written orders by that output column: a wildcard
|
|
8739
|
+
// expansion reads `p.name` but names the output `name`.
|
|
8740
|
+
const exact = select.find((item) => item.expression.kind === "column" && item.expression.reference === expression.reference);
|
|
8741
|
+
if (exact !== undefined) {
|
|
8742
|
+
return exact.alias === expression.reference
|
|
8743
|
+
? order
|
|
8744
|
+
: { ...order, expression: { kind: "column", reference: exact.alias } };
|
|
8745
|
+
}
|
|
8746
|
+
const qualifier = expression.reference.slice(0, separator);
|
|
8747
|
+
const column = expression.reference.slice(separator + 1);
|
|
8748
|
+
if (!sourceAliases.includes(qualifier))
|
|
8749
|
+
return order;
|
|
8750
|
+
const matches = select.filter((item) => item.expression.kind === "column" &&
|
|
8751
|
+
!item.expression.reference.includes(".") &&
|
|
8752
|
+
item.expression.reference === column);
|
|
8753
|
+
const match = matches.length === 1 ? matches[0] : undefined;
|
|
8754
|
+
if (match === undefined)
|
|
8755
|
+
return order;
|
|
8756
|
+
return { ...order, expression: { kind: "column", reference: match.alias } };
|
|
8757
|
+
});
|
|
8758
|
+
}
|
|
8759
|
+
/**
|
|
8760
|
+
* Resolves GROUP BY items the way PostgreSQL does: an integer literal is a select-list ordinal,
|
|
8761
|
+
* and a bare name that is an output alias — but not itself the selected column of that name —
|
|
8762
|
+
* stands for the aliased expression. `GROUP BY 1` and `GROUP BY bucket` over
|
|
8763
|
+
* `SELECT FLOOR(amount / 25) AS bucket` therefore group by the expression. A name that is both
|
|
8764
|
+
* an alias and a source column keeps its source-column meaning, as it does in PostgreSQL.
|
|
8765
|
+
*/
|
|
8766
|
+
function resolveGroupByReferences(groupBy, select) {
|
|
8767
|
+
return groupBy.map((expression) => {
|
|
8768
|
+
if (expression.kind === "literal" && typeof expression.value === "number") {
|
|
8769
|
+
const ordinal = expression.value;
|
|
8770
|
+
const item = Number.isInteger(ordinal) ? select[ordinal - 1] : undefined;
|
|
8771
|
+
if (item === undefined) {
|
|
8772
|
+
throw new TypeError(`GROUP BY ordinal is out of range: ${String(ordinal)}`);
|
|
8773
|
+
}
|
|
8774
|
+
if (hasAggregate(item.expression) || containsWindow(item.expression)) {
|
|
8775
|
+
throw new TypeError("GROUP BY ordinals cannot name an aggregate or window column");
|
|
8776
|
+
}
|
|
8777
|
+
return structuredClone(item.expression);
|
|
8778
|
+
}
|
|
8779
|
+
if (expression.kind !== "column" || expression.reference.includes("."))
|
|
8780
|
+
return expression;
|
|
8781
|
+
const aliased = select.filter((item) => item.alias === expression.reference);
|
|
8782
|
+
const item = aliased.length === 1 ? aliased[0] : undefined;
|
|
8783
|
+
if (item === undefined ||
|
|
8784
|
+
item.expression.kind === "wildcard" ||
|
|
8785
|
+
hasAggregate(item.expression) ||
|
|
8786
|
+
containsWindow(item.expression)) {
|
|
8787
|
+
return expression;
|
|
8788
|
+
}
|
|
8789
|
+
// `SELECT region AS r ... GROUP BY r` groups by region. Were r also a source column,
|
|
8790
|
+
// PostgreSQL would group by that column and then reject the ungrouped region, so reading
|
|
8791
|
+
// the alias never changes an answer PostgreSQL gives.
|
|
8792
|
+
if (item.expression.kind === "column" && item.expression.reference === expression.reference) {
|
|
8793
|
+
return expression;
|
|
8794
|
+
}
|
|
8795
|
+
return structuredClone(item.expression);
|
|
8796
|
+
});
|
|
8797
|
+
}
|
|
8261
8798
|
/**
|
|
8262
8799
|
* FULL OUTER JOIN desugars into UNION ALL of two left joins: the plain left join carries every
|
|
8263
8800
|
* base row (matched or not), and the swapped left join filtered to a NULL-extended base side
|
|
@@ -8341,7 +8878,47 @@ function desugarFullJoin(parts, nextSequence) {
|
|
|
8341
8878
|
}, nextSequence);
|
|
8342
8879
|
}
|
|
8343
8880
|
export function assembleSelectBlock(parts, nextSequence) {
|
|
8344
|
-
|
|
8881
|
+
if (parts.select.some((item) => item.expression.kind === "wildcard")) {
|
|
8882
|
+
if (parts.joins.some((join) => join.full === true)) {
|
|
8883
|
+
throw new TypeError("FULL JOIN cannot be combined with SELECT *");
|
|
8884
|
+
}
|
|
8885
|
+
// A wildcard's width and names come from source schemas, but ordinals, hidden ORDER BY
|
|
8886
|
+
// columns, DISTINCT grouping, windows, grouping sets, and FULL JOIN lowering all depend on
|
|
8887
|
+
// that width. Preserve the raw block until schema binding instead of teaching each lowering
|
|
8888
|
+
// a different late wildcard exception.
|
|
8889
|
+
return {
|
|
8890
|
+
sql: parts.sql,
|
|
8891
|
+
base: parts.base,
|
|
8892
|
+
joins: parts.joins,
|
|
8893
|
+
select: parts.select,
|
|
8894
|
+
predicates: parts.predicates,
|
|
8895
|
+
groupBy: parts.groupBy,
|
|
8896
|
+
having: parts.having,
|
|
8897
|
+
orderBy: parts.orderBy,
|
|
8898
|
+
...(parts.limit === undefined ? {} : { limit: parts.limit }),
|
|
8899
|
+
...(parts.offset === undefined ? {} : { offset: parts.offset }),
|
|
8900
|
+
...(parts.limitParameter === undefined ? {} : { limitParameter: parts.limitParameter }),
|
|
8901
|
+
...(parts.offsetParameter === undefined ? {} : { offsetParameter: parts.offsetParameter }),
|
|
8902
|
+
...(parts.limitWithTies === true ? { limitWithTies: true } : {}),
|
|
8903
|
+
pendingSelectShape: {
|
|
8904
|
+
distinct: parts.distinct,
|
|
8905
|
+
...(parts.groupingSets === undefined
|
|
8906
|
+
? {}
|
|
8907
|
+
: { groupingSets: structuredClone(parts.groupingSets) }),
|
|
8908
|
+
},
|
|
8909
|
+
...(parts.distinct ? { distinctWildcard: true } : {}),
|
|
8910
|
+
};
|
|
8911
|
+
}
|
|
8912
|
+
parts = {
|
|
8913
|
+
...parts,
|
|
8914
|
+
orderBy: resolveOrderQualifiers(resolveOrderByOrdinals(parts.orderBy, parts.select), parts.select, [parts.base, ...parts.joins].map((source) => source.alias)),
|
|
8915
|
+
groupBy: resolveGroupByReferences(parts.groupBy, parts.select),
|
|
8916
|
+
...(parts.groupingSets === undefined
|
|
8917
|
+
? {}
|
|
8918
|
+
: {
|
|
8919
|
+
groupingSets: parts.groupingSets.map((set) => resolveGroupByReferences(set, parts.select)),
|
|
8920
|
+
}),
|
|
8921
|
+
};
|
|
8345
8922
|
if (parts.joins.some((join) => join.full === true)) {
|
|
8346
8923
|
return desugarFullJoin(parts, nextSequence);
|
|
8347
8924
|
}
|
|
@@ -8359,15 +8936,41 @@ export function assembleSelectBlock(parts, nextSequence) {
|
|
|
8359
8936
|
const { limitParameter, offsetParameter } = parts;
|
|
8360
8937
|
const groupBy = [...parts.groupBy];
|
|
8361
8938
|
let distinctWildcard = false;
|
|
8939
|
+
if (distinct &&
|
|
8940
|
+
(groupBy.length > 0 ||
|
|
8941
|
+
having.length > 0 ||
|
|
8942
|
+
select.some((item) => hasAggregate(item.expression) || hasWindow(item.expression)))) {
|
|
8943
|
+
// DISTINCT over a grouped, aggregated, or windowed block applies to that block's output:
|
|
8944
|
+
// run the block as written inside a derived table and take the distinct rows of it,
|
|
8945
|
+
// carrying the ORDER BY and paging outward, which is where they act in PostgreSQL too.
|
|
8946
|
+
const inner = assembleSelectBlock({ ...parts, distinct: false, orderBy: [] }, nextSequence);
|
|
8947
|
+
delete inner.limit;
|
|
8948
|
+
delete inner.offset;
|
|
8949
|
+
delete inner.limitParameter;
|
|
8950
|
+
delete inner.offsetParameter;
|
|
8951
|
+
delete inner.limitWithTies;
|
|
8952
|
+
const source = derivedTableSource(inner, "(distinct)", nextSequence);
|
|
8953
|
+
return assembleSelectBlock({
|
|
8954
|
+
sql,
|
|
8955
|
+
base: source,
|
|
8956
|
+
joins: [],
|
|
8957
|
+
select: inner.select.map((item) => ({
|
|
8958
|
+
expression: { kind: "column", reference: item.alias },
|
|
8959
|
+
alias: item.alias,
|
|
8960
|
+
})),
|
|
8961
|
+
distinct: true,
|
|
8962
|
+
predicates: [],
|
|
8963
|
+
groupBy: [],
|
|
8964
|
+
having: [],
|
|
8965
|
+
orderBy: parts.orderBy,
|
|
8966
|
+
...(limit === undefined ? {} : { limit }),
|
|
8967
|
+
...(offset === undefined ? {} : { offset }),
|
|
8968
|
+
...(limitParameter === undefined ? {} : { limitParameter }),
|
|
8969
|
+
...(offsetParameter === undefined ? {} : { offsetParameter }),
|
|
8970
|
+
...(parts.limitWithTies === true ? { limitWithTies: true } : {}),
|
|
8971
|
+
}, nextSequence);
|
|
8972
|
+
}
|
|
8362
8973
|
if (distinct) {
|
|
8363
|
-
if (select.some((item) => hasAggregate(item.expression)))
|
|
8364
|
-
throw new TypeError("SELECT DISTINCT cannot be combined with aggregate functions");
|
|
8365
|
-
if (select.some((item) => hasWindow(item.expression)))
|
|
8366
|
-
throw new TypeError("SELECT DISTINCT cannot be combined with window functions");
|
|
8367
|
-
if (groupBy.length > 0)
|
|
8368
|
-
throw new TypeError("SELECT DISTINCT cannot be combined with GROUP BY");
|
|
8369
|
-
if (having.length > 0)
|
|
8370
|
-
throw new TypeError("SELECT DISTINCT cannot be combined with HAVING");
|
|
8371
8974
|
if (select.some((item) => item.expression.kind === "wildcard")) {
|
|
8372
8975
|
// The wildcard's columns are unknown until input schemas exist; executor entries
|
|
8373
8976
|
// expand the flag into a concrete select list plus GROUP BY (expandDistinctWildcard).
|
|
@@ -8450,15 +9053,19 @@ function expandDistinctWildcard(plan, columnsOf) {
|
|
|
8450
9053
|
columns: sourceWildcardColumns(source, columnsOf),
|
|
8451
9054
|
}));
|
|
8452
9055
|
const visible = shaped.filter(({ columns }) => (columns?.length ?? 0) > 0);
|
|
8453
|
-
const
|
|
9056
|
+
const contributed = new Map();
|
|
9057
|
+
for (const { columns } of visible) {
|
|
9058
|
+
for (const name of columns ?? [])
|
|
9059
|
+
contributed.set(name, (contributed.get(name) ?? 0) + 1);
|
|
9060
|
+
}
|
|
8454
9061
|
const select = visible.flatMap(({ source, columns }) => {
|
|
8455
9062
|
if (columns === undefined) {
|
|
8456
9063
|
throw new TypeError(`SELECT DISTINCT * requires known columns for: ${source.table}`);
|
|
8457
9064
|
}
|
|
8458
|
-
return columns.map((name) => {
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
});
|
|
9065
|
+
return columns.map((name) => ({
|
|
9066
|
+
expression: { kind: "column", reference: `${source.alias}.${name}` },
|
|
9067
|
+
alias: (contributed.get(name) ?? 0) > 1 ? `${source.alias}.${name}` : name,
|
|
9068
|
+
}));
|
|
8462
9069
|
});
|
|
8463
9070
|
const { distinctWildcard, ...rest } = plan;
|
|
8464
9071
|
void distinctWildcard;
|
|
@@ -8583,6 +9190,12 @@ function withTiesPlan(plan) {
|
|
|
8583
9190
|
* table's own aliases, a set operation's first member's, and otherwise the input table's.
|
|
8584
9191
|
*/
|
|
8585
9192
|
function sourceWildcardColumns(source, columnsOf) {
|
|
9193
|
+
if (source.derived?.base.union !== undefined &&
|
|
9194
|
+
source.derived.select[0]?.expression.kind === "wildcard") {
|
|
9195
|
+
return source.derived.base.union.blocks[0]?.select
|
|
9196
|
+
.map((item) => item.alias)
|
|
9197
|
+
.filter((name) => !name.startsWith("\0"));
|
|
9198
|
+
}
|
|
8586
9199
|
if (source.derived !== undefined)
|
|
8587
9200
|
return source.derived.select.map((item) => item.alias).filter((name) => !name.startsWith("\0"));
|
|
8588
9201
|
if (source.union !== undefined) {
|
|
@@ -8590,8 +9203,202 @@ function sourceWildcardColumns(source, columnsOf) {
|
|
|
8590
9203
|
.map((item) => item.alias)
|
|
8591
9204
|
.filter((name) => !name.startsWith("\0"));
|
|
8592
9205
|
}
|
|
9206
|
+
if (source.windowed !== undefined) {
|
|
9207
|
+
return [
|
|
9208
|
+
...source.windowed.block.select.map((item) => item.alias),
|
|
9209
|
+
...source.windowed.windows.map((window) => window.alias),
|
|
9210
|
+
].filter((name) => !name.startsWith("\0"));
|
|
9211
|
+
}
|
|
9212
|
+
if (source.recursive !== undefined) {
|
|
9213
|
+
return source.recursive.base.select
|
|
9214
|
+
.map((item) => item.alias)
|
|
9215
|
+
.filter((name) => !name.startsWith("\0"));
|
|
9216
|
+
}
|
|
8593
9217
|
return columnsOf(source.table)?.filter((name) => !name.startsWith("\0"));
|
|
8594
9218
|
}
|
|
9219
|
+
/** Whether any block still needs source schemas before its SELECT shape can be lowered. */
|
|
9220
|
+
export function planHasPendingSelectShapes(plan) {
|
|
9221
|
+
if (plan.pendingSelectShape !== undefined ||
|
|
9222
|
+
plan.pendingOutputAliases !== undefined ||
|
|
9223
|
+
plan.pendingSetOrder === true) {
|
|
9224
|
+
return true;
|
|
9225
|
+
}
|
|
9226
|
+
let pending = false;
|
|
9227
|
+
forEachNestedBlock(plan, (nested) => {
|
|
9228
|
+
pending ||= planHasPendingSelectShapes(nested);
|
|
9229
|
+
});
|
|
9230
|
+
const inspect = (expression) => {
|
|
9231
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
9232
|
+
pending ||= planHasPendingSelectShapes(expression.block);
|
|
9233
|
+
return;
|
|
9234
|
+
}
|
|
9235
|
+
childExpressions(expression).forEach(inspect);
|
|
9236
|
+
};
|
|
9237
|
+
forEachBlockExpression(plan, inspect);
|
|
9238
|
+
return pending;
|
|
9239
|
+
}
|
|
9240
|
+
/**
|
|
9241
|
+
* Expands schema-dependent SELECT wildcards, then sends the now-concrete block through the same
|
|
9242
|
+
* lowering used by ordinary named select lists. This is the single boundary at which wildcard
|
|
9243
|
+
* width becomes plan shape: DISTINCT, windows, ORDER BY expressions/ordinals, set operations,
|
|
9244
|
+
* CTE/derived column lists, grouping sets, and FULL JOIN therefore cannot disagree about it.
|
|
9245
|
+
*/
|
|
9246
|
+
export function bindPendingSelectShapes(plan, columnsOf) {
|
|
9247
|
+
if (!planHasPendingSelectShapes(plan))
|
|
9248
|
+
return plan;
|
|
9249
|
+
const bound = structuredClone(plan);
|
|
9250
|
+
const preserveUnoptimizedShape = bound.preserveUnoptimizedShape === true;
|
|
9251
|
+
delete bound.preserveUnoptimizedShape;
|
|
9252
|
+
let sequence = 0;
|
|
9253
|
+
const scanSequence = (block) => {
|
|
9254
|
+
for (const source of [block.base, ...block.joins]) {
|
|
9255
|
+
const match = /\((?:derived|window|union) (\d+)\)/.exec(source.table);
|
|
9256
|
+
if (match?.[1] !== undefined)
|
|
9257
|
+
sequence = Math.max(sequence, Number(match[1]));
|
|
9258
|
+
}
|
|
9259
|
+
forEachNestedBlock(block, scanSequence);
|
|
9260
|
+
};
|
|
9261
|
+
scanSequence(bound);
|
|
9262
|
+
const nextSequence = () => {
|
|
9263
|
+
sequence += 1;
|
|
9264
|
+
return sequence;
|
|
9265
|
+
};
|
|
9266
|
+
const bindExpressionBlocks = (expression) => {
|
|
9267
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
9268
|
+
return { ...expression, block: bindBlock(expression.block) };
|
|
9269
|
+
}
|
|
9270
|
+
if (expression.kind === "window") {
|
|
9271
|
+
return {
|
|
9272
|
+
...expression,
|
|
9273
|
+
partitionBy: expression.partitionBy.map(bindExpressionBlocks),
|
|
9274
|
+
orderBy: expression.orderBy.map((order) => ({
|
|
9275
|
+
...order,
|
|
9276
|
+
expression: bindExpressionBlocks(order.expression),
|
|
9277
|
+
})),
|
|
9278
|
+
...(expression.argument === undefined
|
|
9279
|
+
? {}
|
|
9280
|
+
: { argument: bindExpressionBlocks(expression.argument) }),
|
|
9281
|
+
};
|
|
9282
|
+
}
|
|
9283
|
+
return mapChildExpressions(expression, bindExpressionBlocks);
|
|
9284
|
+
};
|
|
9285
|
+
const bindSource = (source) => {
|
|
9286
|
+
if (source.derived !== undefined)
|
|
9287
|
+
source.derived = bindBlock(source.derived);
|
|
9288
|
+
if (source.union !== undefined) {
|
|
9289
|
+
source.union.blocks = source.union.blocks.map(bindBlock);
|
|
9290
|
+
}
|
|
9291
|
+
if (source.windowed !== undefined) {
|
|
9292
|
+
source.windowed.block = bindBlock(source.windowed.block);
|
|
9293
|
+
}
|
|
9294
|
+
if (source.recursive !== undefined) {
|
|
9295
|
+
source.recursive.base = bindBlock(source.recursive.base);
|
|
9296
|
+
source.recursive.step = bindBlock(source.recursive.step);
|
|
9297
|
+
}
|
|
9298
|
+
};
|
|
9299
|
+
const expandedSelect = (block) => {
|
|
9300
|
+
const sources = [block.base, ...block.joins];
|
|
9301
|
+
const shaped = sources.map((source) => ({
|
|
9302
|
+
source,
|
|
9303
|
+
columns: source.columnAliases ?? sourceWildcardColumns(source, columnsOf),
|
|
9304
|
+
}));
|
|
9305
|
+
const unknown = shaped.find(({ columns }) => columns === undefined);
|
|
9306
|
+
if (unknown !== undefined) {
|
|
9307
|
+
throw new TypeError(`SELECT * requires known columns for: ${unknown.source.table}`);
|
|
9308
|
+
}
|
|
9309
|
+
// Wildcard outputs keep their bare column names, as PostgreSQL returns them; only a name
|
|
9310
|
+
// that two sources both contribute (`SELECT *` over a join on `id`) is alias-qualified, since
|
|
9311
|
+
// a result row cannot carry two columns named `id`.
|
|
9312
|
+
const contributed = new Map();
|
|
9313
|
+
for (const item of block.select) {
|
|
9314
|
+
if (item.expression.kind !== "wildcard")
|
|
9315
|
+
continue;
|
|
9316
|
+
const table = item.expression.table;
|
|
9317
|
+
for (const { source, columns } of shaped) {
|
|
9318
|
+
if (table !== undefined && source.alias !== table)
|
|
9319
|
+
continue;
|
|
9320
|
+
for (const name of columns ?? [])
|
|
9321
|
+
contributed.set(name, (contributed.get(name) ?? 0) + 1);
|
|
9322
|
+
}
|
|
9323
|
+
}
|
|
9324
|
+
const items = block.select.flatMap((item) => {
|
|
9325
|
+
if (item.expression.kind !== "wildcard")
|
|
9326
|
+
return [item];
|
|
9327
|
+
const table = item.expression.table;
|
|
9328
|
+
const selected = table === undefined ? shaped : shaped.filter(({ source }) => source.alias === table);
|
|
9329
|
+
if (table !== undefined && selected.length === 0) {
|
|
9330
|
+
throw new TypeError(`Unknown table for ${table}.*: ${table}`);
|
|
9331
|
+
}
|
|
9332
|
+
return selected.flatMap(({ source, columns }) => (columns ?? []).map((name) => ({
|
|
9333
|
+
expression: { kind: "column", reference: `${source.alias}.${name}` },
|
|
9334
|
+
alias: (contributed.get(name) ?? 0) > 1 ? `${source.alias}.${name}` : name,
|
|
9335
|
+
})));
|
|
9336
|
+
});
|
|
9337
|
+
const aliases = new Set();
|
|
9338
|
+
for (const item of items) {
|
|
9339
|
+
if (aliases.has(item.alias))
|
|
9340
|
+
throw new TypeError(`Duplicate output column: ${item.alias}`);
|
|
9341
|
+
aliases.add(item.alias);
|
|
9342
|
+
}
|
|
9343
|
+
return items;
|
|
9344
|
+
};
|
|
9345
|
+
const carryPlanFlags = (from, to) => {
|
|
9346
|
+
for (const key of [
|
|
9347
|
+
"parameterCount",
|
|
9348
|
+
"usesStatementDatetime",
|
|
9349
|
+
"usesSequenceCalls",
|
|
9350
|
+
"usesVolatileFunctions",
|
|
9351
|
+
]) {
|
|
9352
|
+
const value = from[key];
|
|
9353
|
+
if (value !== undefined)
|
|
9354
|
+
Object.assign(to, { [key]: value });
|
|
9355
|
+
}
|
|
9356
|
+
};
|
|
9357
|
+
function bindBlock(block) {
|
|
9358
|
+
for (const source of [block.base, ...block.joins])
|
|
9359
|
+
bindSource(source);
|
|
9360
|
+
mapBlockExpressions(block, bindExpressionBlocks);
|
|
9361
|
+
let lowered = block;
|
|
9362
|
+
const pending = block.pendingSelectShape;
|
|
9363
|
+
if (pending !== undefined) {
|
|
9364
|
+
const { pendingSelectShape, pendingOutputAliases, distinctWildcard, ...rest } = block;
|
|
9365
|
+
void pendingSelectShape;
|
|
9366
|
+
void pendingOutputAliases;
|
|
9367
|
+
void distinctWildcard;
|
|
9368
|
+
lowered = assembleSelectBlock({
|
|
9369
|
+
sql: rest.sql,
|
|
9370
|
+
base: rest.base,
|
|
9371
|
+
joins: rest.joins,
|
|
9372
|
+
select: expandedSelect(block),
|
|
9373
|
+
distinct: pending.distinct,
|
|
9374
|
+
predicates: rest.predicates,
|
|
9375
|
+
groupBy: rest.groupBy,
|
|
9376
|
+
having: rest.having,
|
|
9377
|
+
orderBy: rest.orderBy,
|
|
9378
|
+
...(rest.limit === undefined ? {} : { limit: rest.limit }),
|
|
9379
|
+
...(rest.offset === undefined ? {} : { offset: rest.offset }),
|
|
9380
|
+
...(rest.limitParameter === undefined ? {} : { limitParameter: rest.limitParameter }),
|
|
9381
|
+
...(rest.offsetParameter === undefined ? {} : { offsetParameter: rest.offsetParameter }),
|
|
9382
|
+
...(rest.limitWithTies === true ? { limitWithTies: true } : {}),
|
|
9383
|
+
...(pending.groupingSets === undefined ? {} : { groupingSets: pending.groupingSets }),
|
|
9384
|
+
}, nextSequence);
|
|
9385
|
+
carryPlanFlags(block, lowered);
|
|
9386
|
+
}
|
|
9387
|
+
if (lowered.pendingSetOrder === true) {
|
|
9388
|
+
const first = lowered.base.union?.blocks[0];
|
|
9389
|
+
lowered.orderBy = resolveOrderByOrdinals(lowered.orderBy, first?.select ?? []);
|
|
9390
|
+
delete lowered.pendingSetOrder;
|
|
9391
|
+
}
|
|
9392
|
+
const aliases = block.pendingOutputAliases;
|
|
9393
|
+
if (aliases !== undefined) {
|
|
9394
|
+
renameBlockOutputs(lowered, aliases.columns, aliases.sourceName);
|
|
9395
|
+
delete lowered.pendingOutputAliases;
|
|
9396
|
+
}
|
|
9397
|
+
return lowered;
|
|
9398
|
+
}
|
|
9399
|
+
const lowered = bindBlock(bound);
|
|
9400
|
+
return preserveUnoptimizedShape ? lowered : optimizePlan(lowered);
|
|
9401
|
+
}
|
|
8595
9402
|
/** Whether any block of the plan still carries an unresolved NATURAL join marker. */
|
|
8596
9403
|
export function planHasNaturalJoins(plan) {
|
|
8597
9404
|
if (plan.joins.some((join) => join.natural === true))
|
|
@@ -8751,7 +9558,13 @@ function expandQualifiedWildcards(plan, columnsOf) {
|
|
|
8751
9558
|
}
|
|
8752
9559
|
return columns.map((name) => {
|
|
8753
9560
|
const output = multiple ? `${source.alias}.${name}` : name;
|
|
8754
|
-
|
|
9561
|
+
// Keep the source-qualified input reference even though one source exposes a bare output
|
|
9562
|
+
// name. ORDER BY can legally spell either form (`id` or `orders.id`), and the latter must
|
|
9563
|
+
// still match this select item after the wildcard has expanded.
|
|
9564
|
+
return {
|
|
9565
|
+
expression: { kind: "column", reference: `${source.alias}.${name}` },
|
|
9566
|
+
alias: output,
|
|
9567
|
+
};
|
|
8755
9568
|
});
|
|
8756
9569
|
});
|
|
8757
9570
|
const aliases = new Set();
|
|
@@ -8768,7 +9581,10 @@ function expandQualifiedWildcards(plan, columnsOf) {
|
|
|
8768
9581
|
/** Wraps compound members into the set-operation source the executor folds left to right. */
|
|
8769
9582
|
export function compoundSelectBlock(sql, blocks, ops, tail, nextSequence) {
|
|
8770
9583
|
// Set-operation output columns are the first member's, so ordinals resolve against them.
|
|
8771
|
-
|
|
9584
|
+
const pendingSetOrder = blocks.some(planHasPendingSelectShapes);
|
|
9585
|
+
if (!pendingSetOrder) {
|
|
9586
|
+
tail = { ...tail, orderBy: resolveOrderByOrdinals(tail.orderBy, blocks[0]?.select ?? []) };
|
|
9587
|
+
}
|
|
8772
9588
|
return {
|
|
8773
9589
|
sql,
|
|
8774
9590
|
base: {
|
|
@@ -8782,9 +9598,12 @@ export function compoundSelectBlock(sql, blocks, ops, tail, nextSequence) {
|
|
|
8782
9598
|
groupBy: [],
|
|
8783
9599
|
having: [],
|
|
8784
9600
|
orderBy: tail.orderBy,
|
|
9601
|
+
...(pendingSetOrder ? { pendingSetOrder: true } : {}),
|
|
8785
9602
|
...(tail.limit === undefined ? {} : { limit: tail.limit }),
|
|
8786
9603
|
...(tail.offset === undefined ? {} : { offset: tail.offset }),
|
|
8787
9604
|
...(tail.limitWithTies === true ? { limitWithTies: true } : {}),
|
|
9605
|
+
...(tail.limitParameter === undefined ? {} : { limitParameter: tail.limitParameter }),
|
|
9606
|
+
...(tail.offsetParameter === undefined ? {} : { offsetParameter: tail.offsetParameter }),
|
|
8788
9607
|
};
|
|
8789
9608
|
}
|
|
8790
9609
|
/**
|
|
@@ -8792,9 +9611,10 @@ export function compoundSelectBlock(sql, blocks, ops, tail, nextSequence) {
|
|
|
8792
9611
|
* either reuses a structurally identical select item's alias or becomes a hidden "(order N)"
|
|
8793
9612
|
* select item, the ordering (and LIMIT/OFFSET) applies inside that block, and an outer block
|
|
8794
9613
|
* projects only the visible aliases away from a derived source. Runs in the shared assembly,
|
|
8795
|
-
* so the builder and SQL front ends produce identical plans.
|
|
8796
|
-
*
|
|
8797
|
-
*
|
|
9614
|
+
* so the builder and SQL front ends produce identical plans. Schema-bound wildcard blocks reach
|
|
9615
|
+
* this function with concrete items; only a manually constructed unresolved wildcard plan keeps
|
|
9616
|
+
* the named-column restriction. DISTINCT's output would change if hidden expressions joined its
|
|
9617
|
+
* grouping, so it keeps the selected-column restriction.
|
|
8798
9618
|
*/
|
|
8799
9619
|
/**
|
|
8800
9620
|
* Whether an ORDER BY item has to travel as a hidden select item: any expression, and also a
|
|
@@ -8851,17 +9671,39 @@ function assembleOrderByExpressionBlock(parts, nextSequence) {
|
|
|
8851
9671
|
expression: { kind: "column", reference: alias },
|
|
8852
9672
|
};
|
|
8853
9673
|
});
|
|
8854
|
-
const inner = assembleSelectBlock({ ...parts, select: [...parts.select, ...hiddenItems], orderBy: rewrittenOrder }, nextSequence);
|
|
8855
9674
|
// Every ordering expression matched a visible select item — no hidden columns, no wrap.
|
|
8856
|
-
if (hiddenItems.length === 0)
|
|
8857
|
-
return
|
|
9675
|
+
if (hiddenItems.length === 0) {
|
|
9676
|
+
return assembleSelectBlock({ ...parts, orderBy: rewrittenOrder }, nextSequence);
|
|
9677
|
+
}
|
|
9678
|
+
// Once hidden columns exist, the visible projection is wrapped in a derived table. Visible
|
|
9679
|
+
// output names such as `people.id` are legal result labels but are not legal internal column
|
|
9680
|
+
// references there — the dot would be read as the vanished `people` source qualifier. Carry
|
|
9681
|
+
// every visible value through a private dot-free name and restore its public label outside.
|
|
9682
|
+
const visibleInner = parts.select.map((item, index) => ({
|
|
9683
|
+
...item,
|
|
9684
|
+
alias: item.alias.includes(".") ? `(order visible ${String(index + 1)})` : item.alias,
|
|
9685
|
+
}));
|
|
9686
|
+
const visibleAlias = new Map(parts.select.map((item, index) => [item.alias, visibleInner[index]?.alias ?? item.alias]));
|
|
9687
|
+
const innerOrder = rewrittenOrder.map((order) => {
|
|
9688
|
+
if (order.expression.kind !== "column" || order.expression.reference.includes(".")) {
|
|
9689
|
+
return order;
|
|
9690
|
+
}
|
|
9691
|
+
const alias = visibleAlias.get(order.expression.reference);
|
|
9692
|
+
return alias === undefined
|
|
9693
|
+
? order
|
|
9694
|
+
: { ...order, expression: { kind: "column", reference: alias } };
|
|
9695
|
+
});
|
|
9696
|
+
const inner = assembleSelectBlock({ ...parts, select: [...visibleInner, ...hiddenItems], orderBy: innerOrder }, nextSequence);
|
|
8858
9697
|
const source = derivedTableSource(inner, "(ordered)", nextSequence);
|
|
8859
9698
|
return {
|
|
8860
9699
|
sql: parts.sql,
|
|
8861
9700
|
base: source,
|
|
8862
9701
|
joins: [],
|
|
8863
|
-
select: parts.select.map((item) => ({
|
|
8864
|
-
expression: {
|
|
9702
|
+
select: parts.select.map((item, index) => ({
|
|
9703
|
+
expression: {
|
|
9704
|
+
kind: "column",
|
|
9705
|
+
reference: visibleInner[index]?.alias ?? item.alias,
|
|
9706
|
+
},
|
|
8865
9707
|
alias: item.alias,
|
|
8866
9708
|
})),
|
|
8867
9709
|
predicates: [],
|
|
@@ -8933,8 +9775,10 @@ export function derivedTableSource(derived, alias, nextSequence) {
|
|
|
8933
9775
|
}
|
|
8934
9776
|
/** The parser's LIMIT range contract, shared with the typed builder. */
|
|
8935
9777
|
export function validateLimit(limit) {
|
|
8936
|
-
|
|
8937
|
-
|
|
9778
|
+
// LIMIT 0 is a legal, empty page: PostgreSQL returns no rows, and clients use it to read a
|
|
9779
|
+
// result's column shape without fetching data.
|
|
9780
|
+
if (!Number.isSafeInteger(limit) || limit < 0 || limit > 100_000)
|
|
9781
|
+
throw new RangeError("LIMIT must be between 0 and 100,000");
|
|
8938
9782
|
return limit;
|
|
8939
9783
|
}
|
|
8940
9784
|
/**
|
|
@@ -8943,10 +9787,28 @@ export function validateLimit(limit) {
|
|
|
8943
9787
|
* which is later than this.
|
|
8944
9788
|
*/
|
|
8945
9789
|
function renameBlockOutputs(block, columns, name) {
|
|
9790
|
+
// Set-operation output names come from the first member. Its width may itself be pending on a
|
|
9791
|
+
// wildcard, so preserve the alias list on the compound until all members have schema-bound.
|
|
9792
|
+
if (block.base.union !== undefined && block.select[0]?.expression.kind === "wildcard") {
|
|
9793
|
+
const first = block.base.union.blocks[0];
|
|
9794
|
+
if (first === undefined || planHasPendingSelectShapes(first)) {
|
|
9795
|
+
block.pendingOutputAliases = { columns: [...columns], sourceName: name };
|
|
9796
|
+
return;
|
|
9797
|
+
}
|
|
9798
|
+
renameBlockOutputs(first, columns, name);
|
|
9799
|
+
return;
|
|
9800
|
+
}
|
|
9801
|
+
if (block.pendingSelectShape !== undefined) {
|
|
9802
|
+
block.pendingOutputAliases = { columns: [...columns], sourceName: name };
|
|
9803
|
+
return;
|
|
9804
|
+
}
|
|
8946
9805
|
if (block.select.some((item) => item.expression.kind === "wildcard")) {
|
|
8947
|
-
throw new TypeError(`A column list needs named columns in the
|
|
9806
|
+
throw new TypeError(`A column list needs named columns in the query body: ${name}`);
|
|
8948
9807
|
}
|
|
8949
9808
|
if (block.select.length !== columns.length) {
|
|
9809
|
+
if (name === "derived table") {
|
|
9810
|
+
throw new TypeError("Column alias list must match the derived table's column count");
|
|
9811
|
+
}
|
|
8950
9812
|
throw new TypeError(`CTE ${name} declares ${String(columns.length)} columns but selects ${String(block.select.length)}`);
|
|
8951
9813
|
}
|
|
8952
9814
|
block.select = block.select.map((item, index) => ({
|
|
@@ -8987,7 +9849,7 @@ function jsonTableColumnValue(column, value) {
|
|
|
8987
9849
|
if (value instanceof Date)
|
|
8988
9850
|
date = value;
|
|
8989
9851
|
else if (typeof value === "string")
|
|
8990
|
-
date =
|
|
9852
|
+
date = datetimeText(value);
|
|
8991
9853
|
else if (typeof value === "number")
|
|
8992
9854
|
date = new Date(value);
|
|
8993
9855
|
else
|
|
@@ -8998,18 +9860,20 @@ function jsonTableColumnValue(column, value) {
|
|
|
8998
9860
|
return date;
|
|
8999
9861
|
}
|
|
9000
9862
|
function timestampLiteral(text) {
|
|
9001
|
-
const
|
|
9002
|
-
|
|
9003
|
-
if (match === null)
|
|
9863
|
+
const date = parseSqlTimestampText(text);
|
|
9864
|
+
if (date === undefined)
|
|
9004
9865
|
throw new TypeError(`Invalid TIMESTAMP literal: ${text}`);
|
|
9005
|
-
const [, day, time = "00:00:00", zone = "Z"] = match;
|
|
9006
|
-
const seconds = time.length === 5 ? `${time}:00` : time;
|
|
9007
|
-
const date = new Date(`${String(day)}T${seconds}${zone === "Z" ? "Z" : zone}`);
|
|
9008
|
-
if (!Number.isFinite(dateMilliseconds(date))) {
|
|
9009
|
-
throw new TypeError(`Invalid TIMESTAMP literal: ${text}`);
|
|
9010
|
-
}
|
|
9011
9866
|
return date;
|
|
9012
9867
|
}
|
|
9868
|
+
/**
|
|
9869
|
+
* Reads datetime text the way the TIMESTAMP literal does — a zoneless `2026-01-02 03:04:05` is
|
|
9870
|
+
* UTC, never the host's zone — and falls back to the JavaScript parser for other spellings.
|
|
9871
|
+
* `new Date("2026-01-02 03:04:05")` alone would read the same text in local time, so a CAST
|
|
9872
|
+
* would answer differently on two machines.
|
|
9873
|
+
*/
|
|
9874
|
+
export function datetimeText(text) {
|
|
9875
|
+
return parseSqlTimestampText(text) ?? new Date(text);
|
|
9876
|
+
}
|
|
9013
9877
|
/** The parser's OFFSET range contract, shared with the typed builder. */
|
|
9014
9878
|
export function validateOffset(offset) {
|
|
9015
9879
|
if (!Number.isSafeInteger(offset) || offset < 0 || offset > 100_000_000)
|
|
@@ -9037,7 +9901,10 @@ function desugarWindows(sql, base, joins, select, predicates, groupBy, having, t
|
|
|
9037
9901
|
const readableWhenGrouped = (expression) => hasAggregate(expression) ||
|
|
9038
9902
|
expressionColumns(expression).length === 0 ||
|
|
9039
9903
|
groupExpressions.has(JSON.stringify(expression));
|
|
9040
|
-
const
|
|
9904
|
+
const internalAliases = select.map((item, index) => item.alias.includes(".") ? `(window visible ${String(index + 1)})` : item.alias);
|
|
9905
|
+
const innerSelect = select.flatMap((item, index) => containsWindow(item.expression)
|
|
9906
|
+
? []
|
|
9907
|
+
: [{ ...item, alias: internalAliases[index] ?? item.alias }]);
|
|
9041
9908
|
const windows = [];
|
|
9042
9909
|
let hidden = 0;
|
|
9043
9910
|
/** A name for one more column the inner block computes for the wrapper to read back. */
|
|
@@ -9099,21 +9966,52 @@ function desugarWindows(sql, base, joins, select, predicates, groupBy, having, t
|
|
|
9099
9966
|
}
|
|
9100
9967
|
return mapChildExpressions(expression, split);
|
|
9101
9968
|
};
|
|
9102
|
-
const projections = select.map((item) => {
|
|
9969
|
+
const projections = select.map((item, index) => {
|
|
9970
|
+
const internalAlias = internalAliases[index] ?? item.alias;
|
|
9103
9971
|
if (!containsWindow(item.expression)) {
|
|
9104
|
-
return {
|
|
9972
|
+
return {
|
|
9973
|
+
expression: { kind: "column", reference: internalAlias },
|
|
9974
|
+
alias: item.alias,
|
|
9975
|
+
};
|
|
9105
9976
|
}
|
|
9106
|
-
// A window that is the whole select item
|
|
9107
|
-
//
|
|
9977
|
+
// A window that is the whole select item carries a private source-safe name through the
|
|
9978
|
+
// windowed source, then the wrapper restores the public output alias.
|
|
9108
9979
|
if (item.expression.kind === "window") {
|
|
9109
|
-
registerWindow(item.expression,
|
|
9110
|
-
return {
|
|
9980
|
+
registerWindow(item.expression, internalAlias);
|
|
9981
|
+
return {
|
|
9982
|
+
expression: { kind: "column", reference: internalAlias },
|
|
9983
|
+
alias: item.alias,
|
|
9984
|
+
};
|
|
9111
9985
|
}
|
|
9112
9986
|
return { expression: split(item.expression), alias: item.alias };
|
|
9113
9987
|
});
|
|
9114
9988
|
if (innerSelect.length === 0) {
|
|
9115
9989
|
innerSelect.push({ expression: { kind: "literal", value: 1 }, alias: "(window 0)" });
|
|
9116
9990
|
}
|
|
9991
|
+
// Window evaluation replaces the FROM sources with one synthetic source. A qualified ORDER
|
|
9992
|
+
// BY that names a selected column must therefore follow that value through its output alias;
|
|
9993
|
+
// leaving `a.id` in the wrapper would try to resolve the vanished table alias `a`.
|
|
9994
|
+
const windowTail = {
|
|
9995
|
+
...tail,
|
|
9996
|
+
orderBy: tail.orderBy.map((order) => {
|
|
9997
|
+
if (order.expression.kind !== "column")
|
|
9998
|
+
return order;
|
|
9999
|
+
const reference = order.expression.reference;
|
|
10000
|
+
const bare = reference.split(".").at(-1) ?? reference;
|
|
10001
|
+
const selectedIndex = select.findIndex((item) => item.alias === reference ||
|
|
10002
|
+
item.alias === bare ||
|
|
10003
|
+
(item.expression.kind === "column" && item.expression.reference === reference));
|
|
10004
|
+
const selected = selectedIndex < 0 ? undefined : select[selectedIndex];
|
|
10005
|
+
const projected = selectedIndex < 0 ? undefined : projections[selectedIndex];
|
|
10006
|
+
const internalReference = projected?.expression.kind === "column" ? projected.expression.reference : selected?.alias;
|
|
10007
|
+
return selected === undefined || internalReference === undefined
|
|
10008
|
+
? order
|
|
10009
|
+
: {
|
|
10010
|
+
...order,
|
|
10011
|
+
expression: { kind: "column", reference: internalReference },
|
|
10012
|
+
};
|
|
10013
|
+
}),
|
|
10014
|
+
};
|
|
9117
10015
|
const inner = {
|
|
9118
10016
|
sql: "(window input)",
|
|
9119
10017
|
base,
|
|
@@ -9136,7 +10034,7 @@ function desugarWindows(sql, base, joins, select, predicates, groupBy, having, t
|
|
|
9136
10034
|
predicates: [],
|
|
9137
10035
|
groupBy: [],
|
|
9138
10036
|
having: [],
|
|
9139
|
-
...
|
|
10037
|
+
...windowTail,
|
|
9140
10038
|
};
|
|
9141
10039
|
}
|
|
9142
10040
|
/**
|
|
@@ -9349,6 +10247,29 @@ function tokenize(sql) {
|
|
|
9349
10247
|
index += 2;
|
|
9350
10248
|
continue;
|
|
9351
10249
|
}
|
|
10250
|
+
if (pair === "::") {
|
|
10251
|
+
// PostgreSQL's postfix cast, `expression::type`.
|
|
10252
|
+
push({ kind: "operator", text: "::", start: index, end: index + 2 });
|
|
10253
|
+
index += 2;
|
|
10254
|
+
continue;
|
|
10255
|
+
}
|
|
10256
|
+
if (pair === "!~") {
|
|
10257
|
+
const text = sql[index + 2] === "*" ? "!~*" : "!~";
|
|
10258
|
+
push({ kind: "operator", text, start: index, end: index + text.length });
|
|
10259
|
+
index += text.length;
|
|
10260
|
+
continue;
|
|
10261
|
+
}
|
|
10262
|
+
if (character === "~") {
|
|
10263
|
+
const text = sql[index + 1] === "*" ? "~*" : "~";
|
|
10264
|
+
push({ kind: "operator", text, start: index, end: index + text.length });
|
|
10265
|
+
index += text.length;
|
|
10266
|
+
continue;
|
|
10267
|
+
}
|
|
10268
|
+
if (character === "^") {
|
|
10269
|
+
push({ kind: "operator", text: "^", start: index, end: index + 1 });
|
|
10270
|
+
index += 1;
|
|
10271
|
+
continue;
|
|
10272
|
+
}
|
|
9352
10273
|
if (["+", "-", "*", "/", "%", "=", ">", "<"].includes(character))
|
|
9353
10274
|
push({ kind: "operator", text: character, start: index, end: index + 1 });
|
|
9354
10275
|
else if (["(", ")", "[", "]", ",", "."].includes(character))
|