@atscript/db 0.1.161 → 0.1.163

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 (65) hide show
  1. package/dist/agg.cjs +1 -1
  2. package/dist/agg.d.cts +1 -1
  3. package/dist/agg.d.mts +1 -1
  4. package/dist/agg.mjs +1 -1
  5. package/dist/{aggregate-fns-B_sgl4mB.mjs → aggregate-fns-DsS9UQVB.mjs} +1 -1
  6. package/dist/{aggregate-fns-DFtKAv7Q.cjs → aggregate-fns-DupYzFD7.cjs} +1 -1
  7. package/dist/{buckets-B4T_alUe.d.cts → buckets-B8UOod-v.d.cts} +488 -57
  8. package/dist/{buckets-8mG67Mtw.d.mts → buckets-CHyspjYP.d.mts} +488 -57
  9. package/dist/{column-diff-DpeUbhEI.cjs → column-diff-BIGX3sRH.cjs} +853 -232
  10. package/dist/{column-diff-DR1glykD.mjs → column-diff-BfNIlMwX.mjs} +836 -233
  11. package/dist/{validator-B3xlbrWf.d.cts → db-error-B_DRL1E9.d.cts} +18 -89
  12. package/dist/{validator-B3xlbrWf.d.mts → db-error-B_DRL1E9.d.mts} +18 -89
  13. package/dist/{db-error-JcfJqqpk.cjs → db-error-DmF1_xrz.cjs} +13 -0
  14. package/dist/{db-error-eSsQX7rt.mjs → db-error-hraC12B9.mjs} +8 -1
  15. package/dist/{db-validator-plugin-BWy60OvG.d.cts → db-validator-plugin-wRqMYNBI.d.cts} +6 -0
  16. package/dist/{db-validator-plugin-BWy60OvG.d.mts → db-validator-plugin-wRqMYNBI.d.mts} +6 -0
  17. package/dist/{fk-diff-5gX42hBt.d.mts → fk-diff-BNGawZi9.d.cts} +34 -10
  18. package/dist/{fk-diff-BWdQ7kRZ.d.cts → fk-diff-rj9N2QLw.d.mts} +34 -10
  19. package/dist/index.cjs +15 -11
  20. package/dist/index.d.cts +24 -21
  21. package/dist/index.d.mts +24 -21
  22. package/dist/index.mjs +10 -12
  23. package/dist/{nested-writer-BOj_c_lI.mjs → nested-writer-ClUX0DB0.mjs} +16 -22
  24. package/dist/{nested-writer-Bl1SZS7x.cjs → nested-writer-yCy6wGuN.cjs} +16 -22
  25. package/dist/{object-CtPYTIvy.d.cts → object-Czia_qD4.d.cts} +7 -1
  26. package/dist/{object-CtPYTIvy.d.mts → object-Czia_qD4.d.mts} +7 -1
  27. package/dist/{object-Djg28csK.cjs → object-GjsuPNqo.cjs} +19 -0
  28. package/dist/{object-TkiJQ-Dp.mjs → object-PQ1J8jri.mjs} +14 -1
  29. package/dist/ops.cjs +6 -21
  30. package/dist/ops.mjs +6 -21
  31. package/dist/plugin.cjs +263 -323
  32. package/dist/plugin.mjs +163 -223
  33. package/dist/rel.cjs +2 -2
  34. package/dist/rel.d.cts +3 -3
  35. package/dist/rel.d.mts +3 -3
  36. package/dist/rel.mjs +2 -2
  37. package/dist/{relation-helpers-17_2f5hX.d.mts → relation-helpers-DloMywNs.d.cts} +1 -1
  38. package/dist/{relation-helpers-BLBrLQYi.d.cts → relation-helpers-zwt19E7r.d.mts} +1 -1
  39. package/dist/{relation-loader-DeulLrAw.cjs → relation-loader-DujL_Mh9.cjs} +1 -1
  40. package/dist/{relation-loader-CNBMPfRr.mjs → relation-loader-Qv3LrQ42.mjs} +1 -1
  41. package/dist/{search-fields-CT_UZ9fg.cjs → search-fields-B49-RCWd.cjs} +65 -1
  42. package/dist/{search-fields-CE8cmj9X.mjs → search-fields-BPNsxrCk.mjs} +48 -2
  43. package/dist/search-fields-CHqqcC3W.d.cts +30 -0
  44. package/dist/search-fields-CHqqcC3W.d.mts +30 -0
  45. package/dist/server-stamped-B6XbUQ4p.cjs +33 -0
  46. package/dist/server-stamped-BoP56K9M.mjs +22 -0
  47. package/dist/shared.cjs +42 -26
  48. package/dist/shared.d.cts +98 -4
  49. package/dist/shared.d.mts +98 -4
  50. package/dist/shared.mjs +4 -2
  51. package/dist/sync.cjs +228 -65
  52. package/dist/sync.d.cts +41 -5
  53. package/dist/sync.d.mts +41 -5
  54. package/dist/sync.mjs +228 -66
  55. package/dist/{validator-B4YDLUkj.cjs → validator-DK9Ijpo7.cjs} +3 -2
  56. package/dist/validator-DX97RzC2.d.cts +108 -0
  57. package/dist/validator-DX97RzC2.d.mts +108 -0
  58. package/dist/{validator-B3fPJjS1.mjs → validator-DuDWsIdQ.mjs} +3 -2
  59. package/dist/validator.cjs +6 -3
  60. package/dist/validator.d.cts +5 -4
  61. package/dist/validator.d.mts +5 -4
  62. package/dist/validator.mjs +5 -4
  63. package/dist/{validation-utils-BGjqc2jU.mjs → view-validation-BcgDq7M9.mjs} +364 -20
  64. package/dist/{validation-utils-CtLNlBtB.cjs → view-validation-BiDLCYMw.cjs} +452 -18
  65. package/package.json +6 -6
package/dist/shared.d.mts CHANGED
@@ -1,11 +1,35 @@
1
- import { AnnotationSpec, AtscriptDoc, SemanticInterfaceNode, SemanticNode, SemanticPropNode, SemanticQueryExprNode, SemanticQueryFieldRefNode, SemanticStructureNode, TAnnotationTokens, TMessages, TQueryScope, Token } from "@atscript/core";
1
+ import { s as stableJson } from "./object-Czia_qD4.mjs";
2
+ import { n as ENCRYPTED_SEARCH_FEATURES, t as ENCRYPTED_INCOMPATIBLE } from "./search-fields-CHqqcC3W.mjs";
3
+ import { AnnotationSpec, AtscriptDoc, SemanticInterfaceNode, SemanticNode, SemanticPropNode, SemanticQueryExprNode, SemanticQueryFieldRefNode, SemanticStructureNode, TAnnotationSpecConfig, TAnnotationTokens, TMessages, TQueryScope, Token } from "@atscript/core";
2
4
 
