@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.
- package/ARCHITECTURE.md +29 -12
- package/README.md +12 -4
- package/docs/MIGRATION-FORMAT.md +259 -70
- package/docs/MODEL-FORMAT.md +38 -6
- package/docs/SQLITE-RELATIONAL.md +31 -2
- package/package.json +4 -4
- package/schemas/jaren-migration.draft-07.schema.json +169 -5
- package/schemas/jaren-migration.schema.json +164 -0
- package/src/ddl.js +8 -86
- package/src/dialects/sqlite.js +1 -0
- package/src/document-steps.js +23 -5
- package/src/drivers/bun.js +13 -3
- package/src/drivers/file-identity.js +14 -0
- package/src/drivers/node.js +2 -0
- package/src/foreign-key-scope.js +50 -0
- package/src/migrate.js +291 -497
- package/src/migration-target.js +104 -0
- package/src/physical-transform.js +207 -0
- package/src/schema-sql.js +184 -0
- package/src/table-migration.js +17 -17
- package/types/index.d.ts +113 -13
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
package/src/dialects/sqlite.js
CHANGED
|
@@ -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'",
|
package/src/document-steps.js
CHANGED
|
@@ -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
|
-
|
|
189
|
-
|
|
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([
|
|
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)) {
|
package/src/drivers/bun.js
CHANGED
|
@@ -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',
|