@minnowdb/core 0.6.0 → 0.6.2

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.
@@ -12,6 +12,34 @@ export const storeNames = [
12
12
  "gc",
13
13
  ];
14
14
  export const MAX_MANIFEST_CHANGED_TABLE_IDS = 1_024;
15
+ /**
16
+ * A deep copy for the plain record shapes this module validates — objects, arrays, and
17
+ * primitives, bigint included. Records cross this boundary on every catalog read and every
18
+ * compaction-job advance, and `structuredClone` here was a sixth of a settle phase's CPU; any
19
+ * value outside the plain shape falls back to `structuredClone` for that value, so the copy
20
+ * stays exact whatever arrives.
21
+ */
22
+ function clonePlainRecord(value) {
23
+ return clonePlainValue(value);
24
+ }
25
+ function clonePlainValue(value) {
26
+ if (typeof value !== "object" || value === null)
27
+ return value;
28
+ if (Array.isArray(value)) {
29
+ const copy = new Array(value.length);
30
+ for (let index = 0; index < value.length; index += 1)
31
+ copy[index] = clonePlainValue(value[index]);
32
+ return copy;
33
+ }
34
+ const prototype = Object.getPrototypeOf(value);
35
+ if (prototype !== Object.prototype && prototype !== null)
36
+ return structuredClone(value);
37
+ const copy = {};
38
+ for (const key of Object.keys(value)) {
39
+ copy[key] = clonePlainValue(value[key]);
40
+ }
41
+ return copy;
42
+ }
15
43
  export function canonicalManifestChangedTableIds(ids) {
16
44
  if (ids.length > MAX_MANIFEST_CHANGED_TABLE_IDS) {
17
45
  throw new RangeError(`Manifest changed-table IDs cannot exceed ${String(MAX_MANIFEST_CHANGED_TABLE_IDS)}`);
@@ -51,7 +79,7 @@ export function validateSqlDomain(domain, context) {
51
79
  }
52
80
  validateEnumValues(domain.values, domain.name);
53
81
  }
54
- return structuredClone(domain);
82
+ return clonePlainRecord(domain);
55
83
  }
56
84
  export const MAX_TABLE_COLUMNS = 1_024;
57
85
  export const MAX_ENUM_VALUES = 4_096;
