@minnowdb/core 0.4.1 → 0.6.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/README.md +5 -5
- package/dist/engine/cancellation.d.ts +2 -0
- package/dist/engine/cancellation.js +4 -0
- package/dist/engine/catalog.d.ts +5 -1
- package/dist/engine/catalog.js +5 -1
- package/dist/engine/client.d.ts +34 -6
- package/dist/engine/client.js +87 -19
- package/dist/engine/database.d.ts +43 -20
- package/dist/engine/database.js +823 -164
- package/dist/engine/defaults.js +11 -0
- package/dist/engine/errors.d.ts +19 -0
- package/dist/engine/errors.js +31 -0
- package/dist/engine/fts.d.ts +2 -15
- package/dist/engine/live.d.ts +1 -7
- package/dist/engine/live.js +12 -13
- package/dist/engine/optimizer.js +546 -39
- package/dist/engine/query-cache.js +1 -0
- package/dist/engine/query.d.ts +16 -278
- package/dist/engine/query.js +260 -74
- package/dist/engine/result-wire.d.ts +2 -0
- package/dist/engine/result-wire.js +21 -5
- package/dist/engine/schema-wire.d.ts +14 -1
- package/dist/engine/schema-wire.js +7 -1
- package/dist/engine/schema.d.ts +83 -32
- package/dist/engine/schema.js +180 -14
- package/dist/engine/sql-domains.d.ts +11 -0
- package/dist/engine/sql-domains.js +65 -1
- package/dist/engine/sql-json.js +22 -3
- package/dist/engine/sql-semantics.js +21 -3
- package/dist/engine/vector.d.ts +2 -2
- package/dist/engine/vector.js +328 -79
- package/dist/engine/worker-host.js +119 -44
- package/dist/plan/index.d.ts +5 -4
- package/dist/plan/index.js +3 -3
- package/dist/plan/model.d.ts +218 -0
- package/dist/plan/model.js +1 -0
- package/dist/storage/indexeddb.js +4 -12
- package/dist/storage/toolkit/record-core.js +7 -22
- package/dist/storage/types.d.ts +26 -8
- package/dist/storage/types.js +85 -0
- package/dist/transactions/index.d.ts +5 -3
- package/dist/transactions/index.js +58 -8
- package/dist/worker-protocol/index.d.ts +6 -1
- package/dist/worker-protocol/index.js +5 -2
- package/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +75 -19
package/dist/storage/types.d.ts
CHANGED
|
@@ -111,7 +111,7 @@ export type SqlDomain = {
|
|
|
111
111
|
precision?: number;
|
|
112
112
|
scale?: number;
|
|
113
113
|
} | {
|
|
114
|
-
kind: "json" | "jsonb" | "uuid" | "time" | "interval";
|
|
114
|
+
kind: "json" | "jsonb" | "uuid" | "date" | "time" | "interval";
|
|
115
115
|
} | {
|
|
116
116
|
kind: "array";
|
|
117
117
|
element: string;
|
|
@@ -136,6 +136,11 @@ export type ColumnDefault = {
|
|
|
136
136
|
} | {
|
|
137
137
|
kind: "autoincrement";
|
|
138
138
|
};
|
|
139
|
+
/** A stored generated-column expression, evaluated from the row on every insert and update. */
|
|
140
|
+
export interface ColumnGenerated {
|
|
141
|
+
readonly kind: "stored";
|
|
142
|
+
readonly sql: string;
|
|
143
|
+
}
|
|
139
144
|
export interface TableColumnRecord {
|
|
140
145
|
id: string;
|
|
141
146
|
name: string;
|
|
@@ -152,6 +157,8 @@ export interface TableColumnRecord {
|
|
|
152
157
|
nullable: boolean;
|
|
153
158
|
/** Fills omitted or SQL `DEFAULT` slots at insert time; explicit NULL is never replaced. */
|
|
154
159
|
defaultValue?: ColumnDefault;
|
|
160
|
+
/** Recomputed from sibling columns on every insert and update; callers cannot assign it. */
|
|
161
|
+
generatedValue?: ColumnGenerated;
|
|
155
162
|
/**
|
|
156
163
|
* What rows written before this column existed read as, instead of NULL.
|
|
157
164
|
*
|
|
@@ -264,6 +271,15 @@ export interface SecondaryIndexRecord {
|
|
|
264
271
|
* accelerator. Index identity, key shape, and UNIQUE enforcement are structural.
|
|
265
272
|
*/
|
|
266
273
|
export declare function secondaryIndexWriteContractChanged(previous: Readonly<Record<string, SecondaryIndexRecord>> | null | undefined, next: Readonly<Record<string, SecondaryIndexRecord>> | null | undefined): boolean;
|
|
274
|
+
export interface TableForeignKeyRecord {
|
|
275
|
+
name: string;
|
|
276
|
+
columns: string[];
|
|
277
|
+
parentTable: string;
|
|
278
|
+
parentColumns: string[];
|
|
279
|
+
onDelete: "restrict" | "cascade" | "set null";
|
|
280
|
+
/** False records relationship metadata without write/delete enforcement. Absent means true. */
|
|
281
|
+
enforced?: boolean;
|
|
282
|
+
}
|
|
267
283
|
export interface TableRecord {
|
|
268
284
|
id: string;
|
|
269
285
|
name: string;
|
|
@@ -279,13 +295,7 @@ export interface TableRecord {
|
|
|
279
295
|
/** BEFORE/AFTER triggers on this table, fired by the committing writer in its transaction. */
|
|
280
296
|
triggers?: TriggerRecord[];
|
|
281
297
|
/** FOREIGN KEY constraints. Column tuples are always explicit, including scalar keys. */
|
|
282
|
-
foreignKeys?:
|
|
283
|
-
name: string;
|
|
284
|
-
columns: string[];
|
|
285
|
-
parentTable: string;
|
|
286
|
-
parentColumns: string[];
|
|
287
|
-
onDelete: "restrict" | "cascade" | "set null";
|
|
288
|
-
}>;
|
|
298
|
+
foreignKeys?: TableForeignKeyRecord[];
|
|
289
299
|
/**
|
|
290
300
|
* Row-level CHECK constraints (E141-06), each the text of a boolean expression over this
|
|
291
301
|
* table's own columns. Text rather than a compiled form because the record crosses the worker
|
|
@@ -329,6 +339,12 @@ export interface TableRecord {
|
|
|
329
339
|
/** Compare-and-swap revision for catalog evolution. */
|
|
330
340
|
revision: number;
|
|
331
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Validates one relationship against its child and parent records. This is shared by every
|
|
344
|
+
* storage adapter and snapshot restore path so informational relationships cannot bypass the
|
|
345
|
+
* same structural and domain checks as enforced foreign keys.
|
|
346
|
+
*/
|
|
347
|
+
export declare function validateTableForeignKey(childTable: TableRecord, key: TableForeignKeyRecord, parentTable: TableRecord): void;
|
|
332
348
|
/** Exact UTF-8 bytes used when the canonical record-wire JSON codec persists a table record. */
|
|
333
349
|
export declare function catalogRecordRetainedBytes(record: TableRecord): number;
|
|
334
350
|
export declare function manifestRecordRetainedBytes(record: Manifest): number;
|
|
@@ -1815,6 +1831,8 @@ export interface CatalogMutationOptions {
|
|
|
1815
1831
|
/** Atomic replacement fields for one catalog record. */
|
|
1816
1832
|
export interface TableRecordUpdate extends CatalogMutationOptions {
|
|
1817
1833
|
columns?: TableColumnRecord[];
|
|
1834
|
+
/** Replaces the complete relationship catalog. */
|
|
1835
|
+
foreignKeys?: TableRecord["foreignKeys"];
|
|
1818
1836
|
/** Replaces the full-text index state map; null clears it. */
|
|
1819
1837
|
ftsColumns?: Record<string, FtsColumnIndexRecord> | null;
|
|
1820
1838
|
/** Replaces the secondary-index state map; null clears it. */
|
package/dist/storage/types.js
CHANGED
|
@@ -141,6 +141,22 @@ export function validateTableColumns(columns) {
|
|
|
141
141
|
...(column.enumValues === undefined ? {} : { enumValues: column.enumValues }),
|
|
142
142
|
}, column.defaultValue);
|
|
143
143
|
}
|
|
144
|
+
if (column.generatedValue !== undefined) {
|
|
145
|
+
const generated = column.generatedValue;
|
|
146
|
+
if (typeof generated !== "object" ||
|
|
147
|
+
generated === null ||
|
|
148
|
+
!("kind" in generated) ||
|
|
149
|
+
generated.kind !== "stored" ||
|
|
150
|
+
!("sql" in generated) ||
|
|
151
|
+
typeof generated.sql !== "string" ||
|
|
152
|
+
generated.sql.length === 0 ||
|
|
153
|
+
generated.sql.trim() !== generated.sql) {
|
|
154
|
+
throw new TypeError(`Generated SQL must be a trimmed non-empty expression: ${column.name}`);
|
|
155
|
+
}
|
|
156
|
+
if (column.defaultValue !== undefined || column.backfill !== undefined || column.hidden) {
|
|
157
|
+
throw new TypeError(`Generated columns cannot have defaults, backfills, or hidden metadata: ${column.name}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
144
160
|
if (column.backfill !== undefined) {
|
|
145
161
|
const value = column.backfill;
|
|
146
162
|
const validType = column.type === "datetime"
|
|
@@ -258,6 +274,70 @@ export function secondaryIndexWriteContractChanged(previous, next) {
|
|
|
258
274
|
left.locator !== right.locator);
|
|
259
275
|
});
|
|
260
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Validates one relationship against its child and parent records. This is shared by every
|
|
279
|
+
* storage adapter and snapshot restore path so informational relationships cannot bypass the
|
|
280
|
+
* same structural and domain checks as enforced foreign keys.
|
|
281
|
+
*/
|
|
282
|
+
export function validateTableForeignKey(childTable, key, parentTable) {
|
|
283
|
+
if (typeof key.name !== "string" ||
|
|
284
|
+
key.name.length === 0 ||
|
|
285
|
+
typeof key.parentTable !== "string" ||
|
|
286
|
+
key.parentTable.length === 0 ||
|
|
287
|
+
!Array.isArray(key.columns) ||
|
|
288
|
+
!Array.isArray(key.parentColumns) ||
|
|
289
|
+
key.columns.length === 0 ||
|
|
290
|
+
key.columns.some((column) => typeof column !== "string" || column.length === 0) ||
|
|
291
|
+
key.parentColumns.some((column) => typeof column !== "string" || column.length === 0) ||
|
|
292
|
+
new Set(key.columns).size !== key.columns.length ||
|
|
293
|
+
new Set(key.parentColumns).size !== key.parentColumns.length ||
|
|
294
|
+
!["restrict", "cascade", "set null"].includes(key.onDelete) ||
|
|
295
|
+
(key.enforced !== undefined && typeof key.enforced !== "boolean")) {
|
|
296
|
+
throw new TypeError("FOREIGN KEY metadata is invalid");
|
|
297
|
+
}
|
|
298
|
+
if (key.parentTable !== parentTable.name) {
|
|
299
|
+
throw new TypeError(`FOREIGN KEY ${key.name} resolved to the wrong parent table`);
|
|
300
|
+
}
|
|
301
|
+
if (key.enforced === false && key.onDelete !== "restrict") {
|
|
302
|
+
throw new TypeError(`Informational FOREIGN KEY ${key.name} cannot declare ON DELETE actions`);
|
|
303
|
+
}
|
|
304
|
+
const children = key.columns.map((name) => childTable.columns.find((column) => column.name === name && column.hidden !== true));
|
|
305
|
+
if (children.some((column) => column === undefined)) {
|
|
306
|
+
throw new TypeError(`FOREIGN KEY ${key.name} names a column this table does not have`);
|
|
307
|
+
}
|
|
308
|
+
const addressIds = parentTable.primaryKeyColumnIds?.length
|
|
309
|
+
? parentTable.primaryKeyColumnIds
|
|
310
|
+
: parentTable.uniqueKeyColumnId === undefined
|
|
311
|
+
? []
|
|
312
|
+
: [parentTable.uniqueKeyColumnId];
|
|
313
|
+
const parents = addressIds.map((id) => parentTable.columns.find((column) => column.id === id && column.hidden !== true));
|
|
314
|
+
const addressNames = parents.map((column) => column?.name ?? "");
|
|
315
|
+
if (addressNames.length !== key.parentColumns.length ||
|
|
316
|
+
addressNames.some((name, index) => name !== key.parentColumns[index])) {
|
|
317
|
+
throw new TypeError(`FOREIGN KEY ${key.name} must reference the parent primary or unique key`);
|
|
318
|
+
}
|
|
319
|
+
if (children.length !== parents.length) {
|
|
320
|
+
throw new TypeError(`FOREIGN KEY ${key.name} has ${String(children.length)} child columns for ${String(parents.length)} parent columns`);
|
|
321
|
+
}
|
|
322
|
+
children.forEach((child, index) => {
|
|
323
|
+
const parent = parents[index];
|
|
324
|
+
if (child === undefined || parent === undefined) {
|
|
325
|
+
throw new TypeError(`FOREIGN KEY ${key.name} is missing a key column`);
|
|
326
|
+
}
|
|
327
|
+
if (child.type !== parent.type) {
|
|
328
|
+
throw new TypeError(`FOREIGN KEY ${key.name} compares ${child.type} with ${parent.type}`);
|
|
329
|
+
}
|
|
330
|
+
if ((child.integer === true) !== (parent.integer === true)) {
|
|
331
|
+
throw new TypeError(`FOREIGN KEY ${key.name} compares an integer domain with an approximate number domain`);
|
|
332
|
+
}
|
|
333
|
+
if (JSON.stringify(child.sqlDomain ?? null) !== JSON.stringify(parent.sqlDomain ?? null)) {
|
|
334
|
+
throw new TypeError(`FOREIGN KEY ${key.name} compares different SQL value domains`);
|
|
335
|
+
}
|
|
336
|
+
if (key.onDelete === "set null" && !child.nullable) {
|
|
337
|
+
throw new TypeError(`FOREIGN KEY ${key.name} cannot SET NULL a NOT NULL column`);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
261
341
|
const catalogRecordTextEncoder = new TextEncoder();
|
|
262
342
|
function durableRecordRetainedBytes(record, label) {
|
|
263
343
|
// Keep this byte accounting identical to the canonical record-wire JSON codec without
|
|
@@ -389,6 +469,11 @@ export function validateTableRecordBounds(record) {
|
|
|
389
469
|
throw new RangeError(`A table cannot exceed ${String(MAX_TABLE_CONSTRAINTS)} named constraints`);
|
|
390
470
|
}
|
|
391
471
|
const constraintNames = new Set();
|
|
472
|
+
for (const key of record.foreignKeys ?? []) {
|
|
473
|
+
if (key.enforced !== undefined && typeof key.enforced !== "boolean") {
|
|
474
|
+
throw new TypeError(`FOREIGN KEY enforcement is invalid: ${key.name}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
392
477
|
for (const constraintName of namedConstraints) {
|
|
393
478
|
validateCatalogName(constraintName, "Constraint name");
|
|
394
479
|
if (constraintNames.has(constraintName)) {
|
|
@@ -195,9 +195,11 @@ export declare class DatabaseTransaction {
|
|
|
195
195
|
setUniqueKeyChanges(changes: UniqueKeyChanges): void;
|
|
196
196
|
/**
|
|
197
197
|
* Attaches one batch's full-text deltas; applied atomically with the publish. A second
|
|
198
|
-
* batch for the same table merges per column — postings
|
|
199
|
-
*
|
|
200
|
-
*
|
|
198
|
+
* batch for the same table merges per column — postings stay sorted by term and row ID,
|
|
199
|
+
* duplicate row locators collapse to their greatest term frequency, and token totals are
|
|
200
|
+
* summed — so a scope can mutate one indexed table any number of times. Secondary indexes
|
|
201
|
+
* use stable hashed key locators rather than reserved row IDs, so operation order does not
|
|
202
|
+
* imply locator order.
|
|
201
203
|
*/
|
|
202
204
|
setFtsChanges(changes: FtsChanges): void;
|
|
203
205
|
/**
|
|
@@ -712,9 +712,11 @@ export class DatabaseTransaction {
|
|
|
712
712
|
}
|
|
713
713
|
/**
|
|
714
714
|
* Attaches one batch's full-text deltas; applied atomically with the publish. A second
|
|
715
|
-
* batch for the same table merges per column — postings
|
|
716
|
-
*
|
|
717
|
-
*
|
|
715
|
+
* batch for the same table merges per column — postings stay sorted by term and row ID,
|
|
716
|
+
* duplicate row locators collapse to their greatest term frequency, and token totals are
|
|
717
|
+
* summed — so a scope can mutate one indexed table any number of times. Secondary indexes
|
|
718
|
+
* use stable hashed key locators rather than reserved row IDs, so operation order does not
|
|
719
|
+
* imply locator order.
|
|
718
720
|
*/
|
|
719
721
|
setFtsChanges(changes) {
|
|
720
722
|
this.#assertActive();
|
|
@@ -746,6 +748,7 @@ export class DatabaseTransaction {
|
|
|
746
748
|
});
|
|
747
749
|
continue;
|
|
748
750
|
}
|
|
751
|
+
let duplicateTokens = 0;
|
|
749
752
|
for (const posting of column.postings) {
|
|
750
753
|
const held = present.postings.get(posting.term);
|
|
751
754
|
if (held === undefined) {
|
|
@@ -756,13 +759,18 @@ export class DatabaseTransaction {
|
|
|
756
759
|
});
|
|
757
760
|
}
|
|
758
761
|
else {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
held
|
|
762
|
+
const merged = mergeFtsPostingRows(held, posting);
|
|
763
|
+
duplicateTokens = safeWholeNumberSum([
|
|
764
|
+
duplicateTokens,
|
|
765
|
+
postingTokenCount(held) + postingTokenCount(posting) - postingTokenCount(merged),
|
|
766
|
+
], "Duplicate posting token count");
|
|
767
|
+
present.postings.set(posting.term, merged);
|
|
763
768
|
}
|
|
764
769
|
}
|
|
765
|
-
|
|
770
|
+
const totalTokens = tokenTotals.get(column.columnId) ?? present.totalTokens;
|
|
771
|
+
if (duplicateTokens > totalTokens)
|
|
772
|
+
throw new Error("Posting token merge is inconsistent");
|
|
773
|
+
present.totalTokens = totalTokens - duplicateTokens;
|
|
766
774
|
}
|
|
767
775
|
}
|
|
768
776
|
#materializedFtsChanges() {
|
|
@@ -1615,6 +1623,48 @@ function safeWholeNumberSum(values, label) {
|
|
|
1615
1623
|
}
|
|
1616
1624
|
return total;
|
|
1617
1625
|
}
|
|
1626
|
+
/** Linear merge of two canonical postings for one term. */
|
|
1627
|
+
function mergeFtsPostingRows(left, right) {
|
|
1628
|
+
if (left.term !== right.term)
|
|
1629
|
+
throw new TypeError("Posting terms differ during transaction merge");
|
|
1630
|
+
const rowIds = [];
|
|
1631
|
+
const tf = [];
|
|
1632
|
+
let leftIndex = 0;
|
|
1633
|
+
let rightIndex = 0;
|
|
1634
|
+
const push = (rowId, frequency) => {
|
|
1635
|
+
const previous = rowIds[rowIds.length - 1];
|
|
1636
|
+
if (previous === rowId) {
|
|
1637
|
+
tf[tf.length - 1] = Math.max(tf[tf.length - 1] ?? 1, frequency);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
rowIds.push(rowId);
|
|
1641
|
+
tf.push(frequency);
|
|
1642
|
+
};
|
|
1643
|
+
while (leftIndex < left.rowIds.length && rightIndex < right.rowIds.length) {
|
|
1644
|
+
const leftRowId = left.rowIds[leftIndex] ?? 0n;
|
|
1645
|
+
const rightRowId = right.rowIds[rightIndex] ?? 0n;
|
|
1646
|
+
if (leftRowId <= rightRowId) {
|
|
1647
|
+
push(leftRowId, left.tf[leftIndex] ?? 1);
|
|
1648
|
+
leftIndex += 1;
|
|
1649
|
+
}
|
|
1650
|
+
if (rightRowId <= leftRowId) {
|
|
1651
|
+
push(rightRowId, right.tf[rightIndex] ?? 1);
|
|
1652
|
+
rightIndex += 1;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
while (leftIndex < left.rowIds.length) {
|
|
1656
|
+
push(left.rowIds[leftIndex] ?? 0n, left.tf[leftIndex] ?? 1);
|
|
1657
|
+
leftIndex += 1;
|
|
1658
|
+
}
|
|
1659
|
+
while (rightIndex < right.rowIds.length) {
|
|
1660
|
+
push(right.rowIds[rightIndex] ?? 0n, right.tf[rightIndex] ?? 1);
|
|
1661
|
+
rightIndex += 1;
|
|
1662
|
+
}
|
|
1663
|
+
return { term: left.term, rowIds, tf };
|
|
1664
|
+
}
|
|
1665
|
+
function postingTokenCount(posting) {
|
|
1666
|
+
return safeWholeNumberSum(posting.tf, "Posting token count");
|
|
1667
|
+
}
|
|
1618
1668
|
function requiredCompactionJobId(id) {
|
|
1619
1669
|
if (id === null)
|
|
1620
1670
|
throw new Error("Compaction source blocks require a compaction job");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const protocolVersion:
|
|
1
|
+
export declare const protocolVersion: 3;
|
|
2
2
|
/** Outstanding request/response pairs retained by either side of one database RPC connection. */
|
|
3
3
|
export declare const MAX_DATABASE_RPC_IN_FLIGHT = 256;
|
|
4
4
|
export type WorkerOperation = "benchmark" | "cancelBenchmark" | "memorySample" | "datasetList" | "datasetCreate" | "datasetDelete" | "runQuery" | "suiteReference" | "suiteWrite" | "suiteFeatureMatrix" | "suiteLive";
|
|
@@ -46,6 +46,11 @@ export type RpcRequest = {
|
|
|
46
46
|
handleId: string | null;
|
|
47
47
|
method: string;
|
|
48
48
|
args: unknown[];
|
|
49
|
+
} | {
|
|
50
|
+
version: typeof protocolVersion;
|
|
51
|
+
/** The request whose work should stop. Cancellation has no response frame of its own. */
|
|
52
|
+
requestId: string;
|
|
53
|
+
kind: "rpc-cancel";
|
|
49
54
|
};
|
|
50
55
|
export type RpcResponse = {
|
|
51
56
|
version: typeof protocolVersion;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const protocolVersion =
|
|
1
|
+
export const protocolVersion = 3;
|
|
2
2
|
/** Outstanding request/response pairs retained by either side of one database RPC connection. */
|
|
3
3
|
export const MAX_DATABASE_RPC_IN_FLIGHT = 256;
|
|
4
4
|
export function parseRequest(value) {
|
|
@@ -64,8 +64,11 @@ export function parseRpcRequest(value) {
|
|
|
64
64
|
if (typeof value !== "object" || value === null)
|
|
65
65
|
return null;
|
|
66
66
|
const candidate = value;
|
|
67
|
-
if (candidate.kind !== "rpc-init" &&
|
|
67
|
+
if (candidate.kind !== "rpc-init" &&
|
|
68
|
+
candidate.kind !== "rpc-call" &&
|
|
69
|
+
candidate.kind !== "rpc-cancel") {
|
|
68
70
|
return null;
|
|
71
|
+
}
|
|
69
72
|
if (candidate.version !== protocolVersion)
|
|
70
73
|
throw new Error("Unsupported protocol version");
|
|
71
74
|
if (typeof candidate.requestId !== "string" || candidate.requestId.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minnowdb/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -149,6 +149,11 @@
|
|
|
149
149
|
"classification": "different",
|
|
150
150
|
"reason": "Minnow returns arrays as canonical JSON text at the JavaScript boundary while PostgreSQL clients commonly return native arrays."
|
|
151
151
|
},
|
|
152
|
+
{
|
|
153
|
+
"id": "type.date",
|
|
154
|
+
"classification": "different",
|
|
155
|
+
"reason": "Minnow returns zoneless DATE values as canonical YYYY-MM-DD text while PostgreSQL clients commonly materialize them as midnight Date objects."
|
|
156
|
+
},
|
|
152
157
|
{
|
|
153
158
|
"id": "json.query",
|
|
154
159
|
"classification": "different",
|
package/sql-feature-matrix.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 5,
|
|
3
3
|
"description": "Executable PostgreSQL compatibility examples for MinnowDatabase. postgres-feature-profile.json records differences, extensions, and embedded exclusions.",
|
|
4
4
|
"features": [
|
|
5
5
|
{
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
{
|
|
21
21
|
"id": "select.distinct",
|
|
22
22
|
"status": "supported",
|
|
23
|
-
"example": "SELECT DISTINCT region FROM rows"
|
|
23
|
+
"example": "SELECT DISTINCT region FROM rows",
|
|
24
|
+
"notes": "Plain projections and SELECT DISTINCT * are supported. DISTINCT combined with grouping, HAVING, or aggregate/window expressions is rejected."
|
|
24
25
|
},
|
|
25
26
|
{
|
|
26
27
|
"id": "select.scalar-subquery",
|
|
@@ -127,7 +128,7 @@
|
|
|
127
128
|
"id": "group-by.rollup",
|
|
128
129
|
"status": "supported",
|
|
129
130
|
"example": "SELECT region, SUM(amount) AS total FROM rows GROUP BY ROLLUP(region)",
|
|
130
|
-
"notes": "ROLLUP/CUBE/GROUPING SETS desugar into a UNION ALL of grouped blocks.
|
|
131
|
+
"notes": "ROLLUP/CUBE/GROUPING SETS desugar into a UNION ALL of grouped blocks. GROUPING() distinguishes rolled-up columns from data NULLs. SQLite itself has none of these."
|
|
131
132
|
},
|
|
132
133
|
{
|
|
133
134
|
"id": "group-by.grouping-sets",
|
|
@@ -245,7 +246,7 @@
|
|
|
245
246
|
"id": "mutation.update-keyed",
|
|
246
247
|
"status": "supported",
|
|
247
248
|
"example": "UPDATE keyed SET score = score + 1 WHERE score > 0",
|
|
248
|
-
"notes": "Requires a unique-key table;
|
|
249
|
+
"notes": "Requires a unique-key table. A standalone statement reads and publishes inside one retryable write scope; an explicit transaction stages it with the transaction's other statements."
|
|
249
250
|
},
|
|
250
251
|
{
|
|
251
252
|
"id": "mutation.delete-keyed",
|
|
@@ -316,7 +317,7 @@
|
|
|
316
317
|
"id": "predicate.quantified",
|
|
317
318
|
"status": "supported",
|
|
318
319
|
"example": "SELECT region FROM rows WHERE amount > ALL (SELECT amount FROM dims)",
|
|
319
|
-
"notes": "ANY/SOME/ALL
|
|
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
321
|
},
|
|
321
322
|
{
|
|
322
323
|
"id": "predicate.ilike",
|
|
@@ -387,30 +388,67 @@
|
|
|
387
388
|
"id": "subquery.correlated",
|
|
388
389
|
"status": "supported",
|
|
389
390
|
"example": "SELECT region FROM rows r WHERE amount > (SELECT AVG(amount) FROM rows q WHERE q.region = r.region)",
|
|
390
|
-
"notes": "
|
|
391
|
+
"notes": "Correlated subqueries decorrelate into derived-table joins at compile time; both executors run plain joins."
|
|
391
392
|
},
|
|
392
393
|
{
|
|
393
394
|
"id": "subquery.correlated-exists",
|
|
394
395
|
"status": "supported",
|
|
395
396
|
"example": "SELECT amount FROM rows r WHERE EXISTS (SELECT region FROM dims d WHERE d.region = r.region)",
|
|
396
|
-
"notes": "EXISTS
|
|
397
|
+
"notes": "Top-level EXISTS and NOT EXISTS lower to semi-joins and anti-joins. Nested boolean forms use hidden per-probe match flags."
|
|
397
398
|
},
|
|
398
399
|
{
|
|
399
400
|
"id": "subquery.correlated-select",
|
|
400
401
|
"status": "supported",
|
|
401
402
|
"example": "SELECT r.region, (SELECT AVG(q.amount) FROM rows q WHERE q.region = r.region) AS regional FROM rows r",
|
|
402
|
-
"notes": "Correlated scalar aggregates decorrelate in the select list
|
|
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."
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"id": "subquery.correlated-select-grouped",
|
|
407
|
+
"status": "supported",
|
|
408
|
+
"example": "SELECT r.region, COUNT(*) AS c, (SELECT AVG(q.amount) FROM rows q WHERE q.region = r.region) AS regional FROM rows r GROUP BY r.region",
|
|
409
|
+
"notes": "The decorrelated value is functionally determined by the outer grouping keys and is carried as an internal group key."
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"id": "subquery.correlated-scalar-non-equi",
|
|
413
|
+
"status": "supported",
|
|
414
|
+
"example": "SELECT r.amount, (SELECT COUNT(*) FROM rows q WHERE q.amount < r.amount) AS lower_count FROM rows r",
|
|
415
|
+
"notes": "Non-equality scalar aggregates group inner rows once per distinct outer probe tuple, then join the result back by equality."
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
"id": "subquery.correlated-exists-expression",
|
|
419
|
+
"status": "supported",
|
|
420
|
+
"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."
|
|
403
422
|
},
|
|
404
423
|
{
|
|
405
424
|
"id": "subquery.correlated-non-equi",
|
|
406
425
|
"status": "supported",
|
|
407
426
|
"example": "SELECT r.amount FROM rows r WHERE EXISTS (SELECT q.amount FROM rows q WHERE q.amount < r.amount)",
|
|
408
|
-
"notes": "Non-equality EXISTS and NOT EXISTS correlations lower to semi-joins and anti-joins
|
|
427
|
+
"notes": "Non-equality EXISTS and NOT EXISTS correlations lower to semi-joins and anti-joins. Scalar aggregates use distinct outer probes."
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"id": "subquery.correlated-in-non-equi",
|
|
431
|
+
"status": "supported",
|
|
432
|
+
"example": "SELECT r.amount FROM rows r WHERE r.region IN (SELECT q.region FROM rows q WHERE q.amount < r.amount)",
|
|
433
|
+
"notes": "A range-correlated IN predicate lowers to a semi-join carrying both its correlation and membership comparisons."
|
|
409
434
|
},
|
|
410
435
|
{
|
|
411
436
|
"id": "subquery.correlated-not-in",
|
|
412
437
|
"status": "supported",
|
|
413
|
-
"example": "SELECT region FROM rows r WHERE region NOT IN (SELECT d.region FROM dims d WHERE d.region = r.region)"
|
|
438
|
+
"example": "SELECT region FROM rows r WHERE region NOT IN (SELECT d.region FROM dims d WHERE d.region = r.region)",
|
|
439
|
+
"notes": "Correlated NOT IN preserves empty-set and NULL semantics rather than treating it as a simple anti-join."
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
"id": "subquery.correlated-not-in-non-equi",
|
|
443
|
+
"status": "supported",
|
|
444
|
+
"example": "SELECT r.amount FROM rows r WHERE 'north' NOT IN (SELECT q.region FROM rows q WHERE q.amount < r.amount)",
|
|
445
|
+
"notes": "Range correlation uses an anti-join for exact matches plus per-probe total and non-NULL counts."
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
"id": "subquery.correlated-quantified",
|
|
449
|
+
"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."
|
|
414
452
|
},
|
|
415
453
|
{
|
|
416
454
|
"id": "cte.recursive",
|
|
@@ -456,13 +494,13 @@
|
|
|
456
494
|
"id": "aggregate.filter",
|
|
457
495
|
"status": "supported",
|
|
458
496
|
"example": "SELECT region, COUNT(*) FILTER (WHERE amount > 5) AS big FROM rows GROUP BY region",
|
|
459
|
-
"notes": "Desugars into a CASE inside
|
|
497
|
+
"notes": "Desugars into a CASE inside COUNT/SUM/AVG/MIN/MAX and preserves DISTINCT. JSON_ARRAYAGG does not support FILTER."
|
|
460
498
|
},
|
|
461
499
|
{
|
|
462
500
|
"id": "window.aggregate-over",
|
|
463
501
|
"status": "supported",
|
|
464
502
|
"example": "SELECT SUM(amount) OVER (PARTITION BY region) AS total FROM rows",
|
|
465
|
-
"notes": "
|
|
503
|
+
"notes": "Without explicit framing, the default is the whole partition when unordered and a peer-aware running frame when ordered. ROWS, RANGE, GROUPS, and exclusions are tracked separately below."
|
|
466
504
|
},
|
|
467
505
|
{
|
|
468
506
|
"id": "window.in-expression",
|
|
@@ -569,7 +607,7 @@
|
|
|
569
607
|
"id": "trigger.create-after",
|
|
570
608
|
"status": "supported",
|
|
571
609
|
"example": "CREATE TRIGGER keyed_audit AFTER INSERT ON keyed BEGIN INSERT INTO rows (region, amount) VALUES (NEW.name, NEW.score); END",
|
|
572
|
-
"notes": "AFTER and BEFORE row triggers on INSERT/UPDATE/DELETE
|
|
610
|
+
"notes": "AFTER and BEFORE row triggers on INSERT/UPDATE/DELETE execute atomically with NEW/OLD references. Bodies support parameter-free INSERT ... VALUES into keyless tables and UPDATE/DELETE against keyed tables; ON CONFLICT and RETURNING are rejected. One cascade level is allowed."
|
|
573
611
|
},
|
|
574
612
|
{
|
|
575
613
|
"id": "trigger.create-before",
|
|
@@ -970,8 +1008,8 @@
|
|
|
970
1008
|
{
|
|
971
1009
|
"id": "json.object",
|
|
972
1010
|
"status": "supported",
|
|
973
|
-
"example": "SELECT JSON_OBJECT('a' VALUE 1, '
|
|
974
|
-
"notes": "Defaults to NULL ON NULL and WITHOUT UNIQUE KEYS. NULL keys are rejected. The constructor returns JSON text; cast or store it as JSON/JSONB for domain validation and JSONB canonicalization."
|
|
1011
|
+
"example": "SELECT JSON_OBJECT('a' VALUE 1, 'detail' VALUE JSON_OBJECT('name' VALUE 'Acme')) AS document",
|
|
1012
|
+
"notes": "Defaults to NULL ON NULL and WITHOUT UNIQUE KEYS. NULL keys are rejected. JSON-producing arguments embed as documents rather than escaped strings; ordinary text remains a string. The constructor returns JSON text; cast or store it as JSON/JSONB for domain validation and JSONB canonicalization."
|
|
975
1013
|
},
|
|
976
1014
|
{
|
|
977
1015
|
"id": "json.array",
|
|
@@ -990,6 +1028,12 @@
|
|
|
990
1028
|
"example": "CREATE TABLE defaulted (id INTEGER PRIMARY KEY, tier TEXT DEFAULT 'basic')",
|
|
991
1029
|
"notes": "DEFAULT accepts a variable-free scalar expression. Omission or SQL DEFAULT invokes it; explicit NULL follows the column's independent nullability."
|
|
992
1030
|
},
|
|
1031
|
+
{
|
|
1032
|
+
"id": "ddl.generated-column",
|
|
1033
|
+
"status": "supported",
|
|
1034
|
+
"example": "CREATE TABLE generated_value (base INTEGER, doubled INTEGER GENERATED ALWAYS AS (base * 2) STORED)",
|
|
1035
|
+
"notes": "Stored generated columns recompute on every write and may be indexed, but cannot be caller-assigned or used as Minnow's row-addressing primary/unique key. Expressions are immutable and row-local."
|
|
1036
|
+
},
|
|
993
1037
|
{
|
|
994
1038
|
"id": "ddl.create-table-key-clause",
|
|
995
1039
|
"status": "supported",
|
|
@@ -1117,7 +1161,13 @@
|
|
|
1117
1161
|
"id": "from.lateral",
|
|
1118
1162
|
"status": "supported",
|
|
1119
1163
|
"example": "SELECT x.amount FROM rows r, LATERAL (SELECT amount FROM dims WHERE dims.region = r.region) x",
|
|
1120
|
-
"notes": "Equality-correlated derived sources become set-at-a-time joins. Correlated grouping, ordering, and LIMIT remain unsupported."
|
|
1164
|
+
"notes": "Equality and range-correlated derived sources become set-at-a-time joins. Correlated grouping, ordering, and LIMIT remain unsupported."
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
"id": "from.lateral-non-equi",
|
|
1168
|
+
"status": "supported",
|
|
1169
|
+
"example": "SELECT x.amount FROM rows r, LATERAL (SELECT q.amount FROM rows q WHERE q.amount < r.amount) x",
|
|
1170
|
+
"notes": "Non-equality correlation lowers to an ordinary general join rather than executing the derived source once per outer row."
|
|
1121
1171
|
},
|
|
1122
1172
|
{
|
|
1123
1173
|
"id": "aggregate.string-agg",
|
|
@@ -1146,8 +1196,8 @@
|
|
|
1146
1196
|
{
|
|
1147
1197
|
"id": "aggregate.json",
|
|
1148
1198
|
"status": "supported",
|
|
1149
|
-
"example": "SELECT JSON_ARRAYAGG(region) AS regions FROM rows",
|
|
1150
|
-
"notes": "Supports DISTINCT, 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."
|
|
1199
|
+
"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."
|
|
1151
1201
|
},
|
|
1152
1202
|
{
|
|
1153
1203
|
"id": "type.array",
|
|
@@ -1161,6 +1211,12 @@
|
|
|
1161
1211
|
"example": "SELECT TIME '12:00:00' AS at",
|
|
1162
1212
|
"notes": "A time of day without a time zone, returned as canonical text."
|
|
1163
1213
|
},
|
|
1214
|
+
{
|
|
1215
|
+
"id": "type.date",
|
|
1216
|
+
"status": "supported",
|
|
1217
|
+
"example": "SELECT CAST('2026-08-26' AS DATE) AS day",
|
|
1218
|
+
"notes": "A calendar date without a time zone, returned as canonical YYYY-MM-DD text."
|
|
1219
|
+
},
|
|
1164
1220
|
{
|
|
1165
1221
|
"id": "ddl.sequence",
|
|
1166
1222
|
"status": "supported",
|
|
@@ -1178,7 +1234,7 @@
|
|
|
1178
1234
|
"id": "ddl.create-view",
|
|
1179
1235
|
"status": "supported",
|
|
1180
1236
|
"example": "CREATE VIEW west AS SELECT region, amount FROM rows WHERE region = 'west'",
|
|
1181
|
-
"notes": "The catalog stores the query text and
|
|
1237
|
+
"notes": "The catalog stores the query text and inferred schema, so reads expand a view anywhere a table can be read, including inside a write scope. A view is never a write target. CREATE OR REPLACE redefines one; dependent views follow it and cycles are rejected on read. CREATE VIEW column-name lists are not supported."
|
|
1182
1238
|
},
|
|
1183
1239
|
{
|
|
1184
1240
|
"id": "ddl.drop-view",
|