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