@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.
- package/README.md +3 -2
- package/dist/engine/artifact-cache.d.ts +5 -0
- package/dist/engine/artifact-cache.js +64 -13
- package/dist/engine/database.js +294 -12
- package/dist/engine/optimizer.d.ts +7 -0
- package/dist/engine/optimizer.js +855 -84
- package/dist/engine/point-read.d.ts +57 -0
- package/dist/engine/point-read.js +189 -0
- package/dist/engine/query.d.ts +6 -0
- package/dist/engine/query.js +101 -18
- package/dist/engine/vector.js +69 -5
- package/dist/plan/model.d.ts +7 -1
- package/dist/storage/toolkit/record-core.js +104 -75
- package/dist/storage/types.js +34 -6
- package/dist/transactions/index.js +9 -1
- package/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +78 -10
package/dist/storage/types.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|
package/sql-feature-matrix.json
CHANGED
|
@@ -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
|
|
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
|
|
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": "
|
|
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
|
|
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,
|
|
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
|
}
|