@minnowdb/core 0.6.5 → 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 +58 -34
- 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 +9 -2
- 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 +280 -39
- 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 +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/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 +15 -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 +8 -3
- package/sql-feature-matrix.json +71 -11
package/dist/engine/query.js
CHANGED
|
@@ -9,7 +9,7 @@ import { stringArgument } from "./sql-semantics.js";
|
|
|
9
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, externalSqlDomainColumnValue, 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) {
|
|
@@ -81,7 +81,7 @@ export const volatileScalarFunctionNames = new Set([
|
|
|
81
81
|
* reading, so they are resolved once per execution rather than evaluated per row: every row of
|
|
82
82
|
* one statement sees one instant, both executors agree, and constant folding leaves them alone.
|
|
83
83
|
*/
|
|
84
|
-
|
|
84
|
+
const statementDatetimeNames = new Set([
|
|
85
85
|
"CURRENT_DATE",
|
|
86
86
|
"CURRENT_TIMESTAMP",
|
|
87
87
|
"LOCALTIME",
|
|
@@ -607,7 +607,7 @@ function assertReplacementResultLength(source, search, replacement) {
|
|
|
607
607
|
throw new RangeError(`REPLACE result exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
|
|
608
608
|
}
|
|
609
609
|
}
|
|
610
|
-
|
|
610
|
+
const dateTruncUnits = new Set([
|
|
611
611
|
"year",
|
|
612
612
|
"quarter",
|
|
613
613
|
"month",
|
|
@@ -637,7 +637,7 @@ const intervalUnits = new Map([
|
|
|
637
637
|
* rather than converted, because a month is not a fixed number of them: adding one to January 31
|
|
638
638
|
* has to land on the end of February, which only calendar arithmetic can do.
|
|
639
639
|
*/
|
|
640
|
-
|
|
640
|
+
function intervalLiteral(text) {
|
|
641
641
|
let months = 0;
|
|
642
642
|
let milliseconds = 0;
|
|
643
643
|
let matched = 0;
|
|
@@ -663,7 +663,7 @@ export function intervalLiteral(text) {
|
|
|
663
663
|
* does not exist in the target month clamps to that month's last day, which is what both SQLite
|
|
664
664
|
* and PostgreSQL do with 31 January plus a month.
|
|
665
665
|
*/
|
|
666
|
-
|
|
666
|
+
function dateAddValue(value, months, milliseconds) {
|
|
667
667
|
if (value === null || value === undefined)
|
|
668
668
|
return null;
|
|
669
669
|
const calendarDate = isDateDomainValue(value);
|
|
@@ -690,7 +690,7 @@ export function dateAddValue(value, months, milliseconds) {
|
|
|
690
690
|
? dateDomainValue(dateIsoString(result).slice(0, 10))
|
|
691
691
|
: result;
|
|
692
692
|
}
|
|
693
|
-
|
|
693
|
+
function dateTruncValue(unit, value) {
|
|
694
694
|
if (typeof unit !== "string" || !dateTruncUnits.has(unit.toLowerCase())) {
|
|
695
695
|
throw new TypeError("DATE_TRUNC requires a unit of year, quarter, month, week, day, hour, minute, or second");
|
|
696
696
|
}
|
|
@@ -888,6 +888,7 @@ export function compileQuery(sql, options = {}) {
|
|
|
888
888
|
}
|
|
889
889
|
let compiled;
|
|
890
890
|
try {
|
|
891
|
+
resolvePlanExactNumericConstants(plan);
|
|
891
892
|
compiled = options.optimize === false ? plan : optimizePlan(plan);
|
|
892
893
|
}
|
|
893
894
|
catch (error) {
|
|
@@ -995,7 +996,7 @@ export function compileCheckExpression(sql, name) {
|
|
|
995
996
|
if (hasAggregate(expression) || containsWindow(expression) || containsParameter(expression)) {
|
|
996
997
|
throw new TypeError(`CHECK ${name} takes a row condition over this table's own columns`);
|
|
997
998
|
}
|
|
998
|
-
return expression;
|
|
999
|
+
return resolveExactNumericConstants(expression);
|
|
999
1000
|
}
|
|
1000
1001
|
/**
|
|
1001
1002
|
* Parses and type-checks one catalog default. PostgreSQL defaults are variable-free scalar
|
|
@@ -1529,7 +1530,7 @@ export function blockHasSubqueries(plan) {
|
|
|
1529
1530
|
return blockHas(plan);
|
|
1530
1531
|
}
|
|
1531
1532
|
/** True when any `?`/`$n` placeholder remains anywhere in the expression tree. */
|
|
1532
|
-
|
|
1533
|
+
function containsParameter(expression) {
|
|
1533
1534
|
if (expression.kind === "parameter")
|
|
1534
1535
|
return true;
|
|
1535
1536
|
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
@@ -1537,7 +1538,7 @@ export function containsParameter(expression) {
|
|
|
1537
1538
|
}
|
|
1538
1539
|
return childExpressions(expression).some(containsParameter);
|
|
1539
1540
|
}
|
|
1540
|
-
|
|
1541
|
+
function blockHasParameters(block) {
|
|
1541
1542
|
if (block.limitParameter !== undefined ||
|
|
1542
1543
|
block.offsetParameter !== undefined ||
|
|
1543
1544
|
(block.limitValidationParameters?.length ?? 0) > 0 ||
|
|
@@ -2991,12 +2992,188 @@ export function expandFtsColumns(plan, searchableColumnsFor) {
|
|
|
2991
2992
|
expandBlock(plan);
|
|
2992
2993
|
return plan;
|
|
2993
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
|
+
}
|
|
2994
3171
|
/**
|
|
2995
3172
|
* Aggregates the frame members named by position, for the frames whose rows are not contiguous.
|
|
2996
3173
|
* EXCLUDE puts a hole in the middle of a frame, which the prefix sums the common path uses
|
|
2997
3174
|
* cannot represent, so those positions walk their members instead.
|
|
2998
3175
|
*/
|
|
2999
|
-
function aggregateWindowMembers(window, values, members) {
|
|
3176
|
+
function aggregateWindowMembers(window, values, members, avgScale) {
|
|
3000
3177
|
const present = members.filter((member) => {
|
|
3001
3178
|
const value = values[member];
|
|
3002
3179
|
return value !== null && value !== undefined;
|
|
@@ -3026,7 +3203,9 @@ function aggregateWindowMembers(window, values, members) {
|
|
|
3026
3203
|
throw new Error("Exact NUMERIC sum disappeared");
|
|
3027
3204
|
total = next;
|
|
3028
3205
|
}
|
|
3029
|
-
return window.name === "SUM"
|
|
3206
|
+
return window.name === "SUM"
|
|
3207
|
+
? total
|
|
3208
|
+
: exactNumericBinary("/", total, present.length, avgScale);
|
|
3030
3209
|
}
|
|
3031
3210
|
const total = present.reduce((sum, member) => sum + numeric(values[member]), 0);
|
|
3032
3211
|
return window.name === "SUM" ? total : total / present.length;
|
|
@@ -3051,7 +3230,7 @@ function aggregateWindowMembers(window, values, members) {
|
|
|
3051
3230
|
* RANGE frames take only UNBOUNDED and CURRENT ROW bounds, where CURRENT ROW spans the peer
|
|
3052
3231
|
* group. COUNT of an empty frame is 0 and every other aggregate NULL.
|
|
3053
3232
|
*/
|
|
3054
|
-
function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys) {
|
|
3233
|
+
function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys, avgScale) {
|
|
3055
3234
|
const frame = window.frame ?? {
|
|
3056
3235
|
unit: "range",
|
|
3057
3236
|
start: { kind: "unbounded-preceding" },
|
|
@@ -3063,11 +3242,11 @@ function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKey
|
|
|
3063
3242
|
while (end < indexes.length && samePartition(indexes[start] ?? 0, indexes[end] ?? 0)) {
|
|
3064
3243
|
end += 1;
|
|
3065
3244
|
}
|
|
3066
|
-
applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end);
|
|
3245
|
+
applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end, avgScale);
|
|
3067
3246
|
start = end;
|
|
3068
3247
|
}
|
|
3069
3248
|
}
|
|
3070
|
-
function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end) {
|
|
3249
|
+
function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end, avgScale) {
|
|
3071
3250
|
const size = end - start;
|
|
3072
3251
|
// Peer-group bounds per position; with no OVER ordering the whole partition is one peer group.
|
|
3073
3252
|
const peerStart = new Array(size).fill(0);
|
|
@@ -3198,7 +3377,7 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
|
|
|
3198
3377
|
if (!dropped)
|
|
3199
3378
|
members.push(member);
|
|
3200
3379
|
}
|
|
3201
|
-
value = aggregateWindowMembers(window, values, members);
|
|
3380
|
+
value = aggregateWindowMembers(window, values, members, avgScale);
|
|
3202
3381
|
const row = rows[indexes[start + position] ?? -1];
|
|
3203
3382
|
if (row !== undefined)
|
|
3204
3383
|
row[window.alias] = asQueryValue(value);
|
|
@@ -3230,7 +3409,7 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
|
|
|
3230
3409
|
}
|
|
3231
3410
|
else if (prefixExact !== undefined) {
|
|
3232
3411
|
const total = exactNumericBinary("-", prefixExact[high] ?? exactZero, prefixExact[low] ?? exactZero);
|
|
3233
|
-
value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull);
|
|
3412
|
+
value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull, avgScale);
|
|
3234
3413
|
}
|
|
3235
3414
|
else {
|
|
3236
3415
|
const total = (prefixSum?.[high] ?? 0) - (prefixSum?.[low] ?? 0);
|
|
@@ -3407,7 +3586,13 @@ export function applyWindowFunctions(result, windows, options = {}) {
|
|
|
3407
3586
|
continue;
|
|
3408
3587
|
}
|
|
3409
3588
|
if (window.name !== "ROW_NUMBER" && window.name !== "RANK" && window.name !== "DENSE_RANK") {
|
|
3410
|
-
|
|
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);
|
|
3411
3596
|
continue;
|
|
3412
3597
|
}
|
|
3413
3598
|
let rowNumber = 0;
|
|
@@ -3819,7 +4004,7 @@ function evaluate(expression, context, group) {
|
|
|
3819
4004
|
if (typeof left !== "string" || typeof right !== "string") {
|
|
3820
4005
|
throw new TypeError("|| requires string operands");
|
|
3821
4006
|
}
|
|
3822
|
-
return
|
|
4007
|
+
return concatenatedSqlValue(left, right);
|
|
3823
4008
|
}
|
|
3824
4009
|
const exact = exactNumericBinary(expression.operator, left, right);
|
|
3825
4010
|
if (exact !== undefined)
|
|
@@ -3963,7 +4148,8 @@ function evaluate(expression, context, group) {
|
|
|
3963
4148
|
? null
|
|
3964
4149
|
: (() => {
|
|
3965
4150
|
const sum = sumNumericValues(values);
|
|
3966
|
-
return exactNumericBinary("/", sum, values.length) ??
|
|
4151
|
+
return (exactNumericBinary("/", sum, values.length, expression.avgArgumentScale) ??
|
|
4152
|
+
numeric(sum) / values.length);
|
|
3967
4153
|
})();
|
|
3968
4154
|
if (expression.name === "MIN")
|
|
3969
4155
|
return values.reduce((best, value) => (best === undefined || compareValues(value, best) < 0 ? value : best), undefined);
|
|
@@ -4416,6 +4602,11 @@ export function hasAggregate(expression) {
|
|
|
4416
4602
|
}
|
|
4417
4603
|
return childExpressions(expression).some(hasAggregate);
|
|
4418
4604
|
}
|
|
4605
|
+
function hasWindow(expression) {
|
|
4606
|
+
if (expression.kind === "window")
|
|
4607
|
+
return true;
|
|
4608
|
+
return childExpressions(expression).some(hasWindow);
|
|
4609
|
+
}
|
|
4419
4610
|
/** One root aggregate call, using the same canonical set as parsing and execution. */
|
|
4420
4611
|
export function isAggregateCall(expression) {
|
|
4421
4612
|
return expression.kind === "call" && aggregateNames.has(expression.name);
|
|
@@ -5308,6 +5499,14 @@ class Parser {
|
|
|
5308
5499
|
},
|
|
5309
5500
|
};
|
|
5310
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
|
+
}
|
|
5311
5510
|
/** A CAST target: the SqlColumnType, or "number-integer" for the truncating integer names. */
|
|
5312
5511
|
#castTarget() {
|
|
5313
5512
|
const word = this.#identifier().toUpperCase();
|
|
@@ -5315,9 +5514,9 @@ class Parser {
|
|
|
5315
5514
|
let precision;
|
|
5316
5515
|
let scale;
|
|
5317
5516
|
if (this.#punctuation("(")) {
|
|
5318
|
-
precision =
|
|
5517
|
+
precision = this.#typeWidth();
|
|
5319
5518
|
if (this.#punctuation(","))
|
|
5320
|
-
scale =
|
|
5519
|
+
scale = this.#typeWidth();
|
|
5321
5520
|
else
|
|
5322
5521
|
scale = 0;
|
|
5323
5522
|
this.#expectPunctuation(")");
|
|
@@ -5335,9 +5534,9 @@ class Parser {
|
|
|
5335
5534
|
if (mapped === undefined)
|
|
5336
5535
|
throw new TypeError(`Unsupported CAST target: ${word}`);
|
|
5337
5536
|
if (this.#punctuation("(")) {
|
|
5338
|
-
this.#
|
|
5537
|
+
this.#typeWidth();
|
|
5339
5538
|
if (this.#punctuation(","))
|
|
5340
|
-
this.#
|
|
5539
|
+
this.#typeWidth();
|
|
5341
5540
|
this.#expectPunctuation(")");
|
|
5342
5541
|
}
|
|
5343
5542
|
const integer = word === "INTEGER" || word === "INT" || word === "BIGINT" || word === "SMALLINT";
|
|
@@ -5350,8 +5549,8 @@ class Parser {
|
|
|
5350
5549
|
let precision;
|
|
5351
5550
|
let scale;
|
|
5352
5551
|
if (this.#punctuation("(")) {
|
|
5353
|
-
precision =
|
|
5354
|
-
scale = this.#punctuation(",") ?
|
|
5552
|
+
precision = this.#typeWidth();
|
|
5553
|
+
scale = this.#punctuation(",") ? this.#typeWidth() : 0;
|
|
5355
5554
|
this.#expectPunctuation(")");
|
|
5356
5555
|
}
|
|
5357
5556
|
return {
|
|
@@ -5376,7 +5575,7 @@ class Parser {
|
|
|
5376
5575
|
}
|
|
5377
5576
|
// Character widths document intent and do not truncate values.
|
|
5378
5577
|
if (this.#punctuation("(")) {
|
|
5379
|
-
this.#
|
|
5578
|
+
this.#typeWidth();
|
|
5380
5579
|
if (this.#punctuation(",")) {
|
|
5381
5580
|
throw new TypeError(`${word} takes one width`);
|
|
5382
5581
|
}
|
|
@@ -5396,6 +5595,7 @@ class Parser {
|
|
|
5396
5595
|
? this.#updateStatement()
|
|
5397
5596
|
: this.#deleteStatement();
|
|
5398
5597
|
this.#take("eof");
|
|
5598
|
+
resolveStatementExactNumericConstants(statement);
|
|
5399
5599
|
return statement;
|
|
5400
5600
|
}
|
|
5401
5601
|
#insertStatement() {
|
|
@@ -5428,7 +5628,9 @@ class Parser {
|
|
|
5428
5628
|
};
|
|
5429
5629
|
}
|
|
5430
5630
|
if (this.#isKeyword("SELECT")) {
|
|
5431
|
-
const
|
|
5631
|
+
const insertSource = this.#selectBlock("(insert select)");
|
|
5632
|
+
resolvePlanExactNumericConstants(insertSource);
|
|
5633
|
+
const query = optimizePlan(insertSource);
|
|
5432
5634
|
if (query.select.some((item) => item.expression.kind === "wildcard")) {
|
|
5433
5635
|
throw new TypeError("INSERT ... SELECT requires an explicit select list");
|
|
5434
5636
|
}
|
|
@@ -5691,7 +5893,9 @@ class Parser {
|
|
|
5691
5893
|
throw new TypeError("MERGE does not support RETURNING; read the rows back with a SELECT");
|
|
5692
5894
|
}
|
|
5693
5895
|
this.#take("eof");
|
|
5694
|
-
|
|
5896
|
+
const statement = { kind: "merge", table, alias, source, on, branches };
|
|
5897
|
+
resolveStatementExactNumericConstants(statement);
|
|
5898
|
+
return statement;
|
|
5695
5899
|
}
|
|
5696
5900
|
#updateStatement() {
|
|
5697
5901
|
this.#keyword("UPDATE");
|
|
@@ -5751,7 +5955,7 @@ class Parser {
|
|
|
5751
5955
|
if (hasAggregate(expression) || expressionColumns(expression).length > 0) {
|
|
5752
5956
|
throw new TypeError(`${label} must be constant expressions`);
|
|
5753
5957
|
}
|
|
5754
|
-
return asQueryValue(evaluate(expression, {}));
|
|
5958
|
+
return asQueryValue(evaluate(resolveExactNumericConstants(expression), {}));
|
|
5755
5959
|
}
|
|
5756
5960
|
#unionMember(sql) {
|
|
5757
5961
|
if (this.#punctuation("(")) {
|
|
@@ -6932,6 +7136,11 @@ class Parser {
|
|
|
6932
7136
|
continue;
|
|
6933
7137
|
}
|
|
6934
7138
|
const operator = this.#peek().text;
|
|
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
|
+
}
|
|
6935
7144
|
// || and the JSON arrows share PostgreSQL's loosest "any other operator" level, applying
|
|
6936
7145
|
// left-to-right to whole arithmetic terms: `'a' || d ->> 'k'` is `('a' || d) ->> 'k'`.
|
|
6937
7146
|
const precedence = operator === "*" || operator === "/" || operator === "%"
|
|
@@ -7021,10 +7230,29 @@ class Parser {
|
|
|
7021
7230
|
if (token.kind === "number") {
|
|
7022
7231
|
this.#index += 1;
|
|
7023
7232
|
const value = Number(token.text);
|
|
7024
|
-
|
|
7025
|
-
|
|
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);
|
|
7026
7243
|
}
|
|
7027
|
-
|
|
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
|
+
};
|
|
7028
7256
|
}
|
|
7029
7257
|
if (token.kind === "string") {
|
|
7030
7258
|
this.#index += 1;
|
|
@@ -8115,6 +8343,8 @@ export function assembleSelectBlock(parts, nextSequence) {
|
|
|
8115
8343
|
if (distinct) {
|
|
8116
8344
|
if (select.some((item) => hasAggregate(item.expression)))
|
|
8117
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");
|
|
8118
8348
|
if (groupBy.length > 0)
|
|
8119
8349
|
throw new TypeError("SELECT DISTINCT cannot be combined with GROUP BY");
|
|
8120
8350
|
if (having.length > 0)
|
|
@@ -8193,7 +8423,7 @@ export function assembleSelectBlock(parts, nextSequence) {
|
|
|
8193
8423
|
* over those same columns provides the deduplication through the grouped executor. Runs
|
|
8194
8424
|
* exactly once per execution entry, like MATCH(*) expansion.
|
|
8195
8425
|
*/
|
|
8196
|
-
|
|
8426
|
+
function expandDistinctWildcard(plan, columnsOf) {
|
|
8197
8427
|
if (plan.distinctWildcard !== true)
|
|
8198
8428
|
return plan;
|
|
8199
8429
|
const shaped = [plan.base, ...plan.joins].map((source) => ({
|
|
@@ -8285,7 +8515,7 @@ export function planHasSourceColumnAliases(plan) {
|
|
|
8285
8515
|
* scan cannot know whether the next row ties — and the ordered result is trimmed here: rows up
|
|
8286
8516
|
* to the limit, plus every following row equal to the last one on all ORDER BY columns.
|
|
8287
8517
|
*/
|
|
8288
|
-
|
|
8518
|
+
function withTiesPlan(plan) {
|
|
8289
8519
|
// Ordering by an expression or an unselected column wraps the real block in a projection that
|
|
8290
8520
|
// hides the sort column. The tie test needs that column, so the inner block runs and the
|
|
8291
8521
|
// projection is applied after trimming instead of before.
|
|
@@ -8472,7 +8702,7 @@ export function expandSourceColumnAliases(plan, columnsOf) {
|
|
|
8472
8702
|
* one source, and `alias.column` when it reads several, so two sources cannot collide.
|
|
8473
8703
|
* Runs once per execution entry, like MATCH(*) and DISTINCT * expansion.
|
|
8474
8704
|
*/
|
|
8475
|
-
|
|
8705
|
+
function expandQualifiedWildcards(plan, columnsOf) {
|
|
8476
8706
|
const qualified = (block) => {
|
|
8477
8707
|
if (block.select.some((item) => item.expression.kind === "wildcard" && item.expression.table))
|
|
8478
8708
|
return true;
|
|
@@ -8748,7 +8978,7 @@ function jsonTableColumnValue(column, value) {
|
|
|
8748
8978
|
}
|
|
8749
8979
|
return date;
|
|
8750
8980
|
}
|
|
8751
|
-
|
|
8981
|
+
function timestampLiteral(text) {
|
|
8752
8982
|
const trimmed = text.trim();
|
|
8753
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);
|
|
8754
8984
|
if (match === null)
|
|
@@ -8922,13 +9152,14 @@ function defaultAlias(expression) {
|
|
|
8922
9152
|
return "expression";
|
|
8923
9153
|
}
|
|
8924
9154
|
/**
|
|
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
|
|
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.
|
|
8927
9158
|
*/
|
|
8928
9159
|
function validNumericLiteral(text, radix) {
|
|
8929
9160
|
const digits = radix === 16 ? "0-9a-fA-F" : radix === 8 ? "0-7" : radix === 2 ? "01" : "0-9";
|
|
8930
9161
|
const group = `[${digits}]+(?:_[${digits}]+)*`;
|
|
8931
|
-
const pattern = radix === 10 ? `^${group}(?:\\.${group})?$` : `^${group}$`;
|
|
9162
|
+
const pattern = radix === 10 ? `^${group}(?:\\.${group})?(?:[eE][+-]?\\d+)?$` : `^${group}$`;
|
|
8932
9163
|
return new RegExp(pattern).test(text);
|
|
8933
9164
|
}
|
|
8934
9165
|
function tokenize(sql) {
|
|
@@ -8986,6 +9217,16 @@ function tokenize(sql) {
|
|
|
8986
9217
|
// T662: underscores may separate digits.
|
|
8987
9218
|
while (index < sql.length && /[\d._]/.test(sql[index] ?? ""))
|
|
8988
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
|
+
}
|
|
8989
9230
|
const text = sql.slice(start, index);
|
|
8990
9231
|
if (!validNumericLiteral(text, 10))
|
|
8991
9232
|
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 {};
|
package/dist/engine/schema.d.ts
CHANGED
|
@@ -55,6 +55,17 @@ export declare function foreignKeyName(tableName: string, columnName: string): s
|
|
|
55
55
|
export type HasDefault<TValue> = TValue & {
|
|
56
56
|
readonly __minnowHasDefault?: true;
|
|
57
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* A JSON column's select value carrying a declared document shape. The value is still JSON
|
|
60
|
+
* text — the brand is an optional phantom property, so plain strings stay assignable in both
|
|
61
|
+
* directions — and adapters read the shape to offer typed traversal. The tuple wrapper keeps
|
|
62
|
+
* `never` and union shapes intact through inference.
|
|
63
|
+
*/
|
|
64
|
+
export type JsonShape<TShape> = string & {
|
|
65
|
+
readonly __minnowJsonShape?: readonly [TShape];
|
|
66
|
+
};
|
|
67
|
+
/** A JSON column without a declared shape selects as exactly `string`, with no brand. */
|
|
68
|
+
type JsonColumnValue<TShape> = [TShape] extends [never] ? string : JsonShape<TShape>;
|
|
58
69
|
export interface ColumnBuilder<TValue extends SchemaValue, TNullable extends boolean, TUnique extends boolean = false, THasDefault extends boolean = false, TInput extends SchemaValue = TValue, TDomain extends SqlDomain["kind"] | undefined = undefined, TGenerated extends boolean = false> {
|
|
59
70
|
readonly kind: "column";
|
|
60
71
|
readonly type: SchemaColumnType;
|
|
@@ -179,8 +190,13 @@ export declare const column: {
|
|
|
179
190
|
precision?: number;
|
|
180
191
|
scale?: number;
|
|
181
192
|
}) => ColumnBuilder<string, false, false, false, string | number, "numeric", false>;
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
/**
|
|
194
|
+
* JSON text at the JavaScript boundary. Optionally declare the document's shape —
|
|
195
|
+
* `column.json<{ name: string }>()` — for adapters to type `->`/`->>` traversal; the runtime
|
|
196
|
+
* value stays JSON text either way.
|
|
197
|
+
*/
|
|
198
|
+
json: <TShape = never>() => ColumnBuilder<JsonColumnValue<TShape>, false, false, false, string, "json", false>;
|
|
199
|
+
jsonb: <TShape = never>() => ColumnBuilder<JsonColumnValue<TShape>, false, false, false, string, "jsonb", false>;
|
|
184
200
|
uuid: () => ColumnBuilder<string, false, false, false, string, "uuid", false>;
|
|
185
201
|
/** A zoneless calendar date, represented publicly as canonical `YYYY-MM-DD` text. */
|
|
186
202
|
date: () => ColumnBuilder<string, false, false, false, string, "date", false>;
|
package/dist/engine/schema.js
CHANGED
|
@@ -228,8 +228,17 @@ export const column = {
|
|
|
228
228
|
const sqlDomain = validateSqlDomain({ kind: "numeric", ...options }, "numeric column");
|
|
229
229
|
return createColumn("string", { sqlDomain });
|
|
230
230
|
},
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
/**
|
|
232
|
+
* JSON text at the JavaScript boundary. Optionally declare the document's shape —
|
|
233
|
+
* `column.json<{ name: string }>()` — for adapters to type `->`/`->>` traversal; the runtime
|
|
234
|
+
* value stays JSON text either way.
|
|
235
|
+
*/
|
|
236
|
+
json: () => createColumn("string", {
|
|
237
|
+
sqlDomain: { kind: "json" },
|
|
238
|
+
}),
|
|
239
|
+
jsonb: () => createColumn("string", {
|
|
240
|
+
sqlDomain: { kind: "jsonb" },
|
|
241
|
+
}),
|
|
233
242
|
uuid: () => createColumn("string", { sqlDomain: { kind: "uuid" } }),
|
|
234
243
|
/** A zoneless calendar date, represented publicly as canonical `YYYY-MM-DD` text. */
|
|
235
244
|
date: () => createColumn("string", { sqlDomain: { kind: "date" } }),
|