@minnowdb/core 0.4.0 → 0.5.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.
@@ -3,6 +3,12 @@
3
3
  * them (name, fields, instanceof) without bundling the executor.
4
4
  */
5
5
  type ErrorValue = boolean | number | string | Date;
6
+ /** A database operation named a table that is not present in the current catalog. */
7
+ export declare class UnknownTableError extends TypeError {
8
+ readonly tableName: string;
9
+ readonly name = "UnknownTableError";
10
+ constructor(tableName: string);
11
+ }
6
12
  export declare class UniqueConstraintError extends Error {
7
13
  readonly tableName: string;
8
14
  readonly columnName: string;
@@ -5,6 +5,15 @@
5
5
  function formatValue(value) {
6
6
  return value instanceof Date ? dateIsoString(value) : String(value);
7
7
  }
8
+ /** A database operation named a table that is not present in the current catalog. */
9
+ export class UnknownTableError extends TypeError {
10
+ tableName;
11
+ name = "UnknownTableError";
12
+ constructor(tableName) {
13
+ super(`Unknown table: ${tableName}`);
14
+ this.tableName = tableName;
15
+ }
16
+ }
8
17
  export class UniqueConstraintError extends Error {
9
18
  tableName;
10
19
  columnName;
@@ -37,6 +37,8 @@ function digestResult(result) {
37
37
  };
38
38
  for (const column of result.columns)
39
39
  mixString(column);
40
+ for (const domain of result.columnDomains)
41
+ mixString(JSON.stringify(domain));
40
42
  for (const row of result.rows) {
41
43
  for (const column of result.columns) {
42
44
  const value = row[column] ?? null;
@@ -76,6 +78,9 @@ function sameResult(left, right) {
76
78
  for (let index = 0; index < left.columns.length; index += 1) {
77
79
  if (left.columns[index] !== right.columns[index])
78
80
  return false;
81
+ if (JSON.stringify(left.columnDomains[index]) !== JSON.stringify(right.columnDomains[index])) {
82
+ return false;
83
+ }
79
84
  }
80
85
  for (let rowIndex = 0; rowIndex < left.rows.length; rowIndex += 1) {
81
86
  const leftRow = left.rows[rowIndex];
@@ -109,7 +114,11 @@ function cloneRow(row, columns) {
109
114
  }
110
115
  function cloneResult(result) {
111
116
  const columns = [...result.columns];
112
- return { columns, rows: result.rows.map((row) => cloneRow(row, columns)) };
117
+ return {
118
+ columns,
119
+ columnDomains: structuredClone(result.columnDomains),
120
+ rows: result.rows.map((row) => cloneRow(row, columns)),
121
+ };
113
122
  }
114
123
  /** Type-tagged, length-delimited structural identity. Unlike JSON, it preserves Date vs string,
115
124
  * -0, non-finite numbers, and undefined fields, so deduplication cannot merge distinct plans. */
@@ -906,10 +906,15 @@ function foldExpression(expression) {
906
906
  : targetWord === "json" ||
907
907
  targetWord === "jsonb" ||
908
908
  targetWord === "uuid" ||
909
+ targetWord === "date" ||
909
910
  targetWord === "time" ||
910
911
  targetWord === "interval"
911
912
  ? { kind: targetWord }
912
- : undefined;
913
+ : expression.name === "JSON_QUERY" ||
914
+ expression.name === "JSON_OBJECT" ||
915
+ expression.name === "JSON_ARRAY"
916
+ ? { kind: "json" }
917
+ : undefined;
913
918
  return {
914
919
  kind: "literal",
915
920
  value: folded,
@@ -38,6 +38,7 @@ export function copyQueryResult(result) {
38
38
  const columns = result.columns;
39
39
  const copy = {
40
40
  columns: [...columns],
41
+ columnDomains: structuredClone(result.columnDomains),
41
42
  rows: result.rows.map((row) => {
42
43
  const copy = { ...row };
43
44
  for (const name of columns) {
@@ -7,8 +7,12 @@ export type QueryValue = boolean | number | string | Date | null;
7
7
  export type QueryRow = Record<string, QueryValue>;
8
8
  export interface QueryResult {
9
9
  columns: string[];
10
+ /** Logical SQL domain for each output column, positionally aligned with `columns`. */
11
+ columnDomains: Array<SqlDomain | null>;
10
12
  rows: QueryRow[];
11
13
  }
14
+ /** Domain metadata for an execution path that has no catalog-backed type information. */
15
+ export declare function unknownColumnDomains(columns: readonly string[]): null[];
12
16
  export interface PreparedQuery {
13
17
  readonly sql: string;
14
18
  readonly tables: string[];
@@ -72,7 +76,7 @@ export declare function intervalLiteral(text: string): {
72
76
  * does not exist in the target month clamps to that month's last day, which is what both SQLite
73
77
  * and PostgreSQL do with 31 January plus a month.
74
78
  */
75
- export declare function dateAddValue(value: unknown, months: unknown, milliseconds: unknown): Date | null;
79
+ export declare function dateAddValue(value: unknown, months: unknown, milliseconds: unknown): Date | string | null;
76
80
  export declare function dateTruncValue(unit: unknown, value: unknown): Date | null;
77
81
  export type Expression = {
78
82
  kind: "literal";
@@ -387,6 +391,8 @@ export interface ForeignKeyDefinition {
387
391
  parentColumn?: string;
388
392
  parentColumns?: string[];
389
393
  onDelete: "restrict" | "cascade" | "set null";
394
+ /** False is reserved for schema/catalog declarations; SQL-created keys are enforced. */
395
+ enforced?: boolean;
390
396
  }
391
397
  export interface UniqueConstraintDefinition {
392
398
  name: string;
@@ -687,15 +693,19 @@ export interface SqlColumnSchema {
687
693
  * derived table always has concrete column types even when its result is empty.
688
694
  */
689
695
  export declare function inferBlockSchema(plan: CompiledQuery, schemas: ReadonlyMap<string, readonly SqlColumnSchema[]>): SqlColumnSchema[];
696
+ /** Best-effort logical domains for a public result without making execution a new typecheck. */
697
+ export declare function inferResultColumnDomains(plan: CompiledQuery, schemas: ReadonlyMap<string, readonly SqlColumnSchema[]>): Array<SqlDomain | null>;
690
698
  /** Whether a final result can contain one of the engine's tagged string-domain values. */
691
699
  export declare function queryResultNeedsExternalization(plan: CompiledQuery, schemas: ReadonlyMap<string, readonly SqlColumnSchema[]>): boolean;
692
700
  export declare function referencedColumns(plan: CompiledQuery, schemas: ReadonlyMap<string, readonly string[]>): Map<string, string[]>;
693
701
  export declare function createPreparedQuery(plan: CompiledQuery, tables: ReadonlyMap<string, DatabaseRow[]>, options?: QueryExecutionOptions): PreparedQuery;
694
702
  /** Internal columnar entry point used after MinnowDatabase materializes a stable snapshot. */
695
- export declare function createPreparedColumnarQuery(plan: CompiledQuery, tables: ReadonlyMap<string, ColumnarTable>, memory?: QueryMemoryContext, options?: {
703
+ export declare function createPreparedColumnarQuery(plan: CompiledQuery, tables: ReadonlyMap<string, ColumnarTable>, memory?: QueryMemoryContext, preparedOptions?: {
696
704
  ftsStats?: ReadonlyMap<string, FtsStats>;
697
705
  /** False only when catalog-backed schema inference proves every output is already public. */
698
706
  outputNeedsExternalization?: boolean;
707
+ /** Catalog-inferred logical domains, positionally aligned with the visible output. */
708
+ outputColumnDomains?: ReadonlyArray<SqlDomain | null>;
699
709
  }): PreparedQuery;
700
710
  export declare function executeQuery(plan: CompiledQuery, tables: ReadonlyMap<string, DatabaseRow[]>, options?: QueryExecutionOptions): QueryResult;
701
711
  /**
@@ -843,6 +853,7 @@ export declare function distinctFromComparison(left: unknown, right: unknown): b
843
853
  * Leaf values come from the executor-specific callback so both executors share these semantics.
844
854
  */
845
855
  export declare function evaluateBooleanExpression(expression: Expression, evaluateValue: (expression: Expression) => unknown): boolean | null;
856
+ export declare function comparisonHolds(operator: PredicateOperator, leftValue: unknown, rightValue: unknown): boolean;
846
857
  export declare function hasAggregate(expression: Expression): boolean;
847
858
  /** The column references inside one expression; a subquery contributes none (its own scope). */
848
859
  export declare function expressionColumnNames(expression: Expression): string[];
@@ -9,8 +9,19 @@ import { stringArgument } from "./sql-semantics.js";
9
9
  import { jsonAtPath, jsonConstructor, jsonIsValid, jsonValueOf, 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, 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)}`);
@@ -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",
@@ -1797,7 +1827,11 @@ export function inferBlockSchema(plan, schemas) {
1797
1827
  ...(scale === undefined ? {} : { scale }),
1798
1828
  };
1799
1829
  }
1800
- if (target === "json" || target === "jsonb" || target === "uuid" || target === "time") {
1830
+ if (target === "json" ||
1831
+ target === "jsonb" ||
1832
+ target === "uuid" ||
1833
+ target === "date" ||
1834
+ target === "time") {
1801
1835
  return { kind: target };
1802
1836
  }
1803
1837
  if (target === "interval")
@@ -1825,12 +1859,32 @@ export function inferBlockSchema(plan, schemas) {
1825
1859
  }
1826
1860
  if (expression.kind !== "call")
1827
1861
  return undefined;
1862
+ if (expression.name === "CURRENT_DATE")
1863
+ return { kind: "date" };
1828
1864
  if (expression.name === "CAST") {
1829
1865
  const target = expression.arguments[1];
1830
1866
  return target?.kind === "literal" && typeof target.value === "string"
1831
1867
  ? castDomain(target.value)
1832
1868
  : undefined;
1833
1869
  }
1870
+ if (expression.name === "JSON_ARRAYAGG" ||
1871
+ expression.name === "JSON_QUERY" ||
1872
+ expression.name === "JSON_OBJECT" ||
1873
+ expression.name === "JSON_ARRAY") {
1874
+ return { kind: "json" };
1875
+ }
1876
+ if (expression.name === "GEN_RANDOM_UUID")
1877
+ return { kind: "uuid" };
1878
+ if (expression.name === "DATE_ADD") {
1879
+ const input = inferDomain(expression.arguments[0] ?? { kind: "literal", value: null });
1880
+ const milliseconds = expression.arguments[2];
1881
+ return input?.kind === "date" &&
1882
+ milliseconds?.kind === "literal" &&
1883
+ typeof milliseconds.value === "number" &&
1884
+ milliseconds.value % 86_400_000 === 0
1885
+ ? input
1886
+ : undefined;
1887
+ }
1834
1888
  if (expression.name === "SUM" ||
1835
1889
  expression.name === "AVG" ||
1836
1890
  expression.name === "MIN" ||
@@ -1963,9 +2017,13 @@ export function inferBlockSchema(plan, schemas) {
1963
2017
  return "boolean";
1964
2018
  if (expression.name === "DATE_TRUNC")
1965
2019
  return "datetime";
1966
- if (expression.name === "CURRENT_DATE" || expression.name === "CURRENT_TIMESTAMP") {
1967
- return "datetime";
2020
+ if (expression.name === "DATE_ADD") {
2021
+ return inferDomain(expression)?.kind === "date" ? "string" : "datetime";
1968
2022
  }
2023
+ if (expression.name === "CURRENT_DATE")
2024
+ return "string";
2025
+ if (expression.name === "CURRENT_TIMESTAMP")
2026
+ return "datetime";
1969
2027
  // LOCALTIME is the statement clock's canonical 'HH:MM:SS' text; explicit TIME values use
1970
2028
  // the same public representation while carrying a logical domain internally.
1971
2029
  if (expression.name === "LOCALTIME")
@@ -2062,6 +2120,22 @@ export function inferBlockSchema(plan, schemas) {
2062
2120
  ];
2063
2121
  });
2064
2122
  }
2123
+ /** Best-effort logical domains for a public result without making execution a new typecheck. */
2124
+ export function inferResultColumnDomains(plan, schemas) {
2125
+ try {
2126
+ return inferBlockSchema(plan, schemas).map(({ sqlDomain }) => sqlDomain ?? null);
2127
+ }
2128
+ catch {
2129
+ return plan.select.flatMap((item) => {
2130
+ try {
2131
+ return inferBlockSchema({ ...plan, select: [item] }, schemas).map(({ sqlDomain }) => sqlDomain ?? null);
2132
+ }
2133
+ catch {
2134
+ return [null];
2135
+ }
2136
+ });
2137
+ }
2138
+ }
2065
2139
  /** Whether a final result can contain one of the engine's tagged string-domain values. */
2066
2140
  export function queryResultNeedsExternalization(plan, schemas) {
2067
2141
  // This is only a result-boundary optimization flag, not a second typecheck. In particular,
@@ -2080,7 +2154,7 @@ export function queryResultNeedsExternalization(plan, schemas) {
2080
2154
  if (target?.kind === "literal" &&
2081
2155
  typeof target.value === "string" &&
2082
2156
  (target.value.startsWith("numeric:") ||
2083
- ["json", "jsonb", "uuid", "time", "interval"].includes(target.value))) {
2157
+ ["json", "jsonb", "uuid", "date", "time", "interval"].includes(target.value))) {
2084
2158
  return true;
2085
2159
  }
2086
2160
  }
@@ -2278,15 +2352,17 @@ export function createPreparedQuery(plan, tables, options = {}) {
2278
2352
  }
2279
2353
  }
2280
2354
  /** Internal columnar entry point used after MinnowDatabase materializes a stable snapshot. */
2281
- export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemoryContext(), options = {}) {
2355
+ export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemoryContext(), preparedOptions = {}) {
2282
2356
  plan = resolveStatementDatetimes(plan);
2283
2357
  const ties = withTiesPlan(plan);
2284
2358
  if (ties.plan !== plan) {
2285
- return trimPreparedResults(createPreparedColumnarQuery(ties.plan, tables, memory, options), ties.trim);
2359
+ return trimPreparedResults(createPreparedColumnarQuery(ties.plan, tables, memory, preparedOptions), ties.trim);
2286
2360
  }
2287
2361
  const columnarColumns = (tableName) => {
2288
2362
  const table = tables.get(tableName);
2289
- return table === undefined ? undefined : [...table.columns.keys()];
2363
+ return table === undefined
2364
+ ? undefined
2365
+ : [...table.columns.keys()].filter((name) => !name.startsWith("\0"));
2290
2366
  };
2291
2367
  plan = expandSourceColumnAliases(plan, columnarColumns);
2292
2368
  plan = expandNaturalJoins(plan, columnarColumns);
@@ -2298,14 +2374,32 @@ export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemo
2298
2374
  try {
2299
2375
  prepared = prepareVectorQuery(plan, tables, {
2300
2376
  memoryContext: memory,
2301
- ...(options.ftsStats === undefined ? {} : { ftsStats: options.ftsStats }),
2377
+ ...(preparedOptions.ftsStats === undefined ? {} : { ftsStats: preparedOptions.ftsStats }),
2302
2378
  });
2303
2379
  }
2304
2380
  catch (error) {
2305
2381
  memory.close();
2306
2382
  throw error;
2307
2383
  }
2308
- const outputNeedsExternalization = options.outputNeedsExternalization;
2384
+ const outputNeedsExternalization = preparedOptions.outputNeedsExternalization;
2385
+ let outputColumnDomains = preparedOptions.outputColumnDomains;
2386
+ if (outputColumnDomains === undefined) {
2387
+ try {
2388
+ const schemas = new Map([...tables].map(([name, table]) => [
2389
+ name,
2390
+ [...table.columns].map(([columnName, vector]) => ({
2391
+ name: columnName,
2392
+ type: vector.kind,
2393
+ })),
2394
+ ]));
2395
+ outputColumnDomains = inferResultColumnDomains(plan, schemas);
2396
+ }
2397
+ catch {
2398
+ // This low-level schema-less entry point may not have enough source information. Known
2399
+ // expression domains (including JSON constructors) are still inferred whenever it does.
2400
+ outputColumnDomains = undefined;
2401
+ }
2402
+ }
2309
2403
  return {
2310
2404
  sql: plan.sql,
2311
2405
  tables: [plan.base.table, ...plan.joins.map((join) => join.table)],
@@ -2315,17 +2409,17 @@ export function createPreparedColumnarQuery(plan, tables, memory = new QueryMemo
2315
2409
  execute() {
2316
2410
  if (closed || prepared === undefined)
2317
2411
  throw new Error("Prepared query is closed");
2318
- return markExternalizationState(prepared.execute(), outputNeedsExternalization);
2412
+ return markExternalizationState(withColumnDomains(prepared.execute(), outputColumnDomains), outputNeedsExternalization);
2319
2413
  },
2320
2414
  async executeAsync(options) {
2321
2415
  if (closed || prepared === undefined)
2322
2416
  throw new Error("Prepared query is closed");
2323
- return markExternalizationState(await prepared.executeAsync(options), outputNeedsExternalization);
2417
+ return markExternalizationState(withColumnDomains(await prepared.executeAsync(options), outputColumnDomains), outputNeedsExternalization);
2324
2418
  },
2325
2419
  executeBatches(options, consume) {
2326
2420
  if (closed || prepared === undefined)
2327
2421
  throw new Error("Prepared query is closed");
2328
- return prepared.executeBatches(options, (batch) => consume(markExternalizationState(batch, outputNeedsExternalization)));
2422
+ return prepared.executeBatches(options, (batch) => consume(markExternalizationState(withColumnDomains(batch, outputColumnDomains), outputNeedsExternalization)));
2329
2423
  },
2330
2424
  close() {
2331
2425
  if (closed)
@@ -2386,6 +2480,7 @@ function createPreparedRowQuery(plan, tables, memory) {
2386
2480
  options.signal?.throwIfAborted();
2387
2481
  await consume({
2388
2482
  columns: [...result.columns],
2483
+ columnDomains: [...result.columnDomains],
2389
2484
  rows: result.rows.slice(start, start + options.batchRows),
2390
2485
  });
2391
2486
  }
@@ -2485,7 +2580,7 @@ export function combineUnionResults(results, ops) {
2485
2580
  const membership = new Set(next.map(encode));
2486
2581
  combined = dedupe(combined).filter((row) => op === "intersect" ? membership.has(encode(row)) : !membership.has(encode(row)));
2487
2582
  }
2488
- return { columns: [...columns], rows: combined };
2583
+ return { columns: [...columns], columnDomains: [...first.columnDomains], rows: combined };
2489
2584
  }
2490
2585
  function encodeRowKey(columns) {
2491
2586
  return (row) => JSON.stringify(columns.map((name) => {
@@ -2687,9 +2782,17 @@ export function resolveStatementDatetimes(plan, now = new Date()) {
2687
2782
  return plan;
2688
2783
  const iso = dateIsoString(now);
2689
2784
  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)],
2785
+ [
2786
+ "CURRENT_DATE",
2787
+ {
2788
+ kind: "literal",
2789
+ value: dateDomainValue(iso.slice(0, 10)),
2790
+ internalSqlValue: true,
2791
+ sqlDomain: { kind: "date" },
2792
+ },
2793
+ ],
2794
+ ["CURRENT_TIMESTAMP", { kind: "literal", value: copyDate(now) }],
2795
+ ["LOCALTIME", { kind: "literal", value: iso.slice(11, 19) }],
2693
2796
  ]);
2694
2797
  const resolved = clonePlanTree(plan);
2695
2798
  const substitute = (expression) => {
@@ -2698,7 +2801,12 @@ export function resolveStatementDatetimes(plan, now = new Date()) {
2698
2801
  return expression;
2699
2802
  }
2700
2803
  if (expression.kind === "call" && values.has(expression.name)) {
2701
- return { kind: "literal", value: values.get(expression.name) ?? null };
2804
+ const value = values.get(expression.name);
2805
+ return value === undefined
2806
+ ? { kind: "literal", value: null }
2807
+ : value.kind === "literal"
2808
+ ? { ...value }
2809
+ : value;
2702
2810
  }
2703
2811
  if (expression.kind === "window") {
2704
2812
  // A window's own clauses are positions mapChildExpressions cannot rebuild; the plan is
@@ -3273,6 +3381,7 @@ export function applyWindowFunctions(result, windows, options = {}) {
3273
3381
  }
3274
3382
  return {
3275
3383
  columns: [...result.columns, ...windows.map((window) => window.alias)],
3384
+ columnDomains: [...result.columnDomains, ...windows.map(() => null)],
3276
3385
  rows,
3277
3386
  };
3278
3387
  }
@@ -3435,7 +3544,24 @@ function executeRowQueryInternal(plan, tables, memory) {
3435
3544
  const columns = plan.select[0]?.expression.kind === "wildcard"
3436
3545
  ? Object.keys(rows[0] ?? {})
3437
3546
  : plan.select.map((item) => item.alias);
3438
- return ties.trim({ columns, rows });
3547
+ const schemas = new Map([...tables].map(([name, tableRows]) => {
3548
+ const names = [...new Set(tableRows.flatMap((row) => Object.keys(row)))];
3549
+ return [
3550
+ name,
3551
+ names.map((columnName) => {
3552
+ const value = tableRows.map((row) => row[columnName]).find((entry) => entry !== null);
3553
+ const type = value instanceof Date
3554
+ ? "datetime"
3555
+ : typeof value === "boolean"
3556
+ ? "boolean"
3557
+ : typeof value === "number"
3558
+ ? "number"
3559
+ : "string";
3560
+ return { name: columnName, type };
3561
+ }),
3562
+ ];
3563
+ }));
3564
+ return ties.trim({ columns, columnDomains: inferResultColumnDomains(plan, schemas), rows });
3439
3565
  }
3440
3566
  /** Matches the vector executor's deliberately inexpensive modeled result-payload accounting. */
3441
3567
  function rowQueryPayloadBytes(row) {
@@ -4084,7 +4210,7 @@ export function evaluateBooleanExpression(expression, evaluateValue) {
4084
4210
  return value;
4085
4211
  throw new TypeError("Boolean conditions require boolean operands");
4086
4212
  }
4087
- function comparisonHolds(operator, leftValue, rightValue) {
4213
+ export function comparisonHolds(operator, leftValue, rightValue) {
4088
4214
  if (operator === "IS NULL")
4089
4215
  return leftValue === null || leftValue === undefined;
4090
4216
  if (operator === "IS NOT NULL")
@@ -4247,7 +4373,13 @@ export function expressionAliases(expression) {
4247
4373
  return new Set(expressionColumns(expression).flatMap((reference) => reference.includes(".") ? [reference.split(".")[0] ?? ""] : []));
4248
4374
  }
4249
4375
  function comparable(value) {
4250
- return value instanceof Date ? dateMilliseconds(value) : value;
4376
+ if (value instanceof Date)
4377
+ return dateMilliseconds(value);
4378
+ if (isDateDomainValue(value)) {
4379
+ const external = externalSqlDomainValue(value);
4380
+ return typeof external === "string" ? Date.parse(`${external}T00:00:00.000Z`) : value;
4381
+ }
4382
+ return value;
4251
4383
  }
4252
4384
  /**
4253
4385
  * Resolves NULL placement for one order term. An omitted placement follows PostgreSQL: NULLS LAST
@@ -4333,7 +4465,9 @@ export function externalizeQueryResult(result) {
4333
4465
  }
4334
4466
  rows[rowIndex] = output;
4335
4467
  }
4336
- const externalized = changed ? { columns: [...result.columns], rows } : result;
4468
+ const externalized = changed
4469
+ ? { columns: [...result.columns], columnDomains: [...result.columnDomains], rows }
4470
+ : result;
4337
4471
  // Public TEXT is allowed to begin with the private tag prefix, so externalization is not
4338
4472
  // byte-idempotent. Record the boundary crossing: a wrapper such as executeQuery(query()) must
4339
4473
  // not interpret the now-public bytes a second time.
@@ -5060,7 +5194,7 @@ class Parser {
5060
5194
  }
5061
5195
  return `numeric:${precision === undefined ? "" : String(precision)}:${scale === undefined ? "" : String(scale)}`;
5062
5196
  }
5063
- if (["JSON", "JSONB", "UUID", "TIME", "INTERVAL"].includes(word)) {
5197
+ if (["JSON", "JSONB", "UUID", "DATE", "TIME", "INTERVAL"].includes(word)) {
5064
5198
  return word.toLowerCase();
5065
5199
  }
5066
5200
  if (word === "DOUBLE") {
@@ -5099,7 +5233,7 @@ class Parser {
5099
5233
  },
5100
5234
  };
5101
5235
  }
5102
- if (["JSON", "JSONB", "UUID", "TIME", "INTERVAL"].includes(word)) {
5236
+ if (["JSON", "JSONB", "UUID", "DATE", "TIME", "INTERVAL"].includes(word)) {
5103
5237
  return { type: "string", sqlDomain: { kind: word.toLowerCase() } };
5104
5238
  }
5105
5239
  if (word === "DOUBLE") {
@@ -6954,10 +7088,12 @@ class Parser {
6954
7088
  };
6955
7089
  }
6956
7090
  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 };
7091
+ return {
7092
+ kind: "literal",
7093
+ value: dateDomainValue(this.#take("string").text),
7094
+ internalSqlValue: true,
7095
+ sqlDomain: { kind: "date" },
7096
+ };
6961
7097
  }
6962
7098
  if ((upper === "TIMESTAMP" || upper === "DATETIME") && this.#peek().kind === "string") {
6963
7099
  return { kind: "literal", value: timestampLiteral(this.#take("string").text) };
@@ -8338,6 +8474,10 @@ export function transparentProjectionSource(plan) {
8338
8474
  export function projectResultColumns(result, aliases) {
8339
8475
  return copyQueryResultExternalization(result, {
8340
8476
  columns: [...aliases],
8477
+ columnDomains: aliases.map((alias) => {
8478
+ const position = result.columns.indexOf(alias);
8479
+ return position < 0 ? null : (result.columnDomains[position] ?? null);
8480
+ }),
8341
8481
  rows: result.rows.map((row) => {
8342
8482
  const projected = {};
8343
8483
  for (const alias of aliases)
@@ -1,3 +1,4 @@
1
+ import type { SqlDomain } from "../storage/types.js";
1
2
  import type { QueryResult, QueryRow, QueryValue } from "./query.js";
2
3
  /**
3
4
  * The shape a query result takes on the worker channel. A result is rows of objects at the
@@ -49,6 +50,7 @@ export type WireResultColumn = {
49
50
  export interface WireQueryResult {
50
51
  readonly kind: "columnar-result";
51
52
  readonly columns: string[];
53
+ readonly columnDomains: Array<SqlDomain | null>;
52
54
  readonly rowCount: number;
53
55
  readonly values: WireResultColumn[];
54
56
  }