3
5
  //#region src/shared/annotation-utils.d.ts
4
- /** Asserts the field carrying this annotation does not also carry any of `others`. */
6
+ /**
7
+ * Names of a field's effective annotations: its own plus the ones it inherits
8
+ * through its type ref (`price: Order.price`), with the `passedWhenReferred:
9
+ * false` ones dropped — the set codegen writes into the field's runtime
10
+ * metadata (`AtscriptDoc.evalAnnotationsForNode`). Compile checks that mirror
11
+ * a runtime metadata check read this, not the own-only `countAnnotations`.
12
+ * @since 0.1.162
13
+ */
14
+ declare function effectiveAnnotationNames(field: SemanticNode, doc?: AtscriptDoc): Set<string>;
15
+ /**
16
+ * Asserts the field carrying this annotation does not also carry any of
17
+ * `others` — its own or inherited through its type ref (since 0.1.162).
18
+ */
5
19
  declare function validateExclusiveWith(token: Token, selfName: string, others: Array<{
6
20
  key: string;
7
21
  displayName?: string;
8
- }>): TMessages;
22
+ }>, doc?: AtscriptDoc): TMessages;
23
+ /**
24
+ * Reports each of `pairs` (`[annotation, why]`) the field carrying `token`
25
+ * also carries — its own or inherited through its type ref (the field's
26
+ * effective annotations, what the runtime mirror reads off the metadata):
27
+ * `${tag} cannot coexist with @${annotation} — ${why}`.
28
+ * @since 0.1.162
29
+ */
30
+ declare function validateIncompatible(token: Token, tag: string, pairs: ReadonlyArray<readonly [annotation: string, why: string]>, doc?: AtscriptDoc): TMessages;
31
+ /** Whether a prop is a field of a view interface (`@db.view` / `@db.view.for`). */
32
+ declare function isViewField(field: SemanticNode): boolean;
9
33
  /** Primitive base type (`string`, `number`, `decimal`, …) of a resolved definition, if it is a primitive. */
10
34
  declare function primitiveBaseType(def: SemanticNode | undefined): string | undefined;
11
35
  /**
@@ -164,4 +188,74 @@ declare const NOT_DIMENSION_REASON = "not a dimension";
164
188
  */
165
189
  declare function isStrictAggregateStruct(struct: SemanticStructureNode): boolean;
166
190
  //#endregion
167
- export { NOT_DIMENSION_REASON, TFKFieldMatch, TRefArgumentOptions, earlierJoinTargets, findFKFieldsPointingTo, findViewCycle, forEachFieldRef, getAnnotationAlias, getDbTableOwner, getNavTargetTypeName, getParentStruct, getParentTypeName, hasAnyViewAnnotation, integerMemberIndexProblem, isAliasDecl, isDbSourceDecl, isNullableUnion, isStrictAggregateStruct, joinTargets, primitiveBaseType, refActionAnnotation, searchFieldVerdict, validateExclusiveWith, validateFieldBaseType, validateQueryScope, validateRefArgument, validateSiblingStringField, viewJoins, viewScopeTypes };
191
+ //#region src/shared/view-validation.d.ts
192
+ /** Read seals of a view field ({@link viewFieldSeals}). @since 0.1.162 */
193
+ interface TViewFieldSeals {
194
+ writeOnly: boolean;
195
+ encrypted: boolean;
196
+ }
197
+ /**
198
+ * The read seals a view field takes from the source field it reads, as the
199
+ * compiler sees them (compile-time mirror of the runtime `sourceFieldSeals`,
200
+ * which stamps them onto the view column — these annotations are
201
+ * `passedWhenReferred: false`, so they are not in the field's own metadata):
202
+ * - `writeOnly` — the source path or an ancestor object is `@db.writeOnly`;
203
+ * - `encrypted` — the source path or an ancestor object is `@db.encrypted`.
204
+ *
205
+ * (A `@db.json` source is no seal: a view reads a primitive JSON column's
206
+ * typed value, and an object one needs its own `@db.json`.)
207
+ *
208
+ * The source is the chain ref (`pin?: Account.pin`), or else the entry table's
209
+ * same-named field (or an aggregate's field) of a `@db.view.for` view. A source
210
+ * field that is itself a view field passes on its own seals. Computed fields
211
+ * and fields of anything but a view report no seal.
212
+ * @since 0.1.162
213
+ */
214
+ declare function viewFieldSeals(field: SemanticNode, doc: AtscriptDoc, depth?: number): TViewFieldSeals;
215
+ /**
216
+ * A search / index feature (`@db.column.searchable`, `@db.index.fulltext`,
217
+ * `@db.index.geo`, `@db.search.vector`, `@db.search.filter`) on a field that
218
+ * would read ciphertext: a field inside an `@db.encrypted` object, or a view
219
+ * field over an encrypted source — or `undefined`. The field's own
220
+ * `@db.encrypted` is reported by that spec.
221
+ * @since 0.1.162
222
+ */
223
+ declare function encryptedSearchProblem(token: Token, doc: AtscriptDoc, annotation: string): TMessages[number] | undefined;
224
+ /**
225
+ * An annotation's `validate` refusing it on a field that reads ciphertext
226
+ * ({@link encryptedSearchProblem}) — for the search features of sibling
227
+ * adapter plugins too.
228
+ * @since 0.1.162
229
+ */
230
+ declare function refuseCiphertext(annotation: string): TAnnotationSpecConfig["validate"];
231
+ /**
232
+ * Navigation relation annotations — a navigation field holds another table's
233
+ * rows, never part of this table's stored value.
234
+ * @since 0.1.162
235
+ */
236
+ declare const NAV_ANNOTATIONS: readonly ["db.rel.to", "db.rel.from", "db.rel.via"];
237
+ /** Whether a prop is a navigation field ({@link NAV_ANNOTATIONS}). */
238
+ declare function isNavProp(prop: SemanticNode): boolean;
239
+ /**
240
+ * Walks the fields stored in `root` (an interface, a structure or a type ref)
241
+ * depth-first — nested objects and type refs included, navigation fields and
242
+ * arrays skipped — and returns the first non-`undefined` result of `visit`.
243
+ * `path` is the field's dotted path below `prefix`; `viaRef` tells whether
244
+ * a type ref was followed to reach it.
245
+ * @since 0.1.162
246
+ */
247
+ declare function findStoredField<T>(root: SemanticNode | undefined, doc: AtscriptDoc, visit: (prop: SemanticNode, path: string, viaRef: boolean) => T | undefined, prefix?: string): T | undefined;
248
+ /**
249
+ * A search feature ({@link ENCRYPTED_SEARCH_FEATURES}) declared inside the
250
+ * TYPE of the `@db.encrypted` field carrying `token`, reached through a type
251
+ * ref (`@db.encrypted profile: Address`, where `Address` has
252
+ * `@db.index.fulltext city`) — the shared type's own annotations cannot see
253
+ * that this use encrypts them. Fields declared inline are reported by their
254
+ * own annotation (see {@link encryptedSearchProblem}).
255
+ * @since 0.1.162
256
+ */
257
+ declare function encryptedTypeSearchProblem(token: Token, doc: AtscriptDoc): TMessages[number] | undefined;
258
+ /** Name of the nearest `@db.encrypted` prop enclosing `field` (inline nested objects), if any. */
259
+ declare function encryptedAncestorName(field: SemanticNode): string | undefined;
260
+ //#endregion
261
+ export { ENCRYPTED_INCOMPATIBLE, ENCRYPTED_SEARCH_FEATURES, NAV_ANNOTATIONS, NOT_DIMENSION_REASON, TFKFieldMatch, TRefArgumentOptions, type TViewFieldSeals, earlierJoinTargets, effectiveAnnotationNames, encryptedAncestorName, encryptedSearchProblem, encryptedTypeSearchProblem, findFKFieldsPointingTo, findStoredField, findViewCycle, forEachFieldRef, getAnnotationAlias, getDbTableOwner, getNavTargetTypeName, getParentStruct, getParentTypeName, hasAnyViewAnnotation, integerMemberIndexProblem, isAliasDecl, isDbSourceDecl, isNavProp, isNullableUnion, isStrictAggregateStruct, isViewField, joinTargets, primitiveBaseType, refActionAnnotation, refuseCiphertext, searchFieldVerdict, stableJson, validateExclusiveWith, validateFieldBaseType, validateIncompatible, validateQueryScope, validateRefArgument, validateSiblingStringField, viewFieldSeals, viewJoins, viewScopeTypes };
package/dist/shared.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { n as ENCRYPTED_SEARCH_FEATURES, t as ENCRYPTED_INCOMPATIBLE } from "./search-fields-BPNsxrCk.mjs";
2
+ import { s as stableJson } from "./object-PQ1J8jri.mjs";
1
3
  import { n as isStrictAggregateStruct, t as NOT_DIMENSION_REASON } from "./aggregate-rules-jdPrxqWa.mjs";
