@minnowdb/core 0.4.1 → 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.
- package/README.md +5 -5
- package/dist/engine/catalog.d.ts +2 -0
- package/dist/engine/catalog.js +4 -1
- package/dist/engine/client.d.ts +3 -3
- package/dist/engine/client.js +6 -5
- package/dist/engine/database.d.ts +20 -6
- package/dist/engine/database.js +312 -95
- package/dist/engine/errors.d.ts +6 -0
- package/dist/engine/errors.js +9 -0
- package/dist/engine/live.js +10 -1
- package/dist/engine/optimizer.js +6 -1
- package/dist/engine/query-cache.js +1 -0
- package/dist/engine/query.d.ts +13 -2
- package/dist/engine/query.js +183 -43
- package/dist/engine/result-wire.d.ts +2 -0
- package/dist/engine/result-wire.js +21 -5
- package/dist/engine/schema-wire.d.ts +7 -0
- package/dist/engine/schema-wire.js +3 -1
- package/dist/engine/schema.d.ts +17 -0
- package/dist/engine/schema.js +42 -7
- package/dist/engine/sql-domains.d.ts +3 -0
- package/dist/engine/sql-domains.js +40 -1
- package/dist/engine/sql-semantics.js +21 -3
- package/dist/engine/vector.js +28 -41
- package/dist/storage/indexeddb.js +4 -12
- package/dist/storage/toolkit/record-core.js +7 -22
- package/dist/storage/types.d.ts +19 -8
- package/dist/storage/types.js +69 -0
- package/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +6 -0
package/dist/engine/errors.d.ts
CHANGED
|
@@ -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;
|
package/dist/engine/errors.js
CHANGED
|
@@ -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;
|
package/dist/engine/live.js
CHANGED
|
@@ -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 {
|
|
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. */
|
package/dist/engine/optimizer.js
CHANGED
|
@@ -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
|
-
:
|
|
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) {
|
package/dist/engine/query.d.ts
CHANGED
|
@@ -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,
|
|
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[];
|
package/dist/engine/query.js
CHANGED
|
@@ -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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
628
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
647
|
-
|
|
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(
|
|
650
|
-
const month = dateUtcMonth(
|
|
651
|
-
const day = dateUtcDate(
|
|
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(
|
|
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(
|
|
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(
|
|
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" ||
|
|
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 === "
|
|
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(),
|
|
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,
|
|
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
|
|
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
|
-
...(
|
|
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 =
|
|
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
|
-
[
|
|
2691
|
-
|
|
2692
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
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
|
}
|