@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.
@@ -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 (lowered === "t" || lowered === "true" || lowered === "1")
36
+ if (/^(?:t(?:r(?:u(?:e)?)?)?|y(?:e(?:s)?)?|on|1)$/.test(lowered))
37
37
  return true;
38
- if (lowered === "f" || lowered === "false" || lowered === "0")
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
- const sequence = this.#invalidationSequence;
68
- if (this.#listeners.size > 0) {
69
- if (this.#subscription === void 0)
70
- this.#startObservation();
71
- const opening = this.#subscription;
72
- if (opening !== void 0)
73
- await opening.catch(() => void 0);
74
- await this.#backend.refresh();
75
- }
76
- if (this.#invalidationSequence === sequence) {
77
- const version = this.#snapshot.status === "loading" ? null : this.#snapshot.version;
78
- if (this.#decodes()) {
79
- const last = this.#lastDelivered;
80
- if (this.#snapshot.status === "error" && last !== void 0)
81
- this.#schedule(last);
82
- } else
83
- this.#schedule({ manifestVersion: version, catalogEpoch: 0, initial: false });
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
+ };
@@ -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.7.10",
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",