@jarenjs/db 0.56.0 → 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 (68) hide show
  1. package/ARCHITECTURE.md +393 -56
  2. package/README.md +585 -53
  3. package/docs/HOSTS.md +269 -0
  4. package/docs/JOBS-FORMAT.md +293 -45
  5. package/docs/LIVE-FORMAT.md +122 -14
  6. package/docs/MIGRATION-FORMAT.md +142 -17
  7. package/docs/MODEL-FORMAT.md +744 -64
  8. package/package.json +21 -7
  9. package/schemas/jaren-model.draft-07.schema.json +224 -162
  10. package/schemas/jaren-model.schema.json +224 -162
  11. package/src/algebra.js +227 -9
  12. package/src/backup.js +161 -0
  13. package/src/cancellation.js +48 -0
  14. package/src/capture.js +218 -45
  15. package/src/cli.js +165 -59
  16. package/src/cursor.js +411 -0
  17. package/src/dag-job.js +154 -21
  18. package/src/ddl.js +102 -8
  19. package/src/dialect.js +267 -112
  20. package/src/dialects/expression-read.js +158 -0
  21. package/src/dialects/postgres.js +618 -0
  22. package/src/dialects/rtree-ddl.js +129 -0
  23. package/src/dialects/sqlite.js +243 -11
  24. package/src/document-files.js +311 -0
  25. package/src/document-steps.js +422 -0
  26. package/src/documents.js +335 -0
  27. package/src/driver.js +448 -61
  28. package/src/drivers/bun.js +37 -1
  29. package/src/drivers/indexeddb-snapshot.js +149 -0
  30. package/src/drivers/node-pool.js +11 -0
  31. package/src/drivers/node-worker-endpoint.js +105 -0
  32. package/src/drivers/node-worker.js +204 -0
  33. package/src/drivers/node.js +41 -7
  34. package/src/drivers/postgres.js +331 -0
  35. package/src/drivers/wasm-oo1.js +97 -0
  36. package/src/drivers/wasm-session.js +67 -0
  37. package/src/drivers/wasm.js +17 -83
  38. package/src/drivers/worker-pool.js +183 -0
  39. package/src/drivers/worker-protocol.js +79 -0
  40. package/src/drivers/worker-queue.js +60 -0
  41. package/src/emit.js +339 -48
  42. package/src/entity.js +20 -22
  43. package/src/errors.js +422 -19
  44. package/src/expression.js +284 -0
  45. package/src/graph.js +64 -8
  46. package/src/index.js +46 -17
  47. package/src/introspect.js +583 -0
  48. package/src/jobs.js +843 -107
  49. package/src/json-bytes.js +58 -0
  50. package/src/maintenance.js +175 -0
  51. package/src/migrate.js +248 -181
  52. package/src/model.js +68 -0
  53. package/src/plan.js +1119 -138
  54. package/src/pragmas.js +314 -0
  55. package/src/profile.js +151 -3
  56. package/src/query.js +1634 -323
  57. package/src/residual.js +17 -0
  58. package/src/series.js +12 -4
  59. package/src/store.js +1505 -264
  60. package/src/tracker.js +203 -29
  61. package/src/udf.js +88 -7
  62. package/types/index.d.ts +1097 -25
  63. package/types/node-pool.d.ts +28 -0
  64. package/types/node-worker.d.ts +54 -0
  65. package/types/node.d.ts +69 -2
  66. package/types/postgres.d.ts +46 -0
  67. package/types/typed.d.ts +25 -3
  68. package/types/wasm.d.ts +14 -0
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';
@@ -138,6 +139,39 @@ export function schemaTypeAt(schema, segments) {
138
139
  /** The scalar schema types a derived index cannot be declared over. */
139
140
  const SCALAR_TYPES = new Set(['string', 'integer', 'number', 'boolean']);
140
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
+
141
175
  /**
142
176
  * Whether a schema node types its value as `array` and nothing else —
143
177
  * `type: 'array'` or `type: ['array']`. A vector column over a member
@@ -402,7 +436,10 @@ export function planCollection(name, collection, dialect, options = undefined) {
402
436
  const mapping = options?.derived === 'stored' ? 'stored' : 'virtual';
403
437
  const rtreeCapable = options?.rtree !== false;
404
438
  const keyType = collection.identity === 'integer'
405
- ? 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'))
406
443
  : collection.identity === 'uuid'
407
444
  ? dialect.typeFor('string', 'key')
408
445
  : dialect.typeFor(
@@ -422,8 +459,45 @@ export function planCollection(name, collection, dialect, options = undefined) {
422
459
  const virtualTables = [];
423
460
  /** @type {Map<string, string>} */
424
461
  const physicalByKey = new Map();
462
+ /** @type {{ column: string, canonical: string, functions: string[] }[]} */
463
+ const expressions = [];
425
464
 
426
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
+ }
427
501
  const columns = [];
