@jarenjs/db 0.73.0 → 0.83.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.
Files changed (53) hide show
  1. package/ARCHITECTURE.md +70 -7
  2. package/README.md +69 -6
  3. package/docs/HOSTS.md +17 -0
  4. package/docs/JOBS-FORMAT.md +26 -0
  5. package/docs/LIVE-FORMAT.md +52 -13
  6. package/docs/MIGRATION-FORMAT.md +34 -0
  7. package/docs/MODEL-FORMAT.md +163 -15
  8. package/docs/NATIVE-PLANS.md +111 -0
  9. package/docs/REPLICATION-FORMAT.md +19 -13
  10. package/docs/SEARCH.md +55 -0
  11. package/package.json +8 -4
  12. package/schemas/jaren-migration.draft-07.schema.json +54 -5
  13. package/schemas/jaren-migration.schema.json +49 -0
  14. package/schemas/jaren-model.authoring.schema.json +360 -0
  15. package/schemas/jaren-model.draft-07.schema.json +128 -0
  16. package/schemas/jaren-model.schema.json +128 -0
  17. package/src/algebra.js +26 -4
  18. package/src/backup.js +12 -7
  19. package/src/cursor.js +27 -4
  20. package/src/dag-job.js +2 -1
  21. package/src/ddl.js +13 -0
  22. package/src/derive.js +14 -3
  23. package/src/dialect.js +12 -0
  24. package/src/dialects/check-read.js +151 -0
  25. package/src/dialects/invariant-sql.js +117 -0
  26. package/src/dialects/postgres.js +28 -4
  27. package/src/dialects/sqlite.js +23 -3
  28. package/src/driver.js +1 -0
  29. package/src/drivers/bun.js +22 -4
  30. package/src/emit.js +133 -25
  31. package/src/entity.js +98 -41
  32. package/src/errors.js +8 -0
  33. package/src/graph.js +8 -1
  34. package/src/index.js +3 -0
  35. package/src/introspect.js +81 -12
  36. package/src/invariants.js +45 -0
  37. package/src/jobs.js +39 -6
  38. package/src/live-nested.js +27 -10
  39. package/src/live.js +51 -136
  40. package/src/migrate.js +136 -22
  41. package/src/model.js +12 -0
  42. package/src/mutation.js +165 -0
  43. package/src/physical.js +147 -0
  44. package/src/plan.js +275 -64
  45. package/src/query.js +175 -78
  46. package/src/search.js +144 -0
  47. package/src/sql.js +60 -0
  48. package/src/store.js +49 -13
  49. package/src/tracker.js +63 -39
  50. package/src/window.js +1 -0
  51. package/types/index.d.ts +59 -4
  52. package/types/search.d.ts +20 -0
  53. package/types/typed.d.ts +1 -0
