@jarenjs/db 0.56.0 → 0.67.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 +412 -56
- package/README.md +600 -57
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +293 -45
- package/docs/LIVE-FORMAT.md +169 -20
- package/docs/MIGRATION-FORMAT.md +142 -17
- package/docs/MODEL-FORMAT.md +752 -64
- package/docs/REPLICATION-FORMAT.md +208 -0
- package/package.json +21 -7
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/schemas/jaren-replication-snapshot.draft-07.schema.json +83 -0
- package/schemas/jaren-replication-snapshot.schema.json +83 -0
- package/schemas/jaren-replication.draft-07.schema.json +82 -0
- package/schemas/jaren-replication.schema.json +82 -0
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +230 -47
- package/src/cli.js +165 -59
- package/src/cursor.js +417 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +102 -8
- package/src/dialect.js +268 -113
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +244 -11
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +448 -61
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +17 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit.js +339 -48
- package/src/entity.js +20 -22
- package/src/errors.js +430 -19
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -17
- package/src/introspect.js +583 -0
- package/src/jobs.js +843 -107
- package/src/json-bytes.js +58 -0
- package/src/live-join.js +250 -0
- package/src/live-nested.js +120 -0
- package/src/live.js +18 -4
- package/src/logical-rows.js +90 -0
- package/src/maintenance.js +175 -0
- package/src/migrate.js +248 -181
- package/src/model.js +68 -0
- package/src/plan.js +1119 -138
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1634 -323
- package/src/replication-format.js +115 -0
- package/src/replication.js +332 -0
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1567 -273
- package/src/tracker.js +203 -29
- package/src/udf.js +88 -7
- package/types/index.d.ts +1158 -27
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +69 -2
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +27 -4
- package/types/wasm.d.ts +14 -0
package/src/migrate.js
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
|
|
24
24
|
import { canonicalizeJson } from '@jarenjs/json/canonical';
|
|
25
25
|
import { hashContent } from '@jarenjs/core/string';
|
|
26
|
+
import { resolveRuntime } from '@jarenjs/core/runtime';
|
|
27
|
+
import { refuseCancelled } from './cancellation.js';
|
|
26
28
|
import { setObjectMember } from '@jarenjs/core/object';
|
|
27
29
|
import { compileJsonQuery } from '@jarenjs/json/query';
|
|
28
30
|
import { compileJsltStylesheet } from '@jarenjs/json/jslt';
|
|
@@ -30,11 +32,19 @@ import { compileJsltStylesheet } from '@jarenjs/json/jslt';
|
|
|
30
32
|
import { DbCompileError } from './errors.js';
|
|
31
33
|
import { chain, toPromise } from './driver.js';
|
|
32
34
|
import { normalizeModel } from './store.js';
|
|
35
|
+
import { CHANGES_TABLE, CHANGES_STATE_TABLE } from './capture.js';
|
|
36
|
+
import { JOBS_TABLE, JOB_CHECKPOINTS_TABLE } from './jobs.js';
|
|
33
37
|
import { planCollection, verifyShape, planEntity, planJoinTable } from './ddl.js';
|
|
34
38
|
import { normalizeEntities, explainMapping } from './model.js';
|
|
35
39
|
import { derivedValue, memberAt, registerDeriveFunctions } from './derive.js';
|
|
36
40
|
import { mergeEntityRow } from './graph.js';
|
|
37
41
|
import { entityCore } from './entity.js';
|
|
42
|
+
import {
|
|
43
|
+
MIGRATION_VERSION, isPerDocumentAssertion, compileDocumentStep, checkMigrationDocument,
|
|
44
|
+
normalizeAssertionBounds, ASSERTION_BOUNDS_DEFAULT, createAssertionBoundGuard,
|
|
45
|
+
} from './document-steps.js';
|
|
46
|
+
|
|
47
|
+
export { MIGRATION_VERSION, isPerDocumentAssertion, ASSERTION_BOUNDS_DEFAULT };
|
|
38
48
|
|
|
39
49
|
/**
|
|
40
50
|
* The physical mapping a connection's driver imposes on derived index
|
|
@@ -44,22 +54,30 @@ import { entityCore } from './entity.js';
|
|
|
44
54
|
* @param {any} connection
|
|
45
55
|
* @returns {{ derived: 'virtual' | 'stored', rtree: boolean }}
|
|
46
56
|
*/
|
|
47
|
-
function mappingFor(connection) {
|
|
57
|
+
function mappingFor(connection, expressions = undefined) {
|
|
58
|
+
const registered = connection.capabilities?.deterministicIndexableFunctions === true;
|
|
48
59
|
return {
|
|
49
|
-
derived:
|
|
50
|
-
? 'virtual' : 'stored',
|
|
60
|
+
derived: registered ? 'virtual' : 'stored',
|
|
51
61
|
// the same reasoning for the R*Tree mapping: a build without the
|
|
52
62
|
// module plans (and verifies) the B-tree shape
|
|
53
63
|
rtree: connection.capabilities?.rtree === true,
|
|
64
|
+
// and the same for a declared index expression: this connection
|
|
65
|
+
// either computes it or calls the engine's own immutable function
|
|
66
|
+
expressions,
|
|
67
|
+
registered,
|
|
54
68
|
};
|
|
55
69
|
}
|
|
56
70
|
|
|
57
|
-
/** The migration format version. */
|
|
58
|
-
export const MIGRATION_VERSION = '0.1';
|
|
59
|
-
|
|
60
71
|
/** The history table name (outside the model's identifier namespace
|
|
61
72
|
* conventions on purpose — a collection cannot collide with it). */
|
|
62
73
|
export const HISTORY_TABLE = '_jaren_migrations';
|
|
74
|
+
/** The tables the engine owns beside a model's: never a shape-drift finding. */
|
|
75
|
+
/** The tables this package owns. A model never declared one, so one
|
|
76
|
+
* found in a database is the engine's own bookkeeping rather than
|
|
77
|
+
* anybody's drift — the drift check skips them and the introspector
|
|
78
|
+
* does not derive them. */
|
|
79
|
+
export const ENGINE_TABLES = new Set([HISTORY_TABLE, CHANGES_TABLE, CHANGES_STATE_TABLE,
|
|
80
|
+
JOBS_TABLE, JOB_CHECKPOINTS_TABLE]);
|
|
63
81
|
|
|
64
82
|
/**
|
|
65
83
|
* The signature-grade identity of a model SHAPE.
|
|
@@ -162,7 +180,8 @@ function deriveStep(collection, plan, columnNames, note) {
|
|
|
162
180
|
* @param {any} fromModel
|
|
163
181
|
* @param {any} toModel
|
|
164
182
|
* @param {{ id?: string, dialect?: any,
|
|
165
|
-
* derived?: 'virtual' | 'stored', rtree?: boolean
|
|
183
|
+
* derived?: 'virtual' | 'stored', rtree?: boolean,
|
|
184
|
+
* expressions?: Record<string, any> }} [options]
|
|
166
185
|
* @returns {{ migration: any, report: {
|
|
167
186
|
* renamed: { from: string, to: string }[],
|
|
168
187
|
* added: string[], removed: string[],
|
|
@@ -173,9 +192,13 @@ export function planMigration(fromModel, toModel, options = undefined) {
|
|
|
173
192
|
const dialect = options?.dialect ?? null;
|
|
174
193
|
if (dialect === null || typeof dialect !== 'object')
|
|
175
194
|
throw new TypeError('planMigration needs { dialect } (the store dialect renders the DDL)');
|
|
176
|
-
const mapping = { derived: options?.derived ?? 'virtual', rtree: options?.rtree !== false
|
|
177
|
-
|
|
178
|
-
|
|
195
|
+
const mapping = { derived: options?.derived ?? 'virtual', rtree: options?.rtree !== false,
|
|
196
|
+
// a model that declares an index EXPRESSION resolves its functions
|
|
197
|
+
// here too: a plan is DDL, and DDL over a function this planner was
|
|
198
|
+
// not told about is DDL nobody can apply
|
|
199
|
+
expressions: options?.expressions, registered: options?.derived !== 'stored' };
|
|
200
|
+
const fromCollections = normalizeModel(fromModel, options?.expressions);
|
|
201
|
+
const toCollections = normalizeModel(toModel, options?.expressions);
|
|
179
202
|
|
|
180
203
|
const steps = [];
|
|
181
204
|
const report = {
|
|
@@ -829,15 +852,19 @@ function renderRebuild(name, fromName, fm, tm, fromMapping, toMapping, dialect,
|
|
|
829
852
|
* equality compares against, and the tests.
|
|
830
853
|
* @param {any} connection
|
|
831
854
|
* @param {any} model
|
|
855
|
+
* @param {Record<string, any>} [expressions] - the host's declared
|
|
856
|
+
* index-expression functions, resolved into the DDL the same way the
|
|
857
|
+
* open path resolves them
|
|
832
858
|
* @returns {any} value-or-promise
|
|
833
859
|
*/
|
|
834
|
-
export function createModelShape(connection, model) {
|
|
860
|
+
export function createModelShape(connection, model, expressions = undefined) {
|
|
835
861
|
const dialect = connection.dialect;
|
|
836
862
|
/** @type {string[]} */
|
|
837
863
|
const statements = [];
|
|
838
|
-
for (const collection of normalizeModel(model).values()) {
|
|
864
|
+
for (const collection of normalizeModel(model, expressions).values()) {
|
|
839
865
|
statements.push(
|
|
840
|
-
...planCollection(collection.name, collection, dialect,
|
|
866
|
+
...planCollection(collection.name, collection, dialect,
|
|
867
|
+
mappingFor(connection, expressions)).createSql);
|
|
841
868
|
}
|
|
842
869
|
const entities = normalizeEntities(model);
|
|
843
870
|
if (entities.size > 0) {
|
|
@@ -870,7 +897,9 @@ export function schemaShapeOf(connection) {
|
|
|
870
897
|
const dialect = connection.dialect;
|
|
871
898
|
return chain(connection.prepare(dialect.introspect.schemaDump()), (statement) =>
|
|
872
899
|
chain(statement.all([]), (rows) => rows
|
|
873
|
-
|
|
900
|
+
// the engine's own tables — history, the change log and its state
|
|
901
|
+
// row, the job queue — are never a model's drift
|
|
902
|
+
.filter((row) => !ENGINE_TABLES.has(String(row.name)) && !ENGINE_TABLES.has(String(row.owner)))
|
|
874
903
|
.map((row) => ({
|
|
875
904
|
type: String(row.type),
|
|
876
905
|
name: String(row.name),
|
|
@@ -935,14 +964,22 @@ function normalizeSchemaSql(sql) {
|
|
|
935
964
|
* @param {((connection: any) => any) | undefined} registerFunctions
|
|
936
965
|
* @returns {any} value-or-promise of `string | null`
|
|
937
966
|
*/
|
|
938
|
-
export function compareShapeToModel(driver, connection, model, registerFunctions) {
|
|
967
|
+
export function compareShapeToModel(driver, connection, model, registerFunctions, expressions) {
|
|
968
|
+
// The comparison IS a text comparison: it builds the model's shape in
|
|
969
|
+
// a reference database and compares the two engines' stored CREATE
|
|
970
|
+
// statements. An engine that keeps none has nothing to compare, and
|
|
971
|
+
// says so here rather than reading an undefined statement — the
|
|
972
|
+
// structural drift check (columns, indexes, foreign-key tuples) runs
|
|
973
|
+
// per collection and per entity either way, and `declaredSqlText` is
|
|
974
|
+
// what tells a reader which half they got.
|
|
975
|
+
if (connection.dialect.capabilities.declaredSqlText !== true) return null;
|
|
939
976
|
return chain(driver.open(':memory:', {}), (reference) =>
|
|
940
977
|
chain(chain(registerDeriveFunctions(reference),
|
|
941
978
|
() => (registerFunctions !== undefined ? registerFunctions(reference) : null)), () => {
|
|
942
979
|
const finish = (result) => chain(reference.close(), () => result);
|
|
943
980
|
let outcome;
|
|
944
981
|
try {
|
|
945
|
-
outcome = chain(createModelShape(reference, model), () =>
|
|
982
|
+
outcome = chain(createModelShape(reference, model, expressions), () =>
|
|
946
983
|
chain(schemaShapeOf(reference), (wanted) =>
|
|
947
984
|
chain(schemaShapeOf(connection), (actual) => {
|
|
948
985
|
const wantedText = JSON.stringify(wanted);
|
|
@@ -975,54 +1012,6 @@ export function compareShapeToModel(driver, connection, model, registerFunctions
|
|
|
975
1012
|
}));
|
|
976
1013
|
}
|
|
977
1014
|
|
|
978
|
-
const STEP_KINDS = new Set(['ddl', 'jslt', 'query', 'sql', 'rebuild', 'derive']);
|
|
979
|
-
|
|
980
|
-
/**
|
|
981
|
-
* Structural validation of one migration document, including the
|
|
982
|
-
* draft refusal (`JD0021`).
|
|
983
|
-
* @param {any} migration
|
|
984
|
-
*/
|
|
985
|
-
function checkMigrationDocument(migration) {
|
|
986
|
-
if (migration === null || typeof migration !== 'object'
|
|
987
|
-
|| migration.$migration !== MIGRATION_VERSION
|
|
988
|
-
|| typeof migration.id !== 'string' || migration.id === ''
|
|
989
|
-
|| typeof migration.from !== 'string' || typeof migration.to !== 'string'
|
|
990
|
-
|| !Array.isArray(migration.steps)) {
|
|
991
|
-
throw refuse('JD0023',
|
|
992
|
-
`migration '${migration?.id ?? '<unknown>'}' is not a valid ${MIGRATION_VERSION} migration document`);
|
|
993
|
-
}
|
|
994
|
-
for (let i = 0; i < migration.steps.length; i++) {
|
|
995
|
-
const step = migration.steps[i];
|
|
996
|
-
if (step === null || typeof step !== 'object' || !STEP_KINDS.has(step.kind)) {
|
|
997
|
-
throw refuse('JD0023',
|
|
998
|
-
`migration '${migration.id}' step ${i} has no recognised kind`);
|
|
999
|
-
}
|
|
1000
|
-
if (step.kind === 'rebuild'
|
|
1001
|
-
&& (typeof step.table !== 'string' || !Array.isArray(step.create)
|
|
1002
|
-
|| typeof step.copy !== 'string' || !Array.isArray(step.indexes))) {
|
|
1003
|
-
throw refuse('JD0023',
|
|
1004
|
-
`migration '${migration.id}' step ${i} is a rebuild without its rendered `
|
|
1005
|
-
+ 'table/create/copy/indexes');
|
|
1006
|
-
}
|
|
1007
|
-
if (step.kind === 'sql' && typeof step.sql !== 'string') {
|
|
1008
|
-
throw refuse('JD0023',
|
|
1009
|
-
`migration '${migration.id}' step ${i} is a sql step without sql text`);
|
|
1010
|
-
}
|
|
1011
|
-
if (step.kind === 'derive'
|
|
1012
|
-
&& (typeof step.collection !== 'string' || !Array.isArray(step.columns)
|
|
1013
|
-
|| step.columns.length === 0)) {
|
|
1014
|
-
throw refuse('JD0023',
|
|
1015
|
-
`migration '${migration.id}' step ${i} is a derive backfill without its columns`);
|
|
1016
|
-
}
|
|
1017
|
-
if (step.kind === 'jslt' && step.draft === true) {
|
|
1018
|
-
throw refuse('JD0021',
|
|
1019
|
-
`migration '${migration.id}' step ${i} is a DRAFT transform for collection `
|
|
1020
|
-
+ `'${step.collection}' — the planner cannot infer a data transform; fill in `
|
|
1021
|
-
+ 'the stylesheet (or delete the step for a pure widening) and remove "draft"');
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
1015
|
/**
|
|
1027
1016
|
* The batched row walk shared by transforms and post-validation:
|
|
1028
1017
|
* `SELECT rowid, json(doc) ... WHERE rowid > ? ORDER BY rowid LIMIT ?`
|
|
@@ -1032,9 +1021,14 @@ function checkMigrationDocument(migration) {
|
|
|
1032
1021
|
* @param {number} batchSize
|
|
1033
1022
|
* @param {(rows: { rid: any, doc: string, key: any }[]) => any} handle
|
|
1034
1023
|
* value-or-promise per batch
|
|
1024
|
+
* @param {boolean} [keyed]
|
|
1025
|
+
* @param {any} [entityMapping]
|
|
1026
|
+
* @param {() => void} [check] - run before every batch: the
|
|
1027
|
+
* cancellation boundary a data step has between its batches
|
|
1035
1028
|
* @returns {any}
|
|
1036
1029
|
*/
|
|
1037
|
-
function walkRows(connection, table, batchSize, handle, keyed = true, entityMapping = null
|
|
1030
|
+
function walkRows(connection, table, batchSize, handle, keyed = true, entityMapping = null,
|
|
1031
|
+
check = undefined) {
|
|
1038
1032
|
// entity tables carry no 'key' column — the transform walk goes by
|
|
1039
1033
|
// row identity alone; only the collection walks select the key. An
|
|
1040
1034
|
// entity's mapped columns ride beside the document so the row can be
|
|
@@ -1046,18 +1040,23 @@ function walkRows(connection, table, batchSize, handle, keyed = true, entityMapp
|
|
|
1046
1040
|
const keySelect = keyed ? `, ${q('key')} AS ${q('k')}` : '';
|
|
1047
1041
|
const columnSelect = entityMapping === null ? '' : entityColumnsOf(entityMapping)
|
|
1048
1042
|
.map((column) => `, ${q(column)}`).join('');
|
|
1049
|
-
const
|
|
1050
|
-
+ `${keySelect}${columnSelect} FROM ${q(table)}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1043
|
+
const select = `SELECT ${rid} AS ${q('rid')}, ${dialect.jsonText(q('doc'))} AS ${q('doc')}`
|
|
1044
|
+
+ `${keySelect}${columnSelect} FROM ${q(table)}`;
|
|
1045
|
+
const ordered = ` ORDER BY ${rid} ${dialect.limitClause(batchSize, undefined)}`;
|
|
1046
|
+
// An INTEGER PRIMARY KEY can be negative. The first batch has no
|
|
1047
|
+
// lower bound; subsequent batches seek from a row actually read.
|
|
1048
|
+
return chain(connection.prepare(select + ordered), (first) => chain(connection.prepare(
|
|
1049
|
+
`${select} WHERE ${rid} > ${dialect.parameterRef(1, 'after')}${ordered}`), (statement) => {
|
|
1050
|
+
const nextBatch = (after) => {
|
|
1051
|
+
if (check !== undefined) check();
|
|
1052
|
+
return chain(after === undefined ? first.all([]) : statement.all([after]), (rows) => {
|
|
1055
1053
|
if (rows.length === 0) return null;
|
|
1056
1054
|
return chain(handle(rows), () =>
|
|
1057
1055
|
nextBatch(rows[rows.length - 1].rid));
|
|
1058
1056
|
});
|
|
1059
|
-
|
|
1060
|
-
|
|
1057
|
+
};
|
|
1058
|
+
return nextBatch(undefined);
|
|
1059
|
+
}));
|
|
1061
1060
|
}
|
|
1062
1061
|
|
|
1063
1062
|
/** The physical columns an entity row carries beside its document. */
|
|
@@ -1085,21 +1084,6 @@ function entityStepMapping(options, table) {
|
|
|
1085
1084
|
return { entity, mapping: explainMapping(options.model).entities[table] };
|
|
1086
1085
|
}
|
|
1087
1086
|
|
|
1088
|
-
/** All documents of a collection or entity (the assertion steps' working
|
|
1089
|
-
* set — a documented whole-collection read), entities read WHOLE. */
|
|
1090
|
-
function allDocs(connection, table, entityMapping = null) {
|
|
1091
|
-
const dialect = connection.dialect;
|
|
1092
|
-
const q = dialect.quoteIdentifier;
|
|
1093
|
-
const columnSelect = entityMapping === null ? '' : entityColumnsOf(entityMapping)
|
|
1094
|
-
.map((column) => `, ${q(column)}`).join('');
|
|
1095
|
-
const sql = `SELECT ${dialect.jsonText(q('doc'))} AS ${q('doc')}${columnSelect} FROM ${q(table)} `
|
|
1096
|
-
+ `ORDER BY ${dialect.rowIdentity()}`;
|
|
1097
|
-
return chain(connection.prepare(sql), (statement) =>
|
|
1098
|
-
chain(statement.all([]), (rows) => rows.map((row) => (entityMapping === null
|
|
1099
|
-
? JSON.parse(row.doc)
|
|
1100
|
-
: mergeEntityRow(entityMapping, row, 'doc')))));
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
1087
|
/**
|
|
1104
1088
|
* Run one migration's steps against a connection.
|
|
1105
1089
|
* @param {any} connection
|
|
@@ -1112,6 +1096,9 @@ function runSteps(connection, migration, options) {
|
|
|
1112
1096
|
const q = dialect.quoteIdentifier;
|
|
1113
1097
|
const step = (i) => {
|
|
1114
1098
|
if (i >= migration.steps.length) return null;
|
|
1099
|
+
// the cancellation boundary between steps: a refusal here rolls the
|
|
1100
|
+
// migration in flight back whole, as any step failure does
|
|
1101
|
+
if (options.check !== undefined) options.check();
|
|
1115
1102
|
const current = migration.steps[i];
|
|
1116
1103
|
const fail = (reason, cause) => {
|
|
1117
1104
|
throw refuse('JD0023',
|
|
@@ -1146,6 +1133,7 @@ function runSteps(connection, migration, options) {
|
|
|
1146
1133
|
];
|
|
1147
1134
|
const runNext = (j) => {
|
|
1148
1135
|
if (j >= statements.length) {
|
|
1136
|
+
if (dialect.capabilities.foreignKeysAlwaysOn === true) return null;
|
|
1149
1137
|
return chain(connection.prepare(dialect.pragma.foreignKeyCheck()),
|
|
1150
1138
|
(checkStatement) => chain(checkStatement.all([]), (violations) => {
|
|
1151
1139
|
if (violations.length > 0) {
|
|
@@ -1191,24 +1179,23 @@ function runSteps(connection, migration, options) {
|
|
|
1191
1179
|
collection: current.collection,
|
|
1192
1180
|
derived: derivedRows,
|
|
1193
1181
|
});
|
|
1194
|
-
}, false), () => derivedRows));
|
|
1182
|
+
}, false, null, options.check), () => derivedRows));
|
|
1195
1183
|
}
|
|
1196
1184
|
if (current.kind === 'jslt') {
|
|
1197
|
-
let transform;
|
|
1198
|
-
try {
|
|
1199
|
-
transform = compileJsltStylesheet(current.stylesheet);
|
|
1200
|
-
}
|
|
1201
|
-
catch (cause) {
|
|
1202
|
-
return fail(`the stylesheet does not compile: ${/** @type {Error} */ (cause).message}`,
|
|
1203
|
-
/** @type {Error} */ (cause));
|
|
1204
|
-
}
|
|
1205
1185
|
const stepEntity = entityStepMapping(options, current.collection);
|
|
1186
|
+
const operation = compileDocumentStep(current, i, {
|
|
1187
|
+
migrationId: migration.id,
|
|
1188
|
+
compileJslt: compileJsltStylesheet,
|
|
1189
|
+
compileQuery: compileJsonQuery,
|
|
1190
|
+
keys: stepEntity === null ? [] : stepEntity.mapping.keys,
|
|
1191
|
+
});
|
|
1206
1192
|
if (stepEntity !== null) {
|
|
1207
1193
|
// an entity row is transformed WHOLE: the mapped columns fold in
|
|
1208
1194
|
// before the stylesheet and split out after it, through the
|
|
1209
1195
|
// entity's own split — a column-mapped member the stylesheet
|
|
1210
1196
|
// wrote used to land in the document and be shadowed on read
|
|
1211
|
-
const core = entityCore(connection, stepEntity.entity, stepEntity.mapping, null
|
|
1197
|
+
const core = entityCore(connection, stepEntity.entity, stepEntity.mapping, null,
|
|
1198
|
+
options.runtime);
|
|
1212
1199
|
const columns = entityColumnsOf(stepEntity.mapping);
|
|
1213
1200
|
const assignments = [
|
|
1214
1201
|
...columns.map((column, i) => `${q(column)} = ${dialect.parameterRef(i + 1, 'v')}`),
|
|
@@ -1221,19 +1208,7 @@ function runSteps(connection, migration, options) {
|
|
|
1221
1208
|
chain(walkRows(connection, current.collection, options.batchSize, (rows) => {
|
|
1222
1209
|
for (const row of rows) {
|
|
1223
1210
|
const whole = mergeEntityRow(stepEntity.mapping, row, 'doc');
|
|
1224
|
-
const next =
|
|
1225
|
-
if (next === null || typeof next !== 'object' || Array.isArray(next))
|
|
1226
|
-
fail(`the transform produced a non-document for row ${row.rid}`);
|
|
1227
|
-
// the key is the row's identity, not the document's to
|
|
1228
|
-
// change: a body that leaves it out keeps it, a body
|
|
1229
|
-
// that rewrites it is refused, as a collection's key is
|
|
1230
|
-
for (const key of stepEntity.mapping.keys) {
|
|
1231
|
-
if (next[key] === undefined) next[key] = whole[key];
|
|
1232
|
-
else if (next[key] !== whole[key]) {
|
|
1233
|
-
fail(`the transform changed the key member '${key}' of row ${row.rid} — `
|
|
1234
|
-
+ `key changes are not supported in ${MIGRATION_VERSION}`);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1211
|
+
const next = operation.apply(whole, row.rid);
|
|
1237
1212
|
const { values, rest } = core.plan.split(next);
|
|
1238
1213
|
const byName = new Map(values.map((value) => [value.name, value.value]));
|
|
1239
1214
|
update.run([...columns.map((column) => byName.get(column) ?? null),
|
|
@@ -1245,7 +1220,7 @@ function runSteps(connection, migration, options) {
|
|
|
1245
1220
|
collection: current.collection,
|
|
1246
1221
|
transformed,
|
|
1247
1222
|
});
|
|
1248
|
-
}, false, stepEntity.mapping), () => transformed));
|
|
1223
|
+
}, false, stepEntity.mapping, options.check), () => transformed));
|
|
1249
1224
|
}
|
|
1250
1225
|
const updateSql = `UPDATE ${q(current.collection)} SET ${q('doc')} = `
|
|
1251
1226
|
+ `${dialect.jsonEncode(dialect.parameterRef(1, 'doc'))} `
|
|
@@ -1255,9 +1230,7 @@ function runSteps(connection, migration, options) {
|
|
|
1255
1230
|
return chain(connection.prepare(updateSql), (update) =>
|
|
1256
1231
|
chain(walkRows(connection, current.collection, options.batchSize, (rows) => {
|
|
1257
1232
|
for (const row of rows) {
|
|
1258
|
-
const next =
|
|
1259
|
-
if (next === null || typeof next !== 'object' || Array.isArray(next))
|
|
1260
|
-
fail(`the transform produced a non-document for row ${row.rid}`);
|
|
1233
|
+
const next = operation.apply(JSON.parse(row.doc), row.rid);
|
|
1261
1234
|
update.run([JSON.stringify(next), row.rid]);
|
|
1262
1235
|
transformed++;
|
|
1263
1236
|
}
|
|
@@ -1266,30 +1239,66 @@ function runSteps(connection, migration, options) {
|
|
|
1266
1239
|
collection: current.collection,
|
|
1267
1240
|
transformed,
|
|
1268
1241
|
});
|
|
1269
|
-
}, keyed), () => transformed));
|
|
1242
|
+
}, keyed, null, options.check), () => transformed));
|
|
1270
1243
|
}
|
|
1271
1244
|
// kind === 'query': the assertion step
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1245
|
+
const assertionMapping = entityStepMapping(options, current.collection)?.mapping ?? null;
|
|
1246
|
+
const operation = compileDocumentStep(current, i, {
|
|
1247
|
+
migrationId: migration.id,
|
|
1248
|
+
compileJslt: compileJsltStylesheet,
|
|
1249
|
+
compileQuery: compileJsonQuery,
|
|
1250
|
+
});
|
|
1251
|
+
const assertOver = operation.assert;
|
|
1252
|
+
const readDoc = (row) => (assertionMapping === null
|
|
1253
|
+
? JSON.parse(row.doc)
|
|
1254
|
+
: mergeEntityRow(assertionMapping, row, 'doc'));
|
|
1255
|
+
|
|
1256
|
+
if (operation.fold !== null) {
|
|
1257
|
+
// an associative aggregate: each batch is answered by the engine
|
|
1258
|
+
// and the partial answers combine, so the collection is never
|
|
1259
|
+
// held. The whole-collection read this replaces is the one
|
|
1260
|
+
// statement a cross-document assertion used to cost.
|
|
1261
|
+
let accumulated = operation.fold.start();
|
|
1262
|
+
let folded = 0;
|
|
1263
|
+
return chain(walkRows(connection, current.collection, options.batchSize, (rows) => {
|
|
1264
|
+
accumulated = operation.fold.combine(accumulated, rows.map(readDoc));
|
|
1265
|
+
folded += rows.length;
|
|
1266
|
+
options.onProgress?.({
|
|
1267
|
+
migration: migration.id,
|
|
1268
|
+
collection: current.collection,
|
|
1269
|
+
asserted: folded,
|
|
1270
|
+
});
|
|
1271
|
+
}, false, assertionMapping, options.check), () => operation.fold.finish(accumulated));
|
|
1275
1272
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1273
|
+
|
|
1274
|
+
if (!operation.perDocument) {
|
|
1275
|
+
// materializing: the answer needs every document at once. That is
|
|
1276
|
+
// a cost, so it is bounded and the bound is crossed BEFORE the
|
|
1277
|
+
// excess is held — the walk stops at the row that would break it
|
|
1278
|
+
const guard = createAssertionBoundGuard(options.assertionBounds, current.collection,
|
|
1279
|
+
`the assertion of migration '${migration.id}' step ${i}`);
|
|
1280
|
+
const gathered = [];
|
|
1281
|
+
return chain(walkRows(connection, current.collection, options.batchSize, (rows) => {
|
|
1282
|
+
for (const row of rows) {
|
|
1283
|
+
const doc = readDoc(row);
|
|
1284
|
+
guard.admit(doc, typeof row.doc === 'string' ? row.doc : undefined);
|
|
1285
|
+
gathered.push(doc);
|
|
1286
|
+
}
|
|
1287
|
+
}, false, assertionMapping, options.check), () => assertOver(gathered));
|
|
1279
1288
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
});
|
|
1289
|
+
// per-document: walk in keyset batches like every other step,
|
|
1290
|
+
// failing fast at the first batch that violates
|
|
1291
|
+
let asserted = 0;
|
|
1292
|
+
return walkRows(connection, current.collection, options.batchSize, (rows) => {
|
|
1293
|
+
const docs = rows.map(readDoc);
|
|
1294
|
+
assertOver(docs);
|
|
1295
|
+
asserted += docs.length;
|
|
1296
|
+
options.onProgress?.({
|
|
1297
|
+
migration: migration.id,
|
|
1298
|
+
collection: current.collection,
|
|
1299
|
+
asserted,
|
|
1300
|
+
});
|
|
1301
|
+
}, false, assertionMapping, options.check);
|
|
1293
1302
|
}), () => step(i + 1));
|
|
1294
1303
|
};
|
|
1295
1304
|
return step(0);
|
|
@@ -1307,10 +1316,9 @@ function runSteps(connection, migration, options) {
|
|
|
1307
1316
|
* @returns {any} value-or-promise
|
|
1308
1317
|
*/
|
|
1309
1318
|
function validateTargetState(connection, model, options) {
|
|
1310
|
-
const collections = [...normalizeModel(model).values()];
|
|
1319
|
+
const collections = [...normalizeModel(model, options.expressions).values()];
|
|
1311
1320
|
const entities = [...normalizeEntities(model).values()];
|
|
1312
1321
|
const dialect = connection.dialect;
|
|
1313
|
-
const q = dialect.quoteIdentifier;
|
|
1314
1322
|
// entity tables carry no 'key' column; the batched walk goes by row
|
|
1315
1323
|
// identity and validates every stored document against the target
|
|
1316
1324
|
const verifyEntity = (i) => {
|
|
@@ -1324,33 +1332,23 @@ function validateTargetState(connection, model, options) {
|
|
|
1324
1332
|
// schema judges; the rest-document alone failed every entity whose
|
|
1325
1333
|
// required members are columns, so a pure widening could not land
|
|
1326
1334
|
const entityMapping = explainMapping(model).entities[entity.name];
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
const valid = outcome === true || outcome?.valid === true;
|
|
1339
|
-
if (!valid) {
|
|
1340
|
-
throw refuse('JD0021',
|
|
1341
|
-
`entity '${entity.name}': a stored document (row ${row.rid}) does not `
|
|
1342
|
-
+ 'validate against the target schema — a narrowing needs a data transform');
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
return nextBatch(rows[rows.length - 1].rid);
|
|
1346
|
-
});
|
|
1347
|
-
return nextBatch(-1);
|
|
1348
|
-
});
|
|
1335
|
+
return chain(walkRows(connection, entity.name, options.batchSize, (rows) => {
|
|
1336
|
+
for (const row of rows) {
|
|
1337
|
+
const outcome = validate(mergeEntityRow(entityMapping, row, 'doc'));
|
|
1338
|
+
const valid = outcome === true || outcome?.valid === true;
|
|
1339
|
+
if (!valid) {
|
|
1340
|
+
throw refuse('JD0021',
|
|
1341
|
+
`entity '${entity.name}': a stored document (row ${row.rid}) does not `
|
|
1342
|
+
+ 'validate against the target schema — a narrowing needs a data transform');
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}, false, entityMapping), () => verifyEntity(i + 1));
|
|
1349
1346
|
};
|
|
1350
1347
|
const verifyNext = (i) => {
|
|
1351
1348
|
if (i >= collections.length) return null;
|
|
1352
1349
|
const collection = collections[i];
|
|
1353
|
-
const plan = planCollection(collection.name, collection, dialect,
|
|
1350
|
+
const plan = planCollection(collection.name, collection, dialect,
|
|
1351
|
+
mappingFor(connection, options.expressions));
|
|
1354
1352
|
const validate = options.compileSchema !== undefined
|
|
1355
1353
|
? options.compileSchema(collection.schema)
|
|
1356
1354
|
: null;
|
|
@@ -1408,8 +1406,13 @@ function replayOnShadow(driver, shadowPath, baseline, migrations, model, options
|
|
|
1408
1406
|
(options.registerFunctions !== undefined ? options.registerFunctions(shadow) : null));
|
|
1409
1407
|
const apply = (i) => {
|
|
1410
1408
|
if (i >= migrations.length) return null;
|
|
1409
|
+
// a rebuild moves rows between tables while their keys point at
|
|
1410
|
+
// the old one, so the switch comes off around it — on an engine
|
|
1411
|
+
// that HAS a switch. One that always enforces cannot rebuild that
|
|
1412
|
+
// way, and `alterTableFull` is why it never has to
|
|
1411
1413
|
const bracket = migrations[i].steps.some(
|
|
1412
|
-
(candidate) => candidate.kind === 'rebuild')
|
|
1414
|
+
(candidate) => candidate.kind === 'rebuild')
|
|
1415
|
+
&& shadow.dialect.capabilities.foreignKeysAlwaysOn !== true;
|
|
1413
1416
|
return chain(
|
|
1414
1417
|
bracket ? shadow.exec(shadow.dialect.pragma.foreignKeys(false)) : null,
|
|
1415
1418
|
() => chain(runSteps(shadow, migrations[i], options), () =>
|
|
@@ -1417,13 +1420,13 @@ function replayOnShadow(driver, shadowPath, baseline, migrations, model, options
|
|
|
1417
1420
|
() => apply(i + 1))));
|
|
1418
1421
|
};
|
|
1419
1422
|
const run = () => chain(registered, () =>
|
|
1420
|
-
chain(createModelShape(shadow, baseline), () => chain(apply(0), () => {
|
|
1423
|
+
chain(createModelShape(shadow, baseline, options.expressions), () => chain(apply(0), () => {
|
|
1421
1424
|
if (model === undefined) return null;
|
|
1422
|
-
const target = [...normalizeModel(model).values()];
|
|
1425
|
+
const target = [...normalizeModel(model, options.expressions).values()];
|
|
1423
1426
|
const verifyNext = (i) => {
|
|
1424
1427
|
if (i >= target.length) return null;
|
|
1425
1428
|
const plan = planCollection(target[i].name, target[i], shadow.dialect,
|
|
1426
|
-
mappingFor(shadow));
|
|
1429
|
+
mappingFor(shadow, options.expressions));
|
|
1427
1430
|
return chain(
|
|
1428
1431
|
verifyShape(shadow, plan, target[i].name, target[i].docPath),
|
|
1429
1432
|
() => verifyNext(i + 1));
|
|
@@ -1485,18 +1488,33 @@ function historyStatements(dialect) {
|
|
|
1485
1488
|
}
|
|
1486
1489
|
|
|
1487
1490
|
/**
|
|
1488
|
-
* Report a database's migration state without touching it
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1491
|
+
* Report a database's migration state without touching it — the
|
|
1492
|
+
* history table is probed, never created, so a fresh file stays byte
|
|
1493
|
+
* for byte what it was: what is applied, what is pending, whether an
|
|
1494
|
+
* applied migration was edited, and — once the chain is fully applied —
|
|
1495
|
+
* whether the physical shape DRIFTED from the model (someone changed
|
|
1496
|
+
* the database by hand, §12).
|
|
1492
1497
|
* @param {{ driver: any, path?: string }} target
|
|
1493
1498
|
* @param {any[]} migrations - the full ordered list
|
|
1494
|
-
* @param {{
|
|
1495
|
-
*
|
|
1499
|
+
* @param {{ model?: any, registerFunctions?: (connection: any) => any,
|
|
1500
|
+
* signal?: AbortSignal, deadline?: number,
|
|
1501
|
+
* runtime?: Partial<import('@jarenjs/core/runtime').Runtime> }} options
|
|
1502
|
+
* - `signal`/`deadline` refuse a call already cancelled (`JD2080`) or
|
|
1503
|
+
* past its deadline (`JD2075`) on the runtime record's clock
|
|
1496
1504
|
* @returns {Promise<{ applied: string[], pending: string[],
|
|
1497
1505
|
* drift: string | null, upToDate: boolean }>}
|
|
1498
1506
|
*/
|
|
1499
|
-
export function migrationStatus(target, migrations, options) {
|
|
1507
|
+
export function migrationStatus(target, migrations, options = {}) {
|
|
1508
|
+
// a call already cancelled, or past its deadline on the caller's
|
|
1509
|
+
// clock, opens nothing
|
|
1510
|
+
try {
|
|
1511
|
+
refuseCancelled({ signal: options.signal, deadline: options.deadline },
|
|
1512
|
+
resolveRuntime(options.runtime).now,
|
|
1513
|
+
{ abortCode: 'JD2080', aborted: 'it ran', passed: 'the status read ran', ran: 'no step ran' });
|
|
1514
|
+
}
|
|
1515
|
+
catch (error) {
|
|
1516
|
+
return Promise.reject(error);
|
|
1517
|
+
}
|
|
1500
1518
|
return toPromise(chain(
|
|
1501
1519
|
target.driver.open(target.path ?? ':memory:', {}),
|
|
1502
1520
|
(connection) => {
|
|
@@ -1506,10 +1524,15 @@ export function migrationStatus(target, migrations, options) {
|
|
|
1506
1524
|
const failClosed = (error) => chain(connection.close(), () => { throw error; });
|
|
1507
1525
|
let work;
|
|
1508
1526
|
try {
|
|
1527
|
+
// §6's "writes NOTHING" holds for a status read too: the history
|
|
1528
|
+
// table is probed, never created, and an absent one reads as an
|
|
1529
|
+
// empty history — the same promise the dry run makes
|
|
1509
1530
|
work = chain(registerDeriveFunctions(connection), () =>
|
|
1510
|
-
chain(connection.
|
|
1511
|
-
|
|
1512
|
-
|
|
1531
|
+
chain(chain(connection.prepare(dialect.introspect.tableExists()), (probe) =>
|
|
1532
|
+
chain(probe.get([HISTORY_TABLE]), (present) => (present === undefined
|
|
1533
|
+
? []
|
|
1534
|
+
: chain(connection.prepare(statements.select), (select) => select.all([]))))),
|
|
1535
|
+
(rows) => chain(rows, () => {
|
|
1513
1536
|
for (let i = 0; i < rows.length; i++) {
|
|
1514
1537
|
const doc = migrations[i];
|
|
1515
1538
|
if (doc === undefined || doc.id !== rows[i].id
|
|
@@ -1531,7 +1554,7 @@ export function migrationStatus(target, migrations, options) {
|
|
|
1531
1554
|
(difference) => ({
|
|
1532
1555
|
applied, pending, drift: difference, upToDate: difference === null,
|
|
1533
1556
|
}));
|
|
1534
|
-
})))
|
|
1557
|
+
})));
|
|
1535
1558
|
}
|
|
1536
1559
|
catch (error) {
|
|
1537
1560
|
return failClosed(error);
|
|
@@ -1556,7 +1579,17 @@ export function migrationStatus(target, migrations, options) {
|
|
|
1556
1579
|
* @param {any[]} migrations
|
|
1557
1580
|
* @param {{ baseline: any, model?: any, compileSchema?: Function,
|
|
1558
1581
|
* dryRun?: boolean, batchSize?: number, onProgress?: Function,
|
|
1559
|
-
* shadow?: boolean, shadowPath?: string
|
|
1582
|
+
* shadow?: boolean, shadowPath?: string, shadowDriver?: any,
|
|
1583
|
+
* signal?: AbortSignal, deadline?: number,
|
|
1584
|
+
* runtime?: Partial<import('@jarenjs/core/runtime').Runtime> }} options
|
|
1585
|
+
* `signal` and `deadline` cancel between migrations, steps and
|
|
1586
|
+
* batches (`JD2080` / `JD2075`, the deadline read against `runtime`'s
|
|
1587
|
+
* clock); a cancelled migration rolls back whole and the completed
|
|
1588
|
+
* ones stand.
|
|
1589
|
+
* `runtime` is the host's runtime record: the clock every applied
|
|
1590
|
+
* migration is stamped with, and the clock and identifiers an entity
|
|
1591
|
+
* step's `default: 'now'` / `default: 'uuid'` fill; the platform's own
|
|
1592
|
+
* when absent
|
|
1560
1593
|
* @returns {Promise<any>}
|
|
1561
1594
|
*/
|
|
1562
1595
|
export function migrate(target, migrations, options) {
|
|
@@ -1571,12 +1604,40 @@ export function migrate(target, migrations, options) {
|
|
|
1571
1604
|
'migrate needs { baseline }: the model the store was first created with '
|
|
1572
1605
|
+ '(the chain anchor and the shadow starting shape)');
|
|
1573
1606
|
const batchSize = options.batchSize ?? 500;
|
|
1607
|
+
const runtime = resolveRuntime(options.runtime);
|
|
1608
|
+
/**
|
|
1609
|
+
* The cancellation boundary: between migrations, between steps and
|
|
1610
|
+
* between the batches of a data step, on the runtime record's clock.
|
|
1611
|
+
* Nothing interrupts a statement that has started; a refusal inside a
|
|
1612
|
+
* migration rolls that migration back whole and the completed ones
|
|
1613
|
+
* stand, so a rerun resumes from the recorded position.
|
|
1614
|
+
*/
|
|
1615
|
+
const check = () => refuseCancelled({ signal: options.signal, deadline: options.deadline }, runtime.now, {
|
|
1616
|
+
abortCode: 'JD2080', aborted: 'its next step', passed: 'its next step',
|
|
1617
|
+
ran: 'no further step ran; a migration in flight rolled back whole and a rerun resumes '
|
|
1618
|
+
+ 'from the recorded position',
|
|
1619
|
+
});
|
|
1574
1620
|
const runOptions = {
|
|
1575
1621
|
batchSize,
|
|
1622
|
+
assertionBounds: normalizeAssertionBounds(options.assertionBounds),
|
|
1576
1623
|
onProgress: options.onProgress,
|
|
1577
1624
|
registerFunctions: options.registerFunctions,
|
|
1625
|
+
// the host's declared index-expression functions ride to every
|
|
1626
|
+
// planner and every connection this run opens — the shadow's
|
|
1627
|
+
// baseline, the reference database and the real store all resolve a
|
|
1628
|
+
// declared expression against the same declarations the open path
|
|
1629
|
+
// was given, or they would plan DDL nobody can apply
|
|
1630
|
+
expressions: options.expressions,
|
|
1578
1631
|
model: options.model,
|
|
1632
|
+
runtime,
|
|
1633
|
+
check,
|
|
1579
1634
|
};
|
|
1635
|
+
try {
|
|
1636
|
+
check();
|
|
1637
|
+
}
|
|
1638
|
+
catch (error) {
|
|
1639
|
+
return Promise.reject(error);
|
|
1640
|
+
}
|
|
1580
1641
|
|
|
1581
1642
|
return toPromise(chain(
|
|
1582
1643
|
target.driver.open(target.path ?? ':memory:', { timeout: target.busyTimeout ?? 5000 }),
|
|
@@ -1650,7 +1711,8 @@ export function migrate(target, migrations, options) {
|
|
|
1650
1711
|
|
|
1651
1712
|
const shadowRun = options.shadow === false
|
|
1652
1713
|
? null
|
|
1653
|
-
: replayOnShadow(
|
|
1714
|
+
: replayOnShadow(options.shadowDriver ?? target.driver,
|
|
1715
|
+
options.shadowPath ?? ':memory:',
|
|
1654
1716
|
options.baseline, migrations, options.model, runOptions);
|
|
1655
1717
|
|
|
1656
1718
|
return chain(shadowRun, () => {
|
|
@@ -1672,8 +1734,9 @@ export function migrate(target, migrations, options) {
|
|
|
1672
1734
|
dialect.ddl.dropTable(migrationStep.table),
|
|
1673
1735
|
dialect.ddl.renameTable(`${migrationStep.table}__rebuild`,
|
|
1674
1736
|
migrationStep.table),
|
|
1675
|
-
...migrationStep.indexes
|
|
1676
|
-
|
|
1737
|
+
...migrationStep.indexes);
|
|
1738
|
+
if (dialect.capabilities.foreignKeysAlwaysOn !== true)
|
|
1739
|
+
rendered.push(dialect.pragma.foreignKeyCheck());
|
|
1677
1740
|
}
|
|
1678
1741
|
else if (migrationStep.kind === 'jslt')
|
|
1679
1742
|
rendered.push(`-- jslt transform over '${migrationStep.collection}'`);
|
|
@@ -1707,6 +1770,8 @@ export function migrate(target, migrations, options) {
|
|
|
1707
1770
|
const applied = [];
|
|
1708
1771
|
const applyNext = (i) => {
|
|
1709
1772
|
if (i >= pending.length) return null;
|
|
1773
|
+
// the boundary between migrations
|
|
1774
|
+
check();
|
|
1710
1775
|
const migration = pending[i];
|
|
1711
1776
|
const last = i === pending.length - 1;
|
|
1712
1777
|
// the §10 procedure's pragma bracket, literally: the
|
|
@@ -1716,7 +1781,8 @@ export function migrate(target, migrations, options) {
|
|
|
1716
1781
|
const bracket = migration.steps.some(
|
|
1717
1782
|
(candidate) => candidate.kind === 'rebuild');
|
|
1718
1783
|
return chain(
|
|
1719
|
-
bracket
|
|
1784
|
+
bracket && dialect.capabilities.foreignKeysAlwaysOn !== true
|
|
1785
|
+
? connection.exec(dialect.pragma.foreignKeys(false)) : null,
|
|
1720
1786
|
() => chain(connection.exec(dialect.tx.beginImmediate), () => {
|
|
1721
1787
|
const body = () => chain(runSteps(connection, migration, runOptions), () =>
|
|
1722
1788
|
chain(last && options.model !== undefined
|
|
@@ -1733,10 +1799,11 @@ export function migrate(target, migrations, options) {
|
|
|
1733
1799
|
})))
|
|
1734
1800
|
: null,
|
|
1735
1801
|
() => chain(connection.prepare(statements.insert), (insert) =>
|
|
1736
|
-
insert.run([migration.id,
|
|
1802
|
+
insert.run([migration.id, runtime.now(), migration.from,
|
|
1737
1803
|
migration.to, migrationChecksum(migration),
|
|
1738
1804
|
migration.steps.length]))));
|
|
1739
1805
|
const restore = () => (bracket
|
|
1806
|
+
&& dialect.capabilities.foreignKeysAlwaysOn !== true
|
|
1740
1807
|
? connection.exec(dialect.pragma.foreignKeys(true)) : null);
|
|
1741
1808
|
const commit = () => chain(connection.exec(dialect.tx.commit), () =>
|
|
1742
1809
|
chain(restore(), () => {
|