428
502
  let noBtree = false;
429
503
  for (let i = 0; i < index.paths.length; i++) {
@@ -448,9 +522,11 @@ export function planCollection(name, collection, dialect, options = undefined) {
448
522
  const known = columnByCanonical.has(canonical);
449
523
  const columnName = generatedColumnName(canonical, columnByCanonical, taken);
450
524
  if (!known) {
525
+ const schemaType = schemaTypeAt(collection.schema, segments);
451
526
  generated.push({
452
527
  name: columnName,
453
- type: dialect.typeFor(schemaTypeAt(collection.schema, segments), 'generated'),
528
+ type: dialect.typeFor(schemaType, 'generated'),
529
+ kind: columnKindFor(schemaType),
454
530
  pathText,
455
531
  canonical,
456
532
  });
@@ -511,6 +587,10 @@ export function planCollection(name, collection, dialect, options = undefined) {
511
587
  keyType,
512
588
  generated,
513
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,
514
594
  columnByCanonical,
515
595
  virtualTables,
516
596
  createSql,
@@ -518,6 +598,7 @@ export function planCollection(name, collection, dialect, options = undefined) {
518
598
  columns: [
519
599
  { name: KEY_COLUMN, type: keyType, generated: false },
520
600
  { name: DOC_COLUMN, type: dialect.docColumnType, generated: false },
601
+ ...identityColumnExpected(dialect),
521
602
  // a STORED derived column is an ordinary one: the flag is what
522
603
  // `pragma_table_xinfo` reports, and it is the difference a file
523
604
  // moved between the two physical mappings shows up as
@@ -639,6 +720,13 @@ export function comparableDeclaredSql(sql) {
639
720
  */
640
721
  function verifyDeclaredSql(connection, plan, disagree) {
641
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;
642
730
  const planned = new Map();
643
731
  // an R*Tree virtual table is NOT owned by the collection table —
644
732
  // `declaredSql` is scoped to `tbl_name`, and a virtual table's is
@@ -716,15 +804,19 @@ export function verifyShape(connection, plan, collection, docPath) {
716
804
  };
717
805
  return chain(connection.prepare(dialect.introspect.columns(plan.table)), (columnsStatement) =>
718
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;
719
811
  const actual = columnRows
720
812
  .map((row) => ({
721
813
  name: String(row.name),
722
- type: String(row.type).toUpperCase(),
814
+ type: comparableType(String(row.type)),
723
815
  generated: Number(row.hidden) !== 0,
724
816
  }))
725
817
  .sort((a, b) => (a.name < b.name ? -1 : 1));
726
818
  const expected = [...plan.expected.columns]
727
- .map((c) => ({ ...c, type: c.type.toUpperCase() }))
819
+ .map((c) => ({ ...c, type: comparableType(c.type) }))
728
820
  .sort((a, b) => (a.name < b.name ? -1 : 1));
729
821
  if (actual.length !== expected.length) {
730
822
  // name what is missing or extra: a count alone sends the reader
@@ -887,8 +979,9 @@ export function planEntity(name, entityMapping, entities, dialect) {
887
979
  targetColumn: column.references.column ?? null,
888
980
  })),
889
981
  expected: {
890
- columns: columns
891
- .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)]
892
985
  .sort((a, b) => (a.name < b.name ? -1 : 1)),
893
986
  indexes: expectedIndexes.sort((a, b) => (a.name < b.name ? -1 : 1)),
894
987
  },
@@ -936,8 +1029,9 @@ export function planJoinTable(tableName, join, entities, dialect) {
936
1029
  targetColumn: column.references.column ?? null,
937
1030
  })),
938
1031
  expected: {
939
- columns: columns
940
- .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)]
941
1035
  .sort((a, b) => (a.name < b.name ? -1 : 1)),
942
1036
  indexes: [],
943
1037
  },