@jarenjs/db 0.87.0 → 0.89.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.
@@ -29,7 +29,7 @@
29
29
  "type": "string"
30
30
  },
31
31
  "steps": {
32
- "description": "Ordered steps; the order is the contract. A pure widening may carry NO steps \u2014 the migration then only moves the recorded shape.",
32
+ "description": "Ordered steps; the order is the contract. A pure widening may carry NO steps the migration then only moves the recorded shape.",
33
33
  "type": "array",
34
34
  "items": {
35
35
  "$ref": "#/definitions/step"
@@ -80,6 +80,53 @@
80
80
  },
81
81
  "additionalProperties": false
82
82
  }
83
+ },
84
+ "target": {
85
+ "type": "object",
86
+ "properties": {
87
+ "objects": {
88
+ "type": "array",
89
+ "items": {
90
+ "type": "object",
91
+ "required": [
92
+ "type",
93
+ "name",
94
+ "owner",
95
+ "sql"
96
+ ],
97
+ "properties": {
98
+ "type": {
99
+ "enum": [
100
+ "table",
101
+ "view",
102
+ "index",
103
+ "trigger"
104
+ ]
105
+ },
106
+ "name": {
107
+ "type": "string"
108
+ },
109
+ "owner": {
110
+ "type": "string"
111
+ },
112
+ "sql": {
113
+ "type": "string"
114
+ }
115
+ }
116
+ }
117
+ },
118
+ "tables": {
119
+ "type": "array",
120
+ "uniqueItems": true,
121
+ "items": {
122
+ "type": "string"
123
+ }
124
+ }
125
+ },
126
+ "required": [
127
+ "objects"
128
+ ],
129
+ "additionalProperties": false
83
130
  }
84
131
  },
85
132
  "additionalProperties": false
@@ -113,6 +160,9 @@
113
160
  },
114
161
  {
115
162
  "$ref": "#/definitions/rebuildStep"
163
+ },
164
+ {
165
+ "$ref": "#/definitions/tableStep"
116
166
  }
117
167
  ]
118
168
  },
@@ -158,6 +208,18 @@
158
208
  },
159
209
  "note": {
160
210
  "type": "string"
211
+ },
212
+ "model": {
213
+ "description": "The immutable model describing this step’s current physical layout.",
214
+ "type": "object",
215
+ "properties": {
216
+ "$model": {
217
+ "const": "0.1"
218
+ }
219
+ },
220
+ "required": [
221
+ "$model"
222
+ ]
161
223
  }
162
224
  },
163
225
  "required": [
@@ -189,6 +251,18 @@
189
251
  },
190
252
  "note": {
191
253
  "type": "string"
254
+ },
255
+ "model": {
256
+ "description": "The immutable model describing this step’s current physical layout.",
257
+ "type": "object",
258
+ "properties": {
259
+ "$model": {
260
+ "const": "0.1"
261
+ }
262
+ },
263
+ "required": [
264
+ "$model"
265
+ ]
192
266
  }
193
267
  },
194
268
  "required": [
@@ -199,7 +273,7 @@
199
273
  "additionalProperties": false
200
274
  },
