@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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { M as NoopLogger } from "./db-view-Ck0I-PMI.mjs";
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
- if (facts.pendingRename && adapter.renameTable) {
819
- await adapter.renameTable(facts.pendingRename);
820
- init.renamedFrom = facts.pendingRename;
821
- init.status = "alter";
822
- }
823
- const hasFkChanges = fkDiff ? hasForeignKeyChanges(fkDiff) : false;
824
- if (adapter.getExistingColumns && adapter.syncColumns) {
825
- const existing = facts.existing ?? await adapter.getExistingColumns();
826
- if (existing.length === 0 && !init.renamedFrom) {
827
- await adapter.ensureTable(ensureOpts);
828
- init.status = "create";
829
- } else if (existing.length > 0) {
830
- const diff = facts.diff ?? computeColumnDiff(readable.fieldDescriptors, existing, adapter.typeMapper?.bind(adapter));
831
- if (hasFkChanges && !adapter.syncForeignKeys && adapter.recreateTable) {
832
- await adapter.recreateTable();
833
- init.recreated = true;
834
- init.status = "alter";
835
- if (diff.primaryKeyChanged) init.pkChange = {
836
- ...diff.primaryKeyChanged,
837
- rebuild: true
838
- };
839
- } else {
840
- if (hasFkChanges && fkDiff && adapter.dropForeignKeys) {
841
- const keysToDrop = [...fkDiff.removed.map((fk) => fkKey(fk.fields)), ...fkDiff.changed.map((fk) => fkKey(fk.desired.fields))];
842
- if (keysToDrop.length > 0) {
843
- await adapter.dropForeignKeys(keysToDrop);
844
- init.status = "alter";
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
- } else if (adapter.syncColumns) if (!storedSnapshot) {
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
- const optionDiff = facts.optionDiff;
863
- if (init.status !== "create" && !init.recreated && init.status !== "error" && optionDiff && optionDiff.changed.length > 0) {
864
- const hasDestructive = optionDiff.changed.some((c) => c.destructive);
865
- if (safe) {
866
- if (hasDestructive) {
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
- init.status = "alter";
869
- markSkipped(init, "table-options");
870
- deps.logger.warn?.(`[schema-sync] Destructive table option change on "${name}" — recreate skipped (safe mode)`);
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
- init.recreated = true;
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
- if (init.status === "error") return new SyncEntry(init);
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 msg = `Index/FK sync failed on ${name}: ${error.message}`;
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
- init.errors = [...init.errors ?? [], msg];
901
- init.status = "error";
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 new SyncEntry({
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
- let recreated = false;
1003
- if (diff && diff.typeChanged.length > 0) {
1004
- if (willDropRecreate(readable, diff, void 0)) out.push("recreate");
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, and that becomes an error entry for this table — not a
1032
- * thrown run.
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, logger, ensureOpts) {
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
- const msg = `Drop of "${name}" failed: ${error.message}`;
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 syncMethod = readable.syncMethod;
1058
- if (willDropRecreate(readable, diff, void 0)) if (safe) {
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, logger, ensureOpts);
1065
- else if (syncMethod === "recreate" && adapter.recreateTable) {
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 (adapter.supportsColumnModify && adapter.syncColumns) {
1159
+ } else if (strategy === "modify") {
1070
1160
  needsSyncColumns = true;
1071
1161
  init.status = "alter";
1072
1162
  } else {
1073
- const errors = [];
1074
- for (const change of diff.typeChanged) {
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 ${label} — rebuild skipped (safe mode)`);
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 = `Primary key of "${name}" changed ${label} but the adapter cannot rebuild primary keys. Migrate manually and re-run.`;
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, name, readable.syncMethod, adapter.supportsColumnModify);
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, name, readable.syncMethod, adapter.supportsColumnModify);
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
- for (const fk of t.inboundFks ?? []) {
1724
+ const liveInbound = (t.inboundFks ?? []).filter((fk) => {
1642
1725
  const removed = d.removedByName.get(fk.table);
1643
- if (removed && !removed.isView) continue;
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
- let init = {
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
- for (const view of d.views) await dropOutdatedView(view, d.viewPlans.get(view.tableName), this.space);
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) await d.tableByDbName.get(childName)?.readable.dbAdapter.dropForeignKeys?.(fkKeys);
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 entry = await executeSyncTable(t, safe, deps, {
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: d.tableDependsOn.get(step.name)
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
- entries.push(await executeSyncView(d.views[step.index], d.viewEntries[step.index]));
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, name, syncMethod, adapterSupportsModify) {
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 && !syncMethod && !adapterSupportsModify) {
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 ?? [], ...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.`)];
2109
+ init.errors = [...init.errors ?? [], pkRebuildUnsupported(name, diff.primaryKeyChanged)];
1973
2110
  }
1974
2111
  }
1975
2112
  /**
@@ -1,4 +1,4 @@
1
- import { r as DepthLimitExceededError } from "./db-error-BHPXOKzc.mjs";
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-DXwEzmYJ.cjs");
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