@jarenjs/db 0.84.3 → 0.86.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.
Files changed (51) hide show
  1. package/ARCHITECTURE.md +27 -3
  2. package/README.md +35 -14
  3. package/docs/HOSTS.md +131 -4
  4. package/docs/MODEL-FORMAT.md +15 -4
  5. package/docs/NATIVE-PLANS.md +24 -7
  6. package/docs/SQLITE-RELATIONAL.md +190 -0
  7. package/package.json +24 -4
  8. package/schemas/jaren-model.authoring.schema.json +157 -0
  9. package/schemas/jaren-model.draft-07.schema.json +157 -0
  10. package/schemas/jaren-model.schema.json +157 -0
  11. package/src/capture.js +4 -2
  12. package/src/ddl.js +8 -3
  13. package/src/dialects/sqlite-relational.js +314 -0
  14. package/src/dialects/sqlite-schema.js +142 -0
  15. package/src/dialects/sqlite.js +17 -0
  16. package/src/driver.js +3 -0
  17. package/src/drivers/bun.js +26 -15
  18. package/src/drivers/node-process-endpoint.js +13 -0
  19. package/src/drivers/node-process.js +177 -0
  20. package/src/drivers/node-worker-endpoint.js +3 -103
  21. package/src/drivers/node-worker.js +6 -178
  22. package/src/drivers/node.js +3 -0
  23. package/src/drivers/snapshot.js +49 -0
  24. package/src/drivers/sqlite-endpoint.js +113 -0
  25. package/src/drivers/worker-client.js +185 -0
  26. package/src/drivers/worker-protocol.js +15 -0
  27. package/src/emit.js +37 -5
  28. package/src/engine-metadata.js +18 -0
  29. package/src/errors.js +1 -0
  30. package/src/index.js +4 -1
  31. package/src/introspect.js +1 -2
  32. package/src/jobs.js +4 -2
  33. package/src/migrate.js +12 -16
  34. package/src/model-api.js +4 -0
  35. package/src/model.js +12 -0
  36. package/src/mutation.js +66 -14
  37. package/src/physical.js +37 -7
  38. package/src/plan.js +66 -3
  39. package/src/query-api.js +5 -0
  40. package/src/query.js +39 -6
  41. package/src/relational-api.js +6 -0
  42. package/src/store.js +6 -4
  43. package/src/table-migration.js +158 -0
  44. package/types/bun.d.ts +3 -0
  45. package/types/entity.d.ts +1 -0
  46. package/types/index.d.ts +11 -2
  47. package/types/model.d.ts +1 -0
  48. package/types/node-process.d.ts +33 -0
  49. package/types/node.d.ts +3 -0
  50. package/types/query.d.ts +2 -0
  51. package/types/relational.d.ts +114 -0
