@minnowdb/core 0.6.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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) {
@@ -903,12 +1028,18 @@ export function compileQuery(sql, options = {}) {
903
1028
  }
904
1029
  if (parser.parameterCount > 0)
905
1030
  compiled.parameterCount = parser.parameterCount;
906
- if (parser.usesStatementDatetime)
907
- compiled.usesStatementDatetime = true;
908
- if (parser.usesSequenceCalls)
909
- compiled.usesSequenceCalls = true;
910
- if (parser.usesVolatileFunctions)
911
- 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
+ }
912
1043
  return compiled;
913
1044
  }
914
1045
  /**
@@ -1921,6 +2052,11 @@ export function inferBlockSchema(plan, schemas) {
1921
2052
  return undefined;
1922
2053
  if (expression.name === "CURRENT_DATE")
1923
2054
  return { kind: "date" };
2055
+ const simple = simpleScalarFunctions.get(expression.name);
2056
+ if (simple?.returns === "date")
2057
+ return { kind: "date" };
2058
+ if (simple?.returns === "interval")
2059
+ return { kind: "interval" };
1924
2060
  if (expression.name === "CAST") {
1925
2061
  const target = expression.arguments[1];
1926
2062
  return target?.kind === "literal" && typeof target.value === "string"
@@ -2023,11 +2159,10 @@ export function inferBlockSchema(plan, schemas) {
2023
2159
  }
2024
2160
  if (expression.kind === "binary") {
2025
2161
  if (expression.operator === "||") {
2026
- for (const side of [expression.left, expression.right]) {
2027
- const type = infer(side);
2028
- if (type !== "string" && type !== "null") {
2029
- throw new TypeError("|| requires string operands");
2030
- }
2162
+ // PostgreSQL's text || anynonarray: one side must be text, the other renders as text.
2163
+ const types = [expression.left, expression.right].map(infer);
2164
+ if (!types.some((type) => type === "string" || type === "null")) {
2165
+ throw new TypeError("|| requires a string operand");
2031
2166
  }
2032
2167
  return "string";
2033
2168
  }
@@ -2079,6 +2214,15 @@ export function inferBlockSchema(plan, schemas) {
2079
2214
  }
2080
2215
  if (expression.name === "JSON_EXISTS" || expression.name === "IS_JSON")
2081
2216
  return "boolean";
2217
+ const simple = simpleScalarFunctions.get(expression.name);
2218
+ if (simple !== undefined) {
2219
+ if (simple.returns === "argument") {
2220
+ const argument = expression.arguments[0];
2221
+ return argument === undefined ? "null" : infer(argument);
2222
+ }
2223
+ // DATE and INTERVAL results are logical domains carried as strings.
2224
+ return simple.returns === "date" || simple.returns === "interval" ? "string" : simple.returns;
2225
+ }
2082
2226
  if (expression.name === "DATE_TRUNC")
2083
2227
  return "datetime";
2084
2228
  if (expression.name === "DATE_ADD") {
@@ -4014,12 +4158,8 @@ function evaluate(expression, context, group) {
4014
4158
  const right = evaluate(expression.right, context, group);
4015
4159
  if (left === null || left === undefined || right === null || right === undefined)
4016
4160
  return null;
4017
- if (expression.operator === "||") {
4018
- if (typeof left !== "string" || typeof right !== "string") {
4019
- throw new TypeError("|| requires string operands");
4020
- }
4161
+ if (expression.operator === "||")
4021
4162
  return concatenatedSqlValue(left, right);
4022
- }
4023
4163
  const exact = exactNumericBinary(expression.operator, left, right);
4024
4164
  if (exact !== undefined)
4025
4165
  return exact;
@@ -4212,6 +4352,11 @@ function evaluatePredicate(predicate, context) {
4212
4352
  return false;
4213
4353
  if (membership.set.has(comparable(value)))
4214
4354
  return predicate.operator === "IN";
4355
+ // A typed value beside text members reads them in its own type (`id IN ('1', '2')`),
4356
+ // which the hashed set cannot see; the per-item path coerces as comparisons do.
4357
+ if (typeof value !== "string" && membership.hasText) {
4358
+ return inListHolds(predicate.operator, value, predicate.right.items.map((item) => evaluate(item, context)));
4359
+ }
4215
4360
  return predicate.operator === "NOT IN" && !membership.hasNull;
4216
4361
  }
4217
4362
  return inListHolds(predicate.operator, evaluate(predicate.left, context), predicate.right.items.map((item) => evaluate(item, context)));
@@ -4240,7 +4385,8 @@ function inListHolds(operator, value, items) {
4240
4385
  hasNull = true;
4241
4386
  continue;
4242
4387
  }
4243
- if (comparable(value) === comparable(item))
4388
+ const member = typeof item === "string" && typeof value !== "string" ? coercedComparable(item, value) : item;
4389
+ if (comparable(value) === comparable(member))
4244
4390
  return operator === "IN";
4245
4391
  }
4246
4392
  return operator === "NOT IN" && !hasNull;
@@ -4259,13 +4405,17 @@ export function cachedListMembership(node, items) {
4259
4405
  if (items.every((item) => item.kind === "literal")) {
4260
4406
  const set = new Set();
4261
4407
  let hasNull = false;
4408
+ let hasText = false;
4262
4409
  for (const item of items) {
4263
4410
  if (item.value === null || item.value === undefined)
4264
4411
  hasNull = true;
4265
- else
4412
+ else {
4413
+ if (typeof item.value === "string")
4414
+ hasText = true;
4266
4415
  set.add(comparable(item.value));
4416
+ }
4267
4417
  }
4268
- cached = { set, hasNull };
4418
+ cached = { set, hasNull, hasText };
4269
4419
  }
4270
4420
  else {
4271
4421
  cached = null;
@@ -4285,6 +4435,14 @@ const extractFields = new Set([
4285
4435
  "second",
4286
4436
  "epoch",
4287
4437
  "dow",
4438
+ "doy",
4439
+ "isodow",
4440
+ "isoyear",
4441
+ "decade",
4442
+ "century",
4443
+ "millennium",
4444
+ "milliseconds",
4445
+ "microseconds",
4288
4446
  ]);
4289
4447
  /**
4290
4448
  * EXTRACT(field FROM datetime), in UTC. `week` is the ISO 8601 week number and `dow` counts
@@ -4297,34 +4455,73 @@ function extractDatePart(field, value) {
4297
4455
  }
4298
4456
  if (value === null || value === undefined)
4299
4457
  return null;
4300
- if (!(value instanceof Date))
4301
- 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;
4302
4477
  switch (normalized) {
4303
- case "year":
4304
- return dateUtcFullYear(value);
4305
- case "quarter":
4306
- return Math.floor(dateUtcMonth(value) / 3) + 1;
4307
- case "month":
4308
- return dateUtcMonth(value) + 1;
4309
- case "week": {
4310
- const date = new Date(Date.UTC(dateUtcFullYear(value), dateUtcMonth(value), dateUtcDate(value)));
4311
- // ISO week: shift to the Thursday of this week, then count weeks from January 1st.
4312
- setDateUtcDate(date, dateUtcDate(date) + 4 - (dateUtcDay(date) || 7));
4313
- const yearStart = Date.UTC(dateUtcFullYear(date), 0, 1);
4314
- return Math.ceil(((dateMilliseconds(date) - yearStart) / 86_400_000 + 1) / 7);
4315
- }
4316
- case "day":
4317
- return dateUtcDate(value);
4318
4478
  case "hour":
4319
- return dateUtcHours(value);
4479
+ return Math.floor(timeOfDay / 3_600_000);
4320
4480
  case "minute":
4321
- return dateUtcMinutes(value);
4481
+ return Math.floor(timeOfDay / 60_000) % 60;
4322
4482
  case "second":
4323
- return dateUtcSeconds(value);
4324
- case "epoch":
4325
- 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
+ }
4326
4494
  default:
4327
- 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
+ }
4328
4525
  }
4329
4526
  }
4330
4527
  /**
@@ -4540,6 +4737,9 @@ export function comparisonHolds(operator, leftValue, rightValue) {
4540
4737
  rightValue === null ||
4541
4738
  rightValue === undefined)
4542
4739
  return false;
4740
+ // An untyped string beside a datetime, number, or boolean reads in that value's type, as
4741
+ // PostgreSQL types an unknown-typed literal by its context (`joined >= '2026-01-01'`).
4742
+ [leftValue, rightValue] = coerceComparisonOperands(leftValue, rightValue);
4543
4743
  const left = comparable(leftValue);
4544
4744
  const right = comparable(rightValue);
4545
4745
  if (operator === "=")
@@ -4625,6 +4825,23 @@ function hasWindow(expression) {
4625
4825
  export function isAggregateCall(expression) {
4626
4826
  return expression.kind === "call" && aggregateNames.has(expression.name);
4627
4827
  }
4828
+ /**
4829
+ * Whether a non-aggregate select expression is a function of the grouping expressions: it is one
4830
+ * of them, or every column it reads sits inside a subexpression that is one of them. PostgreSQL
4831
+ * accepts `FLOOR(amount / 25) * 25` grouped by `FLOOR(amount / 25)` on exactly this rule.
4832
+ */
4833
+ function groupedExpression(expression, grouped) {
4834
+ if (grouped.has(JSON.stringify(expression)))
4835
+ return true;
4836
+ // A full-text node reads its row's searchable columns even when MATCH(*) lists none, so it
4837
+ // is only grouped when the GROUP BY names it outright.
4838
+ if (expression.kind === "column" || containsFtsExpression(expression))
4839
+ return false;
4840
+ const children = childExpressions(expression);
4841
+ if (children.length === 0)
4842
+ return expressionColumns(expression).length === 0;
4843
+ return children.every((child) => expressionColumns(child).length === 0 || groupedExpression(child, grouped));
4844
+ }
4628
4845
  function validateGrouping(plan) {
4629
4846
  const grouped = plan.groupBy.length > 0 || plan.select.some((item) => hasAggregate(item.expression));
4630
4847
  if (!grouped)
@@ -4640,7 +4857,7 @@ function validateGrouping(plan) {
4640
4857
  !containsFtsExpression(item.expression)) {
4641
4858
  continue;
4642
4859
  }
4643
- if (!groupExpressions.has(JSON.stringify(item.expression))) {
4860
+ if (!groupedExpression(item.expression, groupExpressions)) {
4644
4861
  throw new TypeError(`Selected column must appear in GROUP BY: ${item.alias}`);
4645
4862
  }
4646
4863
  }
@@ -4807,6 +5024,12 @@ class Parser {
4807
5024
  /** Placeholders seen so far: positional `?` count, and the highest `$n` number. */
4808
5025
  #positionalParameters = 0;
4809
5026
  #highestNumberedParameter = 0;
5027
+ /**
5028
+ * Set while a set-operation member's SELECT parses. The member leaves a trailing ORDER BY,
5029
+ * LIMIT, OFFSET, or FETCH unparsed: PostgreSQL applies that tail to the whole compound, and a
5030
+ * member that swallowed it would resolve the ordering against its own select list instead.
5031
+ */
5032
+ #compoundMember = false;
4810
5033
  /** Set when the statement names CURRENT_DATE, CURRENT_TIMESTAMP, or LOCALTIME. */
4811
5034
  usesStatementDatetime = false;
4812
5035
  /** Set when the statement names NEXTVAL or CURRVAL. */
@@ -4925,7 +5148,7 @@ class Parser {
4925
5148
  }
4926
5149
  }
4927
5150
  // INTERSECT binds tighter than UNION and EXCEPT, matching PostgreSQL.
4928
- const firstTerm = this.#setTerm(sql);
5151
+ const firstTerm = this.#setTerm(sql, false);
4929
5152
  let plan = firstTerm.block;
4930
5153
  if (this.#isKeyword("UNION") || this.#isKeyword("EXCEPT")) {
4931
5154
  const members = [firstTerm];
@@ -4949,14 +5172,38 @@ class Parser {
4949
5172
  else
4950
5173
  ops.push("except");
4951
5174
  }
4952
- members.push(this.#setTerm("(union member)"));
5175
+ members.push(this.#setTerm("(union member)", true));
5176
+ }
5177
+ // PostgreSQL assigns a trailing ORDER BY or LIMIT to the whole compound, so every member
5178
+ // after the first parsed without one and the tail is still unparsed here.
5179
+ plan = this.#compoundBlock(sql, members, ops, this.#compoundTail());
5180
+ }
5181
+ else if (firstTerm.values === true && !firstTerm.parenthesized) {
5182
+ // A bare VALUES list desugars into a UNION ALL of one-row selects; a trailing ORDER BY
5183
+ // or LIMIT applies to that compound, with ordinals resolving against the row shape.
5184
+ const tail = this.#compoundTail();
5185
+ if (tail.orderBy.length > 0 || tail.limit !== undefined || tail.offset !== undefined) {
5186
+ const union = plan.base.union;
5187
+ plan =
5188
+ union === undefined
5189
+ ? compoundSelectBlock(sql, [plan], [], tail, this.nextDerivedSequence)
5190
+ : compoundSelectBlock(sql, union.blocks, union.ops, tail, this.nextDerivedSequence);
4953
5191
  }
4954
- plan = this.#compoundBlock(sql, members, ops);
4955
5192
  }
4956
5193
  return plan;
4957
5194
  }
4958
- #setTerm(sql) {
4959
- 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);
4960
5207
  if (!this.#isKeyword("INTERSECT"))
4961
5208
  return first;
4962
5209
  const members = [first];
@@ -4969,46 +5216,20 @@ class Parser {
4969
5216
  }
4970
5217
  else
4971
5218
  ops.push("intersect");
4972
- members.push(this.#unionMember("(intersect member)"));
5219
+ members.push(this.#unionMember("(intersect member)", true));
4973
5220
  }
4974
- 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 };
4975
5223
  }
