@minnowdb/core 0.4.1 → 0.6.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.
Files changed (47) hide show
  1. package/README.md +5 -5
  2. package/dist/engine/cancellation.d.ts +2 -0
  3. package/dist/engine/cancellation.js +4 -0
  4. package/dist/engine/catalog.d.ts +5 -1
  5. package/dist/engine/catalog.js +5 -1
  6. package/dist/engine/client.d.ts +34 -6
  7. package/dist/engine/client.js +87 -19
  8. package/dist/engine/database.d.ts +43 -20
  9. package/dist/engine/database.js +823 -164
  10. package/dist/engine/defaults.js +11 -0
  11. package/dist/engine/errors.d.ts +19 -0
  12. package/dist/engine/errors.js +31 -0
  13. package/dist/engine/fts.d.ts +2 -15
  14. package/dist/engine/live.d.ts +1 -7
  15. package/dist/engine/live.js +12 -13
  16. package/dist/engine/optimizer.js +546 -39
  17. package/dist/engine/query-cache.js +1 -0
  18. package/dist/engine/query.d.ts +16 -278
  19. package/dist/engine/query.js +260 -74
  20. package/dist/engine/result-wire.d.ts +2 -0
  21. package/dist/engine/result-wire.js +21 -5
  22. package/dist/engine/schema-wire.d.ts +14 -1
  23. package/dist/engine/schema-wire.js +7 -1
  24. package/dist/engine/schema.d.ts +83 -32
  25. package/dist/engine/schema.js +180 -14
  26. package/dist/engine/sql-domains.d.ts +11 -0
  27. package/dist/engine/sql-domains.js +65 -1
  28. package/dist/engine/sql-json.js +22 -3
  29. package/dist/engine/sql-semantics.js +21 -3
  30. package/dist/engine/vector.d.ts +2 -2
  31. package/dist/engine/vector.js +328 -79
  32. package/dist/engine/worker-host.js +119 -44
  33. package/dist/plan/index.d.ts +5 -4
  34. package/dist/plan/index.js +3 -3
  35. package/dist/plan/model.d.ts +218 -0
  36. package/dist/plan/model.js +1 -0
  37. package/dist/storage/indexeddb.js +4 -12
  38. package/dist/storage/toolkit/record-core.js +7 -22
  39. package/dist/storage/types.d.ts +26 -8
  40. package/dist/storage/types.js +85 -0
  41. package/dist/transactions/index.d.ts +5 -3
  42. package/dist/transactions/index.js +58 -8
  43. package/dist/worker-protocol/index.d.ts +6 -1
  44. package/dist/worker-protocol/index.js +5 -2
  45. package/package.json +1 -1
  46. package/postgres-feature-profile.json +5 -0
  47. package/sql-feature-matrix.json +75 -19