2
- import { C as validateExclusiveWith, S as searchFieldVerdict, T as validateSiblingStringField, _ as getParentTypeName, a as hasAnyViewAnnotation, b as primitiveBaseType, c as joinTargets, d as viewJoins, f as viewScopeTypes, g as getParentStruct, h as getNavTargetTypeName, i as forEachFieldRef, l as validateQueryScope, m as getDbTableOwner, n as findFKFieldsPointingTo, o as isAliasDecl, p as getAnnotationAlias, r as findViewCycle, s as isDbSourceDecl, t as earlierJoinTargets, u as validateRefArgument, v as integerMemberIndexProblem, w as validateFieldBaseType, x as refActionAnnotation, y as isNullableUnion } from "./validation-utils-BGjqc2jU.mjs";
3
- export { NOT_DIMENSION_REASON, earlierJoinTargets, findFKFieldsPointingTo, findViewCycle, forEachFieldRef, getAnnotationAlias, getDbTableOwner, getNavTargetTypeName, getParentStruct, getParentTypeName, hasAnyViewAnnotation, integerMemberIndexProblem, isAliasDecl, isDbSourceDecl, isNullableUnion, isStrictAggregateStruct, joinTargets, primitiveBaseType, refActionAnnotation, searchFieldVerdict, validateExclusiveWith, validateFieldBaseType, validateQueryScope, validateRefArgument, validateSiblingStringField, viewJoins, viewScopeTypes };
4
+ import { A as getParentTypeName, B as validateSiblingStringField, C as viewJoins, D as getDbTableOwner, E as getAnnotationAlias, F as refActionAnnotation, I as searchFieldVerdict, L as validateExclusiveWith, M as isNullableUnion, N as isViewField, O as getNavTargetTypeName, P as primitiveBaseType, R as validateFieldBaseType, S as validateRefArgument, T as effectiveAnnotationNames, _ as hasAnyViewAnnotation, a as findStoredField, b as joinTargets, c as refuseCiphertext, d as viewFieldSeals, g as forEachFieldRef, h as findViewCycle, i as encryptedTypeSearchProblem, j as integerMemberIndexProblem, k as getParentStruct, m as findFKFieldsPointingTo, n as encryptedAncestorName, o as isNavProp, p as earlierJoinTargets, r as encryptedSearchProblem, t as NAV_ANNOTATIONS, v as isAliasDecl, w as viewScopeTypes, x as validateQueryScope, y as isDbSourceDecl, z as validateIncompatible } from "./view-validation-BcgDq7M9.mjs";
5
+ export { ENCRYPTED_INCOMPATIBLE, ENCRYPTED_SEARCH_FEATURES, NAV_ANNOTATIONS, NOT_DIMENSION_REASON, earlierJoinTargets, effectiveAnnotationNames, encryptedAncestorName, encryptedSearchProblem, encryptedTypeSearchProblem, findFKFieldsPointingTo, findStoredField, findViewCycle, forEachFieldRef, getAnnotationAlias, getDbTableOwner, getNavTargetTypeName, getParentStruct, getParentTypeName, hasAnyViewAnnotation, integerMemberIndexProblem, isAliasDecl, isDbSourceDecl, isNavProp, isNullableUnion, isStrictAggregateStruct, isViewField, joinTargets, primitiveBaseType, refActionAnnotation, refuseCiphertext, searchFieldVerdict, stableJson, validateExclusiveWith, validateFieldBaseType, validateIncompatible, validateQueryScope, validateRefArgument, validateSiblingStringField, viewFieldSeals, viewJoins, viewScopeTypes };
package/dist/sync.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_db_error = require("./db-error-JcfJqqpk.cjs");
3
- const require_column_diff = require("./column-diff-DpeUbhEI.cjs");
4
- const require_validator = require("./validator-B4YDLUkj.cjs");
2
+ const require_db_error = require("./db-error-DmF1_xrz.cjs");
3
+ const require_column_diff = require("./column-diff-BIGX3sRH.cjs");
4
+ const require_validator = require("./validator-DK9Ijpo7.cjs");
5
5
  //#region src/schema/json-column-copy.ts