4976
- #compoundBlock(sql, members, ops) {
4977
- for (const [index, member] of members.entries()) {
4978
- 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) {
4979
5228
  if (!member.parenthesized &&
4980
- !last &&
4981
5229
  (member.block.orderBy.length > 0 || member.block.limit !== undefined)) {
4982
5230
  throw new TypeError("ORDER BY or LIMIT in a UNION member requires parentheses");
4983
5231
  }
4984
5232
  }
4985
- // PostgreSQL assigns a trailing ORDER BY or LIMIT to the whole compound. After an
4986
- // unparenthesized last member the clause was greedily parsed into that member and lifts
4987
- // out; after a parenthesized member it is still unparsed.
4988
- const last = members[members.length - 1];
4989
- let tail;
4990
- if (last !== undefined && !last.parenthesized) {
4991
- tail = {
4992
- orderBy: last.block.orderBy,
4993
- ...(last.block.limit === undefined ? {} : { limit: last.block.limit }),
4994
- ...(last.block.offset === undefined ? {} : { offset: last.block.offset }),
4995
- ...(last.block.limitWithTies === true ? { limitWithTies: true } : {}),
4996
- ...(last.block.limitParameter === undefined
4997
- ? {}
4998
- : { limitParameter: last.block.limitParameter }),
4999
- ...(last.block.offsetParameter === undefined
5000
- ? {}
5001
- : { offsetParameter: last.block.offsetParameter }),
5002
- };
5003
- last.block.orderBy = [];
5004
- delete last.block.limit;
5005
- delete last.block.offset;
5006
- delete last.block.limitParameter;
5007
- delete last.block.offsetParameter;
5008
- }
5009
- else {
5010
- tail = { orderBy: this.#orderByClause(), ...this.#tailClauses() };
5011
- }
5012
5233
  return compoundSelectBlock(sql, members.map((member) => member.block), ops, tail, this.nextDerivedSequence);
5013
5234
  }
