@minnowdb/core 0.6.0 → 0.6.1
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/database.js +12 -5
- package/dist/engine/optimizer.d.ts +7 -0
- package/dist/engine/optimizer.js +855 -84
- 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/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +26 -8
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minnowdb/core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "A columnar SQL database for the browser: PostgreSQL-style SQL over durable IndexedDB or OPFS data, with no server or WebAssembly module.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eric Wilhite",
|
|
@@ -129,6 +129,11 @@
|
|
|
129
129
|
"classification": "different",
|
|
130
130
|
"reason": "Minnow returns JSON text; PostgreSQL returns a native JSON value, and member order is unspecified without aggregate-local ORDER BY."
|
|
131
131
|
},
|
|
132
|
+
{
|
|
133
|
+
"id": "subquery.correlated-json-aggregate",
|
|
134
|
+
"classification": "different",
|
|
135
|
+
"reason": "Both engines accept the correlated JSON aggregate and agree on its JSON value, but Minnow returns JSON text while PostgreSQL returns a native JSON value."
|
|
136
|
+
},
|
|
132
137
|
{
|
|
133
138
|
"id": "type.exact-numeric",
|
|
134
139
|
"classification": "different",
|
package/sql-feature-matrix.json
CHANGED
|
@@ -257,8 +257,8 @@
|
|
|
257
257
|
{
|
|
258
258
|
"id": "mutation.returning",
|
|
259
259
|
"status": "supported",
|
|
260
|
-
"example": "DELETE FROM keyed WHERE name = 'x' RETURNING name, score",
|
|
261
|
-
"notes": "RETURNING works on INSERT, UPDATE, and DELETE; inserts echo written values, updates return post-update values, deletes the rows as read."
|
|
260
|
+
"example": "DELETE FROM keyed WHERE name = 'x' RETURNING keyed.name, keyed.score",
|
|
261
|
+
"notes": "RETURNING works on INSERT, UPDATE, and DELETE; inserts echo written values, updates return post-update values, and deletes return the rows as read. Columns and target.* may be target-qualified."
|
|
262
262
|
},
|
|
263
263
|
{
|
|
264
264
|
"id": "mutation.upsert",
|
|
@@ -317,7 +317,7 @@
|
|
|
317
317
|
"id": "predicate.quantified",
|
|
318
318
|
"status": "supported",
|
|
319
319
|
"example": "SELECT region FROM rows WHERE amount > ALL (SELECT amount FROM dims)",
|
|
320
|
-
"notes": "ANY/SOME/ALL use full three-valued logic
|
|
320
|
+
"notes": "ANY/SOME/ALL use full three-valued logic, including when a correlated form is nested below OR, NOT, CASE, or a select expression. SQLite itself has no quantified comparisons."
|
|
321
321
|
},
|
|
322
322
|
{
|
|
323
323
|
"id": "predicate.ilike",
|
|
@@ -400,7 +400,19 @@
|
|
|
400
400
|
"id": "subquery.correlated-select",
|
|
401
401
|
"status": "supported",
|
|
402
402
|
"example": "SELECT r.region, (SELECT AVG(q.amount) FROM rows q WHERE q.region = r.region) AS regional FROM rows r",
|
|
403
|
-
"notes": "Correlated scalar aggregates decorrelate in the select list. In a grouped query, their outer references must be GROUP BY columns and the scalar cannot sit inside an outer aggregate."
|
|
403
|
+
"notes": "Correlated scalar aggregates and single-row projections decorrelate in the select list. In a grouped query, their outer references must be GROUP BY columns and the scalar cannot sit inside an outer aggregate."
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"id": "subquery.correlated-select-limit",
|
|
407
|
+
"status": "supported",
|
|
408
|
+
"example": "SELECT r.region, (SELECT q.amount FROM rows q WHERE q.region = r.region ORDER BY q.amount DESC LIMIT 1) AS peak FROM rows r",
|
|
409
|
+
"notes": "ORDER BY, LIMIT, and OFFSET apply independently to each distinct outer probe. Zero rows yield NULL and more than one unbounded row raises a scalar-cardinality error."
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"id": "subquery.correlated-json-aggregate",
|
|
413
|
+
"status": "supported",
|
|
414
|
+
"example": "SELECT r.region, (SELECT JSON_ARRAYAGG(JSON_OBJECT('amount' VALUE q.amount) ORDER BY q.amount) FROM rows q WHERE q.region = r.region) AS amounts FROM rows r",
|
|
415
|
+
"notes": "JSON aggregate expressions use the same set-at-a-time decorrelation as numeric aggregates and preserve their JSON result domain."
|
|
404
416
|
},
|
|
405
417
|
{
|
|
406
418
|
"id": "subquery.correlated-select-grouped",
|
|
@@ -418,7 +430,7 @@
|
|
|
418
430
|
"id": "subquery.correlated-exists-expression",
|
|
419
431
|
"status": "supported",
|
|
420
432
|
"example": "SELECT r.amount FROM rows r WHERE r.amount > 100 OR EXISTS (SELECT d.region FROM dims d WHERE d.region = r.region)",
|
|
421
|
-
"notes": "Correlated EXISTS and NOT EXISTS remain set-at-a-time
|
|
433
|
+
"notes": "Correlated EXISTS and NOT EXISTS remain set-at-a-time below OR, NOT, or CASE and across deeper correlated EXISTS, IN, NOT IN, or scalar blocks. Generated aliases are unique across the complete plan tree."
|
|
422
434
|
},
|
|
423
435
|
{
|
|
424
436
|
"id": "subquery.correlated-non-equi",
|
|
@@ -438,6 +450,12 @@
|
|
|
438
450
|
"example": "SELECT region FROM rows r WHERE region NOT IN (SELECT d.region FROM dims d WHERE d.region = r.region)",
|
|
439
451
|
"notes": "Correlated NOT IN preserves empty-set and NULL semantics rather than treating it as a simple anti-join."
|
|
440
452
|
},
|
|
453
|
+
{
|
|
454
|
+
"id": "subquery.correlated-membership-expression",
|
|
455
|
+
"status": "supported",
|
|
456
|
+
"example": "SELECT r.amount FROM rows r WHERE r.amount = 3 OR r.region NOT IN (SELECT q.region FROM rows q WHERE q.amount < r.amount)",
|
|
457
|
+
"notes": "Correlated IN and NOT IN retain true, false, and unknown results below OR, NOT, CASE, and in select expressions."
|
|
458
|
+
},
|
|
441
459
|
{
|
|
442
460
|
"id": "subquery.correlated-not-in-non-equi",
|
|
443
461
|
"status": "supported",
|
|
@@ -447,8 +465,8 @@
|
|
|
447
465
|
{
|
|
448
466
|
"id": "subquery.correlated-quantified",
|
|
449
467
|
"status": "supported",
|
|
450
|
-
"example": "SELECT r.amount FROM rows r WHERE r.amount > ALL (SELECT q.amount FROM rows q WHERE q.region = r.region)",
|
|
451
|
-
"notes": "
|
|
468
|
+
"example": "SELECT r.amount FROM rows r WHERE r.amount = 3 OR r.amount > ALL (SELECT q.amount FROM rows q WHERE q.region = r.region)",
|
|
469
|
+
"notes": "Top-level WHERE uses semi/anti joins. Nested expressions group true, false, unknown, and empty-set counts per distinct outer probe tuple."
|
|
452
470
|
},
|
|
453
471
|
{
|
|
454
472
|
"id": "cte.recursive",
|
|
@@ -1197,7 +1215,7 @@
|
|
|
1197
1215
|
"id": "aggregate.json",
|
|
1198
1216
|
"status": "supported",
|
|
1199
1217
|
"example": "SELECT JSON_ARRAYAGG(JSON_OBJECT('region' VALUE region)) AS regions FROM rows",
|
|
1200
|
-
"notes": "Supports DISTINCT, embeds JSON-producing inputs as documents, includes SQL NULL as JSON null, and returns NULL for empty input. FILTER, window use,
|
|
1218
|
+
"notes": "Supports DISTINCT and aggregate-local ORDER BY, embeds JSON-producing inputs as documents, includes SQL NULL as JSON null, and returns NULL for empty input. FILTER, window use, and explicit NULL/ABSENT clauses are not supported; input order is unspecified without ORDER BY."
|
|
1201
1219
|
},
|
|
1202
1220
|
{
|
|
1203
1221
|
"id": "type.array",
|