201
275
  "deriveStep": {
202
- "description": "Recompute named STORED derived index columns from the documents already in a collection. A spatial column is stored only under the physical mapping that cannot index a registered deterministic function (the generated mapping populates itself); a vector column is stored under both. The step is idempotent \u2014 a derived value is a pure function of the document \u2014 so a replay writes what the first run wrote.",
276
+ "description": "Recompute named STORED derived index columns from the documents already in a collection. A spatial column is stored only under the physical mapping that cannot index a registered deterministic function (the generated mapping populates itself); a vector column is stored under both. The step is idempotent a derived value is a pure function of the document so a replay writes what the first run wrote.",
203
277
  "type": "object",
204
278
  "properties": {
205
279
  "kind": {
@@ -273,7 +347,7 @@
273
347
  "additionalProperties": false
274
348
  },
275
349
  "sqlStep": {
276
- "description": "A DATA step spelled directly as one SQL statement (MIGRATION-FORMAT \u00a79.4): the planner's fold of a dropped column into the document, the move of a document member into its new column, an R*Tree backfill. Executed exactly as a ddl step; distinct in kind so a reviewer reads intent and a dry run shows it as data.",
350
+ "description": "A DATA step spelled directly as one SQL statement (MIGRATION-FORMAT §9.4): the planner's fold of a dropped column into the document, the move of a document member into its new column, an R*Tree backfill. Executed exactly as a ddl step; distinct in kind so a reviewer reads intent and a dry run shows it as data.",
277
351
  "type": "object",
278
352
  "properties": {
279
353
  "kind": {
@@ -294,7 +368,7 @@
294
368
  "additionalProperties": false
295
369
  },
296
370
  "rebuildStep": {
297
- "description": "The ALTER TABLE procedure SQLite lacks (MIGRATION-FORMAT \u00a710), self-contained: create the target shape under `<table>__rebuild`, copy the rows through the rendered column mapping, drop the old table, rename, recreate the indexes, then `PRAGMA foreign_key_check` inside the transaction.",
371
+ "description": "The ALTER TABLE procedure SQLite lacks (MIGRATION-FORMAT §10), self-contained: create the target shape under `<table>__rebuild`, copy the rows through the rendered column mapping, drop the old table, rename, recreate the indexes, then `PRAGMA foreign_key_check` inside the transaction.",
298
372
  "type": "object",
299
373
  "properties": {
300
374
  "kind": {
@@ -314,7 +388,7 @@
314
388
  }
315
389
  },
316
390
  "copy": {
317
- "description": "The INSERT \u2026 SELECT that carries every row across the column mapping.",
391
+ "description": "The INSERT SELECT that carries every row across the column mapping.",
318
392
  "type": "string",
319
393
  "minLength": 1
320
394
  },
@@ -338,6 +412,96 @@
338
412
  "indexes"
339
413
  ],
340
414
  "additionalProperties": false
415
+ },
416
+ "tableStep": {
417
+ "description": "A complete reviewed planTableMigration artifact, executed with its source, row, storage and identity guards.",
418
+ "type": "object",
419
+ "properties": {
420
+ "kind": {
421
+ "const": "table"
422
+ },
423
+ "plan": {
424
+ "type": "object",
425
+ "required": [
426
+ "version",
427
+ "id",
428
+ "table",
429
+ "checksum",
430
+ "source",
431
+ "after",
432
+ "rebuild",
433
+ "temporary",
434
+ "unchanged",
435
+ "statements",
436
+ "finish"
437
+ ],
438
+ "properties": {
439
+ "version": {
440
+ "const": 1
441
+ },
442
+ "id": {
443
+ "type": "string",
444
+ "minLength": 1
445
+ },
446
+ "table": {
447
+ "type": "string",
448
+ "minLength": 1
449
+ },
450
+ "checksum": {
451
+ "type": "string",
452
+ "minLength": 1
453
+ },
454
+ "source": {
455
+ "type": "array",
456
+ "items": {
457
+ "type": "object"
458
+ }
459
+ },
460
+ "after": {
461
+ "type": "array",
462
+ "items": {
463
+ "type": "object"
464
+ }
465
+ },
466
+ "rebuild": {
467
+ "type": "boolean"
468
+ },
469
+ "temporary": {
470
+ "type": "string",
471
+ "minLength": 1
472
+ },
473
+ "unchanged": {
474
+ "type": "array",
475
+ "items": {
476
+ "type": "string",
477
+ "minLength": 1
478
+ }
479
+ },
480
+ "statements": {
481
+ "type": "array",
482
+ "items": {
483
+ "type": "string",
484
+ "minLength": 1
485
+ }
486
+ },
487
+ "finish": {
488
+ "type": "array",
489
+ "items": {
490
+ "type": "string",
491
+ "minLength": 1
492
+ }
493
+ }
494
+ }
495
+ },
496
+ "note": {
497
+ "type": "string"
498
+ }
499
+ },
500
+ "required": [
501
+ "kind",
502
+ "plan"
503
+ ],
504
+ "additionalProperties": false
341
505
  }
342
506
  }
343
507
  }
@@ -80,6 +80,53 @@
80
80
  },
