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,496 @@
1
+ // @ts-check
2
+ /**
3
+ * MariaDB dialect (ADR-207) — `dialects/README.md` contract 구현. 실 MariaDB 11.8 검증 기준.
4
+ *
5
+ * postgres 와의 핵심 차이:
6
+ * - 식별자 = 백틱, 한도 64.
7
+ * - enum = **native ENUM 타입**(`enumStrategy: 'enum-type'`) — 값 변경은 CHECK 교체가 아니라
8
+ * `MODIFY COLUMN`(differ 가 alterType 으로 산출). 값 제거는 위배 데이터가 있으면 적용 실패.
9
+ * - `RENAME CONSTRAINT` **미지원**(11.8 실측 — 구문 에러) → `supportsRenameConstraint: false`,
10
+ * FK 이름 동기화는 differ 의 DROP+ADD 대체 패턴.
11
+ * - NOT NULL 토글·코멘트 변경은 `MODIFY COLUMN`(전체 정의 필수 — 생략 속성이 리셋되므로
12
+ * op 의 def 로 항상 전체 절을 재구성).
13
+ * - DDL 은 **암묵 COMMIT** — 러너의 트랜잭션 래핑이 DDL 을 롤백하지 못한다(ADR-190 의
14
+ * 마이그레이션 락 경고와 동일 맥락). 다단 변경 마이그레이션의 중간 실패는 부분 적용으로
15
+ * 남는다 — 생성 파일 헤더·러너 경고가 이를 명시한다.
16
+ * - 인덱스: `DROP INDEX <name> ON <table>`(ON 필수), 표현식/부분(WHERE) 인덱스 미지원(명시 거부 —
17
+ * virtual column + raw 마이그레이션 안내), 온라인 추가는 `ALTER TABLE … ADD INDEX …,
18
+ * ALGORITHM=INPLACE, LOCK=NONE`(--concurrent).
19
+ * - PRIMARY KEY 이름은 항상 'PRIMARY' — `pkName/inlinePkName` 이 그 현실을 반영(ADD/DROP 도
20
+ * 이름 없는 `ADD PRIMARY KEY`/`DROP PRIMARY KEY`).
21
+ * - timestamptz → DATETIME — timezone-aware 타입이 없어 UTC 저장 규약을 권장(ADR-207).
22
+ *
23
+ * @module core/migration/dialects/maria
24
+ */
25
+ import { MegaConfigError } from '../../../errors/config-error.js'
26
+
27
+ // ── dialect 표준 인터페이스 속성 ─────────────────────────────────────────────
28
+ /** MariaDB 식별자 한도 — **64자**(chars, utf8 멀티바이트 식별자도 자수 기준). contract 멤버명은
29
+ * 역사적으로 *MaxBytes 지만 maria 검증은 자수로 한다(비ASCII 과잉 거부 방지 — ADR-208 L-3). */
30
+ export const identifierMaxBytes = 64
31
+ /** 온라인 인덱스 추가 — `ALGORITHM=INPLACE, LOCK=NONE` (--concurrent 옵트인). */
32
+ export const supportsConcurrentIndex = true
33
+ /** enum 표현 전략 — native ENUM 타입(값 변경 = MODIFY COLUMN). */
34
+ export const enumStrategy = 'enum-type'
35
+ /** RENAME COLUMN 자체는 지원(10.5+)하나 DDL 암묵 COMMIT 이라 트랜잭션 내 원자성은 없다. */
36
+ export const supportsRenameInTx = false
37
+ /** DDL 암묵 COMMIT — DROP COLUMN 을 트랜잭션으로 롤백할 수 없다(ADR-190 기존 인지). */
38
+ export const canDropColumnInTx = false
39
+ /** ALTER 가 대부분의 변경을 표현 — 테이블 재생성 불요. */
40
+ export const dependsOnRebuild = false
41
+ /** `ALTER TABLE … RENAME CONSTRAINT` 미지원 — MariaDB 11.8 실측(구문 에러). DROP+ADD 로 대체. */
42
+ export const supportsRenameConstraint = false
43
+ /** ALTER 로 FK 추가/삭제 가능(`ADD CONSTRAINT … FOREIGN KEY` / `DROP CONSTRAINT` — 11.8 실측). */
44
+ export const supportsAlterAddFk = true
45
+ /** InnoDB 는 FK 가 걸린 컬럼의 DROP 을 거부(1553 — FK 인덱스 요구, 11.8 실측) — dropFk 선행 필수. */
46
+ export const requiresDropFkBeforeDropColumn = true
47
+ /** SQL DDL 렌더 dialect — 생성 파일이 `db.query(sql)` 계약을 쓴다(mongo 와 분기, ADR-209). */
48
+ export const usesSqlDdl = true
49
+
50
+ /**
51
+ * SQL 식별자 인용 — 백틱. 한도 초과는 fail-fast(절단 충돌 방지 — postgres 와 동일 정책).
52
+ * @param {string} name @returns {string}
53
+ * @throws {MegaConfigError} `migration.identifier_too_long`
54
+ */
55
+ export function quoteIdent(name) {
56
+ if ([...name].length > identifierMaxBytes) {
57
+ throw new MegaConfigError(
58
+ 'migration.identifier_too_long',
59
+ `식별자 '${name}' 가 MariaDB 한도(${identifierMaxBytes}자)를 초과합니다 — 이름을 줄이거나 {name} 으로 짧은 제약 이름을 명시하세요.`,
60
+ { details: { identifier: name, chars: [...name].length, max: identifierMaxBytes } },
61
+ )
62
+ }
63
+ return `\`${name.replaceAll('`', '``')}\``
64
+ }
65
+
66
+ /**
67
+ * 값 literal 인용 — MariaDB 는 기본 sql_mode 에서 백슬래시가 escape 문자라 `\` 도 이스케이프한다
68
+ * (postgres 의 standard_conforming_strings 와 다른 지점).
69
+ * @param {unknown} value @returns {string}
70
+ */
71
+ export function quoteLiteral(value) {
72
+ if (value === null) return 'NULL'
73
+ if (typeof value === 'number') {
74
+ if (!Number.isFinite(value)) {
75
+ throw new MegaConfigError('migration.schema_invalid', `default 값이 유한한 수가 아닙니다: ${value}`, { details: { value: String(value) } })
76
+ }
77
+ return String(value)
78
+ }
79
+ if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE'
80
+ if (typeof value === 'object' && value !== null && typeof (/** @type {any} */ (value).raw) === 'string') {
81
+ return /** @type {any} */ (value).raw
82
+ }
83
+ return `'${String(value).replaceAll('\\', '\\\\').replaceAll("'", "''")}'`
84
+ }
85
+
86
+ /**
87
+ * 컬럼 타입 → MariaDB SQL 타입.
88
+ * @param {Record<string, any>} def @returns {string}
89
+ */
90
+ export function typeSql(def) {
91
+ switch (def.type) {
92
+ case 'serial':
93
+ return 'INT AUTO_INCREMENT'
94
+ case 'bigSerial':
95
+ return 'BIGINT AUTO_INCREMENT'
96
+ case 'integer':
97
+ return 'INT'
98
+ case 'bigInteger':
99
+ return 'BIGINT'
100
+ case 'smallInteger':
101
+ return 'SMALLINT'
102
+ case 'real':
103
+ return 'FLOAT'
104
+ case 'doublePrecision':
105
+ return 'DOUBLE'
106
+ case 'decimal':
107
+ return `DECIMAL(${def.precision}, ${def.scale})`
108
+ case 'varchar':
109
+ return `VARCHAR(${def.length})`
110
+ case 'text':
111
+ return 'TEXT'
112
+ case 'char':
113
+ return `CHAR(${def.length})`
114
+ case 'boolean':
115
+ return 'TINYINT(1)'
116
+ case 'timestamp':
117
+ case 'timestamptz': // timezone-aware 타입 없음 — UTC 저장 규약 권장(ADR-207)
118
+ return 'DATETIME'
119
+ case 'date':
120
+ return 'DATE'
121
+ case 'time':
122
+ return 'TIME'
123
+ case 'uuid':
124
+ return 'CHAR(36)'
125
+ case 'json':
126
+ case 'jsonb': // MariaDB 는 JSON/JSONB 미구분(JSON = LONGTEXT alias + 검증)
127
+ return 'JSON'
128
+ case 'enum':
129
+ return `ENUM(${def.values.map((/** @type {string} */ v) => quoteLiteral(v)).join(', ')})`
130
+ case 'bytea':
131
+ return 'BLOB'
132
+ default:
133
+ throw new MegaConfigError('migration.schema_invalid', `maria dialect: 알 수 없는 타입 '${def.type}'.`, { details: { type: def.type } })
134
+ }
135
+ }
136
+
137
+ // ── 명명 표준 — postgres 와 동일 규칙(diff 안정성), PK 만 서버 현실('PRIMARY') 반영 ──────────
138
+ /** @param {string} table @param {string[]} cols @param {boolean} [unique] @returns {string} */
139
+ export function indexName(table, cols, unique = false) {
140
+ return `${unique ? 'uniq' : 'idx'}_${table}_${cols.join('_')}`
141
+ }
142
+ /** @param {string} table @param {Record<string, any>} ix @returns {string} */
143
+ export function resolveIndexName(table, ix) {
144
+ if (ix.name !== undefined) return ix.name
145
+ if (ix.expression !== undefined) {
146
+ throw new MegaConfigError('migration.schema_invalid', `표현식 인덱스는 name 이 필수입니다 (table '${table}').`, { details: { table } })
147
+ }
148
+ return indexName(table, ix.columns, ix.unique === true)
149
+ }
150
+ /** @param {string} table @param {string} col @returns {string} */
151
+ export function uniqueName(table, col) {
152
+ return `uniq_${table}_${col}`
153
+ }
154
+ /** @param {string} table @param {string} col @param {string} refTable @returns {string} */
155
+ export function fkName(table, col, refTable) {
156
+ return `fk_${table}_${col}_${refTable}`
157
+ }
158
+ /** @param {string} table @param {string} col @returns {string} */
159
+ export function checkName(table, col) {
160
+ return `chk_${table}_${col}`
161
+ }
162
+ /** MariaDB 의 PRIMARY KEY 이름은 항상 'PRIMARY' — 명시 이름을 줘도 서버가 무시한다. @returns {string} */
163
+ export function pkName() {
164
+ return 'PRIMARY'
165
+ }
166
+ /** @returns {string} */
167
+ export function inlinePkName() {
168
+ return 'PRIMARY'
169
+ }
170
+
171
+ /**
172
+ * enum 의 CHECK 식 — contract 필수 멤버. maria 는 enum-type 전략이라 diff 경로에서는 쓰이지
173
+ * 않지만, 명시 .check() 렌더와 동형 표면을 유지한다.
174
+ * @param {string} col @param {string[]} values @returns {string}
175
+ */
176
+ export function enumCheckExpr(col, values) {
177
+ return `${quoteIdent(col)} IN (${values.map((v) => quoteLiteral(v)).join(', ')})`
178
+ }
179
+
180
+ /**
181
+ * 컬럼 정의 절(타입 + NOT NULL + DEFAULT + COMMENT [+ PRIMARY KEY]) — CREATE TABLE/ADD/MODIFY 공용.
182
+ * MariaDB 의 MODIFY COLUMN 은 전체 정의를 요구(생략 속성 리셋)하므로 이 절이 정의의 단일 정본이다.
183
+ * 코멘트는 postgres 와 달리 컬럼 정의에 인라인.
184
+ * @param {string} name @param {Record<string, any>} def @param {{ inlinePrimary?: boolean }} [opts]
185
+ * @returns {string}
186
+ */
187
+ function columnClause(name, def, { inlinePrimary = true } = {}) {
188
+ let sql = `${quoteIdent(name)} ${typeSql(def)}`
189
+ if (def.notNull === true) sql += ' NOT NULL'
190
+ if (def.default !== undefined) sql += ` DEFAULT ${quoteLiteral(def.default)}`
191
+ if (def.comment !== undefined) sql += ` COMMENT ${quoteLiteral(def.comment)}`
192
+ if (inlinePrimary && def.primary === true) sql += ' PRIMARY KEY'
193
+ return sql
194
+ }
195
+
196
+ /**
197
+ * 한 컬럼의 부속 제약(UNIQUE/CHECK) ADD CONSTRAINT 문 — enum 은 타입이라 CHECK 없음.
198
+ * @param {string} table @param {string} col @param {Record<string, any>} def @returns {string[]}
199
+ */
200
+ function columnConstraintStatements(table, col, def) {
201
+ /** @type {string[]} */
202
+ const out = []
203
+ if (def.unique !== undefined) {
204
+ const name = def.unique === true ? uniqueName(table, col) : def.unique.name
205
+ out.push(`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(name)} UNIQUE IF NOT EXISTS (${quoteIdent(col)})`)
206
+ }
207
+ if (def.check !== undefined) {
208
+ const name = def.check.name ?? checkName(table, col)
209
+ out.push(`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(name)} CHECK (${def.check.expr})`)
210
+ }
211
+ return out
212
+ }
213
+
214
+ /** FK ADD/DROP 문 — DROP 은 `DROP CONSTRAINT`(11.8 실측 지원). */
215
+ function fkAddStatement(/** @type {string} */ table, /** @type {string} */ col, /** @type {Record<string, any>} */ ref) {
216
+ const name = ref.name ?? fkName(table, col, ref.table)
217
+ let sql = `ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(name)} FOREIGN KEY IF NOT EXISTS (${quoteIdent(col)}) REFERENCES ${quoteIdent(ref.table)} (${quoteIdent(ref.column)})`
218
+ if (ref.onDelete !== undefined) sql += ` ON DELETE ${ref.onDelete.toUpperCase()}`
219
+ if (ref.onUpdate !== undefined) sql += ` ON UPDATE ${ref.onUpdate.toUpperCase()}`
220
+ return sql
221
+ }
222
+ function fkDropStatement(/** @type {string} */ table, /** @type {string} */ col, /** @type {Record<string, any>} */ ref) {
223
+ return `ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT IF EXISTS ${quoteIdent(ref.name ?? fkName(table, col, ref.table))}`
224
+ }
225
+
226
+ /**
227
+ * 인덱스 생성 문 — 표현식/부분(WHERE) 인덱스는 MariaDB 미지원이라 명시 거부(silent 누락 금지).
228
+ * concurrent=true 면 온라인 DDL(`ALTER … ADD INDEX …, ALGORITHM=INPLACE, LOCK=NONE`).
229
+ * @param {string} table @param {Record<string, any>} ix @param {{ concurrent?: boolean }} [opts]
230
+ * @returns {string}
231
+ */
232
+ function indexCreateStatement(table, ix, { concurrent = false } = {}) {
233
+ if (ix.expression !== undefined) {
234
+ throw new MegaConfigError(
235
+ 'migration.dialect_feature_unsupported',
236
+ `MariaDB 는 표현식 인덱스를 지원하지 않습니다 (table '${table}') — virtual column + 인덱스를 raw 마이그레이션으로 작성하세요.`,
237
+ { details: { table, expression: ix.expression } },
238
+ )
239
+ }
240
+ if (ix.where !== undefined) {
241
+ throw new MegaConfigError(
242
+ 'migration.dialect_feature_unsupported',
243
+ `MariaDB 는 부분(WHERE) 인덱스를 지원하지 않습니다 (table '${table}') — 조건을 빼거나 raw 마이그레이션으로 대체하세요.`,
244
+ { details: { table, where: ix.where } },
245
+ )
246
+ }
247
+ const name = resolveIndexName(table, ix)
248
+ const cols = ix.columns.map(quoteIdent).join(', ')
249
+ const usingPart = ix.using !== undefined ? ` USING ${String(ix.using).toUpperCase()}` : ''
250
+ if (concurrent) {
251
+ return `ALTER TABLE ${quoteIdent(table)} ADD ${ix.unique === true ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS ${quoteIdent(name)}${usingPart} (${cols}), ALGORITHM=INPLACE, LOCK=NONE`
252
+ }
253
+ return `CREATE ${ix.unique === true ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS ${quoteIdent(name)}${usingPart} ON ${quoteIdent(table)} (${cols})`
254
+ }
255
+
256
+ /**
257
+ * CREATE TABLE 문(+부속 제약) — FK·인덱스 제외(별도 op). 코멘트는 컬럼 절 인라인.
258
+ * @param {Record<string, any>} record @returns {string[]}
259
+ */
260
+ function createTableStatements(record) {
261
+ const table = record.table
262
+ /** @type {string[]} */
263
+ const lines = []
264
+ for (const [name, def] of Object.entries(record.columns)) {
265
+ lines.push(' ' + columnClause(name, def))
266
+ }
267
+ if (record.primaryKey !== undefined) {
268
+ lines.push(` PRIMARY KEY (${record.primaryKey.map(quoteIdent).join(', ')})`)
269
+ }
270
+ /** @type {string[]} */
271
+ const out = [`CREATE TABLE ${quoteIdent(table)} (\n${lines.join(',\n')}\n)`]
272
+ for (const [name, def] of Object.entries(record.columns)) {
273
+ out.push(...columnConstraintStatements(table, name, def))
274
+ }
275
+ return out
276
+ }
277
+
278
+ /**
279
+ * 같은 타입의 길이/정밀도/enum 값 **축소** — 위배 데이터가 있으면 적용 실패·잘림 위험(경고 주석).
280
+ * enum 값 제거는 enum-type 전략에서 alterType 으로 오므로 여기서 함께 잡는다.
281
+ * @param {Record<string, any>} from @param {Record<string, any>} to @returns {boolean}
282
+ */
283
+ export function isNarrowing(from, to) {
284
+ if (from.type !== to.type) return false
285
+ if (typeof from.length === 'number' && typeof to.length === 'number' && to.length < from.length) return true
286
+ if (typeof from.precision === 'number' && typeof to.precision === 'number' && to.precision < from.precision) return true
287
+ if (typeof from.scale === 'number' && typeof to.scale === 'number' && to.scale < from.scale) return true
288
+ if (from.type === 'enum' && Array.isArray(from.values) && Array.isArray(to.values)) {
289
+ if (from.values.some((/** @type {string} */ v) => !to.values.includes(v))) return true
290
+ }
291
+ return false
292
+ }
293
+
294
+ /**
295
+ * 변경 연산(op) 1개 → `{ up: string[], down: string[] }`. op 종류·필드는 differ 가 정본.
296
+ * @param {Record<string, any>} op
297
+ * @returns {{ up: string[], down: string[] }}
298
+ */
299
+ export function renderOp(op) {
300
+ const table = op.table
301
+ const modify = (/** @type {string} */ col, /** @type {Record<string, any>} */ def, /** @type {string[]} */ notes = []) => {
302
+ let sql = `ALTER TABLE ${quoteIdent(table)} MODIFY COLUMN ${columnClause(col, def, { inlinePrimary: false })}`
303
+ for (const n of notes) sql = `${n}\n${sql}`
304
+ return sql
305
+ }
306
+ switch (op.kind) {
307
+ case 'createTable':
308
+ return { up: createTableStatements(op.record), down: [`DROP TABLE IF EXISTS ${quoteIdent(table)}`] }
309
+ case 'dropTable': {
310
+ const down = createTableStatements(op.record)
311
+ for (const ix of op.record.indexes ?? []) down.push(indexCreateStatement(table, ix))
312
+ return {
313
+ up: [
314
+ `-- 경고: 테이블과 모든 데이터가 영구 삭제됩니다 — down 으로 구조(제약·인덱스 포함)는 복원되지만 데이터는 복원되지 않습니다.\nDROP TABLE IF EXISTS ${quoteIdent(table)}`,
315
+ ],
316
+ down,
317
+ }
318
+ }
319
+ case 'addColumn': {
320
+ /** @type {string[]} */
321
+ const up = []
322
+ let add = `ALTER TABLE ${quoteIdent(table)} ADD COLUMN IF NOT EXISTS ${columnClause(op.column, op.def, { inlinePrimary: false })}`
323
+ if (op.def.notNull === true && op.def.default === undefined) {
324
+ add = `-- 경고: 기존 행이 있으면 NOT NULL 컬럼 추가가 실패하거나 암묵 기본값으로 채워질 수 있습니다 — DEFAULT 지정 권장.\n${add}`
325
+ }
326
+ up.push(add)
327
+ up.push(...columnConstraintStatements(table, op.column, op.def))
328
+ return {
329
+ up,
330
+ down: [`-- 경고: 컬럼 데이터가 영구 삭제됩니다.\nALTER TABLE ${quoteIdent(table)} DROP COLUMN IF EXISTS ${quoteIdent(op.column)}`],
331
+ }
332
+ }
333
+ case 'dropColumn': {
334
+ /** @type {string[]} */
335
+ const down = [
336
+ `-- 경고: 구조만 복원됩니다 — 삭제된 컬럼 데이터는 down 으로 복원되지 않습니다.\nALTER TABLE ${quoteIdent(table)} ADD COLUMN IF NOT EXISTS ${columnClause(op.column, op.def, { inlinePrimary: false })}`,
337
+ ]
338
+ down.push(...columnConstraintStatements(table, op.column, op.def))
339
+ if (op.def.references !== undefined) down.push(fkAddStatement(table, op.column, op.def.references))
340
+ return {
341
+ up: [`-- 경고: 컬럼 데이터가 영구 삭제됩니다.\nALTER TABLE ${quoteIdent(table)} DROP COLUMN IF EXISTS ${quoteIdent(op.column)}`],
342
+ down,
343
+ }
344
+ }
345
+ case 'renameColumn':
346
+ return {
347
+ up: [`ALTER TABLE ${quoteIdent(table)} RENAME COLUMN ${quoteIdent(op.from)} TO ${quoteIdent(op.to)}`],
348
+ down: [`ALTER TABLE ${quoteIdent(table)} RENAME COLUMN ${quoteIdent(op.to)} TO ${quoteIdent(op.from)}`],
349
+ }
350
+ case 'alterType': {
351
+ // MODIFY COLUMN 은 전체 정의 교체 — enum 값 변경(enum-type 전략)도 이 경로다.
352
+ /** @type {string[]} */
353
+ const upNotes = []
354
+ /** @type {string[]} */
355
+ const downNotes = []
356
+ if (isNarrowing(op.from, op.to)) {
357
+ upNotes.push('-- 경고: 길이/정밀도/enum 값 축소 — 위배 데이터가 있으면 적용이 실패하거나 잘림이 발생합니다. 적용 전 데이터 분포를 확인하세요.')
358
+ }
359
+ if (isNarrowing(op.to, op.from)) {
360
+ downNotes.push('-- 경고: 길이/정밀도/enum 값 축소 — 위배 데이터가 있으면 적용이 실패하거나 잘림이 발생합니다.')
361
+ }
362
+ return {
363
+ up: [modify(op.column, op.to, upNotes)],
364
+ down: [modify(op.column, op.from, downNotes)],
365
+ }
366
+ }
367
+ case 'setNotNull':
368
+ return { up: [modify(op.column, op.def)], down: [modify(op.column, { ...op.def, notNull: undefined })] }
369
+ case 'dropNotNull':
370
+ return { up: [modify(op.column, op.def)], down: [modify(op.column, { ...op.def, notNull: true })] }
371
+ case 'setDefault':
372
+ return {
373
+ up: [`ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} SET DEFAULT ${quoteLiteral(op.value)}`],
374
+ down: [
375
+ op.prev === undefined
376
+ ? `ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} DROP DEFAULT`
377
+ : `ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} SET DEFAULT ${quoteLiteral(op.prev)}`,
378
+ ],
379
+ }
380
+ case 'dropDefault':
381
+ return {
382
+ up: [`ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} DROP DEFAULT`],
383
+ down: [`ALTER TABLE ${quoteIdent(table)} ALTER COLUMN ${quoteIdent(op.column)} SET DEFAULT ${quoteLiteral(op.prev)}`],
384
+ }
385
+ case 'addUnique':
386
+ return {
387
+ up: [`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(op.name)} UNIQUE IF NOT EXISTS (${quoteIdent(op.column)})`],
388
+ down: [`ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT IF EXISTS ${quoteIdent(op.name)}`],
389
+ }
390
+ case 'dropUnique':
391
+ return {
392
+ up: [`ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT IF EXISTS ${quoteIdent(op.name)}`],
393
+ down: [`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(op.name)} UNIQUE IF NOT EXISTS (${quoteIdent(op.column)})`],
394
+ }
395
+ case 'addCheck': {
396
+ const expr = op.values !== undefined ? enumCheckExpr(op.column, op.values) : op.expr
397
+ return {
398
+ up: [`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(op.name)} CHECK (${expr})`],
399
+ down: [`ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT IF EXISTS ${quoteIdent(op.name)}`],
400
+ }
401
+ }
402
+ case 'dropCheck': {
403
+ const expr = op.values !== undefined ? enumCheckExpr(op.column, op.values) : op.expr
404
+ return {
405
+ up: [`ALTER TABLE ${quoteIdent(table)} DROP CONSTRAINT IF EXISTS ${quoteIdent(op.name)}`],
406
+ down: [`ALTER TABLE ${quoteIdent(table)} ADD CONSTRAINT ${quoteIdent(op.name)} CHECK (${expr})`],
407
+ }
408
+ }
409
+ case 'addFk':
410
+ return { up: [fkAddStatement(table, op.column, op.ref)], down: [fkDropStatement(table, op.column, op.ref)] }
411
+ case 'dropFk':
412
+ return { up: [fkDropStatement(table, op.column, op.ref)], down: [fkAddStatement(table, op.column, op.ref)] }
413
+ case 'addPk':
414
+ return {
415
+ up: [`ALTER TABLE ${quoteIdent(table)} ADD PRIMARY KEY (${op.columns.map(quoteIdent).join(', ')})`],
416
+ down: [`ALTER TABLE ${quoteIdent(table)} DROP PRIMARY KEY`],
417
+ }
418
+ case 'dropPk':
419
+ return {
420
+ up: [`ALTER TABLE ${quoteIdent(table)} DROP PRIMARY KEY`],
421
+ down: [`ALTER TABLE ${quoteIdent(table)} ADD PRIMARY KEY (${op.columns.map(quoteIdent).join(', ')})`],
422
+ }
423
+ // PK 교체 — **한 문장**(DROP PRIMARY KEY, ADD PRIMARY KEY)으로 묶는다(11.8 실측): InnoDB 는
424
+ // FK 인덱스를 겸하는 PK 의 단독 DROP 을 거부(errno 150)하므로 분리 2문은 적용 불가. 단문 결합은
425
+ // 새 PK 가 FK 컬럼을 선두로 유지하는 한 통과한다(아니면 InnoDB 가 명시 에러 — raw 로 인덱스 선행).
426
+ case 'changePk': {
427
+ const one = (/** @type {any} */ to) =>
428
+ `ALTER TABLE ${quoteIdent(table)} DROP PRIMARY KEY, ADD PRIMARY KEY (${to.columns.map(quoteIdent).join(', ')})`
429
+ return { up: [one(op.to)], down: [one(op.from)] }
430
+ }
431
+ case 'addIndex':
432
+ return {
433
+ up: [indexCreateStatement(table, op.index, { concurrent: op.concurrent === true })],
434
+ down: [`DROP INDEX IF EXISTS ${quoteIdent(resolveIndexName(table, op.index))} ON ${quoteIdent(table)}`],
435
+ }
436
+ case 'dropIndex':
437
+ return {
438
+ up: [`DROP INDEX IF EXISTS ${quoteIdent(resolveIndexName(table, op.index))} ON ${quoteIdent(table)}`],
439
+ down: [indexCreateStatement(table, op.index)],
440
+ }
441
+ case 'setComment':
442
+ // 코멘트는 컬럼 정의의 일부 — MODIFY 로 전체 정의를 재기술한다(op.def/prevDef 가 정본).
443
+ return {
444
+ up: [modify(op.column, op.def)],
445
+ down: [modify(op.column, op.prevDef)],
446
+ }
447
+ case 'renameFk':
448
+ // supportsRenameConstraint=false 라 differ 가 발행하지 않는다 — 도달 시 contract 위반.
449
+ throw new MegaConfigError('migration.schema_invalid', `maria dialect: renameFk 는 미지원입니다(RENAME CONSTRAINT 부재 — DROP+ADD 로 산출돼야 함).`, {
450
+ details: { op },
451
+ })
452
+ default:
453
+ throw new MegaConfigError('migration.schema_invalid', `maria dialect: 알 수 없는 변경 연산 '${op.kind}'.`, { details: { kind: op.kind } })
454
+ }
455
+ }
456
+
457
+ /**
458
+ * 연산 목록 → `{ up, down }` (down 은 역순). DDL 암묵 COMMIT 특성상 다단 변경의 중간 실패는
459
+ * 부분 적용으로 남는다 — 러너 락(ADR-190)·생성 파일 헤더가 그 한계를 명시한다.
460
+ * @param {Array<Record<string, any>>} ops
461
+ * @returns {{ up: string[], down: string[] }}
462
+ */
463
+ export function renderOps(ops) {
464
+ /** @type {string[]} */
465
+ const up = []
466
+ /** @type {string[][]} */
467
+ const downs = []
468
+ for (const op of ops) {
469
+ const r = renderOp(op)
470
+ up.push(...r.up)
471
+ downs.push(r.down)
472
+ }
473
+ return { up, down: downs.reverse().flat() }
474
+ }
475
+
476
+ // ── DML facet (ADR-212) — CRUD(model-crud)용 DML 보조. maria 는 RETURNING 미지원. ──
477
+ export const paramStyle = 'positional'
478
+ /** maria 는 위치식 `?`(인덱스 무시). @returns {string} */
479
+ export function placeholder() {
480
+ return '?'
481
+ }
482
+ export const supportsReturning = false
483
+ export const supportsBulkReturning = false
484
+ /** SELECT native(rows 배열) → record[]. @param {any} res @returns {any[]} */
485
+ export function parseReadResult(res) {
486
+ return Array.isArray(res) ? res : []
487
+ }
488
+ /** write native(`{ affectedRows, insertId }`) → 정규화. @param {any} res */
489
+ export function parseWriteResult(res) {
490
+ return { count: typeof res?.affectedRows === 'number' ? res.affectedRows : 0, insertId: res?.insertId }
491
+ }
492
+ /** upsert 절 — `ON DUPLICATE KEY UPDATE col = VALUES(col)`. maria 는 conflict target 미사용(자동 unique 매칭). @param {{ updateCols: string[] }} o */
493
+ export function upsertClause({ updateCols }) {
494
+ const set = updateCols.map((c) => `${quoteIdent(c)} = VALUES(${quoteIdent(c)})`).join(', ')
495
+ return `ON DUPLICATE KEY UPDATE ${set}`
496
+ }