@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.
Files changed (39) hide show
  1. package/dist/agg.cjs +31 -0
  2. package/dist/agg.d.cts +30 -1
  3. package/dist/agg.d.mts +30 -1
  4. package/dist/agg.mjs +31 -1
  5. package/dist/{db-error-DXwEzmYJ.cjs → db-error-C4JuLcvb.cjs} +27 -0
  6. package/dist/{db-error-BHPXOKzc.mjs → db-error-COrO58t5.mjs} +22 -1
  7. package/dist/{db-readable-CkbZn_z-.d.cts → db-readable-B7eYWS5q.d.cts} +10 -1
  8. package/dist/{db-readable-BTl4NzNN.d.mts → db-readable-Bn1bV_eC.d.mts} +10 -1
  9. package/dist/{db-space-BEB5Jl8M.d.mts → db-space-C2UCnGHd.d.cts} +28 -2
  10. package/dist/{db-space-BevOyVrC.d.cts → db-space-DdIPYD0Q.d.mts} +28 -2
  11. package/dist/{db-view-Ck0I-PMI.mjs → db-view-BkF2xEGd.mjs} +98 -8
  12. package/dist/{db-view-C8-MsjND.cjs → db-view-hkwQ-fHJ.cjs} +98 -8
  13. package/dist/index.cjs +4 -3
  14. package/dist/index.d.cts +4 -4
  15. package/dist/index.d.mts +4 -4
  16. package/dist/index.mjs +4 -4
  17. package/dist/{nested-writer-DI-HeTky.mjs → nested-writer-CkDo-ZfH.mjs} +1 -1
  18. package/dist/{nested-writer-DoDhl3X3.cjs → nested-writer-DxPhmWFz.cjs} +1 -1
  19. package/dist/ops.cjs +1 -1
  20. package/dist/ops.mjs +1 -1
  21. package/dist/rel.cjs +2 -2
  22. package/dist/rel.d.cts +1 -1
  23. package/dist/rel.d.mts +1 -1
  24. package/dist/rel.mjs +2 -2
  25. package/dist/{relation-loader-BnUgJsUG.cjs → relation-loader-C8GOpNYJ.cjs} +1 -1
  26. package/dist/{relation-loader-BmeOMj0b.mjs → relation-loader-CUGcxJ18.mjs} +1 -1
  27. package/dist/sync.cjs +280 -143
  28. package/dist/sync.d.cts +24 -6
  29. package/dist/sync.d.mts +24 -6
  30. package/dist/sync.mjs +280 -143
  31. package/dist/{validator-eKYWf3xf.mjs → validator-CeD_fqyW.mjs} +1 -1
  32. package/dist/{validator-BNUHCXIE.cjs → validator-lkCJKuoo.cjs} +1 -1
  33. package/dist/{validator-DuAmWc8L.d.cts → validator-wBARmD68.d.cts} +21 -2
  34. package/dist/{validator-DuAmWc8L.d.mts → validator-wBARmD68.d.mts} +21 -2
  35. package/dist/validator.cjs +2 -2
  36. package/dist/validator.d.cts +1 -1
  37. package/dist/validator.d.mts +1 -1
  38. package/dist/validator.mjs +2 -2
  39. 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-C8-MsjND.cjs");