@@ -0,0 +1,158 @@
1
+ //@ts-check
2
+ /** Reviewable SQLite table rebuilds, guarded by the observed physical schema. */
3
+ import { hashContent } from '@jarenjs/core/string';
4
+ import { canonicalizeJson } from '@jarenjs/json/canonical';
5
+ import { DbCompileError } from './errors.js';
6
+ import { defineTable, planTable } from './dialects/sqlite-schema.js';
7
+ import { relationalEmitter, relationalIdentifier as q, sql } from './dialects/sqlite-relational.js';
8
+ import { sqliteDialect as dialect, sqliteTableMigration } from './dialects/sqlite.js';
9
+ import { sqlTokens } from './dialects/check-read.js';
10
+
11
+ const refuse = (message) => { throw new DbCompileError('JD0021', message); };
12
+ const fingerprint = (v) => canonicalizeJson(v);
13
+ const schema = (connection) => connection.prepare(sqliteTableMigration.schema()).all([]).map((v) => ({ ...v }));
14
+ const createdSql = (text) => text.replace(/^CREATE (TABLE|(?:UNIQUE )?INDEX|TRIGGER) IF NOT EXISTS /i, 'CREATE $1 ');
15
+ const owned = (objects, table) => objects.filter((o) => o.tbl_name === table).map((o) => [o.type, o.name, o.sql]).sort((a, b) => (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0));
16
+ const sync = (connection) => {
17
+ if (connection.dialect.name !== 'sqlite' || !connection.synchronous || connection.mustQueue) refuse('table migration requires an available synchronous SQLite connection');
18
+ };
19
+
20
+ /** Inspect a live schema and generate a table plan without changing it.
21
+ * Rebuilds require explicit opt-in. Unlisted indexes and triggers are preserved.
22
+ * @param {any} connection @param {any} definition
23
+ * @param {{id:string,allowRebuild?:boolean,copy?:Record<string,any>,dropColumns?:string[],dropObjects?:string[]}} options */
24
+ export function planTableMigration(connection, definition, options) {
25
+ sync(connection);
26
+ if (!options || typeof options.id !== 'string' || !options.id) refuse('table migration requires an id');
27
+ for (const key of Object.keys(options)) if (!['id', 'allowRebuild', 'copy', 'dropColumns', 'dropObjects'].includes(key)) refuse(`unknown table migration option '${key}'`);
28
+ if (options.allowRebuild !== undefined && typeof options.allowRebuild !== 'boolean') refuse('allowRebuild must be boolean');
29
+ if (options.dropColumns !== undefined && (!Array.isArray(options.dropColumns)
30
+ || options.dropColumns.some((name) => typeof name !== 'string')
31
+ || new Set(options.dropColumns).size !== options.dropColumns.length)) refuse('dropColumns must be a distinct list of names');
32
+ const target = defineTable(definition);
33
+ const source = schema(connection);
34
+ const before = source.filter((o) => o.tbl_name === target.name);
35
+ const existing = before.find((o) => o.type === 'table');
36
+ if (!existing && source.some((o) => o.name === target.name)) refuse('the target name belongs to a non-table schema object');
37
+ const plan = planTable(target);
38
+ const after = plan.createSql.map((text, index) => ({
39
+ type: index === 0 ? 'table' : /^CREATE (?:UNIQUE )?INDEX/.test(text) ? 'index' : 'trigger',
40
+ name: index === 0 ? target.name : (index <= (target.indexes?.length ?? 0)
41
+ ? target.indexes[index - 1].name : target.triggers[index - 1 - (target.indexes?.length ?? 0)].name),
42
+ tbl_name: target.name, sql: createdSql(text),
43
+ }));
44
+ const dropObjects = options.dropObjects ?? [];
45
+ if (!Array.isArray(dropObjects) || new Set(dropObjects).size !== dropObjects.length
46
+ || dropObjects.some((name) => !before.some((o) => o.name === name && o.type !== 'table') || after.some((o) => o.name === name))) refuse('dropObjects must name distinct existing indexes/triggers absent from the target');
47
+ const preserved = before.filter((o) => o.type !== 'table' && !dropObjects.includes(o.name) && !after.some((a) => a.name === o.name));
48
+ after.push(...preserved);
49
+ const equal = fingerprint(owned(before, target.name)) === fingerprint(owned(after, target.name));
50
+ const rebuild = !!existing && !equal;
51
+ if (rebuild && options.allowRebuild !== true) refuse('the table differs; review a plan with allowRebuild:true');
52
+ const temporary = `_jaren_rebuild_${hashContent(fingerprint([options.id, target.name]))}`;
53
+ if (source.some((o) => o.name === temporary)) refuse('the rebuild temporary name already exists');
54
+ const oldColumns = existing ? connection.prepare(dialect.introspect.columns(target.name)).all([]) : [];
55
+ const oldKey = oldColumns.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
56
+ if (rebuild && sqlTokens(existing.sql).some((t) => t.kind === 'word' && t.value.toUpperCase() === 'AUTOINCREMENT')
57
+ && !target.columns.some((c) => c.identity === 'autoincrement')) refuse('rebuild must preserve AUTOINCREMENT allocation');
58
+ if (rebuild && (oldKey.length !== (target.primaryKey?.length ?? 0)
59
+ || oldKey.some((name) => !target.primaryKey.includes(name)))) refuse('rebuild must preserve every primary-key column');
60
+ const dropped = oldColumns.filter((c) => !target.columns.some((t) => t.name === c.name)).map((c) => c.name);
61
+ if (fingerprint([...dropped].sort()) !== fingerprint([...(options.dropColumns ?? [])].sort())) refuse('every removed source column needs an explicit dropColumns disposition');
62
+ const copy = options.copy ?? {};
63
+ if (!copy || typeof copy !== 'object' || Array.isArray(copy)) refuse('copy must be an assignment object');
64
+ for (const key of Object.keys(copy)) if (!target.columns.some((c) => c.name === key && c.generated === undefined) || oldKey.includes(key)) refuse('copy targets a writable non-key column');
65
+ const writable = target.columns.filter((c) => c.generated === undefined
66
+ && (Object.hasOwn(copy, c.name) || oldColumns.some((old) => old.name === c.name)));
67
+ if (rebuild && !writable.length) refuse('rebuild needs columns to copy');
68
+ const names = writable.map((c) => c.name);
69
+ const expressions = writable.map((c) => Object.hasOwn(copy, c.name) ? copy[c.name] : sql.column(c.name));
70
+ const unchanged = writable.filter((c) => !Object.hasOwn(copy, c.name)).map((c) => c.name);
71
+ // Preserve hidden rowids too, including text/composite-key rowid tables.
72
+ const tableInfo = existing ? connection.prepare(sqliteTableMigration.tableList()).all([]).find((t) => t.schema === 'main' && t.name === target.name) : null;
73
+ if (rebuild && !!tableInfo.wr !== !!target.withoutRowid) refuse('rebuild cannot change rowid ownership');
74
+ if (rebuild && !tableInfo.wr && !(oldKey.length === 1 && oldColumns.find((c) => c.name === oldKey[0]).type.toUpperCase() === 'INTEGER')) {
75
+ const rowid = ['rowid', '_rowid_', 'oid'].find((name) => !oldColumns.some((c) => c.name.toLowerCase() === name)
76
+ && !target.columns.some((c) => c.name.toLowerCase() === name));
77
+ if (!rowid) refuse('the source shadows every rowid alias');
78
+ names.unshift(rowid); expressions.unshift(sql.column(rowid)); unchanged.unshift(rowid);
79
+ }
80
+ const emitter = relationalEmitter({ inline: true });
81
+ const statements = equal ? [] : !existing ? [...plan.createSql] : [
82
+ planTable({ ...target, name: temporary, indexes: [], triggers: [] }).createSql[0],
83
+ `INSERT INTO ${q(temporary)} (${names.map(q).join(', ')}) SELECT ${expressions.map((v) => emitter.expr(v)).join(', ')} FROM ${q(target.name)}`,
84
+ ];
85
+ const finish = rebuild ? [`DROP TABLE ${q(target.name)}`, `ALTER TABLE ${q(temporary)} RENAME TO ${q(target.name)}`,
86
+ ...plan.createSql.slice(1), ...preserved.map((o) => o.sql)] : [];
87
+ const body = { version: 1, id: options.id, table: target.name, source, after, rebuild, temporary, unchanged, statements, finish };
88
+ return { ...body, checksum: fingerprint(body) };
89
+ }
90
+
91
+ /** Run a generated plan atomically. A repeated completed plan changes nothing.
92
+ * A rebuild with enabled foreign keys must start outside a transaction; call
93
+ * withForeignKeysSuspended for an outer scope containing nested rebuilds.
94
+ * @param {any} connection @param {ReturnType<typeof planTableMigration>} plan */
95
+ export function applyTableMigration(connection, plan) {
96
+ sync(connection);
97
+ const { checksum, ...body } = plan;
98
+ if (body.version !== 1 || checksum !== fingerprint(body)) refuse('table migration checksum differs');
99
+ if (fingerprint(owned(schema(connection), plan.table)) === fingerprint(owned(plan.after, plan.table))) return { changed: 0 };
100
+ const run = () => connection.transaction(() => {
101
+ const actual = schema(connection);
102
+ if (fingerprint(owned(actual, plan.table)) === fingerprint(owned(plan.after, plan.table))) return { changed: 0 };
103
+ if (fingerprint(actual) !== fingerprint(plan.source)) refuse('source schema changed after planning');
104
+ const sequenceExists = connection.prepare(sqliteTableMigration.sequenceExists()).get([]);
105
+ const sequence = sequenceExists ? connection.prepare(sqliteTableMigration.sequence()).get([plan.table]) : null;
106
+ for (const statement of plan.statements) connection.exec(statement);
107
+ if (plan.rebuild) {
108
+ const from = q(plan.table), to = q(plan.temporary);
109
+ const counts = connection.prepare(`SELECT (SELECT count(*) FROM ${from}) AS a,(SELECT count(*) FROM ${to}) AS b`).get([]);
110
+ if (counts.a !== counts.b) refuse('rebuild changed the row count');
111
+ if (plan.unchanged.length) {
112
+ // Both values and storage classes must survive; BINARY prevents
113
+ // inherited NOCASE from hiding a changed byte sequence.
114
+ const values = plan.unchanged.flatMap((name) => [`${q(name)} COLLATE BINARY`, `typeof(${q(name)})`]).join(', ');
115
+ const difference = connection.prepare(`SELECT 1 FROM (SELECT ${values} FROM ${from} EXCEPT SELECT ${values} FROM ${to}) LIMIT 1`).get([]);
116
+ if (difference) refuse('rebuild changed a preserved value or storage class');
117
+ }
118
+ }
119
+ for (const statement of plan.finish) connection.exec(statement);
120
+ if (sequence && plan.rebuild) {
121
+ connection.prepare(sqliteTableMigration.raiseSequence()).run([sequence.seq, plan.table]);
122
+ connection.prepare(sqliteTableMigration.seedSequence()).run([plan.table, sequence.seq, plan.table]);
123
+ }
124
+ if (connection.prepare(dialect.pragma.foreignKeyCheck()).get([])) refuse('migration violates foreign-key references');
125
+ if (fingerprint(owned(schema(connection), plan.table)) !== fingerprint(owned(plan.after, plan.table))) refuse('migrated schema differs from the reviewed target');
126
+ return { changed: plan.statements.length + plan.finish.length };
127
+ }, { mode: 'immediate' });
128
+ return plan.rebuild ? withForeignKeysSuspended(connection, run) : run();
129
+ }
130
+
131
+ /** Explicit outer migration scope for SQLite's foreign-key transition.
132
+ * Always restore the connection settings, including failure and nested scopes.
133
+ * @param {any} connection @param {() => any} fn @returns {any} */
134
+ export function withForeignKeysSuspended(connection, fn) {
135
+ sync(connection);
136
+ if (typeof fn !== 'function' || Object.prototype.toString.call(fn) === '[object AsyncFunction]')
137
+ refuse('a physical migration scope requires a synchronous callback');
138
+ const foreignKeys = connection.prepare(dialect.introspect.pragma('foreign_keys')).get([]).foreign_keys;
139
+ const legacy = connection.prepare(dialect.introspect.pragma('legacy_alter_table')).get([]).legacy_alter_table;
140
+ try {
141
+ connection.exec(dialect.pragma.foreignKeys(false));
142
+ if (connection.prepare(dialect.introspect.pragma('foreign_keys')).get([]).foreign_keys !== 0) refuse('foreign_keys cannot change inside a transaction; establish the outer migration scope first');
143
+ connection.exec(dialect.pragma.set('legacy_alter_table', 'ON'));
144
+ return connection.transaction(() => {
145
+ const result = fn();
146
+ if (result != null && typeof result.then === 'function') {
147
+ Promise.resolve(result).catch(() => {});
148
+ refuse('a physical migration scope must settle synchronously');
149
+ }
150
+ if (connection.prepare(dialect.pragma.foreignKeyCheck()).get([])) refuse('migration violates foreign-key references');
151
+ return result;
152
+ }, { mode: 'immediate' });
153
+ }
154
+ finally {
155
+ connection.exec(dialect.pragma.set('legacy_alter_table', legacy ? 'ON' : 'OFF'));
156
+ connection.exec(dialect.pragma.foreignKeys(!!foreignKeys));
157
+ }
158
+ }
package/types/bun.d.ts CHANGED
@@ -7,3 +7,6 @@ export declare function bunDriver(): Driver;
7
7
  export declare function adaptBunDatabase(db: unknown): unknown;
