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