@@ -1789,7 +1817,7 @@ export function normalizeSegmentRecord(record) {
1789
1817
  if (record.partitionOrdinal === undefined) {
1790
1818
  if (level === 2)
1791
1819
  throw new TypeError("A level-two segment requires a partition ordinal");
1792
- return structuredClone(record);
1820
+ return clonePlainRecord(record);
1793
1821
  }
1794
1822
  const partitionOrdinal = nonNegativeWholeNumber(record.partitionOrdinal, "Segment partition ordinal");
1795
1823
  if (record.level !== 2) {
@@ -1812,7 +1840,7 @@ export function normalizeSegmentRecord(record) {
1812
1840
  record.rowIdEndExclusive !== record.rowIdStart + BigInt(rowCount)) {
1813
1841
  throw new RangeError("A partitioned segment must have a contiguous positive row ID envelope");
1814
1842
  }
1815
- return structuredClone({ ...record, partitionOrdinal });
1843
+ return clonePlainRecord({ ...record, partitionOrdinal });
1816
1844
  }
1817
1845
  // Keyed multi-range partition: a merged full-row base whose live rows keep their original
1818
1846
  // ids, described by positive, sorted, non-overlapping spans that sum to the row count.
@@ -1837,7 +1865,7 @@ export function normalizeSegmentRecord(record) {
1837
1865
  if (spanRows !== rowCount) {
1838
1866
  throw new RangeError("Partitioned segment spans must cover exactly the row count");
1839
1867
  }
1840
- return structuredClone({ ...record, partitionOrdinal });
1868
+ return clonePlainRecord({ ...record, partitionOrdinal });
1841
1869
  }
1842
1870
  export function updateTransactionRecord(record, update) {
1843
1871
  const updated = {
@@ -2144,7 +2172,7 @@ export function normalizeGarbageCollectionJobRecord(record) {
2144
2172
  cursor.transactionIndex !== 0)) {
2145
2173
  throw new TypeError("A planned garbage collection job cannot contain progress");
2146
2174
  }
2147
- return structuredClone(normalized);
2175
+ return clonePlainRecord(normalized);
2148
2176
  }
2149
2177
  export function advanceGarbageCollectionJobRecord(record, accounting) {
2150
2178
  const current = normalizeGarbageCollectionJobRecord(record);
@@ -2355,7 +2383,7 @@ export function normalizeCompactionJobRecord(record) {
2355
2383
  }
2356
2384
  validateCompactionRewrite(normalized);
2357
2385
  validateCompactionJobState(normalized);
2358
- return structuredClone(normalized);
2386
+ return clonePlainRecord(normalized);
2359
2387
  }
2360
2388
  export function updateCompactionJobRecord(record, update) {
2361
2389
  const current = normalizeCompactionJobRecord(record);
@@ -35,6 +35,9 @@ export class LeasedSnapshot extends Snapshot {
35
35
  now;
36
36
  #record;
37
37
  #released = false;
38
+ /** The record's ISO expiry, parsed once: expiry is checked before every batched block read. */
39
+ #expiresAtIso;
40
+ #expiresAtMs = Number.NaN;
38
41
  constructor(store, version, record, now) {
39
42
  super(store, version);
40
43
  this.now = now;
@@ -46,7 +49,12 @@ export class LeasedSnapshot extends Snapshot {
46
49
  return this.#record.id;
47
50
  }
48
51
  get expiresAt() {
49
- return new Date(this.#record.expiresAt);
52
+ const iso = this.#record.expiresAt;
53
+ if (iso !== this.#expiresAtIso) {
54
+ this.#expiresAtIso = iso;
55
+ this.#expiresAtMs = Date.parse(iso);
56
+ }
57
+ return new Date(this.#expiresAtMs);
50
58
  }
51
59
  async getBlock(id) {
52
60
  this.#assertOpen();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnowdb/core",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "A columnar SQL database for the browser: PostgreSQL-style SQL over durable IndexedDB or OPFS data, with no server or WebAssembly module.",
5
5
  "license": "MIT",
6
6
  "author": "Eric Wilhite",
@@ -129,6 +129,11 @@
129
129
  "classification": "different",
130
130
  "reason": "Minnow returns JSON text; PostgreSQL returns a native JSON value, and member order is unspecified without aggregate-local ORDER BY."
131
131
  },
132
+ {
133
+ "id": "subquery.correlated-json-aggregate",
134
+ "classification": "different",
135
+ "reason": "Both engines accept the correlated JSON aggregate and agree on its JSON value, but Minnow returns JSON text while PostgreSQL returns a native JSON value."
136
+ },
132
137
  {
133
138
  "id": "type.exact-numeric",
134
139
  "classification": "different",
@@ -257,8 +257,8 @@
257
257
  {
258
258
  "id": "mutation.returning",
259
259
  "status": "supported",
260
- "example": "DELETE FROM keyed WHERE name = 'x' RETURNING name, score",
261
- "notes": "RETURNING works on INSERT, UPDATE, and DELETE; inserts echo written values, updates return post-update values, deletes the rows as read."
260
+ "example": "DELETE FROM keyed WHERE name = 'x' RETURNING keyed.name, keyed.score",
261
+ "notes": "RETURNING works on INSERT, UPDATE, and DELETE; inserts echo written values, updates return post-update values, and deletes return the rows as read. Columns and target.* may be target-qualified."
262
262
  },
263
263
  {
264
264
  "id": "mutation.upsert",
@@ -317,7 +317,7 @@
317
317
  "id": "predicate.quantified",
318
318
  "status": "supported",
319
319
  "example": "SELECT region FROM rows WHERE amount > ALL (SELECT amount FROM dims)",
320
- "notes": "ANY/SOME/ALL use full three-valued logic. Correlated forms are supported as top-level WHERE predicates. SQLite itself has no quantified comparisons."
320
+ "notes": "ANY/SOME/ALL use full three-valued logic, including when a correlated form is nested below OR, NOT, CASE, or a select expression. SQLite itself has no quantified comparisons."
321
321
  },
322
322
  {
323
323
  "id": "predicate.ilike",
@@ -400,7 +400,19 @@
400
400
  "id": "subquery.correlated-select",
401
401
  "status": "supported",
402
402
  "example": "SELECT r.region, (SELECT AVG(q.amount) FROM rows q WHERE q.region = r.region) AS regional FROM rows r",
403
- "notes": "Correlated scalar aggregates decorrelate in the select list. In a grouped query, their outer references must be GROUP BY columns and the scalar cannot sit inside an outer aggregate."
403
+ "notes": "Correlated scalar aggregates and single-row projections decorrelate in the select list. In a grouped query, their outer references must be GROUP BY columns and the scalar cannot sit inside an outer aggregate."
404
+ },
405
+ {
406
+ "id": "subquery.correlated-select-limit",
407
+ "status": "supported",
408
+ "example": "SELECT r.region, (SELECT q.amount FROM rows q WHERE q.region = r.region ORDER BY q.amount DESC LIMIT 1) AS peak FROM rows r",
409
+ "notes": "ORDER BY, LIMIT, and OFFSET apply independently to each distinct outer probe. Zero rows yield NULL and more than one unbounded row raises a scalar-cardinality error."
410
+ },
411
+ {
412
+ "id": "subquery.correlated-json-aggregate",
413
+ "status": "supported",
414
+ "example": "SELECT r.region, (SELECT JSON_ARRAYAGG(JSON_OBJECT('amount' VALUE q.amount) ORDER BY q.amount) FROM rows q WHERE q.region = r.region) AS amounts FROM rows r",
415
+ "notes": "JSON aggregate expressions use the same set-at-a-time decorrelation as numeric aggregates and preserve their JSON result domain."
404
416
  },
405
417
  {
406
418
  "id": "subquery.correlated-select-grouped",
@@ -418,7 +430,7 @@
418
430
  "id": "subquery.correlated-exists-expression",
419
431
  "status": "supported",
420
432
  "example": "SELECT r.amount FROM rows r WHERE r.amount > 100 OR EXISTS (SELECT d.region FROM dims d WHERE d.region = r.region)",
421
- "notes": "Correlated EXISTS and NOT EXISTS remain set-at-a-time when nested below OR, NOT, or CASE."
433
+ "notes": "Correlated EXISTS and NOT EXISTS remain set-at-a-time below OR, NOT, or CASE and across deeper correlated EXISTS, IN, NOT IN, or scalar blocks. Generated aliases are unique across the complete plan tree."
422
434
  },
423
435
  {
424
436
  "id": "subquery.correlated-non-equi",
@@ -438,6 +450,12 @@
438
450
  "example": "SELECT region FROM rows r WHERE region NOT IN (SELECT d.region FROM dims d WHERE d.region = r.region)",
439
451
  "notes": "Correlated NOT IN preserves empty-set and NULL semantics rather than treating it as a simple anti-join."
440
452
  },
453
+ {
454
+ "id": "subquery.correlated-membership-expression",
455
+ "status": "supported",
456
+ "example": "SELECT r.amount FROM rows r WHERE r.amount = 3 OR r.region NOT IN (SELECT q.region FROM rows q WHERE q.amount < r.amount)",
457
+ "notes": "Correlated IN and NOT IN retain true, false, and unknown results below OR, NOT, CASE, and in select expressions."
458
+ },
441
459
  {
442
460
  "id": "subquery.correlated-not-in-non-equi",
443
461
  "status": "supported",
@@ -447,8 +465,8 @@
447
465
  {
448
466
  "id": "subquery.correlated-quantified",
449
467
  "status": "supported",
450
- "example": "SELECT r.amount FROM rows r WHERE r.amount > ALL (SELECT q.amount FROM rows q WHERE q.region = r.region)",
451
- "notes": "At top-level WHERE, correlated ANY lowers to a semi-join on true comparisons and ALL to an anti-join on false or unknown comparisons."
468
+ "example": "SELECT r.amount FROM rows r WHERE r.amount = 3 OR r.amount > ALL (SELECT q.amount FROM rows q WHERE q.region = r.region)",
469
+ "notes": "Top-level WHERE uses semi/anti joins. Nested expressions group true, false, unknown, and empty-set counts per distinct outer probe tuple."
452
470
  },
453
471
  {
454
472
  "id": "cte.recursive",
@@ -536,11 +554,46 @@
536
554
  "example": "SELECT amount, SUM(amount) OVER (ORDER BY amount, joined ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS windowed FROM rows",
537
555
  "notes": "ROWS frames take row-distance bounds; RANGE frames take UNBOUNDED and CURRENT ROW bounds, where CURRENT ROW spans the ordering peer group."
538
556
  },
557
+ {
558
+ "id": "window.frame-range-offset",
559
+ "status": "unsupported",
560
+ "example": "SELECT amount, SUM(amount) OVER (ORDER BY amount RANGE BETWEEN 1 PRECEDING AND CURRENT ROW) AS windowed FROM rows",
561
+ "error": "RANGE frames take only UNBOUNDED and CURRENT ROW bounds; use ROWS",
562
+ "notes": "A numeric RANGE offset bounds the frame by ordering-value distance, which the engine does not implement. ROWS frames take numeric offsets; RANGE frames take UNBOUNDED and CURRENT ROW bounds only."
563
+ },
564
+ {
565
+ "id": "window.distinct-aggregate",
566
+ "status": "unsupported",
567
+ "example": "SELECT SUM(DISTINCT amount) OVER (PARTITION BY region) AS total FROM rows",
568
+ "error": "DISTINCT window aggregates are not supported",
569
+ "notes": "An aggregate used as a window function cannot take DISTINCT; PostgreSQL rejects this form too. DISTINCT aggregates work in grouped aggregation, so aggregate in a grouped block and window over that."
570
+ },
571
+ {
572
+ "id": "window.outside-select",
573
+ "status": "unsupported",
574
+ "example": "SELECT amount FROM rows ORDER BY ROW_NUMBER() OVER (ORDER BY amount)",
575
+ "error": "Window functions are only allowed in the select list",
576
+ "notes": "PostgreSQL also evaluates window functions in ORDER BY; Minnow evaluates them only as select items. Alias the window in the select list and order by the alias."
577
+ },
539
578
  {
540
579
  "id": "join.right",
541
580
  "status": "supported",
542
581
  "example": "SELECT r.region FROM rows r RIGHT JOIN dims d ON d.region = r.region",
543
- "notes": "Desugars to the mirrored LEFT JOIN; supported as the sole join of a block."
582
+ "notes": "Desugars to the mirrored LEFT JOIN; supported as the sole join of a block, and not beside SELECT *."
583
+ },
584
+ {
585
+ "id": "join.right-multi",
586
+ "status": "unsupported",
587
+ "example": "SELECT r.region FROM rows r JOIN dims d ON d.region = r.region RIGHT JOIN dims e ON e.region = r.region",
588
+ "error": "RIGHT JOIN is only supported as the sole join",
589
+ "notes": "RIGHT JOIN desugars by swapping the two sides of a LEFT JOIN, which needs the block to hold exactly one join. Rewrite the block so the preserved side is on the left of a LEFT JOIN."
590
+ },
591
+ {
592
+ "id": "join.right-wildcard",
593
+ "status": "unsupported",
594
+ "example": "SELECT * FROM rows r RIGHT JOIN dims d ON d.region = r.region",
595
+ "error": "RIGHT JOIN cannot be combined with SELECT *",
596
+ "notes": "The desugaring swaps the two sources, which would reorder a wildcard's output columns. Name the output columns explicitly, or write the mirrored LEFT JOIN."
544
597
  },
545
598
  {
546
599
  "id": "join.non-equi",
@@ -740,7 +793,14 @@
740
793
  "id": "join.full",
741
794
  "status": "supported",
742
795
  "example": "SELECT r.amount AS amount, d.label AS label FROM rows r FULL JOIN dims d ON d.region = r.region",
743
- "notes": "Desugars into a union of two left joins, so it must be the sole join, with an equality ON and no grouping or DISTINCT yet."
796
+ "notes": "Desugars into a union of two left joins, so it must be the sole join, with an equality ON, named output columns rather than SELECT *, and no grouping, DISTINCT, or window functions yet."
797
+ },
798
+ {
799
+ "id": "join.full-grouped",
800
+ "status": "unsupported",
801
+ "example": "SELECT r.region AS region, COUNT(*) AS matched FROM rows r FULL JOIN dims d ON d.region = r.region GROUP BY r.region",
802
+ "error": "FULL JOIN cannot be combined with grouping, DISTINCT, or window functions yet",
803
+ "notes": "FULL JOIN desugars into a union of two left joins, and grouping, DISTINCT, and window functions do not distribute over that union yet. Put the FULL JOIN in a derived table and group, deduplicate, or window in the outer block."
744
804
  },
745
805
  {
746
806
  "id": "order-by.ordinal",
@@ -1197,7 +1257,7 @@
1197
1257
  "id": "aggregate.json",
1198
1258
  "status": "supported",
1199
1259
  "example": "SELECT JSON_ARRAYAGG(JSON_OBJECT('region' VALUE region)) AS regions FROM rows",
1200
- "notes": "Supports DISTINCT, embeds JSON-producing inputs as documents, includes SQL NULL as JSON null, and returns NULL for empty input. FILTER, window use, aggregate-local ORDER BY, and explicit NULL/ABSENT clauses are not supported; input order is otherwise unspecified."
1260
+ "notes": "Supports DISTINCT and aggregate-local ORDER BY, embeds JSON-producing inputs as documents, includes SQL NULL as JSON null, and returns NULL for empty input. FILTER, window use, and explicit NULL/ABSENT clauses are not supported; input order is unspecified without ORDER BY."
1201
1261
  },
1202
1262
  {
1203
1263
  "id": "type.array",
@@ -1254,6 +1314,14 @@
1254
1314
  "setup": ["CREATE TABLE parents (id INTEGER PRIMARY KEY, label TEXT NOT NULL)"],
1255
1315
  "example": "CREATE TABLE children (id INTEGER PRIMARY KEY, parent INTEGER REFERENCES parents(id) ON DELETE CASCADE)",
1256
1316
  "notes": "Scalar and composite references target the parent's matching primary-key columns. Writes validate against their transaction; NULL references are satisfied. ON DELETE supports RESTRICT, CASCADE, and SET NULL atomically. Primary keys are immutable, so ON UPDATE has no action."
1317
+ },
1318
+ {
1319
+ "id": "ddl.foreign-key-set-default",
1320
+ "status": "unsupported",
1321
+ "setup": ["CREATE TABLE default_parents (id INTEGER PRIMARY KEY, label TEXT NOT NULL)"],
1322
+ "example": "CREATE TABLE default_children (id INTEGER PRIMARY KEY, parent INTEGER REFERENCES default_parents(id) ON DELETE SET DEFAULT)",
1323
+ "error": "SET DEFAULT is not supported; use SET NULL or CASCADE",
1324
+ "notes": "SET DEFAULT would rewrite orphaned references to the column's default value at delete time; Minnow implements RESTRICT, CASCADE, and SET NULL. The statement is rejected at parse, before touching the catalog."
1257
1325
  }
1258
1326
  ]
1259
1327
  }