@atscript/db 0.1.39 → 0.1.41
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/README.md +18 -18
- package/dist/agg.cjs +8 -3
- package/dist/agg.d.cts +7 -0
- package/dist/agg.d.mts +7 -0
- package/dist/agg.mjs +7 -3
- package/dist/control-DRgryKeg.cjs +14 -0
- package/dist/{control_as-bjmwe24C.mjs → control-IANbnfjG.mjs} +6 -18
- package/dist/db-readable-BQQzfguJ.d.cts +1249 -0
- package/dist/db-readable-Bbr4CjMb.d.mts +1249 -0
- package/dist/db-space-BUrQ5BFm.d.mts +309 -0
- package/dist/db-space-Vxpcnyt5.d.cts +309 -0
- package/dist/db-validator-plugin-07kDiis2.d.cts +22 -0
- package/dist/db-validator-plugin-CiqsHTI_.d.mts +22 -0
- package/dist/db-view-CMI9TOo1.cjs +3096 -0
- package/dist/db-view-Esy2fDxw.mjs +2995 -0
- package/dist/index.cjs +95 -2801
- package/dist/index.d.cts +137 -0
- package/dist/index.d.mts +137 -0
- package/dist/index.mjs +55 -2761
- package/dist/{nested-writer-BkqL7cp3.cjs → nested-writer-BDXsDMPP.cjs} +196 -150
- package/dist/{nested-writer-NEN51mnR.mjs → nested-writer-Dmm1gbZV.mjs} +118 -70
- package/dist/ops-BdRAFLKY.d.mts +67 -0
- package/dist/ops-DXJ4Zw0P.d.cts +67 -0
- package/dist/ops.cjs +123 -0
- package/dist/ops.d.cts +2 -0
- package/dist/ops.d.mts +2 -0
- package/dist/ops.mjs +112 -0
- package/dist/plugin.cjs +90 -109
- package/dist/plugin.d.cts +6 -0
- package/dist/plugin.d.mts +6 -0
- package/dist/plugin.mjs +29 -49
- package/dist/rel.cjs +20 -20
- package/dist/rel.d.cts +119 -0
- package/dist/rel.d.mts +119 -0
- package/dist/rel.mjs +4 -5
- package/dist/{relation-helpers-guFL_oRf.cjs → relation-helpers-BYvsE1tR.cjs} +26 -22
- package/dist/{relation-helpers-DyBIlQnB.mjs → relation-helpers-CLasawQq.mjs} +11 -6
- package/dist/{relation-loader-Dv7qXYq7.mjs → relation-loader-BEOTXNcq.mjs} +63 -43
- package/dist/{relation-loader-CpnDRf9k.cjs → relation-loader-CRC5LcqM.cjs} +74 -49
- package/dist/shared.cjs +13 -13
- package/dist/{shared.d.ts → shared.d.cts} +14 -13
- package/dist/shared.d.mts +71 -0
- package/dist/shared.mjs +2 -3
- package/dist/sync.cjs +300 -252
- package/dist/sync.d.cts +369 -0
- package/dist/sync.d.mts +369 -0
- package/dist/sync.mjs +284 -233
- package/dist/{validation-utils-DEoCMmEb.cjs → validation-utils-DVJDijnB.cjs} +141 -109
- package/dist/{validation-utils-DhR_mtKa.mjs → validation-utils-DhjIjP1-.mjs} +71 -37
- package/package.json +30 -29
- package/LICENSE +0 -21
- package/dist/agg-BJFJ3dFQ.mjs +0 -8
- package/dist/agg-DnUWAOK8.cjs +0 -14
- package/dist/agg.d.ts +0 -13
- package/dist/chunk-CrpGerW8.cjs +0 -31
- package/dist/control_as-BFPERAF_.cjs +0 -28
- package/dist/index.d.ts +0 -1706
- package/dist/logger-B7oxCfLQ.mjs +0 -12
- package/dist/logger-Dt2v_-wb.cjs +0 -18
- package/dist/plugin.d.ts +0 -5
- package/dist/rel.d.ts +0 -1305
- package/dist/relation-loader-D4mTw6yH.cjs +0 -4
- package/dist/relation-loader-Ggy1ujwR.mjs +0 -4
- package/dist/sync.d.ts +0 -1878
package/dist/sync.cjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//#region
|
|
5
|
-
/** Extracts sorted field snapshots from a readable's field descriptors. */
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
require("./nested-writer-BDXsDMPP.cjs");
|
|
3
|
+
const require_db_view = require("./db-view-CMI9TOo1.cjs");
|
|
4
|
+
//#region src/schema/schema-hash.ts
|
|
5
|
+
/** Extracts sorted field snapshots from a readable's field descriptors. */
|
|
6
|
+
function extractFieldSnapshots(fields, typeMapper) {
|
|
6
7
|
return fields.filter((f) => !f.ignored).map((f) => {
|
|
7
8
|
const snap = {
|
|
8
9
|
physicalName: f.physicalName,
|
|
@@ -14,8 +15,17 @@ const require_logger = require('./logger-Dt2v_-wb.cjs');
|
|
|
14
15
|
if (f.defaultValue) snap.defaultValue = f.defaultValue;
|
|
15
16
|
if (typeMapper) snap.mappedType = typeMapper(f);
|
|
16
17
|
return snap;
|
|
17
|
-
}).
|
|
18
|
+
}).toSorted((a, b) => a.physicalName.localeCompare(b.physicalName));
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Extracts a canonical, serializable snapshot from a readable's metadata.
|
|
22
|
+
* Sorted deterministically so the hash is stable across runs.
|
|
23
|
+
*
|
|
24
|
+
* @param readable - The table/view readable.
|
|
25
|
+
* @param typeMapper - Optional adapter-specific type mapper. When provided,
|
|
26
|
+
* each field's mapped type (e.g., "VARCHAR(255)") is stored in the snapshot
|
|
27
|
+
* for precise type change detection.
|
|
28
|
+
*/
|
|
19
29
|
function computeTableSnapshot(readable, typeMapper, tableOptions) {
|
|
20
30
|
const fields = extractFieldSnapshots(readable.fieldDescriptors, typeMapper);
|
|
21
31
|
const indexes = [...readable.indexes.values()].map((idx) => ({
|
|
@@ -25,23 +35,28 @@ function computeTableSnapshot(readable, typeMapper, tableOptions) {
|
|
|
25
35
|
name: f.name,
|
|
26
36
|
sort: f.sort
|
|
27
37
|
}))
|
|
28
|
-
})).
|
|
38
|
+
})).toSorted((a, b) => a.key.localeCompare(b.key));
|
|
29
39
|
const foreignKeys = [...readable.foreignKeys.values()].map((fk) => ({
|
|
30
|
-
fields: [...fk.fields].
|
|
40
|
+
fields: [...fk.fields].toSorted(),
|
|
31
41
|
targetTable: fk.targetTable,
|
|
32
|
-
targetFields: [...fk.targetFields].
|
|
42
|
+
targetFields: [...fk.targetFields].toSorted(),
|
|
33
43
|
onDelete: fk.onDelete,
|
|
34
44
|
onUpdate: fk.onUpdate
|
|
35
|
-
})).
|
|
45
|
+
})).toSorted((a, b) => a.fields.join(",").localeCompare(b.fields.join(",")));
|
|
36
46
|
const snapshot = {
|
|
37
47
|
tableName: readable.tableName,
|
|
38
48
|
fields,
|
|
39
49
|
indexes,
|
|
40
50
|
foreignKeys
|
|
41
51
|
};
|
|
42
|
-
if (tableOptions?.length) snapshot.tableOptions = [...tableOptions].
|
|
52
|
+
if (tableOptions?.length) snapshot.tableOptions = [...tableOptions].toSorted((a, b) => a.key.localeCompare(b.key));
|
|
43
53
|
return snapshot;
|
|
44
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Extracts a canonical, serializable snapshot from a view's metadata.
|
|
57
|
+
* Captures view plan (entry table, joins, filter, materialization) for
|
|
58
|
+
* detecting view definition changes.
|
|
59
|
+
*/
|
|
45
60
|
function computeViewSnapshot(view) {
|
|
46
61
|
const fields = extractFieldSnapshots(view.fieldDescriptors);
|
|
47
62
|
if (view.isExternal) return {
|
|
@@ -55,20 +70,35 @@ function computeViewSnapshot(view) {
|
|
|
55
70
|
viewType: plan.materialized ? "M" : "V",
|
|
56
71
|
entryTable: plan.entryTable,
|
|
57
72
|
joinTables: plan.joins.map((j) => j.targetTable),
|
|
58
|
-
materialized: plan.materialized ||
|
|
73
|
+
materialized: plan.materialized || void 0,
|
|
59
74
|
fields
|
|
60
75
|
};
|
|
61
76
|
if (plan.filter) result.filterHash = fnv1a(JSON.stringify(plan.filter, (_, v) => typeof v === "function" ? "[fn]" : v));
|
|
62
77
|
return result;
|
|
63
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Computes a deterministic hash string from multiple table snapshots.
|
|
81
|
+
* Uses FNV-1a for speed — not cryptographic, just needs stability + collision resistance.
|
|
82
|
+
*/
|
|
64
83
|
function computeSchemaHash(snapshots) {
|
|
65
|
-
const sorted = [...snapshots].
|
|
66
|
-
|
|
67
|
-
return fnv1a(json);
|
|
84
|
+
const sorted = [...snapshots].toSorted((a, b) => a.tableName.localeCompare(b.tableName));
|
|
85
|
+
return fnv1a(JSON.stringify(sorted));
|
|
68
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Computes a hash for a single table/view snapshot.
|
|
89
|
+
* Used for per-table change detection via stored snapshots.
|
|
90
|
+
*/
|
|
69
91
|
function computeTableHash(snapshot) {
|
|
70
92
|
return fnv1a(JSON.stringify(snapshot));
|
|
71
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Converts stored snapshot fields to `TExistingColumn[]` format
|
|
96
|
+
* for use with `computeColumnDiff`. Used by adapters that lack
|
|
97
|
+
* native column introspection (e.g., MongoDB).
|
|
98
|
+
*
|
|
99
|
+
* The `type` field uses `mappedType` when available (adapter-specific),
|
|
100
|
+
* falling back to `designType`.
|
|
101
|
+
*/
|
|
72
102
|
function snapshotToExistingColumns(snapshot) {
|
|
73
103
|
return snapshot.fields.map((f) => ({
|
|
74
104
|
name: f.physicalName,
|
|
@@ -78,15 +108,21 @@ function snapshotToExistingColumns(snapshot) {
|
|
|
78
108
|
dflt_value: serializeDefaultValue(f.defaultValue)
|
|
79
109
|
}));
|
|
80
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Extracts table options from a stored snapshot for diff comparison.
|
|
113
|
+
* Used as fallback when an adapter lacks native table option introspection.
|
|
114
|
+
*/
|
|
81
115
|
function snapshotToExistingTableOptions(snapshot) {
|
|
82
116
|
return snapshot.tableOptions ?? [];
|
|
83
117
|
}
|
|
118
|
+
/** Serializes a TDbDefaultValue to a comparable string. */
|
|
84
119
|
function serializeDefaultValue(dv) {
|
|
85
|
-
if (!dv) return
|
|
120
|
+
if (!dv) return;
|
|
86
121
|
if (dv.kind === "value") return dv.value;
|
|
87
122
|
return `fn:${dv.fn}`;
|
|
88
123
|
}
|
|
89
|
-
/** FNV-1a 32-bit hash → hex string */
|
|
124
|
+
/** FNV-1a 32-bit hash → hex string */
|
|
125
|
+
function fnv1a(str) {
|
|
90
126
|
let hash = 2166136261;
|
|
91
127
|
for (let i = 0; i < str.length; i++) {
|
|
92
128
|
hash ^= str.codePointAt(i);
|
|
@@ -94,13 +130,22 @@ function serializeDefaultValue(dv) {
|
|
|
94
130
|
}
|
|
95
131
|
return Math.trunc(hash).toString(16).padStart(8, "0");
|
|
96
132
|
}
|
|
97
|
-
|
|
98
133
|
//#endregion
|
|
99
|
-
//#region
|
|
134
|
+
//#region src/schema/column-diff.ts
|
|
135
|
+
/**
|
|
136
|
+
* Computes the difference between desired schema fields and existing database columns.
|
|
137
|
+
*
|
|
138
|
+
* @param desired - Field descriptors from the Atscript type (after flattening).
|
|
139
|
+
* @param existing - Columns currently in the database (from introspection).
|
|
140
|
+
* @param typeMapper - Optional function to map field metadata to DB-native type strings.
|
|
141
|
+
* Receives the full field meta (design type, annotations, PK status, etc.)
|
|
142
|
+
* so adapters can produce context-aware types (e.g., `VARCHAR(255)` from maxLength).
|
|
143
|
+
* Required for type change detection.
|
|
144
|
+
*/
|
|
100
145
|
function computeColumnDiff(desired, existing, typeMapper) {
|
|
101
146
|
const existingByName = new Map(existing.map((c) => [c.name, c]));
|
|
102
|
-
const desiredByName = new Map();
|
|
103
|
-
const renamedOldNames = new Set();
|
|
147
|
+
const desiredByName = /* @__PURE__ */ new Map();
|
|
148
|
+
const renamedOldNames = /* @__PURE__ */ new Set();
|
|
104
149
|
const added = [];
|
|
105
150
|
const renamed = [];
|
|
106
151
|
const typeChanged = [];
|
|
@@ -120,8 +165,7 @@ function computeColumnDiff(desired, existing, typeMapper) {
|
|
|
120
165
|
renamedOldNames.add(field.renamedFrom);
|
|
121
166
|
} else {
|
|
122
167
|
if (typeMapper) {
|
|
123
|
-
|
|
124
|
-
if (expectedType.toUpperCase() !== existingCol.type.toUpperCase()) typeChanged.push({
|
|
168
|
+
if (typeMapper(field).toUpperCase() !== existingCol.type.toUpperCase()) typeChanged.push({
|
|
125
169
|
field,
|
|
126
170
|
existingType: existingCol.type
|
|
127
171
|
});
|
|
@@ -134,13 +178,13 @@ function computeColumnDiff(desired, existing, typeMapper) {
|
|
|
134
178
|
});
|
|
135
179
|
}
|
|
136
180
|
const desiredDefault = serializeDefaultValue(field.defaultValue);
|
|
137
|
-
if (existingCol.dflt_value !==
|
|
181
|
+
if (existingCol.dflt_value !== void 0 && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
|
|
138
182
|
field,
|
|
139
183
|
oldDefault: existingCol.dflt_value,
|
|
140
184
|
newDefault: desiredDefault
|
|
141
185
|
});
|
|
142
186
|
}
|
|
143
|
-
else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
|
|
187
|
+
else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
|
|
144
188
|
renamed.push({
|
|
145
189
|
field,
|
|
146
190
|
oldName: field.renamedFrom
|
|
@@ -148,10 +192,9 @@ else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
|
|
|
148
192
|
renamedOldNames.add(field.renamedFrom);
|
|
149
193
|
} else added.push(field);
|
|
150
194
|
}
|
|
151
|
-
const removed = existing.filter((c) => !desiredByName.has(c.name) && !renamedOldNames.has(c.name));
|
|
152
195
|
return {
|
|
153
196
|
added,
|
|
154
|
-
removed,
|
|
197
|
+
removed: existing.filter((c) => !desiredByName.has(c.name) && !renamedOldNames.has(c.name)),
|
|
155
198
|
renamed,
|
|
156
199
|
typeChanged,
|
|
157
200
|
nullableChanged,
|
|
@@ -159,25 +202,30 @@ else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
|
|
|
159
202
|
conflicts
|
|
160
203
|
};
|
|
161
204
|
}
|
|
162
|
-
|
|
163
205
|
//#endregion
|
|
164
|
-
//#region
|
|
206
|
+
//#region src/schema/fk-diff.ts
|
|
207
|
+
/** Canonical key for an FK: sorted local field names, comma-joined. */
|
|
165
208
|
function fkKey(fields) {
|
|
166
|
-
return [...fields].
|
|
209
|
+
return [...fields].toSorted().join(",");
|
|
167
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Compares desired FK constraints against stored snapshot to detect
|
|
213
|
+
* additions, removals, and property changes (target table, target fields,
|
|
214
|
+
* onDelete, onUpdate).
|
|
215
|
+
*/
|
|
168
216
|
function computeForeignKeyDiff(desired, existingSnapshot) {
|
|
169
217
|
const added = [];
|
|
170
218
|
const removed = [];
|
|
171
219
|
const changed = [];
|
|
172
|
-
const existingByKey = new Map();
|
|
220
|
+
const existingByKey = /* @__PURE__ */ new Map();
|
|
173
221
|
for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
|
|
174
|
-
const desiredKeys = new Set();
|
|
222
|
+
const desiredKeys = /* @__PURE__ */ new Set();
|
|
175
223
|
for (const fk of desired.values()) {
|
|
176
224
|
const key = fkKey(fk.fields);
|
|
177
225
|
desiredKeys.add(key);
|
|
178
226
|
const existing = existingByKey.get(key);
|
|
179
227
|
if (!existing) added.push(fk);
|
|
180
|
-
else if (fkPropertiesDiffer(fk, existing)) changed.push({
|
|
228
|
+
else if (fkPropertiesDiffer(fk, existing)) changed.push({
|
|
181
229
|
desired: fk,
|
|
182
230
|
existing
|
|
183
231
|
});
|
|
@@ -189,25 +237,36 @@ else if (fkPropertiesDiffer(fk, existing)) changed.push({
|
|
|
189
237
|
changed
|
|
190
238
|
};
|
|
191
239
|
}
|
|
240
|
+
/** Whether the FK diff contains any changes. */
|
|
192
241
|
function hasForeignKeyChanges(diff) {
|
|
193
242
|
return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
|
|
194
243
|
}
|
|
195
244
|
function fkPropertiesDiffer(desired, existing) {
|
|
196
245
|
if (desired.targetTable !== existing.targetTable) return true;
|
|
197
246
|
if (fkKey(desired.targetFields) !== fkKey(existing.targetFields)) return true;
|
|
198
|
-
if ((desired.onDelete ??
|
|
199
|
-
if ((desired.onUpdate ??
|
|
247
|
+
if ((desired.onDelete ?? void 0) !== (existing.onDelete ?? void 0)) return true;
|
|
248
|
+
if ((desired.onUpdate ?? void 0) !== (existing.onUpdate ?? void 0)) return true;
|
|
200
249
|
return false;
|
|
201
250
|
}
|
|
202
|
-
|
|
203
251
|
//#endregion
|
|
204
|
-
//#region
|
|
252
|
+
//#region src/schema/table-option-diff.ts
|
|
253
|
+
/**
|
|
254
|
+
* Computes the difference between desired and existing table options.
|
|
255
|
+
*
|
|
256
|
+
* Options present in desired but absent from existing are ignored (initial state).
|
|
257
|
+
* Options present in existing but absent from desired are ignored (sticky options).
|
|
258
|
+
* Only value changes on matching keys are tracked.
|
|
259
|
+
*
|
|
260
|
+
* @param desired - Options from Atscript annotations (via adapter.getDesiredTableOptions()).
|
|
261
|
+
* @param existing - Options from DB introspection or snapshot fallback.
|
|
262
|
+
* @param destructiveKeys - Option keys where a value change requires table recreation.
|
|
263
|
+
*/
|
|
205
264
|
function computeTableOptionDiff(desired, existing, destructiveKeys) {
|
|
206
265
|
const existingByKey = new Map(existing.map((o) => [o.key, o.value]));
|
|
207
266
|
const changed = [];
|
|
208
267
|
for (const opt of desired) {
|
|
209
268
|
const existingValue = existingByKey.get(opt.key);
|
|
210
|
-
if (existingValue !==
|
|
269
|
+
if (existingValue !== void 0 && existingValue !== opt.value) changed.push({
|
|
211
270
|
key: opt.key,
|
|
212
271
|
oldValue: existingValue,
|
|
213
272
|
newValue: opt.value,
|
|
@@ -216,43 +275,32 @@ function computeTableOptionDiff(desired, existing, destructiveKeys) {
|
|
|
216
275
|
}
|
|
217
276
|
return { changed };
|
|
218
277
|
}
|
|
219
|
-
|
|
220
278
|
//#endregion
|
|
221
|
-
//#region
|
|
222
|
-
function _define_property$2(obj, key, value) {
|
|
223
|
-
if (key in obj) Object.defineProperty(obj, key, {
|
|
224
|
-
value,
|
|
225
|
-
enumerable: true,
|
|
226
|
-
configurable: true,
|
|
227
|
-
writable: true
|
|
228
|
-
});
|
|
229
|
-
else obj[key] = value;
|
|
230
|
-
return obj;
|
|
231
|
-
}
|
|
279
|
+
//#region src/schema/sync-store.ts
|
|
232
280
|
var SyncStore = class {
|
|
281
|
+
controlTable;
|
|
282
|
+
constructor(space) {
|
|
283
|
+
this.space = space;
|
|
284
|
+
}
|
|
233
285
|
async ensureControlTable() {
|
|
234
286
|
if (!this.controlTable) {
|
|
235
|
-
const { AtscriptControl } = await Promise.resolve().then(
|
|
236
|
-
return require("./control_as-BFPERAF_.cjs");
|
|
237
|
-
});
|
|
287
|
+
const { AtscriptControl } = await Promise.resolve().then(() => require("./control-DRgryKeg.cjs"));
|
|
238
288
|
this.controlTable = this.space.getTable(AtscriptControl);
|
|
239
289
|
}
|
|
240
290
|
await this.controlTable.ensureTable();
|
|
241
291
|
}
|
|
242
292
|
async readControlValue(_id) {
|
|
243
|
-
|
|
293
|
+
return (await this.controlTable.findOne({
|
|
244
294
|
filter: { _id: { $eq: _id } },
|
|
245
295
|
controls: {}
|
|
246
|
-
});
|
|
247
|
-
return row?.value ?? null;
|
|
296
|
+
}))?.value ?? null;
|
|
248
297
|
}
|
|
249
298
|
async writeControlValue(_id, value) {
|
|
250
|
-
|
|
251
|
-
if (existing !== null) await this.controlTable.replaceOne({
|
|
299
|
+
if (await this.readControlValue(_id) !== null) await this.controlTable.replaceOne({
|
|
252
300
|
_id,
|
|
253
301
|
value
|
|
254
302
|
});
|
|
255
|
-
else await this.controlTable.insertOne({
|
|
303
|
+
else await this.controlTable.insertOne({
|
|
256
304
|
_id,
|
|
257
305
|
value
|
|
258
306
|
});
|
|
@@ -283,10 +331,9 @@ else await this.controlTable.insertOne({
|
|
|
283
331
|
name,
|
|
284
332
|
isView: false
|
|
285
333
|
}));
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}));
|
|
334
|
+
const entries = parsed;
|
|
335
|
+
for (const e of entries) e.viewType ??= e.isView ? "V" : void 0;
|
|
336
|
+
return entries;
|
|
290
337
|
}
|
|
291
338
|
async writeTrackedList(readables) {
|
|
292
339
|
const entries = readables.map((r) => {
|
|
@@ -314,7 +361,7 @@ else await this.controlTable.insertOne({
|
|
|
314
361
|
if (existing) {
|
|
315
362
|
const expiresAt = existing.expiresAt;
|
|
316
363
|
if (expiresAt && expiresAt < now) await this.controlTable.deleteOne("sync_lock");
|
|
317
|
-
else return false;
|
|
364
|
+
else return false;
|
|
318
365
|
}
|
|
319
366
|
try {
|
|
320
367
|
await this.controlTable.insertOne({
|
|
@@ -371,38 +418,19 @@ else return false;
|
|
|
371
418
|
}
|
|
372
419
|
throw new Error(`Schema sync lock wait timed out after ${timeoutMs}ms`);
|
|
373
420
|
}
|
|
374
|
-
constructor(space) {
|
|
375
|
-
_define_property$2(this, "space", void 0);
|
|
376
|
-
_define_property$2(this, "controlTable", void 0);
|
|
377
|
-
this.space = space;
|
|
378
|
-
}
|
|
379
421
|
};
|
|
380
422
|
async function readStoredSnapshot(space, tableName, _asView) {
|
|
381
|
-
const { AtscriptControl } = await Promise.resolve().then(
|
|
382
|
-
return require("./control_as-BFPERAF_.cjs");
|
|
383
|
-
});
|
|
423
|
+
const { AtscriptControl } = await Promise.resolve().then(() => require("./control-DRgryKeg.cjs"));
|
|
384
424
|
const table = space.getTable(AtscriptControl);
|
|
385
425
|
await table.ensureTable();
|
|
386
|
-
const
|
|
426
|
+
const value = (await table.findOne({
|
|
387
427
|
filter: { _id: { $eq: `table_snapshot:${tableName}` } },
|
|
388
428
|
controls: {}
|
|
389
|
-
});
|
|
390
|
-
const value = row?.value ?? null;
|
|
429
|
+
}))?.value ?? null;
|
|
391
430
|
return value ? JSON.parse(value) : null;
|
|
392
431
|
}
|
|
393
|
-
|
|
394
432
|
//#endregion
|
|
395
|
-
//#region
|
|
396
|
-
function _define_property$1(obj, key, value) {
|
|
397
|
-
if (key in obj) Object.defineProperty(obj, key, {
|
|
398
|
-
value,
|
|
399
|
-
enumerable: true,
|
|
400
|
-
configurable: true,
|
|
401
|
-
writable: true
|
|
402
|
-
});
|
|
403
|
-
else obj[key] = value;
|
|
404
|
-
return obj;
|
|
405
|
-
}
|
|
433
|
+
//#region src/schema/sync-entry.ts
|
|
406
434
|
const noColor = {
|
|
407
435
|
green: (s) => s,
|
|
408
436
|
red: (s) => s,
|
|
@@ -413,17 +441,64 @@ const noColor = {
|
|
|
413
441
|
underline: (s) => s
|
|
414
442
|
};
|
|
415
443
|
var SyncEntry = class {
|
|
416
|
-
|
|
444
|
+
name;
|
|
445
|
+
/** 'V' = virtual view, 'M' = materialized view, 'E' = external view, undefined = table */
|
|
446
|
+
viewType;
|
|
447
|
+
status;
|
|
448
|
+
syncMethod;
|
|
449
|
+
columnsToAdd;
|
|
450
|
+
columnsToRename;
|
|
451
|
+
typeChanges;
|
|
452
|
+
nullableChanges;
|
|
453
|
+
defaultChanges;
|
|
454
|
+
columnsToDrop;
|
|
455
|
+
optionChanges;
|
|
456
|
+
fkAdded;
|
|
457
|
+
fkRemoved;
|
|
458
|
+
fkChanged;
|
|
459
|
+
columnsAdded;
|
|
460
|
+
columnsRenamed;
|
|
461
|
+
columnsDropped;
|
|
462
|
+
recreated;
|
|
463
|
+
errors;
|
|
464
|
+
renamedFrom;
|
|
465
|
+
constructor(init) {
|
|
466
|
+
this.name = init.name;
|
|
467
|
+
this.viewType = init.viewType;
|
|
468
|
+
this.status = init.status;
|
|
469
|
+
this.syncMethod = init.syncMethod;
|
|
470
|
+
this.columnsToAdd = init.columnsToAdd ?? [];
|
|
471
|
+
this.columnsToRename = init.columnsToRename ?? [];
|
|
472
|
+
this.typeChanges = init.typeChanges ?? [];
|
|
473
|
+
this.nullableChanges = init.nullableChanges ?? [];
|
|
474
|
+
this.defaultChanges = init.defaultChanges ?? [];
|
|
475
|
+
this.columnsToDrop = init.columnsToDrop ?? [];
|
|
476
|
+
this.optionChanges = init.optionChanges ?? [];
|
|
477
|
+
this.fkAdded = init.fkAdded ?? [];
|
|
478
|
+
this.fkRemoved = init.fkRemoved ?? [];
|
|
479
|
+
this.fkChanged = init.fkChanged ?? [];
|
|
480
|
+
this.columnsAdded = init.columnsAdded ?? [];
|
|
481
|
+
this.columnsRenamed = init.columnsRenamed ?? [];
|
|
482
|
+
this.columnsDropped = init.columnsDropped ?? [];
|
|
483
|
+
this.recreated = init.recreated ?? false;
|
|
484
|
+
this.errors = init.errors ?? [];
|
|
485
|
+
this.renamedFrom = init.renamedFrom;
|
|
486
|
+
}
|
|
487
|
+
/** Whether this entry involves destructive operations */
|
|
488
|
+
get destructive() {
|
|
417
489
|
if (this.status === "drop") return this.viewType !== "V" && this.viewType !== "E";
|
|
418
490
|
return this.columnsToDrop.length > 0 || this.typeChanges.length > 0 || this.recreated || this.optionChanges.some((c) => c.destructive);
|
|
419
491
|
}
|
|
420
|
-
/** Whether this entry represents any change (not in-sync) */
|
|
492
|
+
/** Whether this entry represents any change (not in-sync) */
|
|
493
|
+
get hasChanges() {
|
|
421
494
|
return this.status !== "in-sync" && this.status !== "error";
|
|
422
495
|
}
|
|
423
|
-
/** Whether this entry has errors */
|
|
496
|
+
/** Whether this entry has errors */
|
|
497
|
+
get hasErrors() {
|
|
424
498
|
return this.status === "error" || this.errors.length > 0;
|
|
425
499
|
}
|
|
426
|
-
/** Render this entry for display */
|
|
500
|
+
/** Render this entry for display */
|
|
501
|
+
print(mode, colors) {
|
|
427
502
|
const c = colors ?? noColor;
|
|
428
503
|
return mode === "plan" ? this.printPlan(c) : this.printResult(c);
|
|
429
504
|
}
|
|
@@ -486,13 +561,11 @@ var SyncEntry = class {
|
|
|
486
561
|
...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
|
|
487
562
|
""
|
|
488
563
|
];
|
|
489
|
-
|
|
490
|
-
if (hasChanges || this.recreated || this.renamedFrom) {
|
|
564
|
+
if (this.columnsAdded.length > 0 || this.columnsRenamed.length > 0 || this.columnsDropped.length > 0 || this.optionChanges.length > 0 || this.recreated || this.renamedFrom) {
|
|
491
565
|
const rlabel = this.recreated ? "recreated" : "altered";
|
|
492
566
|
const renameInfo = this.renamedFrom ? ` ${c.yellow(`(renamed from ${this.renamedFrom})`)}` : "";
|
|
493
|
-
const color = this.recreated ? c.yellow : c.cyan;
|
|
494
567
|
return [
|
|
495
|
-
` ${
|
|
568
|
+
` ${(this.recreated ? (s) => c.yellow(s) : (s) => c.cyan(s))(`~ ${vp}${label} — ${rlabel}${renameInfo}`)}`,
|
|
496
569
|
...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
|
|
497
570
|
...this.columnsRenamed.map((col) => ` ${c.yellow(`~ ${col} — renamed`)}`),
|
|
498
571
|
...this.columnsDropped.map((col) => ` ${c.red(`- ${col} — dropped`)}`),
|
|
@@ -508,52 +581,10 @@ var SyncEntry = class {
|
|
|
508
581
|
const prefix = this.viewType ? `${c.dim(`[${this.viewType}]`)} ` : "";
|
|
509
582
|
return ` ${c.green("✓")} ${prefix}${c.bold(this.name)} ${c.dim("— in sync")}`;
|
|
510
583
|
}
|
|
511
|
-
constructor(init) {
|
|
512
|
-
_define_property$1(this, "name", void 0);
|
|
513
|
-
/** 'V' = virtual view, 'M' = materialized view, 'E' = external view, undefined = table */ _define_property$1(this, "viewType", void 0);
|
|
514
|
-
_define_property$1(this, "status", void 0);
|
|
515
|
-
_define_property$1(this, "syncMethod", void 0);
|
|
516
|
-
_define_property$1(this, "columnsToAdd", void 0);
|
|
517
|
-
_define_property$1(this, "columnsToRename", void 0);
|
|
518
|
-
_define_property$1(this, "typeChanges", void 0);
|
|
519
|
-
_define_property$1(this, "nullableChanges", void 0);
|
|
520
|
-
_define_property$1(this, "defaultChanges", void 0);
|
|
521
|
-
_define_property$1(this, "columnsToDrop", void 0);
|
|
522
|
-
_define_property$1(this, "optionChanges", void 0);
|
|
523
|
-
_define_property$1(this, "fkAdded", void 0);
|
|
524
|
-
_define_property$1(this, "fkRemoved", void 0);
|
|
525
|
-
_define_property$1(this, "fkChanged", void 0);
|
|
526
|
-
_define_property$1(this, "columnsAdded", void 0);
|
|
527
|
-
_define_property$1(this, "columnsRenamed", void 0);
|
|
528
|
-
_define_property$1(this, "columnsDropped", void 0);
|
|
529
|
-
_define_property$1(this, "recreated", void 0);
|
|
530
|
-
_define_property$1(this, "errors", void 0);
|
|
531
|
-
_define_property$1(this, "renamedFrom", void 0);
|
|
532
|
-
this.name = init.name;
|
|
533
|
-
this.viewType = init.viewType;
|
|
534
|
-
this.status = init.status;
|
|
535
|
-
this.syncMethod = init.syncMethod;
|
|
536
|
-
this.columnsToAdd = init.columnsToAdd ?? [];
|
|
537
|
-
this.columnsToRename = init.columnsToRename ?? [];
|
|
538
|
-
this.typeChanges = init.typeChanges ?? [];
|
|
539
|
-
this.nullableChanges = init.nullableChanges ?? [];
|
|
540
|
-
this.defaultChanges = init.defaultChanges ?? [];
|
|
541
|
-
this.columnsToDrop = init.columnsToDrop ?? [];
|
|
542
|
-
this.optionChanges = init.optionChanges ?? [];
|
|
543
|
-
this.fkAdded = init.fkAdded ?? [];
|
|
544
|
-
this.fkRemoved = init.fkRemoved ?? [];
|
|
545
|
-
this.fkChanged = init.fkChanged ?? [];
|
|
546
|
-
this.columnsAdded = init.columnsAdded ?? [];
|
|
547
|
-
this.columnsRenamed = init.columnsRenamed ?? [];
|
|
548
|
-
this.columnsDropped = init.columnsDropped ?? [];
|
|
549
|
-
this.recreated = init.recreated ?? false;
|
|
550
|
-
this.errors = init.errors ?? [];
|
|
551
|
-
this.renamedFrom = init.renamedFrom;
|
|
552
|
-
}
|
|
553
584
|
};
|
|
554
|
-
|
|
555
585
|
//#endregion
|
|
556
|
-
//#region
|
|
586
|
+
//#region src/schema/sync-executor.ts
|
|
587
|
+
/** Checks if a tracked view's definition changed since the last stored snapshot. */
|
|
557
588
|
async function viewDefinitionChanged(view, store) {
|
|
558
589
|
const storedSnapshot = await store.readTableSnapshot(view.tableName, true);
|
|
559
590
|
if (!storedSnapshot) return false;
|
|
@@ -596,8 +627,7 @@ async function executeSyncTable(readable, safe, trackedNames, deps) {
|
|
|
596
627
|
}
|
|
597
628
|
}
|
|
598
629
|
const typeMapper = adapter.typeMapper?.bind(adapter);
|
|
599
|
-
|
|
600
|
-
await applyColumnDiff(adapter, readable, diff, init, safe, deps.logger);
|
|
630
|
+
await applyColumnDiff(adapter, readable, computeColumnDiff(readable.fieldDescriptors, existing, typeMapper), init, safe, deps.logger);
|
|
601
631
|
}
|
|
602
632
|
} else if (adapter.syncColumns) if (!storedSnapshot) {
|
|
603
633
|
const existed = adapter.tableExists ? await adapter.tableExists() : false;
|
|
@@ -605,10 +635,9 @@ async function executeSyncTable(readable, safe, trackedNames, deps) {
|
|
|
605
635
|
if (!existed) init.status = "create";
|
|
606
636
|
} else {
|
|
607
637
|
const existing = snapshotToExistingColumns(storedSnapshot);
|
|
608
|
-
|
|
609
|
-
await applyColumnDiff(adapter, readable, diff, init, safe, deps.logger);
|
|
638
|
+
await applyColumnDiff(adapter, readable, computeColumnDiff(readable.fieldDescriptors, existing, deps.resolveTypeMapper(adapter)), init, safe, deps.logger);
|
|
610
639
|
}
|
|
611
|
-
else {
|
|
640
|
+
else {
|
|
612
641
|
const existed = adapter.tableExists ? await adapter.tableExists() : true;
|
|
613
642
|
if (!init.recreated) {
|
|
614
643
|
await adapter.ensureTable();
|
|
@@ -626,8 +655,7 @@ else {
|
|
|
626
655
|
init.status = "alter";
|
|
627
656
|
}
|
|
628
657
|
if (hasDestructive) {
|
|
629
|
-
|
|
630
|
-
if (syncMethod === "recreate" && adapter.recreateTable) {
|
|
658
|
+
if (readable.syncMethod === "recreate" && adapter.recreateTable) {
|
|
631
659
|
deps.logger.warn?.(`[schema-sync] Destructive table option change on "${name}" — recreating with data preservation`);
|
|
632
660
|
await adapter.recreateTable();
|
|
633
661
|
init.status = "alter";
|
|
@@ -660,14 +688,14 @@ async function executeSyncView(view, trackedNames, deps) {
|
|
|
660
688
|
const viewType = view.viewPlan.materialized ? "M" : "V";
|
|
661
689
|
let status;
|
|
662
690
|
if (isRenamed || definitionChanged) status = "alter";
|
|
663
|
-
else if (trackedNames.has(view.tableName)) status = "in-sync";
|
|
664
|
-
else status = "create";
|
|
691
|
+
else if (trackedNames.has(view.tableName)) status = "in-sync";
|
|
692
|
+
else status = "create";
|
|
665
693
|
return new SyncEntry({
|
|
666
694
|
name: view.tableName,
|
|
667
695
|
status,
|
|
668
696
|
viewType,
|
|
669
|
-
renamedFrom: isRenamed ? renamedFrom :
|
|
670
|
-
recreated: definitionChanged ||
|
|
697
|
+
renamedFrom: isRenamed ? renamedFrom : void 0,
|
|
698
|
+
recreated: definitionChanged || void 0
|
|
671
699
|
});
|
|
672
700
|
}
|
|
673
701
|
async function applyColumnDiff(adapter, readable, diff, init, safe, logger) {
|
|
@@ -696,7 +724,7 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger) {
|
|
|
696
724
|
} else {
|
|
697
725
|
const errors = [];
|
|
698
726
|
for (const change of diff.typeChanged) {
|
|
699
|
-
const msg = `Type change on ${name}.${change.field.physicalName}
|
|
727
|
+
const msg = `Type change on ${name}.${change.field.physicalName} (${change.existingType} → ${change.field.designType}). Add @db.sync.method "recreate" or "drop", or migrate manually.`;
|
|
700
728
|
logger.error?.(`[schema-sync] ${msg}`);
|
|
701
729
|
errors.push(msg);
|
|
702
730
|
}
|
|
@@ -712,7 +740,7 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger) {
|
|
|
712
740
|
init.recreated = true;
|
|
713
741
|
init.status = "alter";
|
|
714
742
|
} else init.status = "alter";
|
|
715
|
-
else if (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) init.status = "alter";
|
|
743
|
+
else if (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) init.status = "alter";
|
|
716
744
|
if (!init.recreated && init.status !== "error" && (diff.added.length > 0 || diff.renamed.length > 0 || needsSyncColumns) && adapter.syncColumns) {
|
|
717
745
|
const syncResult = await adapter.syncColumns(diff);
|
|
718
746
|
init.columnsAdded = syncResult.added;
|
|
@@ -726,32 +754,30 @@ else if (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) init
|
|
|
726
754
|
init.status = "alter";
|
|
727
755
|
}
|
|
728
756
|
}
|
|
729
|
-
|
|
730
757
|
//#endregion
|
|
731
|
-
//#region
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
value,
|
|
735
|
-
enumerable: true,
|
|
736
|
-
configurable: true,
|
|
737
|
-
writable: true
|
|
738
|
-
});
|
|
739
|
-
else obj[key] = value;
|
|
740
|
-
return obj;
|
|
741
|
-
}
|
|
742
|
-
/** Builds a human-readable description of what changed in an FK constraint. */ function buildFkChangeDetails(desired, existing) {
|
|
758
|
+
//#region src/schema/schema-sync.ts
|
|
759
|
+
/** Builds a human-readable description of what changed in an FK constraint. */
|
|
760
|
+
function buildFkChangeDetails(desired, existing) {
|
|
743
761
|
const parts = [];
|
|
744
762
|
if (existing.targetTable !== desired.targetTable) parts.push(`retarget ${existing.targetTable} → ${desired.targetTable}`);
|
|
745
|
-
if ([...existing.targetFields].
|
|
746
|
-
if ((existing.onDelete ??
|
|
747
|
-
if ((existing.onUpdate ??
|
|
763
|
+
if ([...existing.targetFields].toSorted().join(",") !== [...desired.targetFields].toSorted().join(",")) parts.push(`fields ${existing.targetFields.join(",")} → ${desired.targetFields.join(",")}`);
|
|
764
|
+
if ((existing.onDelete ?? void 0) !== (desired.onDelete ?? void 0)) parts.push(`onDelete ${existing.onDelete ?? "noAction"} → ${desired.onDelete ?? "noAction"}`);
|
|
765
|
+
if ((existing.onUpdate ?? void 0) !== (desired.onUpdate ?? void 0)) parts.push(`onUpdate ${existing.onUpdate ?? "noAction"} → ${desired.onUpdate ?? "noAction"}`);
|
|
748
766
|
return parts.join(", ");
|
|
749
767
|
}
|
|
750
768
|
var SchemaSync = class {
|
|
769
|
+
store;
|
|
770
|
+
logger;
|
|
771
|
+
constructor(space, logger) {
|
|
772
|
+
this.space = space;
|
|
773
|
+
this.logger = logger || require_db_view.NoopLogger;
|
|
774
|
+
this.store = new SyncStore(space);
|
|
775
|
+
}
|
|
751
776
|
/**
|
|
752
777
|
* Resolves types into categorized readables and computes the schema hash.
|
|
753
778
|
* Passes each adapter's typeMapper for precise type tracking in snapshots.
|
|
754
|
-
*/
|
|
779
|
+
*/
|
|
780
|
+
async resolveAndHash(types) {
|
|
755
781
|
const tables = [];
|
|
756
782
|
const views = [];
|
|
757
783
|
const externalViews = [];
|
|
@@ -760,32 +786,29 @@ var SchemaSync = class {
|
|
|
760
786
|
if (readable.isView) {
|
|
761
787
|
const view = readable;
|
|
762
788
|
if (view.isExternal) externalViews.push(view);
|
|
763
|
-
else views.push(readable);
|
|
789
|
+
else views.push(readable);
|
|
764
790
|
} else tables.push(readable);
|
|
765
791
|
}
|
|
766
|
-
const allReadables = [
|
|
767
|
-
...tables,
|
|
768
|
-
...views,
|
|
769
|
-
...externalViews
|
|
770
|
-
];
|
|
771
|
-
const snapshots = allReadables.map((r) => {
|
|
772
|
-
if (r.isView) return computeViewSnapshot(r);
|
|
773
|
-
const tm = r.dbAdapter.typeMapper?.bind(r.dbAdapter);
|
|
774
|
-
const opts = (r.fieldDescriptors, r.dbAdapter.getDesiredTableOptions?.());
|
|
775
|
-
return computeTableSnapshot(r, tm, opts);
|
|
776
|
-
});
|
|
777
|
-
const hash = computeSchemaHash(snapshots);
|
|
778
792
|
return {
|
|
779
793
|
tables,
|
|
780
794
|
views,
|
|
781
795
|
externalViews,
|
|
782
|
-
hash
|
|
796
|
+
hash: computeSchemaHash([
|
|
797
|
+
...tables,
|
|
798
|
+
...views,
|
|
799
|
+
...externalViews
|
|
800
|
+
].map((r) => {
|
|
801
|
+
if (r.isView) return computeViewSnapshot(r);
|
|
802
|
+
const tm = r.dbAdapter.typeMapper?.bind(r.dbAdapter);
|
|
803
|
+
return computeTableSnapshot(r, tm, (r.fieldDescriptors, r.dbAdapter.getDesiredTableOptions?.()));
|
|
804
|
+
}))
|
|
783
805
|
};
|
|
784
806
|
}
|
|
785
807
|
/**
|
|
786
808
|
* Checks an external view: verifies it exists in the DB and columns match.
|
|
787
809
|
* Returns a SyncEntry with status 'in-sync' or 'error'.
|
|
788
|
-
*/
|
|
810
|
+
*/
|
|
811
|
+
async checkExternalView(view) {
|
|
789
812
|
const adapter = view.dbAdapter;
|
|
790
813
|
const name = view.tableName;
|
|
791
814
|
if (adapter.getExistingColumns) {
|
|
@@ -805,8 +828,7 @@ else views.push(readable);
|
|
|
805
828
|
errors: [`External view "${name}" is missing columns: ${missing.join(", ")}`]
|
|
806
829
|
});
|
|
807
830
|
} else if (adapter.tableExists) {
|
|
808
|
-
|
|
809
|
-
if (!exists) return new SyncEntry({
|
|
831
|
+
if (!await adapter.tableExists()) return new SyncEntry({
|
|
810
832
|
name,
|
|
811
833
|
viewType: "E",
|
|
812
834
|
status: "error",
|
|
@@ -822,8 +844,9 @@ else views.push(readable);
|
|
|
822
844
|
/**
|
|
823
845
|
* Detects tables/views present in the previous sync but absent from the current schema.
|
|
824
846
|
* Returns SyncEntry instances with status 'drop'.
|
|
825
|
-
*/
|
|
826
|
-
|
|
847
|
+
*/
|
|
848
|
+
async detectRemoved(currentReadables, previous) {
|
|
849
|
+
previous ??= await this.store.readTrackedList();
|
|
827
850
|
const currentSet = new Set(currentReadables.map((t) => t.tableName));
|
|
828
851
|
const renameFromSet = new Set(currentReadables.map((r) => r.renamedFrom).filter(Boolean));
|
|
829
852
|
const removed = [];
|
|
@@ -838,7 +861,8 @@ else views.push(readable);
|
|
|
838
861
|
* Starts a periodic heartbeat that extends the lock's TTL while sync runs.
|
|
839
862
|
* Returns a handle with `stop()` to cancel and `getAbortReason()` to check
|
|
840
863
|
* whether the lock was stolen or unexpectedly removed.
|
|
841
|
-
*/
|
|
864
|
+
*/
|
|
865
|
+
startHeartbeat(podId, ttlMs) {
|
|
842
866
|
let abortReason;
|
|
843
867
|
let stopped = false;
|
|
844
868
|
const intervalMs = Math.max(Math.floor(ttlMs / 3), 1e3);
|
|
@@ -868,13 +892,15 @@ else views.push(readable);
|
|
|
868
892
|
getAbortReason: () => abortReason
|
|
869
893
|
};
|
|
870
894
|
}
|
|
871
|
-
/** Throws if the heartbeat detected a stolen/missing lock. */
|
|
895
|
+
/** Throws if the heartbeat detected a stolen/missing lock. */
|
|
896
|
+
assertLockHeld(getAbortReason) {
|
|
872
897
|
const reason = getAbortReason();
|
|
873
898
|
if (reason) throw new Error(reason);
|
|
874
899
|
}
|
|
875
900
|
/**
|
|
876
901
|
* Runs schema synchronization with distributed locking.
|
|
877
|
-
*/
|
|
902
|
+
*/
|
|
903
|
+
async run(types, opts) {
|
|
878
904
|
const podId = opts?.podId ?? crypto.randomUUID();
|
|
879
905
|
const lockTtlMs = opts?.lockTtlMs ?? 3e4;
|
|
880
906
|
const waitTimeoutMs = opts?.waitTimeoutMs ?? 6e4;
|
|
@@ -884,30 +910,25 @@ else views.push(readable);
|
|
|
884
910
|
const { tables, views, externalViews, hash } = await this.resolveAndHash(types);
|
|
885
911
|
await this.store.ensureControlTable();
|
|
886
912
|
if (!force) {
|
|
887
|
-
|
|
888
|
-
if (storedHash === hash) return {
|
|
913
|
+
if (await this.store.readHash() === hash) return {
|
|
889
914
|
status: "up-to-date",
|
|
890
915
|
schemaHash: hash,
|
|
891
916
|
entries: []
|
|
892
917
|
};
|
|
893
918
|
}
|
|
894
|
-
|
|
895
|
-
if (!acquired) {
|
|
919
|
+
if (!await this.store.tryAcquireLock(podId, lockTtlMs)) {
|
|
896
920
|
await this.store.waitForLock(waitTimeoutMs, pollIntervalMs);
|
|
897
|
-
|
|
898
|
-
if (storedHash === hash) return {
|
|
921
|
+
if (await this.store.readHash() === hash) return {
|
|
899
922
|
status: "synced-by-peer",
|
|
900
923
|
schemaHash: hash,
|
|
901
924
|
entries: []
|
|
902
925
|
};
|
|
903
|
-
|
|
904
|
-
if (!retryAcquired) throw new Error("Failed to acquire schema sync lock after waiting");
|
|
926
|
+
if (!await this.store.tryAcquireLock(podId, lockTtlMs)) throw new Error("Failed to acquire schema sync lock after waiting");
|
|
905
927
|
}
|
|
906
928
|
const heartbeat = this.startHeartbeat(podId, lockTtlMs);
|
|
907
929
|
try {
|
|
908
930
|
if (!force) {
|
|
909
|
-
|
|
910
|
-
if (storedHash === hash) return {
|
|
931
|
+
if (await this.store.readHash() === hash) return {
|
|
911
932
|
status: "synced-by-peer",
|
|
912
933
|
schemaHash: hash,
|
|
913
934
|
entries: []
|
|
@@ -939,7 +960,7 @@ else views.push(readable);
|
|
|
939
960
|
for (const entry of removed) {
|
|
940
961
|
if (entry.viewType === "E") continue;
|
|
941
962
|
if (entry.viewType) await this.space.dropViewByName(entry.name);
|
|
942
|
-
else await this.space.dropTableByName(entry.name);
|
|
963
|
+
else await this.space.dropTableByName(entry.name);
|
|
943
964
|
}
|
|
944
965
|
entries.push(...removed.filter((e) => e.viewType !== "E"));
|
|
945
966
|
}
|
|
@@ -947,8 +968,8 @@ else await this.space.dropTableByName(entry.name);
|
|
|
947
968
|
for (const readable of allReadables) {
|
|
948
969
|
const adapter = readable.dbAdapter;
|
|
949
970
|
const tm = adapter.typeMapper?.bind(adapter);
|
|
950
|
-
const opts
|
|
951
|
-
const snapshot = readable.isView ? computeViewSnapshot(readable) : computeTableSnapshot(readable, tm, opts
|
|
971
|
+
const opts = adapter.getDesiredTableOptions?.();
|
|
972
|
+
const snapshot = readable.isView ? computeViewSnapshot(readable) : computeTableSnapshot(readable, tm, opts);
|
|
952
973
|
await this.store.writeTableSnapshot(readable.tableName, snapshot);
|
|
953
974
|
}
|
|
954
975
|
if (!safe) for (const entry of removed) {
|
|
@@ -970,7 +991,8 @@ else await this.space.dropTableByName(entry.name);
|
|
|
970
991
|
}
|
|
971
992
|
/**
|
|
972
993
|
* Computes a dry-run plan showing what `run()` would do, without executing any DDL.
|
|
973
|
-
*/
|
|
994
|
+
*/
|
|
995
|
+
async plan(types, opts) {
|
|
974
996
|
const force = opts?.force ?? false;
|
|
975
997
|
const safe = opts?.safe ?? false;
|
|
976
998
|
const { tables, views, externalViews, hash } = await this.resolveAndHash(types);
|
|
@@ -986,8 +1008,7 @@ else await this.space.dropTableByName(entry.name);
|
|
|
986
1008
|
const viewEntries = await Promise.all(views.map((v) => this.planView(v, trackedNames)));
|
|
987
1009
|
const externalEntries = await Promise.all(externalViews.map((v) => this.checkExternalView(v)));
|
|
988
1010
|
if (!force) {
|
|
989
|
-
|
|
990
|
-
if (storedHash === hash) return {
|
|
1011
|
+
if (await this.store.readHash() === hash) return {
|
|
991
1012
|
status: "up-to-date",
|
|
992
1013
|
schemaHash: hash,
|
|
993
1014
|
entries: [
|
|
@@ -1000,7 +1021,23 @@ else await this.space.dropTableByName(entry.name);
|
|
|
1000
1021
|
let removed = await this.detectRemoved(allReadables, previouslyTracked);
|
|
1001
1022
|
if (safe) {
|
|
1002
1023
|
planEntries = planEntries.map((e) => new SyncEntry({
|
|
1003
|
-
|
|
1024
|
+
name: e.name,
|
|
1025
|
+
viewType: e.viewType,
|
|
1026
|
+
status: e.status,
|
|
1027
|
+
syncMethod: e.syncMethod,
|
|
1028
|
+
columnsToAdd: e.columnsToAdd,
|
|
1029
|
+
columnsToRename: e.columnsToRename,
|
|
1030
|
+
nullableChanges: e.nullableChanges,
|
|
1031
|
+
defaultChanges: e.defaultChanges,
|
|
1032
|
+
optionChanges: e.optionChanges,
|
|
1033
|
+
fkAdded: e.fkAdded,
|
|
1034
|
+
fkRemoved: e.fkRemoved,
|
|
1035
|
+
fkChanged: e.fkChanged,
|
|
1036
|
+
columnsAdded: e.columnsAdded,
|
|
1037
|
+
columnsRenamed: e.columnsRenamed,
|
|
1038
|
+
columnsDropped: e.columnsDropped,
|
|
1039
|
+
errors: e.errors,
|
|
1040
|
+
renamedFrom: e.renamedFrom,
|
|
1004
1041
|
columnsToDrop: [],
|
|
1005
1042
|
typeChanges: [],
|
|
1006
1043
|
recreated: false
|
|
@@ -1019,7 +1056,8 @@ else await this.space.dropTableByName(entry.name);
|
|
|
1019
1056
|
]
|
|
1020
1057
|
};
|
|
1021
1058
|
}
|
|
1022
|
-
/** Fallback typeMapper for snapshot-based Path B: compares designType directly, skips unions. */
|
|
1059
|
+
/** Fallback typeMapper for snapshot-based Path B: compares designType directly, skips unions. */
|
|
1060
|
+
resolveTypeMapper(adapter) {
|
|
1023
1061
|
return adapter.typeMapper?.bind(adapter) ?? ((f) => f.designType === "union" ? "union" : f.designType);
|
|
1024
1062
|
}
|
|
1025
1063
|
async planTable(readable, trackedNames) {
|
|
@@ -1050,8 +1088,7 @@ else await this.space.dropTableByName(entry.name);
|
|
|
1050
1088
|
}
|
|
1051
1089
|
} else if (adapter.syncColumns) if (!storedSnapshot) {
|
|
1052
1090
|
if (!pendingRename) {
|
|
1053
|
-
|
|
1054
|
-
if (!exists) {
|
|
1091
|
+
if (!(adapter.tableExists ? await adapter.tableExists() : false)) {
|
|
1055
1092
|
init.status = "create";
|
|
1056
1093
|
init.columnsToAdd = readable.fieldDescriptors.filter((f) => !f.ignored);
|
|
1057
1094
|
}
|
|
@@ -1061,9 +1098,8 @@ else await this.space.dropTableByName(entry.name);
|
|
|
1061
1098
|
const diff = computeColumnDiff(readable.fieldDescriptors, existing, this.resolveTypeMapper(adapter));
|
|
1062
1099
|
this.populatePlanFromDiff(diff, init, name, readable.syncMethod, adapter.supportsColumnModify);
|
|
1063
1100
|
}
|
|
1064
|
-
else if (adapter.tableExists) {
|
|
1065
|
-
|
|
1066
|
-
if (!exists) init.status = "create";
|
|
1101
|
+
else if (adapter.tableExists) {
|
|
1102
|
+
if (!await adapter.tableExists()) init.status = "create";
|
|
1067
1103
|
} else init.status = "create";
|
|
1068
1104
|
if (init.status !== "create") {
|
|
1069
1105
|
const optionDiff = await this.diffTableOptions(readable);
|
|
@@ -1096,7 +1132,8 @@ else if (adapter.tableExists) {
|
|
|
1096
1132
|
}
|
|
1097
1133
|
/**
|
|
1098
1134
|
* Populates plan init from a column diff (shared by Path A and Path B).
|
|
1099
|
-
*/
|
|
1135
|
+
*/
|
|
1136
|
+
populatePlanFromDiff(diff, init, name, syncMethod, adapterSupportsModify) {
|
|
1100
1137
|
init.columnsToAdd = diff.added;
|
|
1101
1138
|
init.columnsToRename = diff.renamed.map((r) => ({
|
|
1102
1139
|
from: r.oldName,
|
|
@@ -1117,27 +1154,27 @@ else if (adapter.tableExists) {
|
|
|
1117
1154
|
newDefault: dc.newDefault
|
|
1118
1155
|
}));
|
|
1119
1156
|
init.columnsToDrop = diff.removed.map((c) => c.name);
|
|
1120
|
-
|
|
1121
|
-
if (hasChanges) init.status = "alter";
|
|
1157
|
+
if (diff.added.length > 0 || diff.renamed.length > 0 || diff.typeChanged.length > 0 || diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0 || diff.removed.length > 0) init.status = "alter";
|
|
1122
1158
|
if (diff.conflicts.length > 0) {
|
|
1123
1159
|
init.status = "error";
|
|
1124
1160
|
init.errors = [...init.errors ?? [], ...diff.conflicts.map((c) => `Column rename conflict on ${name}: cannot rename "${c.oldName}" → "${c.field.physicalName}" because "${c.conflictsWith}" already exists.`)];
|
|
1125
1161
|
}
|
|
1126
1162
|
if (diff.typeChanged.length > 0 && !syncMethod && !adapterSupportsModify) {
|
|
1127
1163
|
init.status = "error";
|
|
1128
|
-
init.errors = [...init.errors ?? [], ...diff.typeChanged.map((tc) => `Type change on ${name}.${tc.field.physicalName}
|
|
1164
|
+
init.errors = [...init.errors ?? [], ...diff.typeChanged.map((tc) => `Type change on ${name}.${tc.field.physicalName} (${tc.existingType} → ${tc.field.designType}). Add @db.sync.method "recreate" or "drop", or migrate manually.`)];
|
|
1129
1165
|
}
|
|
1130
1166
|
}
|
|
1131
1167
|
/**
|
|
1132
1168
|
* Computes table option diff using DB-first introspection with snapshot fallback.
|
|
1133
1169
|
* Returns null if the adapter has no table options.
|
|
1134
|
-
*/
|
|
1170
|
+
*/
|
|
1171
|
+
async diffTableOptions(readable) {
|
|
1135
1172
|
const adapter = readable.dbAdapter;
|
|
1136
1173
|
const desired = adapter.getDesiredTableOptions?.();
|
|
1137
1174
|
if (!desired || desired.length === 0) return null;
|
|
1138
1175
|
let existing;
|
|
1139
1176
|
if (adapter.getExistingTableOptions) existing = await adapter.getExistingTableOptions();
|
|
1140
|
-
else {
|
|
1177
|
+
else {
|
|
1141
1178
|
const snapshot = await this.store.readTableSnapshot(readable.tableName);
|
|
1142
1179
|
existing = snapshot ? snapshotToExistingTableOptions(snapshot) : [];
|
|
1143
1180
|
}
|
|
@@ -1151,14 +1188,14 @@ else {
|
|
|
1151
1188
|
const isRenamed = renamedFrom && trackedNames.has(renamedFrom);
|
|
1152
1189
|
let status;
|
|
1153
1190
|
if (isRenamed) status = "alter";
|
|
1154
|
-
else if (trackedNames.has(view.tableName)) status = await viewDefinitionChanged(view, this.store) ? "alter" : "in-sync";
|
|
1155
|
-
else status = "create";
|
|
1191
|
+
else if (trackedNames.has(view.tableName)) status = await viewDefinitionChanged(view, this.store) ? "alter" : "in-sync";
|
|
1192
|
+
else status = "create";
|
|
1156
1193
|
return new SyncEntry({
|
|
1157
1194
|
name: view.tableName,
|
|
1158
1195
|
status,
|
|
1159
1196
|
viewType,
|
|
1160
|
-
renamedFrom: isRenamed ? renamedFrom :
|
|
1161
|
-
recreated: status === "alter" && !isRenamed ? true :
|
|
1197
|
+
renamedFrom: isRenamed ? renamedFrom : void 0,
|
|
1198
|
+
recreated: status === "alter" && !isRenamed ? true : void 0
|
|
1162
1199
|
});
|
|
1163
1200
|
}
|
|
1164
1201
|
buildExecutorDeps() {
|
|
@@ -1170,36 +1207,47 @@ else status = "create";
|
|
|
1170
1207
|
diffTableOptions: this.diffTableOptions.bind(this)
|
|
1171
1208
|
};
|
|
1172
1209
|
}
|
|
1173
|
-
constructor(space, logger) {
|
|
1174
|
-
_define_property(this, "space", void 0);
|
|
1175
|
-
_define_property(this, "store", void 0);
|
|
1176
|
-
_define_property(this, "logger", void 0);
|
|
1177
|
-
this.space = space;
|
|
1178
|
-
this.logger = logger || require_logger.NoopLogger;
|
|
1179
|
-
this.store = new SyncStore(space);
|
|
1180
|
-
}
|
|
1181
1210
|
};
|
|
1182
|
-
|
|
1183
1211
|
//#endregion
|
|
1184
|
-
//#region
|
|
1212
|
+
//#region src/sync.ts
|
|
1213
|
+
/**
|
|
1214
|
+
* Synchronizes database schema with distributed locking.
|
|
1215
|
+
* Safe to call from multiple concurrent processes/pods.
|
|
1216
|
+
*
|
|
1217
|
+
* ```typescript
|
|
1218
|
+
* import { syncSchema } from '@atscript/db/sync'
|
|
1219
|
+
*
|
|
1220
|
+
* const db = new DbSpace(() => new SqliteAdapter(driver))
|
|
1221
|
+
* await syncSchema(db, [UsersType, PostsType, CommentsType])
|
|
1222
|
+
* ```
|
|
1223
|
+
*
|
|
1224
|
+
* The function:
|
|
1225
|
+
* 1. Creates an `__atscript_control` table for lock coordination
|
|
1226
|
+
* 2. Computes a schema hash — skips entirely if nothing changed
|
|
1227
|
+
* 3. Acquires a distributed lock so only one process syncs
|
|
1228
|
+
* 4. Creates tables, adds new columns, syncs indexes
|
|
1229
|
+
* 5. Stores the new hash and releases the lock
|
|
1230
|
+
*
|
|
1231
|
+
* @param space - The DbSpace containing the adapter factory.
|
|
1232
|
+
* @param types - Atscript annotated types to synchronize.
|
|
1233
|
+
* @param opts - Lock TTL, wait timeout, force mode, etc.
|
|
1234
|
+
*/
|
|
1185
1235
|
async function syncSchema(space, types, opts) {
|
|
1186
|
-
|
|
1187
|
-
return sync.run(types, opts);
|
|
1236
|
+
return new SchemaSync(space).run(types, opts);
|
|
1188
1237
|
}
|
|
1189
|
-
|
|
1190
1238
|
//#endregion
|
|
1191
|
-
exports.SchemaSync = SchemaSync
|
|
1192
|
-
exports.SyncEntry = SyncEntry
|
|
1193
|
-
exports.computeColumnDiff = computeColumnDiff
|
|
1194
|
-
exports.computeForeignKeyDiff = computeForeignKeyDiff
|
|
1195
|
-
exports.computeSchemaHash = computeSchemaHash
|
|
1196
|
-
exports.computeTableHash = computeTableHash
|
|
1197
|
-
exports.computeTableOptionDiff = computeTableOptionDiff
|
|
1198
|
-
exports.computeTableSnapshot = computeTableSnapshot
|
|
1199
|
-
exports.computeViewSnapshot = computeViewSnapshot
|
|
1200
|
-
exports.fkKey = fkKey
|
|
1201
|
-
exports.hasForeignKeyChanges = hasForeignKeyChanges
|
|
1202
|
-
exports.readStoredSnapshot = readStoredSnapshot
|
|
1203
|
-
exports.snapshotToExistingColumns = snapshotToExistingColumns
|
|
1204
|
-
exports.snapshotToExistingTableOptions = snapshotToExistingTableOptions
|
|
1205
|
-
exports.syncSchema = syncSchema
|
|
1239
|
+
exports.SchemaSync = SchemaSync;
|
|
1240
|
+
exports.SyncEntry = SyncEntry;
|
|
1241
|
+
exports.computeColumnDiff = computeColumnDiff;
|
|
1242
|
+
exports.computeForeignKeyDiff = computeForeignKeyDiff;
|
|
1243
|
+
exports.computeSchemaHash = computeSchemaHash;
|
|
1244
|
+
exports.computeTableHash = computeTableHash;
|
|
1245
|
+
exports.computeTableOptionDiff = computeTableOptionDiff;
|
|
1246
|
+
exports.computeTableSnapshot = computeTableSnapshot;
|
|
1247
|
+
exports.computeViewSnapshot = computeViewSnapshot;
|
|
1248
|
+
exports.fkKey = fkKey;
|
|
1249
|
+
exports.hasForeignKeyChanges = hasForeignKeyChanges;
|
|
1250
|
+
exports.readStoredSnapshot = readStoredSnapshot;
|
|
1251
|
+
exports.snapshotToExistingColumns = snapshotToExistingColumns;
|
|
1252
|
+
exports.snapshotToExistingTableOptions = snapshotToExistingTableOptions;
|
|
1253
|
+
exports.syncSchema = syncSchema;
|