81
81
  "additionalProperties": false
82
82
  }
83
+ },
84
+ "target": {
85
+ "type": "object",
86
+ "properties": {
87
+ "objects": {
88
+ "type": "array",
89
+ "items": {
90
+ "type": "object",
91
+ "required": [
92
+ "type",
93
+ "name",
94
+ "owner",
95
+ "sql"
96
+ ],
97
+ "properties": {
98
+ "type": {
99
+ "enum": [
100
+ "table",
101
+ "view",
102
+ "index",
103
+ "trigger"
104
+ ]
105
+ },
106
+ "name": {
107
+ "type": "string"
108
+ },
109
+ "owner": {
110
+ "type": "string"
111
+ },
112
+ "sql": {
113
+ "type": "string"
114
+ }
115
+ }
116
+ }
117
+ },
118
+ "tables": {
119
+ "type": "array",
120
+ "uniqueItems": true,
121
+ "items": {
122
+ "type": "string"
123
+ }
124
+ }
125
+ },
126
+ "required": [
127
+ "objects"
128
+ ],
129
+ "additionalProperties": false
83
130
  }
84
131
  },
85
132
  "additionalProperties": false
@@ -113,6 +160,9 @@
113
160
  },
114
161
  {
115
162
  "$ref": "#/$defs/rebuildStep"
163
+ },
164
+ {
165
+ "$ref": "#/$defs/tableStep"
116
166
  }
117
167
  ]
118
168
  },
@@ -158,6 +208,18 @@
158
208
  },
159
209
  "note": {
160
210
  "type": "string"
211
+ },
212
+ "model": {
213
+ "description": "The immutable model describing this step’s current physical layout.",
214
+ "type": "object",
215
+ "properties": {
216
+ "$model": {
217
+ "const": "0.1"
218
+ }
219
+ },
220
+ "required": [
221
+ "$model"
222
+ ]
161
223
  }
162
224
  },
163
225
  "required": [
@@ -189,6 +251,18 @@
189
251
  },
190
252
  "note": {
191
253
  "type": "string"
254
+ },
255
+ "model": {
256
+ "description": "The immutable model describing this step’s current physical layout.",
257
+ "type": "object",
258
+ "properties": {
259
+ "$model": {
260
+ "const": "0.1"
261
+ }
262
+ },
263
+ "required": [
264
+ "$model"
265
+ ]
192
266
  }
193
267
  },
194
268
  "required": [
@@ -338,6 +412,96 @@
338
412
  "indexes"
339
413
  ],
340
414
  "additionalProperties": false
