@simplysm/orm-common 13.0.83 → 13.0.85

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 (73) hide show
  1. package/dist/ddl/initialize.d.ts +2 -2
  2. package/dist/ddl/initialize.js +1 -1
  3. package/dist/ddl/initialize.js.map +1 -1
  4. package/dist/ddl/table-ddl.d.ts +1 -1
  5. package/dist/exec/queryable.d.ts +115 -115
  6. package/dist/exec/queryable.js +68 -68
  7. package/dist/exec/queryable.js.map +1 -1
  8. package/dist/expr/expr.d.ts +248 -248
  9. package/dist/expr/expr.js +250 -250
  10. package/dist/query-builder/base/expr-renderer-base.d.ts +7 -7
  11. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +3 -3
  12. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
  13. package/dist/query-builder/mssql/mssql-expr-renderer.js +5 -5
  14. package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
  15. package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
  16. package/dist/query-builder/mssql/mssql-query-builder.js +7 -7
  17. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +2 -2
  18. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
  19. package/dist/query-builder/mysql/mysql-expr-renderer.js +4 -4
  20. package/dist/query-builder/mysql/mysql-query-builder.d.ts +10 -10
  21. package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
  22. package/dist/query-builder/mysql/mysql-query-builder.js +4 -4
  23. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +2 -2
  24. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
  25. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +4 -4
  26. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +8 -8
  27. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
  28. package/dist/query-builder/postgresql/postgresql-query-builder.js +7 -7
  29. package/dist/query-builder/query-builder.d.ts +1 -1
  30. package/dist/schema/factory/column-builder.d.ts +46 -46
  31. package/dist/schema/factory/column-builder.js +25 -25
  32. package/dist/schema/factory/index-builder.d.ts +22 -22
  33. package/dist/schema/factory/index-builder.js +14 -14
  34. package/dist/schema/factory/relation-builder.d.ts +93 -93
  35. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  36. package/dist/schema/factory/relation-builder.js +37 -37
  37. package/dist/schema/procedure-builder.d.ts +38 -38
  38. package/dist/schema/procedure-builder.d.ts.map +1 -1
  39. package/dist/schema/procedure-builder.js +26 -26
  40. package/dist/schema/table-builder.d.ts +38 -38
  41. package/dist/schema/table-builder.d.ts.map +1 -1
  42. package/dist/schema/table-builder.js +29 -29
  43. package/dist/schema/view-builder.d.ts +26 -26
  44. package/dist/schema/view-builder.d.ts.map +1 -1
  45. package/dist/schema/view-builder.js +18 -18
  46. package/dist/types/db.d.ts +40 -40
  47. package/dist/types/expr.d.ts +75 -75
  48. package/dist/types/expr.d.ts.map +1 -1
  49. package/dist/types/query-def.d.ts +32 -32
  50. package/dist/types/query-def.d.ts.map +1 -1
  51. package/package.json +2 -2
  52. package/src/ddl/initialize.ts +16 -16
  53. package/src/ddl/table-ddl.ts +1 -1
  54. package/src/exec/queryable.ts +163 -163
  55. package/src/expr/expr.ts +257 -257
  56. package/src/query-builder/base/expr-renderer-base.ts +8 -8
  57. package/src/query-builder/mssql/mssql-expr-renderer.ts +20 -20
  58. package/src/query-builder/mssql/mssql-query-builder.ts +28 -28
  59. package/src/query-builder/mysql/mysql-expr-renderer.ts +22 -22
  60. package/src/query-builder/mysql/mysql-query-builder.ts +65 -65
  61. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +15 -15
  62. package/src/query-builder/postgresql/postgresql-query-builder.ts +43 -43
  63. package/src/query-builder/query-builder.ts +1 -1
  64. package/src/schema/factory/column-builder.ts +48 -48
  65. package/src/schema/factory/index-builder.ts +22 -22
  66. package/src/schema/factory/relation-builder.ts +95 -95
  67. package/src/schema/procedure-builder.ts +38 -38
  68. package/src/schema/table-builder.ts +38 -38
  69. package/src/schema/view-builder.ts +28 -28
  70. package/src/types/db.ts +41 -41
  71. package/src/types/expr.ts +79 -79
  72. package/src/types/query-def.ts +37 -37
  73. package/tests/ddl/basic.expected.ts +8 -8
