@minnowdb/core 0.6.4 → 0.6.6
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/dist/engine/artifact-cache.d.ts +2 -1
- package/dist/engine/batch.d.ts +0 -1
- package/dist/engine/batch.js +1 -1
- package/dist/engine/buffered-writer.js +1 -12
- package/dist/engine/byte-estimates.d.ts +11 -0
- package/dist/engine/byte-estimates.js +25 -0
- package/dist/engine/database.js +78 -38
- package/dist/engine/fts.d.ts +0 -1
- package/dist/engine/fts.js +1 -1
- package/dist/engine/join-index.d.ts +0 -1
- package/dist/engine/optimizer.js +11 -3
- package/dist/engine/point-read.d.ts +1 -1
- package/dist/engine/point-read.js +3 -2
- package/dist/engine/query-cache.js +1 -12
- package/dist/engine/query.d.ts +20 -59
- package/dist/engine/query.js +374 -71
- package/dist/engine/result-wire.d.ts +2 -1
- package/dist/engine/schema.d.ts +18 -2
- package/dist/engine/schema.js +11 -2
- package/dist/engine/sort-keys.d.ts +2 -3
- package/dist/engine/sort-keys.js +1 -1
- package/dist/engine/sql-domains.d.ts +36 -2
- package/dist/engine/sql-domains.js +143 -6
- package/dist/engine/sql-json.d.ts +12 -2
- package/dist/engine/sql-json.js +41 -1
- package/dist/engine/sql-semantics.d.ts +2 -1
- package/dist/engine/vector.d.ts +7 -7
- package/dist/engine/vector.js +8 -4
- package/dist/engine/write-block-planner.d.ts +2 -1
- package/dist/plan/model.d.ts +19 -0
- package/dist/storage/opfs/leader.d.ts +0 -4
- package/dist/storage/opfs/leader.js +2 -2
- package/dist/storage/opfs/snapshot-ledger.d.ts +3 -2
- package/dist/storage/toolkit/index.d.ts +1 -1
- package/dist/storage/toolkit/record-core.d.ts +0 -1
- package/dist/storage/toolkit/record-core.js +0 -7
- package/dist/testing/opfs-shim.d.ts +2 -1
- package/dist/testing/simulator.js +3 -0
- package/dist/worker-protocol/index.d.ts +1 -1
- package/dist/worker-protocol/index.js +0 -1
- package/package.json +2 -1
- package/postgres-feature-profile.json +18 -3
- package/sql-feature-matrix.json +95 -11
package/dist/engine/query.js
CHANGED
|
@@ -6,10 +6,10 @@ 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, parseJsonPath } from "./sql-json.js";
|
|
9
|
+
import { jsonArrowStep, 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, dateDomainValue, exactNumericBinary, exactNumericValue, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, isExactNumeric, isSqlDomainValue, jsonDomainValue, normalizeSqlDomainValue, preservedJsonDomainValue, protectedSqlTextValue, timeDomainValue, uuidDomainValue, } from "./sql-domains.js";
|
|
12
|
+
import { arrayDomainValue, boundedJsonText, collatedDomainValue, concatenatedSqlValue, dateDomainValue, exactNumericAsNumber, exactNumericBinary, exactNumericLiteral, exactNumericValue, externalSqlDomainColumnValue, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, isExactNumeric, isSqlDomainValue, jsonDomainValue, normalizeSqlDomainValue, preservedJsonDomainValue, protectedSqlTextValue, timeDomainValue, uuidDomainValue, } from "./sql-domains.js";
|
|
13
13
|
import { columnarTableFromRows, prepareVectorQuery, } from "./vector.js";
|
|
14
14
|
/** Domain metadata for an execution path that has no catalog-backed type information. */
|
|
15
15
|
export function unknownColumnDomains(columns) {
|
|
@@ -62,6 +62,8 @@ export const scalarFunctionNames = new Set([
|
|
|
62
62
|
"JSON_ARRAY",
|
|
63
63
|
"IS_JSON",
|
|
64
64
|
"ARRAY",
|
|
65
|
+
"MINNOW_JSON_GET",
|
|
66
|
+
"MINNOW_JSON_GET_TEXT",
|
|
65
67
|
"MINNOW_TUPLE_KEY",
|
|
66
68
|
"MINNOW_COLLATE",
|
|
67
69
|
"NEXTVAL",
|
|
@@ -79,7 +81,7 @@ export const volatileScalarFunctionNames = new Set([
|
|
|
79
81
|
* reading, so they are resolved once per execution rather than evaluated per row: every row of
|
|
80
82
|
* one statement sees one instant, both executors agree, and constant folding leaves them alone.
|
|
81
83
|
*/
|
|
82
|
-
|
|
84
|
+
const statementDatetimeNames = new Set([
|
|
83
85
|
"CURRENT_DATE",
|
|
84
86
|
"CURRENT_TIMESTAMP",
|
|
85
87
|
"LOCALTIME",
|
|
@@ -167,16 +169,20 @@ function castValue(value, target) {
|
|
|
167
169
|
return protectedSqlTextValue(dateIsoString(value));
|
|
168
170
|
}
|
|
169
171
|
if (target === "number" || target === "number-integer") {
|
|
172
|
+
// Externalize first, exactly as the string and datetime targets do: a NUMERIC (or other
|
|
173
|
+
// domain) value is an internally tagged string, and CAST(numeric_column AS DOUBLE
|
|
174
|
+
// PRECISION) must read its decimal text, not fail on the tag (T703).
|
|
175
|
+
const external = externalSqlDomainValue(value);
|
|
170
176
|
let parsed;
|
|
171
|
-
if (typeof
|
|
172
|
-
parsed =
|
|
173
|
-
else if (typeof
|
|
174
|
-
parsed =
|
|
175
|
-
else if (typeof
|
|
176
|
-
const text =
|
|
177
|
+
if (typeof external === "number")
|
|
178
|
+
parsed = external;
|
|
179
|
+
else if (typeof external === "boolean")
|
|
180
|
+
parsed = external ? 1 : 0;
|
|
181
|
+
else if (typeof external === "string") {
|
|
182
|
+
const text = external.trim();
|
|
177
183
|
const candidate = text === "" ? Number.NaN : Number(text);
|
|
178
184
|
if (!Number.isFinite(candidate)) {
|
|
179
|
-
throw new TypeError(`Cannot cast this string to a number: ${
|
|
185
|
+
throw new TypeError(`Cannot cast this string to a number: ${text}`);
|
|
180
186
|
}
|
|
181
187
|
parsed = candidate;
|
|
182
188
|
}
|
|
@@ -201,12 +207,13 @@ function castValue(value, target) {
|
|
|
201
207
|
throw new TypeError(`Only 0 and 1 cast to boolean, got ${String(value)}`);
|
|
202
208
|
}
|
|
203
209
|
if (typeof value === "string") {
|
|
204
|
-
const
|
|
210
|
+
const external = externalSqlDomainValue(value);
|
|
211
|
+
const text = typeof external === "string" ? external.trim().toLowerCase() : "";
|
|
205
212
|
if (text === "true" || text === "t" || text === "1")
|
|
206
213
|
return true;
|
|
207
214
|
if (text === "false" || text === "f" || text === "0")
|
|
208
215
|
return false;
|
|
209
|
-
throw new TypeError(`Cannot cast this string to a boolean: ${value}`);
|
|
216
|
+
throw new TypeError(`Cannot cast this string to a boolean: ${typeof external === "string" ? external : value}`);
|
|
210
217
|
}
|
|
211
218
|
}
|
|
212
219
|
if (target === "datetime") {
|
|
@@ -396,6 +403,27 @@ export function scalarFunctionValue(name, values) {
|
|
|
396
403
|
// JSON_QUERY returns JSON text, so a selected string keeps its quotes.
|
|
397
404
|
return preservedJsonDomainValue(JSON.stringify(found.value));
|
|
398
405
|
}
|
|
406
|
+
case "MINNOW_JSON_GET": {
|
|
407
|
+
if (values[1] === null || values[1] === undefined)
|
|
408
|
+
return null;
|
|
409
|
+
const found = jsonArrowStep(first, values[1], "->");
|
|
410
|
+
if (!found.found)
|
|
411
|
+
return null;
|
|
412
|
+
// -> returns a JSON value: a selected string keeps its quotes, a JSON null is the
|
|
413
|
+
// one-character document "null" rather than SQL NULL, exactly as PostgreSQL has it.
|
|
414
|
+
return preservedJsonDomainValue(JSON.stringify(found.value));
|
|
415
|
+
}
|
|
416
|
+
case "MINNOW_JSON_GET_TEXT": {
|
|
417
|
+
if (values[1] === null || values[1] === undefined)
|
|
418
|
+
return null;
|
|
419
|
+
const found = jsonArrowStep(first, values[1], "->>");
|
|
420
|
+
// ->> returns text: strings unquoted, other scalars as their JSON rendering, objects
|
|
421
|
+
// and arrays serialized, and a JSON null as SQL NULL.
|
|
422
|
+
if (!found.found || found.value === null || found.value === undefined)
|
|
423
|
+
return null;
|
|
424
|
+
const value = found.value;
|
|
425
|
+
return protectedSqlTextValue(typeof value === "string" ? value : JSON.stringify(value));
|
|
426
|
+
}
|
|
399
427
|
case "LPAD":
|
|
400
428
|
case "RPAD": {
|
|
401
429
|
if (values[1] === null || values[1] === undefined)
|
|
@@ -579,7 +607,7 @@ function assertReplacementResultLength(source, search, replacement) {
|
|
|
579
607
|
throw new RangeError(`REPLACE result exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
|
|
580
608
|
}
|
|
581
609
|
}
|
|
582
|
-
|
|
610
|
+
const dateTruncUnits = new Set([
|
|
583
611
|
"year",
|
|
584
612
|
"quarter",
|
|
585
613
|
"month",
|
|
@@ -609,7 +637,7 @@ const intervalUnits = new Map([
|
|
|
609
637
|
* rather than converted, because a month is not a fixed number of them: adding one to January 31
|
|
610
638
|
* has to land on the end of February, which only calendar arithmetic can do.
|
|
611
639
|
*/
|
|
612
|
-
|
|
640
|
+
function intervalLiteral(text) {
|
|
613
641
|
let months = 0;
|
|
614
642
|
let milliseconds = 0;
|
|
615
643
|
let matched = 0;
|
|
@@ -635,7 +663,7 @@ export function intervalLiteral(text) {
|
|
|
635
663
|
* does not exist in the target month clamps to that month's last day, which is what both SQLite
|
|
636
664
|
* and PostgreSQL do with 31 January plus a month.
|
|
637
665
|
*/
|
|
638
|
-
|
|
666
|
+
function dateAddValue(value, months, milliseconds) {
|
|
639
667
|
if (value === null || value === undefined)
|
|
640
668
|
return null;
|
|
641
669
|
const calendarDate = isDateDomainValue(value);
|
|
@@ -662,7 +690,7 @@ export function dateAddValue(value, months, milliseconds) {
|
|
|
662
690
|
? dateDomainValue(dateIsoString(result).slice(0, 10))
|
|
663
691
|
: result;
|
|
664
692
|
}
|
|
665
|
-
|
|
693
|
+
function dateTruncValue(unit, value) {
|
|
666
694
|
if (typeof unit !== "string" || !dateTruncUnits.has(unit.toLowerCase())) {
|
|
667
695
|
throw new TypeError("DATE_TRUNC requires a unit of year, quarter, month, week, day, hour, minute, or second");
|
|
668
696
|
}
|
|
@@ -860,6 +888,7 @@ export function compileQuery(sql, options = {}) {
|
|
|
860
888
|
}
|
|
861
889
|
let compiled;
|
|
862
890
|
try {
|
|
891
|
+
resolvePlanExactNumericConstants(plan);
|
|
863
892
|
compiled = options.optimize === false ? plan : optimizePlan(plan);
|
|
864
893
|
}
|
|
865
894
|
catch (error) {
|
|
@@ -967,7 +996,7 @@ export function compileCheckExpression(sql, name) {
|
|
|
967
996
|
if (hasAggregate(expression) || containsWindow(expression) || containsParameter(expression)) {
|
|
968
997
|
throw new TypeError(`CHECK ${name} takes a row condition over this table's own columns`);
|
|
969
998
|
}
|
|
970
|
-
return expression;
|
|
999
|
+
return resolveExactNumericConstants(expression);
|
|
971
1000
|
}
|
|
972
1001
|
/**
|
|
973
1002
|
* Parses and type-checks one catalog default. PostgreSQL defaults are variable-free scalar
|
|
@@ -1314,6 +1343,22 @@ export function evaluateJoinedRowExpression(expression, rows) {
|
|
|
1314
1343
|
export function evaluateRowExpression(expression, alias, row) {
|
|
1315
1344
|
return asQueryValue(evaluate(expression, { [alias]: row }));
|
|
1316
1345
|
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Wraps one value from an executed subquery block as a substituted literal. Block results are
|
|
1348
|
+
* internal: domain values keep their tags and protected text keeps its wrapper. The literal must
|
|
1349
|
+
* say so — an unmarked string literal is re-protected as user text at evaluation, which both
|
|
1350
|
+
* leaks the internal tag into the outer result and makes equality against internal column
|
|
1351
|
+
* values never match (T694: `WHERE amount = (SELECT amount ...)` on a NUMERIC column returned
|
|
1352
|
+
* no rows). Carrying the block's column domain also keeps the outer result's domain metadata.
|
|
1353
|
+
*/
|
|
1354
|
+
function substitutedResultLiteral(value, sqlDomain) {
|
|
1355
|
+
return {
|
|
1356
|
+
kind: "literal",
|
|
1357
|
+
value: value ?? null,
|
|
1358
|
+
...(typeof value === "string" ? { internalSqlValue: true } : {}),
|
|
1359
|
+
...(sqlDomain === null || sqlDomain === undefined ? {} : { sqlDomain }),
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1317
1362
|
/**
|
|
1318
1363
|
* Clones a plan and returns its subquery sites in post-order: executing each step's block and
|
|
1319
1364
|
* substituting its result leaves the returned plan free of subquery nodes. A scalar subquery must
|
|
@@ -1338,10 +1383,7 @@ export function subqueryResolutionSteps(plan) {
|
|
|
1338
1383
|
if (result.rows.length > 1) {
|
|
1339
1384
|
throw new TypeError(`A scalar subquery returned ${String(result.rows.length)} rows`);
|
|
1340
1385
|
}
|
|
1341
|
-
replace(
|
|
1342
|
-
kind: "literal",
|
|
1343
|
-
value: result.rows[0]?.[result.columns[0] ?? ""] ?? null,
|
|
1344
|
-
});
|
|
1386
|
+
replace(substitutedResultLiteral(result.rows[0]?.[result.columns[0] ?? ""], result.columnDomains[0]));
|
|
1345
1387
|
},
|
|
1346
1388
|
});
|
|
1347
1389
|
return;
|
|
@@ -1412,10 +1454,7 @@ export function subqueryResolutionSteps(plan) {
|
|
|
1412
1454
|
}
|
|
1413
1455
|
expression.right = {
|
|
1414
1456
|
kind: "list",
|
|
1415
|
-
items: result.rows.map((row) => (
|
|
1416
|
-
kind: "literal",
|
|
1417
|
-
value: row[result.columns[0] ?? ""] ?? null,
|
|
1418
|
-
})),
|
|
1457
|
+
items: result.rows.map((row) => substitutedResultLiteral(row[result.columns[0] ?? ""], result.columnDomains[0])),
|
|
1419
1458
|
};
|
|
1420
1459
|
},
|
|
1421
1460
|
});
|
|
@@ -1458,10 +1497,7 @@ export function subqueryResolutionSteps(plan) {
|
|
|
1458
1497
|
}
|
|
1459
1498
|
predicate.right = {
|
|
1460
1499
|
kind: "list",
|
|
1461
|
-
items: result.rows.map((row) => (
|
|
1462
|
-
kind: "literal",
|
|
1463
|
-
value: row[result.columns[0] ?? ""] ?? null,
|
|
1464
|
-
})),
|
|
1500
|
+
items: result.rows.map((row) => substitutedResultLiteral(row[result.columns[0] ?? ""], result.columnDomains[0])),
|
|
1465
1501
|
};
|
|
1466
1502
|
},
|
|
1467
1503
|
});
|
|
@@ -1494,7 +1530,7 @@ export function blockHasSubqueries(plan) {
|
|
|
1494
1530
|
return blockHas(plan);
|
|
1495
1531
|
}
|
|
1496
1532
|
/** True when any `?`/`$n` placeholder remains anywhere in the expression tree. */
|
|
1497
|
-
|
|
1533
|
+
function containsParameter(expression) {
|
|
1498
1534
|
if (expression.kind === "parameter")
|
|
1499
1535
|
return true;
|
|
1500
1536
|
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
@@ -1502,7 +1538,7 @@ export function containsParameter(expression) {
|
|
|
1502
1538
|
}
|
|
1503
1539
|
return childExpressions(expression).some(containsParameter);
|
|
1504
1540
|
}
|
|
1505
|
-
|
|
1541
|
+
function blockHasParameters(block) {
|
|
1506
1542
|
if (block.limitParameter !== undefined ||
|
|
1507
1543
|
block.offsetParameter !== undefined ||
|
|
1508
1544
|
(block.limitValidationParameters?.length ?? 0) > 0 ||
|
|
@@ -1886,7 +1922,8 @@ export function inferBlockSchema(plan, schemas) {
|
|
|
1886
1922
|
if (expression.name === "JSON_ARRAYAGG" ||
|
|
1887
1923
|
expression.name === "JSON_QUERY" ||
|
|
1888
1924
|
expression.name === "JSON_OBJECT" ||
|
|
1889
|
-
expression.name === "JSON_ARRAY"
|
|
1925
|
+
expression.name === "JSON_ARRAY" ||
|
|
1926
|
+
expression.name === "MINNOW_JSON_GET") {
|
|
1890
1927
|
return { kind: "json" };
|
|
1891
1928
|
}
|
|
1892
1929
|
if (expression.name === "GEN_RANDOM_UUID")
|
|
@@ -2025,6 +2062,8 @@ export function inferBlockSchema(plan, schemas) {
|
|
|
2025
2062
|
expression.name === "JSON_OBJECT" ||
|
|
2026
2063
|
expression.name === "JSON_ARRAY" ||
|
|
2027
2064
|
expression.name === "ARRAY" ||
|
|
2065
|
+
expression.name === "MINNOW_JSON_GET" ||
|
|
2066
|
+
expression.name === "MINNOW_JSON_GET_TEXT" ||
|
|
2028
2067
|
expression.name === "MINNOW_TUPLE_KEY" ||
|
|
2029
2068
|
expression.name === "MINNOW_COLLATE" ||
|
|
2030
2069
|
expression.name === "GEN_RANDOM_UUID") {
|
|
@@ -2953,12 +2992,188 @@ export function expandFtsColumns(plan, searchableColumnsFor) {
|
|
|
2953
2992
|
expandBlock(plan);
|
|
2954
2993
|
return plan;
|
|
2955
2994
|
}
|
|
2995
|
+
/**
|
|
2996
|
+
* Annotates every `AVG(column)` in this block's expression positions with the argument column's
|
|
2997
|
+
* declared NUMERIC scale, resolved against the given schemas. PostgreSQL floors an AVG's
|
|
2998
|
+
* internal division scale at the summed values' display scale; the canonical NUMERIC encoding
|
|
2999
|
+
* strips trailing fractional zeros, so without the annotation the divide cannot know the digits
|
|
3000
|
+
* a declared scale above its own selection would render, and the display padding would fabricate
|
|
3001
|
+
* zeros where PostgreSQL computes real digits. Runs where the catalog is known, per block —
|
|
3002
|
+
* nested blocks execute through their own schema-aware entry. Copy-on-write: plans without an
|
|
3003
|
+
* AVG pass through untouched, and the input is often the compile cache's own object.
|
|
3004
|
+
*/
|
|
3005
|
+
export function annotateAvgArgumentScales(plan, schemas) {
|
|
3006
|
+
const containsAvg = (expression) => (expression.kind === "call" && expression.name === "AVG") ||
|
|
3007
|
+
childExpressions(expression).some(containsAvg);
|
|
3008
|
+
const roots = [];
|
|
3009
|
+
forEachBlockExpression(plan, (expression) => roots.push(expression));
|
|
3010
|
+
if (!roots.some(containsAvg))
|
|
3011
|
+
return plan;
|
|
3012
|
+
plan = clonePlanTree(plan);
|
|
3013
|
+
const sources = [plan.base, ...plan.joins];
|
|
3014
|
+
const declaredScale = (reference) => {
|
|
3015
|
+
const parts = reference.split(".");
|
|
3016
|
+
const matches = parts.length === 2
|
|
3017
|
+
? sources
|
|
3018
|
+
.filter(({ alias }) => alias === parts[0])
|
|
3019
|
+
.flatMap((source) => (schemas.get(source.table) ?? []).filter(({ name }) => name === parts[1]))
|
|
3020
|
+
: sources.flatMap((source) => (schemas.get(source.table) ?? []).filter(({ name }) => name === parts[0]));
|
|
3021
|
+
const domain = matches.length === 1 ? matches[0]?.sqlDomain : undefined;
|
|
3022
|
+
return domain?.kind === "numeric" ? domain.scale : undefined;
|
|
3023
|
+
};
|
|
3024
|
+
const annotate = (expression) => {
|
|
3025
|
+
if (expression.kind === "call" && expression.name === "AVG") {
|
|
3026
|
+
const argument = expression.arguments[0];
|
|
3027
|
+
if (argument?.kind === "column") {
|
|
3028
|
+
const scale = declaredScale(argument.reference);
|
|
3029
|
+
if (scale !== undefined && scale > 0)
|
|
3030
|
+
expression.avgArgumentScale = scale;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
for (const child of childExpressions(expression))
|
|
3034
|
+
annotate(child);
|
|
3035
|
+
};
|
|
3036
|
+
forEachBlockExpression(plan, annotate);
|
|
3037
|
+
return plan;
|
|
3038
|
+
}
|
|
3039
|
+
/**
|
|
3040
|
+
* The exact fold of a constant arithmetic subtree, or undefined where none applies. PostgreSQL
|
|
3041
|
+
* types every decimal constant — and every integer constant too large for its integer types —
|
|
3042
|
+
* as NUMERIC, so arithmetic among constants happens in exact decimal space before any float8
|
|
3043
|
+
* context sees the result: `0.1 + 0.2` is exactly 0.3. A subtree folds only when it is
|
|
3044
|
+
* "seeded" by such a constant (one carrying exact digits); pure safe-integer arithmetic keeps
|
|
3045
|
+
* its historical Float64 evaluation, including non-truncating division.
|
|
3046
|
+
*/
|
|
3047
|
+
function exactConstantFold(expression) {
|
|
3048
|
+
if (expression.kind === "literal") {
|
|
3049
|
+
if (expression.exactText !== undefined) {
|
|
3050
|
+
return { value: exactNumericLiteral(expression.exactText), seeded: true };
|
|
3051
|
+
}
|
|
3052
|
+
const value = expression.value;
|
|
3053
|
+
if (typeof value === "string" && expression.sqlDomain?.kind === "numeric") {
|
|
3054
|
+
return isExactNumeric(value) ? { value, seeded: true } : undefined;
|
|
3055
|
+
}
|
|
3056
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
3057
|
+
return { value, seeded: false };
|
|
3058
|
+
if (value === null)
|
|
3059
|
+
return { value: null, seeded: false };
|
|
3060
|
+
return undefined;
|
|
3061
|
+
}
|
|
3062
|
+
if (expression.kind !== "binary" || expression.operator === "||")
|
|
3063
|
+
return undefined;
|
|
3064
|
+
const left = exactConstantFold(expression.left);
|
|
3065
|
+
if (left === undefined)
|
|
3066
|
+
return undefined;
|
|
3067
|
+
const right = exactConstantFold(expression.right);
|
|
3068
|
+
if (right === undefined)
|
|
3069
|
+
return undefined;
|
|
3070
|
+
const seeded = left.seeded || right.seeded;
|
|
3071
|
+
if (left.value === null || right.value === null)
|
|
3072
|
+
return { value: null, seeded };
|
|
3073
|
+
const folded = exactNumericBinary(expression.operator, left.value, right.value, 0, false);
|
|
3074
|
+
return folded === undefined ? undefined : { value: folded, seeded };
|
|
3075
|
+
}
|
|
3076
|
+
/**
|
|
3077
|
+
* Rewrites every seeded constant arithmetic subtree of this expression to its exact fold. A
|
|
3078
|
+
* folded value demotes back to an ordinary number literal when it reads back identically from
|
|
3079
|
+
* a Float64 — PostgreSQL's own cast when a numeric constant meets a float8 operand, and it
|
|
3080
|
+
* keeps every number-typed fast path — and stays a tagged exact-NUMERIC literal when the
|
|
3081
|
+
* number boundary would visibly round it. A fold that overflows its bounds is left unfolded
|
|
3082
|
+
* for execution to report.
|
|
3083
|
+
*/
|
|
3084
|
+
function resolveExactNumericConstants(expression) {
|
|
3085
|
+
let folded;
|
|
3086
|
+
try {
|
|
3087
|
+
folded = exactConstantFold(expression);
|
|
3088
|
+
}
|
|
3089
|
+
catch {
|
|
3090
|
+
folded = undefined;
|
|
3091
|
+
}
|
|
3092
|
+
if (folded?.seeded === true) {
|
|
3093
|
+
const value = folded.value;
|
|
3094
|
+
if (value === null || typeof value === "number")
|
|
3095
|
+
return { kind: "literal", value };
|
|
3096
|
+
const fits = exactNumericAsNumber(value);
|
|
3097
|
+
if (fits !== undefined)
|
|
3098
|
+
return { kind: "literal", value: fits };
|
|
3099
|
+
return { kind: "literal", value, internalSqlValue: true, sqlDomain: { kind: "numeric" } };
|
|
3100
|
+
}
|
|
3101
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
3102
|
+
resolvePlanExactNumericConstants(expression.block);
|
|
3103
|
+
return expression;
|
|
3104
|
+
}
|
|
3105
|
+
if (expression.kind === "window") {
|
|
3106
|
+
return {
|
|
3107
|
+
...expression,
|
|
3108
|
+
partitionBy: expression.partitionBy.map(resolveExactNumericConstants),
|
|
3109
|
+
orderBy: expression.orderBy.map((order) => ({
|
|
3110
|
+
...order,
|
|
3111
|
+
expression: resolveExactNumericConstants(order.expression),
|
|
3112
|
+
})),
|
|
3113
|
+
...(expression.argument === undefined
|
|
3114
|
+
? {}
|
|
3115
|
+
: { argument: resolveExactNumericConstants(expression.argument) }),
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
3118
|
+
return mapChildExpressions(expression, resolveExactNumericConstants);
|
|
3119
|
+
}
|
|
3120
|
+
/** Applies `resolveExactNumericConstants` to every expression of a plan, nested blocks included. */
|
|
3121
|
+
function resolvePlanExactNumericConstants(plan) {
|
|
3122
|
+
mapBlockExpressions(plan, resolveExactNumericConstants);
|
|
3123
|
+
forEachNestedBlock(plan, resolvePlanExactNumericConstants);
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* The statement counterpart of `resolvePlanExactNumericConstants`: every expression slot a
|
|
3127
|
+
* mutation evaluates later. INSERT VALUES cells are absent because the parser evaluates them
|
|
3128
|
+
* to values on the spot, resolving each expression first.
|
|
3129
|
+
*/
|
|
3130
|
+
function resolveStatementExactNumericConstants(statement) {
|
|
3131
|
+
const resolve = resolveExactNumericConstants;
|
|
3132
|
+
if (statement.kind === "insert") {
|
|
3133
|
+
const conflict = statement.onConflict;
|
|
3134
|
+
if (conflict?.assignments !== undefined) {
|
|
3135
|
+
for (const assignment of conflict.assignments) {
|
|
3136
|
+
assignment.expression = resolve(assignment.expression);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
if (conflict?.where !== undefined)
|
|
3140
|
+
conflict.where = resolve(conflict.where);
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
if (statement.kind === "update" || statement.kind === "delete") {
|
|
3144
|
+
if (statement.kind === "update") {
|
|
3145
|
+
for (const assignment of statement.assignments) {
|
|
3146
|
+
assignment.expression = resolve(assignment.expression);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
for (const predicate of statement.predicates) {
|
|
3150
|
+
predicate.left = resolve(predicate.left);
|
|
3151
|
+
predicate.right = resolve(predicate.right);
|
|
3152
|
+
}
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
if (statement.kind === "merge") {
|
|
3156
|
+
statement.on = resolve(statement.on);
|
|
3157
|
+
for (const branch of statement.branches) {
|
|
3158
|
+
if (branch.condition !== undefined)
|
|
3159
|
+
branch.condition = resolve(branch.condition);
|
|
3160
|
+
if (branch.action.kind === "update") {
|
|
3161
|
+
for (const assignment of branch.action.assignments) {
|
|
3162
|
+
assignment.expression = resolve(assignment.expression);
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
if (branch.action.kind === "insert") {
|
|
3166
|
+
branch.action.values = branch.action.values.map(resolve);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
2956
3171
|
/**
|
|
2957
3172
|
* Aggregates the frame members named by position, for the frames whose rows are not contiguous.
|
|
2958
3173
|
* EXCLUDE puts a hole in the middle of a frame, which the prefix sums the common path uses
|
|
2959
3174
|
* cannot represent, so those positions walk their members instead.
|
|
2960
3175
|
*/
|
|
2961
|
-
function aggregateWindowMembers(window, values, members) {
|
|
3176
|
+
function aggregateWindowMembers(window, values, members, avgScale) {
|
|
2962
3177
|
const present = members.filter((member) => {
|
|
2963
3178
|
const value = values[member];
|
|
2964
3179
|
return value !== null && value !== undefined;
|
|
@@ -2988,7 +3203,9 @@ function aggregateWindowMembers(window, values, members) {
|
|
|
2988
3203
|
throw new Error("Exact NUMERIC sum disappeared");
|
|
2989
3204
|
total = next;
|
|
2990
3205
|
}
|
|
2991
|
-
return window.name === "SUM"
|
|
3206
|
+
return window.name === "SUM"
|
|
3207
|
+
? total
|
|
3208
|
+
: exactNumericBinary("/", total, present.length, avgScale);
|
|
2992
3209
|
}
|
|
2993
3210
|
const total = present.reduce((sum, member) => sum + numeric(values[member]), 0);
|
|
2994
3211
|
return window.name === "SUM" ? total : total / present.length;
|
|
@@ -3013,7 +3230,7 @@ function aggregateWindowMembers(window, values, members) {
|
|
|
3013
3230
|
* RANGE frames take only UNBOUNDED and CURRENT ROW bounds, where CURRENT ROW spans the peer
|
|
3014
3231
|
* group. COUNT of an empty frame is 0 and every other aggregate NULL.
|
|
3015
3232
|
*/
|
|
3016
|
-
function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys) {
|
|
3233
|
+
function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys, avgScale) {
|
|
3017
3234
|
const frame = window.frame ?? {
|
|
3018
3235
|
unit: "range",
|
|
3019
3236
|
start: { kind: "unbounded-preceding" },
|
|
@@ -3025,11 +3242,11 @@ function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKey
|
|
|
3025
3242
|
while (end < indexes.length && samePartition(indexes[start] ?? 0, indexes[end] ?? 0)) {
|
|
3026
3243
|
end += 1;
|
|
3027
3244
|
}
|
|
3028
|
-
applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end);
|
|
3245
|
+
applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end, avgScale);
|
|
3029
3246
|
start = end;
|
|
3030
3247
|
}
|
|
3031
3248
|
}
|
|
3032
|
-
function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end) {
|
|
3249
|
+
function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end, avgScale) {
|
|
3033
3250
|
const size = end - start;
|
|
3034
3251
|
// Peer-group bounds per position; with no OVER ordering the whole partition is one peer group.
|
|
3035
3252
|
const peerStart = new Array(size).fill(0);
|
|
@@ -3160,7 +3377,7 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
|
|
|
3160
3377
|
if (!dropped)
|
|
3161
3378
|
members.push(member);
|
|
3162
3379
|
}
|
|
3163
|
-
value = aggregateWindowMembers(window, values, members);
|
|
3380
|
+
value = aggregateWindowMembers(window, values, members, avgScale);
|
|
3164
3381
|
const row = rows[indexes[start + position] ?? -1];
|
|
3165
3382
|
if (row !== undefined)
|
|
3166
3383
|
row[window.alias] = asQueryValue(value);
|
|
@@ -3192,7 +3409,7 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
|
|
|
3192
3409
|
}
|
|
3193
3410
|
else if (prefixExact !== undefined) {
|
|
3194
3411
|
const total = exactNumericBinary("-", prefixExact[high] ?? exactZero, prefixExact[low] ?? exactZero);
|
|
3195
|
-
value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull);
|
|
3412
|
+
value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull, avgScale);
|
|
3196
3413
|
}
|
|
3197
3414
|
else {
|
|
3198
3415
|
const total = (prefixSum?.[high] ?? 0) - (prefixSum?.[low] ?? 0);
|
|
@@ -3369,7 +3586,13 @@ export function applyWindowFunctions(result, windows, options = {}) {
|
|
|
3369
3586
|
continue;
|
|
3370
3587
|
}
|
|
3371
3588
|
if (window.name !== "ROW_NUMBER" && window.name !== "RANK" && window.name !== "DENSE_RANK") {
|
|
3372
|
-
|
|
3589
|
+
// PostgreSQL computes a window AVG's quotient to at least the summed values' display
|
|
3590
|
+
// scale; canonical NUMERIC values no longer carry it, so read the argument column's
|
|
3591
|
+
// declared scale from the inner result the way the summed dscale would have carried it.
|
|
3592
|
+
const argumentDomain = window.name === "AVG" && window.argumentAlias !== undefined
|
|
3593
|
+
? result.columnDomains[result.columns.indexOf(window.argumentAlias)]
|
|
3594
|
+
: undefined;
|
|
3595
|
+
applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys, argumentDomain?.kind === "numeric" ? argumentDomain.scale : undefined);
|
|
3373
3596
|
continue;
|
|
3374
3597
|
}
|
|
3375
3598
|
let rowNumber = 0;
|
|
@@ -3781,7 +4004,7 @@ function evaluate(expression, context, group) {
|
|
|
3781
4004
|
if (typeof left !== "string" || typeof right !== "string") {
|
|
3782
4005
|
throw new TypeError("|| requires string operands");
|
|
3783
4006
|
}
|
|
3784
|
-
return
|
|
4007
|
+
return concatenatedSqlValue(left, right);
|
|
3785
4008
|
}
|
|
3786
4009
|
const exact = exactNumericBinary(expression.operator, left, right);
|
|
3787
4010
|
if (exact !== undefined)
|
|
@@ -3925,7 +4148,8 @@ function evaluate(expression, context, group) {
|
|
|
3925
4148
|
? null
|
|
3926
4149
|
: (() => {
|
|
3927
4150
|
const sum = sumNumericValues(values);
|
|
3928
|
-
return exactNumericBinary("/", sum, values.length) ??
|
|
4151
|
+
return (exactNumericBinary("/", sum, values.length, expression.avgArgumentScale) ??
|
|
4152
|
+
numeric(sum) / values.length);
|
|
3929
4153
|
})();
|
|
3930
4154
|
if (expression.name === "MIN")
|
|
3931
4155
|
return values.reduce((best, value) => (best === undefined || compareValues(value, best) < 0 ? value : best), undefined);
|
|
@@ -4378,6 +4602,11 @@ export function hasAggregate(expression) {
|
|
|
4378
4602
|
}
|
|
4379
4603
|
return childExpressions(expression).some(hasAggregate);
|
|
4380
4604
|
}
|
|
4605
|
+
function hasWindow(expression) {
|
|
4606
|
+
if (expression.kind === "window")
|
|
4607
|
+
return true;
|
|
4608
|
+
return childExpressions(expression).some(hasWindow);
|
|
4609
|
+
}
|
|
4381
4610
|
/** One root aggregate call, using the same canonical set as parsing and execution. */
|
|
4382
4611
|
export function isAggregateCall(expression) {
|
|
4383
4612
|
return expression.kind === "call" && aggregateNames.has(expression.name);
|
|
@@ -4478,9 +4707,6 @@ function asQueryValue(value) {
|
|
|
4478
4707
|
return null;
|
|
4479
4708
|
throw new TypeError("Query produced an unsupported value");
|
|
4480
4709
|
}
|
|
4481
|
-
function asExternalQueryValue(value) {
|
|
4482
|
-
return asQueryValue(externalSqlDomainValue(value));
|
|
4483
|
-
}
|
|
4484
4710
|
const alreadyExternalResults = new WeakSet();
|
|
4485
4711
|
function markExternalizationState(result, outputNeedsExternalization) {
|
|
4486
4712
|
if (outputNeedsExternalization === false)
|
|
@@ -4505,11 +4731,12 @@ export function externalizeQueryResult(result) {
|
|
|
4505
4731
|
// Ordinary primitive results are already public values. Most queries never touch one of the
|
|
4506
4732
|
// tagged PostgreSQL domains, so keep their row objects and avoid rebuilding a large result
|
|
4507
4733
|
// set merely to discover that every value is unchanged.
|
|
4508
|
-
for (
|
|
4734
|
+
for (let position = 0; position < result.columns.length; position += 1) {
|
|
4735
|
+
const name = result.columns[position] ?? "";
|
|
4509
4736
|
const value = row[name];
|
|
4510
4737
|
if (value !== undefined && !isSqlDomainValue(value))
|
|
4511
4738
|
continue;
|
|
4512
|
-
const external =
|
|
4739
|
+
const external = asQueryValue(externalSqlDomainColumnValue(value, result.columnDomains[position]));
|
|
4513
4740
|
if (external === value)
|
|
4514
4741
|
continue;
|
|
4515
4742
|
if (output === row)
|
|
@@ -5272,6 +5499,14 @@ class Parser {
|
|
|
5272
5499
|
},
|
|
5273
5500
|
};
|
|
5274
5501
|
}
|
|
5502
|
+
/** A type name's precision, scale, or width: plain digits, as PostgreSQL requires there. */
|
|
5503
|
+
#typeWidth() {
|
|
5504
|
+
const token = this.#take("number");
|
|
5505
|
+
if (!/^\d+$/.test(token.text)) {
|
|
5506
|
+
throw new SqlCompileError(`A type width must be an integer: ${token.text}`, token.start, token.end - token.start);
|
|
5507
|
+
}
|
|
5508
|
+
return Number(token.text);
|
|
5509
|
+
}
|
|
5275
5510
|
/** A CAST target: the SqlColumnType, or "number-integer" for the truncating integer names. */
|
|
5276
5511
|
#castTarget() {
|
|
5277
5512
|
const word = this.#identifier().toUpperCase();
|
|
@@ -5279,9 +5514,9 @@ class Parser {
|
|
|
5279
5514
|
let precision;
|
|
5280
5515
|
let scale;
|
|
5281
5516
|
if (this.#punctuation("(")) {
|
|
5282
|
-
precision =
|
|
5517
|
+
precision = this.#typeWidth();
|
|
5283
5518
|
if (this.#punctuation(","))
|
|
5284
|
-
scale =
|
|
5519
|
+
scale = this.#typeWidth();
|
|
5285
5520
|
else
|
|
5286
5521
|
scale = 0;
|
|
5287
5522
|
this.#expectPunctuation(")");
|
|
@@ -5299,9 +5534,9 @@ class Parser {
|
|
|
5299
5534
|
if (mapped === undefined)
|
|
5300
5535
|
throw new TypeError(`Unsupported CAST target: ${word}`);
|
|
5301
5536
|
if (this.#punctuation("(")) {
|
|
5302
|
-
this.#
|
|
5537
|
+
this.#typeWidth();
|
|
5303
5538
|
if (this.#punctuation(","))
|
|
5304
|
-
this.#
|
|
5539
|
+
this.#typeWidth();
|
|
5305
5540
|
this.#expectPunctuation(")");
|
|
5306
5541
|
}
|
|
5307
5542
|
const integer = word === "INTEGER" || word === "INT" || word === "BIGINT" || word === "SMALLINT";
|
|
@@ -5314,8 +5549,8 @@ class Parser {
|
|
|
5314
5549
|
let precision;
|
|
5315
5550
|
let scale;
|
|
5316
5551
|
if (this.#punctuation("(")) {
|
|
5317
|
-
precision =
|
|
5318
|
-
scale = this.#punctuation(",") ?
|
|
5552
|
+
precision = this.#typeWidth();
|
|
5553
|
+
scale = this.#punctuation(",") ? this.#typeWidth() : 0;
|
|
5319
5554
|
this.#expectPunctuation(")");
|
|
5320
5555
|
}
|
|
5321
5556
|
return {
|
|
@@ -5340,7 +5575,7 @@ class Parser {
|
|
|
5340
5575
|
}
|
|
5341
5576
|
// Character widths document intent and do not truncate values.
|
|
5342
5577
|
if (this.#punctuation("(")) {
|
|
5343
|
-
this.#
|
|
5578
|
+
this.#typeWidth();
|
|
5344
5579
|
if (this.#punctuation(",")) {
|
|
5345
5580
|
throw new TypeError(`${word} takes one width`);
|
|
5346
5581
|
}
|
|
@@ -5360,6 +5595,7 @@ class Parser {
|
|
|
5360
5595
|
? this.#updateStatement()
|
|
5361
5596
|
: this.#deleteStatement();
|
|
5362
5597
|
this.#take("eof");
|
|
5598
|
+
resolveStatementExactNumericConstants(statement);
|
|
5363
5599
|
return statement;
|
|
5364
5600
|
}
|
|
5365
5601
|
#insertStatement() {
|
|
@@ -5392,7 +5628,9 @@ class Parser {
|
|
|
5392
5628
|
};
|
|
5393
5629
|
}
|
|
5394
5630
|
if (this.#isKeyword("SELECT")) {
|
|
5395
|
-
const
|
|
5631
|
+
const insertSource = this.#selectBlock("(insert select)");
|
|
5632
|
+
resolvePlanExactNumericConstants(insertSource);
|
|
5633
|
+
const query = optimizePlan(insertSource);
|
|
5396
5634
|
if (query.select.some((item) => item.expression.kind === "wildcard")) {
|
|
5397
5635
|
throw new TypeError("INSERT ... SELECT requires an explicit select list");
|
|
5398
5636
|
}
|
|
@@ -5655,7 +5893,9 @@ class Parser {
|
|
|
5655
5893
|
throw new TypeError("MERGE does not support RETURNING; read the rows back with a SELECT");
|
|
5656
5894
|
}
|
|
5657
5895
|
this.#take("eof");
|
|
5658
|
-
|
|
5896
|
+
const statement = { kind: "merge", table, alias, source, on, branches };
|
|
5897
|
+
resolveStatementExactNumericConstants(statement);
|
|
5898
|
+
return statement;
|
|
5659
5899
|
}
|
|
5660
5900
|
#updateStatement() {
|
|
5661
5901
|
this.#keyword("UPDATE");
|
|
@@ -5715,7 +5955,7 @@ class Parser {
|
|
|
5715
5955
|
if (hasAggregate(expression) || expressionColumns(expression).length > 0) {
|
|
5716
5956
|
throw new TypeError(`${label} must be constant expressions`);
|
|
5717
5957
|
}
|
|
5718
|
-
return asQueryValue(evaluate(expression, {}));
|
|
5958
|
+
return asQueryValue(evaluate(resolveExactNumericConstants(expression), {}));
|
|
5719
5959
|
}
|
|
5720
5960
|
#unionMember(sql) {
|
|
5721
5961
|
if (this.#punctuation("(")) {
|
|
@@ -6896,17 +7136,41 @@ class Parser {
|
|
|
6896
7136
|
continue;
|
|
6897
7137
|
}
|
|
6898
7138
|
const operator = this.#peek().text;
|
|
6899
|
-
//
|
|
7139
|
+
// A bracket after a complete expression is PostgreSQL's array subscript, which has no
|
|
7140
|
+
// other reading in this grammar — name the missing feature instead of "Expected eof".
|
|
7141
|
+
if (operator === "[") {
|
|
7142
|
+
throw new TypeError("Array subscripts are not supported");
|
|
7143
|
+
}
|
|
7144
|
+
// || and the JSON arrows share PostgreSQL's loosest "any other operator" level, applying
|
|
7145
|
+
// left-to-right to whole arithmetic terms: `'a' || d ->> 'k'` is `('a' || d) ->> 'k'`.
|
|
6900
7146
|
const precedence = operator === "*" || operator === "/" || operator === "%"
|
|
6901
7147
|
? 20
|
|
6902
7148
|
: operator === "+" || operator === "-"
|
|
6903
7149
|
? 10
|
|
6904
|
-
: operator === "||"
|
|
7150
|
+
: operator === "||" || operator === "->" || operator === "->>"
|
|
6905
7151
|
? 5
|
|
6906
7152
|
: -1;
|
|
6907
7153
|
if (precedence < minimumPrecedence)
|
|
6908
7154
|
break;
|
|
6909
7155
|
this.#index += 1;
|
|
7156
|
+
if (operator === "->" || operator === "->>") {
|
|
7157
|
+
const key = this.#additive(precedence + 1);
|
|
7158
|
+
// A key fixed at compile time fails here rather than per row, like SQL/JSON paths.
|
|
7159
|
+
if (key.kind === "literal" && typeof key.value === "number") {
|
|
7160
|
+
if (!Number.isInteger(key.value)) {
|
|
7161
|
+
throw new TypeError(`${operator} array positions are integers`);
|
|
7162
|
+
}
|
|
7163
|
+
}
|
|
7164
|
+
else if (key.kind === "literal" && typeof key.value === "boolean") {
|
|
7165
|
+
throw new TypeError(`${operator} keys are member names or array positions`);
|
|
7166
|
+
}
|
|
7167
|
+
left = {
|
|
7168
|
+
kind: "call",
|
|
7169
|
+
name: operator === "->" ? "MINNOW_JSON_GET" : "MINNOW_JSON_GET_TEXT",
|
|
7170
|
+
arguments: [left, key],
|
|
7171
|
+
};
|
|
7172
|
+
continue;
|
|
7173
|
+
}
|
|
6910
7174
|
// `placed_at + INTERVAL '1 month'`. An interval is not a value any column can hold, so it
|
|
6911
7175
|
// never becomes an expression of its own: it is read here, where the thing it applies to is
|
|
6912
7176
|
// already in hand, and folds into the date arithmetic DATE_ADD performs.
|
|
@@ -6966,10 +7230,29 @@ class Parser {
|
|
|
6966
7230
|
if (token.kind === "number") {
|
|
6967
7231
|
this.#index += 1;
|
|
6968
7232
|
const value = Number(token.text);
|
|
6969
|
-
|
|
6970
|
-
|
|
7233
|
+
// A safe integer spelled as one is exactly itself; every other spelling — a decimal
|
|
7234
|
+
// point, an exponent, or digits beyond 2^53 — is a numeric constant PostgreSQL would
|
|
7235
|
+
// type NUMERIC. Keep its exact digits: as an annotation when the value reads back
|
|
7236
|
+
// identically from a number, or as a tagged exact-NUMERIC literal when it would not.
|
|
7237
|
+
if (!/[.eE]/.test(token.text) && Number.isSafeInteger(value)) {
|
|
7238
|
+
return { kind: "literal", value };
|
|
7239
|
+
}
|
|
7240
|
+
let tagged;
|
|
7241
|
+
try {
|
|
7242
|
+
tagged = exactNumericLiteral(token.text);
|
|
6971
7243
|
}
|
|
6972
|
-
|
|
7244
|
+
catch (error) {
|
|
7245
|
+
throw new SqlCompileError(error instanceof Error ? error.message : `Invalid number: ${token.text}`, token.start, token.end - token.start);
|
|
7246
|
+
}
|
|
7247
|
+
const fits = exactNumericAsNumber(tagged);
|
|
7248
|
+
if (fits !== undefined)
|
|
7249
|
+
return { kind: "literal", value: fits, exactText: token.text };
|
|
7250
|
+
return {
|
|
7251
|
+
kind: "literal",
|
|
7252
|
+
value: tagged,
|
|
7253
|
+
internalSqlValue: true,
|
|
7254
|
+
sqlDomain: { kind: "numeric" },
|
|
7255
|
+
};
|
|
6973
7256
|
}
|
|
6974
7257
|
if (token.kind === "string") {
|
|
6975
7258
|
this.#index += 1;
|
|
@@ -7380,7 +7663,9 @@ class Parser {
|
|
|
7380
7663
|
const name = (upper === "ANY_VALUE" ? "MIN" : (functionSpellings.get(upper) ?? upper));
|
|
7381
7664
|
if (name === "MINNOW_TUPLE_KEY" ||
|
|
7382
7665
|
name === "MINNOW_COLLATE" ||
|
|
7383
|
-
name === "MINNOW_SINGLE_VALUE"
|
|
7666
|
+
name === "MINNOW_SINGLE_VALUE" ||
|
|
7667
|
+
name === "MINNOW_JSON_GET" ||
|
|
7668
|
+
name === "MINNOW_JSON_GET_TEXT") {
|
|
7384
7669
|
throw new TypeError(`Unsupported function: ${identifier}`);
|
|
7385
7670
|
}
|
|
7386
7671
|
if (!aggregateNames.has(name) && !scalarFunctionNames.has(name))
|
|
@@ -8058,6 +8343,8 @@ export function assembleSelectBlock(parts, nextSequence) {
|
|
|
8058
8343
|
if (distinct) {
|
|
8059
8344
|
if (select.some((item) => hasAggregate(item.expression)))
|
|
8060
8345
|
throw new TypeError("SELECT DISTINCT cannot be combined with aggregate functions");
|
|
8346
|
+
if (select.some((item) => hasWindow(item.expression)))
|
|
8347
|
+
throw new TypeError("SELECT DISTINCT cannot be combined with window functions");
|
|
8061
8348
|
if (groupBy.length > 0)
|
|
8062
8349
|
throw new TypeError("SELECT DISTINCT cannot be combined with GROUP BY");
|
|
8063
8350
|
if (having.length > 0)
|
|
@@ -8136,7 +8423,7 @@ export function assembleSelectBlock(parts, nextSequence) {
|
|
|
8136
8423
|
* over those same columns provides the deduplication through the grouped executor. Runs
|
|
8137
8424
|
* exactly once per execution entry, like MATCH(*) expansion.
|
|
8138
8425
|
*/
|
|
8139
|
-
|
|
8426
|
+
function expandDistinctWildcard(plan, columnsOf) {
|
|
8140
8427
|
if (plan.distinctWildcard !== true)
|
|
8141
8428
|
return plan;
|
|
8142
8429
|
const shaped = [plan.base, ...plan.joins].map((source) => ({
|
|
@@ -8228,7 +8515,7 @@ export function planHasSourceColumnAliases(plan) {
|
|
|
8228
8515
|
* scan cannot know whether the next row ties — and the ordered result is trimmed here: rows up
|
|
8229
8516
|
* to the limit, plus every following row equal to the last one on all ORDER BY columns.
|
|
8230
8517
|
*/
|
|
8231
|
-
|
|
8518
|
+
function withTiesPlan(plan) {
|
|
8232
8519
|
// Ordering by an expression or an unselected column wraps the real block in a projection that
|
|
8233
8520
|
// hides the sort column. The tie test needs that column, so the inner block runs and the
|
|
8234
8521
|
// projection is applied after trimming instead of before.
|
|
@@ -8415,7 +8702,7 @@ export function expandSourceColumnAliases(plan, columnsOf) {
|
|
|
8415
8702
|
* one source, and `alias.column` when it reads several, so two sources cannot collide.
|
|
8416
8703
|
* Runs once per execution entry, like MATCH(*) and DISTINCT * expansion.
|
|
8417
8704
|
*/
|
|
8418
|
-
|
|
8705
|
+
function expandQualifiedWildcards(plan, columnsOf) {
|
|
8419
8706
|
const qualified = (block) => {
|
|
8420
8707
|
if (block.select.some((item) => item.expression.kind === "wildcard" && item.expression.table))
|
|
8421
8708
|
return true;
|
|
@@ -8691,7 +8978,7 @@ function jsonTableColumnValue(column, value) {
|
|
|
8691
8978
|
}
|
|
8692
8979
|
return date;
|
|
8693
8980
|
}
|
|
8694
|
-
|
|
8981
|
+
function timestampLiteral(text) {
|
|
8695
8982
|
const trimmed = text.trim();
|
|
8696
8983
|
const match = /^(\d{4}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?))?(Z|[+-]\d{2}:?\d{2})?$/.exec(trimmed);
|
|
8697
8984
|
if (match === null)
|
|
@@ -8865,13 +9152,14 @@ function defaultAlias(expression) {
|
|
|
8865
9152
|
return "expression";
|
|
8866
9153
|
}
|
|
8867
9154
|
/**
|
|
8868
|
-
* Whether a numeric literal's digits are well formed: at most one decimal point
|
|
8869
|
-
* and underscores only between digits, never leading, trailing, or
|
|
9155
|
+
* Whether a numeric literal's digits are well formed: at most one decimal point and an optional
|
|
9156
|
+
* exponent (radix 10 only), and underscores only between digits, never leading, trailing, or
|
|
9157
|
+
* doubled (T662). Exponent digits are plain, as in PostgreSQL.
|
|
8870
9158
|
*/
|
|
8871
9159
|
function validNumericLiteral(text, radix) {
|
|
8872
9160
|
const digits = radix === 16 ? "0-9a-fA-F" : radix === 8 ? "0-7" : radix === 2 ? "01" : "0-9";
|
|
8873
9161
|
const group = `[${digits}]+(?:_[${digits}]+)*`;
|
|
8874
|
-
const pattern = radix === 10 ? `^${group}(?:\\.${group})?$` : `^${group}$`;
|
|
9162
|
+
const pattern = radix === 10 ? `^${group}(?:\\.${group})?(?:[eE][+-]?\\d+)?$` : `^${group}$`;
|
|
8875
9163
|
return new RegExp(pattern).test(text);
|
|
8876
9164
|
}
|
|
8877
9165
|
function tokenize(sql) {
|
|
@@ -8929,6 +9217,16 @@ function tokenize(sql) {
|
|
|
8929
9217
|
// T662: underscores may separate digits.
|
|
8930
9218
|
while (index < sql.length && /[\d._]/.test(sql[index] ?? ""))
|
|
8931
9219
|
index += 1;
|
|
9220
|
+
// Scientific notation: an exponent marker joins the token only when digits follow, so
|
|
9221
|
+
// `1e2` is the numeric constant 100 while `SELECT 1 e` still reads `e` as an alias.
|
|
9222
|
+
if (/[eE]/.test(sql[index] ?? "")) {
|
|
9223
|
+
const signed = /[+-]/.test(sql[index + 1] ?? "") ? 1 : 0;
|
|
9224
|
+
if (/\d/.test(sql[index + 1 + signed] ?? "")) {
|
|
9225
|
+
index += 2 + signed;
|
|
9226
|
+
while (index < sql.length && /\d/.test(sql[index] ?? ""))
|
|
9227
|
+
index += 1;
|
|
9228
|
+
}
|
|
9229
|
+
}
|
|
8932
9230
|
const text = sql.slice(start, index);
|
|
8933
9231
|
if (!validNumericLiteral(text, 10))
|
|
8934
9232
|
throw new SqlCompileError(`Invalid number: ${text}`, start, index - start);
|
|
@@ -9022,7 +9320,12 @@ function tokenize(sql) {
|
|
|
9022
9320
|
index += 1;
|
|
9023
9321
|
continue;
|
|
9024
9322
|
}
|
|
9025
|
-
if (
|
|
9323
|
+
if (pair === "->" && sql[index + 2] === ">") {
|
|
9324
|
+
push({ kind: "operator", text: "->>", start: index, end: index + 3 });
|
|
9325
|
+
index += 3;
|
|
9326
|
+
continue;
|
|
9327
|
+
}
|
|
9328
|
+
if ([">=", "<=", "!=", "<>", "||", "->"].includes(pair)) {
|
|
9026
9329
|
push({ kind: "operator", text: pair, start: index, end: index + 2 });
|
|
9027
9330
|
index += 2;
|
|
9028
9331
|
continue;
|