import { Association, Attribute, DbColumn, LogicalModel, ModelId } from './types'; /** modelId 생성기 — 순수성/테스트 결정성을 위해 주입 가능. 기본은 crypto.randomUUID. */ export type IdFactory = () => ModelId; /** * 한 관계가 자식에 요구하는 FK 속성의 "원하는 상태" 스펙. * modelId가 없다(매칭 후 reconcile이 신규에만 부여). 매칭 키 = associationRef + sourceAttributeRef. */ export interface ForeignAttributeSpec { associationRef: ModelId; sourceAttributeRef: ModelId; name: string; type: string; identifier: boolean; notNull: boolean; /** 부모 식별자의 컬럼을 미러한 dbAttrs (modelId 없음 — reconcile이 부여). **물리명은 빈값**(상속). */ dbAttrs: Omit[]; /** * 부모 PK 컬럼의 물리명 — **유일화 판정 전용 내부 축**이고 `specToAttribute` 가 영속하지 않는다. * 물리명을 비워 상속시키므로(위) 충돌 시 무엇을 기준으로 유일화할지가 spec 안에 남아 있어야 한다. */ originPhysicalNames?: readonly string[]; } export interface ReconcilePlan { /** 신규 생성할 FK 속성 (modelId·dbAttr modelId 부여됨, order 포함) */ add: Attribute[]; /** 기존 FK 속성에 적용할 부분 패치 (부모와 동기화돼야 하는 필드만) */ update: { attributeId: ModelId; patch: Partial; }[]; /** 더 이상 근거 관계가 없어 제거할 FK 속성 */ remove: ModelId[]; } /** * 한 관계(association)가 자식(end2)에 만들어야 할 FK 속성 스펙들을 계산. * 부모(end1)의 현재 유효 식별자(자기 PK + 전파받은 FK 식별자)를 order순으로 미러한다. * self-association은 식별 불가 — identifier를 강제로 false로 둔다(INV-3). */ export declare function computeForeignAttributes(logical: LogicalModel, association: Association): ForeignAttributeSpec[]; /** * 자식 엔터티의 현재 FK 속성을, 들어오는 모든 관계가 요구하는 desired 집합과 맞춘다(diff). * - desired에만 있음 → add (신규 modelId 부여, 기존 최대 order 뒤에 append) * - 양쪽에 있음 → 동기화 필드 차이만 update * - current에만 있음(근거 관계 소멸) → remove * 멱등(INV-5): 이미 맞춰진 상태에서 다시 호출하면 빈 계획을 돌려준다. */ export declare function reconcileForeignAttributes(logical: LogicalModel, childEntityId: ModelId, idFactory?: IdFactory): ReconcilePlan; /** * 같은 자식에 두 번째 계획이 도착했을 때(비등깊이 다중 경로 — A→D 직행 + A→B→D 경유) 병합한다. * next는 "원본 + base 적용" 상태 기준의 diff이므로, base의 add를 겨냥한 후속 update/remove를 * add 쪽으로 fold하면 병합 결과는 다시 *원본* 기준 diff가 된다(반환 계약 보존). * add의 modelId는 work에 이미 적용된 것을 그대로 유지해야 한다 — 후손 계획의 * sourceAttributeRef가 이 id를 참조하므로 재계산(새 id 발급)은 정합을 깨뜨린다. */ /** * 두 reconcile 계획을 하나로 접는다 — 늦게 온 patch 가 이긴다(같은 속성이면 필드 단위 덮어쓰기). * ★다중 경로 전파(CC-2)뿐 아니라 **여러 루트의 cascade 가 같은 후손에 겹칠 때**도 쓴다 * (`childReconcileWithCascade` — 다이아몬드에서 같은 자식이 두 번 계획되면 op 배치에 같은 경로의 * `$set` 이 두 번 실려 호스트가 배치를 거부한다). */ export declare function mergePlans(base: ReconcilePlan, next: ReconcilePlan): ReconcilePlan; /** * changedEntityId의 식별자 변경이 영향을 주는 모든 후손 자식의 reconcile 계획. * 클론에 단계별로 적용하며 다음 단계 입력을 갱신하므로, 반환 계획은 *원본* logical 기준이다. * self-association은 비식별이라 자식 식별자를 바꾸지 않아 더 전파되지 않는다(자연 종료). * * 비등깊이 다중 경로(A→D + A→B→D)에서는 D의 desired가 두 단계에 걸쳐 도착한다 — * 늦게 온 계획은 mergePlans로 병합하고, 계획을 받은 자식은 재큐해 후손을 재연쇄한다. * 종료(INV-6): reconcile이 멱등 diff라 비순환 그래프는 자연 수렴(재처리 횟수 ≤ 최장 경로 깊이). * 식별 사이클(A⇄B)은 desired가 무한 성장하므로 엔터티당 처리 횟수를 entities.length로 캡해 절단한다 * (비순환에선 최장 경로 ≤ 엔터티 수라 캡이 정확성을 해치지 않는다). */ export declare function planPropagation(logical: LogicalModel, changedEntityId: ModelId, idFactory?: IdFactory): Map; /** * "자식의 FK 집합이 직접 바뀌는" 트리거(관계 삭제·identifying 토글·부모 엔터티 삭제)의 * seed reconcile + 후손 cascade 통합 계획. futureLogical은 트리거 변경이 이미 반영된 상태여야 한다. * 반환은 seed(자식)부터 후손 순서의 목록 — 빈 계획은 제외. * * editor controller(→Command)와 agent resolver(→OpShape)가 이 산식을 공유한다 — 계약 4(cascade = * 클라 명시 분해)의 계산 주체를 한 곳에 두어 쓰기 경로별 분해 드리프트를 구조적으로 차단한다. */ export declare function planChildReconcileWithCascade(futureLogical: LogicalModel, childId: ModelId, idFactory?: IdFactory): Array<{ entityId: ModelId; plan: ReconcilePlan; }>; /** * 병합이 남길 수 있는 **중복 FK 추가**를 접는다. `mergePlans` 는 add 를 단순 이어붙이는데, 두 루트가 * 같은 후손에 같은 FK 를 계획하면 idFactory 가 매번 새 modelId 를 내므로 **같은 컬럼이 두 개** 추가된다 * (제거·수정과 달리 modelId 가 달라 중복으로 안 보인다). 근거 관계와 원본 컬럼이 같으면 같은 FK 다. * (editor `commandPlans` 로컬에 있던 것을 다중 자식 병합의 core 이관과 함께 올렸다 — 소비처 2곳.) */ export declare function dedupeAdds(plan: ReconcilePlan): ReconcilePlan; /** * 여러 seed 자식의 reconcile + 후손 cascade 를 **엔터티당 계획 하나**로 병합한 통합 계획. * * 한 조작이 여러 자식을 동시에 reconcile 할 때 cascade 서브트리는 **겹칠 수 있다** — 삭제 대상의 * 직접 자식이면서 동시에 다른 자식의 손자인 다이아몬드(실측 `Cancellation → {CCS, CLI}` + `CCS → CLI`). * 루트마다 따로 계획하면 같은 후손에 같은 계획이 두 벌 생기고, 로컬 apply 는 멱등이라 조용히 지나가지만 * **op 배치에는 같은 경로의 `$set` 이 두 번 실려** 호스트가 배치 전체를 거부한다. ⇒ `mergePlans` 로 접고 * `dedupeAdds` 로 중복 FK 추가를 걷는다. * * editor `childReconcileWithCascade`(→Command)와 agent resolver(→OpShape)가 이 병합을 공유한다 — * 종전에는 editor 만 병합하고 resolver `entity.remove` 는 자식별 루프라 다이아몬드에 노출돼 있었다. */ export declare function planChildrenReconcileWithCascade(futureLogical: LogicalModel, childIds: readonly ModelId[], idFactory?: IdFactory): Array<{ entityId: ModelId; plan: ReconcilePlan; }>; /** * 주어진 속성이 "연관관계가 관리하는 FK 컬럼"이면 그 소유 연관관계를 돌려준다(아니면 undefined). * * 논리 모델러 계보(ERwin/ER-Studio/PowerDesigner)에서 FK 컬럼은 관계가 migrate한 파생 속성이라 * 관계가 소유한다 — 컬럼을 직접 지우는 게 아니라 관계를 삭제/변경해 제거해야 한다. 이 판별로 * FK 속성의 직접 삭제를 차단(어포던스+가드)하고 사용자를 관계 삭제 경로로 유도한다. * * FK 컬럼 불변식(`isFkDerivedColumn` = RELATION* + 물리 컬럼 보유)으로 먼저 게이트해, 관계의 * 부모측 end가 가리키는 PK(비-RELATION) 오탐을 배제한다. 그다음 두 신호로 소유 관계를 찾는다: * * 1순위 `derivedFrom.associationRef` — 전파가 심은 권위 신호. v2 네이티브 doc에 보존되고 어댑터 * hydration이 복원한다. GUI·sync 스킬·에이전트 어느 경로로 만들어졌든 FK 자신에 붙어 있어, * 관계 end의 `attributeRef`가 stale해도(자기참조 재생성·sync가 링크를 다르게 심은 경우 등) 정확하다. * 2순위 관계 end의 `attributeRef` 역참조 — `derivedFrom`이 없는 경우(레거시 v1 로드 FK 등)의 폴백. * 방향 규약은 end2=자식이지만 드문 로드 형태가 end1에 실을 수 있어 양 end를 모두 검사한다. * * (초기엔 attributeRef만 신뢰했으나, sync 스킬로 만든 자기참조 FK가 `derivedFrom`은 정확한데 * end2.attributeRef는 옛 FK id를 가리키는 실사례로 derivedFrom을 1순위로 승격.) * * `derivedFrom`이 이미 삭제된 관계를 가리키거나(고아) 둘 다 매칭 없으면 undefined → 직접 삭제(정리) 허용. */ export declare function findFkOwningAssociation(logical: LogicalModel, entityId: ModelId, attributeId: ModelId): Association | undefined;