freelang-v4 4.3.0

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 (261) hide show
  1. package/README.md +548 -0
  2. package/dist/ast.d.ts +367 -0
  3. package/dist/ast.js +4 -0
  4. package/dist/ast.js.map +1 -0
  5. package/dist/async-basic.test.d.ts +1 -0
  6. package/dist/async-basic.test.js +88 -0
  7. package/dist/async-basic.test.js.map +1 -0
  8. package/dist/async-jest.test.d.ts +1 -0
  9. package/dist/async-jest.test.js +99 -0
  10. package/dist/async-jest.test.js.map +1 -0
  11. package/dist/channel-jest.test.d.ts +1 -0
  12. package/dist/channel-jest.test.js +148 -0
  13. package/dist/channel-jest.test.js.map +1 -0
  14. package/dist/checker-jest.test.d.ts +1 -0
  15. package/dist/checker-jest.test.js +160 -0
  16. package/dist/checker-jest.test.js.map +1 -0
  17. package/dist/checker.d.ts +149 -0
  18. package/dist/checker.js +1565 -0
  19. package/dist/checker.js.map +1 -0
  20. package/dist/checker.test.d.ts +1 -0
  21. package/dist/checker.test.js +217 -0
  22. package/dist/checker.test.js.map +1 -0
  23. package/dist/compiler-jest.test.d.ts +1 -0
  24. package/dist/compiler-jest.test.js +233 -0
  25. package/dist/compiler-jest.test.js.map +1 -0
  26. package/dist/compiler.d.ts +127 -0
  27. package/dist/compiler.js +1588 -0
  28. package/dist/compiler.js.map +1 -0
  29. package/dist/compiler.test.d.ts +1 -0
  30. package/dist/compiler.test.js +313 -0
  31. package/dist/compiler.test.js.map +1 -0
  32. package/dist/db-100m-full.d.ts +5 -0
  33. package/dist/db-100m-full.js +78 -0
  34. package/dist/db-100m-full.js.map +1 -0
  35. package/dist/db-100m-no-index.d.ts +12 -0
  36. package/dist/db-100m-no-index.js +119 -0
  37. package/dist/db-100m-no-index.js.map +1 -0
  38. package/dist/db-100m-real.d.ts +5 -0
  39. package/dist/db-100m-real.js +131 -0
  40. package/dist/db-100m-real.js.map +1 -0
  41. package/dist/db-100m-streaming.d.ts +15 -0
  42. package/dist/db-100m-streaming.js +164 -0
  43. package/dist/db-100m-streaming.js.map +1 -0
  44. package/dist/db-100m-test.d.ts +5 -0
  45. package/dist/db-100m-test.js +111 -0
  46. package/dist/db-100m-test.js.map +1 -0
  47. package/dist/db-jest.test.d.ts +1 -0
  48. package/dist/db-jest.test.js +182 -0
  49. package/dist/db-jest.test.js.map +1 -0
  50. package/dist/db-runtime.d.ts +24 -0
  51. package/dist/db-runtime.js +204 -0
  52. package/dist/db-runtime.js.map +1 -0
  53. package/dist/db.d.ts +249 -0
  54. package/dist/db.js +593 -0
  55. package/dist/db.js.map +1 -0
  56. package/dist/file-io-jest.test.d.ts +1 -0
  57. package/dist/file-io-jest.test.js +225 -0
  58. package/dist/file-io-jest.test.js.map +1 -0
  59. package/dist/for-of-jest.test.d.ts +1 -0
  60. package/dist/for-of-jest.test.js +230 -0
  61. package/dist/for-of-jest.test.js.map +1 -0
  62. package/dist/for-of.test.d.ts +1 -0
  63. package/dist/for-of.test.js +305 -0
  64. package/dist/for-of.test.js.map +1 -0
  65. package/dist/function-literal-jest.test.d.ts +1 -0
  66. package/dist/function-literal-jest.test.js +180 -0
  67. package/dist/function-literal-jest.test.js.map +1 -0
  68. package/dist/function-literal.test.d.ts +1 -0
  69. package/dist/function-literal.test.js +245 -0
  70. package/dist/function-literal.test.js.map +1 -0
  71. package/dist/generics-jest.test.d.ts +1 -0
  72. package/dist/generics-jest.test.js +93 -0
  73. package/dist/generics-jest.test.js.map +1 -0
  74. package/dist/ir-gen.d.ts +15 -0
  75. package/dist/ir-gen.js +400 -0
  76. package/dist/ir-gen.js.map +1 -0
  77. package/dist/ir.d.ts +114 -0
  78. package/dist/ir.js +5 -0
  79. package/dist/ir.js.map +1 -0
  80. package/dist/lexer.d.ts +110 -0
  81. package/dist/lexer.js +467 -0
  82. package/dist/lexer.js.map +1 -0
  83. package/dist/lexer.test.d.ts +1 -0
  84. package/dist/lexer.test.js +426 -0
  85. package/dist/lexer.test.js.map +1 -0
  86. package/dist/main.d.ts +2 -0
  87. package/dist/main.js +241 -0
  88. package/dist/main.js.map +1 -0
  89. package/dist/module-jest.test.d.ts +1 -0
  90. package/dist/module-jest.test.js +123 -0
  91. package/dist/module-jest.test.js.map +1 -0
  92. package/dist/parser.d.ts +56 -0
  93. package/dist/parser.js +1060 -0
  94. package/dist/parser.js.map +1 -0
  95. package/dist/parser.test.d.ts +1 -0
  96. package/dist/parser.test.js +461 -0
  97. package/dist/parser.test.js.map +1 -0
  98. package/dist/pattern-matching-jest.test.d.ts +1 -0
  99. package/dist/pattern-matching-jest.test.js +158 -0
  100. package/dist/pattern-matching-jest.test.js.map +1 -0
  101. package/dist/pkg/init.d.ts +1 -0
  102. package/dist/pkg/init.js +118 -0
  103. package/dist/pkg/init.js.map +1 -0
  104. package/dist/pkg/install.d.ts +1 -0
  105. package/dist/pkg/install.js +77 -0
  106. package/dist/pkg/install.js.map +1 -0
  107. package/dist/pkg/registry.d.ts +23 -0
  108. package/dist/pkg/registry.js +106 -0
  109. package/dist/pkg/registry.js.map +1 -0
  110. package/dist/pkg/run.d.ts +1 -0
  111. package/dist/pkg/run.js +76 -0
  112. package/dist/pkg/run.js.map +1 -0
  113. package/dist/pkg/toml.d.ts +5 -0
  114. package/dist/pkg/toml.js +117 -0
  115. package/dist/pkg/toml.js.map +1 -0
  116. package/dist/repl.d.ts +15 -0
  117. package/dist/repl.js +197 -0
  118. package/dist/repl.js.map +1 -0
  119. package/dist/runtime/bytecode.d.ts +92 -0
  120. package/dist/runtime/bytecode.js +253 -0
  121. package/dist/runtime/bytecode.js.map +1 -0
  122. package/dist/runtime/value.d.ts +102 -0
  123. package/dist/runtime/value.js +302 -0
  124. package/dist/runtime/value.js.map +1 -0
  125. package/dist/runtime/vm.d.ts +65 -0
  126. package/dist/runtime/vm.js +293 -0
  127. package/dist/runtime/vm.js.map +1 -0
  128. package/dist/struct-instance-jest.test.d.ts +1 -0
  129. package/dist/struct-instance-jest.test.js +209 -0
  130. package/dist/struct-instance-jest.test.js.map +1 -0
  131. package/dist/struct-instance.test.d.ts +1 -0
  132. package/dist/struct-instance.test.js +291 -0
  133. package/dist/struct-instance.test.js.map +1 -0
  134. package/dist/struct-jest.test.d.ts +1 -0
  135. package/dist/struct-jest.test.js +176 -0
  136. package/dist/struct-jest.test.js.map +1 -0
  137. package/dist/struct.test.d.ts +1 -0
  138. package/dist/struct.test.js +231 -0
  139. package/dist/struct.test.js.map +1 -0
  140. package/dist/trait-jest.test.d.ts +1 -0
  141. package/dist/trait-jest.test.js +120 -0
  142. package/dist/trait-jest.test.js.map +1 -0
  143. package/dist/vm-jest.test.d.ts +1 -0
  144. package/dist/vm-jest.test.js +569 -0
  145. package/dist/vm-jest.test.js.map +1 -0
  146. package/dist/vm.d.ts +81 -0
  147. package/dist/vm.js +1956 -0
  148. package/dist/vm.js.map +1 -0
  149. package/dist/vm.test.d.ts +1 -0
  150. package/dist/vm.test.js +337 -0
  151. package/dist/vm.test.js.map +1 -0
  152. package/dist/web-repl/sandbox.d.ts +11 -0
  153. package/dist/web-repl/sandbox.js +76 -0
  154. package/dist/web-repl/sandbox.js.map +1 -0
  155. package/dist/web-repl/server.d.ts +1 -0
  156. package/dist/web-repl/server.js +111 -0
  157. package/dist/web-repl/server.js.map +1 -0
  158. package/dist/while-loop-jest.test.d.ts +1 -0
  159. package/dist/while-loop-jest.test.js +201 -0
  160. package/dist/while-loop-jest.test.js.map +1 -0
  161. package/dist/while-loop.test.d.ts +1 -0
  162. package/dist/while-loop.test.js +262 -0
  163. package/dist/while-loop.test.js.map +1 -0
  164. package/docs/EXPERIENCE.md +787 -0
  165. package/docs/README.md +175 -0
  166. package/docs/V1_V2_V3_ANALYSIS.md +107 -0
  167. package/docs/_config.yml +36 -0
  168. package/docs/api-reference.md +459 -0
  169. package/docs/architecture.md +470 -0
  170. package/docs/benchmarks.md +295 -0
  171. package/docs/comparison.md +454 -0
  172. package/docs/index.md +335 -0
  173. package/docs/language-completeness.md +228 -0
  174. package/docs/learning-guide.md +651 -0
  175. package/package.json +65 -0
  176. package/src/api/deploy_key.fl +294 -0
  177. package/src/api/issue.fl +302 -0
  178. package/src/api/org.fl +356 -0
  179. package/src/api/repo.fl +394 -0
  180. package/src/api/team.fl +299 -0
  181. package/src/api/user.fl +385 -0
  182. package/src/api/webhook.fl +273 -0
  183. package/src/ast.ts +158 -0
  184. package/src/async-basic.test.ts +94 -0
  185. package/src/async-jest.test.ts +107 -0
  186. package/src/channel-jest.test.ts +158 -0
  187. package/src/checker-jest.test.ts +189 -0
  188. package/src/checker.test.ts +279 -0
  189. package/src/checker.ts +1861 -0
  190. package/src/commands/analyze.fl +227 -0
  191. package/src/commands/auth.fl +315 -0
  192. package/src/commands/batch.fl +349 -0
  193. package/src/commands/config.fl +199 -0
  194. package/src/commands/deploy_key.fl +352 -0
  195. package/src/commands/issue.fl +275 -0
  196. package/src/commands/main.fl +492 -0
  197. package/src/commands/org.fl +425 -0
  198. package/src/commands/repo.fl +581 -0
  199. package/src/commands/team.fl +244 -0
  200. package/src/commands/user.fl +423 -0
  201. package/src/commands/webhook.fl +400 -0
  202. package/src/compiler-jest.test.ts +275 -0
  203. package/src/compiler.test.ts +375 -0
  204. package/src/compiler.ts +1770 -0
  205. package/src/config.fl +175 -0
  206. package/src/core/batch.fl +355 -0
  207. package/src/core/cache.fl +284 -0
  208. package/src/core/ensure.fl +324 -0
  209. package/src/db-100m-full.ts +96 -0
  210. package/src/db-100m-no-index.ts +133 -0
  211. package/src/db-100m-real.ts +152 -0
  212. package/src/db-100m-streaming.ts +154 -0
  213. package/src/db-100m-test.ts +136 -0
  214. package/src/db-jest.test.ts +161 -0
  215. package/src/db-runtime.ts +242 -0
  216. package/src/db.ts +676 -0
  217. package/src/errors.fl +134 -0
  218. package/src/for-of-jest.test.ts +246 -0
  219. package/src/for-of.test.ts +308 -0
  220. package/src/function-literal-jest.test.ts +193 -0
  221. package/src/function-literal.test.ts +248 -0
  222. package/src/generics-jest.test.ts +104 -0
  223. package/src/http/client.fl +327 -0
  224. package/src/ir-gen.ts +459 -0
  225. package/src/ir.ts +80 -0
  226. package/src/lexer.test.ts +499 -0
  227. package/src/lexer.ts +522 -0
  228. package/src/main.ts +223 -0
  229. package/src/models.fl +162 -0
  230. package/src/module-jest.test.ts +145 -0
  231. package/src/parser.test.ts +542 -0
  232. package/src/parser.ts +1211 -0
  233. package/src/pattern-matching-jest.test.ts +170 -0
  234. package/src/pkg/init.ts +91 -0
  235. package/src/pkg/install.ts +56 -0
  236. package/src/pkg/registry.ts +103 -0
  237. package/src/pkg/run.ts +49 -0
  238. package/src/pkg/toml.ts +129 -0
  239. package/src/repl.ts +190 -0
  240. package/src/runtime/bytecode.ts +291 -0
  241. package/src/runtime/value.ts +322 -0
  242. package/src/runtime/vm.ts +354 -0
  243. package/src/self-host/bootstrap.fl +68 -0
  244. package/src/self-host/interpreter.fl +361 -0
  245. package/src/self-host/lexer-simple.fl +22 -0
  246. package/src/self-host/lexer.fl +305 -0
  247. package/src/self-host/parser.fl +580 -0
  248. package/src/struct-instance-jest.test.ts +221 -0
  249. package/src/struct-instance.test.ts +293 -0
  250. package/src/struct-jest.test.ts +187 -0
  251. package/src/struct.test.ts +234 -0
  252. package/src/trait-jest.test.ts +136 -0
  253. package/src/vm-jest.test.ts +754 -0
  254. package/src/vm.ts +1976 -0
  255. package/src/web-repl/public/index.html +50 -0
  256. package/src/web-repl/public/main.js +105 -0
  257. package/src/web-repl/public/style.css +225 -0
  258. package/src/web-repl/sandbox.ts +88 -0
  259. package/src/web-repl/server.ts +97 -0
  260. package/src/while-loop-jest.test.ts +218 -0
  261. package/src/while-loop.test.ts +267 -0