415
+ },
416
+ "tableStep": {
417
+ "description": "A complete reviewed planTableMigration artifact, executed with its source, row, storage and identity guards.",
418
+ "type": "object",
419
+ "properties": {
420
+ "kind": {
421
+ "const": "table"
422
+ },
423
+ "plan": {
424
+ "type": "object",
425
+ "required": [
426
+ "version",
427
+ "id",
428
+ "table",
429
+ "checksum",
430
+ "source",
431
+ "after",
432
+ "rebuild",
433
+ "temporary",
434
+ "unchanged",
435
+ "statements",
436
+ "finish"
437
+ ],
438
+ "properties": {
439
+ "version": {
440
+ "const": 1
441
+ },
442
+ "id": {
443
+ "type": "string",
444
+ "minLength": 1
445
+ },
446
+ "table": {
447
+ "type": "string",
448
+ "minLength": 1
449
+ },
450
+ "checksum": {
451
+ "type": "string",
452
+ "minLength": 1
453
+ },
454
+ "source": {
455
+ "type": "array",
456
+ "items": {
457
+ "type": "object"
458
+ }
459
+ },
460
+ "after": {
461
+ "type": "array",
462
+ "items": {
463
+ "type": "object"
464
+ }
465
+ },
466
+ "rebuild": {
467
+ "type": "boolean"
468
+ },
469
+ "temporary": {
470
+ "type": "string",
471
+ "minLength": 1
472
+ },
473
+ "unchanged": {
474
+ "type": "array",
475
+ "items": {
476
+ "type": "string",
477
+ "minLength": 1
478
+ }
479
+ },
480
+ "statements": {
481
+ "type": "array",
482
+ "items": {
483
+ "type": "string",
484
+ "minLength": 1
485
+ }
486
+ },
487
+ "finish": {
488
+ "type": "array",
489
+ "items": {
490
+ "type": "string",
491
+ "minLength": 1
492
+ }
493
+ }
494
+ }
495
+ },
496
+ "note": {
497
+ "type": "string"
498
+ }
499
+ },
500
+ "required": [
501
+ "kind",
502
+ "plan"
503
+ ],
504
+ "additionalProperties": false
341
505
  }
342
506
  }
343
507
  }
package/src/ddl.js CHANGED
@@ -21,6 +21,10 @@ import { chain } from './driver.js';
21
21
  import { BBOX_COMPONENTS, BBOX_INDEX_ORDER, derivedMappingFor } from './derive.js';
22
22
  import { expressionSql, expressionStem, expressionFunctions } from './expression.js';
23
23
  import { planTable } from './dialects/sqlite-schema.js';
24
+ import { comparableDeclaredSql } from './schema-sql.js';
25
+ export { normalizeDeclaredSql, comparableDeclaredSql } from './schema-sql.js';
26
+
27
+ const MANAGED_SQL_OPTIONS = Object.freeze({ columnOrder: /** @type {const} */ ('ignore') });
24
28
 
25
29
  /** The fixed physical column names of the 0.1 mapping. */
26
30
  export const KEY_COLUMN = 'key';
@@ -620,88 +624,6 @@ export function planCollection(name, collection, dialect, options = undefined) {
620
624
  };
621
625
  }
622
626
 