8
8
  /** Construct and adapt from a loaded `bun:sqlite`-shaped module. */
9
9
  export declare function fromBunModule(mod: unknown, path: string, options?: unknown): unknown;
10
+
11
+ /** Disk-backed consistent snapshot; refuses an existing destination. */
12
+ export declare function snapshotDatabase(connection: unknown, target: string): Promise<{ path: string; pages: number }>;
@@ -0,0 +1 @@
1
+ export { entityCore } from './index.js';
package/types/index.d.ts CHANGED
@@ -514,6 +514,8 @@ export interface StoreCapabilities {
514
514
  readonly sessions: boolean;
515
515
  readonly sessionReason: string | null;
516
516
  readonly worker: boolean;
517
+ readonly process: boolean;
518
+ readonly ownerTermination: boolean;
517
519
  readonly pooling: boolean;
518
520
  readonly poolReaders: number;
519
521
  readonly poolWriters: number;
@@ -602,8 +604,11 @@ export interface UntrackedReads<T = unknown> {
602
604
  /** Closed native mutation forms over declared SQLite column layouts. */
603
605
  export type EntityMutation = {
604
606
  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: 'update'; key?: EntityKeyArg; where?: unknown; expectedRevision?: number; set?: Readonly<Record<string, unknown>>;
608
+ expressions?: Readonly<Record<string, import('./relational.js').SqlInput>>; reporting?: 'matched' | 'changed' }
609
+ | { op: 'delete'; key?: EntityKeyArg; where?: unknown; expectedRevision?: number }
610
+ | { op: 'upsert'; values: Readonly<Record<string, unknown>>; conflict: readonly string[]; update?: readonly string[];
611
+ conflictWhere?: import('./relational.js').SqlInput; onConflict?: 'nothing' | 'update'; reporting?: 'matched' | 'changed' }
607
612
  | { op: 'insert-select'; source: string; where?: unknown; select: Readonly<Record<string, string | { $literal: unknown }>>;
608
613
  conflict: readonly string[]; onConflict: 'nothing' });
609
614
  export interface MutationResult {
@@ -1207,6 +1212,8 @@ export declare const MAINTENANCE_OPERATIONS: readonly string[];
1207
1212
 
1208
1213
  export declare function normalizeEntities(model: unknown): Map<string, unknown>;
1209
1214
  export declare function explainMapping(model: unknown): unknown;
1215
+ /** One normalization shared by the entity engine and its physical mapping. */
1216
+ export declare function compileEntityModel(model: unknown): { entities: Map<string, unknown>; mapping: unknown };
1210
1217
  /** The relation tables of normalized entities, keyed by entity name
1211
1218
  * then by relation member (MODEL-FORMAT §10.1) — what every entity set
1212
1219
  * exposes as `relations` and every scope carries for all its roots. */
@@ -2037,3 +2044,5 @@ export declare function planInvariants(model: unknown, options: { dialect: Diale
2037
2044
  export declare function planPhysicalMigration(connection: unknown, fromModel: unknown, toModel: unknown,
2038
2045
  options: { id: string; steps: readonly unknown[]; dispositions: Readonly<Record<string, 'preserve' | 'replace' | 'drop'>>;
2039
2046
  assertions?: readonly { sql: string; params?: readonly unknown[]; expected: readonly unknown[] }[] }): unknown;
2047
+
2048
+ export { sql, relational, planRelational, defineTable, planTable, planTableMigration, applyTableMigration, withForeignKeysSuspended } from './relational.js';
@@ -0,0 +1 @@
1
+ export { normalizeEntities, explainMapping, compileEntityModel, readSchema, introspectModel, INTROSPECT_CODES, relationTables } from './index.js';
@@ -0,0 +1,33 @@
1
+ import type { NodeOpenOptions } from './node.js';
2
+ import type { CancellationCapabilities, Driver } from './index.js';
3
+ import type { NodeWorkerConnection, NodeWorkerOptions, WorkerMetrics } from './node-worker.js';
4
+
5
+ export interface NodeProcessOptions extends NodeWorkerOptions { maxOwners?: number; timeoutMs?: number; maxRequestBytes?: number }
6
+ export interface ProcessSettlement {
7
+ readonly path: string | null;
8
+ readonly generation: number;
9
+ readonly pid: number | null;
10
+ readonly status: 'starting' | 'healthy' | 'quarantined' | 'exited';
11
+ readonly transaction: 'none' | 'active' | 'committed' | 'rolled-back' | 'unknown';
12
+ readonly safeToReplace: boolean;
13
+ readonly exitCode: number | null;
14
+ readonly exitSignal: string | null;
15
+ }
16
+ export interface NodeProcessConnection extends NodeWorkerConnection {
17
+ readonly capabilities: Readonly<Record<string, unknown>> & {
18
+ readonly process: true; readonly ownerTermination: true;
19
+ readonly cancellation: CancellationCapabilities & {readonly midStatement: false};
20
+ };
21
+ supervise<T>(body: (connection: NodeProcessConnection) => T | Promise<T>, options?: {signal?: AbortSignal; timeoutMs?: number}): Promise<T>;
22
+ cancel(reason?: string): Error;
23
+ settlement(): ProcessSettlement;
24
+ /** Resolves only after the OS reports owner exit, never from a caller deadline. */
25
+ settled(): Promise<ProcessSettlement>;
26
+ metrics(): WorkerMetrics & {readonly owner: ProcessSettlement; readonly supervised: number};
27
+ restart(): Promise<NodeProcessConnection>;
28
+ }
29
+ export interface NodeProcessDriver extends Driver {
30
+ open(path?: string, options?: NodeOpenOptions): Promise<NodeProcessConnection>;
31
+ metrics(): Readonly<{capacity: number; owners: number; quarantined: number; healthy: number}>;
32
+ }
33
+ export declare function nodeProcessDriver(options?: NodeProcessOptions): NodeProcessDriver;
package/types/node.d.ts CHANGED
@@ -88,3 +88,6 @@ export declare function openNullTarget(): DocumentTarget;
88
88
  /** Read an explicit collection bundle, materialized under the declared bounds. */
89
89
  export declare function readCollectionBundle(source: DocumentByteSource,
90
90
  bounds: { maxBytes: number | null; maxRows: number | null }): Promise<Record<string, unknown[]>>;
91
+
92
+ /** Disk-backed consistent snapshot; refuses an existing destination. */
93
+ export declare function snapshotDatabase(connection: unknown, target: string): Promise<{ path: string; pages: number }>;
@@ -0,0 +1,2 @@
1
+ export { createQueryEngine, createQueryState, collectEntityRoots, entityRoot, createEntityQueryEngine, createLoadEngine,
2
+ INCLUDE_DEPTH_DEFAULT, INCLUDE_ROWS_DEFAULT, INCLUDE_BYTES_DEFAULT } from './index.js';
@@ -0,0 +1,114 @@
1
+ /** Structural SQLite authoring, with explicit native SQL semantics. */
2
+ export type SqlValue = string | number | bigint | Uint8Array | null;
3
+ export type SqlInput = SqlValue | SqlExpression;
4
+ export type SqlOperator = '=' | '<>' | '<' | '<=' | '>' | '>=' | 'IS' | 'IS NOT'
5
+ | '+' | '-' | '*' | '/' | '%' | '||' | 'AND' | 'OR' | 'LIKE' | 'NOT LIKE' | 'GLOB';
6
+ export type SqlFunction = 'coalesce' | 'nullif' | 'trim' | 'ltrim' | 'rtrim' | 'lower' | 'upper'
7
+ | 'length' | 'abs' | 'round' | 'typeof' | 'json_extract' | 'json_valid'
8
+ | 'count' | 'sum' | 'total' | 'avg' | 'min' | 'max'
9
+ | 'date' | 'time' | 'datetime' | 'julianday' | 'unixepoch' | 'strftime';
10
+ export type SqlType = 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB' | 'NUMERIC';
11
+ export type SqlCollation = 'BINARY' | 'NOCASE' | 'RTRIM';
12
+ export type SqlExpression =
13
+ | { readonly $sql: 'column'; readonly name: string; readonly table?: string }
14
+ | { readonly $sql: 'value'; readonly value: SqlValue }
15
+ | { readonly $sql: 'param'; readonly name: string }
16
+ | { readonly $sql: 'binary'; readonly op: SqlOperator; readonly left: SqlInput; readonly right: SqlInput }
17
+ | { readonly $sql: 'not'; readonly value: SqlInput }
18
+ | { readonly $sql: 'in'; readonly value: SqlInput; readonly values: readonly SqlInput[] | SqlSelect; readonly negate?: boolean }
19
+ | { readonly $sql: 'call'; readonly name: SqlFunction; readonly args: readonly SqlInput[]; readonly distinct?: boolean }
20
+ | { readonly $sql: 'cast'; readonly value: SqlInput; readonly type: SqlType }
21
+ | { readonly $sql: 'collate'; readonly value: SqlInput; readonly collation: SqlCollation }
22
+ | { readonly $sql: 'case'; readonly branches: readonly { when: SqlInput; then: SqlInput }[]; readonly otherwise?: SqlInput }
23
+ | { readonly $sql: 'scalar' | 'exists'; readonly query: SqlSelect };
24
+ export interface SqlOrder { readonly by: SqlInput; readonly direction?: 'asc' | 'desc'; readonly nulls?: 'first' | 'last' }
25
+ export type SqlProjection = '*' | Readonly<Record<string, SqlInput>>;
26
+ export type SqlSource = string | { readonly table: string; readonly as?: string } | { readonly query: SqlSelect; readonly as?: string };
27
+ export interface SqlSelect {
28
+ readonly from?: SqlSource;
29
+ readonly columns?: SqlProjection;
30
+ readonly joins?: readonly { source: SqlSource; type?: 'inner' | 'left' | 'cross'; on?: SqlInput }[];
31
+ readonly where?: SqlInput;
32
+ readonly groupBy?: readonly SqlInput[];
33
+ readonly having?: SqlInput;
34
+ readonly orderBy?: readonly SqlOrder[];
35
+ readonly distinct?: boolean;
36
+ readonly limit?: number;
37
+ readonly offset?: number;
38
+ readonly union?: readonly SqlSelect[];
39
+ readonly all?: boolean;
40
+ }
41
+ export type SqlConflict = { readonly target?: readonly (string | SqlExpression)[]; readonly where?: SqlInput } & (
42
+ { readonly action: 'nothing' } | { readonly action: 'update'; readonly set: Readonly<Record<string, SqlInput>>; readonly updateWhere?: SqlInput });
43
+ export type SqlMutation = { readonly table: string; readonly returning?: SqlProjection } & (
44
+ | { readonly op: 'update'; readonly set: Readonly<Record<string, SqlInput>>; readonly where: SqlInput; readonly reporting?: 'matched' | 'changed' }
45
+ | { readonly op: 'delete'; readonly where: SqlInput }
46
+ | ({ readonly op: 'insert'; readonly conflict?: SqlConflict; readonly ignore?: boolean } & (
47
+ { readonly values: Readonly<Record<string, SqlInput>> } | { readonly source: SqlSelect; readonly columns: readonly string[] })));
48
+ export interface RelationalOptions { readonly externals?: Readonly<Record<string, SqlValue>> }
49
+ export interface RelationalPlan { readonly sql: string; readonly params: readonly SqlValue[]; readonly access: 'read' | 'write' }
50
+ export interface RelationalMutationResult { readonly affected: number; readonly rows?: readonly Record<string, unknown>[]; readonly lastInsertRowid?: number | bigint }
51
+ export interface RelationalEngine {
52
+ plan(document: SqlSelect | SqlMutation, options?: RelationalOptions): RelationalPlan;
53
+ all<T = Record<string, unknown>>(document: SqlSelect, options?: RelationalOptions): T[];
54
+ get<T = Record<string, unknown>>(document: SqlSelect, options?: RelationalOptions): T | undefined;
55
+ iterate<T = Record<string, unknown>>(document: SqlSelect, options?: RelationalOptions): IterableIterator<T>;
56
+ execute(document: SqlMutation, options?: RelationalOptions): RelationalMutationResult;
57
+ }
58
+ export declare const sql: {
59
+ column(name: string, table?: string): SqlExpression;
60
+ value(value: SqlValue): SqlExpression;
61
+ param(name: string): SqlExpression;
62
+ binary(op: SqlOperator, left: SqlInput, right: SqlInput): SqlExpression;
63
+ not(value: SqlInput): SqlExpression;
64
+ in(value: SqlInput, values: readonly SqlInput[] | SqlSelect, negate?: boolean): SqlExpression;
65
+ call(name: SqlFunction, args: readonly SqlInput[], options?: { distinct?: boolean }): SqlExpression;
66
+ cast(value: SqlInput, type: SqlType): SqlExpression;
67
+ collate(value: SqlInput, collation: SqlCollation): SqlExpression;
68
+ case(branches: readonly { when: SqlInput; then: SqlInput }[], otherwise?: SqlInput): SqlExpression;
69
+ scalar(query: SqlSelect): SqlExpression;
70
+ exists(query: SqlSelect): SqlExpression;
71
+ };
72
+ export declare function planRelational(document: SqlSelect | SqlMutation, options?: RelationalOptions): RelationalPlan;
73
+ /** Requires a synchronous SQLite connection; no model is opened. */
74
+ export declare function relational(connection: unknown): RelationalEngine;
75
+
76
+ export interface TableColumn {
77
+ readonly name: string; readonly type: SqlType | 'ANY'; readonly nullable?: boolean;
78
+ readonly default?: SqlInput; readonly collation?: SqlCollation;
79
+ readonly identity?: 'rowid' | 'autoincrement'; readonly check?: SqlInput;
80
+ readonly generated?: SqlInput; readonly stored?: boolean;
81
+ }
82
+ export type ForeignKeyAction = 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default';
83
+ export type TableConstraint = { readonly name?: string } & (
84
+ { readonly kind: 'unique'; readonly columns: readonly string[] }
85
+ | { readonly kind: 'check'; readonly expression: SqlInput }
86
+ | { readonly kind: 'foreignKey'; readonly columns: readonly string[]; readonly table: string; readonly references: readonly string[];
87
+ readonly onDelete?: ForeignKeyAction; readonly onUpdate?: ForeignKeyAction; readonly deferred?: boolean });
88
+ export interface TableIndex { readonly name: string; readonly terms: readonly Omit<SqlOrder, 'nulls'>[]; readonly unique?: boolean; readonly where?: SqlInput }
89
+ export type TriggerRaise = { readonly raise: { readonly action: 'abort' | 'fail' | 'rollback'; readonly message: string } | { readonly action: 'ignore' } };
90
+ export interface TableTrigger {
91
+ readonly name: string; readonly timing: 'before' | 'after'; readonly event: 'insert' | 'update' | 'delete';
92
+ readonly of?: readonly string[]; readonly when?: SqlInput; readonly steps: readonly (SqlMutation | TriggerRaise)[];
93
+ }
94
+ export interface TableDefinition {
95
+ readonly name: string; readonly columns: readonly TableColumn[]; readonly primaryKey?: readonly string[];
96
+ readonly constraints?: readonly TableConstraint[]; readonly indexes?: readonly TableIndex[]; readonly triggers?: readonly TableTrigger[];
97
+ readonly strict?: boolean; readonly withoutRowid?: boolean;
98
+ }
99
+ export interface TablePlan { readonly table: string; readonly createSql: readonly string[]; readonly expected: unknown }
100
+ export declare function defineTable(definition: TableDefinition): TableDefinition;
101
+ export declare function planTable(definition: TableDefinition): TablePlan;
102
+ export interface TableMigrationOptions {
103
+ readonly id: string; readonly allowRebuild?: boolean; readonly copy?: Readonly<Record<string, SqlInput>>;
104
+ readonly dropColumns?: readonly string[]; readonly dropObjects?: readonly string[];
105
+ }
106
+ export interface TableMigrationPlan {
107
+ readonly version: 1; readonly id: string; readonly table: string; readonly checksum: string;
108
+ readonly source: readonly unknown[]; readonly after: readonly unknown[]; readonly rebuild: boolean;
109
+ readonly temporary: string; readonly unchanged: readonly string[]; readonly statements: readonly string[]; readonly finish: readonly string[];
110
+ }
111
+ export declare function planTableMigration(connection: unknown, definition: TableDefinition, options: TableMigrationOptions): TableMigrationPlan;
112
+ export declare function applyTableMigration(connection: unknown, plan: TableMigrationPlan): { changed: number };
113
+ export declare function withForeignKeysSuspended<T>(connection: unknown, fn: () => T): T;
114
+ export { sqliteDialect } from './index.js';