@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.
Files changed (110) hide show
  1. package/ARCHITECTURE.md +420 -71
  2. package/README.md +711 -79
  3. package/docs/HOSTS.md +269 -0
  4. package/docs/JOBS-FORMAT.md +309 -45
  5. package/docs/LIVE-FORMAT.md +156 -19
  6. package/docs/MIGRATION-FORMAT.md +247 -40
  7. package/docs/MODEL-FORMAT.md +968 -86
  8. package/package.json +21 -8
  9. package/schemas/jaren-migration.draft-07.schema.json +73 -0
  10. package/schemas/jaren-migration.schema.json +73 -0
  11. package/schemas/jaren-model.draft-07.schema.json +224 -162
  12. package/schemas/jaren-model.schema.json +224 -162
  13. package/src/algebra.js +227 -9
  14. package/src/backup.js +161 -0
  15. package/src/cancellation.js +48 -0
  16. package/src/capture.js +255 -44
  17. package/src/cli.js +337 -50
  18. package/src/cursor.js +411 -0
  19. package/src/dag-job.js +154 -21
  20. package/src/ddl.js +125 -11
  21. package/src/dialect.js +267 -112
  22. package/src/dialects/expression-read.js +158 -0
  23. package/src/dialects/postgres.js +618 -0
  24. package/src/dialects/rtree-ddl.js +129 -0
  25. package/src/dialects/sqlite.js +245 -12
  26. package/src/document-files.js +311 -0
  27. package/src/document-steps.js +422 -0
  28. package/src/documents.js +335 -0
  29. package/src/driver.js +503 -69
  30. package/src/drivers/bun.js +37 -1
  31. package/src/drivers/indexeddb-snapshot.js +149 -0
  32. package/src/drivers/node-pool.js +11 -0
  33. package/src/drivers/node-worker-endpoint.js +105 -0
  34. package/src/drivers/node-worker.js +204 -0
  35. package/src/drivers/node.js +41 -7
  36. package/src/drivers/postgres.js +331 -0
  37. package/src/drivers/wasm-oo1.js +97 -0
  38. package/src/drivers/wasm-session.js +67 -0
  39. package/src/drivers/wasm.js +18 -83
  40. package/src/drivers/worker-pool.js +183 -0
  41. package/src/drivers/worker-protocol.js +79 -0
  42. package/src/drivers/worker-queue.js +60 -0
  43. package/src/emit-model.js +14 -0
  44. package/src/emit.js +349 -51
  45. package/src/entity.js +102 -59
  46. package/src/errors.js +430 -2
  47. package/src/expression.js +284 -0
  48. package/src/graph.js +64 -8
  49. package/src/index.js +48 -19
  50. package/src/introspect.js +583 -0
  51. package/src/jobs.js +870 -99
  52. package/src/json-bytes.js +58 -0
  53. package/src/live-time.js +12 -3
  54. package/src/live.js +11 -1
  55. package/src/maintenance.js +175 -0
  56. package/src/migrate.js +606 -333
  57. package/src/model.js +241 -8
  58. package/src/plan.js +1238 -160
  59. package/src/pragmas.js +314 -0
  60. package/src/profile.js +151 -3
  61. package/src/query.js +1748 -312
  62. package/src/residual.js +17 -0
  63. package/src/series.js +12 -4
  64. package/src/store.js +1672 -276
  65. package/src/tracker.js +367 -68
  66. package/src/udf.js +88 -7
  67. package/types/index.d.ts +1246 -32
  68. package/types/node-pool.d.ts +28 -0
  69. package/types/node-worker.d.ts +54 -0
  70. package/types/node.d.ts +72 -3
  71. package/types/postgres.d.ts +46 -0
  72. package/types/typed.d.ts +81 -3
  73. package/types/wasm.d.ts +21 -0
  74. package/dist/types/algebra.d.ts +0 -230
  75. package/dist/types/app.d.ts +0 -49
  76. package/dist/types/capture.d.ts +0 -85
  77. package/dist/types/cli.d.ts +0 -2
  78. package/dist/types/dag-job.d.ts +0 -40
  79. package/dist/types/ddl.d.ts +0 -229
  80. package/dist/types/derive.d.ts +0 -250
  81. package/dist/types/dialect.d.ts +0 -154
  82. package/dist/types/dialects/sqlite.d.ts +0 -9
  83. package/dist/types/driver.d.ts +0 -110
  84. package/dist/types/drivers/bun.d.ts +0 -47
  85. package/dist/types/drivers/node.d.ts +0 -37
  86. package/dist/types/drivers/wasm.d.ts +0 -65
  87. package/dist/types/emit-model.d.ts +0 -44
  88. package/dist/types/emit.d.ts +0 -75
  89. package/dist/types/entity.d.ts +0 -23
  90. package/dist/types/errors.d.ts +0 -170
  91. package/dist/types/graph.d.ts +0 -28
  92. package/dist/types/index.d.ts +0 -37
  93. package/dist/types/jobs.d.ts +0 -140
  94. package/dist/types/knn.d.ts +0 -69
  95. package/dist/types/live-time.d.ts +0 -141
  96. package/dist/types/live.d.ts +0 -64
  97. package/dist/types/migrate.d.ts +0 -170
  98. package/dist/types/model.d.ts +0 -36
  99. package/dist/types/patch-sql.d.ts +0 -37
  100. package/dist/types/plan.d.ts +0 -142
  101. package/dist/types/profile.d.ts +0 -80
  102. package/dist/types/query.d.ts +0 -112
  103. package/dist/types/residual.d.ts +0 -64
  104. package/dist/types/series.d.ts +0 -227
  105. package/dist/types/store.d.ts +0 -60
  106. package/dist/types/tracker.d.ts +0 -43
  107. package/dist/types/typed.d.ts +0 -15
  108. package/dist/types/types.d.ts +0 -26
  109. package/dist/types/udf.d.ts +0 -75
  110. 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
- const canonical = segments
74
- .map((s) => ('name' in s ? `.${s.name}` : `[${s.index}]`))
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
- ? dialect.typeFor('integer', 'key')
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(schemaTypeAt(collection.schema, segments), 'generated'),
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).toUpperCase(),
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.toUpperCase() }))
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
  },