2
+ const require_db_view = require("./db-view-hkwQ-fHJ.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
- if (facts.pendingRename && adapter.renameTable) {
820
- await adapter.renameTable(facts.pendingRename);
821
- init.renamedFrom = facts.pendingRename;
822
- init.status = "alter";
823
- }
824
- const hasFkChanges = fkDiff ? hasForeignKeyChanges(fkDiff) : false;
825
- if (adapter.getExistingColumns && adapter.syncColumns) {
826
- const existing = facts.existing ?? await adapter.getExistingColumns();
827
- if (existing.length === 0 && !init.renamedFrom) {
828
- await adapter.ensureTable(ensureOpts);
829
- init.status = "create";
830
- } else if (existing.length > 0) {
831
- const diff = facts.diff ?? computeColumnDiff(readable.fieldDescriptors, existing, adapter.typeMapper?.bind(adapter));
832
- if (hasFkChanges && !adapter.syncForeignKeys && adapter.recreateTable) {
833
- await adapter.recreateTable();
834
- init.recreated = true;
835
- init.status = "alter";
836
- if (diff.primaryKeyChanged) init.pkChange = {
837
- ...diff.primaryKeyChanged,
838
- rebuild: true
839
- };
840
- } else {
841
- if (hasFkChanges && fkDiff && adapter.dropForeignKeys) {
842
- const keysToDrop = [...fkDiff.removed.map((fk) => fkKey(fk.fields)), ...fkDiff.changed.map((fk) => fkKey(fk.desired.fields))];
843
- if (keysToDrop.length > 0) {
844
- await adapter.dropForeignKeys(keysToDrop);
845
- init.status = "alter";
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
- } else if (adapter.syncColumns) if (!storedSnapshot) {
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
- const optionDiff = facts.optionDiff;
864
- if (init.status !== "create" && !init.recreated && init.status !== "error" && optionDiff && optionDiff.changed.length > 0) {
865
- const hasDestructive = optionDiff.changed.some((c) => c.destructive);
866
- if (safe) {
867
- if (hasDestructive) {
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
- init.status = "alter";
870
- markSkipped(init, "table-options");
871
- deps.logger.warn?.(`[schema-sync] Destructive table option change on "${name}" — recreate skipped (safe mode)`);
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
- init.recreated = true;
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
- if (init.status === "error") return new SyncEntry(init);
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 msg = `Index/FK sync failed on ${name}: ${error.message}`;
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
- init.errors = [...init.errors ?? [], msg];
902
- init.status = "error";
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 new SyncEntry({
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
- let recreated = false;
1004
- if (diff && diff.typeChanged.length > 0) {
1005
- if (willDropRecreate(readable, diff, void 0)) out.push("recreate");
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, and that becomes an error entry for this table — not a
1033
- * thrown run.
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, logger, ensureOpts) {
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
- const msg = `Drop of "${name}" failed: ${error.message}`;
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 syncMethod = readable.syncMethod;
1059
- if (willDropRecreate(readable, diff, void 0)) if (safe) {
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, logger, ensureOpts);
1066
- else if (syncMethod === "recreate" && adapter.recreateTable) {
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 (adapter.supportsColumnModify && adapter.syncColumns) {
1160
+ } else if (strategy === "modify") {
1071
1161
  needsSyncColumns = true;
1072
1162
  init.status = "alter";
1073
1163
  } else {
1074
- const errors = [];
1075
- for (const change of diff.typeChanged) {
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 ${label} — rebuild skipped (safe mode)`);
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 = `Primary key of "${name}" changed ${label} but the adapter cannot rebuild primary keys. Migrate manually and re-run.`;
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, name, readable.syncMethod, adapter.supportsColumnModify);
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, name, readable.syncMethod, adapter.supportsColumnModify);
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
- for (const fk of t.inboundFks ?? []) {
1725
+ const liveInbound = (t.inboundFks ?? []).filter((fk) => {
1643
1726
  const removed = d.removedByName.get(fk.table);
1644
- if (removed && !removed.isView) continue;
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
- let init = {
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
- for (const view of d.views) await dropOutdatedView(view, d.viewPlans.get(view.tableName), this.space);
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) await d.tableByDbName.get(childName)?.readable.dbAdapter.dropForeignKeys?.(fkKeys);
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 entry = await executeSyncTable(t, safe, deps, {
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: d.tableDependsOn.get(step.name)
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
- entries.push(await executeSyncView(d.views[step.index], d.viewEntries[step.index]));
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, name, syncMethod, adapterSupportsModify) {
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 && !syncMethod && !adapterSupportsModify) {
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 ?? [], ...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.`)];
2110
+ init.errors = [...init.errors ?? [], pkRebuildUnsupported(name, diff.primaryKeyChanged)];
1974
2111
  }
1975
2112
  }
1976
2113
  /**