import { DbColumn, LogicalModel, ModelId } from './types'; /** owner 표시 속성 한 건의 슬롯 보충 계획 — 컨트롤러가 `updateAttribute { dbAttrs }`로 커밋한다. */ export interface EmbedSlotFill { /** 표시 속성을 보유한 소유자 엔티티. */ entityId: ModelId; /** `EMBED_OWN` 표시 속성. */ attributeId: ModelId; /** 보충 후 전체 슬롯 목록(기존 슬롯 그대로 + 꼬리 빈 슬롯). */ dbAttrs: DbColumn[]; /** 보충한 슬롯 수(로그·테스트 가독용). */ added: number; } /** * `embeddableRef`를 사용하는 모든 owner의 슬롯을 선언 컬럼 수까지 채우는 계획. * 대상이 없거나 이미 맞으면 빈 배열(무변경). 컬렉션 EMBED(@ElementCollection)는 컬럼이 임베더블에 * 남고 owner 슬롯을 비우는 것이 정상이라 제외한다(`controller.setEmbedCardinality`가 `dbAttrs: []`). */ export declare function planEmbedSlotFill(model: LogicalModel, embeddableRef: ModelId, genId?: () => ModelId): EmbedSlotFill[]; /** 한 사용처의 슬롯 재배치 계획 — 컨트롤러/resolver가 `updateAttribute { dbAttrs }`로 커밋한다. */ export interface EmbedSlotReorder { /** 표시 속성을 보유한 소유자 엔티티. */ entityId: ModelId; /** `EMBED_OWN` 표시 속성. */ attributeId: ModelId; /** 재배치 후 전체 슬롯 목록(같은 슬롯 객체들의 순서만 바뀐다 — 신규·삭제 없음). */ dbAttrs: DbColumn[]; /** 자리를 옮긴 슬롯 수(로그·테스트 가독용). */ moved: number; } /** * 임베드 슬롯 **정렬 전파** — 임베더블 속성이 재정렬되면 사용처(owner) 슬롯도 같은 순열로 옮긴다. * `planEmbedSlotFill`(꼬리 보충)의 순서 축 짝이다. * * ## 왜 가능한가 — 사후 복원은 불가지만 조작 시점엔 근거가 있다 * 슬롯↔소스 대응은 **위치(인덱스)뿐**이라(슬롯에 소스 링크가 없다) *이미 어긋난 뒤에는* 어느 슬롯이 * 어느 컬럼이었는지 복원할 수 없다 — 이 파일 헤더가 적어 둔 그 한계다. 그러나 **재정렬을 실행하는 * 시점에는 순열을 알 수 있다**: 임베더블 컬럼(`DbColumn.modelId`)이 재정렬 전후로 동일한 신원을 * 유지하므로, 전/후 평탄 순서를 컬럼 id로 짝지으면 슬롯을 옮길 자리가 유일하게 결정된다. * ⇒ 저장 스키마에 소스 링크를 새로 심지 않고도 앞으로의 유입을 차단한다(예방이 본체 — 축적분은 * 아래 게이트가 보류하고 `CHK-JPA-21`이 계속 지목한다). * * ## 안전성 — 보충과 달리 충돌 게이트가 필요 없다 * 순열은 슬롯 **집합을 보존**한다(같은 객체들을 옮길 뿐). 따라서 그 엔티티의 실효 물리명 다중집합이 * 불변이고 `CHK-NAME-2`(물리명 중복)·DDL 컬럼 집합에 영향이 없다 — 보충이 새 이름을 상속시켜 * 충돌을 만들 수 있던 것과 성격이 다르다. 바뀌는 것은 *어느 override가 어느 소스 컬럼에 붙는가*이고, * 그 대응이 바로 재정렬이 옮긴 것이다. * * ## 게이트 둘 — 확실할 때만 옮긴다 * 1. **순열일 때만**: `nextAttributeOrder`가 현재 속성 집합과 같아야 한다. 추가·삭제가 섞였으면 * 보충(꼬리) 축 소관이고, 위치 대응이 이번 조작 안에서 이미 흔들려 순열 근거가 없다. * 2. **대응이 성립하던 사용처만**: 슬롯 수가 재정렬 전 선언 컬럼 수와 같아야 한다. 이미 뒤처진/앞선 * 사용처(축적 드리프트)는 인덱스 대응 자체가 깨져 있어 옮기면 **더 나빠진다**([[dont-invent-unknown-values]] * — 근거 없는 재배치는 추측이다). 그런 사용처는 `CHK-JPA-21`이 계속 지목한다. * * @param model 재정렬 **이전** 상태(현재 모델). * @param embeddableRef 재정렬된 임베더블(사용처가 `embedded.embeddableRef`로 가리키는 그 엔티티). * @param nextAttributeOrder 재정렬 후 임베더블 속성 순서(전체 — `reorderedSequence` 산출). */ export declare function planEmbedSlotReorder(model: LogicalModel, embeddableRef: ModelId, nextAttributeOrder: readonly ModelId[]): EmbedSlotReorder[]; /** 한 사용처의 **슬롯 제거** 계획 — 임베더블 속성이 삭제되면 그 자리 슬롯을 걷어낸다. */ export interface EmbedSlotRemoval { /** 표시 속성을 보유한 소유자 엔티티. */ entityId: ModelId; /** `EMBED_OWN` 표시 속성. */ attributeId: ModelId; /** 제거 후 전체 슬롯 목록(남는 슬롯의 **순서·정체성 보존** — 신규 슬롯 없음). */ dbAttrs: DbColumn[]; /** 걷어낸 슬롯 — **호출자가 무엇이 사라지는지 볼 수 있게** 실어 보낸다(S5 `dropped` 동형). */ dropped: DbColumn[]; } /** * 임베드 슬롯 **삭제 전파** — 임베더블 속성이 삭제되면 사용처(owner)의 그 자리 슬롯을 함께 걷어낸다. * `planEmbedSlotFill`(꼬리 보충)의 **역방향 짝**이고, 게이트는 `planEmbedSlotReorder`(순열)와 * `planEmbedSlotShapeNormalization`(형상)에서 각각 하나씩 물려받는다. * * ## 왜 필요한가 — 삭제는 «남은 슬롯의 뜻»을 바꾼다 * 슬롯↔소스 대응은 **위치(인덱스)뿐**이라, 선언 속성 하나가 사라지면 그 뒤 슬롯이 **한 칸씩 밀려** * 각자 남의 컬럼을 상속한다. 보충 부재가 *"컬럼이 모자란다"*(양의 문제)라면 이쪽은 * *"남은 컬럼이 다른 것을 가리킨다"*(뜻의 문제)라 조용하고 더 나쁘다. * ★실측(프로덕션 `OrderItemOriginalAmt` · 사용처 62슬롯): 선언 속성 **하나만** 지우면 * `CHK-NAME-2` 가 **+30**(사용처 2 → 34) 된다. * * ## 왜 가능한가 — reorder 와 같은 근거 * *사후 복원은 불가지만 조작 시점엔 근거가 있다.* 삭제를 실행하는 시점에는 어느 선언 컬럼 * (`DbColumn.modelId`)이 사라지는지 알 수 있으므로, **남는 컬럼이 이전 평탄 목록의 어느 위치였는지**로 * 남길 슬롯이 유일하게 결정된다. `planEmbedSlotReorder` 는 **재사용할 수 없다** — 그쪽 게이트 1이 * *"순열일 때만"* 이라 집합이 줄면 즉시 빈 계획이 된다. * * ## 게이트 셋 — 확실할 때만 걷어낸다 * 1. **삭제만**: 지울 것이 실제로 있고(집합이 줄고) 그 속성이 컬럼을 가질 때. 추가가 섞였으면 위치 * 대응이 이번 조작 안에서 흔들려 근거가 없다(호출자가 가른다 — reorder 게이트 1과 같은 규율). * 2. **대응이 성립하던 사용처만**: 슬롯 수가 삭제 전 선언 컬럼 수와 같아야 한다. 이미 뒤처진/앞선 * 사용처는 인덱스 대응 자체가 깨져 있어 걷어내면 **더 나빠진다**(reorder 게이트 2 동형). * 3. ★**사라지는 자리가 진짜 override 를 쥐지 않았을 때만**: 슬롯이 **선언과 다른** 물리명·공유참조를 * 들고 있으면 그건 사람이 넣은 `@AttributeOverride` 라 도구가 버릴 수 없다(S5 게이트 2 동형). * ⚠️★**「빈 슬롯인가」로 재면 안 된다 — 「선언과 다른가」로 재야 한다.** 연결 시점 * `flattenEmbeddableColumns` 가 소스 값을 **verbatim 복사**하므로 방금 연결한 사용처의 슬롯은 전부 * 물리명을 «명시 보유»한 형태고, 저장·재로드하면 `embedColTo` 가 source diff 로 환원해 빈 순수 * 참조가 된다 — **같은 논리 상태가 세션에 따라 다른 표현**을 갖는다. «빈 슬롯»으로 재면 그 표현 * 차이에 걸려 전파가 죽는다(프로덕션 실측: 전파 **75 → 2** · 보류 17 → **90**). 선언과 대조하면 * **버릴 것이 실제로 있는지**만 본다. * ⚠️**「선언과 같으니 버려도 무해」가 아니다** — 삭제 축에서는 선언 자체가 사라지므로 그 값을 다시 * 구할 데가 없다. 버릴 수 있는 근거는 *복원 가능성*이 아니라 **그 슬롯이 독자적으로 아는 정보가 * 없다**는 것이다: 컬럼이 사라지는 것은 선언을 지운 **정당한 결과**이고, 남기면 오히려 그 슬롯이 * 밀려 남의 컬럼을 상속한다. 반대로 선언과 다른 값은 슬롯만 아는 정보라 사라지면 복구 불가다. * * ## 왜 충돌 게이트가 없는가 (S5 와 다른 점) * S5(형상 정규화)는 **빈 슬롯을 삽입**하므로 새 이름이 생겨 충돌을 만들 수 있고, 그래서 정규화 후 * 이름 다중집합을 실제로 계산하는 게이트를 뒀다. 삭제는 반대로 **이름이 늘 수 없다**: * - 남는 슬롯의 **상속원이 바뀌지 않는다** — 새 선언 목록은 옛 목록의 **부분수열**이고 남길 슬롯을 * 그 부분수열로 골랐으므로, 남는 슬롯 j 의 짝은 삭제 전 그 슬롯의 짝과 **같은 컬럼**이다. * - 사라지는 슬롯은 게이트 3에 의해 **빈 슬롯이거나 선언과 같은 값**뿐이라, 그 이름은 사라지는 선언 * 컬럼의 이름과 같고 함께 사라진다. * ⇒ 실효 이름 다중집합은 **엄격히 줄거나 같다**(`CHK-NAME-2` 는 감소 방향) — reorder 가 *"순열은 슬롯 * 집합을 보존한다"* 로 게이트를 뺀 것과 같은 자리다. * ⚠️애초 설계에는 S5 대칭으로 게이트 4(산출물 대조)를 두려 했으나 **구현 중 기각**했다 — 위 논증으로 * 발동이 불가능한 데다, 판정에 쓸 `duplicateNameCount` 가 **삭제 전 model** 을 보므로 남는 슬롯이 * *옛* 선언 j 를 상속한 것으로 계산해 **틀린 상태로 판정**한다(정확히 재려면 삭제 반영 future 가 필요한데 * 그 비용을 발동 0 인 게이트에 치를 이유가 없다). 대신 그 불변식을 테스트가 단언한다. * * @param model 삭제 **이전** 상태(현재 모델) — 사라지는 슬롯의 정체를 알아야 한다. * @param embeddableRef 속성이 삭제된 임베더블(사용처가 `embedded.embeddableRef`로 가리키는 그 엔티티). * @param removedAttributeIds 삭제되는 임베더블 속성 id 집합. */ export declare function planEmbedSlotRemoval(model: LogicalModel, embeddableRef: ModelId, removedAttributeIds: readonly ModelId[]): EmbedSlotRemoval[]; /** 한 사용처의 **형상 정규화** 계획 — legacy(1 속성 = 1 슬롯) → fresh(1 컬럼 = 1 슬롯). */ export interface EmbedSlotShapeNormalization { /** 표시 속성을 보유한 소유자 엔티티. */ entityId: ModelId; /** `EMBED_OWN` 표시 속성. */ attributeId: ModelId; /** 정규화 후 전체 슬롯 목록 — 기존 슬롯이 **제자리로 이동** + 빈 슬롯 삽입 + 잉여 제거. */ dbAttrs: DbColumn[]; /** 자리를 옮긴 기존 슬롯 수(로그·테스트 가독용). */ moved: number; /** 새로 삽입한 빈 슬롯 수(선언 다중 컬럼 속성의 2번째 이후 자리). */ inserted: number; /** 대응할 선언 컬럼이 없어 빠진 슬롯 — **호출자가 무엇이 사라지는지 볼 수 있게** 실어 보낸다. */ dropped: DbColumn[]; } /** * 임베드 슬롯 **형상 정규화** — v1 유래 legacy 사용처를 선언 형상(fresh)에 맞춘다. * `planEmbedSlotFill`(꼬리 보충)·`planEmbedSlotReorder`(순열)에 이어지는 **세 번째 축**이고, * 앞의 둘이 손대지 못하던 «중간이 어긋난» 형상을 다룬다. * * ## 왜 여기서는 대응을 복원할 수 있는가 — 이 파일 헤더의 한계에 대한 예외 * 헤더는 *"어느 슬롯이 어느 컬럼이었는지 복원할 근거가 모델에 없다(소스 링크 부재)"* 라고 적었고 * 그건 **임의 드리프트**에 대해 참이다. legacy 형상은 임의가 아니라 **체계적 산물**이라 다르다: * - `migrateLegacyMoney` 가 v1→v2 에서 레거시 1컬럼 `CustomMoneyType` 을 amount(**verbatim 보존**) + * currency(빈 슬롯) 2컬럼으로 승격하는데, 그 판정이 `attrType === 'NORMAL'` 이라 **임베더블 선언에만 * 적용되고 owner 의 `EMBED_OWN` 사용처 슬롯은 따라가지 않는다**. * - v1 의 DIVIDER 는 이름이 대시인 가짜 `NORMAL` 속성이었고 사용처 평탄화에 **한 칸**을 차지했는데, * 로드 시 `DIVIDER` 로 승격되며 선언 측에서 `dbAttrs: []` 가 됐다(`types.ts`). * ⇒ 남는 형상이 정확히 **「1 속성 = 1 슬롯」**이고, 그 슬롯은 선언 속성의 **첫 컬럼**(Money 면 amount)이다. * 즉 대응의 근거는 슬롯이 아니라 **형상 규칙**이 들고 있다. * * ★실측이 이 산식을 확증한다(프로덕션 v2 24문서 · EMBED_OWN 사용처 100): * fresh **70** · legacy **27** · other 3 이고, legacy 27 은 **27/27 이 `슬롯 수 == 선언 속성 수`**다 * (경쟁 가설 «물질화 컬럼 수»는 단독 성립 **0**). legacy 임베더블의 다중 컬럼 선언 속성은 * **전부 `Money:2`**(175건)이라 «첫 컬럼» 이 모호해질 자리가 없다. * * ## 왜 삭제가 아니라 정렬인가 (S4 → S5 흡수) * DIVIDER 자리 슬롯만 **지우는** 안은 시뮬레이션이 기각했다 — 그 빈 슬롯이 인덱스 상속으로 이름을 얻어 * 어떤 컬럼의 *유일 방출자*가 되어 있어 실 컬럼 2개를 잃었다. 삭제는 **재인덱싱**이라 현재 상태로 * 계산한 어떤 기준도 삭제 후를 예측하지 못한다. 정렬은 반대로 **선언 컬럼마다 슬롯이 하나씩** 생기므로 * 컬럼 집합이 구성상 보존되고, 그 뒤에야 잉여가 «대응 없음»으로 명확히 식별된다. * * ## 게이트 셋 * 1. **legacy 형상일 때만** — 슬롯 수가 선언 **속성 수**와 같고 선언 **컬럼 수**와는 다를 때. * 둘이 같으면(다중 컬럼·DIVIDER 없는 임베더블) 이미 정렬돼 있어 할 일이 없다. * 2. **잉여는 빈 슬롯일 때만 뺀다** — 대응 자리가 없는 슬롯이 물리명·공유참조를 **명시 보유**하면 * 그건 사람이 넣은 값이라 도구가 버릴 수 없다(실측 22/22 가 빈 슬롯이지만 규칙으로 박아 둔다). * 3. **이름 충돌을 만들지 않을 때만** — 삽입 슬롯은 선언 기본명을 상속하므로, 같은 임베더블을 두 번 쓰는 * 엔티티에서 서로 같은 이름이 될 수 있다. 정규화 **후** 실효 이름 다중집합을 실제로 계산해 * 중복이 늘면 그 사용처는 보류한다(`CHK-JPA-21` 이 계속 지목하므로 무신호가 아니다). * ★기준 계산이 아니라 **산출물 대조**다 — S4 가 기준 계산으로 두 개의 다른 답을 얻은 자리다. */ export declare function planEmbedSlotShapeNormalization(model: LogicalModel, embeddableRef: ModelId, genId?: () => ModelId): EmbedSlotShapeNormalization[];