@@ -6,11 +6,22 @@ import { cachedQueryTerms, ftsBm25Row, FtsStatsAccumulator, ftsMatchTruth, rende
6
6
  import { QueryMemoryContext } from "./memory.js";
7
7
  import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
8
8
  import { stringArgument } from "./sql-semantics.js";
9
- import { jsonAtPath, jsonConstructor, jsonIsValid, jsonValueOf, parseJsonPath, } from "./sql-json.js";
9
+ import { jsonAtPath, jsonConstructor, jsonIsValid, parseJsonPath } from "./sql-json.js";
10
10
  import { optimizePlan } from "./optimizer.js";
11
11
  import { compareSqlValues as compareValues, compileLikePattern, compileSimilarPattern, encodeSqlEqualityValue, roundSqlNumber, } from "./sql-semantics.js";
12
- import { arrayDomainValue, boundedJsonText, collatedDomainValue, exactNumericBinary, exactNumericValue, externalSqlDomainValue, intervalDomainValue, isExactNumeric, isSqlDomainValue, jsonDomainValue, normalizeSqlDomainValue, protectedSqlTextValue, timeDomainValue, uuidDomainValue, } from "./sql-domains.js";
12
+ import { arrayDomainValue, boundedJsonText, collatedDomainValue, dateDomainValue, exactNumericBinary, exactNumericValue, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, isExactNumeric, isSqlDomainValue, jsonDomainValue, normalizeSqlDomainValue, preservedJsonDomainValue, protectedSqlTextValue, timeDomainValue, uuidDomainValue, } from "./sql-domains.js";
13
13
  import { columnarTableFromRows, prepareVectorQuery, } from "./vector.js";
14
+ /** Domain metadata for an execution path that has no catalog-backed type information. */
15
+ export function unknownColumnDomains(columns) {
16
+ return columns.map(() => null);
17
+ }
18
+ function withColumnDomains(result, domains) {
19
+ const columnDomains = domains ?? unknownColumnDomains(result.columns);
20
+ if (columnDomains.length !== result.columns.length) {
21
+ throw new TypeError(`Query result column domains must align with the result columns (${String(columnDomains.length)} domains for ${String(result.columns.length)} columns)`);
22
+ }
23
+ return { ...result, columnDomains: [...columnDomains] };
24
+ }
14
25
  export const scalarFunctionNames = new Set([
15
26
  "ROUND",
16
27
  "COALESCE",
@@ -138,6 +149,8 @@ function castValue(value, target) {
138
149
  return jsonDomainValue(value, true);
139
150
  if (target === "uuid")
140
151
  return uuidDomainValue(value);
152
+ if (target === "date")
153
+ return dateDomainValue(value);
141
154
  if (target === "time")
142
155
  return timeDomainValue(value);
143
156
  if (target === "interval")
@@ -199,8 +212,9 @@ function castValue(value, target) {
199
212
  if (target === "datetime") {
200
213
  if (value instanceof Date)
201
214
  return value;
202
- if (typeof value === "string" || typeof value === "number") {
203
- const parsed = new Date(value);
215
+ const external = externalSqlDomainValue(value);
216
+ if (typeof external === "string" || typeof external === "number") {
217
+ const parsed = new Date(external);
204
218
  if (Number.isFinite(dateMilliseconds(parsed)))
205
219
  return parsed;
206
220
  throw new TypeError(`Cannot cast this value to a datetime: ${String(value)}`);
@@ -250,7 +264,7 @@ export function scalarFunctionValue(name, values) {
250
264
  }
251
265
  if (name === "JSON_ARRAY" || name === "JSON_OBJECT") {
252
266
  // These build from every argument, so a NULL first one is data, not an early exit.
253
- return jsonConstructor(name, values);
267
+ return preservedJsonDomainValue(jsonConstructor(name, values));
254
268
  }
255
269
  if (name === "ARRAY")
256
270
  return arrayDomainValue(values);
@@ -380,7 +394,7 @@ export function scalarFunctionValue(name, values) {
380
394
  if (!found.found || found.value === undefined)
381
395
  return null;
382
396
  // JSON_QUERY returns JSON text, so a selected string keeps its quotes.
383
- return JSON.stringify(found.value);
397
+ return preservedJsonDomainValue(JSON.stringify(found.value));
384
398
  }
385
399
  case "LPAD":
386
400
  case "RPAD": {
@@ -624,10 +638,18 @@ export function intervalLiteral(text) {
624
638
  export function dateAddValue(value, months, milliseconds) {
625
639
  if (value === null || value === undefined)
626
640
  return null;
627
- if (!(value instanceof Date))
628
- throw new TypeError("Date arithmetic requires a datetime value");
641
+ const calendarDate = isDateDomainValue(value);
642
+ const external = externalSqlDomainValue(value);
643
+ const input = value instanceof Date
644
+ ? value
645
+ : calendarDate && typeof external === "string"
646
+ ? new Date(`${external}T00:00:00.000Z`)
647
+ : undefined;
648
+ if (input === undefined)
649
+ throw new TypeError("Date arithmetic requires a date or datetime value");
629
650
  const monthCount = Number(months ?? 0);
630
- const shifted = copyDate(value);
651
+ const millisecondCount = Number(milliseconds ?? 0);
652
+ const shifted = copyDate(input);
631
653
  if (monthCount !== 0) {
632
654
  const day = dateUtcDate(shifted);
633
655
  setDateUtcDate(shifted, 1);
@@ -635,7 +657,10 @@ export function dateAddValue(value, months, milliseconds) {
635
657
  const lastDay = new Date(Date.UTC(dateUtcFullYear(shifted), dateUtcMonth(shifted) + 1, 0));
636
658
  setDateUtcDate(shifted, Math.min(day, dateUtcDate(lastDay)));
637
659
  }
638
- return new Date(dateMilliseconds(shifted) + Number(milliseconds ?? 0));
660
+ const result = new Date(dateMilliseconds(shifted) + millisecondCount);
661
+ return calendarDate && millisecondCount % 86_400_000 === 0
662
+ ? dateDomainValue(dateIsoString(result).slice(0, 10))
663
+ : result;
639
664
  }
640
665
  export function dateTruncValue(unit, value) {
641
666
  if (typeof unit !== "string" || !dateTruncUnits.has(unit.toLowerCase())) {
@@ -643,12 +668,18 @@ export function dateTruncValue(unit, value) {
643
668
  }
644
669
  if (value === null || value === undefined)
645
670
  return null;
646
- if (!(value instanceof Date))
647
- throw new TypeError("DATE_TRUNC requires a datetime value");
671
+ const external = externalSqlDomainValue(value);
672
+ const input = value instanceof Date
673
+ ? value
674
+ : isDateDomainValue(value) && typeof external === "string"
675
+ ? new Date(`${external}T00:00:00.000Z`)
676
+ : undefined;
677
+ if (input === undefined)
678
+ throw new TypeError("DATE_TRUNC requires a date or datetime value");
648
679
  const normalized = unit.toLowerCase();
649
- const year = dateUtcFullYear(value);
650
- const month = dateUtcMonth(value);
651
- const day = dateUtcDate(value);
680
+ const year = dateUtcFullYear(input);
681
+ const month = dateUtcMonth(input);
682
+ const day = dateUtcDate(input);
652
683
  switch (normalized) {
653
684
  case "year":
654
685
  return new Date(Date.UTC(year, 0, 1));
@@ -664,11 +695,11 @@ export function dateTruncValue(unit, value) {
664
695
  case "day":
665
696
  return new Date(Date.UTC(year, month, day));
666
697
  case "hour":
667
- return new Date(Date.UTC(year, month, day, dateUtcHours(value)));
698
+ return new Date(Date.UTC(year, month, day, dateUtcHours(input)));
668
699
  case "minute":
669
- return new Date(Date.UTC(year, month, day, dateUtcHours(value), dateUtcMinutes(value)));
700
+ return new Date(Date.UTC(year, month, day, dateUtcHours(input), dateUtcMinutes(input)));
670
701
  default:
671
- return new Date(Date.UTC(year, month, day, dateUtcHours(value), dateUtcMinutes(value), dateUtcSeconds(value)));
702
+ return new Date(Date.UTC(year, month, day, dateUtcHours(input), dateUtcMinutes(input), dateUtcSeconds(input)));
672
703
  }
673
704
  }
674
705
  /** The output column type of one window: rankings and most aggregates count, MIN/MAX carry. */
@@ -737,7 +768,6 @@ const createTableTypeNames = new Map([
737
768
  ["TIMESTAMP", "datetime"],
738
769
  ["TIMESTAMPTZ", "datetime"],
739
770
  ["DATETIME", "datetime"],
740
- ["DATE", "datetime"],
741
771
  ]);
742
772
  const clauseKeywords = new Set([
743
773
  "WHERE",
@@ -1735,7 +1765,10 @@ export function bindStatementParameters(statement, params) {
1735
1765
  */
1736
1766
  export function inferBlockSchema(plan, schemas) {
1737
1767
  const sources = [plan.base, ...plan.joins];
1738
- const multipleSources = sources.length > 1;
1768
+ const multipleSources = sources.filter((source) => {
1769
+ const schema = schemas.get(source.table);
1770
+ return schema === undefined || schema.some((column) => !column.name.startsWith("\0"));
1771
+ }).length > 1;
1739
1772
  const wildcardSchema = (source) => (schemas.get(source.table) ?? [])
1740
1773
  .filter((column) => !column.name.startsWith("\0"))
1741
1774
  .map((column) => ({
@@ -1797,7 +1830,11 @@ export function inferBlockSchema(plan, schemas) {
1797
1830
  ...(scale === undefined ? {} : { scale }),
1798
1831
  };
1799
1832
  }
1800
- if (target === "json" || target === "jsonb" || target === "uuid" || target === "time") {
1833
+ if (target === "json" ||
1834
+ target === "jsonb" ||
1835
+ target === "uuid" ||
1836
+ target === "date" ||
1837
+ target === "time") {
1801
1838
  return { kind: target };
1802
1839
  }
1803
1840
  if (target === "interval")
@@ -1825,12 +1862,32 @@ export function inferBlockSchema(plan, schemas) {
1825
1862
  }
1826
1863
  if (expression.kind !== "call")
1827
1864
  return undefined;
1865
+ if (expression.name === "CURRENT_DATE")
1866
+ return { kind: "date" };
1828
1867
  if (expression.name === "CAST") {
1829
1868
  const target = expression.arguments[1];
1830
1869
  return target?.kind === "literal" && typeof target.value === "string"
1831
1870
  ? castDomain(target.value)
1832
1871
  : undefined;
1833
1872
  }
1873
+ if (expression.name === "JSON_ARRAYAGG" ||
1874
+ expression.name === "JSON_QUERY" ||
1875
+ expression.name === "JSON_OBJECT" ||
1876
+ expression.name === "JSON_ARRAY") {
1877
+ return { kind: "json" };
1878
+ }
1879
+ if (expression.name === "GEN_RANDOM_UUID")
1880
+ return { kind: "uuid" };
1881
+ if (expression.name === "DATE_ADD") {
1882
+ const input = inferDomain(expression.arguments[0] ?? { kind: "literal", value: null });
1883
+ const milliseconds = expression.arguments[2];
1884
+ return input?.kind === "date" &&
1885
+ milliseconds?.kind === "literal" &&
1886
+ typeof milliseconds.value === "number" &&
1887
+ milliseconds.value % 86_400_000 === 0
1888
+ ? input
1889
+ : undefined;
1890
+ }
1834
1891
  if (expression.name === "SUM" ||
1835
1892
  expression.name === "AVG" ||
1836
1893
  expression.name === "MIN" ||
@@ -1963,9 +2020,13 @@ export function inferBlockSchema(plan, schemas) {
1963
2020
  return "boolean";
1964
2021
  if (expression.name === "DATE_TRUNC")
1965
2022
  return "datetime";
1966
- if (expression.name === "CURRENT_DATE" || expression.name === "CURRENT_TIMESTAMP") {
1967
- return "datetime";
2023
+ if (expression.name === "DATE_ADD") {
2024
+ return inferDomain(expression)?.kind === "date" ? "string" : "datetime";
1968
2025
  }
2026
+ if (expression.name === "CURRENT_DATE")
2027
+ return "string";
2028
+ if (expression.name === "CURRENT_TIMESTAMP")
2029
+ return "datetime";
1969
2030
  // LOCALTIME is the statement clock's canonical 'HH:MM:SS' text; explicit TIME values use
1970
2031
  // the same public representation while carrying a logical domain internally.
1971
2032
  if (expression.name === "LOCALTIME")
@@ -2062,6 +2123,22 @@ export function inferBlockSchema(plan, schemas) {
2062
2123
  ];
2063
2124
  });
2064
2125
  }
2126
+ /** Best-effort logical domains for a public result without making execution a new typecheck. */
2127
+ export function inferResultColumnDomains(plan, schemas) {
2128
+ try {
2129
+ return inferBlockSchema(plan, schemas).map(({ sqlDomain }) => sqlDomain ?? null);
2130
+ }
2131
+ catch {
2132
+ return plan.select.flatMap((item) => {
2133
+ try {
2134
+ return inferBlockSchema({ ...plan, select: [item] }, schemas).map(({ sqlDomain }) => sqlDomain ?? null);
2135
+ }
2136
+ catch {
2137
+ return [null];
2138
+ }
2139
+ });
2140
+ }
2141
+ }
2065
2142
  /** Whether a final result can contain one of the engine's tagged string-domain values. */
2066
2143
  export function queryResultNeedsExternalization(plan, schemas) {
2067
2144
  // This is only a result-boundary optimization flag, not a second typecheck. In particular,
@@ -2080,7 +2157,7 @@ export function queryResultNeedsExternalization(plan, schemas) {
2080
2157
  if (target?.kind === "literal" &&
2081
2158
  typeof target.value === "string" &&
2082
2159
  (target.value.startsWith("numeric:") ||
2083
- ["json", "jsonb", "uuid", "time", "interval"].includes(target.value))) {
2160
+ ["json", "jsonb", "uuid", "date", "time", "interval"].includes(target.value))) {
2084
2161
  return true;
2085
2162
  }
2086
2163
  }
@@ -2278,15 +2355,17 @@ export function createPreparedQuery(plan, tables, options = {}) {
2278
2355
  }
2279
2356
  }
2280
2357
  /** Internal columnar entry point used after MinnowDatabase materializes a stable snapshot. */
2281
- export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemoryContext(), options = {}) {
2358
+ export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemoryContext(), preparedOptions = {}) {
2282
2359
  plan = resolveStatementDatetimes(plan);
2283
2360
  const ties = withTiesPlan(plan);
2284
2361
  if (ties.plan !== plan) {
2285
- return trimPreparedResults(createPreparedColumnarQuery(ties.plan, tables, memory, options), ties.trim);
2362
+ return trimPreparedResults(createPreparedColumnarQuery(ties.plan, tables, memory, preparedOptions), ties.trim);
2286
2363
  }
2287
2364
  const columnarColumns = (tableName) => {
2288
2365
  const table = tables.get(tableName);
2289
- return table === undefined ? undefined : [...table.columns.keys()];
2366
+ return table === undefined
2367
+ ? undefined
2368
+ : [...table.columns.keys()].filter((name) => !name.startsWith("\0"));
2290
2369
  };
2291
2370
  plan = expandSourceColumnAliases(plan, columnarColumns);
2292
2371
  plan = expandNaturalJoins(plan, columnarColumns);
@@ -2298,14 +2377,32 @@ export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemo
2298
2377
  try {
2299
2378
  prepared = prepareVectorQuery(plan, tables, {
2300
2379
  memoryContext: memory,
2301
- ...(options.ftsStats === undefined ? {} : { ftsStats: options.ftsStats }),
2380
+ ...(preparedOptions.ftsStats === undefined ? {} : { ftsStats: preparedOptions.ftsStats }),
2302
2381
  });
2303
2382
  }
2304
2383
  catch (error) {
2305
2384
  memory.close();
2306
2385
  throw error;
2307
2386
  }
2308
- const outputNeedsExternalization = options.outputNeedsExternalization;
2387
+ const outputNeedsExternalization = preparedOptions.outputNeedsExternalization;
2388
+ let outputColumnDomains = preparedOptions.outputColumnDomains;
2389
+ if (outputColumnDomains === undefined) {
2390
+ try {
2391
+ const schemas = new Map([...tables].map(([name, table]) => [
2392
+ name,
2393
+ [...table.columns].map(([columnName, vector]) => ({
2394
+ name: columnName,
2395
+ type: vector.kind,
2396
+ })),
2397
+ ]));
2398
+ outputColumnDomains = inferResultColumnDomains(plan, schemas);
2399
+ }
2400
+ catch {
2401
+ // This low-level schema-less entry point may not have enough source information. Known
2402
+ // expression domains (including JSON constructors) are still inferred whenever it does.
2403
+ outputColumnDomains = undefined;
2404
+ }
2405
+ }
2309
2406
  return {
2310
2407
  sql: plan.sql,
2311
2408
  tables: [plan.base.table, ...plan.joins.map((join) => join.table)],
@@ -2315,17 +2412,17 @@ export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemo
2315
2412
  execute() {
2316
2413
  if (closed || prepared === undefined)
2317
2414
  throw new Error("Prepared query is closed");
2318
- return markExternalizationState(prepared.execute(), outputNeedsExternalization);
2415
+ return markExternalizationState(withColumnDomains(prepared.execute(), outputColumnDomains), outputNeedsExternalization);
2319
2416
  },
2320
2417
  async executeAsync(options) {
2321
2418
  if (closed || prepared === undefined)
2322
2419
  throw new Error("Prepared query is closed");
2323
- return markExternalizationState(await prepared.executeAsync(options), outputNeedsExternalization);
2420
+ return markExternalizationState(withColumnDomains(await prepared.executeAsync(options), outputColumnDomains), outputNeedsExternalization);
2324
2421
  },
2325
2422
  executeBatches(options, consume) {
2326
2423
  if (closed || prepared === undefined)
2327
2424
  throw new Error("Prepared query is closed");
2328
- return prepared.executeBatches(options, (batch) => consume(markExternalizationState(batch, outputNeedsExternalization)));
2425
+ return prepared.executeBatches(options, (batch) => consume(markExternalizationState(withColumnDomains(batch, outputColumnDomains), outputNeedsExternalization)));
2329
2426
  },
2330
2427
  close() {
2331
2428
  if (closed)
@@ -2386,6 +2483,7 @@ function createPreparedRowQuery(plan, tables, memory) {
2386
2483
  options.signal?.throwIfAborted();
2387
2484
  await consume({
2388
2485
  columns: [...result.columns],
2486
+ columnDomains: [...result.columnDomains],
2389
2487
  rows: result.rows.slice(start, start + options.batchRows),
2390
2488
  });
2391
2489
  }
@@ -2485,7 +2583,7 @@ export function combineUnionResults(results, ops) {
2485
2583
  const membership = new Set(next.map(encode));
2486
2584
  combined = dedupe(combined).filter((row) => op === "intersect" ? membership.has(encode(row)) : !membership.has(encode(row)));
2487
2585
  }
2488
- return { columns: [...columns], rows: combined };
2586
+ return { columns: [...columns], columnDomains: [...first.columnDomains], rows: combined };
2489
2587
  }
2490
2588
  function encodeRowKey(columns) {
2491
2589
  return (row) => JSON.stringify(columns.map((name) => {
@@ -2687,9 +2785,17 @@ export function resolveStatementDatetimes(plan, now = new Date()) {
2687
2785
  return plan;
2688
2786
  const iso = dateIsoString(now);
2689
2787
  const values = new Map([
2690
- ["CURRENT_DATE", new Date(`${iso.slice(0, 10)}T00:00:00.000Z`)],
2691
- ["CURRENT_TIMESTAMP", copyDate(now)],
2692
- ["LOCALTIME", iso.slice(11, 19)],
2788
+ [
2789
+ "CURRENT_DATE",
2790
+ {
2791
+ kind: "literal",
2792
+ value: dateDomainValue(iso.slice(0, 10)),
2793
+ internalSqlValue: true,
2794
+ sqlDomain: { kind: "date" },
2795
+ },
2796
+ ],
2797
+ ["CURRENT_TIMESTAMP", { kind: "literal", value: copyDate(now) }],
2798
+ ["LOCALTIME", { kind: "literal", value: iso.slice(11, 19) }],
2693
2799
  ]);
2694
2800
  const resolved = clonePlanTree(plan);
2695
2801
  const substitute = (expression) => {
@@ -2698,7 +2804,12 @@ export function resolveStatementDatetimes(plan, now = new Date()) {
2698
2804
  return expression;
2699
2805
  }
2700
2806
  if (expression.kind === "call" && values.has(expression.name)) {
2701
- return { kind: "literal", value: values.get(expression.name) ?? null };
2807
+ const value = values.get(expression.name);
2808
+ return value === undefined
2809
+ ? { kind: "literal", value: null }
2810
+ : value.kind === "literal"
2811
+ ? { ...value }
2812
+ : value;
2702
2813
  }
2703
2814
  if (expression.kind === "window") {
2704
2815
  // A window's own clauses are positions mapChildExpressions cannot rebuild; the plan is
@@ -3273,6 +3384,7 @@ export function applyWindowFunctions(result, windows, options = {}) {
3273
3384
  }
3274
3385
  return {
3275
3386
  columns: [...result.columns, ...windows.map((window) => window.alias)],
3387
+ columnDomains: [...result.columnDomains, ...windows.map(() => null)],
3276
3388
  rows,
3277
3389
  };
3278
3390
  }
@@ -3405,10 +3517,10 @@ function executeRowQueryInternal(plan, tables, memory) {
3405
3517
  // Only a wildcard select needs the source shapes: every other select resolves against its
3406
3518
  // own output aliases.
3407
3519
  const orderSources = plan.select[0]?.expression.kind === "wildcard"
3408
- ? [plan.base, ...plan.joins].map((source) => ({
3409
- alias: source.alias,
3410
- columns: rowTableColumnNames(tables.get(source.table) ?? []),
3411
- }))
3520
+ ? [plan.base, ...plan.joins].flatMap((source) => {
3521
+ const columns = rowTableColumnNames(tables.get(source.table) ?? []).filter((name) => !name.startsWith("\0"));
3522
+ return columns.length === 0 ? [] : [{ alias: source.alias, columns }];
3523
+ })
3412
3524
  : [];
3413
3525
  const sortColumns = plan.orderBy.map(({ expression, direction, nulls }) => ({
3414
3526
  outputName: orderOutputName(expression, plan.select, orderSources),
@@ -3435,7 +3547,24 @@ function executeRowQueryInternal(plan, tables, memory) {
3435
3547
  const columns = plan.select[0]?.expression.kind === "wildcard"
3436
3548
  ? Object.keys(rows[0] ?? {})
3437
3549
  : plan.select.map((item) => item.alias);
3438
- return ties.trim({ columns, rows });
3550
+ const schemas = new Map([...tables].map(([name, tableRows]) => {
3551
+ const names = [...new Set(tableRows.flatMap((row) => Object.keys(row)))];
3552
+ return [
3553
+ name,
3554
+ names.map((columnName) => {
3555
+ const value = tableRows.map((row) => row[columnName]).find((entry) => entry !== null);
3556
+ const type = value instanceof Date
3557
+ ? "datetime"
3558
+ : typeof value === "boolean"
3559
+ ? "boolean"
3560
+ : typeof value === "number"
3561
+ ? "number"
3562
+ : "string";
3563
+ return { name: columnName, type };
3564
+ }),
3565
+ ];
3566
+ }));
3567
+ return ties.trim({ columns, columnDomains: inferResultColumnDomains(plan, schemas), rows });
3439
3568
  }
3440
3569
  /** Matches the vector executor's deliberately inexpensive modeled result-payload accounting. */
3441
3570
  function rowQueryPayloadBytes(row) {
@@ -3574,11 +3703,10 @@ function isSqlJoinKey(value) {
3574
3703
  }
3575
3704
  function project(select, context, group) {
3576
3705
  if (select[0]?.expression.kind === "wildcard") {
3577
- const aliases = Object.keys(context);
3578
- return Object.fromEntries(aliases.flatMap((alias) => Object.entries(context[alias] ?? {}).map(([name, value]) => [
3579
- aliases.length === 1 ? name : `${alias}.${name}`,
3580
- value,
3581
- ])));
3706
+ const aliases = Object.keys(context).filter((alias) => Object.keys(context[alias] ?? {}).some((name) => !name.startsWith("\0")));
3707
+ return Object.fromEntries(aliases.flatMap((alias) => Object.entries(context[alias] ?? {})
3708
+ .filter(([name]) => !name.startsWith("\0"))
3709
+ .map(([name, value]) => [aliases.length === 1 ? name : `${alias}.${name}`, value])));
3582
3710
  }
3583
3711
  return Object.fromEntries(select.map((item) => [item.alias, asQueryValue(evaluate(item.expression, context, group))]));
3584
3712
  }
@@ -3749,7 +3877,7 @@ function evaluate(expression, context, group) {
3749
3877
  if (expression.name === "JSON_ARRAYAGG") {
3750
3878
  return values.length === 0
3751
3879
  ? null
3752
- : JSON.stringify(values.map((value) => jsonValueOf(value ?? null)));
3880
+ : preservedJsonDomainValue(jsonConstructor("JSON_ARRAY", values));
3753
3881
  }
3754
3882
  if (expression.name === "MIN")
3755
3883
  return values.reduce((best, value) => (best === undefined || compareValues(value, best) < 0 ? value : best), undefined);
@@ -4084,7 +4212,7 @@ export function evaluateBooleanExpression(expression, evaluateValue) {
4084
4212
  return value;
4085
4213
  throw new TypeError("Boolean conditions require boolean operands");
4086
4214
  }
4087
- function comparisonHolds(operator, leftValue, rightValue) {
4215
+ export function comparisonHolds(operator, leftValue, rightValue) {
4088
4216
  if (operator === "IS NULL")
4089
4217
  return leftValue === null || leftValue === undefined;
4090
4218
  if (operator === "IS NOT NULL")
@@ -4247,7 +4375,13 @@ export function expressionAliases(expression) {
4247
4375
  return new Set(expressionColumns(expression).flatMap((reference) => reference.includes(".") ? [reference.split(".")[0] ?? ""] : []));
4248
4376
  }
4249
4377
  function comparable(value) {
4250
- return value instanceof Date ? dateMilliseconds(value) : value;
4378
+ if (value instanceof Date)
4379
+ return dateMilliseconds(value);
4380
+ if (isDateDomainValue(value)) {
4381
+ const external = externalSqlDomainValue(value);
4382
+ return typeof external === "string" ? Date.parse(`${external}T00:00:00.000Z`) : value;
4383
+ }
4384
+ return value;
4251
4385
  }
4252
4386
  /**
4253
4387
  * Resolves NULL placement for one order term. An omitted placement follows PostgreSQL: NULLS LAST
@@ -4333,7 +4467,9 @@ export function externalizeQueryResult(result) {
4333
4467
  }
4334
4468
  rows[rowIndex] = output;
4335
4469
  }
4336
- const externalized = changed ? { columns: [...result.columns], rows } : result;
4470
+ const externalized = changed
4471
+ ? { columns: [...result.columns], columnDomains: [...result.columnDomains], rows }
4472
+ : result;
4337
4473
  // Public TEXT is allowed to begin with the private tag prefix, so externalization is not
4338
4474
  // byte-idempotent. Record the boundary crossing: a wrapper such as executeQuery(query()) must
4339
4475
  // not interpret the now-public bytes a second time.
@@ -4727,6 +4863,7 @@ class Parser {
4727
4863
  const columnType = this.#columnType();
4728
4864
  let nullable = true;
4729
4865
  let defaultValue;
4866
+ let generatedValue;
4730
4867
  for (;;) {
4731
4868
  if (this.#isKeyword("DEFAULT")) {
4732
4869
  // PostgreSQL-compatible variable-free scalar expression, retained in the catalog.
@@ -4734,6 +4871,10 @@ class Parser {
4734
4871
  defaultValue = this.#columnDefault();
4735
4872
  continue;
4736
4873
  }
4874
+ if (this.#isKeyword("GENERATED")) {
4875
+ generatedValue = this.#generatedColumn();
4876
+ continue;
4877
+ }
4737
4878
  if (this.#isKeyword("CHECK")) {
4738
4879
  checks.push(this.#checkConstraint(`${table}_${name}_check`));
4739
4880
  continue;
@@ -4779,6 +4920,7 @@ class Parser {
4779
4920
  ...columnType,
4780
4921
  ...(nullable ? { nullable: true } : {}),
4781
4922
  ...(defaultValue === undefined ? {} : { defaultValue }),
4923
+ ...(generatedValue === undefined ? {} : { generatedValue }),
4782
4924
  });
4783
4925
  if (!this.#punctuation(","))
4784
4926
  break;
@@ -4804,6 +4946,23 @@ class Parser {
4804
4946
  }
4805
4947
  }
4806
4948
  }
4949
+ for (const column of columns) {
4950
+ if (column.defaultValue !== undefined && column.generatedValue !== undefined) {
4951
+ throw new TypeError(`Generated column ${column.name} cannot also have a DEFAULT`);
4952
+ }
4953
+ if (column.generatedValue === undefined)
4954
+ continue;
4955
+ for (const reference of expressionColumnNames(compileCheckExpression(column.generatedValue.sql, `generated ${table}.${column.name}`))) {
4956
+ const referencedName = reference.split(".").at(-1) ?? reference;
4957
+ const referenced = columns.find(({ name }) => name === referencedName);
4958
+ if (referenced === undefined) {
4959
+ throw new TypeError(`Generated column ${column.name} refers to a column this table has no: ${referencedName}`);
4960
+ }
4961
+ if (referenced === column || referenced.generatedValue !== undefined) {
4962
+ throw new TypeError(`Generated column ${column.name} cannot reference a generated column: ${referencedName}`);
4963
+ }
4964
+ }
4965
+ }
4807
4966
  // Preserve the released single-UNIQUE row-addressing behavior when no PRIMARY KEY was
4808
4967
  // declared. Additional UNIQUE constraints remain independently enforced secondary keys.
4809
4968
  const promotedUnique = primaryKey === undefined && uniqueConstraints[0]?.columns.length === 1
@@ -4926,6 +5085,23 @@ class Parser {
4926
5085
  throw new TypeError("DEFAULT requires an expression");
4927
5086
  return columnDefaultFor(expression, sql);
4928
5087
  }
5088
+ /** GENERATED [ALWAYS] AS (expression) STORED. */
5089
+ #generatedColumn() {
5090
+ this.#keyword("GENERATED");
5091
+ if (this.#isKeyword("ALWAYS"))
5092
+ this.#keyword("ALWAYS");
5093
+ this.#keyword("AS");
5094
+ const open = this.#peek();
5095
+ this.#expectPunctuation("(");
5096
+ const expression = this.#expression();
5097
+ const close = this.#peek();
5098
+ this.#expectPunctuation(")");
5099
+ this.#keyword("STORED");
5100
+ if (hasAggregate(expression) || containsWindow(expression) || containsParameter(expression)) {
5101
+ throw new TypeError("Generated columns take an immutable row expression");
5102
+ }
5103
+ return { kind: "stored", sql: this.text.slice(open.start + 1, close.start).trim() };
5104
+ }
4929
5105
  /** DROP VIEW [IF EXISTS] name (F031-16). */
4930
5106
  parseDropView() {
4931
5107
  this.#keyword("DROP");
@@ -5060,7 +5236,7 @@ class Parser {
5060
5236
  }
5061
5237
  return `numeric:${precision === undefined ? "" : String(precision)}:${scale === undefined ? "" : String(scale)}`;
5062
5238
  }
5063
- if (["JSON", "JSONB", "UUID", "TIME", "INTERVAL"].includes(word)) {
5239
+ if (["JSON", "JSONB", "UUID", "DATE", "TIME", "INTERVAL"].includes(word)) {
5064
5240
  return word.toLowerCase();
5065
5241
  }
5066
5242
  if (word === "DOUBLE") {
@@ -5099,7 +5275,7 @@ class Parser {
5099
5275
  },
5100
5276
  };
5101
5277
  }
5102
- if (["JSON", "JSONB", "UUID", "TIME", "INTERVAL"].includes(word)) {
5278
+ if (["JSON", "JSONB", "UUID", "DATE", "TIME", "INTERVAL"].includes(word)) {
5103
5279
  return { type: "string", sqlDomain: { kind: word.toLowerCase() } };
5104
5280
  }
5105
5281
  if (word === "DOUBLE") {
@@ -6954,10 +7130,12 @@ class Parser {
6954
7130
  };
6955
7131
  }
6956
7132
  if (upper === "DATE" && this.#peek().kind === "string") {
6957
- const date = new Date(`${this.#take("string").text}T00:00:00.000Z`);
6958
- if (!Number.isFinite(dateMilliseconds(date)))
6959
- throw new TypeError("Invalid DATE literal");
6960
- return { kind: "literal", value: date };
7133
+ return {
7134
+ kind: "literal",
7135
+ value: dateDomainValue(this.#take("string").text),
7136
+ internalSqlValue: true,
7137
+ sqlDomain: { kind: "date" },
7138
+ };
6961
7139
  }
6962
7140
  if ((upper === "TIMESTAMP" || upper === "DATETIME") && this.#peek().kind === "string") {
6963
7141
  return { kind: "literal", value: timestampLiteral(this.#take("string").text) };
@@ -7601,8 +7779,8 @@ class Parser {
7601
7779
  /**
7602
7780
  * GROUPING SETS desugar: one grouped block per set, combined with UNION ALL. A grouped column
7603
7781
  * absent from a member's set projects as NULLIF(expr, expr) — always NULL, but carrying the
7604
- * expression's type through schema inference. The GROUPING() marker function is deliberately
7605
- * unsupported, so rollup NULLs and data NULLs are indistinguishable; the docs call this out.
7782
+ * expression's type through schema inference. GROUPING() becomes one constant bitmask per member
7783
+ * block, distinguishing columns aggregated away by the set from data NULLs.
7606
7784
  */
7607
7785
  function desugarGroupingSets(parts, nextSequence) {
7608
7786
  const { groupingSets, limit, offset, limitParameter, offsetParameter, ...blockParts } = parts;
@@ -7872,16 +8050,17 @@ export function assembleSelectBlock(parts, nextSequence) {
7872
8050
  export function expandDistinctWildcard(plan, columnsOf) {
7873
8051
  if (plan.distinctWildcard !== true)
7874
8052
  return plan;
7875
- const sources = [plan.base, ...plan.joins];
7876
- const multiple = sources.length > 1;
7877
- const select = sources.flatMap((source) => {
7878
- const columns = columnsOf(source.table);
7879
- if (columns === undefined || columns.length === 0) {
8053
+ const shaped = [plan.base, ...plan.joins].map((source) => ({
8054
+ source,
8055
+ columns: sourceWildcardColumns(source, columnsOf),
8056
+ }));
8057
+ const visible = shaped.filter(({ columns }) => (columns?.length ?? 0) > 0);
8058
+ const multiple = visible.length > 1;
8059
+ const select = visible.flatMap(({ source, columns }) => {
8060
+ if (columns === undefined) {
7880
8061
  throw new TypeError(`SELECT DISTINCT * requires known columns for: ${source.table}`);
7881
8062
  }
7882
- return columns
7883
- .filter((name) => !name.startsWith("\0"))
7884
- .map((name) => {
8063
+ return columns.map((name) => {
7885
8064
  const output = multiple ? `${source.alias}.${name}` : name;
7886
8065
  return { expression: { kind: "column", reference: output }, alias: output };
7887
8066
  });
@@ -7977,10 +8156,12 @@ export function withTiesPlan(plan) {
7977
8156
  return { plan, trim: (result) => result };
7978
8157
  if (plan.orderBy.length === 0)
7979
8158
  throw new TypeError("FETCH ... WITH TIES requires ORDER BY");
7980
- const sources = [plan.base, ...plan.joins].map((source) => ({
7981
- alias: source.alias,
7982
- columns: source.derived?.select.map((item) => item.alias) ?? [],
7983
- }));
8159
+ const sources = [plan.base, ...plan.joins].flatMap((source) => {
8160
+ const columns = source.derived?.select
8161
+ .map((item) => item.alias)
8162
+ .filter((name) => !name.startsWith("\0"));
8163
+ return columns?.length === 0 ? [] : [{ alias: source.alias, columns: columns ?? [] }];
8164
+ });
7984
8165
  // orderOutputName throws when a sort key has no output column, which is the same failure the
7985
8166
  // executors report; nothing here has to re-check it.
7986
8167
  const keys = plan.orderBy.map(({ expression }) => orderOutputName(expression, plan.select, sources));
@@ -8160,7 +8341,8 @@ export function expandQualifiedWildcards(plan, columnsOf) {
8160
8341
  const expandBlock = (block) => {
8161
8342
  forEachNestedBlock(block, expandBlock);
8162
8343
  const sources = [block.base, ...block.joins];
8163
- const multiple = sources.length > 1;
8344
+ const multiple = sources.filter((source) => (sourceWildcardColumns(source, columnsOf)?.length ?? 0) > 0)
8345
+ .length > 1;
8164
8346
  block.select = block.select.flatMap((item) => {
8165
8347
  if (item.expression.kind !== "wildcard" || item.expression.table === undefined)
8166
8348
  return [item];
@@ -8250,9 +8432,9 @@ function assembleOrderByExpressionBlock(parts, nextSequence) {
8250
8432
  throw new TypeError("Window functions are only allowed in the select list");
8251
8433
  }
8252
8434
  // A bare literal is almost always a SQL ordinal (ORDER BY 2); sorting by a constant would
8253
- // silently do nothing, so reject it the way the engine always has.
8435
+ // silently do nothing. Valid ordinals have already resolved; a remaining literal is invalid.
8254
8436
  if (order.expression.kind === "literal") {
8255
- throw new TypeError("ORDER BY ordinals are not supported; name the column or alias");
8437
+ throw new TypeError("ORDER BY position is outside the select list");
8256
8438
  }
8257
8439
  }
8258
8440
  const selectSignatures = new Map(parts.select.map((item) => [JSON.stringify(item.expression), item.alias]));
@@ -8338,6 +8520,10 @@ export function transparentProjectionSource(plan) {
8338
8520
  export function projectResultColumns(result, aliases) {
8339
8521
  return copyQueryResultExternalization(result, {
8340
8522
  columns: [...aliases],
8523
+ columnDomains: aliases.map((alias) => {
8524
+ const position = result.columns.indexOf(alias);
8525
+ return position < 0 ? null : (result.columnDomains[position] ?? null);
8526
+ }),
8341
8527
  rows: result.rows.map((row) => {
8342
8528
  const projected = {};
8343
8529
  for (const alias of aliases)