@minnowdb/core 0.7.2 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/client.d.ts +2 -2
- package/dist/engine/client.js +7 -2
- package/dist/engine/database.d.ts +11 -1
- package/dist/engine/database.js +642 -175
- package/dist/engine/optimizer.js +48 -26
- package/dist/engine/query.d.ts +98 -0
- package/dist/engine/query.js +1452 -165
- package/dist/engine/sql-domains.d.ts +11 -0
- package/dist/engine/sql-domains.js +56 -0
- package/dist/engine/sql-functions.js +129 -3
- package/dist/engine/sql-json.d.ts +2 -0
- package/dist/engine/sql-json.js +4 -0
- package/dist/engine/sql-semantics.d.ts +9 -1
- package/dist/engine/sql-semantics.js +19 -3
- package/dist/engine/vector.js +29 -13
- package/dist/engine/worker-server.js +4 -1
- package/dist/plan/model.d.ts +14 -2
- package/dist/storage/indexeddb.js +13 -8
- package/dist/storage/memory.d.ts +2 -1
- package/dist/storage/memory.js +29 -4
- package/dist/storage/toolkit/record-core.js +11 -6
- package/dist/storage/types.d.ts +11 -0
- package/dist/testing/sqllogictest.js +3 -1
- package/package.json +1 -1
- package/postgres-feature-profile.json +9 -4
- package/sql-feature-matrix.json +541 -4
|
@@ -7,7 +7,18 @@ import type { SqlDomain } from "../storage/types.js";
|
|
|
7
7
|
export declare function protectedSqlTextValue(value: string): string;
|
|
8
8
|
/** Removes only the ordinary-TEXT wrapper, leaving real domain values tagged. */
|
|
9
9
|
export declare function externalSqlTextValue(value: unknown): unknown;
|
|
10
|
+
/** The fractional digits a finite number shows when written as a decimal: 1.25 has 2, 8 has 0. */
|
|
11
|
+
export declare function decimalScaleOfNumber(value: number): number;
|
|
10
12
|
export declare function isExactNumeric(value: unknown): value is string;
|
|
13
|
+
/**
|
|
14
|
+
* PostgreSQL's numeric ROUND and TRUNC at `digits` fractional places, which may be negative to
|
|
15
|
+
* work left of the decimal point (`ROUND(12345.67, -1)` is 12350). ROUND is half away from
|
|
16
|
+
* zero, TRUNC toward zero. The result is canonical; a caller wanting display scale reads it
|
|
17
|
+
* from the inferred column domain.
|
|
18
|
+
*/
|
|
19
|
+
export declare function exactNumericRounded(value: string, digits: number, mode: "round" | "trunc"): string;
|
|
20
|
+
/** ABS, FLOOR, CEIL, and SIGN over an exact NUMERIC value, as PostgreSQL's numeric variants. */
|
|
21
|
+
export declare function exactNumericUnary(name: "ABS" | "FLOOR" | "CEIL" | "SIGN", value: string): string;
|
|
11
22
|
export declare function exactNumericValue(value: unknown, precision?: number, scale?: number): string | null;
|
|
12
23
|
/**
|
|
13
24
|
* Tags a SQL numeric constant with its exact digits, as written. Unlike `exactNumericValue`
|
|
@@ -87,9 +87,55 @@ function taggedDecimalParts(value) {
|
|
|
87
87
|
return void 0;
|
|
88
88
|
return decimalParts(value.slice(NUMERIC.length));
|
|
89
89
|
}
|
|
90
|
+
function decimalScaleOfNumber(value) {
|
|
91
|
+
return normalizeDecimal(decimalParts(value)).scale;
|
|
92
|
+
}
|
|
90
93
|
function isExactNumeric(value) {
|
|
91
94
|
return taggedDecimalParts(value) !== void 0;
|
|
92
95
|
}
|
|
96
|
+
function exactNumericRounded(value, digits, mode) {
|
|
97
|
+
const parts = taggedDecimalParts(value);
|
|
98
|
+
if (parts === void 0)
|
|
99
|
+
throw new TypeError("ROUND and TRUNC take an exact NUMERIC value");
|
|
100
|
+
if (!Number.isSafeInteger(digits) || digits > 1e5 || digits < -1e5) {
|
|
101
|
+
throw new RangeError(`NUMERIC scale is outside the supported range: ${String(digits)}`);
|
|
102
|
+
}
|
|
103
|
+
let result = parts;
|
|
104
|
+
if (parts.scale > digits) {
|
|
105
|
+
const divisor = pow10(parts.scale - digits);
|
|
106
|
+
let quotient = parts.coefficient / divisor;
|
|
107
|
+
if (mode === "round") {
|
|
108
|
+
const remainder = parts.coefficient % divisor;
|
|
109
|
+
if ((remainder < 0n ? -remainder : remainder) * 2n >= divisor) {
|
|
110
|
+
quotient += parts.coefficient < 0n ? -1n : 1n;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
result = digits >= 0 ? { coefficient: quotient, scale: digits } : { coefficient: quotient * pow10(-digits), scale: 0 };
|
|
114
|
+
}
|
|
115
|
+
return boundedTaggedDomainValue(NUMERIC, formatDecimal(normalizeDecimal(result)), "NUMERIC result");
|
|
116
|
+
}
|
|
117
|
+
function exactNumericUnary(name, value) {
|
|
118
|
+
const parts = taggedDecimalParts(value);
|
|
119
|
+
if (parts === void 0)
|
|
120
|
+
throw new TypeError(`${name} takes an exact NUMERIC value`);
|
|
121
|
+
const { coefficient, scale } = parts;
|
|
122
|
+
let result;
|
|
123
|
+
if (name === "ABS") {
|
|
124
|
+
result = { coefficient: coefficient < 0n ? -coefficient : coefficient, scale };
|
|
125
|
+
} else if (name === "SIGN") {
|
|
126
|
+
result = { coefficient: coefficient === 0n ? 0n : coefficient < 0n ? -1n : 1n, scale: 0 };
|
|
127
|
+
} else {
|
|
128
|
+
const divisor = pow10(scale);
|
|
129
|
+
let quotient = coefficient / divisor;
|
|
130
|
+
const remainder = coefficient % divisor;
|
|
131
|
+
if (name === "FLOOR" && remainder < 0n)
|
|
132
|
+
quotient -= 1n;
|
|
133
|
+
if (name === "CEIL" && remainder > 0n)
|
|
134
|
+
quotient += 1n;
|
|
135
|
+
result = { coefficient: quotient, scale: 0 };
|
|
136
|
+
}
|
|
137
|
+
return boundedTaggedDomainValue(NUMERIC, formatDecimal(normalizeDecimal(result)), "NUMERIC result");
|
|
138
|
+
}
|
|
93
139
|
function exactNumericValue(value, precision, scale) {
|
|
94
140
|
if (value === null || value === void 0)
|
|
95
141
|
return null;
|
|
@@ -582,6 +628,13 @@ function collatorFor(locale, displayName) {
|
|
|
582
628
|
return created;
|
|
583
629
|
}
|
|
584
630
|
function externalSqlDomainColumnValue(value, domain) {
|
|
631
|
+
if (domain?.kind === "numeric" && typeof value === "number" && Number.isFinite(value)) {
|
|
632
|
+
try {
|
|
633
|
+
return externalSqlDomainValue(exactNumericValue(value));
|
|
634
|
+
} catch {
|
|
635
|
+
return value;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
585
638
|
if (domain?.kind === "numeric" && domain.scale !== void 0 && domain.scale > 0 && typeof value === "string" && value.startsWith(NUMERIC)) {
|
|
586
639
|
const text = value.slice(NUMERIC.length);
|
|
587
640
|
const dot = text.indexOf(".");
|
|
@@ -676,11 +729,14 @@ export {
|
|
|
676
729
|
collatedDomainValue,
|
|
677
730
|
concatenatedSqlValue,
|
|
678
731
|
dateDomainValue,
|
|
732
|
+
decimalScaleOfNumber,
|
|
679
733
|
enumDomainCompare,
|
|
680
734
|
exactNumericAsNumber,
|
|
681
735
|
exactNumericBinary,
|
|
682
736
|
exactNumericCompare,
|
|
683
737
|
exactNumericLiteral,
|
|
738
|
+
exactNumericRounded,
|
|
739
|
+
exactNumericUnary,
|
|
684
740
|
exactNumericValue,
|
|
685
741
|
externalSqlDomainColumnValue,
|
|
686
742
|
externalSqlDomainValue,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dateIsoString, dateMilliseconds, dateUtcDate, dateUtcDay, dateUtcFullYear, dateUtcHours, dateUtcMinutes, dateUtcMonth, dateUtcSeconds } from "../date-value.js";
|
|
2
2
|
import { MAX_SQL_SCALAR_RESULT_CHARACTERS } from "./cache-limits.js";
|
|
3
|
-
import { dateDomainValue, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, protectedSqlTextValue } from "./sql-domains.js";
|
|
3
|
+
import { dateDomainValue, exactNumericRounded, exactNumericUnary, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, isExactNumeric, protectedSqlTextValue } from "./sql-domains.js";
|
|
4
4
|
import { compileRegexPattern, parseSqlTimestampText, stringArgument } from "./sql-semantics.js";
|
|
5
5
|
function text(name, value) {
|
|
6
6
|
const source = stringArgument(name, value);
|
|
@@ -561,6 +561,12 @@ function ageInterval(later, earlier) {
|
|
|
561
561
|
}
|
|
562
562
|
return intervalDomainValue(`${String(sign * months)} months ${String(sign * days)} days ${String(sign * milliseconds / 1e3)} seconds`);
|
|
563
563
|
}
|
|
564
|
+
function integerArgument(name, value) {
|
|
565
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value)) {
|
|
566
|
+
throw new TypeError(`${name} requires integers`);
|
|
567
|
+
}
|
|
568
|
+
return value;
|
|
569
|
+
}
|
|
564
570
|
function nullish(value) {
|
|
565
571
|
return value === null || value === void 0;
|
|
566
572
|
}
|
|
@@ -782,6 +788,124 @@ const simpleScalarFunctions = /* @__PURE__ */ new Map([
|
|
|
782
788
|
}
|
|
783
789
|
}
|
|
784
790
|
],
|
|
791
|
+
[
|
|
792
|
+
"REGEXP_SUBSTR",
|
|
793
|
+
{
|
|
794
|
+
minArgs: 2,
|
|
795
|
+
maxArgs: 3,
|
|
796
|
+
returns: "string",
|
|
797
|
+
evaluate: (values) => {
|
|
798
|
+
const expression = compileRegexPattern(text("SUBSTRING", values[1]), regexFlags("SUBSTRING", values[2]));
|
|
799
|
+
const match = expression.exec(text("SUBSTRING", values[0]));
|
|
800
|
+
if (match === null)
|
|
801
|
+
return null;
|
|
802
|
+
const group = match.length > 1 ? match[1] : match[0];
|
|
803
|
+
return group === void 0 ? null : bounded(group, "SUBSTRING");
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
],
|
|
807
|
+
[
|
|
808
|
+
"NUM_NONNULLS",
|
|
809
|
+
{
|
|
810
|
+
minArgs: 1,
|
|
811
|
+
maxArgs: Number.POSITIVE_INFINITY,
|
|
812
|
+
returns: "number",
|
|
813
|
+
nullOnNull: false,
|
|
814
|
+
evaluate: (values) => values.filter((value) => !nullish(value)).length
|
|
815
|
+
}
|
|
816
|
+
],
|
|
817
|
+
[
|
|
818
|
+
"NUM_NULLS",
|
|
819
|
+
{
|
|
820
|
+
minArgs: 1,
|
|
821
|
+
maxArgs: Number.POSITIVE_INFINITY,
|
|
822
|
+
returns: "number",
|
|
823
|
+
nullOnNull: false,
|
|
824
|
+
evaluate: (values) => values.filter((value) => nullish(value)).length
|
|
825
|
+
}
|
|
826
|
+
],
|
|
827
|
+
[
|
|
828
|
+
"GCD",
|
|
829
|
+
{
|
|
830
|
+
minArgs: 2,
|
|
831
|
+
maxArgs: 2,
|
|
832
|
+
returns: "number",
|
|
833
|
+
evaluate: (values) => {
|
|
834
|
+
let a = Math.abs(integerArgument("GCD", values[0]));
|
|
835
|
+
let b = Math.abs(integerArgument("GCD", values[1]));
|
|
836
|
+
while (b !== 0)
|
|
837
|
+
[a, b] = [b, a % b];
|
|
838
|
+
return a;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
],
|
|
842
|
+
[
|
|
843
|
+
"LCM",
|
|
844
|
+
{
|
|
845
|
+
minArgs: 2,
|
|
846
|
+
maxArgs: 2,
|
|
847
|
+
returns: "number",
|
|
848
|
+
evaluate: (values) => {
|
|
849
|
+
const a = Math.abs(integerArgument("LCM", values[0]));
|
|
850
|
+
const b = Math.abs(integerArgument("LCM", values[1]));
|
|
851
|
+
if (a === 0 || b === 0)
|
|
852
|
+
return 0;
|
|
853
|
+
let x = a;
|
|
854
|
+
let y = b;
|
|
855
|
+
while (y !== 0)
|
|
856
|
+
[x, y] = [y, x % y];
|
|
857
|
+
const result = a / x * b;
|
|
858
|
+
if (!Number.isSafeInteger(result))
|
|
859
|
+
throw new RangeError("LCM result is out of range");
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
],
|
|
864
|
+
[
|
|
865
|
+
"TO_HEX",
|
|
866
|
+
{
|
|
867
|
+
minArgs: 1,
|
|
868
|
+
maxArgs: 1,
|
|
869
|
+
returns: "string",
|
|
870
|
+
evaluate: (values) => {
|
|
871
|
+
const value = values[0];
|
|
872
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value)) {
|
|
873
|
+
throw new TypeError("TO_HEX requires an integer");
|
|
874
|
+
}
|
|
875
|
+
if (value < 0) {
|
|
876
|
+
const width = value >= -2147483648 ? 32 : 64;
|
|
877
|
+
return BigInt.asUintN(width, BigInt(value)).toString(16);
|
|
878
|
+
}
|
|
879
|
+
return BigInt(value).toString(16);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
],
|
|
883
|
+
[
|
|
884
|
+
"QUOTE_LITERAL",
|
|
885
|
+
{
|
|
886
|
+
minArgs: 1,
|
|
887
|
+
maxArgs: 1,
|
|
888
|
+
returns: "string",
|
|
889
|
+
evaluate: (values) => {
|
|
890
|
+
const value = values[0];
|
|
891
|
+
const rendered2 = typeof value === "string" ? value : typeof value === "number" ? String(value) : String(value);
|
|
892
|
+
const escaped = rendered2.replace(/'/g, "''");
|
|
893
|
+
return bounded(escaped.includes("\\") ? `E'${escaped.replace(/\\/g, "\\\\")}'` : `'${escaped}'`, "QUOTE_LITERAL");
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
],
|
|
897
|
+
[
|
|
898
|
+
"QUOTE_IDENT",
|
|
899
|
+
{
|
|
900
|
+
minArgs: 1,
|
|
901
|
+
maxArgs: 1,
|
|
902
|
+
returns: "string",
|
|
903
|
+
evaluate: (values) => {
|
|
904
|
+
const name = text("QUOTE_IDENT", values[0]);
|
|
905
|
+
return /^[a-z_][a-z0-9_]*$/.test(name) ? name : bounded(`"${name.replace(/"/g, '""')}"`, "QUOTE_IDENT");
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
],
|
|
785
909
|
[
|
|
786
910
|
"MINNOW_REGEX_MATCH",
|
|
787
911
|
{
|
|
@@ -849,7 +973,7 @@ const simpleScalarFunctions = /* @__PURE__ */ new Map([
|
|
|
849
973
|
minArgs: 1,
|
|
850
974
|
maxArgs: 1,
|
|
851
975
|
returns: "number",
|
|
852
|
-
evaluate: (values) => Math.sign(number("SIGN", values[0]))
|
|
976
|
+
evaluate: (values) => isExactNumeric(values[0]) ? exactNumericUnary("SIGN", values[0]) : Math.sign(number("SIGN", values[0]))
|
|
853
977
|
}
|
|
854
978
|
],
|
|
855
979
|
[
|
|
@@ -859,8 +983,10 @@ const simpleScalarFunctions = /* @__PURE__ */ new Map([
|
|
|
859
983
|
maxArgs: 2,
|
|
860
984
|
returns: "number",
|
|
861
985
|
evaluate: (values) => {
|
|
862
|
-
const operand = number("TRUNC", values[0]);
|
|
863
986
|
const digits = values.length > 1 ? integer("TRUNC", values[1]) : 0;
|
|
987
|
+
if (isExactNumeric(values[0]))
|
|
988
|
+
return exactNumericRounded(values[0], digits, "trunc");
|
|
989
|
+
const operand = number("TRUNC", values[0]);
|
|
864
990
|
const scale = 10 ** digits;
|
|
865
991
|
return Math.trunc(operand * scale) / scale;
|
|
866
992
|
}
|
|
@@ -35,4 +35,6 @@ export declare function jsonIsValid(document: unknown, kind: string): boolean;
|
|
|
35
35
|
* than the default. Constructors return JSON text at the JavaScript boundary.
|
|
36
36
|
*/
|
|
37
37
|
export declare function jsonConstructor(name: "JSON_ARRAY" | "JSON_OBJECT", values: readonly unknown[]): string;
|
|
38
|
+
/** to_json(x) / to_jsonb(x): one SQL value as a JSON document (a JSON-domain value as itself). */
|
|
39
|
+
export declare function jsonDocumentOf(value: unknown): string;
|
|
38
40
|
export {};
|
package/dist/engine/sql-json.js
CHANGED
|
@@ -175,6 +175,9 @@ function boundedJsonDocument(value, caller) {
|
|
|
175
175
|
}
|
|
176
176
|
return document;
|
|
177
177
|
}
|
|
178
|
+
function jsonDocumentOf(value) {
|
|
179
|
+
return boundedJsonValue(value ?? null, "TO_JSON value");
|
|
180
|
+
}
|
|
178
181
|
function boundedJsonValue(value, label) {
|
|
179
182
|
const domainDocument = jsonDomainDocument(value);
|
|
180
183
|
if (domainDocument !== void 0) {
|
|
@@ -215,6 +218,7 @@ export {
|
|
|
215
218
|
jsonArrowStep,
|
|
216
219
|
jsonAtPath,
|
|
217
220
|
jsonConstructor,
|
|
221
|
+
jsonDocumentOf,
|
|
218
222
|
jsonIsValid,
|
|
219
223
|
parseJsonPath
|
|
220
224
|
};
|
|
@@ -12,6 +12,13 @@ export declare function parseSqlTimestampText(text: string): Date | undefined;
|
|
|
12
12
|
* comparable-types error below, so a genuine mismatch still fails.
|
|
13
13
|
*/
|
|
14
14
|
export declare function coercedComparable(text: string, other: unknown): unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Reads an untyped string constant in a primitive column type, the way PostgreSQL types an
|
|
17
|
+
* unknown-typed literal by its context: a timestamp spelling for datetime, a finite number for
|
|
18
|
+
* number, `t`/`true`/`1` and `f`/`false`/`0` for boolean. Text that does not parse is returned
|
|
19
|
+
* unchanged, so the caller's own type check still reports the mismatch.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readUntypedText(type: "datetime" | "number" | "boolean" | "string", text: string): unknown;
|
|
15
22
|
/**
|
|
16
23
|
* Applies the untyped-literal reading to a comparison's two operands: a plain string beside a
|
|
17
24
|
* typed value (datetime, DATE, number, boolean) is read in that value's type when it parses.
|
|
@@ -29,7 +36,8 @@ export declare function compareSqlStrings(left: string, right: string): number;
|
|
|
29
36
|
export declare function encodeSqlEqualityValue(value: unknown): readonly unknown[];
|
|
30
37
|
/**
|
|
31
38
|
* SQLite-compatible ROUND behavior for Minnow's finite number type: precision truncates to an
|
|
32
|
-
* integer and clamps to
|
|
39
|
+
* integer and clamps to -30..30, with ties rounded away from zero. A negative precision rounds
|
|
40
|
+
* left of the decimal point as PostgreSQL does: ROUND(1250, -2) is 1300.
|
|
33
41
|
*/
|
|
34
42
|
export declare function roundSqlNumber(value: number, precision?: number): number;
|
|
35
43
|
/** A whole-string SQL pattern matcher. Unlike RegExp, test never coerces its input. */
|
|
@@ -14,15 +14,24 @@ function parseSqlTimestampText(text) {
|
|
|
14
14
|
}
|
|
15
15
|
function coercedComparable(text, other) {
|
|
16
16
|
if (other instanceof Date || isDateDomainValue(other))
|
|
17
|
+
return readUntypedText("datetime", text);
|
|
18
|
+
if (typeof other === "number")
|
|
19
|
+
return readUntypedText("number", text);
|
|
20
|
+
if (typeof other === "boolean")
|
|
21
|
+
return readUntypedText("boolean", text);
|
|
22
|
+
return text;
|
|
23
|
+
}
|
|
24
|
+
function readUntypedText(type, text) {
|
|
25
|
+
if (type === "datetime")
|
|
17
26
|
return parseSqlTimestampText(text) ?? text;
|
|
18
|
-
if (
|
|
27
|
+
if (type === "number") {
|
|
19
28
|
const trimmed = text.trim();
|
|
20
29
|
if (trimmed === "")
|
|
21
30
|
return text;
|
|
22
31
|
const parsed = Number(trimmed);
|
|
23
32
|
return Number.isFinite(parsed) ? parsed : text;
|
|
24
33
|
}
|
|
25
|
-
if (
|
|
34
|
+
if (type === "boolean") {
|
|
26
35
|
const lowered = text.trim().toLowerCase();
|
|
27
36
|
if (lowered === "t" || lowered === "true" || lowered === "1")
|
|
28
37
|
return true;
|
|
@@ -133,7 +142,13 @@ function encodeSqlEqualityValue(value) {
|
|
|
133
142
|
throw new TypeError("Query produced an unsupported value");
|
|
134
143
|
}
|
|
135
144
|
function roundSqlNumber(value, precision = 0) {
|
|
136
|
-
const digits = Math.min(30, Math.max(
|
|
145
|
+
const digits = Math.min(30, Math.max(-30, Math.trunc(precision)));
|
|
146
|
+
if (digits < 0) {
|
|
147
|
+
if (!Number.isFinite(value))
|
|
148
|
+
return value;
|
|
149
|
+
const scale = 10 ** -digits;
|
|
150
|
+
return roundSqlNumber(value / scale) * scale;
|
|
151
|
+
}
|
|
137
152
|
if (digits === 0 && Number.isFinite(value)) {
|
|
138
153
|
const magnitude = Math.abs(value);
|
|
139
154
|
if (magnitude >= 4503599627370496)
|
|
@@ -628,6 +643,7 @@ export {
|
|
|
628
643
|
defineSqlResultProperty,
|
|
629
644
|
encodeSqlEqualityValue,
|
|
630
645
|
parseSqlTimestampText,
|
|
646
|
+
readUntypedText,
|
|
631
647
|
roundSqlNumber,
|
|
632
648
|
stringArgument
|
|
633
649
|
};
|
package/dist/engine/vector.js
CHANGED
|
@@ -2,15 +2,15 @@ import { dateMilliseconds } from "../date-value.js";
|
|
|
2
2
|
import { crossJoinPlan, isCrossJoinPlan } from "../plan/model.js";
|
|
3
3
|
import { MAX_TEMP_RUN_BATCH_BYTES, MAX_TEMP_RUN_PAGE_BYTES, MAX_TEMP_RUN_PAGES_PER_BATCH } from "../storage/types.js";
|
|
4
4
|
import { throwIfAborted } from "./cancellation.js";
|
|
5
|
-
import { cachedListMembership, childExpressions, distinctFromComparison, nullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionEvaluator, scalarFunctionValue, unknownColumnDomains } from "./query.js";
|
|
5
|
+
import { cachedListMembership, childExpressions, distinctFromComparison, nullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionEvaluator, scalarFunctionValue, unknownColumnDomains, integerQuotient } from "./query.js";
|
|
6
6
|
import { jsonConstructor } from "./sql-json.js";
|
|
7
7
|
import { bm25DocumentScore, cachedQueryTerms, FtsStatsAccumulator, fullTermsMask, renderDocumentValue, termFrequencies, termsMask, tokenize } from "./fts.js";
|
|
8
8
|
import { ByteGroupIndex } from "./group-index.js";
|
|
9
9
|
import { ByteJoinIndex } from "./join-index.js";
|
|
10
10
|
import { UnknownTableError } from "./errors.js";
|
|
11
11
|
import { QueryMemoryBudgetError, QueryMemoryContext } from "./memory.js";
|
|
12
|
-
import { coerceComparisonOperands, compareSqlValues, compileSimilarPattern, defineSqlResultProperty } from "./sql-semantics.js";
|
|
13
|
-
import { concatenatedSqlValue, exactNumericBinary, externalSqlDomainValue, isDateDomainValue, isExactNumeric, preservedJsonDomainValue, protectedSqlTextValue } from "./sql-domains.js";
|
|
12
|
+
import { coerceComparisonOperands, compareSqlValues, compileSimilarPattern, defineSqlResultProperty, readUntypedText } from "./sql-semantics.js";
|
|
13
|
+
import { concatenatedSqlValue, exactNumericBinary, externalSqlDomainValue, isDateDomainValue, isExactNumeric, preservedJsonDomainValue, protectedSqlTextValue, isSqlDomainValue } from "./sql-domains.js";
|
|
14
14
|
import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
|
|
15
15
|
const DEFAULT_BATCH_ROWS = 2048;
|
|
16
16
|
const MAX_SPLIT_LIST_MEMBERS = 64;
|
|
@@ -899,6 +899,7 @@ function bindExpression(expression, sources, aggregateSpecs, aggregateIndexes, m
|
|
|
899
899
|
left: bindExpression(expression.left, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
|
|
900
900
|
right: bindExpression(expression.right, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
|
|
901
901
|
...expression.kind === "condition" && expression.escape !== void 0 ? { escape: expression.escape } : {},
|
|
902
|
+
...expression.kind === "binary" && expression.integer === true ? { integer: true } : {},
|
|
902
903
|
signature
|
|
903
904
|
};
|
|
904
905
|
}
|
|
@@ -3865,7 +3866,7 @@ function evaluateFinalExpression(plan, expression, group) {
|
|
|
3865
3866
|
throw new TypeError("Selected full-text expression must appear in GROUP BY");
|
|
3866
3867
|
}
|
|
3867
3868
|
if (expression.kind === "binary") {
|
|
3868
|
-
return binaryValue(expression.operator, evaluateFinalExpression(plan, expression.left, group), evaluateFinalExpression(plan, expression.right, group));
|
|
3869
|
+
return binaryValue(expression.operator, evaluateFinalExpression(plan, expression.left, group), evaluateFinalExpression(plan, expression.right, group), expression.integer === true);
|
|
3869
3870
|
}
|
|
3870
3871
|
if (expression.name === "COALESCE") {
|
|
3871
3872
|
for (const argument of expression.arguments) {
|
|
@@ -4218,7 +4219,9 @@ function constantBoundExpressionValue(expression) {
|
|
|
4218
4219
|
if (left === void 0 || right === void 0)
|
|
4219
4220
|
return void 0;
|
|
4220
4221
|
try {
|
|
4221
|
-
return {
|
|
4222
|
+
return {
|
|
4223
|
+
value: binaryValue(expression.operator, left.value, right.value, expression.integer === true)
|
|
4224
|
+
};
|
|
4222
4225
|
} catch {
|
|
4223
4226
|
return void 0;
|
|
4224
4227
|
}
|
|
@@ -4269,7 +4272,7 @@ function dictionaryNumericExpression(expression) {
|
|
|
4269
4272
|
source: column.source,
|
|
4270
4273
|
vector: column.vector,
|
|
4271
4274
|
signature: expression.signature,
|
|
4272
|
-
evaluate: columnFirst ? (dictionaryValue) => binaryValue(expression.operator, dictionaryValue, value) : (dictionaryValue) => binaryValue(expression.operator, value, dictionaryValue)
|
|
4275
|
+
evaluate: columnFirst ? (dictionaryValue) => binaryValue(expression.operator, dictionaryValue, value, expression.integer === true) : (dictionaryValue) => binaryValue(expression.operator, value, dictionaryValue, expression.integer === true)
|
|
4273
4276
|
};
|
|
4274
4277
|
}
|
|
4275
4278
|
return void 0;
|
|
@@ -4404,10 +4407,12 @@ function booleanTruth(expression, evaluateValue) {
|
|
|
4404
4407
|
const distinct = distinctFromComparison(evaluateValue(expression.left), evaluateValue(expression.right));
|
|
4405
4408
|
return operator === "IS DISTINCT FROM" ? distinct : !distinct;
|
|
4406
4409
|
}
|
|
4407
|
-
const
|
|
4408
|
-
const
|
|
4409
|
-
if (
|
|
4410
|
+
const evaluatedLeft = evaluateValue(expression.left);
|
|
4411
|
+
const evaluatedRight = evaluateValue(expression.right);
|
|
4412
|
+
if (evaluatedLeft === null || evaluatedLeft === void 0 || evaluatedRight === null || evaluatedRight === void 0) {
|
|
4410
4413
|
return null;
|
|
4414
|
+
}
|
|
4415
|
+
const [left, right] = coerceComparisonOperands(evaluatedLeft, evaluatedRight);
|
|
4411
4416
|
const a = comparable(left);
|
|
4412
4417
|
const b = comparable(right);
|
|
4413
4418
|
if (operator === "=")
|
|
@@ -4544,9 +4549,10 @@ function compiledBatchExpression(plan, expression) {
|
|
|
4544
4549
|
}
|
|
4545
4550
|
case "binary": {
|
|
4546
4551
|
const operator = expression.operator;
|
|
4552
|
+
const integer = expression.integer === true;
|
|
4547
4553
|
const left = compiledBatchExpression(plan, expression.left);
|
|
4548
4554
|
const right = compiledBatchExpression(plan, expression.right);
|
|
4549
|
-
compiled = (batch, row) => binaryValue(operator, left(batch, row), right(batch, row));
|
|
4555
|
+
compiled = (batch, row) => binaryValue(operator, left(batch, row), right(batch, row), integer);
|
|
4550
4556
|
break;
|
|
4551
4557
|
}
|
|
4552
4558
|
case "case": {
|
|
@@ -4659,7 +4665,7 @@ function evaluateExpression(expression, rowsBySource) {
|
|
|
4659
4665
|
return expression.op === "match" ? ftsBatchTruth(expression, null, rowsBySource, 0) : ftsBm25BatchValue(expression, null, rowsBySource, 0);
|
|
4660
4666
|
}
|
|
4661
4667
|
if (expression.kind === "binary") {
|
|
4662
|
-
return binaryValue(expression.operator, evaluateExpression(expression.left, rowsBySource), evaluateExpression(expression.right, rowsBySource));
|
|
4668
|
+
return binaryValue(expression.operator, evaluateExpression(expression.left, rowsBySource), evaluateExpression(expression.right, rowsBySource), expression.integer === true);
|
|
4663
4669
|
}
|
|
4664
4670
|
if (expression.name === "COALESCE") {
|
|
4665
4671
|
for (const argument of expression.arguments) {
|
|
@@ -4673,7 +4679,7 @@ function evaluateExpression(expression, rowsBySource) {
|
|
|
4673
4679
|
throw new TypeError(`${expression.name} requires grouped execution`);
|
|
4674
4680
|
return scalarFunctionValue(expression.name, expression.arguments.map((argument) => evaluateExpression(argument, rowsBySource)));
|
|
4675
4681
|
}
|
|
4676
|
-
function binaryValue(operator, left, right) {
|
|
4682
|
+
function binaryValue(operator, left, right, integer = false) {
|
|
4677
4683
|
if (left === null || left === void 0 || right === null || right === void 0)
|
|
4678
4684
|
return null;
|
|
4679
4685
|
if (typeof left === "number" && typeof right === "number") {
|
|
@@ -4684,12 +4690,22 @@ function binaryValue(operator, left, right) {
|
|
|
4684
4690
|
if (operator === "*")
|
|
4685
4691
|
return left * right;
|
|
4686
4692
|
if (operator === "/")
|
|
4687
|
-
return right === 0 ? null : left / right;
|
|
4693
|
+
return integer ? integerQuotient(left, right) : right === 0 ? null : left / right;
|
|
4688
4694
|
if (operator === "%")
|
|
4689
4695
|
return right === 0 ? null : left % right;
|
|
4690
4696
|
}
|
|
4691
4697
|
if (operator === "||")
|
|
4692
4698
|
return concatenatedSqlValue(left, right);
|
|
4699
|
+
if (typeof left === "string" && typeof right === "number" && !isSqlDomainValue(left)) {
|
|
4700
|
+
const read = readUntypedText("number", left);
|
|
4701
|
+
if (typeof read === "number")
|
|
4702
|
+
return binaryValue(operator, read, right, integer);
|
|
4703
|
+
}
|
|
4704
|
+
if (typeof right === "string" && typeof left === "number" && !isSqlDomainValue(right)) {
|
|
4705
|
+
const read = readUntypedText("number", right);
|
|
4706
|
+
if (typeof read === "number")
|
|
4707
|
+
return binaryValue(operator, left, read, integer);
|
|
4708
|
+
}
|
|
4693
4709
|
const exact = exactNumericBinary(operator, left, right);
|
|
4694
4710
|
if (exact !== void 0)
|
|
4695
4711
|
return exact;
|
|
@@ -554,10 +554,13 @@ class DatabaseRpcServer {
|
|
|
554
554
|
return handle.session.execute(sql, params);
|
|
555
555
|
}
|
|
556
556
|
if (method === "stage") {
|
|
557
|
-
const [op, tableName, input] = args;
|
|
557
|
+
const [op, tableName, input, options] = args;
|
|
558
558
|
if (!isStageOp(op)) {
|
|
559
559
|
throw new Error(`Unsupported write stage operation: ${String(op)}`);
|
|
560
560
|
}
|
|
561
|
+
if (op === "upsertBatch") {
|
|
562
|
+
return handle.session.upsertBatch(tableName, input, options);
|
|
563
|
+
}
|
|
561
564
|
return handle.session[op](tableName, input);
|
|
562
565
|
}
|
|
563
566
|
if (method === "commit")
|
package/dist/plan/model.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type ComparisonOperator = "=" | "!=" | "<>" | ">" | ">=" | "<" | "<=";
|
|
|
17
17
|
export type AggregateName = "COUNT" | "SUM" | "AVG" | "MIN" | "MAX" | "JSON_ARRAYAGG" | "STRING_AGG"
|
|
18
18
|
/** Optimizer-only aggregate that enforces scalar-subquery cardinality. */
|
|
19
19
|
| "MINNOW_SINGLE_VALUE";
|
|
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"
|
|
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" | "TO_JSON" | "IS_JSON" | "ARRAY"
|
|
21
21
|
/** Parser-produced `->` JSON member/element access returning a JSON value. */
|
|
22
22
|
| "MINNOW_JSON_GET"
|
|
23
23
|
/** Parser-produced `->>` JSON member/element access returning text. */
|
|
@@ -27,7 +27,7 @@ export type ScalarFunctionName = "ROUND" | "COALESCE" | "DATE_TRUNC" | "DATE_ADD
|
|
|
27
27
|
/** Parser-produced wrapper carrying one explicit collation through ordering/comparison. */
|
|
28
28
|
| "MINNOW_COLLATE" | "NEXTVAL" | "CURRVAL" | "RANDOM" | "GEN_RANDOM_UUID"
|
|
29
29
|
/** Table-driven PostgreSQL string, math, datetime, regex, and formatting functions. */
|
|
30
|
-
| "CONCAT" | "CONCAT_WS" | "LEFT" | "RIGHT" | "REVERSE" | "REPEAT" | "INITCAP" | "SPLIT_PART" | "STRPOS" | "STARTS_WITH" | "TRANSLATE" | "ASCII" | "CHR" | "BTRIM" | "MD5" | "FORMAT" | "REGEXP_REPLACE" | "MINNOW_REGEX_MATCH" | "EXP" | "LN" | "LOG" | "LOG10" | "SIGN" | "TRUNC" | "PI" | "CBRT" | "DIV" | "WIDTH_BUCKET" | "SIN" | "COS" | "TAN" | "ASIN" | "ACOS" | "ATAN" | "ATAN2" | "DEGREES" | "RADIANS" | "TO_CHAR" | "TO_DATE" | "TO_TIMESTAMP" | "MAKE_DATE" | "MAKE_TIMESTAMP" | "AGE";
|
|
30
|
+
| "CONCAT" | "CONCAT_WS" | "LEFT" | "RIGHT" | "REVERSE" | "REPEAT" | "INITCAP" | "SPLIT_PART" | "STRPOS" | "STARTS_WITH" | "TRANSLATE" | "ASCII" | "CHR" | "BTRIM" | "MD5" | "REGEXP_SUBSTR" | "NUM_NONNULLS" | "NUM_NULLS" | "GCD" | "LCM" | "TO_HEX" | "QUOTE_LITERAL" | "QUOTE_IDENT" | "FORMAT" | "REGEXP_REPLACE" | "MINNOW_REGEX_MATCH" | "EXP" | "LN" | "LOG" | "LOG10" | "SIGN" | "TRUNC" | "PI" | "CBRT" | "DIV" | "WIDTH_BUCKET" | "SIN" | "COS" | "TAN" | "ASIN" | "ACOS" | "ATAN" | "ATAN2" | "DEGREES" | "RADIANS" | "TO_CHAR" | "TO_DATE" | "TO_TIMESTAMP" | "MAKE_DATE" | "MAKE_TIMESTAMP" | "AGE";
|
|
31
31
|
/** Exact BM25 corpus statistics attached to a cloned scoring node before execution. */
|
|
32
32
|
export interface FtsStats {
|
|
33
33
|
/** Every row of the corpus, including all-null documents. */
|
|
@@ -50,6 +50,12 @@ export type Expression = {
|
|
|
50
50
|
* PostgreSQL types every decimal constant NUMERIC before evaluating it.
|
|
51
51
|
*/
|
|
52
52
|
exactText?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Written with a decimal point or exponent, or folded from such a constant: PostgreSQL
|
|
55
|
+
* types it NUMERIC, so it is never an integer operand even when its value is whole
|
|
56
|
+
* (`7 / 2.0` is 3.5 where `7 / 2` is 3).
|
|
57
|
+
*/
|
|
58
|
+
decimal?: true;
|
|
53
59
|
}
|
|
54
60
|
/** A `?` or `$n` placeholder; `index` is 0-based. Replaced by a literal at bind time. */
|
|
55
61
|
| {
|
|
@@ -68,6 +74,12 @@ export type Expression = {
|
|
|
68
74
|
operator: BinaryOperator;
|
|
69
75
|
left: Expression;
|
|
70
76
|
right: Expression;
|
|
77
|
+
/**
|
|
78
|
+
* Integer division: both operands are integer-typed, so `/` truncates toward zero as
|
|
79
|
+
* PostgreSQL's integer `/` does. Set by the compiler for constants and by catalog
|
|
80
|
+
* binding for columns; absent, `/` is ordinary Float64 or exact-NUMERIC division.
|
|
81
|
+
*/
|
|
82
|
+
integer?: true;
|
|
71
83
|
} | {
|
|
72
84
|
kind: "call";
|
|
73
85
|
name: AggregateName | ScalarFunctionName;
|
|
@@ -3919,7 +3919,12 @@ class IndexedDbBlockStore {
|
|
|
3919
3919
|
await assertActiveGarbageCollectionMarker(gcStore, current);
|
|
3920
3920
|
}
|
|
3921
3921
|
const updated = updateGarbageCollectionPlanningRecord(current, input);
|
|
3922
|
-
await assertGarbageCollectionCandidateProvenanceInTransaction(transaction,
|
|
3922
|
+
await assertGarbageCollectionCandidateProvenanceInTransaction(transaction, {
|
|
3923
|
+
candidateManifestVersions: input.candidateManifestVersions ?? [],
|
|
3924
|
+
candidateSegmentIds: input.candidateSegmentIds ?? [],
|
|
3925
|
+
candidateBlockIds: input.candidateBlockIds ?? [],
|
|
3926
|
+
candidateTransactionIds: input.candidateTransactionIds ?? []
|
|
3927
|
+
});
|
|
3923
3928
|
gcStore.put(garbageCollectionJobEnvelope(updated), key);
|
|
3924
3929
|
await transactionDone(transaction);
|
|
3925
3930
|
return structuredClone(updated);
|
|
@@ -9717,15 +9722,15 @@ function assertGenericTransactionUpdateAllowed(record, update) {
|
|
|
9717
9722
|
throw new TypeError("Only commitTransaction can set a committed transaction version");
|
|
9718
9723
|
}
|
|
9719
9724
|
}
|
|
9720
|
-
async function assertGarbageCollectionCandidateProvenanceInTransaction(transaction,
|
|
9725
|
+
async function assertGarbageCollectionCandidateProvenanceInTransaction(transaction, candidates) {
|
|
9721
9726
|
const manifestStore = transaction.objectStore("manifests");
|
|
9722
|
-
for (const version of
|
|
9727
|
+
for (const version of candidates.candidateManifestVersions) {
|
|
9723
9728
|
const value = await requestResult(manifestStore.get(version));
|
|
9724
9729
|
if (value === void 0) {
|
|
9725
9730
|
throw new Error(`Garbage collection candidate manifest is missing: ${String(version)}`);
|
|
9726
9731
|
}
|
|
9727
9732
|
}
|
|
9728
|
-
for (const id of
|
|
9733
|
+
for (const id of candidates.candidateTransactionIds) {
|
|
9729
9734
|
const value = await requestResult(transaction.objectStore("transactions").get(id));
|
|
9730
9735
|
const record = value === void 0 ? void 0 : asTransactionRecord(value);
|
|
9731
9736
|
if (record === void 0 || record.status !== "aborted" && (record.status !== "committed" || record.committedVersion === null)) {
|
|
@@ -9733,10 +9738,10 @@ async function assertGarbageCollectionCandidateProvenanceInTransaction(transacti
|
|
|
9733
9738
|
}
|
|
9734
9739
|
}
|
|
9735
9740
|
const manifestProvenBlockIds = /* @__PURE__ */ new Set();
|
|
9736
|
-
const manifestValues = await Promise.all(
|
|
9741
|
+
const manifestValues = await Promise.all(candidates.candidateBlockIds.map((id) => requestResult(transaction.objectStore("catalog").get(manifestBlockKey(id)))));
|
|
9737
9742
|
for (const [index, value] of manifestValues.entries()) {
|
|
9738
9743
|
if (value !== void 0) {
|
|
9739
|
-
const id =
|
|
9744
|
+
const id = candidates.candidateBlockIds[index] ?? "";
|
|
9740
9745
|
asManifestBlockRecord(value, id);
|
|
9741
9746
|
manifestProvenBlockIds.add(id);
|
|
9742
9747
|
}
|
|
@@ -9757,12 +9762,12 @@ async function assertGarbageCollectionCandidateProvenanceInTransaction(transacti
|
|
|
9757
9762
|
return isTerminalCompactionJob(record) && (record.sourceBlockIds.includes(id) || record.outputBlockIds.includes(id));
|
|
9758
9763
|
});
|
|
9759
9764
|
};
|
|
9760
|
-
for (const id of
|
|
9765
|
+
for (const id of candidates.candidateBlockIds) {
|
|
9761
9766
|
if (await blockHasProvenance(id))
|
|
9762
9767
|
continue;
|
|
9763
9768
|
throw new Error(`Garbage collection block candidate has no persisted provenance: ${id}`);
|
|
9764
9769
|
}
|
|
9765
|
-
for (const id of
|
|
9770
|
+
for (const id of candidates.candidateSegmentIds) {
|
|
9766
9771
|
const segmentValue = await requestResult(transaction.objectStore("segments").get(id));
|
|
9767
9772
|
if (segmentValue !== void 0)
|
|
9768
9773
|
continue;
|
package/dist/storage/memory.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { type BeginSnapshotFrameExportInput, type SnapshotFrameExportSession, ty
|
|
|
3
3
|
* The in-process store: record semantics live in `RecordCore` (shared with the OPFS store),
|
|
4
4
|
* block and temp-page bytes live in Maps here, and atomicity comes from running every mutating
|
|
5
5
|
* record operation on a promise-chain queue — each queued body is synchronous, so no operation
|
|
6
|
-
* ever observes another mid-mutation.
|
|
6
|
+
* ever observes another mid-mutation. Every commit acknowledges on the next event-loop turn
|
|
7
|
+
* (`commitTurn`), so a write loop yields to background work as it would on disk.
|
|
7
8
|
*/
|
|
8
9
|
export declare class MemoryBlockStore implements BlockStore {
|
|
9
10
|
#private;
|