@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.
- package/README.md +5 -5
- package/dist/engine/cancellation.d.ts +2 -0
- package/dist/engine/cancellation.js +4 -0
- package/dist/engine/catalog.d.ts +5 -1
- package/dist/engine/catalog.js +5 -1
- package/dist/engine/client.d.ts +34 -6
- package/dist/engine/client.js +87 -19
- package/dist/engine/database.d.ts +43 -20
- package/dist/engine/database.js +823 -164
- package/dist/engine/defaults.js +11 -0
- package/dist/engine/errors.d.ts +19 -0
- package/dist/engine/errors.js +31 -0
- package/dist/engine/fts.d.ts +2 -15
- package/dist/engine/live.d.ts +1 -7
- package/dist/engine/live.js +12 -13
- package/dist/engine/optimizer.js +546 -39
- package/dist/engine/query-cache.js +1 -0
- package/dist/engine/query.d.ts +16 -278
- package/dist/engine/query.js +260 -74
- package/dist/engine/result-wire.d.ts +2 -0
- package/dist/engine/result-wire.js +21 -5
- package/dist/engine/schema-wire.d.ts +14 -1
- package/dist/engine/schema-wire.js +7 -1
- package/dist/engine/schema.d.ts +83 -32
- package/dist/engine/schema.js +180 -14
- package/dist/engine/sql-domains.d.ts +11 -0
- package/dist/engine/sql-domains.js +65 -1
- package/dist/engine/sql-json.js +22 -3
- package/dist/engine/sql-semantics.js +21 -3
- package/dist/engine/vector.d.ts +2 -2
- package/dist/engine/vector.js +328 -79
- package/dist/engine/worker-host.js +119 -44
- package/dist/plan/index.d.ts +5 -4
- package/dist/plan/index.js +3 -3
- package/dist/plan/model.d.ts +218 -0
- package/dist/plan/model.js +1 -0
- package/dist/storage/indexeddb.js +4 -12
- package/dist/storage/toolkit/record-core.js +7 -22
- package/dist/storage/types.d.ts +26 -8
- package/dist/storage/types.js +85 -0
- package/dist/transactions/index.d.ts +5 -3
- package/dist/transactions/index.js +58 -8
- package/dist/worker-protocol/index.d.ts +6 -1
- package/dist/worker-protocol/index.js +5 -2
- package/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +75 -19
package/dist/engine/query.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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)}`);
|
|
@@ -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
|
-
|
|
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",
|
|
@@ -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.
|
|
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" ||
|
|
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 === "
|
|
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(),
|
|
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,
|
|
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
|
|
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
|
-
...(
|
|
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 =
|
|
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
|
-
[
|
|
2691
|
-
|
|
2692
|
-
|
|
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
|
-
|
|
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].
|
|
3409
|
-
|
|
3410
|
-
columns:
|
|
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
|
-
|
|
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] ?? {})
|
|
3579
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
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.
|
|
7605
|
-
*
|
|
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
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
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].
|
|
7981
|
-
|
|
7982
|
-
|
|
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 >
|
|
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
|
|
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
|
|
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)
|