623
- /**
624
- * Normalize a stored `CREATE` statement for comparison: collapse runs of
625
- * whitespace, drop whitespace around punctuation, and strip the
626
- * `IF NOT EXISTS` SQLite does not keep. What survives is every token that
627
- * carries meaning, so two statements compare equal exactly when they
628
- * declare the same physical object.
629
- * @param {string} sql
630
- * @returns {string}
631
- */
632
- export function normalizeDeclaredSql(sql) {
633
- return String(sql)
634
- .replace(/\s+/g, ' ')
635
- .replace(/\s*([(),])\s*/g, '$1')
636
- .replace(/\bIF NOT EXISTS\s+/i, '')
637
- .trim();
638
- }
639
-
640
- /**
641
- * Split a comma-separated list at TOP-LEVEL commas only, so a
642
- * `CHECK(x IN (1,2))` or a multi-column constraint stays one item.
643
- * @param {string} body
644
- * @returns {string[]}
645
- */
646
- function splitTopLevel(body) {
647
- /** @type {string[]} */
648
- const parts = [];
649
- let depth = 0;
650
- let quote = '';
651
- let start = 0;
652
- for (let i = 0; i < body.length; i++) {
653
- const c = body[i];
654
- if (quote !== '') {
655
- if (c === quote) quote = '';
656
- continue;
657
- }
658
- if (c === '"' || c === "'") quote = c;
659
- else if (c === '(') depth++;
660
- else if (c === ')') depth--;
661
- else if (c === ',' && depth === 0) {
662
- parts.push(body.slice(start, i));
663
- start = i + 1;
664
- }
665
- }
666
- parts.push(body.slice(start));
667
- return parts.map((part) => part.trim()).filter((part) => part !== '');
668
- }
669
-
670
- /**
671
- * A comparable form of one `CREATE` statement.
672
- *
673
- * For a TABLE the column definitions compare as a SET, because
674
- * `ALTER TABLE … ADD COLUMN` can only append — so a migrated table and a
675
- * freshly built one legitimately differ in column order, and this store
676
- * never reads a column positionally. Everything else is exact: each
677
- * column's full definition (type, `PRIMARY KEY`, `NOT NULL`, `DEFAULT`,
678
- * `CHECK`, `GENERATED … AS`, `REFERENCES … ON DELETE …`), the table
679
- * constraints, and the trailing table options (`STRICT`,
680
- * `WITHOUT ROWID`).
681
- *
682
- * For an INDEX the text compares whole, because an index IS its order —
683
- * `(a,b)` and `(b,a)` serve different lookups — as are its partial
684
- * predicate and each term's collation and direction.
685
- * @param {string} sql
686
- * @returns {string}
687
- */
688
- export function comparableDeclaredSql(sql) {
689
- const normalized = normalizeDeclaredSql(sql);
690
- const open = normalized.indexOf('(');
691
- const close = normalized.lastIndexOf(')');
692
- if (!/^CREATE\s+TABLE\b/i.test(normalized) || open < 0 || close < open)
693
- return normalized;
694
- const head = normalized.slice(0, open);
695
- const options = normalized.slice(close + 1).trim();
696
- const items = splitTopLevel(normalized.slice(open + 1, close));
697
- // a column definition opens with the quoted column name; anything else
698
- // (PRIMARY KEY(...), UNIQUE(...), CHECK(...), FOREIGN KEY(...)) is a
699
- // table constraint, and those are unordered too
700
- const columns = items.filter((item) => item.startsWith('"')).sort();
701
- const constraints = items.filter((item) => !item.startsWith('"')).sort();
702
- return `${head}(${[...columns, ...constraints].join(',')})${options}`;
703
- }
704
-
705
627
  /**
706
628
  * The declared-SQL half of verification: compare every schema object the
707
629
  * table owns against the statements the plan would have created.
@@ -738,7 +660,7 @@ function verifyDeclaredSql(connection, plan, disagree) {
738
660
  // comparison for free.
739
661
  const owned = new Set(plan.virtualTables?.map((virtual) => virtual.name) ?? []);
740
662
  for (const sql of plan.createSql) {
741
- const comparable = comparableDeclaredSql(sql);
663
+ const comparable = comparableDeclaredSql(sql, MANAGED_SQL_OPTIONS);
742
664
  // the object's name is the first quoted identifier in the statement
743
665
  const name = /"((?:[^"]|"")*)"/.exec(comparable)?.[1]?.replace(/""/g, '"');
744
666
  if (name === undefined || owned.has(name)) continue;
@@ -756,8 +678,8 @@ function verifyDeclaredSql(connection, plan, disagree) {
756
678
  disagree(`the model declares the virtual table '${virtual.name}', `
757
679
  + 'which the database does not have');
758
680
  }
759
- const have = comparableDeclaredSql(row.sql);
760
- const wanted = comparableDeclaredSql(virtual.createSql);
681
+ const have = comparableDeclaredSql(row.sql, MANAGED_SQL_OPTIONS);
682
+ const wanted = comparableDeclaredSql(virtual.createSql, MANAGED_SQL_OPTIONS);
761
683
  if (have !== wanted) {
762
684
  disagree(`'${virtual.name}' is declared as\n ${have}\nand the model declares\n ${wanted}`);
763
685
  }
@@ -768,7 +690,7 @@ function verifyDeclaredSql(connection, plan, disagree) {
768
690
  (statement) => chain(statement.all([]), (rows) => {
769
691
  /** @type {Map<string, string>} */
770
692
  const actual = new Map();
