mega-framework 0.1.6 → 0.1.7

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 (233) hide show
  1. package/bin/mega-ws-hub.js +2 -2
  2. package/package.json +32 -8
  3. package/sample/crud/.env +1 -1
  4. package/sample/crud/.env.example +1 -1
  5. package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
  6. package/sample/crud/.mega/journal/snapshot.json +261 -0
  7. package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
  8. package/sample/crud/apps/main/controllers/web-controller.js +7 -5
  9. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
  10. package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
  11. package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
  12. package/sample/crud/apps/main/models/log-partition-model.js +105 -0
  13. package/sample/crud/apps/main/models/note-model.js +79 -0
  14. package/sample/crud/apps/main/models/user-level-model.js +24 -0
  15. package/sample/crud/apps/main/models/user-model.js +146 -0
  16. package/sample/crud/apps/main/models/user-type-model.js +21 -0
  17. package/sample/crud/apps/main/models/wallet-model.js +24 -0
  18. package/sample/crud/apps/main/routes/users.js +55 -10
  19. package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
  20. package/sample/crud/apps/main/services/auth-service.js +39 -24
  21. package/sample/crud/apps/main/services/log-partition-service.js +101 -0
  22. package/sample/crud/apps/main/services/note-service.js +6 -6
  23. package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
  24. package/sample/crud/apps/main/services/user-service.js +62 -21
  25. package/sample/crud/apps/main/views/auth/login.ejs +6 -6
  26. package/sample/crud/apps/main/views/auth/register.ejs +46 -5
  27. package/sample/crud/apps/main/views/users/edit.ejs +42 -5
  28. package/sample/crud/apps/main/views/users/list.ejs +6 -2
  29. package/sample/crud/apps/main/views/users/new.ejs +56 -4
  30. package/sample/crud/docs/log_partition_design.mm.md +23 -0
  31. package/sample/crud/mega.config.js +3 -2
  32. package/sample/crud/package.json +1 -1
  33. package/sample/crud/scripts/start-ws-hub.sh +2 -2
  34. package/sample/simple/package.json +2 -2
  35. package/src/adapters/adapter-manager.js +2 -1
  36. package/src/adapters/adapter-options.js +30 -0
  37. package/src/adapters/maria-adapter.js +26 -3
  38. package/src/adapters/mega-db-adapter.js +7 -1
  39. package/src/adapters/mongo-adapter.js +19 -1
  40. package/src/adapters/postgres-adapter.js +25 -2
  41. package/src/adapters/sqlite-adapter.js +20 -1
  42. package/src/cli/commands/new.js +13 -3
  43. package/src/cli/commands/scaffold.js +137 -33
  44. package/src/cli/generators/index.js +82 -2
  45. package/src/cli/index.js +353 -100
  46. package/src/core/ajv-mapper.js +27 -2
  47. package/src/core/boot.js +464 -245
  48. package/src/core/cluster-metrics.js +13 -4
  49. package/src/core/ctx-builder.js +6 -2
  50. package/src/core/envelope.js +112 -12
  51. package/src/core/hub-link.js +65 -4
  52. package/src/core/i18n.js +11 -1
  53. package/src/core/index.js +6 -2
  54. package/src/core/mega-app.js +201 -463
  55. package/src/core/mega-cluster.js +4 -1
  56. package/src/core/mega-server.js +40 -9
  57. package/src/core/migration/dialect-registry.js +107 -0
  58. package/src/core/migration/dialects/README.md +62 -0
  59. package/src/core/migration/dialects/maria.js +496 -0
  60. package/src/core/migration/dialects/mongo.js +824 -0
  61. package/src/core/migration/dialects/postgres.js +563 -0
  62. package/src/core/migration/dialects/sqlite.js +476 -0
  63. package/src/core/migration/differ.js +456 -0
  64. package/src/core/migration/generate.js +508 -0
  65. package/src/core/migration/journal.js +167 -0
  66. package/src/core/migration/model-scan.js +84 -0
  67. package/src/core/migration/mongo-migration-db.js +97 -0
  68. package/src/core/migration/schema-builder.js +400 -0
  69. package/src/core/migration/schema-validator.js +315 -0
  70. package/src/core/migration-lock.js +205 -0
  71. package/src/core/migration-runner.js +166 -38
  72. package/src/core/multipart.js +28 -5
  73. package/src/core/pipeline.js +129 -0
  74. package/src/core/router.js +70 -65
  75. package/src/core/security.js +67 -9
  76. package/src/core/workers-manager.js +12 -1
  77. package/src/core/ws-cluster.js +10 -3
  78. package/src/core/ws-message.js +48 -4
  79. package/src/core/ws-presence.js +624 -0
  80. package/src/core/ws-roster.js +4 -1
  81. package/src/core/ws-upgrade.js +118 -12
  82. package/src/index.js +1 -1
  83. package/src/lib/hub-protocol.js +29 -0
  84. package/src/lib/mega-health.js +25 -4
  85. package/src/lib/mega-job-queue.js +98 -21
  86. package/src/lib/mega-job.js +29 -0
  87. package/src/lib/mega-metrics.js +3 -12
  88. package/src/lib/mega-plugin.js +34 -3
  89. package/src/lib/mega-schedule.js +40 -22
  90. package/src/lib/mega-shutdown.js +114 -39
  91. package/src/lib/mega-tracing.js +66 -19
  92. package/src/lib/mega-worker.js +5 -1
  93. package/src/lib/otel-resource.js +36 -0
  94. package/src/{cli → lib}/ws-hub.js +51 -8
  95. package/src/models/crud-sql-builder.js +133 -0
  96. package/src/models/mega-model.js +82 -2
  97. package/src/models/model-crud.js +483 -0
  98. package/src/models/mongo-crud.js +285 -0
  99. package/templates/model/code-mongo.tpl +35 -0
  100. package/templates/model/code.tpl +15 -1
  101. package/templates/model/test-mongo.tpl +38 -0
  102. package/templates/model/test.tpl +4 -0
  103. package/types/adapters/adapter-manager.d.ts +95 -0
  104. package/types/adapters/adapter-options.d.ts +91 -0
  105. package/types/adapters/file-adapter.d.ts +94 -0
  106. package/types/adapters/file-session-adapter.d.ts +101 -0
  107. package/types/adapters/index.d.ts +20 -0
  108. package/types/adapters/maria-adapter.d.ts +115 -0
  109. package/types/adapters/mega-adapter.d.ts +215 -0
  110. package/types/adapters/mega-bus-adapter.d.ts +45 -0
  111. package/types/adapters/mega-cache-adapter.d.ts +47 -0
  112. package/types/adapters/mega-db-adapter.d.ts +47 -0
  113. package/types/adapters/mega-lock-adapter.d.ts +62 -0
  114. package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
  115. package/types/adapters/mega-session-adapter.d.ts +32 -0
  116. package/types/adapters/mongo-adapter.d.ts +139 -0
  117. package/types/adapters/nats-adapter.d.ts +108 -0
  118. package/types/adapters/postgres-adapter.d.ts +139 -0
  119. package/types/adapters/redis-adapter.d.ts +70 -0
  120. package/types/adapters/redis-session-adapter.d.ts +82 -0
  121. package/types/adapters/redlock-adapter.d.ts +149 -0
  122. package/types/adapters/registry.d.ts +46 -0
  123. package/types/adapters/sqlite-adapter.d.ts +106 -0
  124. package/types/auth/index.d.ts +24 -0
  125. package/types/cli/commands/console-cmd.d.ts +37 -0
  126. package/types/cli/commands/new.d.ts +16 -0
  127. package/types/cli/commands/routes.d.ts +36 -0
  128. package/types/cli/commands/scaffold.d.ts +78 -0
  129. package/types/cli/commands/test-cmd.d.ts +14 -0
  130. package/types/cli/generators/index.d.ts +112 -0
  131. package/types/cli/index.d.ts +249 -0
  132. package/types/cli/template-engine.d.ts +40 -0
  133. package/types/core/ajv-mapper.d.ts +27 -0
  134. package/types/core/boot.d.ts +233 -0
  135. package/types/core/cluster-metrics.d.ts +52 -0
  136. package/types/core/config-loader.d.ts +13 -0
  137. package/types/core/config-validator.d.ts +30 -0
  138. package/types/core/ctx-builder.d.ts +80 -0
  139. package/types/core/envelope.d.ts +79 -0
  140. package/types/core/error-mapper.d.ts +17 -0
  141. package/types/core/formbody.d.ts +41 -0
  142. package/types/core/hub-link.d.ts +264 -0
  143. package/types/core/i18n.d.ts +178 -0
  144. package/types/core/index.d.ts +28 -0
  145. package/types/core/mega-app.d.ts +529 -0
  146. package/types/core/mega-cluster.d.ts +104 -0
  147. package/types/core/mega-server.d.ts +91 -0
  148. package/types/core/mega-service.d.ts +31 -0
  149. package/types/core/migration/dialect-registry.d.ts +22 -0
  150. package/types/core/migration/dialects/maria.d.ts +99 -0
  151. package/types/core/migration/dialects/mongo.d.ts +89 -0
  152. package/types/core/migration/dialects/postgres.d.ts +117 -0
  153. package/types/core/migration/dialects/sqlite.d.ts +111 -0
  154. package/types/core/migration/differ.d.ts +47 -0
  155. package/types/core/migration/generate.d.ts +56 -0
  156. package/types/core/migration/journal.d.ts +52 -0
  157. package/types/core/migration/model-scan.d.ts +19 -0
  158. package/types/core/migration/mongo-migration-db.d.ts +7 -0
  159. package/types/core/migration/schema-builder.d.ts +197 -0
  160. package/types/core/migration/schema-validator.d.ts +20 -0
  161. package/types/core/migration-lock.d.ts +33 -0
  162. package/types/core/migration-runner.d.ts +101 -0
  163. package/types/core/multipart.d.ts +86 -0
  164. package/types/core/openapi.d.ts +62 -0
  165. package/types/core/pipeline.d.ts +92 -0
  166. package/types/core/router.d.ts +159 -0
  167. package/types/core/routes-loader.d.ts +21 -0
  168. package/types/core/scope-registry.d.ts +14 -0
  169. package/types/core/security.d.ts +77 -0
  170. package/types/core/services-loader.d.ts +27 -0
  171. package/types/core/session-cleanup-schedule.d.ts +19 -0
  172. package/types/core/session-store.d.ts +18 -0
  173. package/types/core/session.d.ts +77 -0
  174. package/types/core/static-assets.d.ts +73 -0
  175. package/types/core/template.d.ts +106 -0
  176. package/types/core/workers-manager.d.ts +79 -0
  177. package/types/core/ws-cluster.d.ts +208 -0
  178. package/types/core/ws-compression.d.ts +112 -0
  179. package/types/core/ws-controller.d.ts +65 -0
  180. package/types/core/ws-message.d.ts +106 -0
  181. package/types/core/ws-presence.d.ts +273 -0
  182. package/types/core/ws-roster.d.ts +96 -0
  183. package/types/core/ws-upgrade.d.ts +231 -0
  184. package/types/errors/config-error.d.ts +10 -0
  185. package/types/errors/http-errors.d.ts +120 -0
  186. package/types/errors/index.d.ts +3 -0
  187. package/types/errors/mega-error.d.ts +32 -0
  188. package/types/index.d.ts +39 -0
  189. package/types/lib/asp/config.d.ts +49 -0
  190. package/types/lib/asp/crypto.d.ts +43 -0
  191. package/types/lib/asp/errors.d.ts +30 -0
  192. package/types/lib/asp/nonce-cache.d.ts +52 -0
  193. package/types/lib/asp/plugin.d.ts +30 -0
  194. package/types/lib/asp/ws-terminator.d.ts +45 -0
  195. package/types/lib/env-mapper.d.ts +14 -0
  196. package/types/lib/hub-protocol.d.ts +106 -0
  197. package/types/lib/index.d.ts +22 -0
  198. package/types/lib/logger/telegram-core.d.ts +104 -0
  199. package/types/lib/logger/telegram-transport.d.ts +45 -0
  200. package/types/lib/mega-brute-force.d.ts +66 -0
  201. package/types/lib/mega-circuit-breaker.d.ts +241 -0
  202. package/types/lib/mega-cron.d.ts +66 -0
  203. package/types/lib/mega-hash.d.ts +32 -0
  204. package/types/lib/mega-health.d.ts +41 -0
  205. package/types/lib/mega-job-queue.d.ts +176 -0
  206. package/types/lib/mega-job-worker.d.ts +130 -0
  207. package/types/lib/mega-job.d.ts +138 -0
  208. package/types/lib/mega-logger.d.ts +45 -0
  209. package/types/lib/mega-metrics.d.ts +285 -0
  210. package/types/lib/mega-plugin.d.ts +245 -0
  211. package/types/lib/mega-retry.d.ts +85 -0
  212. package/types/lib/mega-schedule.d.ts +260 -0
  213. package/types/lib/mega-shutdown.d.ts +135 -0
  214. package/types/lib/mega-tracing.d.ts +224 -0
  215. package/types/lib/mega-worker.d.ts +127 -0
  216. package/types/lib/otel-resource.d.ts +16 -0
  217. package/types/lib/worker-runner/process-entry.d.ts +1 -0
  218. package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
  219. package/types/lib/worker-runner/thread-entry.d.ts +1 -0
  220. package/types/lib/ws-hub.d.ts +234 -0
  221. package/types/models/crud-sql-builder.d.ts +48 -0
  222. package/types/models/index.d.ts +1 -0
  223. package/types/models/mega-model.d.ts +138 -0
  224. package/types/models/model-crud.d.ts +82 -0
  225. package/types/models/mongo-crud.d.ts +59 -0
  226. package/types/test/index.d.ts +84 -0
  227. package/.env +0 -127
  228. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
  229. package/sample/crud/apps/main/models/note.js +0 -71
  230. package/sample/crud/apps/main/models/user.js +0 -86
  231. package/sample/crud/package-lock.json +0 -5665
  232. package/sample/crud/yarn.lock +0 -2142
  233. package/sample/simple/package-lock.json +0 -1851