@@ -39,20 +39,20 @@ import { MysqlExprRenderer } from "./mysql-expr-renderer";
39
39
  /**
40
40
  * MySQL QueryBuilder
41
41
  *
42
- * MySQL 특이사항:
43
- * - OUTPUT 미지원: multi-statement 패턴으로 우회 (INSERT + SET @var + SELECT)
44
- * - INSERT OUTPUT: LAST_INSERT_ID() AI column 조회, 비-AI는 record에서 PK 추출
45
- * - UPDATE/UPSERT OUTPUT: WHERE condition이 변경될 있으므로 PK를 먼저 임시 Table에 저장 SELECT
46
- * - DELETE OUTPUT: Delete output columns 임시 Table에 저장
47
- * - switchFk: 전역 설정 (SET FOREIGN_KEY_CHECKS), Table 파라미터 무시됨
48
- * - FK Add Index automatic 생성됨
42
+ * MySQL specifics:
43
+ * - No OUTPUT support: workaround via multi-statement pattern (INSERT + SET @var + SELECT)
44
+ * - INSERT OUTPUT: uses LAST_INSERT_ID() for AI column, extracts PK from record for non-AI
45
+ * - UPDATE/UPSERT OUTPUT: saves PK to temp table first since WHERE condition may change after UPDATE, then SELECT
46
+ * - DELETE OUTPUT: saves output columns to temp table before delete
47
+ * - switchFk: global setting (SET FOREIGN_KEY_CHECKS), table parameter is ignored
48
+ * - Index is automatically created when adding FK
49
49
  */
