@minnowdb/core 0.7.10 → 0.8.0
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 +3 -0
- package/dist/engine/client.js +8 -0
- package/dist/engine/database.js +189 -35
- package/dist/engine/errors.d.ts +2 -2
- package/dist/engine/errors.js +1 -1
- package/dist/engine/live-aggregate.d.ts +12 -0
- package/dist/engine/live-aggregate.js +226 -0
- package/dist/engine/live-patch.d.ts +21 -0
- package/dist/engine/live-patch.js +31 -0
- package/dist/engine/live.d.ts +12 -0
- package/dist/engine/live.js +52 -49
- package/dist/engine/optimizer.js +11 -6
- package/dist/engine/query-cache.js +2 -13
- package/dist/engine/query-generations.d.ts +8 -0
- package/dist/engine/query-generations.js +61 -0
- package/dist/engine/query-identity.d.ts +3 -0
- package/dist/engine/query-identity.js +41 -0
- package/dist/engine/query.d.ts +6 -11
- package/dist/engine/query.js +294 -428
- package/dist/engine/sql-domains.d.ts +4 -0
- package/dist/engine/sql-domains.js +121 -0
- package/dist/engine/sql-semantics.js +7 -4
- package/dist/engine/typed-live.js +28 -20
- package/dist/engine/windows.d.ts +12 -0
- package/dist/engine/windows.js +386 -0
- package/dist/plan/model.d.ts +1 -1
- package/package.json +1 -1
- package/postgres-feature-profile.json +6 -1
- package/sql-feature-matrix.json +20 -27
|
@@ -91,3 +91,7 @@ export declare function concatenatedSqlValue(leftValue: unknown, rightValue: unk
|
|
|
91
91
|
*/
|
|
92
92
|
export declare function intervalDomainCompare(left: unknown, right: unknown): number | undefined;
|
|
93
93
|
export declare function isSqlDomainValue(value: unknown): value is string;
|
|
94
|
+
/** SQL extraction from TIME and INTERVAL without collapsing calendar months into a timestamp. */
|
|
95
|
+
export declare function temporalDomainPart(field: string, value: unknown): number | undefined;
|
|
96
|
+
/** Structural ordering for shipped JSONB and one-dimensional ARRAY values. */
|
|
97
|
+
export declare function structuredDomainCompare(left: unknown, right: unknown): number | undefined;
|
|
@@ -722,6 +722,125 @@ function intervalDomainCompare(left, right) {
|
|
|
722
722
|
function isSqlDomainValue(value) {
|
|
723
723
|
return typeof value === "string" && value.startsWith(PREFIX);
|
|
724
724
|
}
|
|
725
|
+
function temporalDomainPart(field, value) {
|
|
726
|
+
if (typeof value !== "string")
|
|
727
|
+
return void 0;
|
|
728
|
+
let months = 0;
|
|
729
|
+
let days = 0;
|
|
730
|
+
let usecs;
|
|
731
|
+
const interval = value.startsWith(INTERVAL_VALUE);
|
|
732
|
+
if (interval) {
|
|
733
|
+
[months, days, usecs] = JSON.parse(value.slice(INTERVAL_VALUE.length));
|
|
734
|
+
} else if (value.startsWith(TIME_VALUE)) {
|
|
735
|
+
const [hour = "0", minute = "0", second = "0"] = value.slice(TIME_VALUE.length).split(":");
|
|
736
|
+
usecs = Number(hour) * 36e8 + Number(minute) * 6e7 + Math.round(Number(second) * 1e6);
|
|
737
|
+
} else
|
|
738
|
+
return void 0;
|
|
739
|
+
switch (field) {
|
|
740
|
+
case "hour":
|
|
741
|
+
return Math.trunc(usecs / 36e8);
|
|
742
|
+
case "minute":
|
|
743
|
+
return Math.trunc(usecs / 6e7) % 60;
|
|
744
|
+
case "second":
|
|
745
|
+
return usecs % 6e7 / 1e6;
|
|
746
|
+
case "milliseconds":
|
|
747
|
+
return usecs % 6e7 / 1e3;
|
|
748
|
+
case "microseconds":
|
|
749
|
+
return usecs % 6e7;
|
|
750
|
+
case "epoch":
|
|
751
|
+
return interval ? (Math.trunc(months / 12) * 365.25 + months % 12 * 30 + days) * 86400 + usecs / 1e6 : usecs / 1e6;
|
|
752
|
+
}
|
|
753
|
+
if (interval) {
|
|
754
|
+
const years = Math.trunc(months / 12);
|
|
755
|
+
switch (field) {
|
|
756
|
+
case "year":
|
|
757
|
+
return years;
|
|
758
|
+
case "month":
|
|
759
|
+
return months % 12;
|
|
760
|
+
case "day":
|
|
761
|
+
return days;
|
|
762
|
+
case "quarter":
|
|
763
|
+
return Math.trunc(months % 12 / 3) + 1;
|
|
764
|
+
case "decade":
|
|
765
|
+
return Math.trunc(years / 10);
|
|
766
|
+
case "century":
|
|
767
|
+
return Math.trunc(years / 100);
|
|
768
|
+
case "millennium":
|
|
769
|
+
return Math.trunc(years / 1e3);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
throw new TypeError(`Cannot extract ${field} from ${interval ? "INTERVAL" : "TIME"}`);
|
|
773
|
+
}
|
|
774
|
+
function structuredDomainCompare(left, right) {
|
|
775
|
+
if (typeof left !== "string" || typeof right !== "string")
|
|
776
|
+
return void 0;
|
|
777
|
+
const array = left.startsWith(ARRAY_VALUE) && right.startsWith(ARRAY_VALUE);
|
|
778
|
+
const jsonb = left.startsWith(JSONB_VALUE) && right.startsWith(JSONB_VALUE);
|
|
779
|
+
if (!array && !jsonb)
|
|
780
|
+
return void 0;
|
|
781
|
+
if (left === right)
|
|
782
|
+
return 0;
|
|
783
|
+
const prefix = array ? ARRAY_VALUE : JSONB_VALUE;
|
|
784
|
+
const a = JSON.parse(left.slice(prefix.length));
|
|
785
|
+
const b = JSON.parse(right.slice(prefix.length));
|
|
786
|
+
if (array && Array.isArray(a) && Array.isArray(b)) {
|
|
787
|
+
for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
|
|
788
|
+
const x = a[index];
|
|
789
|
+
const y = b[index];
|
|
790
|
+
const order = x === null || y === null ? x === y ? 0 : x === null ? 1 : -1 : compareJsonStructure(x, y);
|
|
791
|
+
if (order !== 0)
|
|
792
|
+
return order;
|
|
793
|
+
}
|
|
794
|
+
return a.length - b.length;
|
|
795
|
+
}
|
|
796
|
+
if (Array.isArray(a) && a.length === 0)
|
|
797
|
+
return Array.isArray(b) && b.length === 0 ? 0 : -1;
|
|
798
|
+
if (Array.isArray(b) && b.length === 0)
|
|
799
|
+
return 1;
|
|
800
|
+
return compareJsonStructure(a, b);
|
|
801
|
+
}
|
|
802
|
+
function compareJsonStructure(left, right) {
|
|
803
|
+
if (left === right)
|
|
804
|
+
return 0;
|
|
805
|
+
const rank = (value) => value === null ? 0 : typeof value === "string" ? 1 : typeof value === "number" ? 2 : typeof value === "boolean" ? 3 : Array.isArray(value) ? 4 : 5;
|
|
806
|
+
const difference = rank(left) - rank(right);
|
|
807
|
+
if (difference !== 0)
|
|
808
|
+
return difference;
|
|
809
|
+
if (typeof left === "number" && typeof right === "number")
|
|
810
|
+
return left - right;
|
|
811
|
+
if (typeof left === "string" && typeof right === "string")
|
|
812
|
+
return left < right ? -1 : 1;
|
|
813
|
+
if (typeof left === "boolean" && typeof right === "boolean")
|
|
814
|
+
return Number(left) - Number(right);
|
|
815
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
816
|
+
if (left.length !== right.length)
|
|
817
|
+
return left.length - right.length;
|
|
818
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
819
|
+
const order = compareJsonStructure(left[index], right[index]);
|
|
820
|
+
if (order !== 0)
|
|
821
|
+
return order;
|
|
822
|
+
}
|
|
823
|
+
return 0;
|
|
824
|
+
}
|
|
825
|
+
const a = left;
|
|
826
|
+
const b = right;
|
|
827
|
+
const encoder = new TextEncoder();
|
|
828
|
+
const keyOrder = (x, y) => encoder.encode(x).length - encoder.encode(y).length || (x === y ? 0 : x < y ? -1 : 1);
|
|
829
|
+
const keysA = Object.keys(a).sort(keyOrder);
|
|
830
|
+
const keysB = Object.keys(b).sort(keyOrder);
|
|
831
|
+
if (keysA.length !== keysB.length)
|
|
832
|
+
return keysA.length - keysB.length;
|
|
833
|
+
for (let index = 0; index < keysA.length; index += 1) {
|
|
834
|
+
const x = keysA[index] ?? "";
|
|
835
|
+
const y = keysB[index] ?? "";
|
|
836
|
+
if (x !== y)
|
|
837
|
+
return x < y ? -1 : 1;
|
|
838
|
+
const order = compareJsonStructure(a[x], b[y]);
|
|
839
|
+
if (order !== 0)
|
|
840
|
+
return order;
|
|
841
|
+
}
|
|
842
|
+
return 0;
|
|
843
|
+
}
|
|
725
844
|
export {
|
|
726
845
|
arrayDomainValue,
|
|
727
846
|
boundedJsonText,
|
|
@@ -751,6 +870,8 @@ export {
|
|
|
751
870
|
normalizeSqlDomainValue,
|
|
752
871
|
preservedJsonDomainValue,
|
|
753
872
|
protectedSqlTextValue,
|
|
873
|
+
structuredDomainCompare,
|
|
874
|
+
temporalDomainPart,
|
|
754
875
|
timeDomainValue,
|
|
755
876
|
uuidDomainValue
|
|
756
877
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dateMilliseconds } from "../date-value.js";
|
|
2
2
|
import { assertWellFormedString } from "../block-format/unicode.js";
|
|
3
3
|
import { MAX_SQL_NESTING_DEPTH, MAX_SQL_PATTERN_CHARACTERS, MAX_SQL_PATTERN_MATCH_STEPS } from "./cache-limits.js";
|
|
4
|
-
import { collatedDomainCompare, enumDomainCompare, exactNumericCompare, externalSqlDomainValue, externalSqlTextValue, intervalDomainCompare, isDateDomainValue, isSqlDomainValue } from "./sql-domains.js";
|
|
4
|
+
import { structuredDomainCompare, collatedDomainCompare, enumDomainCompare, exactNumericCompare, externalSqlDomainValue, externalSqlTextValue, intervalDomainCompare, isDateDomainValue, isSqlDomainValue } from "./sql-domains.js";
|
|
5
5
|
const SQL_TIMESTAMP_TEXT = /^(\d{4}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?))?(Z|[+-]\d{2}:?\d{2})?$/;
|
|
6
6
|
function parseSqlTimestampText(text) {
|
|
7
7
|
const match = SQL_TIMESTAMP_TEXT.exec(text.trim());
|
|
@@ -33,9 +33,9 @@ function readUntypedText(type, text) {
|
|
|
33
33
|
}
|
|
34
34
|
if (type === "boolean") {
|
|
35
35
|
const lowered = text.trim().toLowerCase();
|
|
36
|
-
if (
|
|
36
|
+
if (/^(?:t(?:r(?:u(?:e)?)?)?|y(?:e(?:s)?)?|on|1)$/.test(lowered))
|
|
37
37
|
return true;
|
|
38
|
-
if (
|
|
38
|
+
if (/^(?:f(?:a(?:l(?:s(?:e)?)?)?)?|n(?:o)?|of(?:f)?|0)$/.test(lowered))
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
41
|
return text;
|
|
@@ -72,6 +72,9 @@ function compareSqlValues(left, right) {
|
|
|
72
72
|
}
|
|
73
73
|
return compareSqlStrings(plainLeft, plainRight);
|
|
74
74
|
}
|
|
75
|
+
const structured = structuredDomainCompare(left, right);
|
|
76
|
+
if (structured !== void 0)
|
|
77
|
+
return structured;
|
|
75
78
|
const collated = collatedDomainCompare(left, right);
|
|
76
79
|
if (collated !== void 0)
|
|
77
80
|
return collated;
|
|
@@ -347,7 +350,7 @@ function generalLikeMatcher(tokens, caseInsensitive) {
|
|
|
347
350
|
}
|
|
348
351
|
function compileLikePattern(pattern, caseInsensitive = false, escape) {
|
|
349
352
|
assertBoundedPattern(pattern, "LIKE pattern");
|
|
350
|
-
const exactEscape = escape === void 0 ? void 0 : exactSingleCharacter(escape, "LIKE escape");
|
|
353
|
+
const exactEscape = escape === void 0 || escape === "" ? void 0 : exactSingleCharacter(escape, "LIKE escape");
|
|
351
354
|
const key = JSON.stringify([caseInsensitive, exactEscape ?? null, pattern]);
|
|
352
355
|
const cached = cachedPattern(likeCache, key);
|
|
353
356
|
if (cached !== void 0)
|
|
@@ -6,7 +6,7 @@ function reconcileRows(previous, next, retained) {
|
|
|
6
6
|
for (let index = 0; index < next.length; index += 1) {
|
|
7
7
|
const row = next[index];
|
|
8
8
|
const was = provenance?.[index] ?? -1;
|
|
9
|
-
if (was >= 0 && was < previous.length) {
|
|
9
|
+
if (was >= 0 && was < previous.length && sameLiveValue(previous[was], row)) {
|
|
10
10
|
rows[index] = previous[was];
|
|
11
11
|
if (was !== index)
|
|
12
12
|
changed = true;
|
|
@@ -38,6 +38,7 @@ class LiveQuery {
|
|
|
38
38
|
#executionAbort;
|
|
39
39
|
#invalidationSequence = 0;
|
|
40
40
|
#closed = false;
|
|
41
|
+
#refreshLeases = 0;
|
|
41
42
|
constructor(backend, source, onClose) {
|
|
42
43
|
this.#backend = backend;
|
|
43
44
|
this.#source = source;
|
|
@@ -57,32 +58,39 @@ class LiveQuery {
|
|
|
57
58
|
return;
|
|
58
59
|
subscribed = false;
|
|
59
60
|
this.#listeners.delete(registered);
|
|
60
|
-
if (this.#listeners.size === 0)
|
|
61
|
+
if (this.#listeners.size === 0 && this.#refreshLeases === 0)
|
|
61
62
|
this.#stopObservation();
|
|
62
63
|
};
|
|
63
64
|
};
|
|
64
65
|
async refresh() {
|
|
65
66
|
if (this.#closed)
|
|
66
67
|
throw new Error("Live query is closed");
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (this.#
|
|
79
|
-
const
|
|
80
|
-
if (this.#
|
|
81
|
-
this.#
|
|
82
|
-
|
|
83
|
-
|
|
68
|
+
this.#refreshLeases += 1;
|
|
69
|
+
try {
|
|
70
|
+
const sequence = this.#invalidationSequence;
|
|
71
|
+
if (this.#listeners.size > 0 || this.#decodes()) {
|
|
72
|
+
if (this.#subscription === void 0)
|
|
73
|
+
this.#startObservation();
|
|
74
|
+
const opening = this.#subscription;
|
|
75
|
+
if (opening !== void 0)
|
|
76
|
+
await opening.catch(() => void 0);
|
|
77
|
+
await this.#backend.refresh();
|
|
78
|
+
}
|
|
79
|
+
if (this.#invalidationSequence === sequence) {
|
|
80
|
+
const version = this.#snapshot.status === "loading" ? null : this.#snapshot.version;
|
|
81
|
+
if (this.#decodes()) {
|
|
82
|
+
const last = this.#lastDelivered;
|
|
83
|
+
if (this.#snapshot.status === "error" && last !== void 0)
|
|
84
|
+
this.#schedule(last);
|
|
85
|
+
} else
|
|
86
|
+
this.#schedule({ manifestVersion: version, catalogEpoch: 0, initial: false });
|
|
87
|
+
}
|
|
88
|
+
await this.#waitForIdle();
|
|
89
|
+
} finally {
|
|
90
|
+
this.#refreshLeases -= 1;
|
|
91
|
+
if (this.#refreshLeases === 0 && this.#listeners.size === 0)
|
|
92
|
+
this.#stopObservation();
|
|
84
93
|
}
|
|
85
|
-
await this.#waitForIdle();
|
|
86
94
|
}
|
|
87
95
|
#decodes() {
|
|
88
96
|
return this.#source.decode !== void 0 && this.#backend.subscribe !== void 0;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { QueryResult, WindowSpec } from "../plan/model.js";
|
|
2
|
+
import { QueryMemoryContext } from "./memory.js";
|
|
3
|
+
interface WindowOptions {
|
|
4
|
+
copyRows?: boolean;
|
|
5
|
+
memoryContext?: QueryMemoryContext;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
/** Synchronous executor used by standalone queries; shares every kernel with asynchronous reads. */
|
|
9
|
+
export declare function applyWindowFunctions(result: QueryResult, windows: readonly WindowSpec[], options?: WindowOptions): QueryResult;
|
|
10
|
+
/** Cooperatively runs window passes so cancellation can arrive while a large partition is active. */
|
|
11
|
+
export declare function applyWindowFunctionsAsync(result: QueryResult, windows: readonly WindowSpec[], options?: WindowOptions): Promise<QueryResult>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { QueryMemoryContext } from "./memory.js";
|
|
2
|
+
import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
|
|
3
|
+
import { compareSqlValues } from "./sql-semantics.js";
|
|
4
|
+
import { exactNumericBinary, exactNumericValue, isExactNumeric } from "./sql-domains.js";
|
|
5
|
+
import { throwIfAborted } from "./cancellation.js";
|
|
6
|
+
class FrameAggregate {
|
|
7
|
+
#width;
|
|
8
|
+
#values;
|
|
9
|
+
#counts;
|
|
10
|
+
#window;
|
|
11
|
+
#exact;
|
|
12
|
+
#prefixEnd = 0;
|
|
13
|
+
#prefixTotal = null;
|
|
14
|
+
#prefixCount = 0;
|
|
15
|
+
constructor(values, window, memory, prefix) {
|
|
16
|
+
this.#window = window;
|
|
17
|
+
this.#exact = values.some(isExactNumeric);
|
|
18
|
+
if (prefix) {
|
|
19
|
+
this.#width = 0;
|
|
20
|
+
this.#values = values;
|
|
21
|
+
this.#counts = new Uint32Array(0);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const width = 2 ** Math.ceil(Math.log2(Math.max(1, values.length)));
|
|
25
|
+
this.#width = width;
|
|
26
|
+
memory.tally(width * 2 * 20, "Window aggregate tree");
|
|
27
|
+
this.#values = new Array(width * 2).fill(null);
|
|
28
|
+
this.#counts = new Uint32Array(width * 2);
|
|
29
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
30
|
+
const value = values[index] ?? null;
|
|
31
|
+
this.#values[width + index] = value;
|
|
32
|
+
this.#counts[width + index] = value === null ? 0 : 1;
|
|
33
|
+
}
|
|
34
|
+
for (let index = width - 1; index > 0; index -= 1) {
|
|
35
|
+
this.#counts[index] = (this.#counts[index * 2] ?? 0) + (this.#counts[index * 2 + 1] ?? 0);
|
|
36
|
+
this.#values[index] = this.#combine(this.#values[index * 2] ?? null, this.#values[index * 2 + 1] ?? null);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
#combine(left, right) {
|
|
40
|
+
if (left === null)
|
|
41
|
+
return right;
|
|
42
|
+
if (right === null)
|
|
43
|
+
return left;
|
|
44
|
+
if (this.#window.name === "COUNT")
|
|
45
|
+
return null;
|
|
46
|
+
if (this.#window.name === "MIN")
|
|
47
|
+
return compareSqlValues(left, right) <= 0 ? left : right;
|
|
48
|
+
if (this.#window.name === "MAX")
|
|
49
|
+
return compareSqlValues(left, right) >= 0 ? left : right;
|
|
50
|
+
if (this.#exact) {
|
|
51
|
+
const total = exactNumericBinary("+", left, right);
|
|
52
|
+
if (total === void 0)
|
|
53
|
+
throw new TypeError("Window SUM requires numeric values");
|
|
54
|
+
return total;
|
|
55
|
+
}
|
|
56
|
+
if (typeof left !== "number" || typeof right !== "number")
|
|
57
|
+
throw new TypeError("Window SUM requires numeric values");
|
|
58
|
+
return left + right;
|
|
59
|
+
}
|
|
60
|
+
prefixValue(end, scale) {
|
|
61
|
+
while (this.#prefixEnd < end) {
|
|
62
|
+
const value = this.#values[this.#prefixEnd] ?? null;
|
|
63
|
+
this.#prefixEnd += 1;
|
|
64
|
+
if (value === null)
|
|
65
|
+
continue;
|
|
66
|
+
this.#prefixCount += 1;
|
|
67
|
+
this.#prefixTotal = this.#combine(this.#prefixTotal, value);
|
|
68
|
+
}
|
|
69
|
+
return this.#finish(this.#prefixTotal, this.#prefixCount, scale);
|
|
70
|
+
}
|
|
71
|
+
value(ranges, scale) {
|
|
72
|
+
let total = null;
|
|
73
|
+
let count = 0;
|
|
74
|
+
for (const [from, to] of ranges) {
|
|
75
|
+
let low = from + this.#width;
|
|
76
|
+
let high = to + this.#width;
|
|
77
|
+
let left = null;
|
|
78
|
+
let right = null;
|
|
79
|
+
while (low < high) {
|
|
80
|
+
if (low % 2 === 1) {
|
|
81
|
+
count += this.#counts[low] ?? 0;
|
|
82
|
+
left = this.#combine(left, this.#values[low] ?? null);
|
|
83
|
+
low += 1;
|
|
84
|
+
}
|
|
85
|
+
if (high % 2 === 1) {
|
|
86
|
+
high -= 1;
|
|
87
|
+
count += this.#counts[high] ?? 0;
|
|
88
|
+
right = this.#combine(this.#values[high] ?? null, right);
|
|
89
|
+
}
|
|
90
|
+
low = Math.floor(low / 2);
|
|
91
|
+
high = Math.floor(high / 2);
|
|
92
|
+
}
|
|
93
|
+
total = this.#combine(total, this.#combine(left, right));
|
|
94
|
+
}
|
|
95
|
+
return this.#finish(total, count, scale);
|
|
96
|
+
}
|
|
97
|
+
#finish(total, count, scale) {
|
|
98
|
+
if (this.#window.name === "COUNT")
|
|
99
|
+
return count;
|
|
100
|
+
if (count === 0)
|
|
101
|
+
return null;
|
|
102
|
+
if (this.#window.name !== "AVG")
|
|
103
|
+
return total;
|
|
104
|
+
if (this.#exact)
|
|
105
|
+
return exactNumericBinary("/", total ?? exactNumericValue(0), count, scale) ?? null;
|
|
106
|
+
if (typeof total !== "number")
|
|
107
|
+
throw new TypeError("Window AVG requires numeric values");
|
|
108
|
+
return total / count;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function frameRanges(partition, window, position) {
|
|
112
|
+
const size = partition.indexes.length;
|
|
113
|
+
const frame = window.frame ?? {
|
|
114
|
+
unit: "range",
|
|
115
|
+
start: { kind: "unbounded-preceding" },
|
|
116
|
+
end: { kind: window.orderAliases.length === 0 ? "unbounded-following" : "current-row" }
|
|
117
|
+
};
|
|
118
|
+
const groupEdge = (group) => group < 0 ? 0 : partition.groupStarts[group] ?? size;
|
|
119
|
+
const bound = (edge, start) => {
|
|
120
|
+
switch (edge.kind) {
|
|
121
|
+
case "unbounded-preceding":
|
|
122
|
+
return 0;
|
|
123
|
+
case "unbounded-following":
|
|
124
|
+
return size;
|
|
125
|
+
case "current-row":
|
|
126
|
+
return frame.unit === "rows" ? position + (start ? 0 : 1) : start ? partition.peerStart[position] ?? 0 : partition.peerEnd[position] ?? size;
|
|
127
|
+
case "preceding":
|
|
128
|
+
case "following": {
|
|
129
|
+
const delta = (edge.offset ?? 0) * (edge.kind === "preceding" ? -1 : 1);
|
|
130
|
+
if (frame.unit === "range") {
|
|
131
|
+
const current = partition.orderValues[position] ?? null;
|
|
132
|
+
if (current === null)
|
|
133
|
+
return start ? partition.peerStart[position] ?? 0 : partition.peerEnd[position] ?? size;
|
|
134
|
+
if (typeof current !== "number" && !isExactNumeric(current))
|
|
135
|
+
throw new TypeError("Offset RANGE frames require numeric ORDER BY values");
|
|
136
|
+
const order = window.orderAliases[0];
|
|
137
|
+
if (window.orderAliases.length !== 1 || order === void 0)
|
|
138
|
+
throw new TypeError("Offset RANGE frames require exactly one ORDER BY expression");
|
|
139
|
+
const descending = order.direction === "desc";
|
|
140
|
+
const distance = descending ? -delta : delta;
|
|
141
|
+
const target = isExactNumeric(current) ? exactNumericBinary("+", current, distance) ?? null : current + distance;
|
|
142
|
+
let low2 = 0;
|
|
143
|
+
let high2 = size;
|
|
144
|
+
while (low2 < high2) {
|
|
145
|
+
const middle = Math.floor((low2 + high2) / 2);
|
|
146
|
+
const value = partition.orderValues[middle] ?? null;
|
|
147
|
+
const comparison = value === null ? (order.nulls ?? (descending ? "first" : "last")) === "first" ? -1 : 1 : compareSqlValues(value, target) * (descending ? -1 : 1);
|
|
148
|
+
if (comparison < 0 || !start && comparison === 0)
|
|
149
|
+
low2 = middle + 1;
|
|
150
|
+
else
|
|
151
|
+
high2 = middle;
|
|
152
|
+
}
|
|
153
|
+
return low2;
|
|
154
|
+
}
|
|
155
|
+
return frame.unit === "groups" ? groupEdge((partition.groupOrdinal[position] ?? 0) + delta + (start ? 0 : 1)) : position + delta + (start ? 0 : 1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const low = Math.max(0, Math.min(size, bound(frame.start, true)));
|
|
160
|
+
const high = Math.max(0, Math.min(size, bound(frame.end, false)));
|
|
161
|
+
if (high <= low)
|
|
162
|
+
return [];
|
|
163
|
+
if (frame.exclude === void 0 || frame.exclude === "no-others")
|
|
164
|
+
return [[low, high]];
|
|
165
|
+
const from = frame.exclude === "current-row" ? position : partition.peerStart[position] ?? position;
|
|
166
|
+
const to = frame.exclude === "current-row" ? position + 1 : partition.peerEnd[position] ?? position + 1;
|
|
167
|
+
const ranges = [];
|
|
168
|
+
if (low < Math.min(high, from))
|
|
169
|
+
ranges.push([low, Math.min(high, from)]);
|
|
170
|
+
if (frame.exclude === "ties" && position >= low && position < high)
|
|
171
|
+
ranges.push([position, position + 1]);
|
|
172
|
+
if (Math.max(low, to) < high)
|
|
173
|
+
ranges.push([Math.max(low, to), high]);
|
|
174
|
+
return ranges;
|
|
175
|
+
}
|
|
176
|
+
function* applyPartition(rows, partition, window, result, memory) {
|
|
177
|
+
const { indexes, peerStart, peerEnd, groupOrdinal } = partition;
|
|
178
|
+
const size = indexes.length;
|
|
179
|
+
memory.tally(size * 8, "Window argument values");
|
|
180
|
+
const values = Array.from(indexes, (index) => window.argumentAlias === void 0 ? 1 : rows[index]?.[window.argumentAlias] ?? null);
|
|
181
|
+
const aggregates = /* @__PURE__ */ new Set(["SUM", "AVG", "COUNT", "MIN", "MAX"]);
|
|
182
|
+
const frame = window.frame;
|
|
183
|
+
const prefix = frame === void 0 || (frame.exclude === void 0 || frame.exclude === "no-others") && frame.start.kind === "unbounded-preceding" && (frame.end.kind === "current-row" || frame.end.kind === "unbounded-following");
|
|
184
|
+
const whole = frame?.end.kind === "unbounded-following" || frame === void 0 && window.orderAliases.length === 0;
|
|
185
|
+
const aggregate = aggregates.has(window.name) ? new FrameAggregate(values, window, memory, prefix) : void 0;
|
|
186
|
+
const domain = result.columnDomains[result.columns.indexOf(window.argumentAlias ?? "")];
|
|
187
|
+
for (let position = 0; position < size; position += 1) {
|
|
188
|
+
if (position % 2048 === 0)
|
|
189
|
+
yield;
|
|
190
|
+
let value;
|
|
191
|
+
switch (window.name) {
|
|
192
|
+
case "ROW_NUMBER":
|
|
193
|
+
value = position + 1;
|
|
194
|
+
break;
|
|
195
|
+
case "RANK":
|
|
196
|
+
value = (peerStart[position] ?? 0) + 1;
|
|
197
|
+
break;
|
|
198
|
+
case "DENSE_RANK":
|
|
199
|
+
value = (groupOrdinal[position] ?? 0) + 1;
|
|
200
|
+
break;
|
|
201
|
+
case "PERCENT_RANK":
|
|
202
|
+
value = size === 1 ? 0 : (peerStart[position] ?? 0) / (size - 1);
|
|
203
|
+
break;
|
|
204
|
+
case "CUME_DIST":
|
|
205
|
+
value = (peerEnd[position] ?? size) / size;
|
|
206
|
+
break;
|
|
207
|
+
case "NTILE": {
|
|
208
|
+
const buckets = window.offset ?? 1;
|
|
209
|
+
const width = Math.floor(size / buckets);
|
|
210
|
+
const extra = size % buckets;
|
|
211
|
+
const larger = (width + 1) * extra;
|
|
212
|
+
value = position < larger ? Math.floor(position / (width + 1)) + 1 : extra + Math.floor((position - larger) / width) + 1;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case "LAG":
|
|
216
|
+
case "LEAD": {
|
|
217
|
+
const target = position + (window.offset ?? 1) * (window.name === "LAG" ? -1 : 1);
|
|
218
|
+
value = target < 0 || target >= size ? window.fallback ?? null : values[target] ?? null;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
default: {
|
|
222
|
+
if (aggregate !== void 0 && prefix) {
|
|
223
|
+
const end = whole ? size : frame?.unit === "rows" ? position + 1 : peerEnd[position] ?? size;
|
|
224
|
+
value = aggregate.prefixValue(end, domain?.kind === "numeric" ? domain.scale : void 0);
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
const ranges = frameRanges(partition, window, position);
|
|
228
|
+
if (aggregate !== void 0)
|
|
229
|
+
value = aggregate.value(ranges, domain?.kind === "numeric" ? domain.scale : void 0);
|
|
230
|
+
else {
|
|
231
|
+
let remaining = window.name === "NTH_VALUE" ? window.offset ?? 1 : 1;
|
|
232
|
+
value = null;
|
|
233
|
+
if (window.name === "LAST_VALUE") {
|
|
234
|
+
const last = ranges.at(-1);
|
|
235
|
+
value = last === void 0 ? null : values[last[1] - 1] ?? null;
|
|
236
|
+
} else {
|
|
237
|
+
for (const [from, to] of ranges) {
|
|
238
|
+
if (remaining <= to - from) {
|
|
239
|
+
value = values[from + remaining - 1] ?? null;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
remaining -= to - from;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const row = rows[indexes[position] ?? -1];
|
|
249
|
+
if (row !== void 0)
|
|
250
|
+
Object.defineProperty(row, window.alias, {
|
|
251
|
+
value,
|
|
252
|
+
enumerable: true,
|
|
253
|
+
writable: true,
|
|
254
|
+
configurable: true
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function* windowSteps(result, windows, options) {
|
|
259
|
+
const memory = options.memoryContext ?? new QueryMemoryContext();
|
|
260
|
+
const owned = options.memoryContext === void 0;
|
|
261
|
+
const rows = [];
|
|
262
|
+
try {
|
|
263
|
+
memory.tally(result.rows.length * (24 + windows.length * 16 + (options.copyRows === false ? 0 : 48 + result.columns.length * 16)), "Window result rows");
|
|
264
|
+
for (let index = 0; index < result.rows.length; index += 1) {
|
|
265
|
+
if (index % 2048 === 0)
|
|
266
|
+
yield;
|
|
267
|
+
const row = result.rows[index] ?? {};
|
|
268
|
+
rows.push(options.copyRows === false ? row : { ...row });
|
|
269
|
+
}
|
|
270
|
+
const groups = /* @__PURE__ */ new Map();
|
|
271
|
+
for (const window of windows) {
|
|
272
|
+
const key = JSON.stringify([window.partitionAliases, window.orderAliases]);
|
|
273
|
+
const group = groups.get(key);
|
|
274
|
+
if (group === void 0)
|
|
275
|
+
groups.set(key, [window]);
|
|
276
|
+
else
|
|
277
|
+
group.push(window);
|
|
278
|
+
}
|
|
279
|
+
for (const group of groups.values()) {
|
|
280
|
+
const window = group[0];
|
|
281
|
+
if (window === void 0)
|
|
282
|
+
continue;
|
|
283
|
+
const work = memory.createChild();
|
|
284
|
+
try {
|
|
285
|
+
const aliases = [
|
|
286
|
+
...window.partitionAliases,
|
|
287
|
+
...window.orderAliases.map(({ alias }) => alias)
|
|
288
|
+
];
|
|
289
|
+
work.tally(rows.length * (48 + aliases.length * 40), "Window sort and partition buffers");
|
|
290
|
+
const columns = aliases.map((alias) => buildSortKeyColumn(rows.length, (index) => rows[index]?.[alias] ?? null));
|
|
291
|
+
yield;
|
|
292
|
+
const indexes = sortKeyIndexes(rows.length, columns.map((column, index) => {
|
|
293
|
+
const order = window.orderAliases[index - window.partitionAliases.length];
|
|
294
|
+
return { column, descending: order?.direction === "desc", nulls: order?.nulls };
|
|
295
|
+
}));
|
|
296
|
+
const partitions = columns.slice(0, window.partitionAliases.length);
|
|
297
|
+
const orders = columns.slice(window.partitionAliases.length);
|
|
298
|
+
let start = 0;
|
|
299
|
+
while (start < indexes.length) {
|
|
300
|
+
let end = start + 1;
|
|
301
|
+
while (end < indexes.length && partitions.every((column) => column.compare(indexes[start] ?? 0, indexes[end] ?? 0) === 0)) {
|
|
302
|
+
if (end % 2048 === 0)
|
|
303
|
+
yield;
|
|
304
|
+
end += 1;
|
|
305
|
+
}
|
|
306
|
+
const size = end - start;
|
|
307
|
+
const partition = {
|
|
308
|
+
indexes: indexes.subarray(start, end),
|
|
309
|
+
peerStart: new Int32Array(size),
|
|
310
|
+
peerEnd: new Int32Array(size),
|
|
311
|
+
groupOrdinal: new Int32Array(size),
|
|
312
|
+
groupStarts: [],
|
|
313
|
+
orderValues: Array.from(indexes.subarray(start, end), (index) => rows[index]?.[window.orderAliases[0]?.alias ?? ""] ?? null)
|
|
314
|
+
};
|
|
315
|
+
let begin = 0;
|
|
316
|
+
for (let position = 1; position <= size; position += 1) {
|
|
317
|
+
if (position % 2048 === 0)
|
|
318
|
+
yield;
|
|
319
|
+
if (position === size || orders.some((column) => column.compare(indexes[start + begin] ?? 0, indexes[start + position] ?? 0) !== 0)) {
|
|
320
|
+
const ordinal = partition.groupStarts.length;
|
|
321
|
+
partition.groupStarts.push(begin);
|
|
322
|
+
partition.peerStart.fill(begin, begin, position);
|
|
323
|
+
partition.peerEnd.fill(position, begin, position);
|
|
324
|
+
partition.groupOrdinal.fill(ordinal, begin, position);
|
|
325
|
+
begin = position;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for (const member of group) {
|
|
329
|
+
const frameMemory = work.createChild();
|
|
330
|
+
try {
|
|
331
|
+
yield* applyPartition(rows, partition, member, result, frameMemory);
|
|
332
|
+
} finally {
|
|
333
|
+
frameMemory.close();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
start = end;
|
|
337
|
+
}
|
|
338
|
+
} finally {
|
|
339
|
+
work.close();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
columns: [...result.columns, ...windows.map(({ alias }) => alias)],
|
|
344
|
+
columnDomains: [...result.columnDomains, ...windows.map(() => null)],
|
|
345
|
+
rows
|
|
346
|
+
};
|
|
347
|
+
} finally {
|
|
348
|
+
if (owned)
|
|
349
|
+
memory.close();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function applyWindowFunctions(result, windows, options = {}) {
|
|
353
|
+
const steps = windowSteps(result, windows, options);
|
|
354
|
+
try {
|
|
355
|
+
for (; ; ) {
|
|
356
|
+
throwIfAborted(options.signal);
|
|
357
|
+
const step = steps.next();
|
|
358
|
+
if (step.done)
|
|
359
|
+
return step.value;
|
|
360
|
+
}
|
|
361
|
+
} finally {
|
|
362
|
+
steps.return({ columns: [], columnDomains: [], rows: [] });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function applyWindowFunctionsAsync(result, windows, options = {}) {
|
|
366
|
+
const steps = windowSteps(result, windows, options);
|
|
367
|
+
let yieldedAt = performance.now();
|
|
368
|
+
try {
|
|
369
|
+
for (; ; ) {
|
|
370
|
+
throwIfAborted(options.signal);
|
|
371
|
+
const step = steps.next();
|
|
372
|
+
if (step.done)
|
|
373
|
+
return step.value;
|
|
374
|
+
if (performance.now() - yieldedAt >= 8) {
|
|
375
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
376
|
+
yieldedAt = performance.now();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} finally {
|
|
380
|
+
steps.return({ columns: [], columnDomains: [], rows: [] });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
export {
|
|
384
|
+
applyWindowFunctions,
|
|
385
|
+
applyWindowFunctionsAsync
|
|
386
|
+
};
|
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" | "TO_JSON" | "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" | "MINNOW_ARRAY_AT" | "MINNOW_ARRAY_FROM_JSON" | "MINNOW_ARRAY_ELEMENT" | "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. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minnowdb/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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",
|