@minnowdb/core 0.7.8 → 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)
@@ -1,7 +1,10 @@
1
- import type { LiveQueryInput, LiveQueryObserveOptions } from "./live.js";
1
+ import type { LiveQueryInput, LiveQueryObserveOptions, LiveQuerySubscribeOptions } from "./live.js";
2
+ import type { QueryResult } from "./query.js";
2
3
  /** The structural live-query surface shared by MinnowDatabase and its worker client. */
3
4
  export interface LiveQueryBackend {
4
5
  observe(query: LiveQueryInput, options: LiveQueryObserveOptions): Promise<LiveQuerySubscriptionLike>;
6
+ /** Result delivery, used when the source can decode the engine's result itself. */
7
+ subscribe?(query: LiveQueryInput, options: LiveQuerySubscribeOptions): Promise<LiveQuerySubscriptionLike>;
5
8
  refresh(): Promise<void>;
6
9
  close(): void | Promise<void>;
7
10
  }
@@ -22,6 +25,14 @@ export interface LiveQueryDriver {
22
25
  export interface LiveQuerySource<out TRow> {
23
26
  readonly query: LiveQueryInput;
24
27
  execute(signal?: AbortSignal): Promise<readonly TRow[]>;
28
+ /**
29
+ * Turns a result the engine delivered into the adapter's rows. With it, the query subscribes
30
+ * for results rather than invalidations: the engine executes or patches the statement where
31
+ * the data is, compares, and hands over a changed result once — over a worker channel, as
32
+ * one columnar transfer — and `execute` is never called after the statement is registered.
33
+ * Without it, an invalidation is followed by `execute`, which the engine's memo serves.
34
+ */
35
+ decode?(result: QueryResult): readonly TRow[] | Promise<readonly TRow[]>;
25
36
  }
26
37
  export type LiveSnapshot<TRow> = {
27
38
  readonly status: "loading";
@@ -1,6 +1,26 @@
1
1
  import { sameLiveValue } from "./live-equal.js";
2
- function immutableRows(rows) {
3
- return Object.freeze([...rows]);
2
+ function reconcileRows(previous, next, retained) {
3
+ const rows = new Array(next.length);
4
+ let changed = previous.length !== next.length;
5
+ const provenance = retained?.length === next.length ? retained : void 0;
6
+ for (let index = 0; index < next.length; index += 1) {
7
+ const row = next[index];
8
+ const was = provenance?.[index] ?? -1;
9
+ if (was >= 0 && was < previous.length && sameLiveValue(previous[was], row)) {
10
+ rows[index] = previous[was];
11
+ if (was !== index)
12
+ changed = true;
13
+ continue;
14
+ }
15
+ const before = previous[index];
16
+ if (index < previous.length && sameLiveValue(before, row))
17
+ rows[index] = before;
18
+ else {
19
+ rows[index] = row;
20
+ changed = true;
21
+ }
22
+ }
23
+ return changed ? Object.freeze(rows) : void 0;
4
24
  }
5
25
  class LiveQuery {
6
26
  #listeners = /* @__PURE__ */ new Set();
@@ -11,10 +31,14 @@ class LiveQuery {
11
31
  #subscription;
12
32
  #observationGeneration = 0;
13
33
  #queued;
34
+ #lastDelivered;
35
+ #deliveriesReceived = 0;
36
+ #rowsFromDelivery = 0;
14
37
  #execution;
15
38
  #executionAbort;
16
39
  #invalidationSequence = 0;
17
40
  #closed = false;
41
+ #refreshLeases = 0;
18
42
  constructor(backend, source, onClose) {
19
43
  this.#backend = backend;
20
44
  this.#source = source;
@@ -34,27 +58,48 @@ class LiveQuery {
34
58
  return;
35
59
  subscribed = false;
36
60
  this.#listeners.delete(registered);
37
- if (this.#listeners.size === 0)
61
+ if (this.#listeners.size === 0 && this.#refreshLeases === 0)
38
62
  this.#stopObservation();
39
63
  };
40
64
  };
41
65
  async refresh() {
42
66
  if (this.#closed)
43
67
  throw new Error("Live query is closed");
44
- const sequence = this.#invalidationSequence;
45
- if (this.#listeners.size > 0) {
46
- if (this.#subscription === void 0)
47
- this.#startObservation();
48
- const opening = this.#subscription;
49
- if (opening !== void 0)
50
- await opening.catch(() => void 0);
51
- await this.#backend.refresh();
52
- }
53
- if (this.#invalidationSequence === sequence) {
54
- const version = this.#snapshot.status === "loading" ? null : this.#snapshot.version;
55
- 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();
56
93
  }
57
- await this.#waitForIdle();
94
+ }
95
+ #decodes() {
96
+ return this.#source.decode !== void 0 && this.#backend.subscribe !== void 0;
97
+ }
98
+ async #decodeDelivered(result) {
99
+ const source = this.#source;
100
+ if (source.decode === void 0)
101
+ throw new TypeError("Live query source lost its decoder");
102
+ return source.decode(result);
58
103
  }
59
104
  close() {
60
105
  if (this.#closed)
@@ -75,7 +120,36 @@ class LiveQuery {
75
120
  if (this.#subscription !== void 0 || this.#closed)
76
121
  return;
77
122
  const generation = this.#observationGeneration += 1;
123
+ if (this.#source.decode !== void 0 && this.#backend.subscribe !== void 0) {
124
+ const subscription2 = this.#backend.subscribe(this.#source.query, {
125
+ onChange: (result, delivery) => {
126
+ if (generation !== this.#observationGeneration || this.#closed)
127
+ return;
128
+ this.#deliveriesReceived += 1;
129
+ this.#schedule({ result, delivery, sequence: this.#deliveriesReceived });
130
+ },
131
+ onError: (error) => {
132
+ if (generation !== this.#observationGeneration || this.#closed)
133
+ return;
134
+ this.#setError(error, this.#currentVersion());
135
+ },
136
+ onComplete: () => {
137
+ if (generation !== this.#observationGeneration)
138
+ return;
139
+ this.#subscription = void 0;
140
+ }
141
+ });
142
+ this.#subscription = subscription2;
143
+ subscription2.catch((error) => {
144
+ if (generation !== this.#observationGeneration || this.#closed)
145
+ return;
146
+ this.#subscription = void 0;
147
+ this.#setError(error, this.#currentVersion());
148
+ });
149
+ return;
150
+ }
78
151
  const subscription = this.#backend.observe(this.#source.query, {
152
+ suppressUnchanged: true,
79
153
  onInvalidate: (invalidation) => {
80
154
  if (generation !== this.#observationGeneration || this.#closed)
81
155
  return;
@@ -109,9 +183,9 @@ class LiveQuery {
109
183
  void subscription.then((handle) => handle.close()).catch(() => void 0);
110
184
  }
111
185
  }
112
- #schedule(invalidation) {
186
+ #schedule(work) {
113
187
  this.#invalidationSequence += 1;
114
- this.#queued = invalidation;
188
+ this.#queued = work;
115
189
  if (this.#execution !== void 0)
116
190
  return;
117
191
  const execution = this.#drain();
@@ -125,24 +199,35 @@ class LiveQuery {
125
199
  }
126
200
  async #drain() {
127
201
  while (this.#queued !== void 0 && !this.#closed) {
128
- const invalidation = this.#queued;
202
+ const work = this.#queued;
129
203
  this.#queued = void 0;
130
204
  const abort = new AbortController();
131
205
  this.#executionAbort = abort;
206
+ const invalidation = "result" in work ? work.delivery : work;
132
207
  try {
133
- const rows = immutableRows(await this.#source.execute(abort.signal));
208
+ let executed;
209
+ if ("result" in work) {
210
+ this.#lastDelivered = work;
211
+ executed = await this.#decodeDelivered(work.result);
212
+ this.#lastDelivered = void 0;
213
+ } else
214
+ executed = await this.#source.execute(abort.signal);
134
215
  if (this.#executionWasCancelled(abort))
135
216
  continue;
136
217
  if (this.#hasQueuedInvalidation())
137
218
  continue;
138
219
  const previous = this.#snapshot.rows;
139
- if (this.#snapshot.status === "ready" && this.#snapshot.version === invalidation.manifestVersion && sameLiveValue(previous, rows)) {
140
- continue;
141
- }
142
- if (sameLiveValue(previous, rows) && this.#snapshot.status !== "loading") {
220
+ const provenance = "result" in work && this.#rowsFromDelivery === work.sequence - 1 ? work.delivery.retained : void 0;
221
+ const rows = reconcileRows(previous, executed, provenance);
222
+ if ("result" in work)
223
+ this.#rowsFromDelivery = work.sequence;
224
+ if (rows === void 0) {
225
+ if (this.#snapshot.status === "ready" && this.#snapshot.version === invalidation.manifestVersion) {
226
+ continue;
227
+ }
143
228
  this.#snapshot = {
144
229
  status: "ready",
145
- rows: previous,
230
+ rows: this.#snapshot.status === "loading" ? Object.freeze([...previous]) : previous,
146
231
  version: invalidation.manifestVersion
147
232
  };
148
233
  } else {
@@ -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 {};