50
50
  export class MysqlQueryBuilder extends QueryBuilderBase {
51
51
  protected expr = new MysqlExprRenderer((def) => this.select(def).sql);
52
52
 
53
- //#region ========== 유틸리티 ==========
53
+ //#region ========== Utilities ==========
54
54
 
55
- /** Table명 Render (MySQL: schema 무시, database.table 사용) */
55
+ /** Render table name (MySQL: ignores schema, uses database.table only) */
56
56
  protected tableName(obj: QueryDefObjectName): string {
57
57
  if (obj.database != null) {
58
58
  return `${this.expr.wrap(obj.database)}.${this.expr.wrap(obj.name)}`;
@@ -60,7 +60,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
60
60
  return this.expr.wrap(obj.name);
61
61
  }
62
62
 
63
- /** LIMIT Render */
63
+ /** Render LIMIT clause */
64
64
  protected renderLimit(limit: [number, number] | undefined, top: number | undefined): string {
65
65
  if (limit != null) {
66
66
  const [offset, count] = limit;
@@ -75,15 +75,15 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
75
75
  protected renderJoin(join: SelectQueryDefJoin): string {
76
76
  const alias = this.expr.wrap(join.as);
77
77
 
78
- // LATERAL JOIN 필요 여부 감지
78
+ // Detect if LATERAL JOIN is needed
79
79
  if (this.needsLateral(join)) {
80
- // from 배열(UNION ALL)이면 renderFrom(join.from),
81
- // (orderBy, top, select ) renderFrom(join)으로 Subquery Generate
80
+ // If from is an array (UNION ALL), use renderFrom(join.from),
81
+ // otherwise (orderBy, top, select, etc.) use renderFrom(join) to generate subquery
82
82
  const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
83
83
  return ` LEFT OUTER JOIN LATERAL ${from} AS ${alias} ON TRUE`;
84
84
  }
85
85
 
86
- // 일반 JOIN
86
+ // Normal JOIN
87
87
  const from = this.renderFrom(join.from);
88
88
  const where =
89
89
  join.where != null && join.where.length > 0
@@ -128,7 +128,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
128
128
 
129
129
  // LOCK
130
130
  if (def.lock) {
131
- // MySQL에서는 SELECT ... FOR UPDATE (마지막에 붙임)
131
+ // MySQL: SELECT ... FOR UPDATE (appended at the end)
132
132
  }
133
133
 
134
134
  // JOINs
@@ -171,7 +171,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
171
171
  const columns = Object.keys(def.records[0]);
172
172
  const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
173
173
 
174
- // OUTPUT 불필요: 단순 배치 INSERT
174
+ // No OUTPUT needed: simple batch INSERT
175
175
  if (def.output == null) {
176
176
  const valuesList = def.records.map((record) => {
177
177
  const values = columns.map((c) => this.expr.escapeValue(record[c]));
@@ -180,9 +180,9 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
180
180
  return { sql: `INSERT INTO ${table} (${colList}) VALUES ${valuesList.join(", ")}` };
181
181
  }
182
182
 
183
- // OUTPUT 필요: multi-statement로 INSERT + SELECT 실행
184
- // Result셋: [INSERT결과, SELECT결과, INSERT결과, SELECT결과, ...]
185
- // → resultSetIndex=1, resultSetStride=2 로 SELECT 결과만 추출
183
+ // OUTPUT needed: execute INSERT + SELECT via multi-statement
184
+ // Result sets: [INSERT result, SELECT result, INSERT result, SELECT result, ...]
185
+ // → Extract only SELECT results with resultSetIndex=1, resultSetStride=2
186
186
  const output = def.output;
187
187
  const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
188
188
  const statements: string[] = [];
@@ -191,7 +191,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
191
191
  const values = columns.map((c) => this.expr.escapeValue(record[c])).join(", ");
192
192
  statements.push(`INSERT INTO ${table} (${colList}) VALUES (${values})`);
193
193
 
194
- // PK SELECT (aiColName이면 LAST_INSERT_ID() 사용)
194
+ // SELECT by PK (uses LAST_INSERT_ID() for aiColName)
195
195
  const whereForSelect = output.pkColNames.map((pk) => {
196
196
  const wrappedPk = this.expr.wrap(pk);
197
197
  if (pk === output.aiColName) {
@@ -216,23 +216,23 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
216
216
  const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
217
217
  const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
218
218
 
219
- // existsSelectQuery SELECT 1 AS _ 형태로 Render
219
+ // Render existsSelectQuery as SELECT 1 AS _
220
220
  const existsQuerySql = this.select({
221
221
  ...def.existsSelectQuery,
222
222
  select: { _: { type: "value", value: 1 } },
223
223
  }).sql;
224
224
 
225
- // OUTPUT 불필요: 단순 INSERT IF NOT EXISTS
225
+ // No OUTPUT needed: simple INSERT IF NOT EXISTS
226
226
  if (def.output == null) {
227
227
  const sql = `INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
228
228
  return { sql };
229
229
  }
230
230
 
231
- // OUTPUT 필요: multi-statement (INSERT + SET @affected + SELECT)
231
+ // OUTPUT needed: multi-statement (INSERT + SET @affected + SELECT)
232
232
  const output = def.output;
233
233
  const outputCols = output.columns.map((c) => this.expr.wrap(c)).join(", ");
234
234
 
235
- // OUTPUT을 위한 SELECT WHERE condition
235
+ // SELECT WHERE condition for OUTPUT
236
236
  const whereForSelect = output.pkColNames.map((pk) => {
237
237
  const wrappedPk = this.expr.wrap(pk);
238
238
  if (pk === output.aiColName) {
@@ -241,14 +241,14 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
241
241
  return `${wrappedPk} = ${this.expr.escapeValue(def.record[pk])}`;
242
242
  });
243
243
 
244
- // multi-statement: INSERT → SET @affected → SELECT (삽입된 경우만 Result)
244
+ // multi-statement: INSERT → SET @affected → SELECT (result only if inserted)
245
245
  const statements = [
246
246
  `INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`,
247
247
  `SET @sd_affected = ROW_COUNT()`,
248
248
  `SELECT ${outputCols} FROM ${table} WHERE ${whereForSelect.join(" AND ")} AND @sd_affected > 0`,
249
249
  ];
250
250
 
251
- // results[0]=INSERT, results[1]=SET(빈결과), results[2]=SELECT
251
+ // results[0]=INSERT, results[1]=SET(empty result), results[2]=SELECT
252
252
  return { sql: statements.join(";\n"), resultSetIndex: 2 };
253
253
  }
254
254
 
@@ -256,7 +256,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
256
256
  const table = this.tableName(def.table);
257
257
  const selectSql = this.select(def.recordsSelectQuery).sql;
258
258
 
259
- // INSERT INTO SELECT에서 columns 추출
259
+ // Extract columns from INSERT INTO SELECT
260
260
  const selectDef = def.recordsSelectQuery;
261
261
  const colList =
262
262
  selectDef.select != null
@@ -265,15 +265,15 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
265
265
  .join(", ")
266
266
  : "*";
267
267
 
268
- // OUTPUT 불필요: 단순 INSERT INTO SELECT
268
+ // No OUTPUT needed: simple INSERT INTO SELECT
269
269
  if (def.output == null) {
270
270
  return { sql: `INSERT INTO ${table} (${colList}) ${selectSql}` };
271
271
  }
272
272
 
273
- // OUTPUT 필요: multi-statement
273
+ // OUTPUT needed: multi-statement
274
274
  const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
275
275
 
276
- // PK AI 때: LAST_INSERT_ID() + ROW_COUNT() range 조회
276
+ // When PK is AI: query range via LAST_INSERT_ID() + ROW_COUNT()
277
277
  if (def.output.aiColName != null) {
278
278
  const aiCol = this.expr.wrap(def.output.aiColName);
279
279
 
@@ -283,14 +283,14 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
283
283
  `SELECT ${outputCols} FROM ${table} WHERE ${aiCol} >= @sd_first_id AND ${aiCol} < @sd_first_id + @sd_count`,
284
284
  ];
285
285
 
286
- // results[0]=INSERT, results[1]=SET(빈결과), results[2]=SELECT
286
+ // results[0]=INSERT, results[1]=SET(empty result), results[2]=SELECT
287
287
  return { sql: statements.join(";\n"), resultSetIndex: 2 };
288
288
  }
289
289
 
290
- // PK AI 아님: 임시 Table로 PK 저장 조회
290
+ // PK is not AI: save PKs to temp table then query
291
291
  const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
292
292
 
293
- // recordsSelectQuery에서 PK column만 추출한 SELECT Generate
293
+ // Generate SELECT extracting only PK columns from recordsSelectQuery
294
294
  const pkSelectDef: SelectQueryDef = {
295
295
  ...def.recordsSelectQuery,
296
296
  select: Object.fromEntries(
@@ -299,7 +299,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
299
299
  };
300
300
  const pkSelectSql = this.select(pkSelectDef).sql;
301
301
 
302
- // 임시 Table의 PK target에서 SELECT
302
+ // SELECT from target using PK from temp table
303
303
  const pkConditions = def.output.pkColNames.map((pk) => {
304
304
  const wrappedPk = this.expr.wrap(pk);
305
305
  return `${table}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
@@ -329,7 +329,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
329
329
  ([col, expr]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(expr)}`,
330
330
  );
331
331
 
332
- // OUTPUT 불필요: 단순 UPDATE
332
+ // No OUTPUT needed: simple UPDATE
333
333
  if (def.output == null) {
334
334
  let sql = `UPDATE ${table} AS ${alias}`;
335
335
  sql += this.renderJoins(def.joins);
@@ -341,11 +341,11 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
341
341
  return { sql };
342
342
  }
343
343
 
344
- // OUTPUT 필요: multi-statement (임시table에 PK 저장 + UPDATE + SELECT + DROP)
344
+ // OUTPUT needed: multi-statement (save PK to temp table + UPDATE + SELECT + DROP)
345
345
  const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
346
346
  const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
347
347
 
348
- // UPDATE 대상 PK를 임시 Table에 저장 (UPDATE WHERE condition 달라질 있으므로)
348
+ // Save target PKs to temp table (since WHERE condition may change after UPDATE)
349
349
  const pkSelectCols = def.output.pkColNames
350
350
  .map((pk) => `${alias}.${this.expr.wrap(pk)} AS ${this.expr.wrap(pk)}`)
351
351
  .join(", ");
@@ -360,14 +360,14 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
360
360
  updateSql += this.renderWhere(def.where);
361
361
  if (def.top != null) updateSql += ` LIMIT ${def.top}`;
362
362
 
363
- // 임시 Table의 PK SELECT (변경된 value 조회)
363
+ // SELECT using PK from temp table (query updated values)
364
364
  const pkConditions = def.output.pkColNames.map((pk) => {
365
365
  const wrappedPk = this.expr.wrap(pk);
366
366
  return `${alias}.${wrappedPk} = ${tempTableName}.${wrappedPk}`;
367
367
  });
368
368
  const selectSql = `SELECT ${outputCols} FROM ${table} AS ${alias}, ${tempTableName} WHERE ${pkConditions.join(" AND ")}`;
369
369
 
370
- // 임시 Drop table
370
+ // Drop temp table
371
371
  const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
372
372
 
373
373
  const statements = [createTempSql, updateSql, selectSql, dropSql];
@@ -382,7 +382,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
382
382
  const table = this.tableName(def.table);
383
383
  const alias = this.expr.wrap(def.as);
384
384
 
385
- // OUTPUT 불필요: 단순 DELETE
385
+ // No OUTPUT needed: simple DELETE
386
386
  if (def.output == null) {
387
387
  let sql = `DELETE ${alias} FROM ${table} AS ${alias}`;
388
388
  sql += this.renderJoins(def.joins);
@@ -393,25 +393,25 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
393
393
  return { sql };
394
394
  }
395
395
 
396
- // OUTPUT 필요: multi-statement (Delete 임시table 저장 + DELETE + SELECT + DROP)
396
+ // OUTPUT needed: multi-statement (save to temp table before delete + DELETE + SELECT + DROP)
397
397
  const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
398
398
  const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
399
399
 
400
- // Delete 임시 Table에 저장
400
+ // Save to temp table before delete
401
401
  let createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${outputCols} FROM ${table} AS ${alias}`;
402
402
  createTempSql += this.renderJoins(def.joins);
403
403
  createTempSql += this.renderWhere(def.where);
404
404
 
405
- // DELETE 실행
405
+ // Execute DELETE
406
406
  let deleteSql = `DELETE ${alias} FROM ${table} AS ${alias}`;
407
407
  deleteSql += this.renderJoins(def.joins);
408
408
  deleteSql += this.renderWhere(def.where);
409
409
  if (def.top != null) deleteSql += ` LIMIT ${def.top}`;
410
410
 
411
- // 임시 Table에서 result return
411
+ // Return results from temp table
412
412
  const selectSql = `SELECT * FROM ${tempTableName}`;
413
413
 
414
- // 임시 Drop table
414
+ // Drop temp table
415
415
  const dropSql = `DROP TEMPORARY TABLE ${tempTableName}`;
416
416
 
417
417
  const statements = [createTempSql, deleteSql, selectSql, dropSql];
@@ -427,7 +427,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
427
427
  const alias = this.expr.wrap(def.existsSelectQuery.as);
428
428
  const existsQuerySql = this.select(def.existsSelectQuery).sql;
429
429
 
430
- // UPDATE SET part (alias.column 형태)
430
+ // UPDATE SET part (alias.column format)
431
431
  const updateSetParts = Object.entries(def.updateRecord).map(
432
432
  ([col, e]) => `${alias}.${this.expr.wrap(col)} = ${this.expr.render(e)}`,
433
433
  );
@@ -437,16 +437,16 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
437
437
  const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
438
438
  const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
439
439
 
440
- // WHERE condition 추출 (existsSelectQuery where)
440
+ // Extract WHERE condition (from existsSelectQuery's where)
441
441
  const whereCondition =
442
442
  def.existsSelectQuery.where != null && def.existsSelectQuery.where.length > 0
443
443
  ? this.expr.renderWhere(def.existsSelectQuery.where)
444
444
  : "1=1";
445
445
 
446
- // OUTPUT 불필요: multi-statement (UPDATE + INSERT WHERE NOT EXISTS)
446
+ // No OUTPUT needed: multi-statement (UPDATE + INSERT WHERE NOT EXISTS)
447
447
  if (def.output == null) {
448
- // UPDATE: 존재하면 UPDATE
449
- // INSERT SELECT WHERE NOT EXISTS: 존재 안하면 INSERT
448
+ // UPDATE: updates if exists
449
+ // INSERT SELECT WHERE NOT EXISTS: inserts if not exists
450
450
  const statements = [
451
451
  `UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`,
452
452
  `INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`,
@@ -454,22 +454,22 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
454
454
  return { sql: statements.join(";\n") };
455
455
  }
456
456
 
457
- // OUTPUT 필요: multi-statement (CREATE TEMP + UPDATE + INSERT + SELECT + DROP)
457
+ // OUTPUT needed: multi-statement (CREATE TEMP + UPDATE + INSERT + SELECT + DROP)
458
458
  const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
459
459
  const tempTableName = this.expr.wrap("SD_TEMP_" + Uuid.generate().toString().replace(/-/g, ""));
460
460
 
461
- // UPDATE 대상 PK를 임시 Table에 저장 (UPDATE WHERE condition 달라질 있으므로)
461
+ // Save target PKs to temp table (since WHERE condition may change after UPDATE)
462
462
  const pkSelectCols = def.output.pkColNames.map((pk) => this.expr.wrap(pk)).join(", ");
463
463
  const createTempSql = `CREATE TEMPORARY TABLE ${tempTableName} AS SELECT ${pkSelectCols} FROM ${table} AS ${alias} WHERE ${whereCondition}`;
464
464
 
465
- // UPDATE (존재하면 Update)
465
+ // UPDATE (update if exists)
466
466
  const updateSql = `UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")} WHERE ${whereCondition}`;
467
467
 
468
468
  // INSERT (NOT EXISTS Pattern)
469
469
  const insertSql = `INSERT INTO ${table} (${insertColList}) SELECT ${insertValues} WHERE NOT EXISTS (${existsQuerySql})`;
470
470
 
471
- // SELECT: UPDATE result 또는 INSERT result 조회 (UNION ALL로 합침)
472
- // UPDATE 케이스: temp Table의 PK 조회
471
+ // SELECT: query UPDATE result or INSERT result (merged with UNION ALL)
472
+ // UPDATE case: query by PK from temp table
473
473
  const output = def.output;
474
474
  const updatePkConditions = output.pkColNames.map((pk) => {
475
475
  const wrappedPk = this.expr.wrap(pk);
@@ -477,7 +477,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
477
477
  });
478
478
  const selectUpdateSql = `SELECT ${outputCols} FROM ${table} WHERE ${updatePkConditions.join(" AND ")}`;
479
479
 
480
- // INSERT 케이스: insertRecord의 PK 조회 (AI면 LAST_INSERT_ID(), 임시 Table이 비어있을 때만)
480
+ // INSERT case: query by PK from insertRecord (LAST_INSERT_ID() for AI, only when temp table is empty)
481
481
  const insertPkConditions = output.pkColNames.map((pk) => {
482
482
  const wrappedPk = this.expr.wrap(pk);
483
483
  if (pk === output.aiColName) {
@@ -544,7 +544,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
544
544
  }
545
545
 
546
546
  protected truncate(def: TruncateQueryDef): QueryBuildResult {
547
- // MySQL: TRUNCATE AUTO_INCREMENT automatic 리셋
547
+ // MySQL: TRUNCATE automatically resets AUTO_INCREMENT
548
548
  return { sql: `TRUNCATE TABLE ${this.tableName(def.table)}` };
549
549
  }
550
550
 
@@ -608,7 +608,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
608
608
 
609
609
  protected renameColumn(def: RenameColumnQueryDef): QueryBuildResult {
610
610
  const table = this.tableName(def.table);
611
- // MySQL 8.0+: RENAME COLUMN 지원
611
+ // MySQL 8.0+: RENAME COLUMN supported
612
612
  return {
613
613
  sql: `ALTER TABLE ${table} RENAME COLUMN ${this.expr.wrap(def.column)} TO ${this.expr.wrap(def.newName)}`,
614
614
  };
@@ -635,7 +635,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
635
635
  const targetTable = this.tableName(fk.targetTable);
636
636
  const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
637
637
 
638
- // MySQL FK Add automatic으로 Index 생성하므로 별도 IDX 불필요
638
+ // MySQL automatically creates index when adding FK, so no separate index needed
639
639
  return {
640
640
  sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`,
641
641
  };
@@ -676,7 +676,7 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
676
676
  protected createProc(def: CreateProcQueryDef): QueryBuildResult {
677
677
  const proc = this.tableName(def.procedure);
678
678
 
679
- // params processing
679
+ // Process params
680
680
  const paramList =
681
681
  def.params
682
682
  ?.map((p) => {
@@ -719,9 +719,9 @@ export class MysqlQueryBuilder extends QueryBuilderBase {
719
719
  //#region ========== Utils ==========
720
720
 
721
721
  protected clearSchema(def: ClearSchemaQueryDef): QueryBuildResult {
722
- // MySQL: 모든 Table DROP (MySQL에서 database schema 동의어)
723
- // information_schema에서 Table 목록 조회 DROP
724
- // SQL Injection 방지: 식별자 유효성 Validation
722
+ // MySQL: DROP all tables (in MySQL, database and schema are synonymous)
723
+ // Query table list from information_schema then DROP
724
+ // SQL injection prevention: identifier validation
725
725
  if (!/^[a-zA-Z0-9_]+$/.test(def.database)) {
726
726
  throw new Error(`Invalid database name: ${def.database}`);
727
727
  }
@@ -741,14 +741,14 @@ SET FOREIGN_KEY_CHECKS = 1`,
741
741
  }
742
742
 
743
743
  protected schemaExists(def: SchemaExistsQueryDef): QueryBuildResult {
744
- // MySQL: database schema 동의어
744
+ // MySQL: database and schema are synonymous
745
745
  const dbName = this.expr.escapeString(def.database);
746
746
  return {
747
747
  sql: `SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${dbName}'`,
748
748
  };
749
749
  }
750
750
 
751
- /** MySQL 전역 설정만 지원 (table 파라미터 무시됨) */
751
+ /** MySQL only supports global setting (table parameter is ignored) */
752
752
  protected switchFk(def: SwitchFkQueryDef): QueryBuildResult {
753
753
  return def.enabled
754
754
  ? { sql: "SET FOREIGN_KEY_CHECKS = 1" }
@@ -72,14 +72,14 @@ import { ExprRendererBase } from "../base/expr-renderer-base";
72
72
  * PostgreSQL expression renderer
73
73
  */
74
74
  export class PostgresqlExprRenderer extends ExprRendererBase {
75
- //#region ========== 유틸리티 (public - QueryBuilder에서도 사용) ==========
75
+ //#region ========== Utilities (public - also used by QueryBuilder) ==========
76
76
 
77
- /** 식별자 감싸기 */
77
+ /** Wrap identifier */
78
78
  wrap(name: string): string {
79
79
  return `"${name.replace(/"/g, '""')}"`;
80
80
  }
81
81
 
82
- /** SQL 문자열 리터럴용 escape (따옴표 없이 return) */
82
+ /** Escape for SQL string literals (returns without quotes) */
83
83
  escapeString(value: string): string {
84
84
  return value.replace(/'/g, "''");
85
85
  }
@@ -176,7 +176,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
176
176
  //#region ========== comparison (null-safe) ==========
177
177
 
178
178
  protected eq(expr: ExprEq): string {
179
- // PostgreSQL: null-safe equal (IS NOT DISTINCT FROM operator 사용)
179
+ // PostgreSQL: null-safe equal (uses IS NOT DISTINCT FROM operator)
180
180
  const left = this.render(expr.source);
181
181
  const right = this.render(expr.target);
182
182
  return `${left} IS NOT DISTINCT FROM ${right}`;
@@ -217,7 +217,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
217
217
  }
218
218
 
219
219
  protected like(expr: ExprLike): string {
220
- // ESCAPE '\' 항상 Add
220
+ // Always add ESCAPE '\'
221
221
  return `${this.render(expr.source)} LIKE ${this.render(expr.pattern)} ESCAPE '\\'`;
222
222
  }
223
223
 
@@ -228,7 +228,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
228
228
 
229
229
  protected in(expr: ExprIn): string {
230
230
  if (expr.values.length === 0) {
231
- return "FALSE"; // IN 항상 false
231
+ return "FALSE"; // empty IN is always false
232
232
  }
233
233
  const values = expr.values.map((v) => this.render(v)).join(", ");
234
234
  return `${this.render(expr.source)} IN (${values})`;
@@ -239,7 +239,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
239
239
  }
240
240
 
241
241
  protected exists(expr: ExprExists): string {
242
- // SELECT 1로 Render
242
+ // Render as SELECT 1
243
243
  const subquery = this.buildSelect({
244
244
  ...expr.query,
245
245
  select: { _: { type: "value", value: 1 } },
@@ -267,10 +267,10 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
267
267
 
268
268
  //#endregion
269
269
 
270
- //#region ========== 문자열 (null Process) ==========
270
+ //#region ========== String (null handling) ==========
271
271
 
272
272
  protected concat(expr: ExprConcat): string {
273
- // PostgreSQL: || 연산자와 COALESCE 사용
273
+ // PostgreSQL: uses || operator with COALESCE
274
274
  const args = expr.args.map((a) => `COALESCE(${this.render(a)}, '')`);
275
275
  return args.join(" || ");
276
276
  }
@@ -304,12 +304,12 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
304
304
  }
305
305
 
306
306
  protected length(expr: ExprLength): string {
307
- // PostgreSQL: LENGTH() (null Process)
307
+ // PostgreSQL: LENGTH() (null handling)
308
308
  return `LENGTH(COALESCE(${this.render(expr.arg)}, ''))`;
309
309
  }
310
310
 
311
311
  protected byteLength(expr: ExprByteLength): string {
312
- // PostgreSQL: OCTET_LENGTH() (null Process)
312
+ // PostgreSQL: OCTET_LENGTH() (null handling)
313
313
  return `OCTET_LENGTH(COALESCE(${this.render(expr.arg)}, ''))`;
314
314
  }
315
315
 
@@ -378,7 +378,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
378
378
 
379
379
  protected isoWeekStartDate(expr: ExprIsoWeekStartDate): string {
380
380
  const src = this.render(expr.arg);
381
- // ISO 주의 시작일 (월요일)
381
+ // ISO week start date (Monday)
382
382
  return `DATE_TRUNC('week', ${src})::DATE`;
383
383
  }
384
384
 
@@ -511,13 +511,13 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
511
511
 
512
512
  protected greatest(expr: ExprGreatest): string {
513
513
  if (expr.args.length === 0) throw new Error("greatest requires at least one argument.");
514
- // PostgreSQL: GREATEST 네이티브 지원
514
+ // PostgreSQL: native GREATEST support
515
515
  return `GREATEST(${expr.args.map((a) => this.render(a)).join(", ")})`;
516
516
  }
517
517
 
518
518
  protected least(expr: ExprLeast): string {
519
519
  if (expr.args.length === 0) throw new Error("least requires at least one argument.");
520
- // PostgreSQL: LEAST 네이티브 지원
520
+ // PostgreSQL: native LEAST support
521
521
  return `LEAST(${expr.args.map((a) => this.render(a)).join(", ")})`;
522
522
  }
523
523
 
@@ -541,7 +541,7 @@ export class PostgresqlExprRenderer extends ExprRendererBase {
541
541
  const fn = this.renderWindowFn(expr.fn);
542
542
  let over = this.renderWindowSpec(expr.spec);
543
543
 
544
- // LAST_VALUE Basic 프레임이 CURRENT ROW까지만 보므로 전체 프레임 명시 필요
544
+ // LAST_VALUE default frame only sees up to CURRENT ROW, so full frame must be specified
545
545
  if (expr.fn.type === "lastValue" && over.length > 0) {
546
546
  over += " ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING";
547
547
  }