@jarenjs/db 0.49.2 → 0.66.1
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/ARCHITECTURE.md +420 -71
- package/README.md +711 -79
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +309 -45
- package/docs/LIVE-FORMAT.md +156 -19
- package/docs/MIGRATION-FORMAT.md +247 -40
- package/docs/MODEL-FORMAT.md +968 -86
- package/package.json +21 -8
- package/schemas/jaren-migration.draft-07.schema.json +73 -0
- package/schemas/jaren-migration.schema.json +73 -0
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +255 -44
- package/src/cli.js +337 -50
- package/src/cursor.js +411 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +125 -11
- package/src/dialect.js +267 -112
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +245 -12
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +503 -69
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +18 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit-model.js +14 -0
- package/src/emit.js +349 -51
- package/src/entity.js +102 -59
- package/src/errors.js +430 -2
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -19
- package/src/introspect.js +583 -0
- package/src/jobs.js +870 -99
- package/src/json-bytes.js +58 -0
- package/src/live-time.js +12 -3
- package/src/live.js +11 -1
- package/src/maintenance.js +175 -0
- package/src/migrate.js +606 -333
- package/src/model.js +241 -8
- package/src/plan.js +1238 -160
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1748 -312
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1672 -276
- package/src/tracker.js +367 -68
- package/src/udf.js +88 -7
- package/types/index.d.ts +1246 -32
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +72 -3
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +81 -3
- package/types/wasm.d.ts +21 -0
- package/dist/types/algebra.d.ts +0 -230
- package/dist/types/app.d.ts +0 -49
- package/dist/types/capture.d.ts +0 -85
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/dag-job.d.ts +0 -40
- package/dist/types/ddl.d.ts +0 -229
- package/dist/types/derive.d.ts +0 -250
- package/dist/types/dialect.d.ts +0 -154
- package/dist/types/dialects/sqlite.d.ts +0 -9
- package/dist/types/driver.d.ts +0 -110
- package/dist/types/drivers/bun.d.ts +0 -47
- package/dist/types/drivers/node.d.ts +0 -37
- package/dist/types/drivers/wasm.d.ts +0 -65
- package/dist/types/emit-model.d.ts +0 -44
- package/dist/types/emit.d.ts +0 -75
- package/dist/types/entity.d.ts +0 -23
- package/dist/types/errors.d.ts +0 -170
- package/dist/types/graph.d.ts +0 -28
- package/dist/types/index.d.ts +0 -37
- package/dist/types/jobs.d.ts +0 -140
- package/dist/types/knn.d.ts +0 -69
- package/dist/types/live-time.d.ts +0 -141
- package/dist/types/live.d.ts +0 -64
- package/dist/types/migrate.d.ts +0 -170
- package/dist/types/model.d.ts +0 -36
- package/dist/types/patch-sql.d.ts +0 -37
- package/dist/types/plan.d.ts +0 -142
- package/dist/types/profile.d.ts +0 -80
- package/dist/types/query.d.ts +0 -112
- package/dist/types/residual.d.ts +0 -64
- package/dist/types/series.d.ts +0 -227
- package/dist/types/store.d.ts +0 -60
- package/dist/types/tracker.d.ts +0 -43
- package/dist/types/typed.d.ts +0 -15
- package/dist/types/types.d.ts +0 -26
- package/dist/types/udf.d.ts +0 -75
- package/dist/types/window.d.ts +0 -52
package/src/ddl.js
CHANGED
|
@@ -18,6 +18,7 @@ import { analyzeQuery } from '@jarenjs/json/query';
|
|
|
18
18
|
import { DbCompileError } from './errors.js';
|
|
19
19
|
import { chain } from './driver.js';
|
|
20
20
|
import { BBOX_COMPONENTS, BBOX_INDEX_ORDER, derivedMappingFor } from './derive.js';
|
|
21
|
+
import { expressionSql, expressionStem, expressionFunctions } from './expression.js';
|
|
21
22
|
|
|
22
23
|
/** The fixed physical column names of the 0.1 mapping. */
|
|
23
24
|
export const KEY_COLUMN = 'key';
|
|
@@ -70,12 +71,32 @@ export function compileIndexPath(expression, docPath) {
|
|
|
70
71
|
`the index path '${expression}' selects the whole document — index a member`,
|
|
71
72
|
docPath);
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
return { segments, canonical: canonicalOf(segments) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The canonical spelling of a member path — the key every generated
|
|
79
|
+
* column and every promoted reference is matched by. INJECTIVE: a
|
|
80
|
+
* member literally named `a.b` and the nested path `a` → `b` used to
|
|
81
|
+
* spell the same `.a.b`, so an index over one silently served the
|
|
82
|
+
* other and a filter on the flat member answered from the nested
|
|
83
|
+
* value. Names are JSON-quoted, so no two paths share a spelling; the
|
|
84
|
+
* generated column STEM strips the quotes and keeps its old form.
|
|
85
|
+
* @param {import('./dialect.js').JsonPathSegment[]} segments
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
export function canonicalOf(segments) {
|
|
89
|
+
return segments
|
|
90
|
+
.map((s) => ('name' in s
|
|
91
|
+
? (IDENTIFIER.test(s.name) ? `.${s.name}` : `.${JSON.stringify(s.name)}`)
|
|
92
|
+
: `[${s.index}]`))
|
|
75
93
|
.join('');
|
|
76
|
-
return { segments, canonical };
|
|
77
94
|
}
|
|
78
95
|
|
|
96
|
+
/** A member name that spells itself: anything else is JSON-quoted in
|
|
97
|
+
* the canonical, so `.a.b` (nested) and `."a.b"` (one member) differ. */
|
|
98
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
99
|
+
|
|
79
100
|
/**
|
|
80
101
|
* The schema subschema at a segment path, walked structurally through
|
|
81
102
|
* `properties` / `items` / `prefixItems`. The collection's schema is
|
|
@@ -118,6 +139,39 @@ export function schemaTypeAt(schema, segments) {
|
|
|
118
139
|
/** The scalar schema types a derived index cannot be declared over. */
|
|
119
140
|
const SCALAR_TYPES = new Set(['string', 'integer', 'number', 'boolean']);
|
|
120
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The comparison KIND a declared schema type implies — what a column
|
|
144
|
+
* over that member holds, and therefore how its expression has to read
|
|
145
|
+
* the member out of the document. On a dynamically typed engine the
|
|
146
|
+
* kind changes nothing; on one whose columns carry a real SQL type it
|
|
147
|
+
* is the difference between a `text` column and a type error.
|
|
148
|
+
* @param {string | undefined} schemaType
|
|
149
|
+
* @returns {'text' | 'number' | 'boolean' | undefined}
|
|
150
|
+
*/
|
|
151
|
+
export function columnKindFor(schemaType) {
|
|
152
|
+
switch (schemaType) {
|
|
153
|
+
case 'string': return 'text';
|
|
154
|
+
case 'integer': case 'number': return 'number';
|
|
155
|
+
case 'boolean': return 'boolean';
|
|
156
|
+
default: return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The shape row for the identity column a dialect adds to every table
|
|
162
|
+
* it creates, or none. It is an ORDINARY column as far as the catalog
|
|
163
|
+
* is concerned — the engine fills it, but nothing generates it from
|
|
164
|
+
* another column — so the drift check sees it exactly as it sees the
|
|
165
|
+
* key and the document.
|
|
166
|
+
* @param {any} dialect
|
|
167
|
+
* @returns {{ name: string, type: string, generated: boolean }[]}
|
|
168
|
+
*/
|
|
169
|
+
function identityColumnExpected(dialect) {
|
|
170
|
+
return dialect.identityColumn === undefined
|
|
171
|
+
? []
|
|
172
|
+
: [{ name: dialect.identityColumn.name, type: dialect.identityColumn.type, generated: false }];
|
|
173
|
+
}
|
|
174
|
+
|
|
121
175
|
/**
|
|
122
176
|
* Whether a schema node types its value as `array` and nothing else —
|
|
123
177
|
* `type: 'array'` or `type: ['array']`. A vector column over a member
|
|
@@ -382,7 +436,10 @@ export function planCollection(name, collection, dialect, options = undefined) {
|
|
|
382
436
|
const mapping = options?.derived === 'stored' ? 'stored' : 'virtual';
|
|
383
437
|
const rtreeCapable = options?.rtree !== false;
|
|
384
438
|
const keyType = collection.identity === 'integer'
|
|
385
|
-
|
|
439
|
+
// a DATABASE-allocated key: on an engine whose auto-allocation is a
|
|
440
|
+
// column property rather than a consequence of the integer type,
|
|
441
|
+
// that property IS the declared type
|
|
442
|
+
? (dialect.autoKeyType ?? dialect.typeFor('integer', 'key'))
|
|
386
443
|
: collection.identity === 'uuid'
|
|
387
444
|
? dialect.typeFor('string', 'key')
|
|
388
445
|
: dialect.typeFor(
|
|
@@ -402,8 +459,45 @@ export function planCollection(name, collection, dialect, options = undefined) {
|
|
|
402
459
|
const virtualTables = [];
|
|
403
460
|
/** @type {Map<string, string>} */
|
|
404
461
|
const physicalByKey = new Map();
|
|
462
|
+
/** @type {{ column: string, canonical: string, functions: string[] }[]} */
|
|
463
|
+
const expressions = [];
|
|
405
464
|
|
|
406
465
|
for (const index of collection.indexes) {
|
|
466
|
+
// an EXPRESSION index: one column over the declared computation,
|
|
467
|
+
// shared by every index that declares the same canonical expression
|
|
468
|
+
if (index.expression !== undefined) {
|
|
469
|
+
const known = columnByCanonical.has(index.canonical);
|
|
470
|
+
const columnName = generatedColumnName(expressionStem(index.expression),
|
|
471
|
+
columnByCanonical, taken, { key: index.canonical, suffix: 'x' });
|
|
472
|
+
if (!known) {
|
|
473
|
+
const sql = expressionSql(index.expression, dialect, {
|
|
474
|
+
docColumnSql: dialect.quoteIdentifier(DOC_COLUMN),
|
|
475
|
+
declarations: options?.expressions ?? {},
|
|
476
|
+
registered: options?.registered !== false,
|
|
477
|
+
docPath: `${index.docPath}/expression`,
|
|
478
|
+
segmentsOf: (member) =>
|
|
479
|
+
compileIndexPath(member, `${index.docPath}/expression`).segments,
|
|
480
|
+
});
|
|
481
|
+
generated.push({
|
|
482
|
+
name: columnName,
|
|
483
|
+
// an expression's value is TEXT: one declared function, one
|
|
484
|
+
// spelling of its answer, on every engine that computes it
|
|
485
|
+
type: dialect.typeFor('string', 'generated'),
|
|
486
|
+
kind: 'text',
|
|
487
|
+
pathText: null,
|
|
488
|
+
expression: sql,
|
|
489
|
+
canonical: index.canonical,
|
|
490
|
+
});
|
|
491
|
+
expressions.push({ column: columnName, canonical: index.canonical,
|
|
492
|
+
functions: expressionFunctions(index.expression) });
|
|
493
|
+
}
|
|
494
|
+
indexes.push({
|
|
495
|
+
name: `${name}_${index.name}`,
|
|
496
|
+
unique: index.unique,
|
|
497
|
+
columns: [columnName],
|
|
498
|
+
});
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
407
501
|
const columns = [];
|
|
408
502
|
let noBtree = false;
|
|
409
503
|
for (let i = 0; i < index.paths.length; i++) {
|
|
@@ -428,9 +522,11 @@ export function planCollection(name, collection, dialect, options = undefined) {
|
|
|
428
522
|
const known = columnByCanonical.has(canonical);
|
|
429
523
|
const columnName = generatedColumnName(canonical, columnByCanonical, taken);
|
|
430
524
|
if (!known) {
|
|
525
|
+
const schemaType = schemaTypeAt(collection.schema, segments);
|
|
431
526
|
generated.push({
|
|
432
527
|
name: columnName,
|
|
433
|
-
type: dialect.typeFor(
|
|
528
|
+
type: dialect.typeFor(schemaType, 'generated'),
|
|
529
|
+
kind: columnKindFor(schemaType),
|
|
434
530
|
pathText,
|
|
435
531
|
canonical,
|
|
436
532
|
});
|
|
@@ -491,6 +587,10 @@ export function planCollection(name, collection, dialect, options = undefined) {
|
|
|
491
587
|
keyType,
|
|
492
588
|
generated,
|
|
493
589
|
derived,
|
|
590
|
+
/** The declared-expression columns, with the functions each calls:
|
|
591
|
+
* what a store registers before it can so much as SELECT from the
|
|
592
|
+
* table it created. */
|
|
593
|
+
expressions,
|
|
494
594
|
columnByCanonical,
|
|
495
595
|
virtualTables,
|
|
496
596
|
createSql,
|
|
@@ -498,6 +598,7 @@ export function planCollection(name, collection, dialect, options = undefined) {
|
|
|
498
598
|
columns: [
|
|
499
599
|
{ name: KEY_COLUMN, type: keyType, generated: false },
|
|
500
600
|
{ name: DOC_COLUMN, type: dialect.docColumnType, generated: false },
|
|
601
|
+
...identityColumnExpected(dialect),
|
|
501
602
|
// a STORED derived column is an ordinary one: the flag is what
|
|
502
603
|
// `pragma_table_xinfo` reports, and it is the difference a file
|
|
503
604
|
// moved between the two physical mappings shows up as
|
|
@@ -619,6 +720,13 @@ export function comparableDeclaredSql(sql) {
|
|
|
619
720
|
*/
|
|
620
721
|
function verifyDeclaredSql(connection, plan, disagree) {
|
|
621
722
|
const dialect = connection.dialect;
|
|
723
|
+
// An engine that does not keep each object's CREATE text has nothing
|
|
724
|
+
// to compare: the structural check above (columns, their types and
|
|
725
|
+
// generatedness, the indexes and their covered columns in order, and
|
|
726
|
+
// for an entity its foreign-key tuples) is the whole of the drift
|
|
727
|
+
// check there, and `capabilities.declaredSqlText` is what says so
|
|
728
|
+
// rather than a silent pass.
|
|
729
|
+
if (dialect.capabilities.declaredSqlText !== true) return null;
|
|
622
730
|
const planned = new Map();
|
|
623
731
|
// an R*Tree virtual table is NOT owned by the collection table —
|
|
624
732
|
// `declaredSql` is scoped to `tbl_name`, and a virtual table's is
|
|
@@ -696,15 +804,19 @@ export function verifyShape(connection, plan, collection, docPath) {
|
|
|
696
804
|
};
|
|
697
805
|
return chain(connection.prepare(dialect.introspect.columns(plan.table)), (columnsStatement) =>
|
|
698
806
|
chain(columnsStatement.all([]), (columnRows) => {
|
|
807
|
+
// both sides through the dialect's own reduction, so a declared
|
|
808
|
+
// type the catalog reports differently (an auto-key's allocation
|
|
809
|
+
// clause, a width the engine normalizes) compares as itself
|
|
810
|
+
const comparableType = dialect.comparableColumnType;
|
|
699
811
|
const actual = columnRows
|
|
700
812
|
.map((row) => ({
|
|
701
813
|
name: String(row.name),
|
|
702
|
-
type: String(row.type)
|
|
814
|
+
type: comparableType(String(row.type)),
|
|
703
815
|
generated: Number(row.hidden) !== 0,
|
|
704
816
|
}))
|
|
705
817
|
.sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
706
818
|
const expected = [...plan.expected.columns]
|
|
707
|
-
.map((c) => ({ ...c, type: c.type
|
|
819
|
+
.map((c) => ({ ...c, type: comparableType(c.type) }))
|
|
708
820
|
.sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
709
821
|
if (actual.length !== expected.length) {
|
|
710
822
|
// name what is missing or extra: a count alone sends the reader
|
|
@@ -867,8 +979,9 @@ export function planEntity(name, entityMapping, entities, dialect) {
|
|
|
867
979
|
targetColumn: column.references.column ?? null,
|
|
868
980
|
})),
|
|
869
981
|
expected: {
|
|
870
|
-
columns: columns
|
|
871
|
-
.map((column) => ({ name: column.name, type: column.type, generated: false }))
|
|
982
|
+
columns: [...columns
|
|
983
|
+
.map((column) => ({ name: column.name, type: column.type, generated: false })),
|
|
984
|
+
...identityColumnExpected(dialect)]
|
|
872
985
|
.sort((a, b) => (a.name < b.name ? -1 : 1)),
|
|
873
986
|
indexes: expectedIndexes.sort((a, b) => (a.name < b.name ? -1 : 1)),
|
|
874
987
|
},
|
|
@@ -916,8 +1029,9 @@ export function planJoinTable(tableName, join, entities, dialect) {
|
|
|
916
1029
|
targetColumn: column.references.column ?? null,
|
|
917
1030
|
})),
|
|
918
1031
|
expected: {
|
|
919
|
-
columns: columns
|
|
920
|
-
.map((column) => ({ name: column.name, type: column.type, generated: false }))
|
|
1032
|
+
columns: [...columns
|
|
1033
|
+
.map((column) => ({ name: column.name, type: column.type, generated: false })),
|
|
1034
|
+
...identityColumnExpected(dialect)]
|
|
921
1035
|
.sort((a, b) => (a.name < b.name ? -1 : 1)),
|
|
922
1036
|
indexes: [],
|
|
923
1037
|
},
|