package/src/window.js CHANGED
@@ -100,6 +100,7 @@ export function createSortedWindow(terms, limit) {
100
100
  return {
101
101
  compare,
102
102
  size: () => entries.length,
103
+ clear: () => { entries.length = 0; byToken.clear(); },
103
104
  /** The visible slice: the first `limit` entries (all, unbounded). */
104
105
  visible: () => (limit === null ? [...entries] : entries.slice(0, limit)),
105
106
  /**
package/types/index.d.ts CHANGED
@@ -291,6 +291,8 @@ export interface LoadContinuation {
291
291
  * write may change; `'live'` (the default) reports the truth in
292
292
  * `snapshot`. */
293
293
  export interface PageOptions<C = LoadContinuation> extends EntityCursorOptions {
294
+ /** False admits no lookahead row; a full page reports hasMore: null. */
295
+ lookahead?: boolean;
294
296
  limit?: number;
295
297
  after?: C;
296
298
  maxBytes?: number | null;
@@ -305,8 +307,11 @@ export interface PageOptions<C = LoadContinuation> extends EntityCursorOptions {
305
307
  export interface Page<T, C = LoadContinuation> {
306
308
  readonly items: T[];
307
309
  readonly continuation: C | null;
308
- readonly hasMore: boolean;
310
+ readonly hasMore: boolean | null;
309
311
  readonly snapshot: boolean;
312
+ /** Present with lookahead:false; includes a row consumed at the byte boundary.
313
+ * Bytes count the serialized payloads consumed, excluding array punctuation. */
314
+ readonly work?: { readonly rows: number; readonly bytes: number };
310
315
  }
311
316
 
312
317
  export interface LoadExplanation {
@@ -594,12 +599,26 @@ export interface UntrackedReads<T = unknown> {
594
599
  load(spec?: LoadSpec): Promise<T[]>;
595
600
  }
596
601
 
602
+ /** Closed native mutation forms over declared SQLite column layouts. */
603
+ export type EntityMutation = {
604
+ returning?: readonly string[]; maxRows?: number; maxBytes?: number;
605
+ } & ({ op: 'update'; key: EntityKeyArg; expectedRevision?: number; set: Readonly<Record<string, unknown>> }
606
+ | { op: 'upsert'; values: Readonly<Record<string, unknown>>; conflict: readonly string[]; update: readonly string[] }
607
+ | { op: 'insert-select'; source: string; where?: unknown; select: Readonly<Record<string, string | { $literal: unknown }>>;
608
+ conflict: readonly string[]; onConflict: 'nothing' });
609
+ export interface MutationResult {
610
+ readonly mode: 'native'; readonly affected: number; readonly rows: ReadonlyArray<Readonly<Record<string, unknown>>>;
611
+ readonly admitted: { readonly statements: number; readonly rows: number; readonly bytes: number };
612
+ }
613
+
597
614
  export interface EntitySet<T = unknown, I = unknown> {
598
615
  /** The provider phantom: a chain over this set infers its item type. */
599
616
  readonly __item?: T;
600
617
  create(doc: I): Promise<Readonly<T>>;
601
618
  get(key: EntityKeyArg): Promise<Readonly<T> | undefined>;
602
619
  update(key: EntityKeyArg, changes: Partial<T>): Promise<Readonly<T>>;
620
+ /** One bounded native SQLite column mutation; unsupported shapes refuse JD0038. */
621
+ mutate(document: EntityMutation): Promise<MutationResult>;
603
622
  delete(key: EntityKeyArg): Promise<boolean>;
604
623
  load(spec?: LoadSpec): Promise<ReadonlyArray<Readonly<T>>>;
605
624
  /** The graph cursor: one root graph per pull, its includes attached
@@ -695,7 +714,7 @@ export interface SyncStore {
695
714
  * the consumer's words; the handle's writes take it and reads answer it. */
696
715
  collection<T = unknown>(name: string): SyncCollection<T>;
697
716
  entity(name: string): SyncEntitySet;
698
- transaction<R>(fn: (store: TransactionStore) => R): R;
717
+ transaction<R>(fn: (store: TransactionStore) => R, options?: { mode?: 'deferred' | 'immediate' }): R;
699
718
  execute?<R = unknown>(document: unknown, options?: ExecuteOptions): SequenceResult<R>;
700
719
  explain?(document: unknown, options?: ExecuteOptions): unknown;
701
720
  /** The entity roots this store-level provider serves (present with
@@ -813,7 +832,7 @@ export interface TransactionScopeOptions {
813
832
  * upgrade `SQLITE_BUSY` the busy handler cannot retry — what a claim
814
833
  * needs under concurrent writers; `'deferred'` (the default) is the
815
834
  * savepoint as always. A nested `tx.transaction()` is a savepoint
816
- * whichever mode the root chose; the synchronous twin has no mode. */
835
+ * whichever mode the root chose; the root synchronous twin accepts the same mode. */
817
836
  mode?: 'deferred' | 'immediate';
818
837
  }
819
838
 
@@ -845,6 +864,7 @@ export interface SyncSavepointController {
845
864
  /** The synchronous surface a transaction view carries: the store's,
846
865
  * plus the transaction-only savepoint group. */
847
866
  export interface TransactionSyncStore extends SyncStore {
867
+ readonly sql: TrustedSyncSql;
848
868
  readonly savepoints: SyncSavepointController;
849
869
  }
850
870
 
@@ -871,6 +891,7 @@ export interface TransactionStore extends Omit<Store,
871
891
  | 'jobs' | 'replication'> {
872
892
  /** The transactional outbox (JOBS-FORMAT §3): no administration here —
873
893
  * an admin operation is a root call. */
894
+ readonly sql: TrustedSql;
874
895
  readonly jobs?: JobsApi;
875
896
  transaction<R>(fn: (store: TransactionStore) => R | Promise<R>): Promise<Awaited<R>>;
876
897
  /** Named partial rollback over the transaction's one savepoint stack
@@ -978,7 +999,7 @@ export interface LiveEventTime {
978
999
  }
979
1000
 
980
1001
  export interface LiveMode {
981
- readonly strategy: 'rows' | 'window' | 'accumulator' | 'group'
1002
+ readonly strategy: 'rows' | 'window' | 'accumulator' | 'group' | 'distinct'
982
1003
  | 'bucket' | 'rolling' | 'join' | 'graph' | 'nested-group' | 'rerun';
983
1004
  readonly mode: 'incremental' | 'rerun';
984
1005
  /** Present exactly when the strategy is 'rerun': the named reason. */
@@ -1051,6 +1072,8 @@ export interface LiveBounds {
1051
1072
  }
1052
1073
 
1053
1074
  export interface OpenStoreOptions {
1075
+ /** Verify existing objects and create no schema or infrastructure. */
1076
+ adopt?: boolean;
1054
1077
  replication?: ReplicationOptions;
1055
1078
  driver: Driver;
1056
1079
  path?: string;
@@ -1775,6 +1798,8 @@ export interface JobWorker {
1775
1798
  }
1776
1799
 
1777
1800
  export interface JobWorkerOptions {
1801
+ /** Admit only when current durable effect policy returns true. */
1802
+ effectSafety?: (job: ClaimedJob, context: { lease(): JobLease; signal: AbortSignal }) => boolean | Promise<boolean>;
1778
1803
  /**
1779
1804
  * `checkpoints` is bound to THIS attempt and follows its current
1780
1805
  * lease, so a renewal does not strand it. `signal` aborts for either
@@ -1784,6 +1809,9 @@ export interface JobWorkerOptions {
1784
1809
  */
1785
1810
  handlers: Record<string, (payload: unknown, context: {
1786
1811
  job: ClaimedJob;
1812
+ lease(): JobLease;
1813
+ /** Pause as cancelled, permitting explicit requeue for reconciliation. */
1814
+ pause(): Promise<boolean>;
1787
1815
  checkpoints: { load(runId: string): unknown;
1788
1816
  save(runId: string, nodeId: string, value: unknown): unknown;
1789
1817
  complete(runId: string, result: unknown): unknown };
@@ -1824,6 +1852,8 @@ export interface JobsApi {
1824
1852
  * expired), never a silent `false`.
1825
1853
  */
1826
1854
  renew(lease: JobLease, options?: { leaseMs?: number }): Promise<JobLease>;
1855
+ /** Check the existing token/expiry fence without writing. */
1856
+ assertLease(lease: JobLease): Promise<boolean>;
1827
1857
  /** Settle the attempt this lease holds. `true`, or one of the three
1828
1858
  * coded refusals above — a caller that cannot tell "already done"
1829
1859
  * from "you are stale" guesses, and guesses wrong. */
@@ -1899,6 +1929,7 @@ export interface JobsOptions {
1899
1929
  }
1900
1930
 
1901
1931
  export declare function createDagJobRunner(store: Store, options: {
1932
+ effectSafety?: JobWorkerOptions['effectSafety'];
1902
1933
  compileDag: Function;
1903
1934
  documents: Record<string, unknown>;
1904
1935
  tasks?: Record<string, Function | { run: Function; version?: string; taskVersions?: Record<string, string> }>;
@@ -1982,3 +2013,27 @@ export interface ReplicationSnapshot {
1982
2013
  receipts: ReplicationEnvelope[];
1983
2014
  }
1984
2015
  export declare function normalizeReplicationSnapshot(document: unknown): ReplicationSnapshot;
2016
+
2017
+ /** Trusted prepared SQL; statements belong to one transaction scope. */
2018
+ export interface TrustedSql {
2019
+ prepare(sql: string, options: { access: 'read' | 'write'; affects?: readonly string[] }): {
2020
+ run(params?: readonly unknown[]): unknown;
2021
+ get(params?: readonly unknown[]): Record<string, any> | undefined | Promise<Record<string, any> | undefined>;
2022
+ all(params?: readonly unknown[]): Record<string, any>[] | Promise<Record<string, any>[]>;
2023
+ close(): void;
2024
+ };
2025
+ }
2026
+ export interface TrustedSyncSql {
2027
+ prepare(sql: string, options: { access: 'read' | 'write'; affects?: readonly string[] }): {
2028
+ run(params?: readonly unknown[]): unknown;
2029
+ get(params?: readonly unknown[]): Record<string, any> | undefined;
2030
+ all(params?: readonly unknown[]): Record<string, any>[];
2031
+ close(): void;
2032
+ };
2033
+ }
2034
+ export declare function planInvariants(model: unknown, options: { dialect: Dialect }): {
2035
+ type: 'trigger'; name: string; owner: string; rule: string; sql: string;
2036
+ }[];
2037
+ export declare function planPhysicalMigration(connection: unknown, fromModel: unknown, toModel: unknown,
2038
+ options: { id: string; steps: readonly unknown[]; dispositions: Readonly<Record<string, 'preserve' | 'replace' | 'drop'>>;
2039
+ assertions?: readonly { sql: string; params?: readonly unknown[]; expected: readonly unknown[] }[] }): unknown;
@@ -0,0 +1,20 @@
1
+ import type { Store } from './index.js';
2
+ import type { LexicalDefinition } from '@jarenjs/core/search';
3
+
4
+ export interface SearchStorage {
5
+ load(id: string): Promise<string | null>;
6
+ save(id: string, payload: string): Promise<{ changes: number }>;
7
+ }
8
+ export declare function createDbSearchStorage(store: Store, collection: string, options?: { maxBytes?: number }): SearchStorage;
9
+ export interface DbSearch {
10
+ readonly sourceRevision: string;
11
+ refresh(): Promise<any>;
12
+ search(text: string, request?: Record<string, unknown>): Promise<any>;
13
+ row(id: string, revision: string): any;
14
+ subscribe(observer: (event: any) => void): () => void;
15
+ explain(): { mode: string; nativeFTS: boolean; reason: string; maxRows: number; maxBytes: number; capture: string; externalChanges: string };
16
+ stats(): any;
17
+ dispose(): Promise<void>;
18
+ }
19
+ export declare function createDbSearch(store: Store, entity: string, definition: LexicalDefinition,
20
+ options: { source: string; maxRows?: number; maxBytes?: number; storage?: SearchStorage; snapshotKey?: string }): Promise<DbSearch>;
package/types/typed.d.ts CHANGED
@@ -101,6 +101,7 @@ export interface TypedUntrackedReads<E extends MetaMap<E>, M extends EntityMeta>
101
101
  }
102
102
 
103
103
  export interface TypedEntitySet<E extends MetaMap<E>, M extends EntityMeta> {
104
+ mutate(document: import('./index.js').EntityMutation): Promise<import('./index.js').MutationResult>;
104
105
  /** The provider phantom: `from(typed.entity('User'))` infers `User`
105
106
  * without a cast. */
106
107
  readonly __item?: M['doc'];