@minnowdb/core 0.6.9 → 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/dist/date-value.d.ts +5 -0
- package/dist/date-value.js +41 -0
- package/dist/engine/database.js +913 -54
- 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 +39 -0
- package/dist/engine/query.js +811 -203
- 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 +3 -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 +252 -2
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) {
|
|
@@ -903,12 +1028,18 @@ export function compileQuery(sql, options = {}) {
|
|
|
903
1028
|
}
|
|
904
1029
|
if (parser.parameterCount > 0)
|
|
905
1030
|
compiled.parameterCount = parser.parameterCount;
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
+
}
|
|
912
1043
|
return compiled;
|
|
913
1044
|
}
|
|
914
1045
|
/**
|
|
@@ -1921,6 +2052,11 @@ export function inferBlockSchema(plan, schemas) {
|
|
|
1921
2052
|
return undefined;
|
|
1922
2053
|
if (expression.name === "CURRENT_DATE")
|
|
1923
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" };
|
|
1924
2060
|
if (expression.name === "CAST") {
|
|
1925
2061
|
const target = expression.arguments[1];
|
|
1926
2062
|
return target?.kind === "literal" && typeof target.value === "string"
|
|
@@ -2023,11 +2159,10 @@ export function inferBlockSchema(plan, schemas) {
|
|
|
2023
2159
|
}
|
|
2024
2160
|
if (expression.kind === "binary") {
|
|
2025
2161
|
if (expression.operator === "||") {
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
}
|
|
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");
|
|
2031
2166
|
}
|
|
2032
2167
|
return "string";
|
|
2033
2168
|
}
|
|
@@ -2079,6 +2214,15 @@ export function inferBlockSchema(plan, schemas) {
|
|
|
2079
2214
|
}
|
|
2080
2215
|
if (expression.name === "JSON_EXISTS" || expression.name === "IS_JSON")
|
|
2081
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
|
+
}
|
|
2082
2226
|
if (expression.name === "DATE_TRUNC")
|
|
2083
2227
|
return "datetime";
|
|
2084
2228
|
if (expression.name === "DATE_ADD") {
|
|
@@ -4014,12 +4158,8 @@ function evaluate(expression, context, group) {
|
|
|
4014
4158
|
const right = evaluate(expression.right, context, group);
|
|
4015
4159
|
if (left === null || left === undefined || right === null || right === undefined)
|
|
4016
4160
|
return null;
|
|
4017
|
-
if (expression.operator === "||")
|
|
4018
|
-
if (typeof left !== "string" || typeof right !== "string") {
|
|
4019
|
-
throw new TypeError("|| requires string operands");
|
|
4020
|
-
}
|
|
4161
|
+
if (expression.operator === "||")
|
|
4021
4162
|
return concatenatedSqlValue(left, right);
|
|
4022
|
-
}
|
|
4023
4163
|
const exact = exactNumericBinary(expression.operator, left, right);
|
|
4024
4164
|
if (exact !== undefined)
|
|
4025
4165
|
return exact;
|
|
@@ -4212,6 +4352,11 @@ function evaluatePredicate(predicate, context) {
|
|
|
4212
4352
|
return false;
|
|
4213
4353
|
if (membership.set.has(comparable(value)))
|
|
4214
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
|
+
}
|
|
4215
4360
|
return predicate.operator === "NOT IN" && !membership.hasNull;
|
|
4216
4361
|
}
|
|
4217
4362
|
return inListHolds(predicate.operator, evaluate(predicate.left, context), predicate.right.items.map((item) => evaluate(item, context)));
|
|
@@ -4240,7 +4385,8 @@ function inListHolds(operator, value, items) {
|
|
|
4240
4385
|
hasNull = true;
|
|
4241
4386
|
continue;
|
|
4242
4387
|
}
|
|
4243
|
-
|
|
4388
|
+
const member = typeof item === "string" && typeof value !== "string" ? coercedComparable(item, value) : item;
|
|
4389
|
+
if (comparable(value) === comparable(member))
|
|
4244
4390
|
return operator === "IN";
|
|
4245
4391
|
}
|
|
4246
4392
|
return operator === "NOT IN" && !hasNull;
|
|
@@ -4259,13 +4405,17 @@ export function cachedListMembership(node, items) {
|
|
|
4259
4405
|
if (items.every((item) => item.kind === "literal")) {
|
|
4260
4406
|
const set = new Set();
|
|
4261
4407
|
let hasNull = false;
|
|
4408
|
+
let hasText = false;
|
|
4262
4409
|
for (const item of items) {
|
|
4263
4410
|
if (item.value === null || item.value === undefined)
|
|
4264
4411
|
hasNull = true;
|
|
4265
|
-
else
|
|
4412
|
+
else {
|
|
4413
|
+
if (typeof item.value === "string")
|
|
4414
|
+
hasText = true;
|
|
4266
4415
|
set.add(comparable(item.value));
|
|
4416
|
+
}
|
|
4267
4417
|
}
|
|
4268
|
-
cached = { set, hasNull };
|
|
4418
|
+
cached = { set, hasNull, hasText };
|
|
4269
4419
|
}
|
|
4270
4420
|
else {
|
|
4271
4421
|
cached = null;
|
|
@@ -4285,6 +4435,14 @@ const extractFields = new Set([
|
|
|
4285
4435
|
"second",
|
|
4286
4436
|
"epoch",
|
|
4287
4437
|
"dow",
|
|
4438
|
+
"doy",
|
|
4439
|
+
"isodow",
|
|
4440
|
+
"isoyear",
|
|
4441
|
+
"decade",
|
|
4442
|
+
"century",
|
|
4443
|
+
"millennium",
|
|
4444
|
+
"milliseconds",
|
|
4445
|
+
"microseconds",
|
|
4288
4446
|
]);
|
|
4289
4447
|
/**
|
|
4290
4448
|
* EXTRACT(field FROM datetime), in UTC. `week` is the ISO 8601 week number and `dow` counts
|
|
@@ -4297,34 +4455,73 @@ function extractDatePart(field, value) {
|
|
|
4297
4455
|
}
|
|
4298
4456
|
if (value === null || value === undefined)
|
|
4299
4457
|
return null;
|
|
4300
|
-
|
|
4301
|
-
|
|
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;
|
|
4302
4477
|
switch (normalized) {
|
|
4303
|
-
case "year":
|
|
4304
|
-
return dateUtcFullYear(value);
|
|
4305
|
-
case "quarter":
|
|
4306
|
-
return Math.floor(dateUtcMonth(value) / 3) + 1;
|
|
4307
|
-
case "month":
|
|
4308
|
-
return dateUtcMonth(value) + 1;
|
|
4309
|
-
case "week": {
|
|
4310
|
-
const date = new Date(Date.UTC(dateUtcFullYear(value), dateUtcMonth(value), dateUtcDate(value)));
|
|
4311
|
-
// ISO week: shift to the Thursday of this week, then count weeks from January 1st.
|
|
4312
|
-
setDateUtcDate(date, dateUtcDate(date) + 4 - (dateUtcDay(date) || 7));
|
|
4313
|
-
const yearStart = Date.UTC(dateUtcFullYear(date), 0, 1);
|
|
4314
|
-
return Math.ceil(((dateMilliseconds(date) - yearStart) / 86_400_000 + 1) / 7);
|
|
4315
|
-
}
|
|
4316
|
-
case "day":
|
|
4317
|
-
return dateUtcDate(value);
|
|
4318
4478
|
case "hour":
|
|
4319
|
-
return
|
|
4479
|
+
return Math.floor(timeOfDay / 3_600_000);
|
|
4320
4480
|
case "minute":
|
|
4321
|
-
return
|
|
4481
|
+
return Math.floor(timeOfDay / 60_000) % 60;
|
|
4322
4482
|
case "second":
|
|
4323
|
-
return
|
|
4324
|
-
case "
|
|
4325
|
-
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
|
+
}
|
|
4326
4494
|
default:
|
|
4327
|
-
|
|
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
|
+
}
|
|
4328
4525
|
}
|
|
4329
4526
|
}
|
|
4330
4527
|
/**
|
|
@@ -4540,6 +4737,9 @@ export function comparisonHolds(operator, leftValue, rightValue) {
|
|
|
4540
4737
|
rightValue === null ||
|
|
4541
4738
|
rightValue === undefined)
|
|
4542
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);
|
|
4543
4743
|
const left = comparable(leftValue);
|
|
4544
4744
|
const right = comparable(rightValue);
|
|
4545
4745
|
if (operator === "=")
|
|
@@ -4625,6 +4825,23 @@ function hasWindow(expression) {
|
|
|
4625
4825
|
export function isAggregateCall(expression) {
|
|
4626
4826
|
return expression.kind === "call" && aggregateNames.has(expression.name);
|
|
4627
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
|
+
}
|
|
4628
4845
|
function validateGrouping(plan) {
|
|
4629
4846
|
const grouped = plan.groupBy.length > 0 || plan.select.some((item) => hasAggregate(item.expression));
|
|
4630
4847
|
if (!grouped)
|
|
@@ -4640,7 +4857,7 @@ function validateGrouping(plan) {
|
|
|
4640
4857
|
!containsFtsExpression(item.expression)) {
|
|
4641
4858
|
continue;
|
|
4642
4859
|
}
|
|
4643
|
-
if (!
|
|
4860
|
+
if (!groupedExpression(item.expression, groupExpressions)) {
|
|
4644
4861
|
throw new TypeError(`Selected column must appear in GROUP BY: ${item.alias}`);
|
|
4645
4862
|
}
|
|
4646
4863
|
}
|
|
@@ -4807,6 +5024,12 @@ class Parser {
|
|
|
4807
5024
|
/** Placeholders seen so far: positional `?` count, and the highest `$n` number. */
|
|
4808
5025
|
#positionalParameters = 0;
|
|
4809
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;
|
|
4810
5033
|
/** Set when the statement names CURRENT_DATE, CURRENT_TIMESTAMP, or LOCALTIME. */
|
|
4811
5034
|
usesStatementDatetime = false;
|
|
4812
5035
|
/** Set when the statement names NEXTVAL or CURRVAL. */
|
|
@@ -4925,7 +5148,7 @@ class Parser {
|
|
|
4925
5148
|
}
|
|
4926
5149
|
}
|
|
4927
5150
|
// INTERSECT binds tighter than UNION and EXCEPT, matching PostgreSQL.
|
|
4928
|
-
const firstTerm = this.#setTerm(sql);
|
|
5151
|
+
const firstTerm = this.#setTerm(sql, false);
|
|
4929
5152
|
let plan = firstTerm.block;
|
|
4930
5153
|
if (this.#isKeyword("UNION") || this.#isKeyword("EXCEPT")) {
|
|
4931
5154
|
const members = [firstTerm];
|
|
@@ -4949,14 +5172,38 @@ class Parser {
|
|
|
4949
5172
|
else
|
|
4950
5173
|
ops.push("except");
|
|
4951
5174
|
}
|
|
4952
|
-
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);
|
|
4953
5191
|
}
|
|
4954
|
-
plan = this.#compoundBlock(sql, members, ops);
|
|
4955
5192
|
}
|
|
4956
5193
|
return plan;
|
|
4957
5194
|
}
|
|
4958
|
-
|
|
4959
|
-
|
|
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);
|
|
4960
5207
|
if (!this.#isKeyword("INTERSECT"))
|
|
4961
5208
|
return first;
|
|
4962
5209
|
const members = [first];
|
|
@@ -4969,46 +5216,20 @@ class Parser {
|
|
|
4969
5216
|
}
|
|
4970
5217
|
else
|
|
4971
5218
|
ops.push("intersect");
|
|
4972
|
-
members.push(this.#unionMember("(intersect member)"));
|
|
5219
|
+
members.push(this.#unionMember("(intersect member)", true));
|
|
4973
5220
|
}
|
|
4974
|
-
|
|
5221
|
+
const tail = member ? { orderBy: [] } : this.#compoundTail();
|
|
5222
|
+
return { block: this.#compoundBlock(sql, members, ops, tail), parenthesized: false };
|
|
4975
5223
|
}
|
|
4976
|
-
#compoundBlock(sql, members, ops) {
|
|
4977
|
-
|
|
4978
|
-
|
|
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) {
|
|
4979
5228
|
if (!member.parenthesized &&
|
|
4980
|
-
!last &&
|
|
4981
5229
|
(member.block.orderBy.length > 0 || member.block.limit !== undefined)) {
|
|
4982
5230
|
throw new TypeError("ORDER BY or LIMIT in a UNION member requires parentheses");
|
|
4983
5231
|
}
|
|
4984
5232
|
}
|
|
4985
|
-
// PostgreSQL assigns a trailing ORDER BY or LIMIT to the whole compound. After an
|
|
4986
|
-
// unparenthesized last member the clause was greedily parsed into that member and lifts
|
|
4987
|
-
// out; after a parenthesized member it is still unparsed.
|
|
4988
|
-
const last = members[members.length - 1];
|
|
4989
|
-
let tail;
|
|
4990
|
-
if (last !== undefined && !last.parenthesized) {
|
|
4991
|
-
tail = {
|
|
4992
|
-
orderBy: last.block.orderBy,
|
|
4993
|
-
...(last.block.limit === undefined ? {} : { limit: last.block.limit }),
|
|
4994
|
-
...(last.block.offset === undefined ? {} : { offset: last.block.offset }),
|
|
4995
|
-
...(last.block.limitWithTies === true ? { limitWithTies: true } : {}),
|
|
4996
|
-
...(last.block.limitParameter === undefined
|
|
4997
|
-
? {}
|
|
4998
|
-
: { limitParameter: last.block.limitParameter }),
|
|
4999
|
-
...(last.block.offsetParameter === undefined
|
|
5000
|
-
? {}
|
|
5001
|
-
: { offsetParameter: last.block.offsetParameter }),
|
|
5002
|
-
};
|
|
5003
|
-
last.block.orderBy = [];
|
|
5004
|
-
delete last.block.limit;
|
|
5005
|
-
delete last.block.offset;
|
|
5006
|
-
delete last.block.limitParameter;
|
|
5007
|
-
delete last.block.offsetParameter;
|
|
5008
|
-
}
|
|
5009
|
-
else {
|
|
5010
|
-
tail = { orderBy: this.#orderByClause(), ...this.#tailClauses() };
|
|
5011
|
-
}
|
|
5012
5233
|
return compoundSelectBlock(sql, members.map((member) => member.block), ops, tail, this.nextDerivedSequence);
|
|
5013
5234
|
}
|
|
5014
5235
|
/** CREATE [UNIQUE] INDEX name ON table(column [ASC|DESC], ...). */
|
|
@@ -5153,9 +5374,9 @@ class Parser {
|
|
|
5153
5374
|
continue;
|
|
5154
5375
|
}
|
|
5155
5376
|
const name = this.#identifier();
|
|
5156
|
-
const columnType = this.#columnType();
|
|
5157
|
-
let nullable = true;
|
|
5158
|
-
let defaultValue;
|
|
5377
|
+
const { serial, ...columnType } = this.#columnType();
|
|
5378
|
+
let nullable = serial !== true;
|
|
5379
|
+
let defaultValue = serial === true ? { kind: "autoincrement" } : undefined;
|
|
5159
5380
|
let generatedValue;
|
|
5160
5381
|
for (;;) {
|
|
5161
5382
|
if (this.#isKeyword("DEFAULT")) {
|
|
@@ -5165,9 +5386,29 @@ class Parser {
|
|
|
5165
5386
|
continue;
|
|
5166
5387
|
}
|
|
5167
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
|
+
}
|
|
5168
5399
|
generatedValue = this.#generatedColumn();
|
|
5169
5400
|
continue;
|
|
5170
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
|
+
}
|
|
5171
5412
|
if (this.#isKeyword("CHECK")) {
|
|
5172
5413
|
checks.push(this.#checkConstraint(`${table}_${name}_check`));
|
|
5173
5414
|
continue;
|
|
@@ -5379,6 +5620,44 @@ class Parser {
|
|
|
5379
5620
|
return columnDefaultFor(expression, sql);
|
|
5380
5621
|
}
|
|
5381
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
|
+
}
|
|
5382
5661
|
#generatedColumn() {
|
|
5383
5662
|
this.#keyword("GENERATED");
|
|
5384
5663
|
if (this.#isKeyword("ALWAYS"))
|
|
@@ -5502,6 +5781,10 @@ class Parser {
|
|
|
5502
5781
|
break;
|
|
5503
5782
|
}
|
|
5504
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;
|
|
5505
5788
|
return {
|
|
5506
5789
|
kind: "add-column",
|
|
5507
5790
|
table,
|
|
@@ -5510,7 +5793,9 @@ class Parser {
|
|
|
5510
5793
|
...columnType,
|
|
5511
5794
|
...(nullable ? { nullable: true } : {}),
|
|
5512
5795
|
...(defaultValue === undefined ? {} : { defaultValue }),
|
|
5796
|
+
...(backfill === undefined ? {} : { backfill }),
|
|
5513
5797
|
},
|
|
5798
|
+
...(backfill === undefined ? {} : { allowNonNullableWithBackfill: true }),
|
|
5514
5799
|
};
|
|
5515
5800
|
}
|
|
5516
5801
|
/** A type name's precision, scale, or width: plain digits, as PostgreSQL requires there. */
|
|
@@ -5559,6 +5844,10 @@ class Parser {
|
|
|
5559
5844
|
#columnType() {
|
|
5560
5845
|
const declared = this.#identifier();
|
|
5561
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
|
+
}
|
|
5562
5851
|
if (word === "NUMERIC" || word === "DECIMAL") {
|
|
5563
5852
|
let precision;
|
|
5564
5853
|
let scale;
|
|
@@ -5641,13 +5930,18 @@ class Parser {
|
|
|
5641
5930
|
...this.#returningClause(table),
|
|
5642
5931
|
};
|
|
5643
5932
|
}
|
|
5644
|
-
if (this.#isKeyword("SELECT")) {
|
|
5645
|
-
|
|
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)");
|
|
5646
5937
|
resolvePlanExactNumericConstants(insertSource);
|
|
5647
5938
|
const query = planHasPendingSelectShapes(insertSource)
|
|
5648
5939
|
? insertSource
|
|
5649
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.
|
|
5650
5943
|
if (!planHasPendingSelectShapes(query) &&
|
|
5944
|
+
!query.select.some((item) => item.expression.kind === "wildcard") &&
|
|
5651
5945
|
columns.length > 0 &&
|
|
5652
5946
|
query.select.length !== columns.length) {
|
|
5653
5947
|
throw new TypeError("INSERT ... SELECT must produce exactly the insert column count");
|
|
@@ -5658,6 +5952,7 @@ class Parser {
|
|
|
5658
5952
|
columns,
|
|
5659
5953
|
rows: [],
|
|
5660
5954
|
query,
|
|
5955
|
+
...this.#onConflictClause(table),
|
|
5661
5956
|
...this.#returningClause(table),
|
|
5662
5957
|
};
|
|
5663
5958
|
}
|
|
@@ -5693,6 +5988,13 @@ class Parser {
|
|
|
5693
5988
|
return {};
|
|
5694
5989
|
this.#keyword("ON");
|
|
5695
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
|
+
}
|
|
5696
5998
|
this.#expectPunctuation("(");
|
|
5697
5999
|
const columns = [];
|
|
5698
6000
|
for (;;) {
|
|
@@ -5753,7 +6055,13 @@ class Parser {
|
|
|
5753
6055
|
};
|
|
5754
6056
|
}
|
|
5755
6057
|
/** RETURNING *, target.*, or [target.]col, ... — execution owns the row semantics. */
|
|
5756
|
-
|
|
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) {
|
|
5757
6065
|
if (!this.#isKeyword("RETURNING"))
|
|
5758
6066
|
return {};
|
|
5759
6067
|
this.#keyword("RETURNING");
|
|
@@ -5761,29 +6069,67 @@ class Parser {
|
|
|
5761
6069
|
this.#index += 1;
|
|
5762
6070
|
return { returning: "*" };
|
|
5763
6071
|
}
|
|
5764
|
-
const
|
|
6072
|
+
const items = [];
|
|
6073
|
+
let plain = true;
|
|
5765
6074
|
for (;;) {
|
|
5766
|
-
const
|
|
5767
|
-
if (
|
|
5768
|
-
|
|
5769
|
-
|
|
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}`);
|
|
5770
6082
|
}
|
|
5771
|
-
if (this.#peek().text === "
|
|
5772
|
-
|
|
5773
|
-
if (columns.length > 0 || this.#peek().text === ",") {
|
|
5774
|
-
throw new TypeError("RETURNING target.* must be the only returned item");
|
|
5775
|
-
}
|
|
5776
|
-
return { returning: "*" };
|
|
6083
|
+
if (items.length > 0 || this.#peek().text === ",") {
|
|
6084
|
+
throw new TypeError("RETURNING target.* must be the only returned item");
|
|
5777
6085
|
}
|
|
5778
|
-
|
|
6086
|
+
return { returning: "*" };
|
|
5779
6087
|
}
|
|
5780
|
-
|
|
5781
|
-
|
|
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
|
+
}
|
|
5782
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
|
+
});
|
|
5783
6116
|
if (!this.#punctuation(","))
|
|
5784
6117
|
break;
|
|
5785
6118
|
}
|
|
5786
|
-
|
|
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 };
|
|
5787
6133
|
}
|
|
5788
6134
|
/**
|
|
5789
6135
|
* MERGE INTO target USING source ON condition WHEN [NOT] MATCHED [AND …] THEN … (F312).
|
|
@@ -5915,15 +6261,19 @@ class Parser {
|
|
|
5915
6261
|
#updateStatement() {
|
|
5916
6262
|
this.#keyword("UPDATE");
|
|
5917
6263
|
const table = this.#identifier();
|
|
6264
|
+
const alias = this.#mutationAlias("SET");
|
|
5918
6265
|
this.#keyword("SET");
|
|
5919
6266
|
const assignments = [];
|
|
5920
6267
|
for (;;) {
|
|
5921
|
-
const column = this.#
|
|
6268
|
+
const column = this.#mutationTargetColumn(table, alias);
|
|
5922
6269
|
this.#operator("=");
|
|
5923
6270
|
const expression = this.#expression();
|
|
5924
6271
|
if (hasAggregate(expression)) {
|
|
5925
6272
|
throw new TypeError("Aggregate functions are not allowed in UPDATE assignments");
|
|
5926
6273
|
}
|
|
6274
|
+
if (containsWindow(expression)) {
|
|
6275
|
+
throw new TypeError("Window functions are not allowed in UPDATE assignments");
|
|
6276
|
+
}
|
|
5927
6277
|
assignments.push({ column, expression });
|
|
5928
6278
|
if (!this.#punctuation(","))
|
|
5929
6279
|
break;
|
|
@@ -5932,14 +6282,54 @@ class Parser {
|
|
|
5932
6282
|
throw new TypeError("UPDATE assignments must set each column once");
|
|
5933
6283
|
}
|
|
5934
6284
|
const predicates = this.#mutationPredicates();
|
|
5935
|
-
return {
|
|
6285
|
+
return {
|
|
6286
|
+
kind: "update",
|
|
6287
|
+
table,
|
|
6288
|
+
...(alias === undefined ? {} : { alias }),
|
|
6289
|
+
assignments,
|
|
6290
|
+
predicates,
|
|
6291
|
+
...this.#returningClause(table, alias),
|
|
6292
|
+
};
|
|
5936
6293
|
}
|
|
5937
6294
|
#deleteStatement() {
|
|
5938
6295
|
this.#keyword("DELETE");
|
|
5939
6296
|
this.#keyword("FROM");
|
|
5940
6297
|
const table = this.#identifier();
|
|
6298
|
+
const alias = this.#mutationAlias("WHERE", "RETURNING");
|
|
5941
6299
|
const predicates = this.#mutationPredicates();
|
|
5942
|
-
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();
|
|
5943
6333
|
}
|
|
5944
6334
|
#mutationPredicates() {
|
|
5945
6335
|
const predicates = [];
|
|
@@ -5970,26 +6360,30 @@ class Parser {
|
|
|
5970
6360
|
if (hasAggregate(expression) || expressionColumns(expression).length > 0) {
|
|
5971
6361
|
throw new TypeError(`${label} must be constant expressions`);
|
|
5972
6362
|
}
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
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))) ||
|
|
5978
6371
|
childExpressions(value).some(needsExecution);
|
|
5979
6372
|
if (needsExecution(expression)) {
|
|
5980
6373
|
return { expression: resolveExactNumericConstants(expression) };
|
|
5981
6374
|
}
|
|
5982
6375
|
return asQueryValue(evaluate(resolveExactNumericConstants(expression), {}));
|
|
5983
6376
|
}
|
|
5984
|
-
#unionMember(sql) {
|
|
6377
|
+
#unionMember(sql, member) {
|
|
5985
6378
|
if (this.#punctuation("(")) {
|
|
5986
6379
|
const block = this.#isKeyword("VALUES") ? this.#valuesBlock() : this.#queryExpression(sql);
|
|
5987
6380
|
this.#expectPunctuation(")");
|
|
5988
6381
|
return { block, parenthesized: true };
|
|
5989
6382
|
}
|
|
5990
6383
|
if (this.#isKeyword("VALUES")) {
|
|
5991
|
-
return { block: this.#valuesBlock(), parenthesized: false };
|
|
6384
|
+
return { block: this.#valuesBlock(), parenthesized: false, values: true };
|
|
5992
6385
|
}
|
|
6386
|
+
this.#compoundMember = member;
|
|
5993
6387
|
return { block: this.#selectBlock(sql), parenthesized: false };
|
|
5994
6388
|
}
|
|
5995
6389
|
#orderByClause() {
|
|
@@ -6166,6 +6560,10 @@ class Parser {
|
|
|
6166
6560
|
return windows;
|
|
6167
6561
|
}
|
|
6168
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;
|
|
6169
6567
|
this.#keyword("SELECT");
|
|
6170
6568
|
let distinct = false;
|
|
6171
6569
|
if (this.#isKeyword("DISTINCT")) {
|
|
@@ -6322,8 +6720,6 @@ class Parser {
|
|
|
6322
6720
|
const having = [];
|
|
6323
6721
|
if (this.#isKeyword("HAVING")) {
|
|
6324
6722
|
this.#keyword("HAVING");
|
|
6325
|
-
if (distinct)
|
|
6326
|
-
throw new TypeError("SELECT DISTINCT cannot be combined with HAVING");
|
|
6327
6723
|
having.push(...splitCondition(this.#expression()));
|
|
6328
6724
|
}
|
|
6329
6725
|
if (this.#isKeyword("WINDOW")) {
|
|
@@ -6338,7 +6734,7 @@ class Parser {
|
|
|
6338
6734
|
break;
|
|
6339
6735
|
}
|
|
6340
6736
|
}
|
|
6341
|
-
const orderBy = this.#orderByClause();
|
|
6737
|
+
const orderBy = compoundMember ? [] : this.#orderByClause();
|
|
6342
6738
|
return assembleSelectBlock({
|
|
6343
6739
|
sql,
|
|
6344
6740
|
base,
|
|
@@ -6349,7 +6745,7 @@ class Parser {
|
|
|
6349
6745
|
groupBy,
|
|
6350
6746
|
having,
|
|
6351
6747
|
orderBy,
|
|
6352
|
-
...this.#tailClauses(),
|
|
6748
|
+
...(compoundMember ? {} : this.#tailClauses()),
|
|
6353
6749
|
...(groupingSets === undefined ? {} : { groupingSets }),
|
|
6354
6750
|
}, this.nextDerivedSequence);
|
|
6355
6751
|
}
|
|
@@ -6414,10 +6810,6 @@ class Parser {
|
|
|
6414
6810
|
if (!this.#punctuation(","))
|
|
6415
6811
|
break;
|
|
6416
6812
|
}
|
|
6417
|
-
if (items.some((item) => item.expression.kind === "wildcard" && item.expression.table === undefined) &&
|
|
6418
|
-
items.length > 1) {
|
|
6419
|
-
throw new TypeError("SELECT * cannot be mixed with other expressions");
|
|
6420
|
-
}
|
|
6421
6813
|
const aliases = new Set();
|
|
6422
6814
|
for (const [index, item] of items.entries()) {
|
|
6423
6815
|
if (item.expression.kind === "list") {
|
|
@@ -7143,16 +7535,48 @@ class Parser {
|
|
|
7143
7535
|
}
|
|
7144
7536
|
// || and the JSON arrows share PostgreSQL's loosest "any other operator" level, applying
|
|
7145
7537
|
// left-to-right to whole arithmetic terms: `'a' || d ->> 'k'` is `('a' || d) ->> 'k'`.
|
|
7146
|
-
const
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
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;
|
|
7153
7550
|
if (precedence < minimumPrecedence)
|
|
7154
7551
|
break;
|
|
7155
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
|
+
}
|
|
7156
7580
|
if (operator === "->" || operator === "->>") {
|
|
7157
7581
|
const key = this.#additive(precedence + 1);
|
|
7158
7582
|
// A key fixed at compile time fails here rather than per row, like SQL/JSON paths.
|
|
@@ -7665,7 +8089,8 @@ class Parser {
|
|
|
7665
8089
|
name === "MINNOW_COLLATE" ||
|
|
7666
8090
|
name === "MINNOW_SINGLE_VALUE" ||
|
|
7667
8091
|
name === "MINNOW_JSON_GET" ||
|
|
7668
|
-
name === "MINNOW_JSON_GET_TEXT"
|
|
8092
|
+
name === "MINNOW_JSON_GET_TEXT" ||
|
|
8093
|
+
name === "MINNOW_REGEX_MATCH") {
|
|
7669
8094
|
throw new TypeError(`Unsupported function: ${identifier}`);
|
|
7670
8095
|
}
|
|
7671
8096
|
if (!aggregateNames.has(name) && !scalarFunctionNames.has(name))
|
|
@@ -7718,6 +8143,25 @@ class Parser {
|
|
|
7718
8143
|
if (name === "JSON_ARRAYAGG" && args[0]?.kind === "wildcard") {
|
|
7719
8144
|
throw new TypeError("JSON_ARRAYAGG requires a scalar value expression");
|
|
7720
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
|
+
}
|
|
7721
8165
|
if (name === "ROUND" && (args.length < 1 || args.length > 2))
|
|
7722
8166
|
throw new TypeError("ROUND requires one or two arguments");
|
|
7723
8167
|
if (name === "COALESCE" && args.length < 1)
|
|
@@ -8211,23 +8655,33 @@ function desugarGroupingSets(parts, nextSequence) {
|
|
|
8211
8655
|
if (hasAggregate(item.expression))
|
|
8212
8656
|
return item;
|
|
8213
8657
|
const signature = signatureOf(item.expression);
|
|
8214
|
-
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)
|
|
8215
8681
|
return item;
|
|
8216
|
-
// MIN over an always-NULL argument: legal in a grouped select, NULL in every group, and
|
|
8217
|
-
// typed like the original expression through MIN's carry and NULLIF's first argument.
|
|
8218
8682
|
return {
|
|
8219
8683
|
alias: item.alias,
|
|
8220
|
-
expression: {
|
|
8221
|
-
kind: "call",
|
|
8222
|
-
name: "MIN",
|
|
8223
|
-
arguments: [
|
|
8224
|
-
{
|
|
8225
|
-
kind: "call",
|
|
8226
|
-
name: "NULLIF",
|
|
8227
|
-
arguments: [structuredClone(item.expression), structuredClone(item.expression)],
|
|
8228
|
-
},
|
|
8229
|
-
],
|
|
8230
|
-
},
|
|
8684
|
+
expression: { kind: "call", name: "MIN", arguments: [nullified] },
|
|
8231
8685
|
};
|
|
8232
8686
|
});
|
|
8233
8687
|
return assembleSelectBlock({
|
|
@@ -8267,6 +8721,80 @@ function resolveOrderByOrdinals(orderBy, select) {
|
|
|
8267
8721
|
return { ...order, expression: { kind: "column", reference: item.alias } };
|
|
8268
8722
|
});
|
|
8269
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
|
+
}
|
|
8270
8798
|
/**
|
|
8271
8799
|
* FULL OUTER JOIN desugars into UNION ALL of two left joins: the plain left join carries every
|
|
8272
8800
|
* base row (matched or not), and the swapped left join filtered to a NULL-extended base side
|
|
@@ -8381,7 +8909,16 @@ export function assembleSelectBlock(parts, nextSequence) {
|
|
|
8381
8909
|
...(parts.distinct ? { distinctWildcard: true } : {}),
|
|
8382
8910
|
};
|
|
8383
8911
|
}
|
|
8384
|
-
parts = {
|
|
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
|
+
};
|
|
8385
8922
|
if (parts.joins.some((join) => join.full === true)) {
|
|
8386
8923
|
return desugarFullJoin(parts, nextSequence);
|
|
8387
8924
|
}
|
|
@@ -8399,15 +8936,41 @@ export function assembleSelectBlock(parts, nextSequence) {
|
|
|
8399
8936
|
const { limitParameter, offsetParameter } = parts;
|
|
8400
8937
|
const groupBy = [...parts.groupBy];
|
|
8401
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
|
+
}
|
|
8402
8973
|
if (distinct) {
|
|
8403
|
-
if (select.some((item) => hasAggregate(item.expression)))
|
|
8404
|
-
throw new TypeError("SELECT DISTINCT cannot be combined with aggregate functions");
|
|
8405
|
-
if (select.some((item) => hasWindow(item.expression)))
|
|
8406
|
-
throw new TypeError("SELECT DISTINCT cannot be combined with window functions");
|
|
8407
|
-
if (groupBy.length > 0)
|
|
8408
|
-
throw new TypeError("SELECT DISTINCT cannot be combined with GROUP BY");
|
|
8409
|
-
if (having.length > 0)
|
|
8410
|
-
throw new TypeError("SELECT DISTINCT cannot be combined with HAVING");
|
|
8411
8974
|
if (select.some((item) => item.expression.kind === "wildcard")) {
|
|
8412
8975
|
// The wildcard's columns are unknown until input schemas exist; executor entries
|
|
8413
8976
|
// expand the flag into a concrete select list plus GROUP BY (expandDistinctWildcard).
|
|
@@ -8490,15 +9053,19 @@ function expandDistinctWildcard(plan, columnsOf) {
|
|
|
8490
9053
|
columns: sourceWildcardColumns(source, columnsOf),
|
|
8491
9054
|
}));
|
|
8492
9055
|
const visible = shaped.filter(({ columns }) => (columns?.length ?? 0) > 0);
|
|
8493
|
-
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
|
+
}
|
|
8494
9061
|
const select = visible.flatMap(({ source, columns }) => {
|
|
8495
9062
|
if (columns === undefined) {
|
|
8496
9063
|
throw new TypeError(`SELECT DISTINCT * requires known columns for: ${source.table}`);
|
|
8497
9064
|
}
|
|
8498
|
-
return columns.map((name) => {
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
});
|
|
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
|
+
}));
|
|
8502
9069
|
});
|
|
8503
9070
|
const { distinctWildcard, ...rest } = plan;
|
|
8504
9071
|
void distinctWildcard;
|
|
@@ -8739,7 +9306,21 @@ export function bindPendingSelectShapes(plan, columnsOf) {
|
|
|
8739
9306
|
if (unknown !== undefined) {
|
|
8740
9307
|
throw new TypeError(`SELECT * requires known columns for: ${unknown.source.table}`);
|
|
8741
9308
|
}
|
|
8742
|
-
|
|
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
|
+
}
|
|
8743
9324
|
const items = block.select.flatMap((item) => {
|
|
8744
9325
|
if (item.expression.kind !== "wildcard")
|
|
8745
9326
|
return [item];
|
|
@@ -8750,7 +9331,7 @@ export function bindPendingSelectShapes(plan, columnsOf) {
|
|
|
8750
9331
|
}
|
|
8751
9332
|
return selected.flatMap(({ source, columns }) => (columns ?? []).map((name) => ({
|
|
8752
9333
|
expression: { kind: "column", reference: `${source.alias}.${name}` },
|
|
8753
|
-
alias:
|
|
9334
|
+
alias: (contributed.get(name) ?? 0) > 1 ? `${source.alias}.${name}` : name,
|
|
8754
9335
|
})));
|
|
8755
9336
|
});
|
|
8756
9337
|
const aliases = new Set();
|
|
@@ -9194,8 +9775,10 @@ export function derivedTableSource(derived, alias, nextSequence) {
|
|
|
9194
9775
|
}
|
|
9195
9776
|
/** The parser's LIMIT range contract, shared with the typed builder. */
|
|
9196
9777
|
export function validateLimit(limit) {
|
|
9197
|
-
|
|
9198
|
-
|
|
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");
|
|
9199
9782
|
return limit;
|
|
9200
9783
|
}
|
|
9201
9784
|
/**
|
|
@@ -9266,7 +9849,7 @@ function jsonTableColumnValue(column, value) {
|
|
|
9266
9849
|
if (value instanceof Date)
|
|
9267
9850
|
date = value;
|
|
9268
9851
|
else if (typeof value === "string")
|
|
9269
|
-
date =
|
|
9852
|
+
date = datetimeText(value);
|
|
9270
9853
|
else if (typeof value === "number")
|
|
9271
9854
|
date = new Date(value);
|
|
9272
9855
|
else
|
|
@@ -9277,18 +9860,20 @@ function jsonTableColumnValue(column, value) {
|
|
|
9277
9860
|
return date;
|
|
9278
9861
|
}
|
|
9279
9862
|
function timestampLiteral(text) {
|
|
9280
|
-
const
|
|
9281
|
-
|
|
9282
|
-
if (match === null)
|
|
9863
|
+
const date = parseSqlTimestampText(text);
|
|
9864
|
+
if (date === undefined)
|
|
9283
9865
|
throw new TypeError(`Invalid TIMESTAMP literal: ${text}`);
|
|
9284
|
-
const [, day, time = "00:00:00", zone = "Z"] = match;
|
|
9285
|
-
const seconds = time.length === 5 ? `${time}:00` : time;
|
|
9286
|
-
const date = new Date(`${String(day)}T${seconds}${zone === "Z" ? "Z" : zone}`);
|
|
9287
|
-
if (!Number.isFinite(dateMilliseconds(date))) {
|
|
9288
|
-
throw new TypeError(`Invalid TIMESTAMP literal: ${text}`);
|
|
9289
|
-
}
|
|
9290
9866
|
return date;
|
|
9291
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
|
+
}
|
|
9292
9877
|
/** The parser's OFFSET range contract, shared with the typed builder. */
|
|
9293
9878
|
export function validateOffset(offset) {
|
|
9294
9879
|
if (!Number.isSafeInteger(offset) || offset < 0 || offset > 100_000_000)
|
|
@@ -9662,6 +10247,29 @@ function tokenize(sql) {
|
|
|
9662
10247
|
index += 2;
|
|
9663
10248
|
continue;
|
|
9664
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
|
+
}
|
|
9665
10273
|
if (["+", "-", "*", "/", "%", "=", ">", "<"].includes(character))
|
|
9666
10274
|
push({ kind: "operator", text: character, start: index, end: index + 1 });
|
|
9667
10275
|
else if (["(", ")", "[", "]", ",", "."].includes(character))
|