package/src/db.ts ADDED
@@ -0,0 +1,676 @@
1
+ // FreeLang v4.2 — 데이터베이스 지원 (sql.js 기반)
2
+ // SQLite, PostgreSQL, MySQL 인터페이스
3
+
4
+ import initSqlJs, { Database as SqlJsDatabase } from "sql.js";
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+
8
+ /**
9
+ * FreeLang Database API
10
+ * 모든 데이터베이스 작업을 통일된 인터페이스로 제공
11
+ */
12
+
13
+ export interface Row {
14
+ [key: string]: any;
15
+ }
16
+
17
+ export interface QueryResult {
18
+ rows: Row[];
19
+ changes: number;
20
+ }
21
+
22
+ export interface TransactionOptions {
23
+ isolation?: "deferred" | "immediate" | "exclusive";
24
+ }
25
+
26
+ /**
27
+ * 모든 DB 드라이버가 구현해야 하는 공통 인터페이스
28
+ */
29
+ export interface DBAdapter {
30
+ query(sql: string, params?: any[]): Promise<Row[]>;
31
+ execute(sql: string, params?: any[]): Promise<{ changes: number }>;
32
+ begin(isolation?: string): Promise<void>;
33
+ commit(): Promise<void>;
34
+ rollback(): Promise<void>;
35
+ close(): Promise<void>;
36
+ readonly driverName: string;
37
+ }
38
+
39
+ /**
40
+ * SQLite 데이터베이스 구현 (sql.js 기반)
41
+ */
42
+ export class SQLiteDB implements DBAdapter {
43
+ private db: SqlJsDatabase | null = null;
44
+ private filename: string;
45
+ private inTransaction: boolean = false;
46
+ private initialized: boolean = false;
47
+ readonly driverName = "sqlite";
48
+
49
+ constructor(filename: string) {
50
+ this.filename = filename;
51
+ }
52
+
53
+ /**
54
+ * 초기화 (비동기)
55
+ */
56
+ async init(): Promise<void> {
57
+ if (this.initialized) return;
58
+
59
+ try {
60
+ const SQL = await initSqlJs();
61
+
62
+ // 파일에서 로드하거나 새 DB 생성
63
+ if (fs.existsSync(this.filename)) {
64
+ const buffer = fs.readFileSync(this.filename);
65
+ this.db = new SQL.Database(buffer);
66
+ } else {
67
+ this.db = new SQL.Database();
68
+ }
69
+
70
+ // 외래 키 제약 활성화
71
+ this.db.run("PRAGMA foreign_keys = ON");
72
+
73
+ this.initialized = true;
74
+ } catch (e: any) {
75
+ throw new Error(`Database initialization failed: ${e.message}`);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 디스크에 저장
81
+ */
82
+ save(): void {
83
+ if (!this.db) return;
84
+
85
+ const data = this.db.export();
86
+ const buffer = Buffer.from(data);
87
+ fs.writeFileSync(this.filename, buffer);
88
+ }
89
+
90
+ /**
91
+ * SELECT 쿼리 실행
92
+ */
93
+ async query(sql: string, params: any[] = []): Promise<Row[]> {
94
+ if (!this.db) await this.init();
95
+ if (!this.db) throw new Error("Database not initialized");
96
+
97
+ try {
98
+ const stmt = this.db.prepare(sql);
99
+ stmt.bind(params);
100
+
101
+ const result: Row[] = [];
102
+
103
+ while (stmt.step()) {
104
+ const row = stmt.getAsObject() as Row;
105
+ result.push(row);
106
+ }
107
+
108
+ stmt.free();
109
+ return result;
110
+ } catch (e: any) {
111
+ throw new Error(`Query error: ${e.message}`);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * 단일 행 조회
117
+ */
118
+ async queryOne(sql: string, params: any[] = []): Promise<Row | null> {
119
+ const result = await this.query(sql, params);
120
+ return result.length > 0 ? result[0] : null;
121
+ }
122
+
123
+ /**
124
+ * INSERT/UPDATE/DELETE 실행
125
+ */
126
+ async execute(sql: string, params: any[] = []): Promise<{ changes: number }> {
127
+ if (!this.db) await this.init();
128
+ if (!this.db) throw new Error("Database not initialized");
129
+
130
+ try {
131
+ this.db.run(sql, params);
132
+ // 트랜잭션 중에는 저장하지 않음
133
+ if (!this.inTransaction) {
134
+ this.save();
135
+ }
136
+ return { changes: this.db.getRowsModified() };
137
+ } catch (e: any) {
138
+ throw new Error(`Execute error: ${e.message}`);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * 트랜잭션 실행
144
+ */
145
+ async transaction<T>(
146
+ callback: () => Promise<T> | T
147
+ ): Promise<T> {
148
+ if (!this.db) await this.init();
149
+ if (!this.db) throw new Error("Database not initialized");
150
+
151
+ try {
152
+ this.db.run("BEGIN");
153
+ this.inTransaction = true;
154
+
155
+ const result = await callback();
156
+
157
+ this.db.run("COMMIT");
158
+ this.inTransaction = false;
159
+ this.save();
160
+
161
+ return result;
162
+ } catch (e: any) {
163
+ this.db.run("ROLLBACK");
164
+ this.inTransaction = false;
165
+ throw new Error(`Transaction error: ${e.message}`);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * 트랜잭션 시작 (수동)
171
+ */
172
+ async begin(isolation: "deferred" | "immediate" | "exclusive" = "deferred"): Promise<void> {
173
+ if (!this.db) await this.init();
174
+ if (!this.db) throw new Error("Database not initialized");
175
+
176
+ const cmd = `BEGIN ${isolation.toUpperCase()}`;
177
+ this.db.run(cmd);
178
+ this.inTransaction = true;
179
+ }
180
+
181
+ /**
182
+ * 트랜잭션 커밋
183
+ */
184
+ async commit(): Promise<void> {
185
+ if (!this.db) throw new Error("Database not initialized");
186
+ if (!this.inTransaction) throw new Error("No active transaction");
187
+
188
+ this.db.run("COMMIT");
189
+ this.inTransaction = false;
190
+ this.save();
191
+ }
192
+
193
+ /**
194
+ * 트랜잭션 롤백
195
+ */
196
+ async rollback(): Promise<void> {
197
+ if (!this.db) throw new Error("Database not initialized");
198
+ if (!this.inTransaction) throw new Error("No active transaction");
199
+
200
+ this.db.run("ROLLBACK");
201
+ this.inTransaction = false;
202
+ }
203
+
204
+ /**
205
+ * 테이블 생성
206
+ */
207
+ async createTable(
208
+ tableName: string,
209
+ columns: { [key: string]: string }
210
+ ): Promise<void> {
211
+ const columnDefs = Object.entries(columns)
212
+ .map(([name, type]) => `${name} ${type}`)
213
+ .join(", ");
214
+
215
+ const sql = `CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefs})`;
216
+
217
+ try {
218
+ await this.execute(sql);
219
+ } catch (e: any) {
220
+ throw new Error(`Create table error: ${e.message}`);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * 테이블 삭제
226
+ */
227
+ async dropTable(tableName: string): Promise<void> {
228
+ try {
229
+ await this.execute(`DROP TABLE IF EXISTS ${tableName}`);
230
+ } catch (e: any) {
231
+ throw new Error(`Drop table error: ${e.message}`);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * 모든 테이블 목록
237
+ */
238
+ async listTables(): Promise<string[]> {
239
+ const result = await this.query(`
240
+ SELECT name FROM sqlite_master
241
+ WHERE type='table' AND name NOT LIKE 'sqlite_%'
242
+ `);
243
+ return result.map((row) => row.name);
244
+ }
245
+
246
+ /**
247
+ * 테이블 스키마 조회
248
+ */
249
+ async getTableSchema(tableName: string): Promise<any[]> {
250
+ const result = await this.query(`PRAGMA table_info(${tableName})`);
251
+ return result;
252
+ }
253
+
254
+ /**
255
+ * 데이터베이스 연결 종료
256
+ */
257
+ async close(): Promise<void> {
258
+ if (this.db) {
259
+ this.save();
260
+ this.db.close();
261
+ this.db = null;
262
+ }
263
+ }
264
+
265
+ /**
266
+ * 데이터베이스 정보
267
+ */
268
+ getInfo(): {
269
+ filename: string;
270
+ initialized: boolean;
271
+ inTransaction: boolean;
272
+ } {
273
+ return {
274
+ filename: this.filename,
275
+ initialized: this.initialized,
276
+ inTransaction: this.inTransaction,
277
+ };
278
+ }
279
+ }
280
+
281
+ /**
282
+ * 쿼리 빌더
283
+ */
284
+ export class QueryBuilder {
285
+ private table: string = "";
286
+ private whereConditions: string[] = [];
287
+ private orderByClause: string = "";
288
+ private limitValue: number = 0;
289
+ private offsetValue: number = 0;
290
+ private selectColumns: string[] = ["*"];
291
+ private joinClauses: string[] = [];
292
+ private params: any[] = [];
293
+
294
+ constructor(table: string) {
295
+ this.table = table;
296
+ }
297
+
298
+ select(...columns: string[]): QueryBuilder {
299
+ this.selectColumns = columns;
300
+ return this;
301
+ }
302
+
303
+ where(condition: string, ...params: any[]): QueryBuilder {
304
+ this.whereConditions.push(condition);
305
+ this.params.push(...params);
306
+ return this;
307
+ }
308
+
309
+ join(table: string, on: string): QueryBuilder {
310
+ this.joinClauses.push(`JOIN ${table} ON ${on}`);
311
+ return this;
312
+ }
313
+
314
+ orderBy(column: string, direction: "ASC" | "DESC" = "ASC"): QueryBuilder {
315
+ this.orderByClause = `ORDER BY ${column} ${direction}`;
316
+ return this;
317
+ }
318
+
319
+ limit(n: number): QueryBuilder {
320
+ this.limitValue = n;
321
+ return this;
322
+ }
323
+
324
+ offset(n: number): QueryBuilder {
325
+ this.offsetValue = n;
326
+ return this;
327
+ }
328
+
329
+ build(): { sql: string; params: any[] } {
330
+ let sql = `SELECT ${this.selectColumns.join(", ")} FROM ${this.table}`;
331
+
332
+ if (this.joinClauses.length > 0) {
333
+ sql += " " + this.joinClauses.join(" ");
334
+ }
335
+
336
+ if (this.whereConditions.length > 0) {
337
+ sql += " WHERE " + this.whereConditions.join(" AND ");
338
+ }
339
+
340
+ if (this.orderByClause) {
341
+ sql += " " + this.orderByClause;
342
+ }
343
+
344
+ if (this.limitValue > 0) {
345
+ sql += ` LIMIT ${this.limitValue}`;
346
+ }
347
+
348
+ if (this.offsetValue > 0) {
349
+ sql += ` OFFSET ${this.offsetValue}`;
350
+ }
351
+
352
+ return { sql, params: this.params };
353
+ }
354
+
355
+ async execute(db: SQLiteDB): Promise<Row[]> {
356
+ const { sql, params } = this.build();
357
+ return await db.query(sql, params);
358
+ }
359
+ }
360
+
361
+ /**
362
+ * 마이그레이션 관리자
363
+ */
364
+ export class MigrationManager {
365
+ private db: SQLiteDB;
366
+ private migrationsDir: string;
367
+
368
+ constructor(db: SQLiteDB, migrationsDir: string = "./migrations") {
369
+ this.db = db;
370
+ this.migrationsDir = migrationsDir;
371
+ }
372
+
373
+ private async init(): Promise<void> {
374
+ // 마이그레이션 메타테이블 생성
375
+ try {
376
+ await this.db.execute(`
377
+ CREATE TABLE IF NOT EXISTS _migrations (
378
+ id INTEGER PRIMARY KEY,
379
+ name TEXT UNIQUE NOT NULL,
380
+ applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
381
+ )
382
+ `);
383
+ } catch (e) {
384
+ // 이미 존재할 수 있음
385
+ }
386
+ }
387
+
388
+ /**
389
+ * 모든 마이그레이션 실행
390
+ */
391
+ async up(): Promise<void> {
392
+ await this.init();
393
+
394
+ if (!fs.existsSync(this.migrationsDir)) {
395
+ console.log("마이그레이션 디렉토리가 없습니다");
396
+ return;
397
+ }
398
+
399
+ const files = fs.readdirSync(this.migrationsDir).sort();
400
+
401
+ for (const file of files) {
402
+ if (!file.endsWith(".sql") || file.endsWith(".down.sql")) continue;
403
+
404
+ const applied = await this.db.queryOne(
405
+ "SELECT * FROM _migrations WHERE name = ?",
406
+ [file]
407
+ );
408
+
409
+ if (applied) {
410
+ console.log(`✓ 스킵: ${file}`);
411
+ continue;
412
+ }
413
+
414
+ const sql = fs.readFileSync(
415
+ path.join(this.migrationsDir, file),
416
+ "utf-8"
417
+ );
418
+
419
+ try {
420
+ // 여러 SQL 명령 실행
421
+ const statements = sql.split(";").filter((s) => s.trim());
422
+ for (const stmt of statements) {
423
+ if (stmt.trim()) {
424
+ await this.db.execute(stmt);
425
+ }
426
+ }
427
+
428
+ await this.db.execute(
429
+ "INSERT INTO _migrations (name) VALUES (?)",
430
+ [file]
431
+ );
432
+ console.log(`✓ 적용: ${file}`);
433
+ } catch (e: any) {
434
+ console.error(`✗ 실패: ${file} - ${e.message}`);
435
+ throw e;
436
+ }
437
+ }
438
+ }
439
+
440
+ /**
441
+ * 마이그레이션 이력 조회
442
+ */
443
+ async status(): Promise<Row[]> {
444
+ await this.init();
445
+ return await this.db.query(
446
+ "SELECT * FROM _migrations ORDER BY applied_at"
447
+ );
448
+ }
449
+
450
+ /**
451
+ * 마이그레이션 롤백 (역순 실행)
452
+ */
453
+ async down(): Promise<void> {
454
+ await this.init();
455
+
456
+ if (!fs.existsSync(this.migrationsDir)) {
457
+ console.log("마이그레이션 디렉토리가 없습니다");
458
+ return;
459
+ }
460
+
461
+ const files = fs.readdirSync(this.migrationsDir)
462
+ .filter(f => f.endsWith(".sql"))
463
+ .sort()
464
+ .reverse();
465
+
466
+ for (const file of files) {
467
+ const downFile = file.replace(".sql", ".down.sql");
468
+ const downPath = path.join(this.migrationsDir, downFile);
469
+
470
+ if (!fs.existsSync(downPath)) {
471
+ console.log(`⊘ 건너뜀: ${file} (롤백 파일 없음)`);
472
+ continue;
473
+ }
474
+
475
+ const applied = await this.db.queryOne(
476
+ "SELECT * FROM _migrations WHERE name = ?",
477
+ [file]
478
+ );
479
+
480
+ if (!applied) {
481
+ console.log(`⊘ 건너뜀: ${file} (미적용)`);
482
+ continue;
483
+ }
484
+
485
+ const sql = fs.readFileSync(downPath, "utf-8");
486
+
487
+ try {
488
+ const statements = sql.split(";").filter((s) => s.trim());
489
+ for (const stmt of statements) {
490
+ if (stmt.trim()) {
491
+ await this.db.execute(stmt);
492
+ }
493
+ }
494
+
495
+ await this.db.execute(
496
+ "DELETE FROM _migrations WHERE name = ?",
497
+ [file]
498
+ );
499
+ console.log(`✓ 롤백: ${file}`);
500
+ } catch (e: any) {
501
+ console.error(`✗ 실패: ${file} - ${e.message}`);
502
+ throw e;
503
+ }
504
+ }
505
+ }
506
+ }
507
+
508
+ /**
509
+ * PostgreSQL 데이터베이스 구현 (pg 드라이버 기반)
510
+ */
511
+ export class PostgreSQLDB implements DBAdapter {
512
+ private client: any = null;
513
+ private connected: boolean = false;
514
+ readonly driverName = "postgresql";
515
+
516
+ constructor(private config: {
517
+ host: string;
518
+ port: number;
519
+ user: string;
520
+ password: string;
521
+ database: string;
522
+ }) {}
523
+
524
+ /**
525
+ * 연결 초기화
526
+ */
527
+ async connect(): Promise<void> {
528
+ const { Client } = require("pg");
529
+ this.client = new Client(this.config);
530
+ await this.client.connect();
531
+ this.connected = true;
532
+ }
533
+
534
+ /**
535
+ * SQL 플레이스홀더 변환 (? → $1, $2, ...)
536
+ */
537
+ private convertPlaceholders(sql: string): string {
538
+ let i = 0;
539
+ return sql.replace(/\?/g, () => `$${++i}`);
540
+ }
541
+
542
+ /**
543
+ * SELECT 쿼리 실행
544
+ */
545
+ async query(sql: string, params: any[] = []): Promise<Row[]> {
546
+ if (!this.connected) throw new Error("Not connected to PostgreSQL");
547
+ const pgSql = this.convertPlaceholders(sql);
548
+ const result = await this.client.query(pgSql, params);
549
+ return result.rows;
550
+ }
551
+
552
+ /**
553
+ * INSERT/UPDATE/DELETE 실행
554
+ */
555
+ async execute(sql: string, params: any[] = []): Promise<{ changes: number }> {
556
+ if (!this.connected) throw new Error("Not connected to PostgreSQL");
557
+ const pgSql = this.convertPlaceholders(sql);
558
+ const result = await this.client.query(pgSql, params);
559
+ return { changes: result.rowCount ?? 0 };
560
+ }
561
+
562
+ /**
563
+ * 트랜잭션 시작
564
+ */
565
+ async begin(isolation: string = "deferred"): Promise<void> {
566
+ if (!this.connected) throw new Error("Not connected to PostgreSQL");
567
+ const cmd = isolation === "deferred" ? "BEGIN" : `BEGIN ${isolation.toUpperCase()}`;
568
+ await this.client.query(cmd);
569
+ }
570
+
571
+ /**
572
+ * 트랜잭션 커밋
573
+ */
574
+ async commit(): Promise<void> {
575
+ if (!this.connected) throw new Error("Not connected to PostgreSQL");
576
+ await this.client.query("COMMIT");
577
+ }
578
+
579
+ /**
580
+ * 트랜잭션 롤백
581
+ */
582
+ async rollback(): Promise<void> {
583
+ if (!this.connected) throw new Error("Not connected to PostgreSQL");
584
+ await this.client.query("ROLLBACK");
585
+ }
586
+
587
+ /**
588
+ * 연결 종료
589
+ */
590
+ async close(): Promise<void> {
591
+ if (this.client) {
592
+ await this.client.end();
593
+ this.client = null;
594
+ this.connected = false;
595
+ }
596
+ }
597
+ }
598
+
599
+ /**
600
+ * MySQL 데이터베이스 구현 (mysql2/promise 드라이버 기반)
601
+ */
602
+ export class MySQLDB implements DBAdapter {
603
+ private conn: any = null;
604
+ private connected: boolean = false;
605
+ readonly driverName = "mysql";
606
+
607
+ constructor(private config: {
608
+ host: string;
609
+ port: number;
610
+ user: string;
611
+ password: string;
612
+ database: string;
613
+ }) {}
614
+
615
+ /**
616
+ * 연결 초기화
617
+ */
618
+ async connect(): Promise<void> {
619
+ const mysql = require("mysql2/promise");
620
+ this.conn = await mysql.createConnection(this.config);
621
+ this.connected = true;
622
+ }
623
+
624
+ /**
625
+ * SELECT 쿼리 실행
626
+ */
627
+ async query(sql: string, params: any[] = []): Promise<Row[]> {
628
+ if (!this.connected) throw new Error("Not connected to MySQL");
629
+ const [rows] = await this.conn.query(sql, params);
630
+ return rows as Row[];
631
+ }
632
+
633
+ /**
634
+ * INSERT/UPDATE/DELETE 실행
635
+ */
636
+ async execute(sql: string, params: any[] = []): Promise<{ changes: number }> {
637
+ if (!this.connected) throw new Error("Not connected to MySQL");
638
+ const [result] = await this.conn.execute(sql, params) as any;
639
+ return { changes: result.affectedRows ?? 0 };
640
+ }
641
+
642
+ /**
643
+ * 트랜잭션 시작
644
+ */
645
+ async begin(isolation: string = "deferred"): Promise<void> {
646
+ if (!this.connected) throw new Error("Not connected to MySQL");
647
+ await this.conn.beginTransaction();
648
+ }
649
+
650
+ /**
651
+ * 트랜잭션 커밋
652
+ */
653
+ async commit(): Promise<void> {
654
+ if (!this.connected) throw new Error("Not connected to MySQL");
655
+ await this.conn.commit();
656
+ }
657
+
658
+ /**
659
+ * 트랜잭션 롤백
660
+ */
661
+ async rollback(): Promise<void> {
662
+ if (!this.connected) throw new Error("Not connected to MySQL");
663
+ await this.conn.rollback();
664
+ }
665
+
666
+ /**
667
+ * 연결 종료
668
+ */
669
+ async close(): Promise<void> {
670
+ if (this.conn) {
671
+ await this.conn.end();
672
+ this.conn = null;
673
+ this.connected = false;
674
+ }
675
+ }
676
+ }