@minnowdb/core 0.6.5 → 0.6.7
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/block-format/codecs.d.ts +0 -8
- package/dist/block-format/codecs.js +1 -1
- 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 +58 -34
- package/dist/engine/fts.d.ts +0 -1
- package/dist/engine/fts.js +1 -1
- package/dist/engine/group-index.d.ts +2 -0
- package/dist/engine/group-index.js +4 -8
- package/dist/engine/join-index.d.ts +0 -1
- package/dist/engine/join-index.js +1 -15
- package/dist/engine/keyed-live.js +2 -36
- package/dist/engine/live-equal.d.ts +7 -0
- package/dist/engine/live-equal.js +42 -0
- package/dist/engine/optimizer.js +11 -16
- 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 +286 -54
- package/dist/engine/result-wire.d.ts +2 -1
- package/dist/engine/schema.d.ts +15 -11
- package/dist/engine/schema.js +11 -19
- package/dist/engine/sort-keys.d.ts +2 -3
- package/dist/engine/sort-keys.js +1 -1
- package/dist/engine/sql-domains.d.ts +27 -2
- package/dist/engine/sql-domains.js +120 -6
- package/dist/engine/sql-json.d.ts +0 -2
- package/dist/engine/sql-json.js +1 -1
- package/dist/engine/sql-semantics.d.ts +2 -1
- package/dist/engine/typed-live.js +3 -41
- package/dist/engine/vector.d.ts +7 -7
- package/dist/engine/vector.js +11 -30
- package/dist/engine/write-block-planner.d.ts +2 -1
- package/dist/plan/model.d.ts +22 -0
- package/dist/plan/model.js +29 -1
- package/dist/storage/indexeddb.js +13 -23
- package/dist/storage/opfs/leader.d.ts +0 -4
- package/dist/storage/opfs/leader.js +5 -14
- 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 +3 -19
- package/dist/storage/types.d.ts +4 -3
- package/dist/storage/types.js +13 -2
- package/dist/testing/block-store-conformance.js +28 -0
- package/dist/testing/opfs-shim.d.ts +2 -1
- package/dist/testing/simulator.js +3 -0
- package/dist/worker-protocol/index.d.ts +0 -32
- package/dist/worker-protocol/index.js +0 -40
- package/package.json +2 -1
- package/postgres-feature-profile.json +8 -3
- package/sql-feature-matrix.json +71 -11
package/dist/engine/query.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { copyDate, dateIsoString, dateMilliseconds, dateUtcDate, dateUtcDay, dateUtcFullYear, dateUtcHours, dateUtcMinutes, dateUtcMonth, dateUtcSeconds, setDateUtcDate, setDateUtcMonth, } from "../date-value.js";
|
|
2
|
+
import { crossJoinPlan } from "../plan/model.js";
|
|
2
3
|
import { assertWellFormedString, wellFormedUtf8ByteLength } from "../block-format/unicode.js";
|
|
3
4
|
import { MAX_SQL_NESTING_DEPTH, MAX_SQL_PARAMETERS, MAX_SQL_SCALAR_RESULT_CHARACTERS, MAX_SQL_TEXT_CHARACTERS, MAX_SQL_TOKENS, } from "./cache-limits.js";
|
|
4
5
|
import { SqlCompileError } from "./errors.js";
|
|
@@ -9,7 +10,7 @@ import { stringArgument } from "./sql-semantics.js";
|
|
|
9
10
|
import { jsonArrowStep, jsonAtPath, jsonConstructor, jsonIsValid, parseJsonPath, } from "./sql-json.js";
|
|
10
11
|
import { optimizePlan } from "./optimizer.js";
|
|
11
12
|
import { compareSqlValues as compareValues, compileLikePattern, compileSimilarPattern, encodeSqlEqualityValue, roundSqlNumber, } from "./sql-semantics.js";
|
|
12
|
-
import { arrayDomainValue, boundedJsonText, collatedDomainValue, dateDomainValue, exactNumericBinary, exactNumericValue, externalSqlDomainColumnValue, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, isExactNumeric, isSqlDomainValue, jsonDomainValue, normalizeSqlDomainValue, preservedJsonDomainValue, protectedSqlTextValue, timeDomainValue, uuidDomainValue, } from "./sql-domains.js";
|
|
13
|
+
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
14
|
import { columnarTableFromRows, prepareVectorQuery, } from "./vector.js";
|
|
14
15
|
/** Domain metadata for an execution path that has no catalog-backed type information. */
|
|
15
16
|
export function unknownColumnDomains(columns) {
|
|
@@ -81,7 +82,7 @@ export const volatileScalarFunctionNames = new Set([
|
|
|
81
82
|
* reading, so they are resolved once per execution rather than evaluated per row: every row of
|
|
82
83
|
* one statement sees one instant, both executors agree, and constant folding leaves them alone.
|
|
83
84
|
*/
|
|
84
|
-
|
|
85
|
+
const statementDatetimeNames = new Set([
|
|
85
86
|
"CURRENT_DATE",
|
|
86
87
|
"CURRENT_TIMESTAMP",
|
|
87
88
|
"LOCALTIME",
|
|
@@ -607,7 +608,7 @@ function assertReplacementResultLength(source, search, replacement) {
|
|
|
607
608
|
throw new RangeError(`REPLACE result exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
|
|
608
609
|
}
|
|
609
610
|
}
|
|
610
|
-
|
|
611
|
+
const dateTruncUnits = new Set([
|
|
611
612
|
"year",
|
|
612
613
|
"quarter",
|
|
613
614
|
"month",
|
|
@@ -637,7 +638,7 @@ const intervalUnits = new Map([
|
|
|
637
638
|
* rather than converted, because a month is not a fixed number of them: adding one to January 31
|
|
638
639
|
* has to land on the end of February, which only calendar arithmetic can do.
|
|
639
640
|
*/
|
|
640
|
-
|
|
641
|
+
function intervalLiteral(text) {
|
|
641
642
|
let months = 0;
|
|
642
643
|
let milliseconds = 0;
|
|
643
644
|
let matched = 0;
|
|
@@ -663,7 +664,7 @@ export function intervalLiteral(text) {
|
|
|
663
664
|
* does not exist in the target month clamps to that month's last day, which is what both SQLite
|
|
664
665
|
* and PostgreSQL do with 31 January plus a month.
|
|
665
666
|
*/
|
|
666
|
-
|
|
667
|
+
function dateAddValue(value, months, milliseconds) {
|
|
667
668
|
if (value === null || value === undefined)
|
|
668
669
|
return null;
|
|
669
670
|
const calendarDate = isDateDomainValue(value);
|
|
@@ -690,7 +691,7 @@ export function dateAddValue(value, months, milliseconds) {
|
|
|
690
691
|
? dateDomainValue(dateIsoString(result).slice(0, 10))
|
|
691
692
|
: result;
|
|
692
693
|
}
|
|
693
|
-
|
|
694
|
+
function dateTruncValue(unit, value) {
|
|
694
695
|
if (typeof unit !== "string" || !dateTruncUnits.has(unit.toLowerCase())) {
|
|
695
696
|
throw new TypeError("DATE_TRUNC requires a unit of year, quarter, month, week, day, hour, minute, or second");
|
|
696
697
|
}
|
|
@@ -888,6 +889,7 @@ export function compileQuery(sql, options = {}) {
|
|
|
888
889
|
}
|
|
889
890
|
let compiled;
|
|
890
891
|
try {
|
|
892
|
+
resolvePlanExactNumericConstants(plan);
|
|
891
893
|
compiled = options.optimize === false ? plan : optimizePlan(plan);
|
|
892
894
|
}
|
|
893
895
|
catch (error) {
|
|
@@ -995,7 +997,7 @@ export function compileCheckExpression(sql, name) {
|
|
|
995
997
|
if (hasAggregate(expression) || containsWindow(expression) || containsParameter(expression)) {
|
|
996
998
|
throw new TypeError(`CHECK ${name} takes a row condition over this table's own columns`);
|
|
997
999
|
}
|
|
998
|
-
return expression;
|
|
1000
|
+
return resolveExactNumericConstants(expression);
|
|
999
1001
|
}
|
|
1000
1002
|
/**
|
|
1001
1003
|
* Parses and type-checks one catalog default. PostgreSQL defaults are variable-free scalar
|
|
@@ -1529,7 +1531,7 @@ export function blockHasSubqueries(plan) {
|
|
|
1529
1531
|
return blockHas(plan);
|
|
1530
1532
|
}
|
|
1531
1533
|
/** True when any `?`/`$n` placeholder remains anywhere in the expression tree. */
|
|
1532
|
-
|
|
1534
|
+
function containsParameter(expression) {
|
|
1533
1535
|
if (expression.kind === "parameter")
|
|
1534
1536
|
return true;
|
|
1535
1537
|
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
@@ -1537,7 +1539,7 @@ export function containsParameter(expression) {
|
|
|
1537
1539
|
}
|
|
1538
1540
|
return childExpressions(expression).some(containsParameter);
|
|
1539
1541
|
}
|
|
1540
|
-
|
|
1542
|
+
function blockHasParameters(block) {
|
|
1541
1543
|
if (block.limitParameter !== undefined ||
|
|
1542
1544
|
block.offsetParameter !== undefined ||
|
|
1543
1545
|
(block.limitValidationParameters?.length ?? 0) > 0 ||
|
|
@@ -2548,6 +2550,9 @@ function createPreparedRowQuery(plan, tables, memory) {
|
|
|
2548
2550
|
},
|
|
2549
2551
|
};
|
|
2550
2552
|
}
|
|
2553
|
+
// protectText defaults OFF here — the row engine's own tables are already inside the SQL
|
|
2554
|
+
// boundary — but ON in vector.ts's columnarTableFromRows, which ingests caller-owned rows.
|
|
2555
|
+
// A new call site must pick deliberately, not inherit whichever default is nearest.
|
|
2551
2556
|
function cloneRowTables(tables, protectText = false) {
|
|
2552
2557
|
return new Map([...tables].map(([name, rows]) => [
|
|
2553
2558
|
name,
|
|
@@ -2991,12 +2996,188 @@ export function expandFtsColumns(plan, searchableColumnsFor) {
|
|
|
2991
2996
|
expandBlock(plan);
|
|
2992
2997
|
return plan;
|
|
2993
2998
|
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Annotates every `AVG(column)` in this block's expression positions with the argument column's
|
|
3001
|
+
* declared NUMERIC scale, resolved against the given schemas. PostgreSQL floors an AVG's
|
|
3002
|
+
* internal division scale at the summed values' display scale; the canonical NUMERIC encoding
|
|
3003
|
+
* strips trailing fractional zeros, so without the annotation the divide cannot know the digits
|
|
3004
|
+
* a declared scale above its own selection would render, and the display padding would fabricate
|
|
3005
|
+
* zeros where PostgreSQL computes real digits. Runs where the catalog is known, per block —
|
|
3006
|
+
* nested blocks execute through their own schema-aware entry. Copy-on-write: plans without an
|
|
3007
|
+
* AVG pass through untouched, and the input is often the compile cache's own object.
|
|
3008
|
+
*/
|
|
3009
|
+
export function annotateAvgArgumentScales(plan, schemas) {
|
|
3010
|
+
const containsAvg = (expression) => (expression.kind === "call" && expression.name === "AVG") ||
|
|
3011
|
+
childExpressions(expression).some(containsAvg);
|
|
3012
|
+
const roots = [];
|
|
3013
|
+
forEachBlockExpression(plan, (expression) => roots.push(expression));
|
|
3014
|
+
if (!roots.some(containsAvg))
|
|
3015
|
+
return plan;
|
|
3016
|
+
plan = clonePlanTree(plan);
|
|
3017
|
+
const sources = [plan.base, ...plan.joins];
|
|
3018
|
+
const declaredScale = (reference) => {
|
|
3019
|
+
const parts = reference.split(".");
|
|
3020
|
+
const matches = parts.length === 2
|
|
3021
|
+
? sources
|
|
3022
|
+
.filter(({ alias }) => alias === parts[0])
|
|
3023
|
+
.flatMap((source) => (schemas.get(source.table) ?? []).filter(({ name }) => name === parts[1]))
|
|
3024
|
+
: sources.flatMap((source) => (schemas.get(source.table) ?? []).filter(({ name }) => name === parts[0]));
|
|
3025
|
+
const domain = matches.length === 1 ? matches[0]?.sqlDomain : undefined;
|
|
3026
|
+
return domain?.kind === "numeric" ? domain.scale : undefined;
|
|
3027
|
+
};
|
|
3028
|
+
const annotate = (expression) => {
|
|
3029
|
+
if (expression.kind === "call" && expression.name === "AVG") {
|
|
3030
|
+
const argument = expression.arguments[0];
|
|
3031
|
+
if (argument?.kind === "column") {
|
|
3032
|
+
const scale = declaredScale(argument.reference);
|
|
3033
|
+
if (scale !== undefined && scale > 0)
|
|
3034
|
+
expression.avgArgumentScale = scale;
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
for (const child of childExpressions(expression))
|
|
3038
|
+
annotate(child);
|
|
3039
|
+
};
|
|
3040
|
+
forEachBlockExpression(plan, annotate);
|
|
3041
|
+
return plan;
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* The exact fold of a constant arithmetic subtree, or undefined where none applies. PostgreSQL
|
|
3045
|
+
* types every decimal constant — and every integer constant too large for its integer types —
|
|
3046
|
+
* as NUMERIC, so arithmetic among constants happens in exact decimal space before any float8
|
|
3047
|
+
* context sees the result: `0.1 + 0.2` is exactly 0.3. A subtree folds only when it is
|
|
3048
|
+
* "seeded" by such a constant (one carrying exact digits); pure safe-integer arithmetic keeps
|
|
3049
|
+
* its historical Float64 evaluation, including non-truncating division.
|
|
3050
|
+
*/
|
|
3051
|
+
function exactConstantFold(expression) {
|
|
3052
|
+
if (expression.kind === "literal") {
|
|
3053
|
+
if (expression.exactText !== undefined) {
|
|
3054
|
+
return { value: exactNumericLiteral(expression.exactText), seeded: true };
|
|
3055
|
+
}
|
|
3056
|
+
const value = expression.value;
|
|
3057
|
+
if (typeof value === "string" && expression.sqlDomain?.kind === "numeric") {
|
|
3058
|
+
return isExactNumeric(value) ? { value, seeded: true } : undefined;
|
|
3059
|
+
}
|
|
3060
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
3061
|
+
return { value, seeded: false };
|
|
3062
|
+
if (value === null)
|
|
3063
|
+
return { value: null, seeded: false };
|
|
3064
|
+
return undefined;
|
|
3065
|
+
}
|
|
3066
|
+
if (expression.kind !== "binary" || expression.operator === "||")
|
|
3067
|
+
return undefined;
|
|
3068
|
+
const left = exactConstantFold(expression.left);
|
|
3069
|
+
if (left === undefined)
|
|
3070
|
+
return undefined;
|
|
3071
|
+
const right = exactConstantFold(expression.right);
|
|
3072
|
+
if (right === undefined)
|
|
3073
|
+
return undefined;
|
|
3074
|
+
const seeded = left.seeded || right.seeded;
|
|
3075
|
+
if (left.value === null || right.value === null)
|
|
3076
|
+
return { value: null, seeded };
|
|
3077
|
+
const folded = exactNumericBinary(expression.operator, left.value, right.value, 0, false);
|
|
3078
|
+
return folded === undefined ? undefined : { value: folded, seeded };
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Rewrites every seeded constant arithmetic subtree of this expression to its exact fold. A
|
|
3082
|
+
* folded value demotes back to an ordinary number literal when it reads back identically from
|
|
3083
|
+
* a Float64 — PostgreSQL's own cast when a numeric constant meets a float8 operand, and it
|
|
3084
|
+
* keeps every number-typed fast path — and stays a tagged exact-NUMERIC literal when the
|
|
3085
|
+
* number boundary would visibly round it. A fold that overflows its bounds is left unfolded
|
|
3086
|
+
* for execution to report.
|
|
3087
|
+
*/
|
|
3088
|
+
function resolveExactNumericConstants(expression) {
|
|
3089
|
+
let folded;
|
|
3090
|
+
try {
|
|
3091
|
+
folded = exactConstantFold(expression);
|
|
3092
|
+
}
|
|
3093
|
+
catch {
|
|
3094
|
+
folded = undefined;
|
|
3095
|
+
}
|
|
3096
|
+
if (folded?.seeded === true) {
|
|
3097
|
+
const value = folded.value;
|
|
3098
|
+
if (value === null || typeof value === "number")
|
|
3099
|
+
return { kind: "literal", value };
|
|
3100
|
+
const fits = exactNumericAsNumber(value);
|
|
3101
|
+
if (fits !== undefined)
|
|
3102
|
+
return { kind: "literal", value: fits };
|
|
3103
|
+
return { kind: "literal", value, internalSqlValue: true, sqlDomain: { kind: "numeric" } };
|
|
3104
|
+
}
|
|
3105
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
3106
|
+
resolvePlanExactNumericConstants(expression.block);
|
|
3107
|
+
return expression;
|
|
3108
|
+
}
|
|
3109
|
+
if (expression.kind === "window") {
|
|
3110
|
+
return {
|
|
3111
|
+
...expression,
|
|
3112
|
+
partitionBy: expression.partitionBy.map(resolveExactNumericConstants),
|
|
3113
|
+
orderBy: expression.orderBy.map((order) => ({
|
|
3114
|
+
...order,
|
|
3115
|
+
expression: resolveExactNumericConstants(order.expression),
|
|
3116
|
+
})),
|
|
3117
|
+
...(expression.argument === undefined
|
|
3118
|
+
? {}
|
|
3119
|
+
: { argument: resolveExactNumericConstants(expression.argument) }),
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
return mapChildExpressions(expression, resolveExactNumericConstants);
|
|
3123
|
+
}
|
|
3124
|
+
/** Applies `resolveExactNumericConstants` to every expression of a plan, nested blocks included. */
|
|
3125
|
+
function resolvePlanExactNumericConstants(plan) {
|
|
3126
|
+
mapBlockExpressions(plan, resolveExactNumericConstants);
|
|
3127
|
+
forEachNestedBlock(plan, resolvePlanExactNumericConstants);
|
|
3128
|
+
}
|
|
3129
|
+
/**
|
|
3130
|
+
* The statement counterpart of `resolvePlanExactNumericConstants`: every expression slot a
|
|
3131
|
+
* mutation evaluates later. INSERT VALUES cells are absent because the parser evaluates them
|
|
3132
|
+
* to values on the spot, resolving each expression first.
|
|
3133
|
+
*/
|
|
3134
|
+
function resolveStatementExactNumericConstants(statement) {
|
|
3135
|
+
const resolve = resolveExactNumericConstants;
|
|
3136
|
+
if (statement.kind === "insert") {
|
|
3137
|
+
const conflict = statement.onConflict;
|
|
3138
|
+
if (conflict?.assignments !== undefined) {
|
|
3139
|
+
for (const assignment of conflict.assignments) {
|
|
3140
|
+
assignment.expression = resolve(assignment.expression);
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
if (conflict?.where !== undefined)
|
|
3144
|
+
conflict.where = resolve(conflict.where);
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
if (statement.kind === "update" || statement.kind === "delete") {
|
|
3148
|
+
if (statement.kind === "update") {
|
|
3149
|
+
for (const assignment of statement.assignments) {
|
|
3150
|
+
assignment.expression = resolve(assignment.expression);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
for (const predicate of statement.predicates) {
|
|
3154
|
+
predicate.left = resolve(predicate.left);
|
|
3155
|
+
predicate.right = resolve(predicate.right);
|
|
3156
|
+
}
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
if (statement.kind === "merge") {
|
|
3160
|
+
statement.on = resolve(statement.on);
|
|
3161
|
+
for (const branch of statement.branches) {
|
|
3162
|
+
if (branch.condition !== undefined)
|
|
3163
|
+
branch.condition = resolve(branch.condition);
|
|
3164
|
+
if (branch.action.kind === "update") {
|
|
3165
|
+
for (const assignment of branch.action.assignments) {
|
|
3166
|
+
assignment.expression = resolve(assignment.expression);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
if (branch.action.kind === "insert") {
|
|
3170
|
+
branch.action.values = branch.action.values.map(resolve);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
2994
3175
|
/**
|
|
2995
3176
|
* Aggregates the frame members named by position, for the frames whose rows are not contiguous.
|
|
2996
3177
|
* EXCLUDE puts a hole in the middle of a frame, which the prefix sums the common path uses
|
|
2997
3178
|
* cannot represent, so those positions walk their members instead.
|
|
2998
3179
|
*/
|
|
2999
|
-
function aggregateWindowMembers(window, values, members) {
|
|
3180
|
+
function aggregateWindowMembers(window, values, members, avgScale) {
|
|
3000
3181
|
const present = members.filter((member) => {
|
|
3001
3182
|
const value = values[member];
|
|
3002
3183
|
return value !== null && value !== undefined;
|
|
@@ -3026,7 +3207,9 @@ function aggregateWindowMembers(window, values, members) {
|
|
|
3026
3207
|
throw new Error("Exact NUMERIC sum disappeared");
|
|
3027
3208
|
total = next;
|
|
3028
3209
|
}
|
|
3029
|
-
return window.name === "SUM"
|
|
3210
|
+
return window.name === "SUM"
|
|
3211
|
+
? total
|
|
3212
|
+
: exactNumericBinary("/", total, present.length, avgScale);
|
|
3030
3213
|
}
|
|
3031
3214
|
const total = present.reduce((sum, member) => sum + numeric(values[member]), 0);
|
|
3032
3215
|
return window.name === "SUM" ? total : total / present.length;
|
|
@@ -3051,7 +3234,7 @@ function aggregateWindowMembers(window, values, members) {
|
|
|
3051
3234
|
* RANGE frames take only UNBOUNDED and CURRENT ROW bounds, where CURRENT ROW spans the peer
|
|
3052
3235
|
* group. COUNT of an empty frame is 0 and every other aggregate NULL.
|
|
3053
3236
|
*/
|
|
3054
|
-
function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys) {
|
|
3237
|
+
function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys, avgScale) {
|
|
3055
3238
|
const frame = window.frame ?? {
|
|
3056
3239
|
unit: "range",
|
|
3057
3240
|
start: { kind: "unbounded-preceding" },
|
|
@@ -3063,11 +3246,11 @@ function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKey
|
|
|
3063
3246
|
while (end < indexes.length && samePartition(indexes[start] ?? 0, indexes[end] ?? 0)) {
|
|
3064
3247
|
end += 1;
|
|
3065
3248
|
}
|
|
3066
|
-
applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end);
|
|
3249
|
+
applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end, avgScale);
|
|
3067
3250
|
start = end;
|
|
3068
3251
|
}
|
|
3069
3252
|
}
|
|
3070
|
-
function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end) {
|
|
3253
|
+
function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end, avgScale) {
|
|
3071
3254
|
const size = end - start;
|
|
3072
3255
|
// Peer-group bounds per position; with no OVER ordering the whole partition is one peer group.
|
|
3073
3256
|
const peerStart = new Array(size).fill(0);
|
|
@@ -3198,7 +3381,7 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
|
|
|
3198
3381
|
if (!dropped)
|
|
3199
3382
|
members.push(member);
|
|
3200
3383
|
}
|
|
3201
|
-
value = aggregateWindowMembers(window, values, members);
|
|
3384
|
+
value = aggregateWindowMembers(window, values, members, avgScale);
|
|
3202
3385
|
const row = rows[indexes[start + position] ?? -1];
|
|
3203
3386
|
if (row !== undefined)
|
|
3204
3387
|
row[window.alias] = asQueryValue(value);
|
|
@@ -3230,7 +3413,7 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
|
|
|
3230
3413
|
}
|
|
3231
3414
|
else if (prefixExact !== undefined) {
|
|
3232
3415
|
const total = exactNumericBinary("-", prefixExact[high] ?? exactZero, prefixExact[low] ?? exactZero);
|
|
3233
|
-
value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull);
|
|
3416
|
+
value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull, avgScale);
|
|
3234
3417
|
}
|
|
3235
3418
|
else {
|
|
3236
3419
|
const total = (prefixSum?.[high] ?? 0) - (prefixSum?.[low] ?? 0);
|
|
@@ -3407,7 +3590,13 @@ export function applyWindowFunctions(result, windows, options = {}) {
|
|
|
3407
3590
|
continue;
|
|
3408
3591
|
}
|
|
3409
3592
|
if (window.name !== "ROW_NUMBER" && window.name !== "RANK" && window.name !== "DENSE_RANK") {
|
|
3410
|
-
|
|
3593
|
+
// PostgreSQL computes a window AVG's quotient to at least the summed values' display
|
|
3594
|
+
// scale; canonical NUMERIC values no longer carry it, so read the argument column's
|
|
3595
|
+
// declared scale from the inner result the way the summed dscale would have carried it.
|
|
3596
|
+
const argumentDomain = window.name === "AVG" && window.argumentAlias !== undefined
|
|
3597
|
+
? result.columnDomains[result.columns.indexOf(window.argumentAlias)]
|
|
3598
|
+
: undefined;
|
|
3599
|
+
applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys, argumentDomain?.kind === "numeric" ? argumentDomain.scale : undefined);
|
|
3411
3600
|
continue;
|
|
3412
3601
|
}
|
|
3413
3602
|
let rowNumber = 0;
|
|
@@ -3819,7 +4008,7 @@ function evaluate(expression, context, group) {
|
|
|
3819
4008
|
if (typeof left !== "string" || typeof right !== "string") {
|
|
3820
4009
|
throw new TypeError("|| requires string operands");
|
|
3821
4010
|
}
|
|
3822
|
-
return
|
|
4011
|
+
return concatenatedSqlValue(left, right);
|
|
3823
4012
|
}
|
|
3824
4013
|
const exact = exactNumericBinary(expression.operator, left, right);
|
|
3825
4014
|
if (exact !== undefined)
|
|
@@ -3963,7 +4152,8 @@ function evaluate(expression, context, group) {
|
|
|
3963
4152
|
? null
|
|
3964
4153
|
: (() => {
|
|
3965
4154
|
const sum = sumNumericValues(values);
|
|
3966
|
-
return exactNumericBinary("/", sum, values.length) ??
|
|
4155
|
+
return (exactNumericBinary("/", sum, values.length, expression.avgArgumentScale) ??
|
|
4156
|
+
numeric(sum) / values.length);
|
|
3967
4157
|
})();
|
|
3968
4158
|
if (expression.name === "MIN")
|
|
3969
4159
|
return values.reduce((best, value) => (best === undefined || compareValues(value, best) < 0 ? value : best), undefined);
|
|
@@ -4416,6 +4606,11 @@ export function hasAggregate(expression) {
|
|
|
4416
4606
|
}
|
|
4417
4607
|
return childExpressions(expression).some(hasAggregate);
|
|
4418
4608
|
}
|
|
4609
|
+
function hasWindow(expression) {
|
|
4610
|
+
if (expression.kind === "window")
|
|
4611
|
+
return true;
|
|
4612
|
+
return childExpressions(expression).some(hasWindow);
|
|
4613
|
+
}
|
|
4419
4614
|
/** One root aggregate call, using the same canonical set as parsing and execution. */
|
|
4420
4615
|
export function isAggregateCall(expression) {
|
|
4421
4616
|
return expression.kind === "call" && aggregateNames.has(expression.name);
|
|
@@ -5308,6 +5503,14 @@ class Parser {
|
|
|
5308
5503
|
},
|
|
5309
5504
|
};
|
|
5310
5505
|
}
|
|
5506
|
+
/** A type name's precision, scale, or width: plain digits, as PostgreSQL requires there. */
|
|
5507
|
+
#typeWidth() {
|
|
5508
|
+
const token = this.#take("number");
|
|
5509
|
+
if (!/^\d+$/.test(token.text)) {
|
|
5510
|
+
throw new SqlCompileError(`A type width must be an integer: ${token.text}`, token.start, token.end - token.start);
|
|
5511
|
+
}
|
|
5512
|
+
return Number(token.text);
|
|
5513
|
+
}
|
|
5311
5514
|
/** A CAST target: the SqlColumnType, or "number-integer" for the truncating integer names. */
|
|
5312
5515
|
#castTarget() {
|
|
5313
5516
|
const word = this.#identifier().toUpperCase();
|
|
@@ -5315,9 +5518,9 @@ class Parser {
|
|
|
5315
5518
|
let precision;
|
|
5316
5519
|
let scale;
|
|
5317
5520
|
if (this.#punctuation("(")) {
|
|
5318
|
-
precision =
|
|
5521
|
+
precision = this.#typeWidth();
|
|
5319
5522
|
if (this.#punctuation(","))
|
|
5320
|
-
scale =
|
|
5523
|
+
scale = this.#typeWidth();
|
|
5321
5524
|
else
|
|
5322
5525
|
scale = 0;
|
|
5323
5526
|
this.#expectPunctuation(")");
|
|
@@ -5335,9 +5538,9 @@ class Parser {
|
|
|
5335
5538
|
if (mapped === undefined)
|
|
5336
5539
|
throw new TypeError(`Unsupported CAST target: ${word}`);
|
|
5337
5540
|
if (this.#punctuation("(")) {
|
|
5338
|
-
this.#
|
|
5541
|
+
this.#typeWidth();
|
|
5339
5542
|
if (this.#punctuation(","))
|
|
5340
|
-
this.#
|
|
5543
|
+
this.#typeWidth();
|
|
5341
5544
|
this.#expectPunctuation(")");
|
|
5342
5545
|
}
|
|
5343
5546
|
const integer = word === "INTEGER" || word === "INT" || word === "BIGINT" || word === "SMALLINT";
|
|
@@ -5350,8 +5553,8 @@ class Parser {
|
|
|
5350
5553
|
let precision;
|
|
5351
5554
|
let scale;
|
|
5352
5555
|
if (this.#punctuation("(")) {
|
|
5353
|
-
precision =
|
|
5354
|
-
scale = this.#punctuation(",") ?
|
|
5556
|
+
precision = this.#typeWidth();
|
|
5557
|
+
scale = this.#punctuation(",") ? this.#typeWidth() : 0;
|
|
5355
5558
|
this.#expectPunctuation(")");
|
|
5356
5559
|
}
|
|
5357
5560
|
return {
|
|
@@ -5376,7 +5579,7 @@ class Parser {
|
|
|
5376
5579
|
}
|
|
5377
5580
|
// Character widths document intent and do not truncate values.
|
|
5378
5581
|
if (this.#punctuation("(")) {
|
|
5379
|
-
this.#
|
|
5582
|
+
this.#typeWidth();
|
|
5380
5583
|
if (this.#punctuation(",")) {
|
|
5381
5584
|
throw new TypeError(`${word} takes one width`);
|
|
5382
5585
|
}
|
|
@@ -5396,6 +5599,7 @@ class Parser {
|
|
|
5396
5599
|
? this.#updateStatement()
|
|
5397
5600
|
: this.#deleteStatement();
|
|
5398
5601
|
this.#take("eof");
|
|
5602
|
+
resolveStatementExactNumericConstants(statement);
|
|
5399
5603
|
return statement;
|
|
5400
5604
|
}
|
|
5401
5605
|
#insertStatement() {
|
|
@@ -5428,7 +5632,9 @@ class Parser {
|
|
|
5428
5632
|
};
|
|
5429
5633
|
}
|
|
5430
5634
|
if (this.#isKeyword("SELECT")) {
|
|
5431
|
-
const
|
|
5635
|
+
const insertSource = this.#selectBlock("(insert select)");
|
|
5636
|
+
resolvePlanExactNumericConstants(insertSource);
|
|
5637
|
+
const query = optimizePlan(insertSource);
|
|
5432
5638
|
if (query.select.some((item) => item.expression.kind === "wildcard")) {
|
|
5433
5639
|
throw new TypeError("INSERT ... SELECT requires an explicit select list");
|
|
5434
5640
|
}
|
|
@@ -5691,7 +5897,9 @@ class Parser {
|
|
|
5691
5897
|
throw new TypeError("MERGE does not support RETURNING; read the rows back with a SELECT");
|
|
5692
5898
|
}
|
|
5693
5899
|
this.#take("eof");
|
|
5694
|
-
|
|
5900
|
+
const statement = { kind: "merge", table, alias, source, on, branches };
|
|
5901
|
+
resolveStatementExactNumericConstants(statement);
|
|
5902
|
+
return statement;
|
|
5695
5903
|
}
|
|
5696
5904
|
#updateStatement() {
|
|
5697
5905
|
this.#keyword("UPDATE");
|
|
@@ -5751,7 +5959,7 @@ class Parser {
|
|
|
5751
5959
|
if (hasAggregate(expression) || expressionColumns(expression).length > 0) {
|
|
5752
5960
|
throw new TypeError(`${label} must be constant expressions`);
|
|
5753
5961
|
}
|
|
5754
|
-
return asQueryValue(evaluate(expression, {}));
|
|
5962
|
+
return asQueryValue(evaluate(resolveExactNumericConstants(expression), {}));
|
|
5755
5963
|
}
|
|
5756
5964
|
#unionMember(sql) {
|
|
5757
5965
|
if (this.#punctuation("(")) {
|
|
@@ -5959,19 +6167,6 @@ class Parser {
|
|
|
5959
6167
|
}
|
|
5960
6168
|
const joins = [];
|
|
5961
6169
|
let rightJoins = 0;
|
|
5962
|
-
/** A cross join: the nested-loop path with a condition every row pair satisfies. */
|
|
5963
|
-
const crossJoin = (source) => ({
|
|
5964
|
-
...source,
|
|
5965
|
-
kind: "inner",
|
|
5966
|
-
left: { kind: "literal", value: null },
|
|
5967
|
-
right: { kind: "literal", value: null },
|
|
5968
|
-
on: {
|
|
5969
|
-
kind: "condition",
|
|
5970
|
-
operator: "=",
|
|
5971
|
-
left: { kind: "literal", value: 1 },
|
|
5972
|
-
right: { kind: "literal", value: 1 },
|
|
5973
|
-
},
|
|
5974
|
-
});
|
|
5975
6170
|
while (this.#peek().text === "," ||
|
|
5976
6171
|
this.#isKeyword("NATURAL") ||
|
|
5977
6172
|
this.#isKeyword("JOIN") ||
|
|
@@ -5982,13 +6177,13 @@ class Parser {
|
|
|
5982
6177
|
this.#isKeyword("CROSS")) {
|
|
5983
6178
|
if (this.#punctuation(",")) {
|
|
5984
6179
|
// F041-07: a comma between table references is a cross join.
|
|
5985
|
-
joins.push(
|
|
6180
|
+
joins.push(crossJoinPlan(this.#source()));
|
|
5986
6181
|
continue;
|
|
5987
6182
|
}
|
|
5988
6183
|
if (this.#isKeyword("CROSS")) {
|
|
5989
6184
|
this.#keyword("CROSS");
|
|
5990
6185
|
this.#keyword("JOIN");
|
|
5991
|
-
joins.push(
|
|
6186
|
+
joins.push(crossJoinPlan(this.#source()));
|
|
5992
6187
|
continue;
|
|
5993
6188
|
}
|
|
5994
6189
|
let kind = "inner";
|
|
@@ -6932,6 +7127,11 @@ class Parser {
|
|
|
6932
7127
|
continue;
|
|
6933
7128
|
}
|
|
6934
7129
|
const operator = this.#peek().text;
|
|
7130
|
+
// A bracket after a complete expression is PostgreSQL's array subscript, which has no
|
|
7131
|
+
// other reading in this grammar — name the missing feature instead of "Expected eof".
|
|
7132
|
+
if (operator === "[") {
|
|
7133
|
+
throw new TypeError("Array subscripts are not supported");
|
|
7134
|
+
}
|
|
6935
7135
|
// || and the JSON arrows share PostgreSQL's loosest "any other operator" level, applying
|
|
6936
7136
|
// left-to-right to whole arithmetic terms: `'a' || d ->> 'k'` is `('a' || d) ->> 'k'`.
|
|
6937
7137
|
const precedence = operator === "*" || operator === "/" || operator === "%"
|
|
@@ -7021,10 +7221,29 @@ class Parser {
|
|
|
7021
7221
|
if (token.kind === "number") {
|
|
7022
7222
|
this.#index += 1;
|
|
7023
7223
|
const value = Number(token.text);
|
|
7024
|
-
|
|
7025
|
-
|
|
7224
|
+
// A safe integer spelled as one is exactly itself; every other spelling — a decimal
|
|
7225
|
+
// point, an exponent, or digits beyond 2^53 — is a numeric constant PostgreSQL would
|
|
7226
|
+
// type NUMERIC. Keep its exact digits: as an annotation when the value reads back
|
|
7227
|
+
// identically from a number, or as a tagged exact-NUMERIC literal when it would not.
|
|
7228
|
+
if (!/[.eE]/.test(token.text) && Number.isSafeInteger(value)) {
|
|
7229
|
+
return { kind: "literal", value };
|
|
7230
|
+
}
|
|
7231
|
+
let tagged;
|
|
7232
|
+
try {
|
|
7233
|
+
tagged = exactNumericLiteral(token.text);
|
|
7026
7234
|
}
|
|
7027
|
-
|
|
7235
|
+
catch (error) {
|
|
7236
|
+
throw new SqlCompileError(error instanceof Error ? error.message : `Invalid number: ${token.text}`, token.start, token.end - token.start);
|
|
7237
|
+
}
|
|
7238
|
+
const fits = exactNumericAsNumber(tagged);
|
|
7239
|
+
if (fits !== undefined)
|
|
7240
|
+
return { kind: "literal", value: fits, exactText: token.text };
|
|
7241
|
+
return {
|
|
7242
|
+
kind: "literal",
|
|
7243
|
+
value: tagged,
|
|
7244
|
+
internalSqlValue: true,
|
|
7245
|
+
sqlDomain: { kind: "numeric" },
|
|
7246
|
+
};
|
|
7028
7247
|
}
|
|
7029
7248
|
if (token.kind === "string") {
|
|
7030
7249
|
this.#index += 1;
|
|
@@ -8115,6 +8334,8 @@ export function assembleSelectBlock(parts, nextSequence) {
|
|
|
8115
8334
|
if (distinct) {
|
|
8116
8335
|
if (select.some((item) => hasAggregate(item.expression)))
|
|
8117
8336
|
throw new TypeError("SELECT DISTINCT cannot be combined with aggregate functions");
|
|
8337
|
+
if (select.some((item) => hasWindow(item.expression)))
|
|
8338
|
+
throw new TypeError("SELECT DISTINCT cannot be combined with window functions");
|
|
8118
8339
|
if (groupBy.length > 0)
|
|
8119
8340
|
throw new TypeError("SELECT DISTINCT cannot be combined with GROUP BY");
|
|
8120
8341
|
if (having.length > 0)
|
|
@@ -8193,7 +8414,7 @@ export function assembleSelectBlock(parts, nextSequence) {
|
|
|
8193
8414
|
* over those same columns provides the deduplication through the grouped executor. Runs
|
|
8194
8415
|
* exactly once per execution entry, like MATCH(*) expansion.
|
|
8195
8416
|
*/
|
|
8196
|
-
|
|
8417
|
+
function expandDistinctWildcard(plan, columnsOf) {
|
|
8197
8418
|
if (plan.distinctWildcard !== true)
|
|
8198
8419
|
return plan;
|
|
8199
8420
|
const shaped = [plan.base, ...plan.joins].map((source) => ({
|
|
@@ -8285,7 +8506,7 @@ export function planHasSourceColumnAliases(plan) {
|
|
|
8285
8506
|
* scan cannot know whether the next row ties — and the ordered result is trimmed here: rows up
|
|
8286
8507
|
* to the limit, plus every following row equal to the last one on all ORDER BY columns.
|
|
8287
8508
|
*/
|
|
8288
|
-
|
|
8509
|
+
function withTiesPlan(plan) {
|
|
8289
8510
|
// Ordering by an expression or an unselected column wraps the real block in a projection that
|
|
8290
8511
|
// hides the sort column. The tie test needs that column, so the inner block runs and the
|
|
8291
8512
|
// projection is applied after trimming instead of before.
|
|
@@ -8472,7 +8693,7 @@ export function expandSourceColumnAliases(plan, columnsOf) {
|
|
|
8472
8693
|
* one source, and `alias.column` when it reads several, so two sources cannot collide.
|
|
8473
8694
|
* Runs once per execution entry, like MATCH(*) and DISTINCT * expansion.
|
|
8474
8695
|
*/
|
|
8475
|
-
|
|
8696
|
+
function expandQualifiedWildcards(plan, columnsOf) {
|
|
8476
8697
|
const qualified = (block) => {
|
|
8477
8698
|
if (block.select.some((item) => item.expression.kind === "wildcard" && item.expression.table))
|
|
8478
8699
|
return true;
|
|
@@ -8748,7 +8969,7 @@ function jsonTableColumnValue(column, value) {
|
|
|
8748
8969
|
}
|
|
8749
8970
|
return date;
|
|
8750
8971
|
}
|
|
8751
|
-
|
|
8972
|
+
function timestampLiteral(text) {
|
|
8752
8973
|
const trimmed = text.trim();
|
|
8753
8974
|
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);
|
|
8754
8975
|
if (match === null)
|
|
@@ -8922,13 +9143,14 @@ function defaultAlias(expression) {
|
|
|
8922
9143
|
return "expression";
|
|
8923
9144
|
}
|
|
8924
9145
|
/**
|
|
8925
|
-
* Whether a numeric literal's digits are well formed: at most one decimal point
|
|
8926
|
-
* and underscores only between digits, never leading, trailing, or
|
|
9146
|
+
* Whether a numeric literal's digits are well formed: at most one decimal point and an optional
|
|
9147
|
+
* exponent (radix 10 only), and underscores only between digits, never leading, trailing, or
|
|
9148
|
+
* doubled (T662). Exponent digits are plain, as in PostgreSQL.
|
|
8927
9149
|
*/
|
|
8928
9150
|
function validNumericLiteral(text, radix) {
|
|
8929
9151
|
const digits = radix === 16 ? "0-9a-fA-F" : radix === 8 ? "0-7" : radix === 2 ? "01" : "0-9";
|
|
8930
9152
|
const group = `[${digits}]+(?:_[${digits}]+)*`;
|
|
8931
|
-
const pattern = radix === 10 ? `^${group}(?:\\.${group})?$` : `^${group}$`;
|
|
9153
|
+
const pattern = radix === 10 ? `^${group}(?:\\.${group})?(?:[eE][+-]?\\d+)?$` : `^${group}$`;
|
|
8932
9154
|
return new RegExp(pattern).test(text);
|
|
8933
9155
|
}
|
|
8934
9156
|
function tokenize(sql) {
|
|
@@ -8986,6 +9208,16 @@ function tokenize(sql) {
|
|
|
8986
9208
|
// T662: underscores may separate digits.
|
|
8987
9209
|
while (index < sql.length && /[\d._]/.test(sql[index] ?? ""))
|
|
8988
9210
|
index += 1;
|
|
9211
|
+
// Scientific notation: an exponent marker joins the token only when digits follow, so
|
|
9212
|
+
// `1e2` is the numeric constant 100 while `SELECT 1 e` still reads `e` as an alias.
|
|
9213
|
+
if (/[eE]/.test(sql[index] ?? "")) {
|
|
9214
|
+
const signed = /[+-]/.test(sql[index + 1] ?? "") ? 1 : 0;
|
|
9215
|
+
if (/\d/.test(sql[index + 1 + signed] ?? "")) {
|
|
9216
|
+
index += 2 + signed;
|
|
9217
|
+
while (index < sql.length && /\d/.test(sql[index] ?? ""))
|
|
9218
|
+
index += 1;
|
|
9219
|
+
}
|
|
9220
|
+
}
|
|
8989
9221
|
const text = sql.slice(start, index);
|
|
8990
9222
|
if (!validNumericLiteral(text, 10))
|
|
8991
9223
|
throw new SqlCompileError(`Invalid number: ${text}`, start, index - start);
|
|
@@ -13,7 +13,7 @@ import type { QueryResult, QueryRow, QueryValue } from "./query.js";
|
|
|
13
13
|
* a byte mask, and a column name like `__proto__` comes back as the own property the engine
|
|
14
14
|
* defined — never as a prototype assignment.
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
type WireResultColumn = {
|
|
17
17
|
kind: "number";
|
|
18
18
|
values: Float64Array;
|
|
19
19
|
nulls?: Uint8Array;
|
|
@@ -67,3 +67,4 @@ export declare function encodeQueryResult(result: QueryResult): EncodedQueryResu
|
|
|
67
67
|
export declare function encodeQueryRows(rows: readonly QueryRow[]): EncodedQueryResult;
|
|
68
68
|
export declare function decodeQueryResult(payload: unknown): QueryResult;
|
|
69
69
|
export declare function isWireQueryResult(value: unknown): value is WireQueryResult;
|
|
70
|
+
export {};
|