@atscript/db 0.1.128 → 0.1.129
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/dist/{db-error-DXwEzmYJ.cjs → db-error-C4JuLcvb.cjs} +27 -0
- package/dist/{db-error-BHPXOKzc.mjs → db-error-COrO58t5.mjs} +22 -1
- package/dist/{db-readable-CkbZn_z-.d.cts → db-readable-B7eYWS5q.d.cts} +10 -1
- package/dist/{db-readable-BTl4NzNN.d.mts → db-readable-Bn1bV_eC.d.mts} +10 -1
- package/dist/{db-space-BEB5Jl8M.d.mts → db-space-C2UCnGHd.d.cts} +28 -2
- package/dist/{db-space-BevOyVrC.d.cts → db-space-DdIPYD0Q.d.mts} +28 -2
- package/dist/{db-view-C8-MsjND.cjs → db-view-CRBgkEp0.cjs} +91 -4
- package/dist/{db-view-Ck0I-PMI.mjs → db-view-Dl0aDTiT.mjs} +91 -4
- package/dist/index.cjs +4 -3
- package/dist/index.d.cts +4 -4
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -4
- package/dist/{nested-writer-DI-HeTky.mjs → nested-writer-CkDo-ZfH.mjs} +1 -1
- package/dist/{nested-writer-DoDhl3X3.cjs → nested-writer-DxPhmWFz.cjs} +1 -1
- package/dist/ops.cjs +1 -1
- package/dist/ops.mjs +1 -1
- package/dist/rel.cjs +2 -2
- package/dist/rel.d.cts +1 -1
- package/dist/rel.d.mts +1 -1
- package/dist/rel.mjs +2 -2
- package/dist/{relation-loader-BnUgJsUG.cjs → relation-loader-C8GOpNYJ.cjs} +1 -1
- package/dist/{relation-loader-BmeOMj0b.mjs → relation-loader-CUGcxJ18.mjs} +1 -1
- package/dist/sync.cjs +280 -143
- package/dist/sync.d.cts +24 -6
- package/dist/sync.d.mts +24 -6
- package/dist/sync.mjs +280 -143
- package/dist/{validator-eKYWf3xf.mjs → validator-CeD_fqyW.mjs} +1 -1
- package/dist/{validator-BNUHCXIE.cjs → validator-lkCJKuoo.cjs} +1 -1
- package/dist/{validator-DuAmWc8L.d.cts → validator-wBARmD68.d.cts} +21 -2
- package/dist/{validator-DuAmWc8L.d.mts → validator-wBARmD68.d.mts} +21 -2
- package/dist/validator.cjs +2 -2
- package/dist/validator.d.cts +1 -1
- package/dist/validator.d.mts +1 -1
- package/dist/validator.mjs +2 -2
- package/package.json +1 -1
package/dist/sync.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_db_view = require("./db-view-
|
|
2
|
+
const require_db_view = require("./db-view-CRBgkEp0.cjs");
|
|
3
3
|
//#region src/schema/schema-hash.ts
|
|
4
4
|
/** Extracts sorted field snapshots from a readable's field descriptors. */
|
|
5
5
|
function extractFieldSnapshots(fields, typeMapper) {
|
|
@@ -569,7 +569,7 @@ const noColor = {
|
|
|
569
569
|
dim: (s) => s,
|
|
570
570
|
underline: (s) => s
|
|
571
571
|
};
|
|
572
|
-
var SyncEntry = class {
|
|
572
|
+
var SyncEntry = class SyncEntry {
|
|
573
573
|
name;
|
|
574
574
|
/** 'V' = virtual view, 'M' = materialized view, 'E' = external view, undefined = table */
|
|
575
575
|
viewType;
|
|
@@ -637,6 +637,19 @@ var SyncEntry = class {
|
|
|
637
637
|
return { ...this };
|
|
638
638
|
}
|
|
639
639
|
/**
|
|
640
|
+
* This entry as an `error` entry with `msg` appended to its errors — the
|
|
641
|
+
* shape a DDL failure inside the entry's step reports (the planned work is
|
|
642
|
+
* kept, the status says it did not land).
|
|
643
|
+
* @since 0.1.129
|
|
644
|
+
*/
|
|
645
|
+
withError(msg) {
|
|
646
|
+
return new SyncEntry({
|
|
647
|
+
...this.toInit(),
|
|
648
|
+
status: "error",
|
|
649
|
+
errors: [...this.errors, msg]
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
640
653
|
* Whether desired work is still pending after this entry — DDL that was
|
|
641
654
|
* not issued because the entry errored or safe mode skipped it (see
|
|
642
655
|
* `skipped`). A pending entry withholds its snapshot and the schema hash,
|
|
@@ -806,6 +819,79 @@ async function viewDefinitionChanged(view, store) {
|
|
|
806
819
|
const currentHash = computeTableHash(computeViewSnapshot(view));
|
|
807
820
|
return computeTableHash(storedSnapshot) !== currentHash;
|
|
808
821
|
}
|
|
822
|
+
/**
|
|
823
|
+
* The plan entry of one table as `plan()` reports it (and as `run()` reports
|
|
824
|
+
* an entry that issues no DDL): the discovery entry plus `dependsOn`, the
|
|
825
|
+
* key change, and — in safe mode — the destructive parts hidden except the
|
|
826
|
+
* skipped ones, which are pending and shown as skipped.
|
|
827
|
+
*/
|
|
828
|
+
function planTableInit(facts, safe, dependsOn) {
|
|
829
|
+
const init = {
|
|
830
|
+
...facts.planEntry,
|
|
831
|
+
dependsOn
|
|
832
|
+
};
|
|
833
|
+
const skipped = safe ? safeModeSkips(facts) : [];
|
|
834
|
+
const pk = facts.diff?.primaryKeyChanged;
|
|
835
|
+
if (pk) init.pkChange = {
|
|
836
|
+
from: pk.from,
|
|
837
|
+
to: pk.to,
|
|
838
|
+
rebuild: !skipped.includes("pk-rebuild")
|
|
839
|
+
};
|
|
840
|
+
if (!safe) return init;
|
|
841
|
+
return {
|
|
842
|
+
...init,
|
|
843
|
+
columnsToDrop: [],
|
|
844
|
+
typeChanges: skipped.includes("recreate") ? init.typeChanges : [],
|
|
845
|
+
skipped: skipped.length > 0 ? skipped : void 0,
|
|
846
|
+
recreated: false
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* How a type change is applied on this table, in the executor's order:
|
|
851
|
+
* `@db.sync.method "drop"` drop-and-recreate, `"recreate"` with data copy,
|
|
852
|
+
* in-place modification (MySQL `MODIFY COLUMN`), or not at all (`"none"` —
|
|
853
|
+
* an `error` entry, see {@link typeChangeErrors}).
|
|
854
|
+
*/
|
|
855
|
+
function typeChangeStrategy(readable, diff) {
|
|
856
|
+
const adapter = readable.dbAdapter;
|
|
857
|
+
if (willDropRecreate(readable, diff, void 0)) return "drop";
|
|
858
|
+
if (readable.syncMethod === "recreate" && adapter.recreateTable) return "recreate";
|
|
859
|
+
if (adapter.supportsColumnModify && adapter.syncColumns) return "modify";
|
|
860
|
+
return "none";
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* The error messages of a type change the adapter cannot apply (strategy
|
|
864
|
+
* `"none"`), one per column: either no `@db.sync.method` is declared, or the
|
|
865
|
+
* declared one needs a primitive this adapter lacks.
|
|
866
|
+
*/
|
|
867
|
+
function typeChangeErrors(readable, diff) {
|
|
868
|
+
const method = readable.syncMethod;
|
|
869
|
+
const advice = method ? `@db.sync.method "${method}" is declared but the adapter has no ${method === "recreate" ? "recreateTable" : "dropTable"} — migrate manually.` : `Add @db.sync.method "recreate" or "drop", or migrate manually.`;
|
|
870
|
+
return diff.typeChanged.map((tc) => `Type change on ${readable.tableName}.${tc.field.physicalName} (${tc.existingType} → ${tc.field.designType}). ${advice}`);
|
|
871
|
+
}
|
|
872
|
+
/** `(a, b → c)` — the key change as the messages print it. */
|
|
873
|
+
function pkLabel(change) {
|
|
874
|
+
return `(${change.from.join(", ")} → ${change.to.join(", ")})`;
|
|
875
|
+
}
|
|
876
|
+
/** A key rebuild on an adapter with neither `rebuildPrimaryKey` nor `recreateTable`. */
|
|
877
|
+
function pkRebuildUnsupported(name, change) {
|
|
878
|
+
return `Primary key of "${name}" changed ${pkLabel(change)} but the adapter cannot rebuild primary keys. Migrate manually and re-run.`;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Applies one table's discovered facts: rename → column ops (Path A live
|
|
882
|
+
* introspection, Path B snapshot diff, Path C schema-less) → table options →
|
|
883
|
+
* indexes / foreign keys / `afterSyncTable`. Never called for an entry the
|
|
884
|
+
* plan already marks `error` (`SchemaSync.execute()` reports the plan entry
|
|
885
|
+
* for those without DDL).
|
|
886
|
+
*
|
|
887
|
+
* A statement the engine refuses anywhere in those phases (since 0.1.129)
|
|
888
|
+
* becomes an `error` entry — the plan entry plus `<phase> failed on <table>:
|
|
889
|
+
* <cause>` — and the run goes on with the next table. The entry names the
|
|
890
|
+
* foreign keys already dropped ahead of the failure (by `execute()` step 2
|
|
891
|
+
* for a key-changing parent, or by Path A before the column ops): they stay
|
|
892
|
+
* gone until the next run's `syncForeignKeys` re-adds the ones the model
|
|
893
|
+
* still wants. `renamedFrom` on that entry is set only when the rename ran.
|
|
894
|
+
*/
|
|
809
895
|
async function executeSyncTable(facts, safe, deps, exec) {
|
|
810
896
|
const { readable, name, storedSnapshot, fkDiff } = facts;
|
|
811
897
|
const adapter = readable.dbAdapter;
|
|
@@ -816,90 +902,104 @@ async function executeSyncTable(facts, safe, deps, exec) {
|
|
|
816
902
|
dependsOn: exec.dependsOn
|
|
817
903
|
};
|
|
818
904
|
const ensureOpts = exec.deferForeignKeysTo ? { deferForeignKeysTo: exec.deferForeignKeysTo } : void 0;
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
if (
|
|
833
|
-
await adapter.
|
|
834
|
-
init.
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
905
|
+
/** FK keys dropped before the statement that failed — named by the error entry. */
|
|
906
|
+
const droppedFks = [...exec.preDroppedFks ?? []];
|
|
907
|
+
let phase = "Rename";
|
|
908
|
+
try {
|
|
909
|
+
if (facts.pendingRename && adapter.renameTable) {
|
|
910
|
+
await adapter.renameTable(facts.pendingRename);
|
|
911
|
+
init.renamedFrom = facts.pendingRename;
|
|
912
|
+
init.status = "alter";
|
|
913
|
+
}
|
|
914
|
+
phase = "Column sync";
|
|
915
|
+
const hasFkChanges = fkDiff ? hasForeignKeyChanges(fkDiff) : false;
|
|
916
|
+
if (adapter.getExistingColumns && adapter.syncColumns) {
|
|
917
|
+
const existing = facts.existing ?? await adapter.getExistingColumns();
|
|
918
|
+
if (existing.length === 0 && !init.renamedFrom) {
|
|
919
|
+
await adapter.ensureTable(ensureOpts);
|
|
920
|
+
init.status = "create";
|
|
921
|
+
} else if (existing.length > 0) {
|
|
922
|
+
const diff = facts.diff ?? computeColumnDiff(readable.fieldDescriptors, existing, adapter.typeMapper?.bind(adapter));
|
|
923
|
+
if (hasFkChanges && !adapter.syncForeignKeys && adapter.recreateTable) {
|
|
924
|
+
await adapter.recreateTable();
|
|
925
|
+
init.recreated = true;
|
|
926
|
+
init.status = "alter";
|
|
927
|
+
if (diff.primaryKeyChanged) init.pkChange = {
|
|
928
|
+
...diff.primaryKeyChanged,
|
|
929
|
+
rebuild: true
|
|
930
|
+
};
|
|
931
|
+
} else {
|
|
932
|
+
if (hasFkChanges && fkDiff && adapter.dropForeignKeys) {
|
|
933
|
+
const keysToDrop = [...fkDiff.removed.map((fk) => fkKey(fk.fields)), ...fkDiff.changed.map((fk) => fkKey(fk.desired.fields))];
|
|
934
|
+
if (keysToDrop.length > 0) {
|
|
935
|
+
await adapter.dropForeignKeys(keysToDrop);
|
|
936
|
+
droppedFks.push(...keysToDrop);
|
|
937
|
+
init.status = "alter";
|
|
938
|
+
}
|
|
846
939
|
}
|
|
940
|
+
await applyColumnDiff(adapter, readable, diff, init, safe, deps.logger, ensureOpts);
|
|
847
941
|
}
|
|
848
|
-
await applyColumnDiff(adapter, readable, diff, init, safe, deps.logger, ensureOpts);
|
|
849
942
|
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
const existed = adapter.tableExists ? await adapter.tableExists() : false;
|
|
853
|
-
await adapter.ensureTable(ensureOpts);
|
|
854
|
-
if (!existed) init.status = "create";
|
|
855
|
-
} else await applyColumnDiff(adapter, readable, facts.diff ?? computeColumnDiff(readable.fieldDescriptors, snapshotToExistingColumns(storedSnapshot), deps.resolveTypeMapper(adapter)), init, safe, deps.logger, ensureOpts);
|
|
856
|
-
else {
|
|
857
|
-
const existed = adapter.tableExists ? await adapter.tableExists() : true;
|
|
858
|
-
if (!init.recreated) {
|
|
943
|
+
} else if (adapter.syncColumns) if (!storedSnapshot) {
|
|
944
|
+
const existed = adapter.tableExists ? await adapter.tableExists() : false;
|
|
859
945
|
await adapter.ensureTable(ensureOpts);
|
|
860
946
|
if (!existed) init.status = "create";
|
|
947
|
+
} else await applyColumnDiff(adapter, readable, facts.diff ?? computeColumnDiff(readable.fieldDescriptors, snapshotToExistingColumns(storedSnapshot), deps.resolveTypeMapper(adapter)), init, safe, deps.logger, ensureOpts);
|
|
948
|
+
else {
|
|
949
|
+
const existed = adapter.tableExists ? await adapter.tableExists() : true;
|
|
950
|
+
if (!init.recreated) {
|
|
951
|
+
await adapter.ensureTable(ensureOpts);
|
|
952
|
+
if (!existed) init.status = "create";
|
|
953
|
+
}
|
|
861
954
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
955
|
+
phase = "Table option sync";
|
|
956
|
+
const optionDiff = facts.optionDiff;
|
|
957
|
+
if (init.status !== "create" && !init.recreated && init.status !== "error" && optionDiff && optionDiff.changed.length > 0) {
|
|
958
|
+
const hasDestructive = optionDiff.changed.some((c) => c.destructive);
|
|
959
|
+
if (safe) {
|
|
960
|
+
if (hasDestructive) {
|
|
961
|
+
init.optionChanges = optionDiff.changed;
|
|
962
|
+
init.status = "alter";
|
|
963
|
+
markSkipped(init, "table-options");
|
|
964
|
+
deps.logger.warn?.(`[schema-sync] Destructive table option change on "${name}" — recreate skipped (safe mode)`);
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
868
967
|
init.optionChanges = optionDiff.changed;
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
}
|
|
873
|
-
} else {
|
|
874
|
-
init.optionChanges = optionDiff.changed;
|
|
875
|
-
const nonDestructive = optionDiff.changed.filter((c) => !c.destructive);
|
|
876
|
-
if (nonDestructive.length > 0 && adapter.applyTableOptions) {
|
|
877
|
-
await adapter.applyTableOptions(nonDestructive);
|
|
878
|
-
init.status = "alter";
|
|
879
|
-
}
|
|
880
|
-
if (hasDestructive) {
|
|
881
|
-
if (willDropRecreate(readable, void 0, optionDiff)) {
|
|
882
|
-
deps.logger.warn?.(`[schema-sync] Destructive table option change on "${name}" — dropping and recreating`);
|
|
883
|
-
await dropAndRecreate(adapter, init, name, deps.logger, ensureOpts);
|
|
884
|
-
} else if (readable.syncMethod === "recreate" && adapter.recreateTable) {
|
|
885
|
-
deps.logger.warn?.(`[schema-sync] Destructive table option change on "${name}" — recreating with data preservation`);
|
|
886
|
-
await adapter.recreateTable();
|
|
968
|
+
const nonDestructive = optionDiff.changed.filter((c) => !c.destructive);
|
|
969
|
+
if (nonDestructive.length > 0 && adapter.applyTableOptions) {
|
|
970
|
+
await adapter.applyTableOptions(nonDestructive);
|
|
887
971
|
init.status = "alter";
|
|
888
|
-
|
|
972
|
+
}
|
|
973
|
+
if (hasDestructive) {
|
|
974
|
+
if (willDropRecreate(readable, void 0, optionDiff)) {
|
|
975
|
+
deps.logger.warn?.(`[schema-sync] Destructive table option change on "${name}" — dropping and recreating`);
|
|
976
|
+
await dropAndRecreate(adapter, init, name, ensureOpts);
|
|
977
|
+
} else if (readable.syncMethod === "recreate" && adapter.recreateTable) {
|
|
978
|
+
deps.logger.warn?.(`[schema-sync] Destructive table option change on "${name}" — recreating with data preservation`);
|
|
979
|
+
await adapter.recreateTable();
|
|
980
|
+
init.status = "alter";
|
|
981
|
+
init.recreated = true;
|
|
982
|
+
}
|
|
889
983
|
}
|
|
890
984
|
}
|
|
891
985
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
try {
|
|
986
|
+
if (init.status === "error") return new SyncEntry(init);
|
|
987
|
+
phase = "Index/FK sync";
|
|
895
988
|
await adapter.syncIndexes();
|
|
896
989
|
if (adapter.syncForeignKeys && !exec.deferForeignKeysTo) await adapter.syncForeignKeys();
|
|
897
990
|
if (adapter.afterSyncTable) await adapter.afterSyncTable();
|
|
898
991
|
} catch (error) {
|
|
899
|
-
const
|
|
992
|
+
const dropped = droppedFks.length > 0 ? ` Dropped foreign keys before the failure: ${droppedFks.join(", ")} — the ones still in the model are re-added by the next run's syncForeignKeys.` : "";
|
|
993
|
+
const msg = `${phase} failed on ${name}: ${error.message}${dropped}`;
|
|
900
994
|
deps.logger.error?.(`[schema-sync] ${msg}`);
|
|
901
|
-
|
|
902
|
-
|
|
995
|
+
const planned = planTableInit(facts, safe, exec.dependsOn);
|
|
996
|
+
return new SyncEntry({
|
|
997
|
+
...planned,
|
|
998
|
+
status: "error",
|
|
999
|
+
errors: [...planned.errors ?? [], msg],
|
|
1000
|
+
renamedFrom: init.renamedFrom,
|
|
1001
|
+
recreated: false
|
|
1002
|
+
});
|
|
903
1003
|
}
|
|
904
1004
|
return new SyncEntry(init);
|
|
905
1005
|
}
|
|
@@ -918,11 +1018,7 @@ async function executeDeferredForeignKeys(readable, entry, deps) {
|
|
|
918
1018
|
} catch (error) {
|
|
919
1019
|
const msg = `FK sync failed on ${readable.tableName}: ${error.message}`;
|
|
920
1020
|
deps.logger.error?.(`[schema-sync] ${msg}`);
|
|
921
|
-
return
|
|
922
|
-
...entry.toInit(),
|
|
923
|
-
status: "error",
|
|
924
|
-
errors: [...entry.errors, msg]
|
|
925
|
-
});
|
|
1021
|
+
return entry.withError(msg);
|
|
926
1022
|
}
|
|
927
1023
|
}
|
|
928
1024
|
/** Determines whether a view's predecessor (on rename) or stale definition must be dropped. */
|
|
@@ -1000,12 +1096,9 @@ function safeModeSkips(facts) {
|
|
|
1000
1096
|
const out = [];
|
|
1001
1097
|
if (facts.planEntry.status === "error") return out;
|
|
1002
1098
|
if (fkDiff && hasForeignKeyChanges(fkDiff) && !adapter.syncForeignKeys && adapter.recreateTable) return out;
|
|
1003
|
-
|
|
1004
|
-
if (
|
|
1005
|
-
|
|
1006
|
-
else if (readable.syncMethod === "recreate" && adapter.recreateTable) recreated = true;
|
|
1007
|
-
}
|
|
1008
|
-
if (recreated) return out;
|
|
1099
|
+
const strategy = diff && diff.typeChanged.length > 0 ? typeChangeStrategy(readable, diff) : "modify";
|
|
1100
|
+
if (strategy === "drop") out.push("recreate");
|
|
1101
|
+
else if (strategy === "recreate") return out;
|
|
1009
1102
|
if (diff && (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) && needsDdlForNullableDefaults(adapter)) out.push("nullable-defaults");
|
|
1010
1103
|
if (diff?.primaryKeyChanged) out.push("pk-rebuild");
|
|
1011
1104
|
if (optionDiff?.changed.some((c) => c.destructive)) out.push("table-options");
|
|
@@ -1029,21 +1122,18 @@ function describeNullableDefaults(diff) {
|
|
|
1029
1122
|
* `dropTable()` + `ensureTable()` for `@db.sync.method "drop"` and destructive
|
|
1030
1123
|
* option changes. Sync-owned drops never CASCADE: when something outside the
|
|
1031
1124
|
* sync inventory (a user view, an unmanaged FK) still depends on the table the
|
|
1032
|
-
* engine refuses
|
|
1033
|
-
*
|
|
1125
|
+
* engine refuses — rethrown as `Drop of "<table>" failed: <cause>` for the
|
|
1126
|
+
* executor's phase catch, which makes it the table's error entry.
|
|
1034
1127
|
*/
|
|
1035
|
-
async function dropAndRecreate(adapter, init, name,
|
|
1128
|
+
async function dropAndRecreate(adapter, init, name, ensureOpts) {
|
|
1036
1129
|
try {
|
|
1037
1130
|
await adapter.dropTable();
|
|
1038
1131
|
await adapter.ensureTable(ensureOpts);
|
|
1039
|
-
init.recreated = true;
|
|
1040
|
-
init.status = "alter";
|
|
1041
1132
|
} catch (error) {
|
|
1042
|
-
|
|
1043
|
-
logger.error?.(`[schema-sync] ${msg}`);
|
|
1044
|
-
init.errors = [...init.errors ?? [], msg];
|
|
1045
|
-
init.status = "error";
|
|
1133
|
+
throw new Error(`Drop of "${name}" failed: ${error.message}`, { cause: error });
|
|
1046
1134
|
}
|
|
1135
|
+
init.recreated = true;
|
|
1136
|
+
init.status = "alter";
|
|
1047
1137
|
}
|
|
1048
1138
|
async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensureOpts) {
|
|
1049
1139
|
const name = readable.tableName;
|
|
@@ -1055,28 +1145,24 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
|
|
|
1055
1145
|
}
|
|
1056
1146
|
let needsSyncColumns = false;
|
|
1057
1147
|
if (diff.typeChanged.length > 0 && init.status !== "error") {
|
|
1058
|
-
const
|
|
1059
|
-
if (
|
|
1148
|
+
const strategy = typeChangeStrategy(readable, diff);
|
|
1149
|
+
if (strategy === "drop") if (safe) {
|
|
1060
1150
|
init.typeChanges = describeTypeChanges(diff);
|
|
1061
1151
|
markSkipped(init, "recreate");
|
|
1062
1152
|
init.status = "alter";
|
|
1063
1153
|
const cols = init.typeChanges.map((tc) => `${tc.column}: ${tc.fromType} → ${tc.toType}`).join(", ");
|
|
1064
1154
|
logger.warn?.(`[schema-sync] Type change on "${name}" (${cols}) — drop-and-recreate skipped (safe mode)`);
|
|
1065
|
-
} else await dropAndRecreate(adapter, init, name,
|
|
1066
|
-
else if (
|
|
1155
|
+
} else await dropAndRecreate(adapter, init, name, ensureOpts);
|
|
1156
|
+
else if (strategy === "recreate") {
|
|
1067
1157
|
await adapter.recreateTable();
|
|
1068
1158
|
init.recreated = true;
|
|
1069
1159
|
init.status = "alter";
|
|
1070
|
-
} else if (
|
|
1160
|
+
} else if (strategy === "modify") {
|
|
1071
1161
|
needsSyncColumns = true;
|
|
1072
1162
|
init.status = "alter";
|
|
1073
1163
|
} else {
|
|
1074
|
-
const errors =
|
|
1075
|
-
for (const
|
|
1076
|
-
const msg = `Type change on ${name}.${change.field.physicalName} (${change.existingType} → ${change.field.designType}). Add @db.sync.method "recreate" or "drop", or migrate manually.`;
|
|
1077
|
-
logger.error?.(`[schema-sync] ${msg}`);
|
|
1078
|
-
errors.push(msg);
|
|
1079
|
-
}
|
|
1164
|
+
const errors = typeChangeErrors(readable, diff);
|
|
1165
|
+
for (const msg of errors) logger.error?.(`[schema-sync] ${msg}`);
|
|
1080
1166
|
init.errors = errors;
|
|
1081
1167
|
init.status = "error";
|
|
1082
1168
|
}
|
|
@@ -1107,14 +1193,13 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
|
|
|
1107
1193
|
}
|
|
1108
1194
|
if (diff.primaryKeyChanged && init.status !== "error") {
|
|
1109
1195
|
const { from, to } = diff.primaryKeyChanged;
|
|
1110
|
-
const label = `(${from.join(", ")} → ${to.join(", ")})`;
|
|
1111
1196
|
if (init.recreated) init.pkChange = {
|
|
1112
1197
|
from,
|
|
1113
1198
|
to,
|
|
1114
1199
|
rebuild: true
|
|
1115
1200
|
};
|
|
1116
1201
|
else if (safe) {
|
|
1117
|
-
logger.warn?.(`[schema-sync] Primary key of "${name}" changed ${
|
|
1202
|
+
logger.warn?.(`[schema-sync] Primary key of "${name}" changed ${pkLabel(diff.primaryKeyChanged)} — rebuild skipped (safe mode)`);
|
|
1118
1203
|
init.pkChange = {
|
|
1119
1204
|
from,
|
|
1120
1205
|
to,
|
|
@@ -1138,7 +1223,7 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
|
|
|
1138
1223
|
};
|
|
1139
1224
|
init.status = "alter";
|
|
1140
1225
|
} else {
|
|
1141
|
-
const msg =
|
|
1226
|
+
const msg = pkRebuildUnsupported(name, diff.primaryKeyChanged);
|
|
1142
1227
|
logger.error?.(`[schema-sync] ${msg}`);
|
|
1143
1228
|
init.errors = [...init.errors ?? [], msg];
|
|
1144
1229
|
init.status = "error";
|
|
@@ -1167,9 +1252,6 @@ function buildFkChangeDetails(desired, existing) {
|
|
|
1167
1252
|
if ((existing.onUpdate ?? void 0) !== (desired.onUpdate ?? void 0)) parts.push(`onUpdate ${existing.onUpdate ?? "noAction"} → ${desired.onUpdate ?? "noAction"}`);
|
|
1168
1253
|
return parts.join(", ");
|
|
1169
1254
|
}
|
|
1170
|
-
function pkLabel(change) {
|
|
1171
|
-
return `(${change.from.join(", ")} → ${change.to.join(", ")})`;
|
|
1172
|
-
}
|
|
1173
1255
|
function sortedUnique(names) {
|
|
1174
1256
|
return [...new Set(names)].toSorted();
|
|
1175
1257
|
}
|
|
@@ -1428,7 +1510,7 @@ var SchemaSync = class {
|
|
|
1428
1510
|
const trackedNames = new Set(previouslyTracked.map((e) => e.name));
|
|
1429
1511
|
const removedTracked = this.detectRemoved(allReadables, previouslyTracked);
|
|
1430
1512
|
const probeRecreateInbound = !safe && removedTracked.some((r) => !r.isView);
|
|
1431
|
-
const tableFacts = await Promise.all(tables.map((r) => this.discoverTable(r, trackedNames, probeRecreateInbound)));
|
|
1513
|
+
const tableFacts = await Promise.all(tables.map((r) => this.discoverTable(r, trackedNames, probeRecreateInbound, safe)));
|
|
1432
1514
|
const tableNames = new Set(tableFacts.map((t) => t.name));
|
|
1433
1515
|
const tableByName = new Map(tableFacts.map((t) => [t.name, t]));
|
|
1434
1516
|
const tableByDbName = new Map(tableFacts.map((t) => [t.dbName, t]));
|
|
@@ -1540,8 +1622,9 @@ var SchemaSync = class {
|
|
|
1540
1622
|
* Introspects one desired table and computes its plan entry.
|
|
1541
1623
|
* `probeRecreateInbound` — whether a drop-and-recreate's live inbound FKs
|
|
1542
1624
|
* are worth probing (a removed table exists that could block it).
|
|
1625
|
+
* `safe` — safe mode skips the key rebuild, so its plan rule does not apply.
|
|
1543
1626
|
*/
|
|
1544
|
-
async discoverTable(readable, trackedNames, probeRecreateInbound) {
|
|
1627
|
+
async discoverTable(readable, trackedNames, probeRecreateInbound, safe) {
|
|
1545
1628
|
const adapter = readable.dbAdapter;
|
|
1546
1629
|
const name = readable.tableName;
|
|
1547
1630
|
const init = {
|
|
@@ -1576,7 +1659,7 @@ var SchemaSync = class {
|
|
|
1576
1659
|
} else if (facts.existing && facts.existing.length > 0) {
|
|
1577
1660
|
const typeMapper = adapter.typeMapper?.bind(adapter);
|
|
1578
1661
|
facts.diff = computeColumnDiff(readable.fieldDescriptors, facts.existing, typeMapper);
|
|
1579
|
-
this.populatePlanFromDiff(facts.diff, init,
|
|
1662
|
+
this.populatePlanFromDiff(facts.diff, init, readable, safe);
|
|
1580
1663
|
}
|
|
1581
1664
|
} else if (adapter.syncColumns) if (!storedSnapshot) {
|
|
1582
1665
|
if (!pendingRename) {
|
|
@@ -1588,7 +1671,7 @@ var SchemaSync = class {
|
|
|
1588
1671
|
} else {
|
|
1589
1672
|
const existing = snapshotToExistingColumns(storedSnapshot);
|
|
1590
1673
|
facts.diff = computeColumnDiff(readable.fieldDescriptors, existing, this.resolveTypeMapper(adapter));
|
|
1591
|
-
this.populatePlanFromDiff(facts.diff, init,
|
|
1674
|
+
this.populatePlanFromDiff(facts.diff, init, readable, safe);
|
|
1592
1675
|
}
|
|
1593
1676
|
else if (adapter.tableExists) {
|
|
1594
1677
|
if (!await adapter.tableExists()) init.status = "create";
|
|
@@ -1639,10 +1722,17 @@ var SchemaSync = class {
|
|
|
1639
1722
|
if (t.populated === true) addRefusal(refusals, t.name, `Primary key of "${t.name}" changed ${label} but the table has rows; schema sync cannot rebuild a populated primary key. Migrate manually (or empty the table) and re-run.`);
|
|
1640
1723
|
else if (t.populated === "unknown") addRefusal(refusals, t.name, `Primary key of "${t.name}" changed ${label} but the adapter cannot tell whether the table has rows under its old name "${t.dbName}" — implement hasRows(tableName) on the adapter, or rename the table in a separate run first.`);
|
|
1641
1724
|
if (!safe) {
|
|
1642
|
-
|
|
1725
|
+
const liveInbound = (t.inboundFks ?? []).filter((fk) => {
|
|
1643
1726
|
const removed = d.removedByName.get(fk.table);
|
|
1644
|
-
|
|
1727
|
+
return !(removed && !removed.isView);
|
|
1728
|
+
});
|
|
1729
|
+
if (t.planEntry.status === "error" && liveInbound.length > 0) addRefusal(refusals, t.name, `Primary key of "${t.name}" changed ${label} but the table's own entry has errors — fix them and re-run: ${t.planEntry.errors?.[0] ?? "see its entry"}`);
|
|
1730
|
+
for (const fk of liveInbound) {
|
|
1645
1731
|
const child = d.tableByDbName.get(fk.table);
|
|
1732
|
+
if (child?.planEntry.status === "error") {
|
|
1733
|
+
addRefusal(refusals, t.name, `Primary key of "${t.name}" changed ${label} but the referencing table "${child.name}" has errors — fix them and re-run: ${child.planEntry.errors?.[0] ?? "see its entry"}`);
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1646
1736
|
if (child?.pendingRename) {
|
|
1647
1737
|
addRefusal(refusals, t.name, `Primary key of "${t.name}" changed ${label} but the referencing table "${child.name}" is renamed in the same run (from "${child.pendingRename}") — rename it in a separate run first, then retarget the foreign key and re-run.`);
|
|
1648
1738
|
continue;
|
|
@@ -1719,25 +1809,7 @@ var SchemaSync = class {
|
|
|
1719
1809
|
break;
|
|
1720
1810
|
case "table": {
|
|
1721
1811
|
const t = d.tableByName.get(step.name);
|
|
1722
|
-
|
|
1723
|
-
...t.planEntry,
|
|
1724
|
-
dependsOn: d.tableDependsOn.get(step.name)
|
|
1725
|
-
};
|
|
1726
|
-
const skipped = safe ? safeModeSkips(t) : [];
|
|
1727
|
-
const pk = t.diff?.primaryKeyChanged;
|
|
1728
|
-
if (pk) init.pkChange = {
|
|
1729
|
-
from: pk.from,
|
|
1730
|
-
to: pk.to,
|
|
1731
|
-
rebuild: !skipped.includes("pk-rebuild")
|
|
1732
|
-
};
|
|
1733
|
-
if (safe) init = {
|
|
1734
|
-
...init,
|
|
1735
|
-
columnsToDrop: [],
|
|
1736
|
-
typeChanges: skipped.includes("recreate") ? init.typeChanges : [],
|
|
1737
|
-
skipped: skipped.length > 0 ? skipped : void 0,
|
|
1738
|
-
recreated: false
|
|
1739
|
-
};
|
|
1740
|
-
entries.push(withRefusals(init, refusals.get(step.name)));
|
|
1812
|
+
entries.push(withRefusals(planTableInit(t, safe, d.tableDependsOn.get(step.name)), refusals.get(step.name)));
|
|
1741
1813
|
break;
|
|
1742
1814
|
}
|
|
1743
1815
|
case "deferred-fks": break;
|
|
@@ -1760,7 +1832,28 @@ var SchemaSync = class {
|
|
|
1760
1832
|
/** Removed entries that were NOT dropped — they stay tracked (I2). */
|
|
1761
1833
|
const retained = [];
|
|
1762
1834
|
const droppedNames = /* @__PURE__ */ new Set();
|
|
1763
|
-
|
|
1835
|
+
/** Managed views whose stale definition could not be dropped (→ error entry, not recreated). */
|
|
1836
|
+
const failedViews = /* @__PURE__ */ new Map();
|
|
1837
|
+
/**
|
|
1838
|
+
* Pending renames that did NOT run (the rename failed, or the entry
|
|
1839
|
+
* issued no DDL): the object still exists under its old name, so it stays
|
|
1840
|
+
* tracked there and keeps its old-name snapshot — the next run sees the
|
|
1841
|
+
* rename as pending again. Keyed by the readable's (new) name.
|
|
1842
|
+
*/
|
|
1843
|
+
const stillUnderOldName = /* @__PURE__ */ new Map();
|
|
1844
|
+
for (const view of d.views) {
|
|
1845
|
+
const plan = d.viewPlans.get(view.tableName);
|
|
1846
|
+
try {
|
|
1847
|
+
await dropOutdatedView(view, plan, this.space);
|
|
1848
|
+
} catch (error) {
|
|
1849
|
+
failedViews.set(view.tableName, this.stepFailed(error, "View sync", view.tableName));
|
|
1850
|
+
if (plan.isRenamed) stillUnderOldName.set(view.tableName, {
|
|
1851
|
+
name: view.renamedFrom,
|
|
1852
|
+
isView: true,
|
|
1853
|
+
viewType: view.viewPlan.materialized ? "M" : "V"
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1764
1857
|
const droppedViewEntries = /* @__PURE__ */ new Map();
|
|
1765
1858
|
for (const r of d.removed) {
|
|
1766
1859
|
if (!r.isView) continue;
|
|
@@ -1784,11 +1877,19 @@ var SchemaSync = class {
|
|
|
1784
1877
|
retained.push(r);
|
|
1785
1878
|
}
|
|
1786
1879
|
}
|
|
1880
|
+
/** FK keys dropped here per child (by its name) — named by a later failure on that child. */
|
|
1881
|
+
const preDroppedFks = /* @__PURE__ */ new Map();
|
|
1787
1882
|
if (!safe) for (const t of d.tables) {
|
|
1788
1883
|
if (!t.diff?.primaryKeyChanged) continue;
|
|
1789
1884
|
const keysByChild = /* @__PURE__ */ new Map();
|
|
1790
1885
|
for (const fk of t.inboundFks ?? []) keysByChild.set(fk.table, [...keysByChild.get(fk.table) ?? [], fkKey(fk.fields)]);
|
|
1791
|
-
for (const [childName, fkKeys] of keysByChild)
|
|
1886
|
+
for (const [childName, fkKeys] of keysByChild) {
|
|
1887
|
+
const child = d.tableByDbName.get(childName);
|
|
1888
|
+
if (child?.readable.dbAdapter.dropForeignKeys) {
|
|
1889
|
+
await child.readable.dbAdapter.dropForeignKeys(fkKeys);
|
|
1890
|
+
preDroppedFks.set(child.name, [...preDroppedFks.get(child.name) ?? [], ...fkKeys]);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1792
1893
|
}
|
|
1793
1894
|
const deferred = [];
|
|
1794
1895
|
const drops = {
|
|
@@ -1805,9 +1906,16 @@ var SchemaSync = class {
|
|
|
1805
1906
|
break;
|
|
1806
1907
|
case "table": {
|
|
1807
1908
|
const t = d.tableByName.get(step.name);
|
|
1808
|
-
const
|
|
1909
|
+
const dependsOn = d.tableDependsOn.get(step.name);
|
|
1910
|
+
const entry = t.planEntry.status === "error" ? new SyncEntry(planTableInit(t, safe, dependsOn)) : await executeSyncTable(t, safe, deps, {
|
|
1809
1911
|
deferForeignKeysTo: step.cycle,
|
|
1810
|
-
dependsOn
|
|
1912
|
+
dependsOn,
|
|
1913
|
+
preDroppedFks: preDroppedFks.get(t.name)
|
|
1914
|
+
});
|
|
1915
|
+
const renamed = t.planEntry.status !== "error" && entry.renamedFrom !== void 0;
|
|
1916
|
+
if (t.pendingRename && !renamed) stillUnderOldName.set(t.name, {
|
|
1917
|
+
name: t.pendingRename,
|
|
1918
|
+
isView: false
|
|
1811
1919
|
});
|
|
1812
1920
|
if (step.cycle) deferred.push({
|
|
1813
1921
|
readable: t.readable,
|
|
@@ -1822,9 +1930,19 @@ var SchemaSync = class {
|
|
|
1822
1930
|
entries[index] = await executeDeferredForeignKeys(readable, entries[index], deps);
|
|
1823
1931
|
}
|
|
1824
1932
|
break;
|
|
1825
|
-
case "view":
|
|
1826
|
-
|
|
1933
|
+
case "view": {
|
|
1934
|
+
const view = d.views[step.index];
|
|
1935
|
+
const planned = d.viewEntries[step.index];
|
|
1936
|
+
let msg = failedViews.get(view.tableName);
|
|
1937
|
+
if (msg === void 0) try {
|
|
1938
|
+
entries.push(await executeSyncView(view, planned));
|
|
1939
|
+
break;
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
msg = this.stepFailed(error, "View sync", view.tableName);
|
|
1942
|
+
}
|
|
1943
|
+
entries.push(planned.withError(msg));
|
|
1827
1944
|
break;
|
|
1945
|
+
}
|
|
1828
1946
|
case "external":
|
|
1829
1947
|
entries.push(d.externalEntries[step.index]);
|
|
1830
1948
|
break;
|
|
@@ -1851,8 +1969,8 @@ var SchemaSync = class {
|
|
|
1851
1969
|
await this.store.writeTableSnapshot(readable.tableName, snapshot);
|
|
1852
1970
|
}
|
|
1853
1971
|
for (const name of droppedNames) await this.store.deleteTableSnapshot(name);
|
|
1854
|
-
for (const readable of allReadables) if (readable.renamedFrom) await this.store.deleteTableSnapshot(readable.renamedFrom);
|
|
1855
|
-
await this.store.writeTrackedList(allReadables, retained);
|
|
1972
|
+
for (const readable of allReadables) if (readable.renamedFrom && !stillUnderOldName.has(readable.tableName)) await this.store.deleteTableSnapshot(readable.renamedFrom);
|
|
1973
|
+
await this.store.writeTrackedList(allReadables.filter((r) => !stillUnderOldName.has(r.tableName)), [...retained, ...stillUnderOldName.values()]);
|
|
1856
1974
|
if (pendingNames.size === 0) await this.store.writeHash(hash);
|
|
1857
1975
|
return {
|
|
1858
1976
|
status: "synced",
|
|
@@ -1898,6 +2016,12 @@ var SchemaSync = class {
|
|
|
1898
2016
|
for (const name of group) droppedNames.add(name);
|
|
1899
2017
|
return group.map((name) => d.dropEntries.get(name));
|
|
1900
2018
|
}
|
|
2019
|
+
/** The error-entry message of a DDL failure inside a view step, logged at error level. */
|
|
2020
|
+
stepFailed(error, phase, name) {
|
|
2021
|
+
const msg = `${phase} failed on ${name}: ${error.message}`;
|
|
2022
|
+
this.logger.error?.(`[schema-sync] ${msg}`);
|
|
2023
|
+
return msg;
|
|
2024
|
+
}
|
|
1901
2025
|
/**
|
|
1902
2026
|
* Surfaces the run outcome per the `onError` policy. Reporting must never be
|
|
1903
2027
|
* silently lost to the NoopLogger default — when no real logger is
|
|
@@ -1953,8 +2077,17 @@ var SchemaSync = class {
|
|
|
1953
2077
|
}
|
|
1954
2078
|
/**
|
|
1955
2079
|
* Populates plan init from a column diff (shared by Path A and Path B).
|
|
2080
|
+
*
|
|
2081
|
+
* The plan's rule set is canonical (since 0.1.129): every change the
|
|
2082
|
+
* executor cannot apply without DDL it does not have is an `error` entry
|
|
2083
|
+
* here, and `executeSyncTable` returns an errored plan entry untouched —
|
|
2084
|
+
* so the executor's own copies of these rules are reached only when
|
|
2085
|
+
* discovery had no diff (a pending rename whose old name it could not
|
|
2086
|
+
* introspect).
|
|
1956
2087
|
*/
|
|
1957
|
-
populatePlanFromDiff(diff, init,
|
|
2088
|
+
populatePlanFromDiff(diff, init, readable, safe) {
|
|
2089
|
+
const name = readable.tableName;
|
|
2090
|
+
const adapter = readable.dbAdapter;
|
|
1958
2091
|
init.columnsToAdd = diff.added;
|
|
1959
2092
|
init.columnsToRename = diff.renamed.map((r) => ({
|
|
1960
2093
|
from: r.oldName,
|
|
@@ -1968,9 +2101,13 @@ var SchemaSync = class {
|
|
|
1968
2101
|
init.status = "error";
|
|
1969
2102
|
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.`)];
|
|
1970
2103
|
}
|
|
1971
|
-
if (diff.typeChanged.length > 0 &&
|
|
2104
|
+
if (diff.typeChanged.length > 0 && typeChangeStrategy(readable, diff) === "none") {
|
|
2105
|
+
init.status = "error";
|
|
2106
|
+
init.errors = [...init.errors ?? [], ...typeChangeErrors(readable, diff)];
|
|
2107
|
+
}
|
|
2108
|
+
if (diff.primaryKeyChanged && !safe && !adapter.rebuildPrimaryKey && !adapter.recreateTable && init.status !== "error") {
|
|
1972
2109
|
init.status = "error";
|
|
1973
|
-
init.errors = [...init.errors ?? [],
|
|
2110
|
+
init.errors = [...init.errors ?? [], pkRebuildUnsupported(name, diff.primaryKeyChanged)];
|
|
1974
2111
|
}
|
|
1975
2112
|
}
|
|
1976
2113
|
/**
|