@@ -0,0 +1,456 @@
1
+ // @ts-check
2
+ /**
3
+ * 스키마 diff 엔진 (ADR-204) — 직전 snapshot 의 모델 record 와 현재 record 를 비교해
4
+ * 변경 연산(op) 목록을 산출한다. SQL 렌더는 dialect({@link module:core/migration/dialects/postgres})
5
+ * 책임 — 본 모듈은 dialect 비의존 연산만 만든다(인덱스 이름 계산만 주입식).
6
+ *
7
+ * # rename 감지 (자동 추론 금지 — P1)
8
+ * 같은 테이블에서 컬럼 drop + add 가 동시에 보이면 rename 인지 기계적으로 알 수 없다(이름만으로
9
+ * 구분 불가 — drop+add 로 처리하면 데이터 손실). 그래서 두 단계로 푼다:
10
+ * 1) `diffModels(...)` 가 `renameCandidates` 를 함께 반환 — 호출자(CLI)가 사용자에게 확인
11
+ * (interactive prompt 또는 `--renames "old:new"`).
12
+ * 2) 확정 매핑을 `renames` 로 다시 넘기면 drop+add 쌍이 `renameColumn` 으로 합성되고,
13
+ * rename 후 정의 차이(타입·제약)는 추가 op 로 이어진다.
14
+ *
15
+ * # op 순서 (up 기준 — down 은 dialect 가 역순 렌더)
16
+ * 제약/인덱스 해제 → 컬럼 drop → 테이블 drop → 테이블 create → rename → 컬럼 변경 →
17
+ * 컬럼 add → 제약 add(FK 는 모든 테이블 생성 뒤) → 인덱스 add. FK 를 createTable 과 분리해
18
+ * 같은 마이그레이션 안의 상호 참조 테이블 생성 순서 문제를 제거한다.
19
+ *
20
+ * @module core/migration/differ
21
+ */
22
+ import { MegaConfigError } from '../../errors/config-error.js'
23
+ import * as postgresDialect from './dialects/postgres.js'
24
+
25
+ /** up 적용 순서 phase — 숫자가 작을수록 먼저. */
26
+ const PHASE = {
27
+ dropIndex: 0,
28
+ dropFk: 1,
29
+ dropCheck: 2,
30
+ dropUnique: 2,
31
+ dropPk: 2,
32
+ dropColumn: 3,
33
+ dropTable: 4,
34
+ createTable: 5,
35
+ renameColumn: 6,
36
+ renameFk: 6,
37
+ alterType: 7,
38
+ setNotNull: 7,
39
+ dropNotNull: 7,
40
+ setDefault: 7,
41
+ dropDefault: 7,
42
+ setComment: 7,
43
+ changePk: 7,
44
+ rebuildTable: 7,
45
+ addColumn: 8,
46
+ addPk: 9,
47
+ addUnique: 9,
48
+ addCheck: 9,
49
+ addFk: 10,
50
+ addIndex: 11,
51
+ }
52
+
53
+ /**
54
+ * `dependsOnRebuild` dialect(sqlite)에서 테이블 재생성이 필요한 변경 종류 — sqlite 의 ALTER 가
55
+ * 표현하지 못하는 연산들. 이 중 하나라도 있으면 그 테이블의 모든 op 를 `rebuildTable` 1개로
56
+ * 수렴한다(재생성이 to-스키마 전체·인덱스를 재구성하므로 부분 op 는 무의미).
57
+ */
58
+ const REBUILD_TRIGGER_KINDS = new Set([
59
+ 'alterType', 'setNotNull', 'dropNotNull', 'setDefault', 'dropDefault',
60
+ 'addCheck', 'dropCheck', 'addFk', 'dropFk', 'renameFk', 'addPk', 'dropPk', 'changePk', 'dropColumn',
61
+ ])
62
+
63
+ /** @param {unknown} a @param {unknown} b @returns {boolean} 구조 동등 비교(JSON 직렬화 기준 — record 는 plain). */
64
+ function isEqual(a, b) {
65
+ return JSON.stringify(a ?? null) === JSON.stringify(b ?? null)
66
+ }
67
+
68
+ /**
69
+ * 컬럼 def 의 "타입 시그니처" — 타입 식별 필드만 (제약 제외). enum values 의 소속은 dialect 의
70
+ * `enumStrategy` 가 정한다: 'check'(postgres/sqlite)면 CHECK 제약(값 변경 = 제약 교체),
71
+ * 'enum-type'(maria 의 native ENUM)이면 타입의 일부(값 변경 = MODIFY COLUMN = alterType).
72
+ * @param {Record<string, any>} def @param {Record<string, any>} dialect @returns {any}
73
+ */
74
+ function typeSignature(def, dialect) {
75
+ return {
76
+ type: def.type,
77
+ length: def.length ?? null,
78
+ precision: def.precision ?? null,
79
+ scale: def.scale ?? null,
80
+ values: dialect.enumStrategy === 'enum-type' && def.type === 'enum' ? def.values : null,
81
+ // mongo 의 중첩 구조(object shape / array items)는 타입의 일부 — 변경이 op 로 잡혀야
82
+ // validator 교체가 생성된다. SQL dialect 에선 항상 null(영향 0).
83
+ shape: def.shape ?? null,
84
+ items: def.items ?? null,
85
+ uniqueItems: def.uniqueItems ?? null,
86
+ }
87
+ }
88
+
89
+ /**
90
+ * 컬럼의 CHECK 제약(명시 check + enum 값 제약) 정규화 — enum 은 values 를 그대로 둔다.
91
+ * **SQL 식은 만들지 않는다** — 렌더(식별자 인용 포함)는 전적으로 dialect 책임이라, op 에는
92
+ * expr(명시 check 의 raw 식) 또는 values(enum)만 싣는다. enum-type 전략 dialect 에선 enum 이
93
+ * 타입 소속이라 CHECK 로 취급하지 않는다.
94
+ * @param {string} table @param {string} col @param {Record<string, any>} def
95
+ * @param {Record<string, any>} dialect
96
+ * @returns {{ name: string, expr: string } | { name: string, values: string[] } | null}
97
+ */
98
+ function checkOf(table, col, def, dialect) {
99
+ if (def.check !== undefined) return { name: def.check.name ?? dialect.checkName(table, col), expr: def.check.expr }
100
+ if (def.type === 'enum' && dialect.enumStrategy !== 'enum-type') return { name: def.enumName ?? dialect.checkName(table, col), values: def.values }
101
+ return null
102
+ }
103
+
104
+ /**
105
+ * UNIQUE 제약 정규화 — { name } | null.
106
+ * @param {string} table @param {string} col @param {Record<string, any>} def
107
+ * @param {Record<string, any>} dialect
108
+ * @returns {{ name: string } | null}
109
+ */
110
+ function uniqueOf(table, col, def, dialect) {
111
+ if (def.unique === undefined) return null
112
+ return { name: def.unique === true ? dialect.uniqueName(table, col) : def.unique.name }
113
+ }
114
+
115
+ /**
116
+ * PK 정규화 — 복합(t.primary)=명시 명명(pkName), CREATE TABLE 인라인 단일 .primary() 는
117
+ * 서버 자동 명명(inlinePkName — dialect 별 상이).
118
+ * @param {Record<string, any>} record
119
+ * @param {Record<string, any>} dialect
120
+ * @returns {{ columns: string[], name: string } | null}
121
+ */
122
+ function pkOf(record, dialect) {
123
+ if (record.primaryKey !== undefined) return { columns: record.primaryKey, name: dialect.pkName(record.table) }
124
+ const single = Object.entries(record.columns).find(([, def]) => def.primary === true)
125
+ if (single !== undefined) return { columns: [single[0]], name: dialect.inlinePkName(record.table) }
126
+ return null
127
+ }
128
+
129
+ /** SERIAL 계열 — 타입이 아니라 SEQUENCE+DEFAULT 합성이라 ALTER TYPE 전환이 불가능하다. */
130
+ const SERIAL_TYPES = new Set(['serial', 'bigSerial'])
131
+
132
+ /**
133
+ * FK 의 실 DB 제약 이름 확정 — 명시 name 우선, 없으면 **그 시점의 컬럼명**으로 합성한 기본 이름.
134
+ * prev 상태의 FK 를 드롭할 때 반드시 prev 컬럼명 기준 이름을 써야 한다(rename 이후 드리프트 방지).
135
+ * @param {string} table @param {string} col @param {Record<string, any>} ref
136
+ * @param {Record<string, any>} dialect
137
+ * @returns {Record<string, any>} name 이 확정된 ref 사본.
138
+ */
139
+ function refWithName(table, col, ref, dialect) {
140
+ return { ...ref, name: ref.name ?? dialect.fkName(table, col, ref.table) }
141
+ }
142
+
143
+ /**
144
+ * 한 테이블(모델) 내부의 컬럼·인덱스·PK diff → ops 누적.
145
+ * @param {any} prev - 이전 record. @param {any} curr - 현재 record.
146
+ * @param {Record<string, string>} tableRenames - 이 테이블의 old→new 컬럼 매핑.
147
+ * @param {Array<Record<string, any>>} ops - 누적 대상.
148
+ * @param {Array<{ table: string, dropped: string[], added: string[] }>} candidates - rename 후보 누적.
149
+ * @param {Record<string, any>} dialect - 명명·렌더 능력의 정본(dialects/README.md contract).
150
+ */
151
+ function diffTable(prev, curr, tableRenames, ops, candidates, dialect) {
152
+ const table = curr.table
153
+ const prevCols = prev.columns
154
+ const currCols = curr.columns
155
+ const prevNames = Object.keys(prevCols)
156
+ const currNames = Object.keys(currCols)
157
+
158
+ /** @type {string[]} */
159
+ const dropped = prevNames.filter((n) => currCols[n] === undefined)
160
+ /** @type {string[]} */
161
+ const added = currNames.filter((n) => prevCols[n] === undefined)
162
+
163
+ // rename 합성 — 확정 매핑이 있는 쌍은 renameColumn + (정의 차이는 아래 공통 비교로).
164
+ /** @type {Array<[string, string]>} 확정된 [old, new]. */
165
+ const renamedPairs = []
166
+ for (const [oldName, newName] of Object.entries(tableRenames)) {
167
+ if (dropped.includes(oldName) && added.includes(newName)) {
168
+ renamedPairs.push([oldName, newName])
169
+ dropped.splice(dropped.indexOf(oldName), 1)
170
+ added.splice(added.indexOf(newName), 1)
171
+ ops.push({ kind: 'renameColumn', table, from: oldName, to: newName })
172
+ // FK 기본 이름(fk_<table>_<col>_<ref>) 동기화 — 컬럼만 rename 하면 실 DB 제약은 옛 이름으로
173
+ // 남아 이후 FK 변경 마이그레이션이 못 찾는다. 명시 name 이면 드리프트가 없어 불필요.
174
+ // ALTER 로 FK 를 못 다루는 dialect(sqlite — supportsAlterAddFk=false)는 FK 가 인라인 정의라
175
+ // RENAME COLUMN 시 스키마가 자동 갱신되고 이름 추적 자체가 없어 동기화가 불필요하다.
176
+ const pRef = prevCols[oldName].references
177
+ const cRef = currCols[newName].references
178
+ if (dialect.supportsAlterAddFk !== false && pRef !== undefined && cRef !== undefined && pRef.name === undefined && cRef.name === undefined) {
179
+ const from = dialect.fkName(table, oldName, pRef.table)
180
+ const to = dialect.fkName(table, newName, cRef.table)
181
+ if (from !== to && isEqual(pRef, cRef)) {
182
+ if (dialect.supportsRenameConstraint === true) {
183
+ ops.push({ kind: 'renameFk', table, column: newName, from, to })
184
+ } else {
185
+ // RENAME CONSTRAINT 미지원 dialect — DROP+ADD 로 동기화(contract 의 대체 패턴).
186
+ // dropFk 는 prev 컬럼명 — down 의 재-ADD 가 rename 원복 후 실행되기 때문(아래 drop-측 주석).
187
+ ops.push({ kind: 'dropFk', table, column: oldName, ref: { ...pRef, name: from } })
188
+ ops.push({ kind: 'addFk', table, column: newName, ref: { ...cRef, name: to } })
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ // 남은 drop+add 동시 발생은 rename 후보로 보고(호출자가 확인 후 재호출).
196
+ if (dropped.length > 0 && added.length > 0) {
197
+ candidates.push({ table, dropped: [...dropped], added: [...added] })
198
+ }
199
+
200
+ for (const name of dropped) {
201
+ // 컬럼 drop 은 부속 제약을 함께 제거(postgres 의존 객체 cascade) — 단 InnoDB(maria)는 FK 가
202
+ // 걸린 컬럼의 DROP 을 거부한다(1553 — FK 인덱스 요구). 그런 dialect 는 dropFk 를 선행 발행하고
203
+ // dropColumn 의 def 에서 references 를 떼어 down 의 FK 재-ADD 가 dropFk.down 과 중복되지 않게
204
+ // 한다(down 역순: dropColumn.down 의 ADD COLUMN → dropFk.down 의 ADD CONSTRAINT — 순서 정합).
205
+ let def = prevCols[name]
206
+ if (def.references !== undefined && dialect.requiresDropFkBeforeDropColumn === true) {
207
+ ops.push({ kind: 'dropFk', table, column: name, ref: refWithName(table, name, def.references, dialect) })
208
+ def = { ...def }
209
+ delete def.references
210
+ }
211
+ ops.push({ kind: 'dropColumn', table, column: name, def })
212
+ }
213
+ for (const name of added) {
214
+ ops.push({ kind: 'addColumn', table, column: name, def: currCols[name] })
215
+ // FK 를 ALTER 로 추가하는 dialect 만 별도 op — sqlite 는 ADD COLUMN 절에 인라인 REFERENCES.
216
+ if (currCols[name].references !== undefined && dialect.supportsAlterAddFk !== false) {
217
+ ops.push({ kind: 'addFk', table, column: name, ref: currCols[name].references })
218
+ }
219
+ }
220
+
221
+ // 공통 컬럼(+rename 쌍) 정의 비교.
222
+ /** @type {Array<[string, string]>} [prevName, currName]. */
223
+ const commonPairs = currNames.filter((n) => prevCols[n] !== undefined).map((n) => /** @type {[string, string]} */ ([n, n]))
224
+ commonPairs.push(...renamedPairs)
225
+ for (const [pn, cn] of commonPairs) {
226
+ const p = prevCols[pn]
227
+ const c = currCols[cn]
228
+
229
+ if (!isEqual(typeSignature(p, dialect), typeSignature(c, dialect))) {
230
+ // SERIAL 전환은 ALTER TYPE 으로 표현되지 않는다(시퀀스+DEFAULT 합성) — silent no-op SQL 을
231
+ // 만드느니 명시 거부하고 raw 마이그레이션을 안내한다.
232
+ if (SERIAL_TYPES.has(p.type) !== SERIAL_TYPES.has(c.type)) {
233
+ throw new MegaConfigError(
234
+ 'migration.unsupported_change',
235
+ `table '${table}' 컬럼 '${cn}': ${p.type} → ${c.type} 전환은 자동 생성이 불가능합니다 — SERIAL 은 타입이 아니라 ` +
236
+ 'SEQUENCE+DEFAULT 합성이라 ALTER TYPE 으로 표현되지 않습니다. SEQUENCE 생성/OWNED BY/DEFAULT 변경을 ' +
237
+ 'raw SQL 마이그레이션(mega generate migration)으로 작성하세요.',
238
+ { details: { table, column: cn, from: p.type, to: c.type } },
239
+ )
240
+ }
241
+ ops.push({ kind: 'alterType', table, column: cn, from: p, to: c })
242
+ }
243
+ // def 동반 — maria 의 NOT NULL 토글은 MODIFY COLUMN(전체 정의 필수)이라 대상 def 가 필요하다.
244
+ if ((p.notNull === true) !== (c.notNull === true)) {
245
+ ops.push({ kind: c.notNull === true ? 'setNotNull' : 'dropNotNull', table, column: cn, def: c })
246
+ }
247
+ if (!isEqual(p.default, c.default)) {
248
+ if (c.default === undefined) ops.push({ kind: 'dropDefault', table, column: cn, prev: p.default, def: c })
249
+ else ops.push({ kind: 'setDefault', table, column: cn, value: c.default, prev: p.default, def: c })
250
+ }
251
+
252
+ // drop-측 제약 op 는 **prev 컬럼명(pn)** 을 싣는다 — up 에서는 rename(phase 6) **전**(phase 1–2)에
253
+ // 이름으로 드롭하고, down 에서는 rename 원복 **후** 재-ADD 가 실행되므로 그 시점의 컬럼명은 pn 이다.
254
+ // cn 을 실으면 rename 동반 시 down 이 존재하지 않는 새 이름을 참조하는 invalid SQL 이 된다.
255
+ // add-측(phase 9–10)은 반대로 rename 이후/원복 이전에 실행돼 cn 이 정본.
256
+ const pu = uniqueOf(table, pn, p, dialect)
257
+ const cu = uniqueOf(table, cn, c, dialect)
258
+ if (!isEqual(pu, cu)) {
259
+ if (pu !== null) ops.push({ kind: 'dropUnique', table, column: pn, name: pu.name })
260
+ if (cu !== null) ops.push({ kind: 'addUnique', table, column: cn, name: cu.name })
261
+ }
262
+
263
+ const pc = checkOf(table, pn, p, dialect)
264
+ const cc = checkOf(table, cn, c, dialect)
265
+ if (!isEqual(pc, cc)) {
266
+ // expr/values 를 그대로 싣는다 — SQL 식 합성(식별자 인용 포함)은 dialect 의 renderOp 책임.
267
+ if (pc !== null) ops.push({ kind: 'dropCheck', table, column: pn, ...pc })
268
+ if (cc !== null) ops.push({ kind: 'addCheck', table, column: cn, ...cc })
269
+ }
270
+
271
+ if (!isEqual(p.references, c.references)) {
272
+ // prev FK 의 실 DB 이름은 prev 컬럼명 기준 — rename 동반 시에도 정확한 이름으로 드롭한다.
273
+ if (p.references !== undefined) ops.push({ kind: 'dropFk', table, column: pn, ref: refWithName(table, pn, p.references, dialect) })
274
+ if (c.references !== undefined) ops.push({ kind: 'addFk', table, column: cn, ref: c.references })
275
+ }
276
+
277
+ if (!isEqual(p.comment, c.comment)) {
278
+ ops.push({ kind: 'setComment', table, column: cn, comment: c.comment ?? null, prev: p.comment, def: c, prevDef: p })
279
+ }
280
+ }
281
+
282
+ // PK 변경 — 인라인 단일(서버 자동 명명) vs 명시 명명(pk_<table>) 출처를 정규화가 흡수.
283
+ // 교체(drop+add 동시)는 changePk 1개로 — InnoDB 는 FK 인덱스를 겸하는 PK 의 단독 DROP 을
284
+ // 거부하므로(errno 150) maria 가 한 문장(DROP PRIMARY KEY, ADD PRIMARY KEY)으로 묶어야 한다.
285
+ const ppk = pkOf(prev, dialect)
286
+ const cpk = pkOf(curr, dialect)
287
+ if (!isEqual(ppk, cpk)) {
288
+ if (ppk !== null && cpk !== null) {
289
+ ops.push({ kind: 'changePk', table, from: ppk, to: cpk })
290
+ } else if (ppk !== null) {
291
+ ops.push({ kind: 'dropPk', table, columns: ppk.columns, name: ppk.name })
292
+ } else if (cpk !== null) {
293
+ ops.push({ kind: 'addPk', table, columns: cpk.columns, name: cpk.name })
294
+ }
295
+ }
296
+
297
+ // 인덱스 — 유효 이름으로 식별, 같은 이름의 정의 변경은 drop+add.
298
+ /** @type {Map<string, any>} */
299
+ const prevIx = new Map((prev.indexes ?? []).map((/** @type {any} */ ix) => [dialect.resolveIndexName(table, ix), ix]))
300
+ /** @type {Map<string, any>} */
301
+ const currIx = new Map((curr.indexes ?? []).map((/** @type {any} */ ix) => [dialect.resolveIndexName(table, ix), ix]))
302
+ for (const [name, ix] of prevIx) {
303
+ const now = currIx.get(name)
304
+ if (now === undefined) ops.push({ kind: 'dropIndex', table, index: ix })
305
+ else if (!isEqual(ix, now)) {
306
+ ops.push({ kind: 'dropIndex', table, index: ix })
307
+ ops.push({ kind: 'addIndex', table, index: now })
308
+ }
309
+ }
310
+ for (const [name, ix] of currIx) {
311
+ if (!prevIx.has(name)) ops.push({ kind: 'addIndex', table, index: ix })
312
+ }
313
+ }
314
+
315
+ /**
316
+ * 한 adapter 그룹의 모델 목록 diff.
317
+ *
318
+ * @param {Array<{ name: string, table: string, record: any }>} prevModels - 직전 snapshot 의 모델.
319
+ * @param {Array<{ name: string, table: string, record: any }>} currModels - 현재 스캔 모델.
320
+ * @param {{ renames?: Record<string, Record<string, string>>, dialect?: Record<string, any> }} [opts]
321
+ * - renames: `{ [table]: { old: new } }` 확정 rename 매핑(미지정 시 drop+add 로 두고 후보만 보고).
322
+ * - dialect: 명명·능력 contract(기본 postgres — 호출자는 driver 별 dialect 를 주입).
323
+ * @returns {{ ops: Array<Record<string, any>>, renameCandidates: Array<{ table: string, dropped: string[], added: string[] }> }}
324
+ */
325
+ export function diffModels(prevModels, currModels, { renames = {}, dialect = postgresDialect } = {}) {
326
+ /** @type {Array<Record<string, any>>} */
327
+ const ops = []
328
+ /** @type {Array<{ table: string, dropped: string[], added: string[] }>} */
329
+ const renameCandidates = []
330
+
331
+ const prevByName = new Map(prevModels.map((m) => [m.name, m]))
332
+ const currByName = new Map(currModels.map((m) => [m.name, m]))
333
+
334
+ /**
335
+ * 테이블 제거 — 보유 FK 의 dropFk 를 **선행 발행**(phase 1 < dropTable 4)한다. FK 로 얽힌
336
+ * 테이블들을 함께 제거할 때 DROP 순서(이름순)에 의존하면 피참조 테이블이 먼저 떨어져
337
+ * "cannot drop table … depend on it" 으로 실패한다 — FK 를 전부 끊고 나면 순서 무관.
338
+ * 상호/순환 참조도 같은 이유로 안전하다(위상 정렬 불요).
339
+ * ALTER 로 FK 를 못 다루는 dialect(sqlite)는 dropFk 자체가 불가능 — dialect 의 dropTable 렌더가
340
+ * 트랜잭션 내 `PRAGMA defer_foreign_keys` 로 순서 문제를 흡수한다.
341
+ * @param {{ table: string, record: any }} prev
342
+ */
343
+ const dropTableWithFks = (prev) => {
344
+ if (dialect.supportsAlterAddFk !== false) {
345
+ for (const [col, def] of Object.entries(prev.record.columns)) {
346
+ const ref = /** @type {any} */ (def).references
347
+ if (ref !== undefined) {
348
+ ops.push({ kind: 'dropFk', table: prev.table, column: col, ref: refWithName(prev.table, col, ref, dialect) })
349
+ }
350
+ }
351
+ }
352
+ ops.push({ kind: 'dropTable', table: prev.table, record: prev.record })
353
+ }
354
+
355
+ /**
356
+ * 테이블 생성 — FK 분리 가능 dialect 는 모든 createTable 뒤 phase 의 addFk 로(상호 참조 안전),
357
+ * sqlite 는 인라인 FK(스키마에 forward reference 허용 — 적용은 DML 시점)라 분리 op 없음.
358
+ * @param {{ table: string, record: any }} curr
359
+ */
360
+ const createTableWithFks = (curr) => {
361
+ ops.push({ kind: 'createTable', table: curr.table, record: curr.record })
362
+ if (dialect.supportsAlterAddFk !== false) {
363
+ for (const [col, def] of Object.entries(curr.record.columns)) {
364
+ if (/** @type {any} */ (def).references !== undefined) {
365
+ ops.push({ kind: 'addFk', table: curr.table, column: col, ref: /** @type {any} */ (def).references })
366
+ }
367
+ }
368
+ }
369
+ for (const ix of curr.record.indexes ?? []) {
370
+ ops.push({ kind: 'addIndex', table: curr.table, index: ix })
371
+ }
372
+ }
373
+
374
+ for (const [name, prev] of prevByName) {
375
+ if (!currByName.has(name)) {
376
+ dropTableWithFks(prev)
377
+ }
378
+ }
379
+ for (const [name, curr] of currByName) {
380
+ const prev = prevByName.get(name)
381
+ if (prev === undefined) {
382
+ createTableWithFks(curr)
383
+ continue
384
+ }
385
+ if (prev.table !== curr.table) {
386
+ // 테이블 이름 변경은 모델 단위 drop+create 로 본다(데이터 이전은 자동화 범위 밖 — escape hatch).
387
+ dropTableWithFks(prev)
388
+ createTableWithFks(curr)
389
+ continue
390
+ }
391
+ // 테이블 내부 diff — rebuild dialect(sqlite)는 ALTER 로 표현 못하는 변경이 하나라도 있으면
392
+ // 그 테이블의 모든 op 를 rebuildTable 1개로 수렴한다(12-step 재생성이 to-스키마 전체를 재구성).
393
+ /** @type {Array<Record<string, any>>} */
394
+ const tableOps = []
395
+ const tableRenames = renames[curr.table] ?? {}
396
+ diffTable(prev.record, curr.record, tableRenames, tableOps, renameCandidates, dialect)
397
+ // 수렴 트리거는 dialect 가 확장할 수 있다 — mongo 는 validator 가 통짜라 addColumn/renameColumn
398
+ // 등 모든 컬럼 수준 변경이 수렴 대상(rebuildTriggerKinds, ADR-209). 기본은 sqlite 집합.
399
+ const triggerKinds = dialect.rebuildTriggerKinds instanceof Set ? dialect.rebuildTriggerKinds : REBUILD_TRIGGER_KINDS
400
+ if (dialect.dependsOnRebuild === true && tableOps.some((op) => triggerKinds.has(op.kind))) {
401
+ ops.push({ kind: 'rebuildTable', table: curr.table, from: prev.record, to: curr.record, renames: tableRenames })
402
+ } else {
403
+ ops.push(...tableOps)
404
+ }
405
+ }
406
+
407
+ // phase 정렬(stable) — 같은 phase 안에서는 산출 순서 유지.
408
+ const phaseOf = (/** @type {any} */ op) => /** @type {Record<string, number>} */ (PHASE)[op.kind]
409
+ const sorted = ops.map((op, i) => ({ op, i })).sort((a, b) => (phaseOf(a.op) - phaseOf(b.op)) || (a.i - b.i)).map((x) => x.op)
410
+ return { ops: sorted, renameCandidates }
411
+ }
412
+
413
+ /**
414
+ * `--renames "old:new,old2:new2"` 문자열을 테이블별 매핑으로 펼친다 — 후보 테이블의 dropped/added
415
+ * 에 해당 쌍이 있을 때만 그 테이블에 적용(테이블 간 동명 컬럼 오적용 방지).
416
+ *
417
+ * **어떤 후보와도 매치되지 않는 쌍은 fail-fast** 한다(`migration.rename_unmatched`) — 오타를
418
+ * 조용히 버리면 사용자는 rename 을 명시했다고 믿는 채 무경고 DROP COLUMN(데이터 손실) 마이그레이션이
419
+ * 생성된다(P1 보호의 입구).
420
+ *
421
+ * @param {string} spec - `old:new[,old:new...]`.
422
+ * @param {Array<{ table: string, dropped: string[], added: string[] }>} candidates
423
+ * @returns {Record<string, Record<string, string>>}
424
+ * @throws {MegaConfigError} `migration.rename_invalid` - 형식 오류. `migration.rename_unmatched` - 후보 불일치.
425
+ */
426
+ export function parseRenamesSpec(spec, candidates) {
427
+ /** @type {Record<string, Record<string, string>>} */
428
+ const out = {}
429
+ for (const pair of spec.split(',').map((s) => s.trim()).filter((s) => s.length > 0)) {
430
+ const idx = pair.indexOf(':')
431
+ const oldName = idx === -1 ? '' : pair.slice(0, idx).trim()
432
+ const newName = idx === -1 ? '' : pair.slice(idx + 1).trim()
433
+ if (oldName.length === 0 || newName.length === 0) {
434
+ throw new MegaConfigError('migration.rename_invalid', `--renames 형식 오류: '${pair}' — "old:new[,old2:new2]" 형식이어야 합니다.`, {
435
+ details: { pair, spec },
436
+ })
437
+ }
438
+ let matched = false
439
+ for (const c of candidates) {
440
+ if (c.dropped.includes(oldName) && c.added.includes(newName)) {
441
+ out[c.table] = { ...(out[c.table] ?? {}), [oldName]: newName }
442
+ matched = true
443
+ }
444
+ }
445
+ if (!matched) {
446
+ throw new MegaConfigError(
447
+ 'migration.rename_unmatched',
448
+ `--renames 매핑 '${oldName}:${newName}' 이 어떤 rename 후보와도 맞지 않습니다. ` +
449
+ `후보: ${candidates.map((c) => `${c.table}(drop: [${c.dropped.join(', ')}], add: [${c.added.join(', ')}])`).join(' / ') || '(없음)'}. ` +
450
+ '매핑의 오타를 수정하거나, 정말 drop+add 의도라면 해당 쌍을 매핑에서 제거하세요.',
451
+ { details: { pair: `${oldName}:${newName}`, candidates } },
452
+ )
453
+ }
454
+ }
455
+ return out
456
+ }