@jarenjs/db 0.84.3 → 0.85.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.
package/src/query.js CHANGED
@@ -34,7 +34,7 @@
34
34
  import { physicalSelection, columnCodec } from './physical.js';
35
35
 
36
36
  import { createSemanticCache } from '@jarenjs/core/cache';
37
- import { analyzeQuery } from '@jarenjs/json/query';
37
+ import { analyzeQuery, JsonQueryRuntimeError } from '@jarenjs/json/query';
38
38
 
39
39
  import { DbCompileError, DbRuntimeError, wrapDriverError, classifyDriverError } from './errors.js';
40
40
  import { chain, attempt, isThenable } from './driver.js';
@@ -1653,7 +1653,7 @@ export function createEntityQueryEngine(context) {
1653
1653
  planned = { ...planned, mode: 'set', plan: null,
1654
1654
  reasons: [{ construct: 'pushdown', reason: BIND_REASONS.pushdown }] };
1655
1655
  }
1656
- if (planned.plan?.group && dialect.name !== 'sqlite') {
1656
+ if ((planned.plan?.group || planned.plan?.scalarAggregate) && dialect.name !== 'sqlite') {
1657
1657
  planned = { ...planned, mode: 'set', plan: null,
1658
1658
  reasons: [{ construct: '$groupby', reason: 'entity grouping runtime guards are qualified for SQLite' }] };
1659
1659
  }
@@ -1872,11 +1872,30 @@ export function createEntityQueryEngine(context) {
1872
1872
  // the database cannot take (a missing external, a boolean, a null,
1873
1873
  // a region with no box) sends the call to the residual, where the
1874
1874
  // ENGINE raises its own error or answers with its own semantics
1875
- const params = entry.slots.map((slot) => slotValue(slot, externals));
1876
- if (params.some((value) => !bindable(value)))
1875
+ const diverted = divertReason(entry, externals);
1876
+ if (diverted !== null) {
1877
+ entry.runtimeReason = diverted;
1878
+ if (strict) throw new DbCompileError('JD0010',
1879
+ `strict mode refused a residual: '${diverted.construct}' — ${diverted.reason}`);
1880
+ if (profile?.refuseFullScan) throw profileEntityRefusal(
1881
+ 'the profile refuses the decoded scan required by the external binding', '/entities');
1877
1882
  return runResidual(entry, document, externals);
1883
+ }
1884
+ const params = entry.slots.map((slot) => slotValue(slot, externals));
1878
1885
  if (entry.statement === null) entry.statement = connection.prepare(entry.sql, { readOnly: true });
1879
1886
  return chain(guardEntityScan(entry), () => chain(entry.statement, (statement) => {
1887
+ if (entry.planned.plan.scalarAggregate) return chain(statement.get(params), (row) => {
1888
+ admittedRows(entry, row ? [row] : []);
1889
+ if (row?._valid === 0) throw new DbRuntimeError('JD2003', 'an aggregate column refuses a lossy or invalid value');
1890
+ if (row?._nulls > 0) throw new JsonQueryRuntimeError('JQ2001', 'an aggregate requires numbers or strings, got null');
1891
+ if (row?._safe === 0) {
1892
+ entry.runtimeReason = { construct: 'aggregate', reason: 'integer accumulation exceeded its runtime exactness bound' };
1893
+ if (strict) throw new DbCompileError('JD0010', entry.runtimeReason.reason);
1894
+ if (profile?.refuseFullScan) throw profileEntityRefusal('the profile refuses the decoded scan required by integer accumulation', '/entities');
1895
+ return runResidual(entry, document, externals);
1896
+ }
1897
+ return wrapValue(entry, row?.value ?? (entry.planned.plan.aggregate === 'sum' ? 0 : undefined));
1898
+ });
1880
1899
  if (entry.planned.plan.aggregate === 'count')
1881
1900
  return chain(statement.get(params), (row) => { admittedRows(entry, row ? [row] : []); return wrapValue(entry, row?.value ?? 0); });
1882
1901
  return chain(statement.all(params), (rows) => {
@@ -1928,7 +1947,8 @@ export function createEntityQueryEngine(context) {
1928
1947
  */
1929
1948
  const divertReason = (entry, externals) => {
1930
1949
  for (const slot of entry.slots) {
1931
- if (bindable(slotValue(slot, externals))) continue;
1950
+ const value = slotValue(slot, externals);
1951
+ if (bindable(value) || (slot.nullable === true && value === null)) continue;
1932
1952
  const name = 'external' in slot ? slot.external
1933
1953
  : 'derived' in slot ? (slot.derived.kind === 'bboxAxis' ? slot.derived.external
1934
1954
  : [slot.derived.centre, slot.derived.radius].find((input) => 'external' in input)?.external) : null;
@@ -1949,6 +1969,10 @@ export function createEntityQueryEngine(context) {
1949
1969
  */
1950
1970
  const cursorClass = (entry, externals) => {
1951
1971
  const buffered = (barrier) => ({ streaming: 'buffered', barrier });
1972
+ if (entry.planned.mode === 'native' && externals !== null) {
1973
+ const diverted = divertReason(entry, externals);
1974
+ if (diverted !== null) return buffered(diverted);
1975
+ }
1952
1976
  if (entry.planned.wrapped === true) {
1953
1977
  return buffered({ construct: 'window', reason: BIND_REASONS.wrappedWindow });
1954
1978
  }
@@ -1997,11 +2021,20 @@ export function createEntityQueryEngine(context) {
1997
2021
  + 'entity binding — read it untracked, or return the binding itself');
1998
2022
  }
1999
2023
  const each = register === undefined ? (item) => item : (item) => register(retEntity, item);
2024
+ entry.runtimeReason = null;
2000
2025
  const classified = cursorClass(entry, externals);
2026
+ if (classified.barrier?.construct === 'external') {
2027
+ entry.admitted = { statements: 0, rows: 0, bytes: 0 };
2028
+ entry.runtimeReason = classified.barrier;
2029
+ if (strict) throw new DbCompileError('JD0010',
2030
+ `strict mode refused a residual: 'external' — ${classified.barrier.reason}`);
2031
+ if (profile?.refuseFullScan) throw profileEntityRefusal(
2032
+ 'the profile refuses the decoded scan required by the external binding', '/entities');
2033
+ }
2001
2034
  refuseBuffered(options, classified, entities.get(entry.planned.retEntity ?? '')?.docPath);
2002
2035
  const signal = options?.signal;
2003
2036
  const deadline = options?.deadline;
2004
- if (entry.planned.wrapped === true || entry.planned.plan?.group) {
2037
+ if (entry.planned.wrapped === true || entry.planned.plan?.group || entry.planned.plan?.scalarAggregate) {
2005
2038
  return cursorFactory({ ...classified, signal, deadline, now: state.now, wrap: driverWrap,
2006
2039
  materialize: () => chain(execute(document, options), (value) => entry.planned.wrapped === true
2007
2040
  ? [value] : value === undefined ? [] : Array.isArray(value) ? value : [value]) });
@@ -0,0 +1,6 @@
1
+ //@ts-check
2
+ /** Lightweight column-first SQLite authoring and execution. */
3
+ export { sql, planRelational, relational } from './dialects/sqlite-relational.js';
4
+ export { defineTable, planTable } from './dialects/sqlite-schema.js';
5
+ export { planTableMigration, applyTableMigration, withForeignKeysSuspended } from './table-migration.js';
6
+ export { sqliteDialect } from './dialects/sqlite.js';
package/src/store.js CHANGED
@@ -34,7 +34,7 @@ import { refuseUnsupportedPragmaKeys, resolvePragmaRequests, configurePragmas }
34
34
  import { createMaintenance } from './maintenance.js';
35
35
  import { createBackup } from './backup.js';
36
36
  import { normalizeProfile, assertProfileRoots } from './profile.js';
37
- import { normalizeEntities, explainMapping, joinTableRoots } from './model.js';
37
+ import { compileEntityModel, joinTableRoots } from './model.js';
38
38
  import { entityCore } from './entity.js';
39
39
  import { verifyPhysical } from './physical.js';
40
40
  import { trustedSql, synchronousBody } from './sql.js';
@@ -59,7 +59,8 @@ import {
59
59
  } from './expression.js';
60
60
 
61
61
  /** The model format version this store implements. */
62
- export const MODEL_VERSION = '0.1';
62
+ import { MODEL_VERSION } from './engine-metadata.js';
63
+ export { MODEL_VERSION };
63
64
 
64
65
  const COLLECTION_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
65
66
  const IDENTITIES = new Set(['uuid', 'integer']);
@@ -975,8 +976,9 @@ export function openStore(model, options) {
975
976
  let mapping;
976
977
  try {
977
978
  collections = normalizeModel(model, options.expressions);
978
- entities = normalizeEntities(model);
979
- mapping = entities.size > 0 ? explainMapping(model) : null;
979
+ const compiled = compileEntityModel(model);
980
+ entities = compiled.entities;
981
+ mapping = entities.size > 0 ? compiled.mapping : null;
980
982
  if ([...entities.values()].some((e) => e.physical !== null) && (options.capture || options.replication))
981
983
  throw new DbCompileError('JD0051', 'column adoption preserves application triggers; complete capture is not qualified');
982
984
  if (options.adopt === true && (options.capture || options.replication))
@@ -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
@@ -602,8 +602,11 @@ export interface UntrackedReads<T = unknown> {
602
602
  /** Closed native mutation forms over declared SQLite column layouts. */
603
603
  export type EntityMutation = {
604
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[] }
605
+ } & ({ op: 'update'; key?: EntityKeyArg; where?: unknown; expectedRevision?: number; set?: Readonly<Record<string, unknown>>;
606
+ expressions?: Readonly<Record<string, import('./relational.js').SqlInput>>; reporting?: 'matched' | 'changed' }
607
+ | { op: 'delete'; key?: EntityKeyArg; where?: unknown; expectedRevision?: number }
608
+ | { op: 'upsert'; values: Readonly<Record<string, unknown>>; conflict: readonly string[]; update?: readonly string[];
609
+ conflictWhere?: import('./relational.js').SqlInput; onConflict?: 'nothing' | 'update'; reporting?: 'matched' | 'changed' }
607
610
  | { op: 'insert-select'; source: string; where?: unknown; select: Readonly<Record<string, string | { $literal: unknown }>>;
608
611
  conflict: readonly string[]; onConflict: 'nothing' });
609
612
  export interface MutationResult {
@@ -1207,6 +1210,8 @@ export declare const MAINTENANCE_OPERATIONS: readonly string[];
1207
1210
 
1208
1211
  export declare function normalizeEntities(model: unknown): Map<string, unknown>;
1209
1212
  export declare function explainMapping(model: unknown): unknown;
1213
+ /** One normalization shared by the entity engine and its physical mapping. */
1214
+ export declare function compileEntityModel(model: unknown): { entities: Map<string, unknown>; mapping: unknown };
1210
1215
  /** The relation tables of normalized entities, keyed by entity name
1211
1216
  * then by relation member (MODEL-FORMAT §10.1) — what every entity set
1212
1217
  * exposes as `relations` and every scope carries for all its roots. */
@@ -2037,3 +2042,5 @@ export declare function planInvariants(model: unknown, options: { dialect: Diale
2037
2042
  export declare function planPhysicalMigration(connection: unknown, fromModel: unknown, toModel: unknown,
2038
2043
  options: { id: string; steps: readonly unknown[]; dispositions: Readonly<Record<string, 'preserve' | 'replace' | 'drop'>>;
2039
2044
  assertions?: readonly { sql: string; params?: readonly unknown[]; expected: readonly unknown[] }[] }): unknown;
2045
+
2046
+ 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';
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';