@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.
@@ -1,4 +1,4 @@
1
- import { copyDate, dateIsoString, dateMilliseconds, dateUtcDate, dateUtcDay, dateUtcFullYear, dateUtcHours, dateUtcMinutes, dateUtcMonth, dateUtcSeconds, setDateUtcDate, setDateUtcMonth, } from "../date-value.js";
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
- const integer = Math.trunc(parsed);
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 input = value instanceof Date
702
- ? value
824
+ const milliseconds = value instanceof Date
825
+ ? dateMilliseconds(value)
703
826
  : isDateDomainValue(value) && typeof external === "string"
704
- ? new Date(`${external}T00:00:00.000Z`)
827
+ ? Date.parse(`${external}T00:00:00.000Z`)
705
828
  : undefined;
706
- if (input === undefined)
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 year = dateUtcFullYear(input);
710
- const month = dateUtcMonth(input);
711
- const day = dateUtcDate(input);
712
- switch (normalized) {
713
- case "year":
714
- return new Date(Date.UTC(year, 0, 1));
715
- case "quarter":
716
- return new Date(Date.UTC(year, Math.floor(month / 3) * 3, 1));
717
- case "month":
718
- return new Date(Date.UTC(year, month, 1));
719
- case "week": {
720
- const start = new Date(Date.UTC(year, month, day));
721
- setDateUtcDate(start, dateUtcDate(start) - ((dateUtcDay(start) + 6) % 7));
722
- return start;
723
- }
724
- case "day":
725
- return new Date(Date.UTC(year, month, day));
726
- case "hour":
727
- return new Date(Date.UTC(year, month, day, dateUtcHours(input)));
728
- case "minute":
729
- return new Date(Date.UTC(year, month, day, dateUtcHours(input), dateUtcMinutes(input)));
730
- default:
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
- compiled = options.optimize === false ? plan : optimizePlan(plan);
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
- if (parser.usesStatementDatetime)
903
- compiled.usesStatementDatetime = true;
904
- if (parser.usesSequenceCalls)
905
- compiled.usesSequenceCalls = true;
906
- if (parser.usesVolatileFunctions)
907
- compiled.usesVolatileFunctions = true;
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
- for (const side of [expression.left, expression.right]) {
2020
- const type = infer(side);
2021
- if (type !== "string" && type !== "null") {
2022
- throw new TypeError("|| requires string operands");
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
- if (comparable(value) === comparable(item))
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
- if (!(value instanceof Date))
4291
- throw new TypeError("EXTRACT requires a datetime value");
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 dateUtcHours(value);
4479
+ return Math.floor(timeOfDay / 3_600_000);
4310
4480
  case "minute":
4311
- return dateUtcMinutes(value);
4481
+ return Math.floor(timeOfDay / 60_000) % 60;
4312
4482
  case "second":
4313
- return dateUtcSeconds(value);
4314
- case "epoch":
4315
- return dateMilliseconds(value) / 1000;
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
- return dateUtcDay(value);
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 (!groupExpressions.has(JSON.stringify(item.expression))) {
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
- #setTerm(sql) {
4949
- const first = this.#unionMember(sql);
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
- return { block: this.#compoundBlock(sql, members, ops), parenthesized: false };
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
- for (const [index, member] of members.entries()) {
4968
- const last = index === members.length - 1;
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
- const insertSource = this.#selectBlock("(insert select)");
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 = optimizePlan(insertSource);
5638
- if (query.select.some((item) => item.expression.kind === "wildcard")) {
5639
- throw new TypeError("INSERT ... SELECT requires an explicit select list");
5640
- }
5641
- if (columns.length > 0 && query.select.length !== columns.length) {
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
- #returningClause(table) {
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 columns = [];
6072
+ const items = [];
6073
+ let plain = true;
5754
6074
  for (;;) {
5755
- const first = this.#identifier();
5756
- if (this.#punctuation(".")) {
5757
- if (first !== table) {
5758
- throw new TypeError(`RETURNING qualifier must name the target table: ${table}`);
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
- this.#index += 1;
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
- columns.push(this.#identifier());
6086
+ return { returning: "*" };
5768
6087
  }
5769
- else {
5770
- columns.push(first);
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
- return { returning: columns };
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.#identifier();
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 { kind: "update", table, assignments, predicates, ...this.#returningClause(table) };
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 { kind: "delete", table, predicates, ...this.#returningClause(table) };
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
- // Set-operation output takes the first member's aliases, so renaming targets it.
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 precedence = operator === "*" || operator === "/" || operator === "%"
7138
- ? 20
7139
- : operator === "+" || operator === "-"
7140
- ? 10
7141
- : operator === "||" || operator === "->" || operator === "->>"
7142
- ? 5
7143
- : -1;
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) || !universe.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
- parts = { ...parts, orderBy: resolveOrderByOrdinals(parts.orderBy, parts.select) };
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 multiple = visible.length > 1;
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
- const output = multiple ? `${source.alias}.${name}` : name;
8460
- return { expression: { kind: "column", reference: output }, alias: output };
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
- return { expression: { kind: "column", reference: output }, alias: output };
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
- tail = { ...tail, orderBy: resolveOrderByOrdinals(tail.orderBy, blocks[0]?.select ?? []) };
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. A wildcard select has no named
8796
- * output list to hide items behind, and DISTINCT's output would change if hidden expressions
8797
- * joined its grouping, so both keep the named-column restriction.
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 inner;
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: { kind: "column", reference: item.alias },
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
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100_000)
8937
- throw new RangeError("LIMIT must be between 1 and 100,000");
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 CTE body: ${name}`);
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 = new Date(value);
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 trimmed = text.trim();
9002
- const match = /^(\d{4}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?))?(Z|[+-]\d{2}:?\d{2})?$/.exec(trimmed);
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 innerSelect = select.filter((item) => !containsWindow(item.expression));
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 { expression: { kind: "column", reference: item.alias }, alias: item.alias };
9972
+ return {
9973
+ expression: { kind: "column", reference: internalAlias },
9974
+ alias: item.alias,
9975
+ };
9105
9976
  }
9106
- // A window that is the whole select item keeps carrying that item's own name, which is what
9107
- // the executor's windowed source and every existing plan already expect.
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, item.alias);
9110
- return { expression: { kind: "column", reference: item.alias }, alias: item.alias };
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
- ...tail,
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))