@minnowdb/core 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/engine/artifact-cache.d.ts +5 -0
- package/dist/engine/artifact-cache.js +64 -13
- package/dist/engine/database.js +294 -12
- package/dist/engine/optimizer.d.ts +7 -0
- package/dist/engine/optimizer.js +855 -84
- package/dist/engine/point-read.d.ts +57 -0
- package/dist/engine/point-read.js +189 -0
- package/dist/engine/query.d.ts +6 -0
- package/dist/engine/query.js +101 -18
- package/dist/engine/vector.js +69 -5
- package/dist/plan/model.d.ts +7 -1
- package/dist/storage/toolkit/record-core.js +104 -75
- package/dist/storage/types.js +34 -6
- package/dist/transactions/index.js +9 -1
- package/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +78 -10
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { CompiledQuery, QueryValue } from "../plan/model.js";
|
|
2
|
+
export type PointReadValue = boolean | number | string | Date;
|
|
3
|
+
export interface PointReadEquality {
|
|
4
|
+
column: string;
|
|
5
|
+
value: PointReadValue;
|
|
6
|
+
}
|
|
7
|
+
export interface PointReadShape {
|
|
8
|
+
table: string;
|
|
9
|
+
/** Conjunctive equalities, in predicate order; may repeat a column. */
|
|
10
|
+
equalities: PointReadEquality[];
|
|
11
|
+
/** Plain column projections, in select order. */
|
|
12
|
+
select: Array<{
|
|
13
|
+
column: string;
|
|
14
|
+
alias: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
/** The statement-shaped half of the analysis, computed once per cached plan. */
|
|
18
|
+
interface PointReadTemplate {
|
|
19
|
+
table: string;
|
|
20
|
+
equalities: Array<{
|
|
21
|
+
column: string;
|
|
22
|
+
value: PointReadValue;
|
|
23
|
+
} | {
|
|
24
|
+
column: string;
|
|
25
|
+
parameter: number;
|
|
26
|
+
}>;
|
|
27
|
+
select: Array<{
|
|
28
|
+
column: string;
|
|
29
|
+
alias: string;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Test-only escape hatch and counters. Not exported from any public entry point: in-repo
|
|
34
|
+
* differential suites import this module directly to force the ordinary executor and to
|
|
35
|
+
* assert the fast path actually served eligible statements.
|
|
36
|
+
*/
|
|
37
|
+
export declare const pointReadTestHooks: {
|
|
38
|
+
disabled: boolean;
|
|
39
|
+
attempted: number;
|
|
40
|
+
served: number;
|
|
41
|
+
};
|
|
42
|
+
/** The template for a cached compiled plan, analyzed once per statement. */
|
|
43
|
+
export declare function cachedPointReadTemplate(plan: CompiledQuery): PointReadTemplate | null;
|
|
44
|
+
/**
|
|
45
|
+
* Substitutes this call's parameters into the statement template. Undefined means a parameter
|
|
46
|
+
* carries a value the fast path cannot compare exactly (NULL, a non-finite number, an invalid
|
|
47
|
+
* Date, or a non-storage value), and the ordinary executor must decide what it means.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolvePointReadShape(template: PointReadTemplate, params: readonly QueryValue[]): PointReadShape | undefined;
|
|
50
|
+
/** Whether the array is non-strictly ascending; memoized per immutable decoded array. */
|
|
51
|
+
export declare function valuesAreAscending(values: Float64Array): boolean;
|
|
52
|
+
/** The [begin, end) run of slots equal to `target` over an ascending array. */
|
|
53
|
+
export declare function equalRunRange(values: Float64Array, target: number): {
|
|
54
|
+
begin: number;
|
|
55
|
+
end: number;
|
|
56
|
+
};
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape analysis for the keyed point-read fast path: a single-table conjunction of
|
|
3
|
+
* column-equals-literal predicates that covers the table's unique key, projecting plain
|
|
4
|
+
* columns. Such a statement addresses at most one row, so execution can skip parameter
|
|
5
|
+
* binding, plan cloning, streamed-view construction, and the vector pipeline entirely and
|
|
6
|
+
* answer from cached decoded blocks. Anything this module cannot prove eligible falls back to
|
|
7
|
+
* the ordinary executor, which stays the authority on errors and semantics.
|
|
8
|
+
*/
|
|
9
|
+
import { dateMilliseconds } from "../date-value.js";
|
|
10
|
+
/**
|
|
11
|
+
* Test-only escape hatch and counters. Not exported from any public entry point: in-repo
|
|
12
|
+
* differential suites import this module directly to force the ordinary executor and to
|
|
13
|
+
* assert the fast path actually served eligible statements.
|
|
14
|
+
*/
|
|
15
|
+
export const pointReadTestHooks = {
|
|
16
|
+
disabled: false,
|
|
17
|
+
attempted: 0,
|
|
18
|
+
served: 0,
|
|
19
|
+
};
|
|
20
|
+
const DUAL_TABLE = "(dual)";
|
|
21
|
+
/** A base-table column reference: bare, or qualified by the base source's alias. */
|
|
22
|
+
function baseColumnReference(expression, alias) {
|
|
23
|
+
if (expression.kind !== "column")
|
|
24
|
+
return undefined;
|
|
25
|
+
const reference = expression.reference;
|
|
26
|
+
const dot = reference.indexOf(".");
|
|
27
|
+
if (dot < 0)
|
|
28
|
+
return reference;
|
|
29
|
+
if (reference.slice(0, dot) !== alias)
|
|
30
|
+
return undefined;
|
|
31
|
+
const column = reference.slice(dot + 1);
|
|
32
|
+
// A nested qualifier ("a.b.c") is not a base-table column.
|
|
33
|
+
return column.includes(".") ? undefined : column;
|
|
34
|
+
}
|
|
35
|
+
/** A usable equality operand: a plain non-null literal of a storage type, verified exactly. */
|
|
36
|
+
function equalityValue(value) {
|
|
37
|
+
if (value === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
40
|
+
return undefined;
|
|
41
|
+
// Strings in the engine's protected NUL namespace are wrapped at the write boundary, so a
|
|
42
|
+
// raw comparison against stored bytes would not reproduce the ordinary path's answer.
|
|
43
|
+
if (typeof value === "string" && value.startsWith("\u0000"))
|
|
44
|
+
return undefined;
|
|
45
|
+
// The internal-slot read never invokes caller-controlled Date methods.
|
|
46
|
+
if (value instanceof Date && Number.isNaN(dateMilliseconds(value)))
|
|
47
|
+
return undefined;
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function templateEquality(expression) {
|
|
51
|
+
if (expression.kind === "parameter")
|
|
52
|
+
return { parameter: expression.index };
|
|
53
|
+
if (expression.kind !== "literal")
|
|
54
|
+
return undefined;
|
|
55
|
+
// Tagged internal values and logical-domain literals compare under domain rules the fast
|
|
56
|
+
// path does not implement; a NULL literal never equals anything.
|
|
57
|
+
if (expression.internalSqlValue === true || expression.sqlDomain !== undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
const value = equalityValue(expression.value);
|
|
60
|
+
return value === undefined ? undefined : { value };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Recognizes the eligible statement shape. Purely syntactic: catalog checks (column existence,
|
|
64
|
+
* types, key coverage, view expansion, physical history) belong to the caller, which falls
|
|
65
|
+
* back to the ordinary executor whenever they cannot be proven.
|
|
66
|
+
*/
|
|
67
|
+
function pointReadTemplate(plan) {
|
|
68
|
+
const base = plan.base;
|
|
69
|
+
if (base.table === DUAL_TABLE ||
|
|
70
|
+
base.derived !== undefined ||
|
|
71
|
+
base.union !== undefined ||
|
|
72
|
+
base.recursive !== undefined ||
|
|
73
|
+
base.windowed !== undefined ||
|
|
74
|
+
base.lateral === true ||
|
|
75
|
+
base.columnAliases !== undefined ||
|
|
76
|
+
plan.joins.length > 0 ||
|
|
77
|
+
plan.groupBy.length > 0 ||
|
|
78
|
+
plan.having.length > 0 ||
|
|
79
|
+
plan.orderBy.length > 0 ||
|
|
80
|
+
plan.select.length === 0 ||
|
|
81
|
+
plan.predicates.length === 0 ||
|
|
82
|
+
plan.limit !== undefined ||
|
|
83
|
+
plan.offset !== undefined ||
|
|
84
|
+
plan.limitParameter !== undefined ||
|
|
85
|
+
plan.offsetParameter !== undefined ||
|
|
86
|
+
(plan.limitValidationParameters?.length ?? 0) > 0 ||
|
|
87
|
+
(plan.offsetValidationParameters?.length ?? 0) > 0 ||
|
|
88
|
+
plan.limitWithTies === true ||
|
|
89
|
+
plan.distinctWildcard === true ||
|
|
90
|
+
plan.usesStatementDatetime === true ||
|
|
91
|
+
plan.usesSequenceCalls === true ||
|
|
92
|
+
plan.usesVolatileFunctions === true) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const select = [];
|
|
96
|
+
for (const item of plan.select) {
|
|
97
|
+
const column = baseColumnReference(item.expression, base.alias);
|
|
98
|
+
if (column === undefined)
|
|
99
|
+
return undefined;
|
|
100
|
+
select.push({ column, alias: item.alias });
|
|
101
|
+
}
|
|
102
|
+
const equalities = [];
|
|
103
|
+
for (const predicate of plan.predicates) {
|
|
104
|
+
if (predicate.operator !== "=" || predicate.escape !== undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
const leftColumn = baseColumnReference(predicate.left, base.alias);
|
|
107
|
+
const rightColumn = baseColumnReference(predicate.right, base.alias);
|
|
108
|
+
const operand = leftColumn !== undefined && rightColumn === undefined
|
|
109
|
+
? templateEquality(predicate.right)
|
|
110
|
+
: rightColumn !== undefined && leftColumn === undefined
|
|
111
|
+
? templateEquality(predicate.left)
|
|
112
|
+
: undefined;
|
|
113
|
+
const column = leftColumn ?? rightColumn;
|
|
114
|
+
if (operand === undefined || column === undefined)
|
|
115
|
+
return undefined;
|
|
116
|
+
equalities.push({ column, ...operand });
|
|
117
|
+
}
|
|
118
|
+
return { table: base.table, equalities, select };
|
|
119
|
+
}
|
|
120
|
+
const templates = new WeakMap();
|
|
121
|
+
/** The template for a cached compiled plan, analyzed once per statement. */
|
|
122
|
+
export function cachedPointReadTemplate(plan) {
|
|
123
|
+
const cached = templates.get(plan);
|
|
124
|
+
if (cached !== undefined)
|
|
125
|
+
return cached;
|
|
126
|
+
const template = pointReadTemplate(plan) ?? null;
|
|
127
|
+
templates.set(plan, template);
|
|
128
|
+
return template;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Substitutes this call's parameters into the statement template. Undefined means a parameter
|
|
132
|
+
* carries a value the fast path cannot compare exactly (NULL, a non-finite number, an invalid
|
|
133
|
+
* Date, or a non-storage value), and the ordinary executor must decide what it means.
|
|
134
|
+
*/
|
|
135
|
+
export function resolvePointReadShape(template, params) {
|
|
136
|
+
const equalities = [];
|
|
137
|
+
for (const entry of template.equalities) {
|
|
138
|
+
if ("value" in entry) {
|
|
139
|
+
equalities.push(entry);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const raw = params[entry.parameter];
|
|
143
|
+
if (raw === undefined)
|
|
144
|
+
return undefined;
|
|
145
|
+
const value = equalityValue(raw);
|
|
146
|
+
if (value === undefined)
|
|
147
|
+
return undefined;
|
|
148
|
+
equalities.push({ column: entry.column, value });
|
|
149
|
+
}
|
|
150
|
+
return { table: template.table, equalities, select: template.select };
|
|
151
|
+
}
|
|
152
|
+
const ascendingRuns = new WeakMap();
|
|
153
|
+
/** Whether the array is non-strictly ascending; memoized per immutable decoded array. */
|
|
154
|
+
export function valuesAreAscending(values) {
|
|
155
|
+
const cached = ascendingRuns.get(values);
|
|
156
|
+
if (cached !== undefined)
|
|
157
|
+
return cached;
|
|
158
|
+
let ascending = true;
|
|
159
|
+
for (let index = 1; index < values.length; index += 1) {
|
|
160
|
+
if ((values[index] ?? 0) < (values[index - 1] ?? 0)) {
|
|
161
|
+
ascending = false;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
ascendingRuns.set(values, ascending);
|
|
166
|
+
return ascending;
|
|
167
|
+
}
|
|
168
|
+
/** The [begin, end) run of slots equal to `target` over an ascending array. */
|
|
169
|
+
export function equalRunRange(values, target) {
|
|
170
|
+
let low = 0;
|
|
171
|
+
let high = values.length;
|
|
172
|
+
while (low < high) {
|
|
173
|
+
const middle = (low + high) >>> 1;
|
|
174
|
+
if ((values[middle] ?? 0) < target)
|
|
175
|
+
low = middle + 1;
|
|
176
|
+
else
|
|
177
|
+
high = middle;
|
|
178
|
+
}
|
|
179
|
+
const begin = low;
|
|
180
|
+
high = values.length;
|
|
181
|
+
while (low < high) {
|
|
182
|
+
const middle = (low + high) >>> 1;
|
|
183
|
+
if ((values[middle] ?? 0) <= target)
|
|
184
|
+
low = middle + 1;
|
|
185
|
+
else
|
|
186
|
+
high = middle;
|
|
187
|
+
}
|
|
188
|
+
return { begin, end: low };
|
|
189
|
+
}
|
package/dist/engine/query.d.ts
CHANGED
|
@@ -583,6 +583,12 @@ export declare function distinctFromComparison(left: unknown, right: unknown): b
|
|
|
583
583
|
export declare function evaluateBooleanExpression(expression: Expression, evaluateValue: (expression: Expression) => unknown): boolean | null;
|
|
584
584
|
export declare function comparisonHolds(operator: PredicateOperator, leftValue: unknown, rightValue: unknown): boolean;
|
|
585
585
|
export declare function hasAggregate(expression: Expression): boolean;
|
|
586
|
+
/** One root aggregate call, using the same canonical set as parsing and execution. */
|
|
587
|
+
export declare function isAggregateCall(expression: Expression): expression is Extract<Expression, {
|
|
588
|
+
kind: "call";
|
|
589
|
+
}> & {
|
|
590
|
+
name: AggregateName;
|
|
591
|
+
};
|
|
586
592
|
/** The column references inside one expression; a subquery contributes none (its own scope). */
|
|
587
593
|
export declare function expressionColumnNames(expression: Expression): string[];
|
|
588
594
|
export declare function expressionColumns(expression: Expression): string[];
|
package/dist/engine/query.js
CHANGED
|
@@ -800,6 +800,7 @@ const aggregateNames = new Set([
|
|
|
800
800
|
"MAX",
|
|
801
801
|
"JSON_ARRAYAGG",
|
|
802
802
|
"STRING_AGG",
|
|
803
|
+
"MINNOW_SINGLE_VALUE",
|
|
803
804
|
]);
|
|
804
805
|
/** Set functions the parser builds from COUNT/SUM rather than from their own accumulator. */
|
|
805
806
|
const statisticalAggregates = new Set([
|
|
@@ -1502,8 +1503,12 @@ export function containsParameter(expression) {
|
|
|
1502
1503
|
return childExpressions(expression).some(containsParameter);
|
|
1503
1504
|
}
|
|
1504
1505
|
export function blockHasParameters(block) {
|
|
1505
|
-
if (block.limitParameter !== undefined ||
|
|
1506
|
+
if (block.limitParameter !== undefined ||
|
|
1507
|
+
block.offsetParameter !== undefined ||
|
|
1508
|
+
(block.limitValidationParameters?.length ?? 0) > 0 ||
|
|
1509
|
+
(block.offsetValidationParameters?.length ?? 0) > 0) {
|
|
1506
1510
|
return true;
|
|
1511
|
+
}
|
|
1507
1512
|
const expressions = [];
|
|
1508
1513
|
forEachBlockExpression(block, (expression) => expressions.push(expression));
|
|
1509
1514
|
return (expressions.some(containsParameter) ||
|
|
@@ -1608,6 +1613,14 @@ function bindBlock(block, values) {
|
|
|
1608
1613
|
block.offset = validateOffset(numeric(values[block.offsetParameter] ?? null));
|
|
1609
1614
|
delete block.offsetParameter;
|
|
1610
1615
|
}
|
|
1616
|
+
for (const index of block.limitValidationParameters ?? []) {
|
|
1617
|
+
validateLimit(numeric(values[index] ?? null));
|
|
1618
|
+
}
|
|
1619
|
+
for (const index of block.offsetValidationParameters ?? []) {
|
|
1620
|
+
validateOffset(numeric(values[index] ?? null));
|
|
1621
|
+
}
|
|
1622
|
+
delete block.limitValidationParameters;
|
|
1623
|
+
delete block.offsetValidationParameters;
|
|
1611
1624
|
for (const item of block.select)
|
|
1612
1625
|
item.expression = bindExpression(item.expression, values);
|
|
1613
1626
|
for (const predicate of [...block.predicates, ...block.having]) {
|
|
@@ -1892,6 +1905,7 @@ export function inferBlockSchema(plan, schemas) {
|
|
|
1892
1905
|
expression.name === "AVG" ||
|
|
1893
1906
|
expression.name === "MIN" ||
|
|
1894
1907
|
expression.name === "MAX" ||
|
|
1908
|
+
expression.name === "MINNOW_SINGLE_VALUE" ||
|
|
1895
1909
|
expression.name === "COALESCE" ||
|
|
1896
1910
|
expression.name === "NULLIF" ||
|
|
1897
1911
|
expression.name === "GREATEST" ||
|
|
@@ -3790,6 +3804,13 @@ function evaluate(expression, context, group) {
|
|
|
3790
3804
|
if (group === undefined)
|
|
3791
3805
|
throw new TypeError(`${expression.name} requires grouped execution`);
|
|
3792
3806
|
const argument = expression.arguments[0] ?? { kind: "wildcard" };
|
|
3807
|
+
if (expression.name === "MINNOW_SINGLE_VALUE") {
|
|
3808
|
+
if (group.length > 1) {
|
|
3809
|
+
throw new TypeError(`A scalar subquery returned ${String(group.length)} rows`);
|
|
3810
|
+
}
|
|
3811
|
+
const row = group[0];
|
|
3812
|
+
return row === undefined ? null : evaluate(argument, row);
|
|
3813
|
+
}
|
|
3793
3814
|
if (expression.name === "STRING_AGG") {
|
|
3794
3815
|
const delimiter = expression.arguments[1];
|
|
3795
3816
|
if (delimiter === undefined)
|
|
@@ -3847,12 +3868,44 @@ function evaluate(expression, context, group) {
|
|
|
3847
3868
|
String(externalSqlDomainValue(member.value)))
|
|
3848
3869
|
.join(""));
|
|
3849
3870
|
}
|
|
3871
|
+
if (expression.name === "JSON_ARRAYAGG") {
|
|
3872
|
+
let members = group.map((row) => ({
|
|
3873
|
+
value: argument.kind === "wildcard" ? 1 : evaluate(argument, row),
|
|
3874
|
+
order: (expression.aggregateOrderBy ?? []).map((item) => evaluate(item.expression, row)),
|
|
3875
|
+
}));
|
|
3876
|
+
if (expression.distinct === true) {
|
|
3877
|
+
const seen = new Set();
|
|
3878
|
+
members = members.filter(({ value }) => {
|
|
3879
|
+
const key = value instanceof Date ? ` d${String(dateMilliseconds(value))}` : value;
|
|
3880
|
+
if (seen.has(key))
|
|
3881
|
+
return false;
|
|
3882
|
+
seen.add(key);
|
|
3883
|
+
return true;
|
|
3884
|
+
});
|
|
3885
|
+
}
|
|
3886
|
+
if ((expression.aggregateOrderBy?.length ?? 0) > 0) {
|
|
3887
|
+
members.sort((left, right) => {
|
|
3888
|
+
for (const [index, order] of (expression.aggregateOrderBy ?? []).entries()) {
|
|
3889
|
+
const a = left.order[index];
|
|
3890
|
+
const b = right.order[index];
|
|
3891
|
+
const placed = nullOrder(a, b, order.nulls, order.direction);
|
|
3892
|
+
if (placed !== undefined && placed !== 0)
|
|
3893
|
+
return placed;
|
|
3894
|
+
const compared = compareValues(a, b);
|
|
3895
|
+
if (compared !== 0)
|
|
3896
|
+
return order.direction === "desc" ? -compared : compared;
|
|
3897
|
+
}
|
|
3898
|
+
return 0;
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
return members.length === 0
|
|
3902
|
+
? null
|
|
3903
|
+
: preservedJsonDomainValue(jsonConstructor("JSON_ARRAY", members.map(({ value }) => value)));
|
|
3904
|
+
}
|
|
3850
3905
|
let values = argument.kind === "wildcard"
|
|
3851
3906
|
? group.map(() => 1)
|
|
3852
3907
|
: group.map((row) => evaluate(argument, row));
|
|
3853
|
-
|
|
3854
|
-
values = values.filter((value) => value !== null && value !== undefined);
|
|
3855
|
-
}
|
|
3908
|
+
values = values.filter((value) => value !== null && value !== undefined);
|
|
3856
3909
|
if (expression.distinct === true) {
|
|
3857
3910
|
const seen = new Set();
|
|
3858
3911
|
values = values.filter((value) => {
|
|
@@ -3874,11 +3927,6 @@ function evaluate(expression, context, group) {
|
|
|
3874
3927
|
const sum = sumNumericValues(values);
|
|
3875
3928
|
return exactNumericBinary("/", sum, values.length) ?? numeric(sum) / values.length;
|
|
3876
3929
|
})();
|
|
3877
|
-
if (expression.name === "JSON_ARRAYAGG") {
|
|
3878
|
-
return values.length === 0
|
|
3879
|
-
? null
|
|
3880
|
-
: preservedJsonDomainValue(jsonConstructor("JSON_ARRAY", values));
|
|
3881
|
-
}
|
|
3882
3930
|
if (expression.name === "MIN")
|
|
3883
3931
|
return values.reduce((best, value) => (best === undefined || compareValues(value, best) < 0 ? value : best), undefined);
|
|
3884
3932
|
return values.reduce((best, value) => (best === undefined || compareValues(value, best) > 0 ? value : best), undefined);
|
|
@@ -4330,6 +4378,10 @@ export function hasAggregate(expression) {
|
|
|
4330
4378
|
}
|
|
4331
4379
|
return childExpressions(expression).some(hasAggregate);
|
|
4332
4380
|
}
|
|
4381
|
+
/** One root aggregate call, using the same canonical set as parsing and execution. */
|
|
4382
|
+
export function isAggregateCall(expression) {
|
|
4383
|
+
return expression.kind === "call" && aggregateNames.has(expression.name);
|
|
4384
|
+
}
|
|
4333
4385
|
function validateGrouping(plan) {
|
|
4334
4386
|
const grouped = plan.groupBy.length > 0 || plan.select.some((item) => hasAggregate(item.expression));
|
|
4335
4387
|
if (!grouped)
|
|
@@ -5336,7 +5388,7 @@ class Parser {
|
|
|
5336
5388
|
rows: [[]],
|
|
5337
5389
|
defaultValues: true,
|
|
5338
5390
|
...this.#onConflictClause(table),
|
|
5339
|
-
...this.#returningClause(),
|
|
5391
|
+
...this.#returningClause(table),
|
|
5340
5392
|
};
|
|
5341
5393
|
}
|
|
5342
5394
|
if (this.#isKeyword("SELECT")) {
|
|
@@ -5353,7 +5405,7 @@ class Parser {
|
|
|
5353
5405
|
columns,
|
|
5354
5406
|
rows: [],
|
|
5355
5407
|
query,
|
|
5356
|
-
...this.#returningClause(),
|
|
5408
|
+
...this.#returningClause(table),
|
|
5357
5409
|
};
|
|
5358
5410
|
}
|
|
5359
5411
|
this.#keyword("VALUES");
|
|
@@ -5380,7 +5432,7 @@ class Parser {
|
|
|
5380
5432
|
columns,
|
|
5381
5433
|
rows,
|
|
5382
5434
|
...this.#onConflictClause(table),
|
|
5383
|
-
...this.#returningClause(),
|
|
5435
|
+
...this.#returningClause(table),
|
|
5384
5436
|
};
|
|
5385
5437
|
}
|
|
5386
5438
|
#onConflictClause(table) {
|
|
@@ -5447,8 +5499,8 @@ class Parser {
|
|
|
5447
5499
|
},
|
|
5448
5500
|
};
|
|
5449
5501
|
}
|
|
5450
|
-
/** RETURNING
|
|
5451
|
-
#returningClause() {
|
|
5502
|
+
/** RETURNING *, target.*, or [target.]col, ... — execution owns the row semantics. */
|
|
5503
|
+
#returningClause(table) {
|
|
5452
5504
|
if (!this.#isKeyword("RETURNING"))
|
|
5453
5505
|
return {};
|
|
5454
5506
|
this.#keyword("RETURNING");
|
|
@@ -5458,7 +5510,23 @@ class Parser {
|
|
|
5458
5510
|
}
|
|
5459
5511
|
const columns = [];
|
|
5460
5512
|
for (;;) {
|
|
5461
|
-
|
|
5513
|
+
const first = this.#identifier();
|
|
5514
|
+
if (this.#punctuation(".")) {
|
|
5515
|
+
if (first !== table) {
|
|
5516
|
+
throw new TypeError(`RETURNING qualifier must name the target table: ${table}`);
|
|
5517
|
+
}
|
|
5518
|
+
if (this.#peek().text === "*") {
|
|
5519
|
+
this.#index += 1;
|
|
5520
|
+
if (columns.length > 0 || this.#peek().text === ",") {
|
|
5521
|
+
throw new TypeError("RETURNING target.* must be the only returned item");
|
|
5522
|
+
}
|
|
5523
|
+
return { returning: "*" };
|
|
5524
|
+
}
|
|
5525
|
+
columns.push(this.#identifier());
|
|
5526
|
+
}
|
|
5527
|
+
else {
|
|
5528
|
+
columns.push(first);
|
|
5529
|
+
}
|
|
5462
5530
|
if (!this.#punctuation(","))
|
|
5463
5531
|
break;
|
|
5464
5532
|
}
|
|
@@ -5609,14 +5677,14 @@ class Parser {
|
|
|
5609
5677
|
throw new TypeError("UPDATE assignments must set each column once");
|
|
5610
5678
|
}
|
|
5611
5679
|
const predicates = this.#mutationPredicates();
|
|
5612
|
-
return { kind: "update", table, assignments, predicates, ...this.#returningClause() };
|
|
5680
|
+
return { kind: "update", table, assignments, predicates, ...this.#returningClause(table) };
|
|
5613
5681
|
}
|
|
5614
5682
|
#deleteStatement() {
|
|
5615
5683
|
this.#keyword("DELETE");
|
|
5616
5684
|
this.#keyword("FROM");
|
|
5617
5685
|
const table = this.#identifier();
|
|
5618
5686
|
const predicates = this.#mutationPredicates();
|
|
5619
|
-
return { kind: "delete", table, predicates, ...this.#returningClause() };
|
|
5687
|
+
return { kind: "delete", table, predicates, ...this.#returningClause(table) };
|
|
5620
5688
|
}
|
|
5621
5689
|
#mutationPredicates() {
|
|
5622
5690
|
const predicates = [];
|
|
@@ -6791,10 +6859,18 @@ class Parser {
|
|
|
6791
6859
|
const all = this.#isKeyword("ALL");
|
|
6792
6860
|
this.#keyword(this.#isKeyword("ANY") ? "ANY" : all ? "ALL" : "SOME");
|
|
6793
6861
|
this.#expectPunctuation("(");
|
|
6862
|
+
// Kysely's fn.any(subquery) emits PostgreSQL's valid ANY((SELECT ...)) spelling.
|
|
6863
|
+
let wrapped = 0;
|
|
6864
|
+
while (this.#punctuation("("))
|
|
6865
|
+
wrapped += 1;
|
|
6794
6866
|
if (!this.#isKeyword("SELECT")) {
|
|
6795
6867
|
throw new TypeError("ANY/ALL take a subquery");
|
|
6796
6868
|
}
|
|
6797
6869
|
const block = this.#selectBlock("(quantified subquery)");
|
|
6870
|
+
while (wrapped > 0) {
|
|
6871
|
+
this.#expectPunctuation(")");
|
|
6872
|
+
wrapped -= 1;
|
|
6873
|
+
}
|
|
6798
6874
|
this.#expectPunctuation(")");
|
|
6799
6875
|
return {
|
|
6800
6876
|
kind: "condition",
|
|
@@ -7296,7 +7372,9 @@ class Parser {
|
|
|
7296
7372
|
// canonical plan names. ANY_VALUE picks an implementation-dependent row of the group
|
|
7297
7373
|
// (T626); MIN is one such choice and reuses its accumulator exactly.
|
|
7298
7374
|
const name = (upper === "ANY_VALUE" ? "MIN" : (functionSpellings.get(upper) ?? upper));
|
|
7299
|
-
if (name === "MINNOW_TUPLE_KEY" ||
|
|
7375
|
+
if (name === "MINNOW_TUPLE_KEY" ||
|
|
7376
|
+
name === "MINNOW_COLLATE" ||
|
|
7377
|
+
name === "MINNOW_SINGLE_VALUE") {
|
|
7300
7378
|
throw new TypeError(`Unsupported function: ${identifier}`);
|
|
7301
7379
|
}
|
|
7302
7380
|
if (!aggregateNames.has(name) && !scalarFunctionNames.has(name))
|
|
@@ -7327,6 +7405,11 @@ class Parser {
|
|
|
7327
7405
|
if (this.#isKeyword("ORDER"))
|
|
7328
7406
|
aggregateOrderBy = this.#orderByClause();
|
|
7329
7407
|
}
|
|
7408
|
+
else if (name === "JSON_ARRAYAGG") {
|
|
7409
|
+
args.push(this.#expression());
|
|
7410
|
+
if (this.#isKeyword("ORDER"))
|
|
7411
|
+
aggregateOrderBy = this.#orderByClause();
|
|
7412
|
+
}
|
|
7330
7413
|
else {
|
|
7331
7414
|
args.push(...this.#expressionList());
|
|
7332
7415
|
}
|
package/dist/engine/vector.js
CHANGED
|
@@ -864,8 +864,11 @@ function bindExpression(expression, sources, aggregateSpecs, aggregateIndexes, m
|
|
|
864
864
|
argument.vector.kind === "datetime"
|
|
865
865
|
? { source: argument.source, vector: argument.vector }
|
|
866
866
|
: undefined;
|
|
867
|
-
const rawNumber = expression.name
|
|
868
|
-
expression.name
|
|
867
|
+
const rawNumber = (expression.name === "COUNT" ||
|
|
868
|
+
expression.name === "SUM" ||
|
|
869
|
+
expression.name === "AVG" ||
|
|
870
|
+
expression.name === "MIN" ||
|
|
871
|
+
expression.name === "MAX") &&
|
|
869
872
|
argument.kind === "column" &&
|
|
870
873
|
argument.vector.kind === "number"
|
|
871
874
|
? { source: argument.source, vector: argument.vector }
|
|
@@ -1536,6 +1539,12 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
|
|
|
1536
1539
|
(spec.orderBy ?? []).map((order) => encodeSpillValue(asQueryValue(evaluateBatchExpression(plan, order.expression, batch, row)))),
|
|
1537
1540
|
]);
|
|
1538
1541
|
}
|
|
1542
|
+
else if (spec.name === "JSON_ARRAYAGG" && (spec.orderBy?.length ?? 0) > 0) {
|
|
1543
|
+
spillRow[`a${String(index)}`] = JSON.stringify([
|
|
1544
|
+
encodeSpillValue(asQueryValue(raw ?? null)),
|
|
1545
|
+
(spec.orderBy ?? []).map((order) => encodeSpillValue(asQueryValue(evaluateBatchExpression(plan, order.expression, batch, row)))),
|
|
1546
|
+
]);
|
|
1547
|
+
}
|
|
1539
1548
|
else {
|
|
1540
1549
|
spillRow[`a${String(index)}`] =
|
|
1541
1550
|
raw === null || raw === undefined ? null : asQueryValue(raw);
|
|
@@ -3763,12 +3772,30 @@ function updateAggregatesFromValues(plan, state, values, memory) {
|
|
|
3763
3772
|
throw new Error("Spilled STRING_AGG order is invalid");
|
|
3764
3773
|
applyAggregateValue(spec, state, index, decoded[0], memory, decoded[1], encodedOrder.map(decodeSpillValue));
|
|
3765
3774
|
}
|
|
3775
|
+
else if (spec.name === "JSON_ARRAYAGG" &&
|
|
3776
|
+
(spec.orderBy?.length ?? 0) > 0 &&
|
|
3777
|
+
typeof value === "string") {
|
|
3778
|
+
const decoded = JSON.parse(value);
|
|
3779
|
+
if (!Array.isArray(decoded) || decoded.length !== 2 || !Array.isArray(decoded[1])) {
|
|
3780
|
+
throw new Error("Spilled JSON_ARRAYAGG input is invalid");
|
|
3781
|
+
}
|
|
3782
|
+
applyAggregateValue(spec, state, index, decodeSpillValue(decoded[0]), memory, undefined, decoded[1].map(decodeSpillValue));
|
|
3783
|
+
}
|
|
3766
3784
|
else {
|
|
3767
3785
|
applyAggregateValue(spec, state, index, value, memory);
|
|
3768
3786
|
}
|
|
3769
3787
|
}
|
|
3770
3788
|
}
|
|
3771
3789
|
function applyAggregateValue(spec, state, index, value, memory, delimiter, orderValues = []) {
|
|
3790
|
+
if (spec.name === "MINNOW_SINGLE_VALUE") {
|
|
3791
|
+
const count = (state.counts[index] ?? 0) + 1;
|
|
3792
|
+
state.counts[index] = count;
|
|
3793
|
+
if (count > 1) {
|
|
3794
|
+
throw new TypeError(`A scalar subquery returned ${String(count)} rows`);
|
|
3795
|
+
}
|
|
3796
|
+
replaceAggregateValue(state, index, asQueryValue(value ?? null), "Scalar subquery value", memory);
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3772
3799
|
if (spec.name === "JSON_ARRAYAGG") {
|
|
3773
3800
|
const member = asQueryValue(value ?? null);
|
|
3774
3801
|
if (spec.distinct === true && !firstOfItsKind(state, index, member, memory))
|
|
@@ -3780,8 +3807,14 @@ function applyAggregateValue(spec, state, index, value, memory, delimiter, order
|
|
|
3780
3807
|
list = [];
|
|
3781
3808
|
lists[index] = list;
|
|
3782
3809
|
}
|
|
3783
|
-
|
|
3784
|
-
|
|
3810
|
+
const retained = (spec.orderBy?.length ?? 0) === 0
|
|
3811
|
+
? member
|
|
3812
|
+
: JSON.stringify([
|
|
3813
|
+
encodeSpillValue(member),
|
|
3814
|
+
orderValues.map((orderValue) => encodeSpillValue(orderValue)),
|
|
3815
|
+
]);
|
|
3816
|
+
list.push(retained);
|
|
3817
|
+
memory.tally(safeMemorySum(QUERY_REFERENCE_BYTES, queryValuePayloadBytes(retained), "JSON_ARRAYAGG member"), "JSON_ARRAYAGG member");
|
|
3785
3818
|
return;
|
|
3786
3819
|
}
|
|
3787
3820
|
if (spec.name === "STRING_AGG") {
|
|
@@ -3921,7 +3954,38 @@ function evaluateFinalExpression(plan, expression, group) {
|
|
|
3921
3954
|
if (count === 0)
|
|
3922
3955
|
return null;
|
|
3923
3956
|
if (expression.name === "JSON_ARRAYAGG") {
|
|
3924
|
-
|
|
3957
|
+
const retained = required(group.lists, "JSON aggregate list state is missing")[aggregateIndex] ?? [];
|
|
3958
|
+
const orderBy = plan.aggregates[aggregateIndex]?.orderBy ?? [];
|
|
3959
|
+
const members = retained.map((encoded) => {
|
|
3960
|
+
if (orderBy.length === 0)
|
|
3961
|
+
return { value: encoded, order: [] };
|
|
3962
|
+
if (typeof encoded !== "string")
|
|
3963
|
+
throw new Error("JSON_ARRAYAGG member is invalid");
|
|
3964
|
+
const pair = JSON.parse(encoded);
|
|
3965
|
+
if (!Array.isArray(pair) || pair.length !== 2 || !Array.isArray(pair[1])) {
|
|
3966
|
+
throw new Error("JSON_ARRAYAGG member is invalid");
|
|
3967
|
+
}
|
|
3968
|
+
return {
|
|
3969
|
+
value: decodeSpillValue(pair[0]),
|
|
3970
|
+
order: pair[1].map(decodeSpillValue),
|
|
3971
|
+
};
|
|
3972
|
+
});
|
|
3973
|
+
if (orderBy.length > 0) {
|
|
3974
|
+
members.sort((left, right) => {
|
|
3975
|
+
for (const [index, order] of orderBy.entries()) {
|
|
3976
|
+
const a = left.order[index];
|
|
3977
|
+
const b = right.order[index];
|
|
3978
|
+
const placed = nullOrder(a, b, order.nulls, order.direction);
|
|
3979
|
+
if (placed !== undefined && placed !== 0)
|
|
3980
|
+
return placed;
|
|
3981
|
+
const compared = compareValues(a, b);
|
|
3982
|
+
if (compared !== 0)
|
|
3983
|
+
return order.direction === "desc" ? -compared : compared;
|
|
3984
|
+
}
|
|
3985
|
+
return 0;
|
|
3986
|
+
});
|
|
3987
|
+
}
|
|
3988
|
+
return preservedJsonDomainValue(jsonConstructor("JSON_ARRAY", members.map(({ value }) => value)));
|
|
3925
3989
|
}
|
|
3926
3990
|
if (expression.name === "STRING_AGG") {
|
|
3927
3991
|
const members = required(group.lists, "STRING_AGG list state is missing")[aggregateIndex] ?? [];
|
package/dist/plan/model.d.ts
CHANGED
|
@@ -14,7 +14,9 @@ export interface QueryResult {
|
|
|
14
14
|
}
|
|
15
15
|
export type BinaryOperator = "+" | "-" | "*" | "/" | "%" | "||";
|
|
16
16
|
export type ComparisonOperator = "=" | "!=" | "<>" | ">" | ">=" | "<" | "<=";
|
|
17
|
-
export type AggregateName = "COUNT" | "SUM" | "AVG" | "MIN" | "MAX" | "JSON_ARRAYAGG" | "STRING_AGG"
|
|
17
|
+
export type AggregateName = "COUNT" | "SUM" | "AVG" | "MIN" | "MAX" | "JSON_ARRAYAGG" | "STRING_AGG"
|
|
18
|
+
/** Optimizer-only aggregate that enforces scalar-subquery cardinality. */
|
|
19
|
+
| "MINNOW_SINGLE_VALUE";
|
|
18
20
|
export type ScalarFunctionName = "ROUND" | "COALESCE" | "DATE_TRUNC" | "DATE_ADD" | "UPPER" | "LOWER" | "LENGTH" | "ABS" | "TRIM" | "LTRIM" | "RTRIM" | "SUBSTR" | "REPLACE" | "INSTR" | "NULLIF" | "GREATEST" | "LEAST" | "FLOOR" | "CEIL" | "MOD" | "POWER" | "SQRT" | "EXTRACT" | "CAST" | "OCTET_LENGTH" | "LPAD" | "RPAD" | "OVERLAY" | "CURRENT_DATE" | "CURRENT_TIMESTAMP" | "LOCALTIME" | "GROUPING" | "JSON_VALUE" | "JSON_QUERY" | "JSON_EXISTS" | "JSON_OBJECT" | "JSON_ARRAY" | "IS_JSON" | "ARRAY"
|
|
19
21
|
/** Optimizer-only, prefix-free equality key for hashable multi-column decorrelation. */
|
|
20
22
|
| "MINNOW_TUPLE_KEY"
|
|
@@ -201,6 +203,10 @@ export interface CompiledQuery {
|
|
|
201
203
|
distinctWildcard?: boolean;
|
|
202
204
|
limitParameter?: number;
|
|
203
205
|
offsetParameter?: number;
|
|
206
|
+
/** Optimizer-relocated LIMIT placeholders that still need bind-time numeric/range validation. */
|
|
207
|
+
limitValidationParameters?: number[];
|
|
208
|
+
/** Optimizer-relocated OFFSET placeholders that still need bind-time numeric/range validation. */
|
|
209
|
+
offsetValidationParameters?: number[];
|
|
204
210
|
limitWithTies?: boolean;
|
|
205
211
|
parameterCount?: number;
|
|
206
212
|
usesStatementDatetime?: boolean;
|