5014
5235
  /** CREATE [UNIQUE] INDEX name ON table(column [ASC|DESC], ...). */
@@ -5153,9 +5374,9 @@ class Parser {
5153
5374
  continue;
5154
5375
  }
5155
5376
  const name = this.#identifier();
5156
- const columnType = this.#columnType();
5157
- let nullable = true;
5158
- let defaultValue;
5377
+ const { serial, ...columnType } = this.#columnType();
5378
+ let nullable = serial !== true;
5379
+ let defaultValue = serial === true ? { kind: "autoincrement" } : undefined;
5159
5380
  let generatedValue;
5160
5381
  for (;;) {
5161
5382
  if (this.#isKeyword("DEFAULT")) {
@@ -5165,9 +5386,29 @@ class Parser {
5165
5386
  continue;
5166
5387
  }
5167
5388
  if (this.#isKeyword("GENERATED")) {
5389
+ if (this.#identityClause()) {
5390
+ // GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY: PostgreSQL's identity column, the
5391
+ // same auto-increment default a SERIAL column carries.
5392
+ if (columnType.type !== "number" || columnType.integer !== true) {
5393
+ throw new TypeError(`Identity column ${name} must be an integer type`);
5394
+ }
5395
+ defaultValue = { kind: "autoincrement" };
5396
+ nullable = false;
5397
+ continue;
5398
+ }
5168
5399
  generatedValue = this.#generatedColumn();
5169
5400
  continue;
5170
5401
  }
5402
+ if (this.#isKeyword("AUTOINCREMENT")) {
5403
+ // SQLite's spelling, after PRIMARY KEY.
5404
+ this.#keyword("AUTOINCREMENT");
5405
+ if (columnType.type !== "number" || columnType.integer !== true) {
5406
+ throw new TypeError(`AUTOINCREMENT column ${name} must be an integer type`);
5407
+ }
5408
+ defaultValue = { kind: "autoincrement" };
5409
+ nullable = false;
5410
+ continue;
5411
+ }
5171
5412
  if (this.#isKeyword("CHECK")) {
5172
5413
  checks.push(this.#checkConstraint(`${table}_${name}_check`));
5173
5414
  continue;
@@ -5379,6 +5620,44 @@ class Parser {
5379
5620
  return columnDefaultFor(expression, sql);
5380
5621
  }
5381
5622
  /** GENERATED [ALWAYS] AS (expression) STORED. */
5623
+ /**
5624
+ * Consumes `GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY` and answers true; leaves the
5625
+ * tokens alone and answers false for the stored generated-column form.
5626
+ */
5627
+ #identityClause() {
5628
+ const start = this.#index;
5629
+ this.#keyword("GENERATED");
5630
+ if (this.#isKeyword("BY")) {
5631
+ this.#keyword("BY");
5632
+ this.#keyword("DEFAULT");
5633
+ }
5634
+ else if (this.#isKeyword("ALWAYS")) {
5635
+ this.#keyword("ALWAYS");
5636
+ }
5637
+ if (this.#isKeyword("AS")) {
5638
+ this.#keyword("AS");
5639
+ if (this.#isKeyword("IDENTITY")) {
5640
+ this.#keyword("IDENTITY");
5641
+ // Sequence options in parentheses are accepted and ignored; the counter starts at 1.
5642
+ if (this.#punctuation("(")) {
5643
+ let depth = 1;
5644
+ while (depth > 0) {
5645
+ const token = this.#peek();
5646
+ if (token.kind === "eof")
5647
+ throw new TypeError("Unterminated identity options");
5648
+ this.#index += 1;
5649
+ if (token.kind === "punctuation" && token.text === "(")
5650
+ depth += 1;
5651
+ if (token.kind === "punctuation" && token.text === ")")
5652
+ depth -= 1;
5653
+ }
5654
+ }
5655
+ return true;
5656
+ }
5657
+ }
5658
+ this.#index = start;
5659
+ return false;
5660
+ }
5382
5661
  #generatedColumn() {
5383
5662
  this.#keyword("GENERATED");
5384
5663
  if (this.#isKeyword("ALWAYS"))
@@ -5502,6 +5781,10 @@ class Parser {
5502
5781
  break;
5503
5782
  }
5504
5783
  this.#take("eof");
5784
+ // PostgreSQL fills existing rows with a constant DEFAULT when the column is added, which is
5785
+ // also what lets the column be NOT NULL. An expression default (NOW(), nextval) is evaluated
5786
+ // per write, so it fills only the rows written afterwards.
5787
+ const backfill = defaultValue?.kind === "literal" ? defaultValue.value : undefined;
5505
5788
  return {
5506
5789
  kind: "add-column",
5507
5790
  table,
@@ -5510,7 +5793,9 @@ class Parser {
5510
5793
  ...columnType,
5511
5794
  ...(nullable ? { nullable: true } : {}),
5512
5795
  ...(defaultValue === undefined ? {} : { defaultValue }),
5796
+ ...(backfill === undefined ? {} : { backfill }),
5513
5797
  },
5798
+ ...(backfill === undefined ? {} : { allowNonNullableWithBackfill: true }),
5514
5799
  };
5515
5800
  }
5516
5801
  /** A type name's precision, scale, or width: plain digits, as PostgreSQL requires there. */
@@ -5559,6 +5844,10 @@ class Parser {
5559
5844
  #columnType() {
5560
5845
  const declared = this.#identifier();
5561
5846
  const word = declared.toUpperCase();
5847
+ if (word === "SERIAL" || word === "BIGSERIAL" || word === "SMALLSERIAL") {
5848
+ // PostgreSQL's serial pseudo-types: an integer column fed by an auto-increment default.
5849
+ return { type: "number", integer: true, serial: true };
5850
+ }
5562
5851
  if (word === "NUMERIC" || word === "DECIMAL") {
5563
5852
  let precision;
5564
5853
  let scale;
@@ -5641,13 +5930,18 @@ class Parser {
5641
5930
  ...this.#returningClause(table),
5642
5931
  };
5643
5932
  }
5644
- if (this.#isKeyword("SELECT")) {
5645
- 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)");
5646
5937
  resolvePlanExactNumericConstants(insertSource);
5647
5938
  const query = planHasPendingSelectShapes(insertSource)
5648
5939
  ? insertSource
5649
5940
  : optimizePlan(insertSource);
5941
+ // A set operation or VALUES source projects `*` over its members until execution; the
5942
+ // materialized result is checked against the column list then, like every other source.
5650
5943
  if (!planHasPendingSelectShapes(query) &&
5944
+ !query.select.some((item) => item.expression.kind === "wildcard") &&
5651
5945
  columns.length > 0 &&
5652
5946
  query.select.length !== columns.length) {
5653
5947
  throw new TypeError("INSERT ... SELECT must produce exactly the insert column count");
@@ -5658,6 +5952,7 @@ class Parser {
5658
5952
  columns,
5659
5953
  rows: [],
5660
5954
  query,
5955
+ ...this.#onConflictClause(table),
5661
5956
  ...this.#returningClause(table),
5662
5957
  };
5663
5958
  }
@@ -5693,6 +5988,13 @@ class Parser {
5693
5988
  return {};
5694
5989
  this.#keyword("ON");
5695
5990
  this.#keyword("CONFLICT");
5991
+ if (this.#isKeyword("DO")) {
5992
+ // PostgreSQL lets DO NOTHING omit the conflict target (any unique key); DO UPDATE has to
5993
+ // name one. Kysely's `onConflict((oc) => oc.doNothing())` emits exactly this spelling.
5994
+ this.#keyword("DO");
5995
+ this.#keyword("NOTHING");
5996
+ return { onConflict: { column: "", anyTarget: true, action: "nothing" } };
5997
+ }
5696
5998
  this.#expectPunctuation("(");
5697
5999
  const columns = [];
5698
6000
  for (;;) {
@@ -5753,7 +6055,13 @@ class Parser {
5753
6055
  };
5754
6056
  }
5755
6057
  /** RETURNING *, target.*, or [target.]col, ... — execution owns the row semantics. */
5756
- #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) {
5757
6065
  if (!this.#isKeyword("RETURNING"))
5758
6066
  return {};
5759
6067
  this.#keyword("RETURNING");
@@ -5761,29 +6069,67 @@ class Parser {
5761
6069
  this.#index += 1;
5762
6070
  return { returning: "*" };
5763
6071
  }
5764
- const columns = [];
6072
+ const items = [];
6073
+ let plain = true;
5765
6074
  for (;;) {
5766
- const first = this.#identifier();
5767
- if (this.#punctuation(".")) {
5768
- if (first !== table) {
5769
- 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}`);
5770
6082
  }
5771
- if (this.#peek().text === "*") {
5772
- this.#index += 1;
5773
- if (columns.length > 0 || this.#peek().text === ",") {
5774
- throw new TypeError("RETURNING target.* must be the only returned item");
5775
- }
5776
- return { returning: "*" };
6083
+ if (items.length > 0 || this.#peek().text === ",") {
6084
+ throw new TypeError("RETURNING target.* must be the only returned item");
5777
6085
  }
5778
- columns.push(this.#identifier());
6086
+ return { returning: "*" };
5779
6087
  }
5780
- else {
5781
- 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
+ }
5782
6107
  }
6108
+ if (column === undefined || explicit)
6109
+ plain = false;
6110
+ items.push({
6111
+ sql: this.text.slice(start.start, end.end),
6112
+ alias: outputAlias,
6113
+ expression,
6114
+ ...(column === undefined ? {} : { column }),
6115
+ });
5783
6116
  if (!this.#punctuation(","))
5784
6117
  break;
5785
6118
  }
5786
- 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 };
5787
6133
  }
5788
6134
  /**
5789
6135
  * MERGE INTO target USING source ON condition WHEN [NOT] MATCHED [AND …] THEN … (F312).
@@ -5915,15 +6261,19 @@ class Parser {
5915
6261
  #updateStatement() {
5916
6262
  this.#keyword("UPDATE");
5917
6263
  const table = this.#identifier();
6264
+ const alias = this.#mutationAlias("SET");
5918
6265
  this.#keyword("SET");
5919
6266
  const assignments = [];
5920
6267
  for (;;) {
5921
- const column = this.#identifier();
6268
+ const column = this.#mutationTargetColumn(table, alias);
5922
6269
  this.#operator("=");
5923
6270
  const expression = this.#expression();
5924
6271
  if (hasAggregate(expression)) {
5925
6272
  throw new TypeError("Aggregate functions are not allowed in UPDATE assignments");
5926
6273
  }
6274
+ if (containsWindow(expression)) {
6275
+ throw new TypeError("Window functions are not allowed in UPDATE assignments");
6276
+ }
5927
6277
  assignments.push({ column, expression });
5928
6278
  if (!this.#punctuation(","))
5929
6279
  break;
@@ -5932,14 +6282,54 @@ class Parser {
5932
6282
  throw new TypeError("UPDATE assignments must set each column once");
5933
6283
  }
5934
6284
  const predicates = this.#mutationPredicates();
5935
- return { 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
+ };
5936
6293
  }
5937
6294
  #deleteStatement() {
5938
6295
  this.#keyword("DELETE");
5939
6296
  this.#keyword("FROM");
5940
6297
  const table = this.#identifier();
6298
+ const alias = this.#mutationAlias("WHERE", "RETURNING");
5941
6299
  const predicates = this.#mutationPredicates();
5942
- return { 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();
5943
6333
  }
5944
6334
  #mutationPredicates() {
5945
6335
  const predicates = [];
@@ -5970,26 +6360,30 @@ class Parser {
5970
6360
  if (hasAggregate(expression) || expressionColumns(expression).length > 0) {
5971
6361
  throw new TypeError(`${label} must be constant expressions`);
5972
6362
  }
5973
- const needsExecution = (value) => (value.kind === "call" &&
5974
- (statementDatetimeNames.has(value.name) ||
5975
- value.name === "NEXTVAL" ||
5976
- value.name === "CURRVAL" ||
5977
- volatileScalarFunctionNames.has(value.name))) ||
6363
+ // A scalar subquery is a value the statement reads when it runs, like the statement clock.
6364
+ const needsExecution = (value) => value.kind === "subquery" ||
6365
+ value.kind === "exists" ||
6366
+ (value.kind === "call" &&
6367
+ (statementDatetimeNames.has(value.name) ||
6368
+ value.name === "NEXTVAL" ||
6369
+ value.name === "CURRVAL" ||
6370
+ volatileScalarFunctionNames.has(value.name))) ||
5978
6371
  childExpressions(value).some(needsExecution);
5979
6372
  if (needsExecution(expression)) {
5980
6373
  return { expression: resolveExactNumericConstants(expression) };
5981
6374
  }
5982
6375
  return asQueryValue(evaluate(resolveExactNumericConstants(expression), {}));
5983
6376
  }
5984
- #unionMember(sql) {
6377
+ #unionMember(sql, member) {
5985
6378
  if (this.#punctuation("(")) {
5986
6379
  const block = this.#isKeyword("VALUES") ? this.#valuesBlock() : this.#queryExpression(sql);
5987
6380
  this.#expectPunctuation(")");
5988
6381
  return { block, parenthesized: true };
5989
6382
  }
5990
6383
  if (this.#isKeyword("VALUES")) {
5991
- return { block: this.#valuesBlock(), parenthesized: false };
6384
+ return { block: this.#valuesBlock(), parenthesized: false, values: true };
5992
6385
  }
6386
+ this.#compoundMember = member;
5993
6387
  return { block: this.#selectBlock(sql), parenthesized: false };
5994
6388
  }
5995
6389
  #orderByClause() {
@@ -6166,6 +6560,10 @@ class Parser {
6166
6560
  return windows;
6167
6561
  }
6168
6562
  #selectBlockBody(sql) {
6563
+ // Captured and cleared first, so a derived table or subquery inside this block parses its
6564
+ // own tail: only the member's outermost SELECT leaves the compound's clauses alone.
6565
+ const compoundMember = this.#compoundMember;
6566
+ this.#compoundMember = false;
6169
6567
  this.#keyword("SELECT");
6170
6568
  let distinct = false;
6171
6569
  if (this.#isKeyword("DISTINCT")) {
@@ -6322,8 +6720,6 @@ class Parser {
6322
6720
  const having = [];
6323
6721
  if (this.#isKeyword("HAVING")) {
6324
6722
  this.#keyword("HAVING");
6325
- if (distinct)
6326
- throw new TypeError("SELECT DISTINCT cannot be combined with HAVING");
6327
6723
  having.push(...splitCondition(this.#expression()));
6328
6724
  }
6329
6725
  if (this.#isKeyword("WINDOW")) {
@@ -6338,7 +6734,7 @@ class Parser {
6338
6734
  break;
6339
6735
  }
6340
6736
  }
6341
- const orderBy = this.#orderByClause();
6737
+ const orderBy = compoundMember ? [] : this.#orderByClause();
6342
6738
  return assembleSelectBlock({
6343
6739
  sql,
6344
6740
  base,
@@ -6349,7 +6745,7 @@ class Parser {
6349
6745
  groupBy,
6350
6746
  having,
6351
6747
  orderBy,
6352
- ...this.#tailClauses(),
6748
+ ...(compoundMember ? {} : this.#tailClauses()),
6353
6749
  ...(groupingSets === undefined ? {} : { groupingSets }),
6354
6750
  }, this.nextDerivedSequence);
6355
6751
  }
@@ -6414,10 +6810,6 @@ class Parser {
6414
6810
  if (!this.#punctuation(","))
6415
6811
  break;
6416
6812
  }
6417
- if (items.some((item) => item.expression.kind === "wildcard" && item.expression.table === undefined) &&
6418
- items.length > 1) {
6419
- throw new TypeError("SELECT * cannot be mixed with other expressions");
6420
- }
6421
6813
  const aliases = new Set();
6422
6814
  for (const [index, item] of items.entries()) {
6423
6815
  if (item.expression.kind === "list") {
@@ -7143,16 +7535,48 @@ class Parser {
7143
7535
  }
7144
7536
  // || and the JSON arrows share PostgreSQL's loosest "any other operator" level, applying
7145
7537
  // left-to-right to whole arithmetic terms: `'a' || d ->> 'k'` is `('a' || d) ->> 'k'`.
7146
- const precedence = operator === "*" || operator === "/" || operator === "%"
7147
- ? 20
7148
- : operator === "+" || operator === "-"
7149
- ? 10
7150
- : operator === "||" || operator === "->" || operator === "->>"
7151
- ? 5
7152
- : -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;
7153
7550
  if (precedence < minimumPrecedence)
7154
7551
  break;
7155
7552
  this.#index += 1;
7553
+ if (operator === "^") {
7554
+ // Exponentiation binds above * and / and, as in PostgreSQL, associates to the left:
7555
+ // 2 ^ 3 ^ 2 is (2 ^ 3) ^ 2.
7556
+ const exponent = this.#additive(precedence + 1);
7557
+ left = { kind: "call", name: "POWER", arguments: [left, exponent] };
7558
+ continue;
7559
+ }
7560
+ if (regexOperator) {
7561
+ const pattern = this.#additive(precedence + 1);
7562
+ const match = {
7563
+ kind: "call",
7564
+ name: "MINNOW_REGEX_MATCH",
7565
+ arguments: [left, pattern, { kind: "literal", value: operator.endsWith("*") ? "i" : "" }],
7566
+ };
7567
+ left = operator.startsWith("!") ? { kind: "not", operand: match } : match;
7568
+ continue;
7569
+ }
7570
+ if (operator === "::") {
7571
+ // PostgreSQL's postfix cast binds tighter than every binary operator, so it applies to
7572
+ // the term just parsed and rides the same CAST call as the standard spelling.
7573
+ left = {
7574
+ kind: "call",
7575
+ name: "CAST",
7576
+ arguments: [left, { kind: "literal", value: this.#castTarget() }],
7577
+ };
7578
+ continue;
7579
+ }
7156
7580
  if (operator === "->" || operator === "->>") {
7157
7581
  const key = this.#additive(precedence + 1);
7158
7582
  // A key fixed at compile time fails here rather than per row, like SQL/JSON paths.
@@ -7665,7 +8089,8 @@ class Parser {
7665
8089
  name === "MINNOW_COLLATE" ||
7666
8090
  name === "MINNOW_SINGLE_VALUE" ||
7667
8091
  name === "MINNOW_JSON_GET" ||
7668
- name === "MINNOW_JSON_GET_TEXT") {
8092
+ name === "MINNOW_JSON_GET_TEXT" ||
8093
+ name === "MINNOW_REGEX_MATCH") {
7669
8094
  throw new TypeError(`Unsupported function: ${identifier}`);
7670
8095
  }
7671
8096
  if (!aggregateNames.has(name) && !scalarFunctionNames.has(name))
@@ -7718,6 +8143,25 @@ class Parser {
7718
8143
  if (name === "JSON_ARRAYAGG" && args[0]?.kind === "wildcard") {
7719
8144
  throw new TypeError("JSON_ARRAYAGG requires a scalar value expression");
7720
8145
  }
8146
+ const simple = simpleScalarFunctions.get(name);
8147
+ if (simple !== undefined) {
8148
+ if (name === "AGE" && args.length === 1) {
8149
+ // AGE(x) is AGE(CURRENT_DATE, x): the statement clock, resolved once per statement.
8150
+ this.usesStatementDatetime = true;
8151
+ args.unshift({ kind: "call", name: "CURRENT_DATE", arguments: [] });
8152
+ }
8153
+ if (args.length < simple.minArgs || args.length > simple.maxArgs) {
8154
+ const range = simple.minArgs === simple.maxArgs
8155
+ ? `exactly ${String(simple.minArgs)}`
8156
+ : simple.maxArgs === Number.POSITIVE_INFINITY
8157
+ ? `at least ${String(simple.minArgs)}`
8158
+ : `${String(simple.minArgs)} to ${String(simple.maxArgs)}`;
8159
+ throw new TypeError(`${name} takes ${range} argument${simple.maxArgs === 1 ? "" : "s"}`);
8160
+ }
8161
+ if (args.some((argument) => argument.kind === "wildcard")) {
8162
+ throw new TypeError(`${name} takes scalar arguments`);
8163
+ }
8164
+ }
7721
8165
  if (name === "ROUND" && (args.length < 1 || args.length > 2))
7722
8166
  throw new TypeError("ROUND requires one or two arguments");
7723
8167
  if (name === "COALESCE" && args.length < 1)
@@ -8211,23 +8655,33 @@ function desugarGroupingSets(parts, nextSequence) {
8211
8655
  if (hasAggregate(item.expression))
8212
8656
  return item;
8213
8657
  const signature = signatureOf(item.expression);
8214
- if (setSignatures.has(signature) || !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)
8215
8681
  return item;
8216
- // MIN over an always-NULL argument: legal in a grouped select, NULL in every group, and
8217
- // typed like the original expression through MIN's carry and NULLIF's first argument.
8218
8682
  return {
8219
8683
  alias: item.alias,
8220
- expression: {
8221
- kind: "call",
8222
- name: "MIN",
8223
- arguments: [
8224
- {
8225
- kind: "call",
8226
- name: "NULLIF",
8227
- arguments: [structuredClone(item.expression), structuredClone(item.expression)],
8228
- },
8229
- ],
8230
- },
8684
+ expression: { kind: "call", name: "MIN", arguments: [nullified] },
8231
8685
  };
8232
8686
  });
8233
8687
  return assembleSelectBlock({
@@ -8267,6 +8721,80 @@ function resolveOrderByOrdinals(orderBy, select) {
8267
8721
  return { ...order, expression: { kind: "column", reference: item.alias } };
8268
8722
  });
8269
8723
  }
8724
+ /**
8725
+ * `SELECT id FROM people AS p ORDER BY p.id`: a qualified sort key that names the same source
8726
+ * column the select list carries unqualified sorts by that output. Only a qualifier that is one
8727
+ * of the block's own sources counts, and only when exactly one selected column matches, so an
8728
+ * ambiguous or misspelled reference still fails in resolution.
8729
+ */
8730
+ function resolveOrderQualifiers(orderBy, select, sourceAliases) {
8731
+ return orderBy.map((order) => {
8732
+ const expression = order.expression;
8733
+ if (expression.kind !== "column")
8734
+ return order;
8735
+ const separator = expression.reference.indexOf(".");
8736
+ if (separator === -1)
8737
+ return order;
8738
+ // A selected column referenced exactly as written orders by that output column: a wildcard
8739
+ // expansion reads `p.name` but names the output `name`.
8740
+ const exact = select.find((item) => item.expression.kind === "column" && item.expression.reference === expression.reference);
8741
+ if (exact !== undefined) {
8742
+ return exact.alias === expression.reference
8743
+ ? order
8744
+ : { ...order, expression: { kind: "column", reference: exact.alias } };
8745
+ }
8746
+ const qualifier = expression.reference.slice(0, separator);
8747
+ const column = expression.reference.slice(separator + 1);
8748
+ if (!sourceAliases.includes(qualifier))
8749
+ return order;
8750
+ const matches = select.filter((item) => item.expression.kind === "column" &&
8751
+ !item.expression.reference.includes(".") &&
8752
+ item.expression.reference === column);
8753
+ const match = matches.length === 1 ? matches[0] : undefined;
8754
+ if (match === undefined)
8755
+ return order;
8756
+ return { ...order, expression: { kind: "column", reference: match.alias } };
8757
+ });
8758
+ }
8759
+ /**
8760
+ * Resolves GROUP BY items the way PostgreSQL does: an integer literal is a select-list ordinal,
8761
+ * and a bare name that is an output alias — but not itself the selected column of that name —
8762
+ * stands for the aliased expression. `GROUP BY 1` and `GROUP BY bucket` over
8763
+ * `SELECT FLOOR(amount / 25) AS bucket` therefore group by the expression. A name that is both
8764
+ * an alias and a source column keeps its source-column meaning, as it does in PostgreSQL.
8765
+ */
8766
+ function resolveGroupByReferences(groupBy, select) {
8767
+ return groupBy.map((expression) => {
8768
+ if (expression.kind === "literal" && typeof expression.value === "number") {
8769
+ const ordinal = expression.value;
8770
+ const item = Number.isInteger(ordinal) ? select[ordinal - 1] : undefined;
8771
+ if (item === undefined) {
8772
+ throw new TypeError(`GROUP BY ordinal is out of range: ${String(ordinal)}`);
8773
+ }
8774
+ if (hasAggregate(item.expression) || containsWindow(item.expression)) {
8775
+ throw new TypeError("GROUP BY ordinals cannot name an aggregate or window column");
8776
+ }
8777
+ return structuredClone(item.expression);
8778
+ }
8779
+ if (expression.kind !== "column" || expression.reference.includes("."))
8780
+ return expression;
8781
+ const aliased = select.filter((item) => item.alias === expression.reference);
8782
+ const item = aliased.length === 1 ? aliased[0] : undefined;
8783
+ if (item === undefined ||
8784
+ item.expression.kind === "wildcard" ||
8785
+ hasAggregate(item.expression) ||
8786
+ containsWindow(item.expression)) {
8787
+ return expression;
8788
+ }
8789
+ // `SELECT region AS r ... GROUP BY r` groups by region. Were r also a source column,
8790
+ // PostgreSQL would group by that column and then reject the ungrouped region, so reading
8791
+ // the alias never changes an answer PostgreSQL gives.
8792
+ if (item.expression.kind === "column" && item.expression.reference === expression.reference) {
8793
+ return expression;
8794
+ }
8795
+ return structuredClone(item.expression);
8796
+ });
8797
+ }
8270
8798
  /**
8271
8799
  * FULL OUTER JOIN desugars into UNION ALL of two left joins: the plain left join carries every
8272
8800
  * base row (matched or not), and the swapped left join filtered to a NULL-extended base side
@@ -8381,7 +8909,16 @@ export function assembleSelectBlock(parts, nextSequence) {
8381
8909
  ...(parts.distinct ? { distinctWildcard: true } : {}),
8382
8910
  };
8383
8911
  }
8384
- parts = { ...parts, orderBy: resolveOrderByOrdinals(parts.orderBy, parts.select) };
8912
+ parts = {
8913
+ ...parts,
8914
+ orderBy: resolveOrderQualifiers(resolveOrderByOrdinals(parts.orderBy, parts.select), parts.select, [parts.base, ...parts.joins].map((source) => source.alias)),
8915
+ groupBy: resolveGroupByReferences(parts.groupBy, parts.select),
8916
+ ...(parts.groupingSets === undefined
8917
+ ? {}
8918
+ : {
8919
+ groupingSets: parts.groupingSets.map((set) => resolveGroupByReferences(set, parts.select)),
8920
+ }),
8921
+ };
8385
8922
  if (parts.joins.some((join) => join.full === true)) {
8386
8923
  return desugarFullJoin(parts, nextSequence);
8387
8924
  }
@@ -8399,15 +8936,41 @@ export function assembleSelectBlock(parts, nextSequence) {
8399
8936
  const { limitParameter, offsetParameter } = parts;
8400
8937
  const groupBy = [...parts.groupBy];
8401
8938
  let distinctWildcard = false;
8939
+ if (distinct &&
8940
+ (groupBy.length > 0 ||
8941
+ having.length > 0 ||
8942
+ select.some((item) => hasAggregate(item.expression) || hasWindow(item.expression)))) {
8943
+ // DISTINCT over a grouped, aggregated, or windowed block applies to that block's output:
8944
+ // run the block as written inside a derived table and take the distinct rows of it,
8945
+ // carrying the ORDER BY and paging outward, which is where they act in PostgreSQL too.
8946
+ const inner = assembleSelectBlock({ ...parts, distinct: false, orderBy: [] }, nextSequence);
8947
+ delete inner.limit;
8948
+ delete inner.offset;
8949
+ delete inner.limitParameter;
8950
+ delete inner.offsetParameter;
8951
+ delete inner.limitWithTies;
8952
+ const source = derivedTableSource(inner, "(distinct)", nextSequence);
8953
+ return assembleSelectBlock({
8954
+ sql,
8955
+ base: source,
8956
+ joins: [],
8957
+ select: inner.select.map((item) => ({
8958
+ expression: { kind: "column", reference: item.alias },
8959
+ alias: item.alias,
8960
+ })),
8961
+ distinct: true,
8962
+ predicates: [],
8963
+ groupBy: [],
8964
+ having: [],
8965
+ orderBy: parts.orderBy,
8966
+ ...(limit === undefined ? {} : { limit }),
8967
+ ...(offset === undefined ? {} : { offset }),
8968
+ ...(limitParameter === undefined ? {} : { limitParameter }),
8969
+ ...(offsetParameter === undefined ? {} : { offsetParameter }),
8970
+ ...(parts.limitWithTies === true ? { limitWithTies: true } : {}),
8971
+ }, nextSequence);
8972
+ }
8402
8973
  if (distinct) {
8403
- if (select.some((item) => hasAggregate(item.expression)))
8404
- throw new TypeError("SELECT DISTINCT cannot be combined with aggregate functions");
8405
- if (select.some((item) => hasWindow(item.expression)))
8406
- throw new TypeError("SELECT DISTINCT cannot be combined with window functions");
8407
- if (groupBy.length > 0)
8408
- throw new TypeError("SELECT DISTINCT cannot be combined with GROUP BY");
8409
- if (having.length > 0)
8410
- throw new TypeError("SELECT DISTINCT cannot be combined with HAVING");
8411
8974
  if (select.some((item) => item.expression.kind === "wildcard")) {
8412
8975
  // The wildcard's columns are unknown until input schemas exist; executor entries
8413
8976
  // expand the flag into a concrete select list plus GROUP BY (expandDistinctWildcard).
@@ -8490,15 +9053,19 @@ function expandDistinctWildcard(plan, columnsOf) {
8490
9053
  columns: sourceWildcardColumns(source, columnsOf),
8491
9054
  }));
8492
9055
  const visible = shaped.filter(({ columns }) => (columns?.length ?? 0) > 0);
8493
- const 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
+ }
8494
9061
  const select = visible.flatMap(({ source, columns }) => {
8495
9062
  if (columns === undefined) {
8496
9063
  throw new TypeError(`SELECT DISTINCT * requires known columns for: ${source.table}`);
8497
9064
  }
8498
- return columns.map((name) => {
8499
- const output = multiple ? `${source.alias}.${name}` : name;
8500
- return { expression: { kind: "column", reference: output }, alias: output };
8501
- });
9065
+ return columns.map((name) => ({
9066
+ expression: { kind: "column", reference: `${source.alias}.${name}` },
9067
+ alias: (contributed.get(name) ?? 0) > 1 ? `${source.alias}.${name}` : name,
9068
+ }));
8502
9069
  });
8503
9070
  const { distinctWildcard, ...rest } = plan;
8504
9071
  void distinctWildcard;
@@ -8739,7 +9306,21 @@ export function bindPendingSelectShapes(plan, columnsOf) {
8739
9306
  if (unknown !== undefined) {
8740
9307
  throw new TypeError(`SELECT * requires known columns for: ${unknown.source.table}`);
8741
9308
  }
8742
- const multiple = shaped.filter(({ columns }) => (columns?.length ?? 0) > 0).length > 1;
9309
+ // Wildcard outputs keep their bare column names, as PostgreSQL returns them; only a name
9310
+ // that two sources both contribute (`SELECT *` over a join on `id`) is alias-qualified, since
9311
+ // a result row cannot carry two columns named `id`.
9312
+ const contributed = new Map();
9313
+ for (const item of block.select) {
9314
+ if (item.expression.kind !== "wildcard")
9315
+ continue;
9316
+ const table = item.expression.table;
9317
+ for (const { source, columns } of shaped) {
9318
+ if (table !== undefined && source.alias !== table)
9319
+ continue;
9320
+ for (const name of columns ?? [])
9321
+ contributed.set(name, (contributed.get(name) ?? 0) + 1);
9322
+ }
9323
+ }
8743
9324
  const items = block.select.flatMap((item) => {
8744
9325
  if (item.expression.kind !== "wildcard")
8745
9326
  return [item];
@@ -8750,7 +9331,7 @@ export function bindPendingSelectShapes(plan, columnsOf) {
8750
9331
  }
8751
9332
  return selected.flatMap(({ source, columns }) => (columns ?? []).map((name) => ({
8752
9333
  expression: { kind: "column", reference: `${source.alias}.${name}` },
8753
- alias: multiple ? `${source.alias}.${name}` : name,
9334
+ alias: (contributed.get(name) ?? 0) > 1 ? `${source.alias}.${name}` : name,
8754
9335
  })));
8755
9336
  });
8756
9337
  const aliases = new Set();
@@ -9194,8 +9775,10 @@ export function derivedTableSource(derived, alias, nextSequence) {
9194
9775
  }
9195
9776
  /** The parser's LIMIT range contract, shared with the typed builder. */
9196
9777
  export function validateLimit(limit) {
9197
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100_000)
9198
- 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");
9199
9782
  return limit;
9200
9783
  }
9201
9784
  /**
@@ -9266,7 +9849,7 @@ function jsonTableColumnValue(column, value) {
9266
9849
  if (value instanceof Date)
9267
9850
  date = value;
9268
9851
  else if (typeof value === "string")
9269
- date = new Date(value);
9852
+ date = datetimeText(value);
9270
9853
  else if (typeof value === "number")
9271
9854
  date = new Date(value);
9272
9855
  else
@@ -9277,18 +9860,20 @@ function jsonTableColumnValue(column, value) {
9277
9860
  return date;
9278
9861
  }
9279
9862
  function timestampLiteral(text) {
9280
- const trimmed = text.trim();
9281
- 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);
9282
- if (match === null)
9863
+ const date = parseSqlTimestampText(text);
9864
+ if (date === undefined)
9283
9865
  throw new TypeError(`Invalid TIMESTAMP literal: ${text}`);
9284
- const [, day, time = "00:00:00", zone = "Z"] = match;
9285
- const seconds = time.length === 5 ? `${time}:00` : time;
9286
- const date = new Date(`${String(day)}T${seconds}${zone === "Z" ? "Z" : zone}`);
9287
- if (!Number.isFinite(dateMilliseconds(date))) {
9288
- throw new TypeError(`Invalid TIMESTAMP literal: ${text}`);
9289
- }
9290
9866
  return date;
9291
9867
  }
9868
+ /**
9869
+ * Reads datetime text the way the TIMESTAMP literal does — a zoneless `2026-01-02 03:04:05` is
9870
+ * UTC, never the host's zone — and falls back to the JavaScript parser for other spellings.
9871
+ * `new Date("2026-01-02 03:04:05")` alone would read the same text in local time, so a CAST
9872
+ * would answer differently on two machines.
9873
+ */
9874
+ export function datetimeText(text) {
9875
+ return parseSqlTimestampText(text) ?? new Date(text);
9876
+ }
9292
9877
  /** The parser's OFFSET range contract, shared with the typed builder. */
9293
9878
  export function validateOffset(offset) {
9294
9879
  if (!Number.isSafeInteger(offset) || offset < 0 || offset > 100_000_000)
@@ -9662,6 +10247,29 @@ function tokenize(sql) {
9662
10247
  index += 2;
9663
10248
  continue;
9664
10249
  }
10250
+ if (pair === "::") {
10251
+ // PostgreSQL's postfix cast, `expression::type`.
10252
+ push({ kind: "operator", text: "::", start: index, end: index + 2 });
10253
+ index += 2;
10254
+ continue;
10255
+ }
10256
+ if (pair === "!~") {
10257
+ const text = sql[index + 2] === "*" ? "!~*" : "!~";
10258
+ push({ kind: "operator", text, start: index, end: index + text.length });
10259
+ index += text.length;
10260
+ continue;
10261
+ }
10262
+ if (character === "~") {
10263
+ const text = sql[index + 1] === "*" ? "~*" : "~";
10264
+ push({ kind: "operator", text, start: index, end: index + text.length });
10265
+ index += text.length;
10266
+ continue;
10267
+ }
10268
+ if (character === "^") {
10269
+ push({ kind: "operator", text: "^", start: index, end: index + 1 });
10270
+ index += 1;
10271
+ continue;
10272
+ }
9665
10273
  if (["+", "-", "*", "/", "%", "=", ">", "<"].includes(character))
9666
10274
  push({ kind: "operator", text: character, start: index, end: index + 1 });
9667
10275
  else if (["(", ")", "[", "]", ",", "."].includes(character))