6
6
  const NONE = {
7
7
  copies: [],
@@ -506,6 +506,24 @@ async function readStoredSnapshot(space, tableName, _asView) {
506
506
  }
507
507
  //#endregion
508
508
  //#region src/schema/sync-entry.ts
509
+ /** How each index change prints: its mark, color and past-tense word. */
510
+ const INDEX_CHANGE_STYLE = {
511
+ add: {
512
+ mark: "+",
513
+ color: "green",
514
+ done: "added"
515
+ },
516
+ drop: {
517
+ mark: "-",
518
+ color: "red",
519
+ done: "dropped"
520
+ },
521
+ rebuild: {
522
+ mark: "~",
523
+ color: "yellow",
524
+ done: "rebuilt"
525
+ }
526
+ };
509
527
  const noColor = {
510
528
  green: (s) => s,
511
529
  red: (s) => s,
@@ -560,6 +578,10 @@ var SyncEntry = class SyncEntry {
560
578
  refused;
561
579
  /** @since 0.1.141 — see {@link TSyncEntryInit.cascadeFrom}. */
562
580
  cascadeFrom;
581
+ /** @since 0.1.162 — see {@link TSyncEntryInit.definitionChanged}. */
582
+ definitionChanged;
583
+ /** @since 0.1.162 — see {@link TSyncEntryInit.indexChanges}. */
584
+ indexChanges;
563
585
  constructor(init) {
564
586
  this.name = init.name;
565
587
  this.viewType = init.viewType;
@@ -590,6 +612,8 @@ var SyncEntry = class SyncEntry {
590
612
  this.dropGroup = init.dropGroup;
591
613
  this.refused = init.refused ?? false;
592
614
  this.cascadeFrom = init.cascadeFrom ?? [];
615
+ this.definitionChanged = init.definitionChanged ?? false;
616
+ this.indexChanges = init.indexChanges ?? [];
593
617
  }
594
618
  /**
595
619
  * The init object this entry was built from — lets callers derive a
@@ -660,11 +684,13 @@ var SyncEntry = class SyncEntry {
660
684
  return mode === "plan" ? [` ${c.red(`! ${cols} — rebuild (${pk.populated ? "rows satisfy the new key" : "table is empty"})`)}`] : [` ${c.yellow(`~ ${cols} — rebuilt`)}`];
661
685
  }
662
686
  /**
663
- * `! col: t1 → t2 — drop` (plan), or `! type col (t1 → t2) — skipped (safe
664
- * mode)` in plan and result when the `'drop'` recreate was skipped.
687
+ * `! col: t1 → t2 — drop` (plan), `~ col: t1 → t2 — type changed` (result),
688
+ * or `! type col (t1 → t2) — skipped (safe mode)` in plan and result when
689
+ * the `'drop'` recreate was skipped.
665
690
  */
666
- printTypeChanges(c) {
691
+ printTypeChanges(c, mode) {
667
692
  if (this.skipped.includes("recreate")) return this.typeChanges.map((tc) => ` ${c.yellow(`! type ${tc.column} (${tc.fromType} → ${tc.toType}) — skipped (safe mode)`)}`);
693
+ if (mode === "result") return this.typeChanges.map((tc) => ` ${c.yellow(`~ ${tc.column}: ${tc.fromType} → ${tc.toType} — type changed`)}`);
668
694
  return this.typeChanges.map((tc) => {
669
695
  const action = this.syncMethod ? ` — ${this.syncMethod}` : " — requires migration";
670
696
  return ` ${c.red(`! ${tc.column}: ${tc.fromType} → ${tc.toType}${action}`)}`;
@@ -708,6 +734,26 @@ var SyncEntry = class SyncEntry {
708
734
  return ` ${tag} ${c.cyan(`option ${oc.key}`)}: ${oc.oldValue} → ${oc.newValue}${action}`;
709
735
  });
710
736
  }
737
+ /** `+ FK(a) → t — add` / `- … — remove` / `~ … — details` (plan); `added` / `removed` (result). */
738
+ printForeignKeys(c, mode) {
739
+ const [add, remove] = mode === "plan" ? ["add", "remove"] : ["added", "removed"];
740
+ return [
741
+ ...this.fkAdded.map((fk) => ` ${c.green(`+ FK(${fk.fields.join(",")}) → ${fk.targetTable} — ${add}`)}`),
742
+ ...this.fkRemoved.map((fk) => ` ${c.red(`- FK(${fk.fields.join(",")})${fk.targetTable ? ` → ${fk.targetTable}` : ""} — ${remove}`)}`),
743
+ ...this.fkChanged.map((fk) => ` ${c.yellow(`~ FK(${fk.fields.join(",")}) → ${fk.targetTable} — ${fk.details}`)}`)
744
+ ];
745
+ }
746
+ /** `+ index name — add` / `- … — drop` / `~ … — rebuild` (plan); `added` / `dropped` / `rebuilt` (result). */
747
+ printIndexChanges(c, mode) {
748
+ return this.indexChanges.map((ic) => {
749
+ const { mark, color, done } = INDEX_CHANGE_STYLE[ic.action];
750
+ return ` ${c[color](`${mark} index ${ic.name} — ${mode === "plan" ? ic.action : done}`)}`;
751
+ });
752
+ }
753
+ /** `· definition changed` — why a managed view is recreated. */
754
+ printDefinitionChanged(c) {
755
+ return this.definitionChanged ? [` ${c.dim("· definition changed")}`] : [];
756
+ }
711
757
  /** `· after: a, b` (plan only — the executor already runs in this order). */
712
758
  printDependsOn(c) {
713
759
  if (this.dependsOn.length === 0) return [];
@@ -746,15 +792,15 @@ var SyncEntry = class SyncEntry {
746
792
  ...this.columnsToAdd.map((col) => ` ${c.green(`+ ${col.physicalName} (${col.designType})${col.derived ? " derived" : ""} — add`)}`),
747
793
  ...this.columnsToRename.map((r) => ` ${c.yellow(`~ ${r.from} → ${r.to} — rename`)}`),
748
794
  ...this.printJsonMigration(c, "plan"),
749
- ...this.printTypeChanges(c),
795
+ ...this.printTypeChanges(c, "plan"),
750
796
  ...this.printNullableDefaults(c),
751
797
  ...this.printDerivedChanges(c, "plan"),
752
798
  ...this.printPkChange(c, "plan"),
753
799
  ...this.columnsToDrop.map((col) => ` ${c.red(`- ${col} — drop`)}`),
754
800
  ...this.printOptionChanges(c, "plan"),
755
- ...this.fkAdded.map((fk) => ` ${c.green(`+ FK(${fk.fields.join(",")}) → ${fk.targetTable} — add`)}`),
756
- ...this.fkRemoved.map((fk) => ` ${c.red(`- FK(${fk.fields.join(",")}) → ${fk.targetTable} — remove`)}`),
757
- ...this.fkChanged.map((fk) => ` ${c.yellow(`~ FK(${fk.fields.join(",")}) → ${fk.targetTable} — ${fk.details}`)}`),
801
+ ...this.printForeignKeys(c, "plan"),
802
+ ...this.printIndexChanges(c, "plan"),
803
+ ...this.printDefinitionChanged(c),
758
804
  ...this.printCascade(c),
759
805
  ...this.printDependsOn(c),
760
806
  ""
@@ -774,7 +820,7 @@ var SyncEntry = class SyncEntry {
774
820
  ...this.columnsAdded.map((col) => ` ${c.green(`+ ${col} — added`)}`),
775
821
  ""
776
822
  ];
777
- if (this.columnsAdded.length > 0 || this.columnsRenamed.length > 0 || this.columnsDropped.length > 0 || this.optionChanges.length > 0 || this.pkChange !== void 0 || this.derivedChanges.length > 0 || this.jsonCopies.length > 0 || this.jsonified.length > 0 || this.skipped.length > 0 || this.recreated || this.renamedFrom) {
823
+ if (this.columnsAdded.length > 0 || this.columnsRenamed.length > 0 || this.columnsDropped.length > 0 || this.optionChanges.length > 0 || this.pkChange !== void 0 || this.derivedChanges.length > 0 || this.jsonCopies.length > 0 || this.jsonified.length > 0 || this.typeChanges.length > 0 || this.nullableChanges.length > 0 || this.defaultChanges.length > 0 || this.fkAdded.length > 0 || this.fkRemoved.length > 0 || this.fkChanged.length > 0 || this.indexChanges.length > 0 || this.skipped.length > 0 || this.recreated || this.renamedFrom || this.status === "alter") {
778
824
  const rlabel = this.recreated ? "recreated" : "altered";
779
825
  const renameInfo = this.renamedFrom ? ` ${c.yellow(`(renamed from ${this.renamedFrom})`)}` : "";
780
826
  return [
@@ -783,11 +829,14 @@ var SyncEntry = class SyncEntry {
783
829
  ...this.columnsRenamed.map((col) => ` ${c.yellow(`~ ${col} — renamed`)}`),
784
830
  ...this.printJsonMigration(c, "result"),
785
831
  ...this.printPkChange(c, "result"),
786
- ...this.printTypeChanges(c),
832
+ ...this.printTypeChanges(c, "result"),
787
833
  ...this.printNullableDefaults(c),
788
834
  ...this.printDerivedChanges(c, "result"),
789
835
  ...this.columnsDropped.map((col) => ` ${c.red(`- ${col} — dropped`)}`),
790
836
  ...this.printOptionChanges(c, "result"),
837
+ ...this.printForeignKeys(c, "result"),
838
+ ...this.printIndexChanges(c, "result"),
839
+ ...this.printDefinitionChanged(c),
791
840
  ...this.printCascade(c),
792
841
  ""
793
842
  ];
@@ -921,10 +970,7 @@ async function executeSyncTable(facts, safe, deps, exec) {
921
970
  await adapter.ensureTable(ensureOpts);
922
971
  init.status = "create";
923
972
  } else if (existing.length > 0) {
924
- let diff = facts.diff ?? require_column_diff.computeColumnDiff(readable.columnDescriptors, existing, adapter.typeMapper?.bind(adapter), {
925
- snapshot: storedSnapshot,
926
- nativeDefaultFns: adapter.nativeDefaultFns()
927
- });
973
+ let diff = facts.diff ?? require_column_diff.computeColumnDiff(readable.columnDescriptors, existing, adapter.typeMapper?.bind(adapter), columnDiffOptions(adapter, storedSnapshot));
928
974
  const migration = facts.jsonMigration ?? planJsonColumnMigration(readable, diff, storedSnapshot);
929
975
  if (migration.errors.length > 0) throw new Error(migration.errors.join(" "));
930
976
  if (migration.copies.length > 0 || migration.jsonify.length > 0) {
@@ -961,10 +1007,7 @@ async function executeSyncTable(facts, safe, deps, exec) {
961
1007
  const existed = adapter.tableExists ? await adapter.tableExists() : false;
962
1008
  await adapter.ensureTable(ensureOpts);
963
1009
  if (!existed) init.status = "create";
964
- } else await applyColumnDiff(adapter, readable, facts.diff ?? require_column_diff.computeColumnDiff(readable.columnDescriptors, require_column_diff.snapshotToExistingColumns(storedSnapshot, readable), deps.resolveTypeMapper(adapter), {
965
- snapshot: storedSnapshot,
966
- nativeDefaultFns: adapter.nativeDefaultFns()
967
- }), init, safe, deps.logger, ensureOpts);
1010
+ } else await applyColumnDiff(adapter, readable, facts.diff ?? require_column_diff.computeColumnDiff(readable.columnDescriptors, require_column_diff.snapshotToExistingColumns(storedSnapshot, readable), deps.resolveTypeMapper(adapter), columnDiffOptions(adapter, storedSnapshot)), init, safe, deps.logger, ensureOpts);
968
1011
  else {
969
1012
  const existed = adapter.tableExists ? await adapter.tableExists() : true;
970
1013
  if (!init.recreated) {
@@ -972,6 +1015,10 @@ async function executeSyncTable(facts, safe, deps, exec) {
972
1015
  if (!existed) init.status = "create";
973
1016
  }
974
1017
  }
1018
+ if (hasFkChanges && fkDiff && init.status !== "create" && init.status !== "error") {
1019
+ Object.assign(init, describeForeignKeyChanges(fkDiff));
1020
+ init.status = "alter";
1021
+ }
975
1022
  phase = "Table option sync";
976
1023
  const optionDiff = facts.optionDiff;
977
1024
  if (init.status !== "create" && !init.recreated && init.status !== "error" && optionDiff && optionDiff.changed.length > 0) {
@@ -1006,8 +1053,15 @@ async function executeSyncTable(facts, safe, deps, exec) {
1006
1053
  if (init.pkChange && facts.populated === true) init.pkChange.populated = true;
1007
1054
  if (init.status === "error") return new SyncEntry(init);
1008
1055
  phase = "Index/FK sync";
1009
- await adapter.syncIndexes(keptIndexes(init));
1010
- if (adapter.syncForeignKeys && !exec.deferForeignKeysTo) await adapter.syncForeignKeys();
1056
+ const indexChanges = await adapter.syncIndexes(keptIndexes(init));
1057
+ if (Array.isArray(indexChanges) && indexChanges.length > 0 && init.status !== "create" && !init.recreated) {
1058
+ init.indexChanges = indexChanges;
1059
+ init.status = "alter";
1060
+ }
1061
+ if (adapter.syncForeignKeys && !exec.deferForeignKeysTo) {
1062
+ const fkChanges = await adapter.syncForeignKeys();
1063
+ if (init.status !== "create" && !init.recreated && mergeLiveForeignKeyChanges(init, fkChanges)) init.status = "alter";
1064
+ }
1011
1065
  if (adapter.afterSyncTable) await adapter.afterSyncTable();
1012
1066
  } catch (error) {
1013
1067
  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.` : "";
@@ -1036,8 +1090,13 @@ async function executeDeferredForeignKeys(readable, entry, deps) {
1036
1090
  const adapter = readable.dbAdapter;
1037
1091
  if (!adapter.syncForeignKeys || entry.status === "error") return entry;
1038
1092
  try {
1039
- await adapter.syncForeignKeys();
1040
- return entry;
1093
+ const fkChanges = await adapter.syncForeignKeys();
1094
+ if (entry.status === "create" || entry.recreated) return entry;
1095
+ const init = entry.toInit();
1096
+ return mergeLiveForeignKeyChanges(init, fkChanges) ? new SyncEntry({
1097
+ ...init,
1098
+ status: "alter"
1099
+ }) : entry;
1041
1100
  } catch (error) {
1042
1101
  const msg = `FK sync failed on ${readable.tableName}: ${error.message}`;
1043
1102
  deps.logger.error?.(`[schema-sync] ${msg}`);
@@ -1083,6 +1142,14 @@ function describeDerivedChanges(diff) {
1083
1142
  derived: dc.field.derived !== void 0
1084
1143
  }));
1085
1144
  }
1145
+ /** The {@link computeColumnDiff} options for a table of `adapter`. */
1146
+ function columnDiffOptions(adapter, snapshot) {
1147
+ return {
1148
+ snapshot,
1149
+ nativeDefaultFns: adapter.nativeDefaultFns(),
1150
+ reportedDefault: adapter.reportedDefault?.bind(adapter)
1151
+ };
1152
+ }
1086
1153
  /** The plan-shaped summary of a diff's type changes (`entry.typeChanges`). */
1087
1154
  function describeTypeChanges(diff) {
1088
1155
  return diff.typeChanged.map((tc) => ({
@@ -1121,6 +1188,28 @@ function markSkipped(init, kind) {
1121
1188
  init.skipped = [...init.skipped ?? [], kind];
1122
1189
  }
1123
1190
  /**
1191
+ * Whether the run (without safe mode) recreates this existing table — an FK
1192
+ * change on an adapter without `syncForeignKeys`, a `'drop'` / `'recreate'`
1193
+ * type change, nullable / default DDL through `recreateTable`, or a key
1194
+ * rebuild without `rebuildPrimaryKey`. The plan predicts no index work for
1195
+ * such a table: the recreate rebuilds every index.
1196
+ */
1197
+ function runRecreatesTable(facts, safe = false) {
1198
+ const { readable, fkDiff } = facts;
1199
+ const adapter = readable.dbAdapter;
1200
+ if (!adapter.recreateTable) return false;
1201
+ if (fkDiff && require_column_diff.hasForeignKeyChanges(fkDiff) && !adapter.syncForeignKeys) return true;
1202
+ const diff = facts.diff;
1203
+ if (!diff) return false;
1204
+ if (diff.typeChanged.length > 0) {
1205
+ const strategy = typeChangeStrategy(readable, diff);
1206
+ if (strategy === "recreate" || strategy === "drop" && !safe) return true;
1207
+ }
1208
+ if (safe) return false;
1209
+ if ((diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) && !adapter.supportsColumnModify && !!adapter.getExistingColumns) return true;
1210
+ return !!diff.primaryKeyChanged && !adapter.rebuildPrimaryKey;
1211
+ }
1212
+ /**
1124
1213
  * Whether nullable/default changes need DDL on this adapter: in-place modify,
1125
1214
  * or a recreate on a live-introspected one (SQLite). Snapshot-based adapters
1126
1215
  * (MongoDB) have no column constraint to change — they only update the
@@ -1130,6 +1219,20 @@ function needsDdlForNullableDefaults(adapter) {
1130
1219
  return !!adapter.supportsColumnModify && !!adapter.syncColumns || !!adapter.recreateTable && !!adapter.getExistingColumns;
1131
1220
  }
1132
1221
  /**
1222
+ * The `syncIndexes` options the run will pass for this table — what the plan
1223
+ * passes `planIndexes`, so both report the same index work.
1224
+ */
1225
+ function plannedIndexOptions(facts, safe) {
1226
+ if (!safe) return;
1227
+ const diff = afterJsonCopy(facts);
1228
+ return keptIndexes({
1229
+ name: facts.name,
1230
+ status: "alter",
1231
+ skipped: safeModeSkips(facts),
1232
+ typeChanges: diff ? describeTypeChanges(diff) : []
1233
+ });
1234
+ }
1235
+ /**
1133
1236
  * The indexes `syncIndexes` keeps as they are because safe mode skipped
1134
1237
  * the change they depend on — a skipped key rebuild, the columns of a
1135
1238
  * skipped type-change recreate (see `TSyncIndexesOptions`).
@@ -1178,6 +1281,68 @@ function describeJsonMigration(migration) {
1178
1281
  jsonified: migration.jsonify.length > 0 ? migration.jsonify : void 0
1179
1282
  };
1180
1283
  }
1284
+ /** Builds a human-readable description of what changed in an FK constraint. */
1285
+ function buildFkChangeDetails(desired, existing) {
1286
+ const parts = [];
1287
+ if (existing.targetTable !== desired.targetTable) parts.push(`retarget ${existing.targetTable} → ${desired.targetTable}`);
1288
+ if ([...existing.targetFields].toSorted().join(",") !== [...desired.targetFields].toSorted().join(",")) parts.push(`fields ${existing.targetFields.join(",")} → ${desired.targetFields.join(",")}`);
1289
+ if ((existing.onDelete ?? void 0) !== (desired.onDelete ?? void 0)) parts.push(`onDelete ${existing.onDelete ?? "noAction"} → ${desired.onDelete ?? "noAction"}`);
1290
+ if ((existing.onUpdate ?? void 0) !== (desired.onUpdate ?? void 0)) parts.push(`onUpdate ${existing.onUpdate ?? "noAction"} → ${desired.onUpdate ?? "noAction"}`);
1291
+ return parts.join(", ");
1292
+ }
1293
+ /**
1294
+ * The plan-shaped summaries of a foreign-key diff (`entry.fkAdded` /
1295
+ * `fkRemoved` / `fkChanged`) — on plan entries and on the run entries that
1296
+ * applied them.
1297
+ */
1298
+ function describeForeignKeyChanges(fkDiff) {
1299
+ return {
1300
+ fkAdded: fkDiff.added.map((fk) => ({
1301
+ fields: require_column_diff.fkColumns(fk).fields,
1302
+ targetTable: fk.targetTable
1303
+ })),
1304
+ fkRemoved: fkDiff.removed.map((fk) => ({
1305
+ fields: fk.fields,
1306
+ targetTable: fk.targetTable
1307
+ })),
1308
+ fkChanged: fkDiff.changed.map((fk) => ({
1309
+ fields: require_column_diff.fkColumns(fk.desired).fields,
1310
+ targetTable: fk.desired.targetTable,
1311
+ details: buildFkChangeDetails({
1312
+ ...fk.desired,
1313
+ targetFields: require_column_diff.fkColumns(fk.desired).targetFields
1314
+ }, fk.existing)
1315
+ }))
1316
+ };
1317
+ }
1318
+ /**
1319
+ * Folds the live FK changes `syncForeignKeys` / `planForeignKeys` report
1320
+ * into the entry: those the snapshot-based FK diff did not already name
1321
+ * (a constraint dropped or added by hand) join `fkAdded` / `fkRemoved`.
1322
+ * `true` when any was added.
1323
+ */
1324
+ function mergeLiveForeignKeyChanges(init, changes) {
1325
+ if (!changes || changes.length === 0) return false;
1326
+ const known = new Set([
1327
+ ...init.fkAdded ?? [],
1328
+ ...init.fkRemoved ?? [],
1329
+ ...init.fkChanged ?? []
1330
+ ].map((fk) => require_column_diff.fkKey(fk.fields)));
1331
+ let merged = false;
1332
+ for (const change of changes) {
1333
+ const key = require_column_diff.fkKey(change.fields);
1334
+ if (known.has(key)) continue;
1335
+ known.add(key);
1336
+ const fk = {
1337
+ fields: change.fields,
1338
+ targetTable: change.targetTable ?? ""
1339
+ };
1340
+ if (change.action === "add") init.fkAdded = [...init.fkAdded ?? [], fk];
1341
+ else init.fkRemoved = [...init.fkRemoved ?? [], fk];
1342
+ merged = true;
1343
+ }
1344
+ return merged;
1345
+ }
1181
1346
  /** The plan-shaped nullable / default change summaries of a diff. */
1182
1347
  function describeNullableDefaults(diff) {
1183
1348
  return {
@@ -1242,11 +1407,12 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
1242
1407
  let needsSyncColumns = false;
1243
1408
  if (diff.typeChanged.length > 0 && init.status !== "error") {
1244
1409
  const strategy = typeChangeStrategy(readable, diff);
1410
+ const typeChanges = describeTypeChanges(diff);
1411
+ if (strategy !== "none") init.typeChanges = typeChanges;
1245
1412
  if (strategy === "drop") if (safe) {
1246
- init.typeChanges = describeTypeChanges(diff);
1247
1413
  markSkipped(init, "recreate");
1248
1414
  init.status = "alter";
1249
- const cols = init.typeChanges.map((tc) => `${tc.column}: ${tc.fromType} → ${tc.toType}`).join(", ");
1415
+ const cols = typeChanges.map((tc) => `${tc.column}: ${tc.fromType} → ${tc.toType}`).join(", ");
1250
1416
  logger.warn?.(`[schema-sync] Type change on "${name}" (${cols}) — drop-and-recreate skipped (safe mode)`);
1251
1417
  } else await dropAndRecreate(adapter, init, name, ensureOpts);
1252
1418
  else if (strategy === "recreate") await recreateKeepingRenames(adapter, diff, init);
@@ -1261,12 +1427,15 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
1261
1427
  }
1262
1428
  }
1263
1429
  if (diff.nullableChanged.length > 0 || diff.defaultChanged.length > 0) {
1264
- if (!safe && !init.recreated && init.status !== "error") if (adapter.supportsColumnModify && adapter.syncColumns) {
1265
- needsSyncColumns = true;
1266
- init.status = "alter";
1267
- } else if (needsDdlForNullableDefaults(adapter)) await recreateKeepingRenames(adapter, diff, init);
1268
- else init.status = "alter";
1269
- else if (init.status !== "error") {
1430
+ if (!safe && !init.recreated && init.status !== "error") {
1431
+ if (adapter.supportsColumnModify && adapter.syncColumns) {
1432
+ needsSyncColumns = true;
1433
+ init.status = "alter";
1434
+ } else if (needsDdlForNullableDefaults(adapter)) await recreateKeepingRenames(adapter, diff, init);
1435
+ else init.status = "alter";
1436
+ Object.assign(init, describeNullableDefaults(diff));
1437
+ } else if (init.status !== "error") {
1438
+ if (!safe || !needsDdlForNullableDefaults(adapter) || init.recreated) Object.assign(init, describeNullableDefaults(diff));
1270
1439
  if (safe && !init.recreated && needsDdlForNullableDefaults(adapter)) {
1271
1440
  Object.assign(init, describeNullableDefaults(diff));
1272
1441
  markSkipped(init, "nullable-defaults");
@@ -1361,15 +1530,6 @@ async function applyColumnDiff(adapter, readable, diff, init, safe, logger, ensu
1361
1530
  function retain(group, ctx) {
1362
1531
  ctx.retained.push(...group.map((name) => ctx.d.removedByName.get(name)));
1363
1532
  }
1364
- /** Builds a human-readable description of what changed in an FK constraint. */
1365
- function buildFkChangeDetails(desired, existing) {
1366
- const parts = [];
1367
- if (existing.targetTable !== desired.targetTable) parts.push(`retarget ${existing.targetTable} → ${desired.targetTable}`);
1368
- if ([...existing.targetFields].toSorted().join(",") !== [...desired.targetFields].toSorted().join(",")) parts.push(`fields ${existing.targetFields.join(",")} → ${desired.targetFields.join(",")}`);
1369
- if ((existing.onDelete ?? void 0) !== (desired.onDelete ?? void 0)) parts.push(`onDelete ${existing.onDelete ?? "noAction"} → ${desired.onDelete ?? "noAction"}`);
1370
- if ((existing.onUpdate ?? void 0) !== (desired.onUpdate ?? void 0)) parts.push(`onUpdate ${existing.onUpdate ?? "noAction"} → ${desired.onUpdate ?? "noAction"}`);
1371
- return parts.join(", ");
1372
- }
1373
1533
  function sortedUnique(names) {
1374
1534
  return [...new Set(names)].toSorted();
1375
1535
  }
@@ -1641,6 +1801,7 @@ var SchemaSync = class {
1641
1801
  this.assertLockHeld(heartbeat.getAbortReason);
1642
1802
  const refusals = this.preflight(d, safe);
1643
1803
  if (refusals.size > 0) {
1804
+ await this.planLiveWork(d, safe);
1644
1805
  const result = {
1645
1806
  status: "refused",
1646
1807
  schemaHash: hash,
@@ -1705,6 +1866,7 @@ var SchemaSync = class {
1705
1866
  renamedFrom: plan.isRenamed ? view.renamedFrom : void 0,
1706
1867
  recreated: viewRecreated(plan) || void 0,
1707
1868
  cascadeFrom: plan.upstreamRecreated,
1869
+ definitionChanged: plan.definitionChanged || void 0,
1708
1870
  dependsOn: viewSources.get(view.tableName).filter((n) => tableNames.has(n) || viewNames.has(n) || externalViewNames.has(n))
1709
1871
  }));
1710
1872
  }
@@ -1842,10 +2004,7 @@ var SchemaSync = class {
1842
2004
  init.columnsToAdd = [...readable.columnDescriptors];
1843
2005
  } else if (facts.existing && facts.existing.length > 0) {
1844
2006
  const typeMapper = adapter.typeMapper?.bind(adapter);
1845
- facts.diff = require_column_diff.computeColumnDiff(readable.columnDescriptors, facts.existing, typeMapper, {
1846
- snapshot: storedSnapshot,
1847
- nativeDefaultFns: adapter.nativeDefaultFns()
1848
- });
2007
+ facts.diff = require_column_diff.computeColumnDiff(readable.columnDescriptors, facts.existing, typeMapper, columnDiffOptions(adapter, storedSnapshot));
1849
2008
  this.populatePlanFromDiff(facts.diff, init, readable, safe);
1850
2009
  const migration = planJsonColumnMigration(readable, facts.diff, storedSnapshot);
1851
2010
  facts.jsonMigration = migration;
@@ -1864,10 +2023,7 @@ var SchemaSync = class {
1864
2023
  }
1865
2024
  } else {
1866
2025
  const existing = require_column_diff.snapshotToExistingColumns(storedSnapshot, readable);
1867
- facts.diff = require_column_diff.computeColumnDiff(readable.columnDescriptors, existing, this.resolveTypeMapper(adapter), {
1868
- snapshot: storedSnapshot,
1869
- nativeDefaultFns: adapter.nativeDefaultFns()
1870
- });
2026
+ facts.diff = require_column_diff.computeColumnDiff(readable.columnDescriptors, existing, this.resolveTypeMapper(adapter), columnDiffOptions(adapter, storedSnapshot));
1871
2027
  this.populatePlanFromDiff(facts.diff, init, readable, safe);
1872
2028
  }
1873
2029
  else if (adapter.tableExists) {
@@ -1885,22 +2041,7 @@ var SchemaSync = class {
1885
2041
  if (init.status !== "create" && init.status !== "error" && facts.fkDiff && require_column_diff.hasForeignKeyChanges(facts.fkDiff)) {
1886
2042
  const fkDiff = facts.fkDiff;
1887
2043
  init.status = "alter";
1888
- init.fkAdded = fkDiff.added.map((fk) => ({
1889
- fields: require_column_diff.fkColumns(fk).fields,
1890
- targetTable: fk.targetTable
1891
- }));
1892
- init.fkRemoved = fkDiff.removed.map((fk) => ({
1893
- fields: fk.fields,
1894
- targetTable: fk.targetTable
1895
- }));
1896
- init.fkChanged = fkDiff.changed.map((fk) => ({
1897
- fields: require_column_diff.fkColumns(fk.desired).fields,
1898
- targetTable: fk.desired.targetTable,
1899
- details: buildFkChangeDetails({
1900
- ...fk.desired,
1901
- targetFields: require_column_diff.fkColumns(fk.desired).targetFields
1902
- }, fk.existing)
1903
- }));
2044
+ Object.assign(init, describeForeignKeyChanges(fkDiff));
1904
2045
  }
1905
2046
  if (facts.diff?.primaryKeyChanged) {
1906
2047
  const tableName = pendingRename ? dbName : void 0;
@@ -1918,6 +2059,26 @@ var SchemaSync = class {
1918
2059
  return facts;
1919
2060
  }
1920
2061
  /**
2062
+ * Folds the index / foreign-key work on each live table as it is now
2063
+ * (`planIndexes` / `planForeignKeys`) into its plan entry. Read only when
2064
+ * plan entries are reported (`plan()`, a refused run) — a run reports what
2065
+ * `syncIndexes` / `syncForeignKeys` did.
2066
+ */
2067
+ async planLiveWork(d, safe) {
2068
+ await Promise.all(d.tables.map((t) => this.planTableLiveWork(t, safe)));
2069
+ }
2070
+ async planTableLiveWork(facts, safe) {
2071
+ const adapter = facts.readable.dbAdapter;
2072
+ const init = facts.planEntry;
2073
+ if (!(init.status !== "create" && init.status !== "error" && (safe || !init.recreated) && !facts.pendingRename && !runRecreatesTable(facts, safe))) return;
2074
+ const indexChanges = await adapter.planIndexes(plannedIndexOptions(facts, safe));
2075
+ if (indexChanges.length > 0) {
2076
+ init.indexChanges = indexChanges;
2077
+ init.status = "alter";
2078
+ }
2079
+ if (adapter.planForeignKeys && mergeLiveForeignKeyChanges(init, await adapter.planForeignKeys())) init.status = "alter";
2080
+ }
2081
+ /**
1921
2082
  * Pure validation over the discovery facts. Every refusal is a change that
1922
2083
  * no order of DDL can apply safely; the messages are user-facing.
1923
2084
  */
@@ -2283,6 +2444,7 @@ var SchemaSync = class {
2283
2444
  await this.store.ensureControlTable();
2284
2445
  const d = await this.discover(resolved, safe);
2285
2446
  const refusals = this.preflight(d, safe);
2447
+ await this.planLiveWork(d, safe);
2286
2448
  const entries = this.buildPlanEntries(d, safe, refusals);
2287
2449
  if (!force) {
2288
2450
  if (await this.store.readHash() === hash) return {
@@ -2435,6 +2597,7 @@ exports.fkKey = require_column_diff.fkKey;
2435
2597
  exports.hasForeignKeyChanges = require_column_diff.hasForeignKeyChanges;
2436
2598
  exports.planSchema = planSchema;
2437
2599
  exports.readStoredSnapshot = readStoredSnapshot;
2600
+ exports.sameDefaultValue = require_column_diff.sameDefaultValue;
2438
2601
  exports.snapshotToExistingColumns = require_column_diff.snapshotToExistingColumns;
2439
2602
  exports.snapshotToExistingTableOptions = require_column_diff.snapshotToExistingTableOptions;
2440
2603
  exports.syncSchema = syncSchema;