771
- for (const row of rows) actual.set(String(row.name), comparableDeclaredSql(row.sql));
693
+ for (const row of rows) actual.set(String(row.name), comparableDeclaredSql(row.sql, MANAGED_SQL_OPTIONS));
772
694
  for (const [name, wanted] of planned) {
773
695
  const have = actual.get(name);
774
696
  if (have === undefined)
@@ -497,6 +497,7 @@ export const sqliteDialect = createDialect({
497
497
 
498
498
  /** SQLite schema inspection and identity preservation statements. */
499
499
  export const sqliteTableMigration = Object.freeze({
500
+ binaryCast: (sql) => `CAST(${sql} AS BLOB)`,
500
501
  schema: () => "SELECT type,name,tbl_name,sql FROM sqlite_schema WHERE sql IS NOT NULL AND substr(name,1,7) <> 'sqlite_' ORDER BY type,name",
501
502
  tableList: () => 'PRAGMA table_list',
502
503
  sequenceExists: () => "SELECT 1 AS present FROM sqlite_schema WHERE name='sqlite_sequence'",
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import { analyzeQuery, createQueryAccumulator } from '@jarenjs/json/query';
20
+ import { setObjectMember } from '@jarenjs/core/object';
20
21
  import { DbCompileError, DbRuntimeError } from './errors.js';
21
22
  import { utf8Length } from './cursor.js';
22
23
 
@@ -31,13 +32,13 @@ export const DOCUMENT_STEP_KINDS = new Set(['jslt', 'query']);
31
32
 
32
33
  /**
33
34
  * The step kinds that act on a PHYSICAL database — rendered DDL, a
34
- * data statement spelled as SQL, the table-rebuild procedure, and the
35
+ * data statement spelled as SQL, guarded table plans, the table-rebuild procedure, and the
35
36
  * backfill that recomputes stored derived COLUMNS. A host without
36
37
  * tables cannot honour any of them, and silently skipping one would
37
38
  * leave a migration half-applied, so it refuses instead.
38
39
  * @type {ReadonlySet<string>}
39
40
  */
40
- export const PHYSICAL_STEP_KINDS = new Set(['ddl', 'sql', 'rebuild', 'derive']);
41
+ export const PHYSICAL_STEP_KINDS = new Set(['ddl', 'sql', 'rebuild', 'derive', 'table']);
41
42
 
42
43
  /**
43
44
  * The refusal a failing step raises, spelled the one way — a reader
@@ -185,8 +186,12 @@ export function compileDocumentStep(step, index, context) {
185
186
  // a body that leaves it out keeps it, a body that rewrites it
186
187
  // is refused, as a collection's key is
187
188
  for (const key of keys) {
188
- if (next[key] === undefined) next[key] = document[key];
189
- else if (next[key] !== document[key]) {
189
+ const present = Object.hasOwn(document, key);
190
+ const previous = present ? document[key] : undefined;
191
+ if (!Object.hasOwn(next, key) || next[key] === undefined) {
192
+ if (present) setObjectMember(next, key, previous);
193
+ }
194
+ else if (next[key] !== previous) {
190
195
  fail(`the transform changed the key member '${key}' of row ${identity} — `
191
196
  + `key changes are not supported in ${MIGRATION_VERSION}`);
192
197
  }
@@ -271,7 +276,7 @@ export function compileDocumentStep(step, index, context) {
271
276
  };
272
277
  }
273
278
 
274
- const STEP_KINDS = new Set(['ddl', 'jslt', 'query', 'sql', 'rebuild', 'derive']);
279
+ const STEP_KINDS = new Set([...DOCUMENT_STEP_KINDS, ...PHYSICAL_STEP_KINDS]);
275
280
 
276
281
  /**
277
282
  * Structural validation of one migration document, including the
@@ -304,6 +309,19 @@ export function checkMigrationDocument(migration) {
304
309
  throw new DbCompileError('JD0023',
305
310
  `migration '${migration.id}' step ${i} is a sql step without sql text`);
306
311
  }
312
+ if (step.kind === 'table' && (!step.plan || typeof step.plan !== 'object' || Array.isArray(step.plan) || step.plan.version !== 1
313
+ || ['id', 'table', 'checksum', 'temporary'].some((key) => typeof step.plan[key] !== 'string' || step.plan[key].length === 0)
314
+ || typeof step.plan.rebuild !== 'boolean'
315
+ || ['source', 'after'].some((key) => !Array.isArray(step.plan[key])
316
+ || step.plan[key].some((value) => !value || typeof value !== 'object' || Array.isArray(value)))
317
+ || ['unchanged', 'statements', 'finish'].some((key) => !Array.isArray(step.plan[key])
318
+ || step.plan[key].some((value) => typeof value !== 'string' || value.length === 0)))) {
319
+ throw new DbCompileError('JD0023', `migration '${migration.id}' step ${i} is a table step without its reviewed plan`);
320
+ }
321
+ if (['jslt', 'query'].includes(step.kind) && step.model !== undefined
322
+ && (!step.model || typeof step.model !== 'object' || step.model.$model !== '0.1')) {
323
+ throw new DbCompileError('JD0023', `migration '${migration.id}' step ${i} has an invalid current model`);
324
+ }
307
325
  if (step.kind === 'derive'
308
326
  && (typeof step.collection !== 'string' || !Array.isArray(step.columns)
309
327
  || step.columns.length === 0)) {
@@ -20,8 +20,9 @@
20
20
 
21
21
  import { lazyOpen, openConnection } from '../driver.js';
22
22
  import { sqliteDialect } from '../dialects/sqlite.js';
23
- import { PRAGMA_NAMES } from '../pragmas.js';
23
+ import { PRAGMAS, PRAGMA_NAMES } from '../pragmas.js';
24
24
  import { writeSqliteSnapshot } from './snapshot.js';
25
+ import { nativeDatabaseIdentity } from './file-identity.js';
25
26
  export { snapshotDatabase } from './snapshot.js';
26
27
 
27
28
  /**
@@ -128,11 +129,19 @@ export function adaptBunDatabase(db, options) {
128
129
  * substitute module.
129
130
  * @param {any} mod - The `bun:sqlite` module (or a substitute)
130
131
  * @param {string} path
131
- * @param {{ readOnly?: boolean, queueTimeout?: number }} [options]
132
+ * @param {{ timeout?: number, readOnly?: boolean, queueTimeout?: number }} [options]
132
133
  * @returns {any}
133
134
  */
134
135
  export function fromBunModule(mod, path, options) {
136
+ const timeout = options?.timeout === undefined ? undefined
137
+ : PRAGMAS.busyTimeout.normalize(options.timeout, 'timeout');
135
138
  const db = options?.readOnly === true ? new mod.Database(path, { readonly: true }) : new mod.Database(path);
139
+ // Bun has no constructor timeout option. Configure newly owned handles
140
+ // before probing; adapting a caller-owned handle preserves its settings.
141
+ if (timeout !== undefined) {
142
+ try { db.run(sqliteDialect.pragma.set('busy_timeout', timeout)); }
143
+ catch (error) { db.close(); throw error; }
144
+ }
136
145
  const backup = {
137
146
  snapshot: true,
138
147
  copy: (target, copyOptions) => writeSqliteSnapshot(target, () => {
@@ -154,9 +163,10 @@ export function bunDriver() {
154
163
  return Object.freeze({
155
164
  name: 'bun-sqlite',
156
165
  dialect: sqliteDialect,
166
+ databaseIdentity: nativeDatabaseIdentity,
157
167
  /**
158
168
  * @param {string} path
159
- * @param {{ readOnly?: boolean, queueTimeout?: number }} [options]
169
+ * @param {{ timeout?: number, readOnly?: boolean, queueTimeout?: number }} [options]
160
170
  * @returns {Promise<any>}
161
171
  */
162
172
  open: (path, options) => lazyOpen('bun:sqlite',