@simplysm/orm-common 13.0.100 → 14.0.4

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 (246) hide show
  1. package/README.md +90 -147
  2. package/dist/create-db-context.d.ts +10 -10
  3. package/dist/create-db-context.js +312 -276
  4. package/dist/create-db-context.js.map +1 -6
  5. package/dist/ddl/column-ddl.d.ts +4 -4
  6. package/dist/ddl/column-ddl.js +41 -35
  7. package/dist/ddl/column-ddl.js.map +1 -6
  8. package/dist/ddl/initialize.d.ts +17 -17
  9. package/dist/ddl/initialize.js +200 -142
  10. package/dist/ddl/initialize.js.map +1 -6
  11. package/dist/ddl/relation-ddl.d.ts +6 -6
  12. package/dist/ddl/relation-ddl.js +55 -48
  13. package/dist/ddl/relation-ddl.js.map +1 -6
  14. package/dist/ddl/schema-ddl.d.ts +4 -4
  15. package/dist/ddl/schema-ddl.js +21 -15
  16. package/dist/ddl/schema-ddl.js.map +1 -6
  17. package/dist/ddl/table-ddl.d.ts +20 -20
  18. package/dist/ddl/table-ddl.js +139 -93
  19. package/dist/ddl/table-ddl.js.map +1 -6
  20. package/dist/define-db-context.js +10 -13
  21. package/dist/define-db-context.js.map +1 -6
  22. package/dist/errors/db-transaction-error.d.ts +15 -15
  23. package/dist/errors/db-transaction-error.d.ts.map +1 -1
  24. package/dist/errors/db-transaction-error.js +53 -19
  25. package/dist/errors/db-transaction-error.js.map +1 -6
  26. package/dist/exec/executable.d.ts +23 -23
  27. package/dist/exec/executable.js +94 -40
  28. package/dist/exec/executable.js.map +1 -6
  29. package/dist/exec/queryable.d.ts +97 -97
  30. package/dist/exec/queryable.js +1310 -1204
  31. package/dist/exec/queryable.js.map +1 -6
  32. package/dist/exec/search-parser.d.ts +31 -31
  33. package/dist/exec/search-parser.d.ts.map +1 -1
  34. package/dist/exec/search-parser.js +158 -59
  35. package/dist/exec/search-parser.js.map +1 -6
  36. package/dist/expr/expr-unit.d.ts +4 -4
  37. package/dist/expr/expr-unit.js +24 -18
  38. package/dist/expr/expr-unit.js.map +1 -6
  39. package/dist/expr/expr.d.ts +108 -108
  40. package/dist/expr/expr.js +1872 -1844
  41. package/dist/expr/expr.js.map +1 -6
  42. package/dist/index.js +23 -1
  43. package/dist/index.js.map +1 -6
  44. package/dist/models/system-migration.js +7 -7
  45. package/dist/models/system-migration.js.map +1 -6
  46. package/dist/query-builder/base/expr-renderer-base.d.ts +10 -10
  47. package/dist/query-builder/base/expr-renderer-base.js +27 -21
  48. package/dist/query-builder/base/expr-renderer-base.js.map +1 -6
  49. package/dist/query-builder/base/query-builder-base.d.ts +21 -21
  50. package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
  51. package/dist/query-builder/base/query-builder-base.js +90 -80
  52. package/dist/query-builder/base/query-builder-base.js.map +1 -6
  53. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +5 -5
  54. package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
  55. package/dist/query-builder/mssql/mssql-expr-renderer.js +447 -420
  56. package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -6
  57. package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
  58. package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
  59. package/dist/query-builder/mssql/mssql-query-builder.js +483 -443
  60. package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -6
  61. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +5 -5
  62. package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
  63. package/dist/query-builder/mysql/mysql-expr-renderer.js +451 -419
  64. package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -6
  65. package/dist/query-builder/mysql/mysql-query-builder.d.ts +10 -10
  66. package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
  67. package/dist/query-builder/mysql/mysql-query-builder.js +570 -479
  68. package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -6
  69. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +5 -5
  70. package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
  71. package/dist/query-builder/postgresql/postgresql-expr-renderer.js +449 -422
  72. package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -6
  73. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +8 -8
  74. package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
  75. package/dist/query-builder/postgresql/postgresql-query-builder.js +511 -460
  76. package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -6
  77. package/dist/query-builder/query-builder.d.ts +1 -1
  78. package/dist/query-builder/query-builder.js +13 -13
  79. package/dist/query-builder/query-builder.js.map +1 -6
  80. package/dist/schema/factory/column-builder.d.ts +84 -84
  81. package/dist/schema/factory/column-builder.js +248 -185
  82. package/dist/schema/factory/column-builder.js.map +1 -6
  83. package/dist/schema/factory/index-builder.d.ts +38 -38
  84. package/dist/schema/factory/index-builder.js +144 -85
  85. package/dist/schema/factory/index-builder.js.map +1 -6
  86. package/dist/schema/factory/relation-builder.d.ts +99 -99
  87. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  88. package/dist/schema/factory/relation-builder.js +274 -136
  89. package/dist/schema/factory/relation-builder.js.map +1 -6
  90. package/dist/schema/procedure-builder.d.ts +51 -51
  91. package/dist/schema/procedure-builder.d.ts.map +1 -1
  92. package/dist/schema/procedure-builder.js +205 -131
  93. package/dist/schema/procedure-builder.js.map +1 -6
  94. package/dist/schema/table-builder.d.ts +55 -55
  95. package/dist/schema/table-builder.d.ts.map +1 -1
  96. package/dist/schema/table-builder.js +274 -205
  97. package/dist/schema/table-builder.js.map +1 -6
  98. package/dist/schema/view-builder.d.ts +44 -44
  99. package/dist/schema/view-builder.d.ts.map +1 -1
  100. package/dist/schema/view-builder.js +189 -116
  101. package/dist/schema/view-builder.js.map +1 -6
  102. package/dist/types/column.d.ts +21 -21
  103. package/dist/types/column.js +60 -30
  104. package/dist/types/column.js.map +1 -6
  105. package/dist/types/db-context-def.d.ts +9 -9
  106. package/dist/types/db-context-def.js +2 -1
  107. package/dist/types/db-context-def.js.map +1 -6
  108. package/dist/types/db.d.ts +47 -47
  109. package/dist/types/db.js +15 -5
  110. package/dist/types/db.js.map +1 -6
  111. package/dist/types/expr.d.ts +81 -81
  112. package/dist/types/expr.d.ts.map +1 -1
  113. package/dist/types/expr.js +3 -1
  114. package/dist/types/expr.js.map +1 -6
  115. package/dist/types/query-def.d.ts +46 -46
  116. package/dist/types/query-def.d.ts.map +1 -1
  117. package/dist/types/query-def.js +31 -24
  118. package/dist/types/query-def.js.map +1 -6
  119. package/dist/utils/result-parser.d.ts +11 -11
  120. package/dist/utils/result-parser.js +362 -221
  121. package/dist/utils/result-parser.js.map +1 -6
  122. package/docs/core.md +117 -145
  123. package/docs/expression.md +186 -203
  124. package/docs/query-builder.md +75 -42
  125. package/docs/queryable.md +189 -151
  126. package/docs/schema-builders.md +172 -283
  127. package/docs/types.md +229 -173
  128. package/package.json +7 -5
  129. package/src/create-db-context.ts +31 -31
  130. package/src/ddl/column-ddl.ts +4 -4
  131. package/src/ddl/initialize.ts +38 -38
  132. package/src/ddl/relation-ddl.ts +6 -6
  133. package/src/ddl/schema-ddl.ts +4 -4
  134. package/src/ddl/table-ddl.ts +24 -24
  135. package/src/errors/db-transaction-error.ts +13 -13
  136. package/src/exec/executable.ts +25 -25
  137. package/src/exec/queryable.ts +152 -152
  138. package/src/exec/search-parser.ts +50 -50
  139. package/src/expr/expr-unit.ts +4 -4
  140. package/src/expr/expr.ts +118 -118
  141. package/src/index.ts +8 -8
  142. package/src/models/system-migration.ts +1 -1
  143. package/src/query-builder/base/expr-renderer-base.ts +21 -21
  144. package/src/query-builder/base/query-builder-base.ts +33 -33
  145. package/src/query-builder/mssql/mssql-expr-renderer.ts +28 -28
  146. package/src/query-builder/mssql/mssql-query-builder.ts +37 -37
  147. package/src/query-builder/mysql/mysql-expr-renderer.ts +29 -29
  148. package/src/query-builder/mysql/mysql-query-builder.ts +70 -70
  149. package/src/query-builder/postgresql/postgresql-expr-renderer.ts +22 -22
  150. package/src/query-builder/postgresql/postgresql-query-builder.ts +54 -54
  151. package/src/query-builder/query-builder.ts +1 -1
  152. package/src/schema/factory/column-builder.ts +86 -86
  153. package/src/schema/factory/index-builder.ts +38 -38
  154. package/src/schema/factory/relation-builder.ts +102 -102
  155. package/src/schema/procedure-builder.ts +52 -52
  156. package/src/schema/table-builder.ts +56 -56
  157. package/src/schema/view-builder.ts +47 -47
  158. package/src/types/column.ts +24 -24
  159. package/src/types/db-context-def.ts +15 -15
  160. package/src/types/db.ts +50 -50
  161. package/src/types/expr.ts +103 -103
  162. package/src/types/query-def.ts +50 -50
  163. package/src/utils/result-parser.ts +88 -88
  164. package/docs/utilities.md +0 -27
  165. package/tests/db-context/create-db-context.spec.ts +0 -193
  166. package/tests/db-context/define-db-context.spec.ts +0 -17
  167. package/tests/ddl/basic.expected.ts +0 -341
  168. package/tests/ddl/basic.spec.ts +0 -557
  169. package/tests/ddl/column-builder.expected.ts +0 -310
  170. package/tests/ddl/column-builder.spec.ts +0 -525
  171. package/tests/ddl/index-builder.expected.ts +0 -38
  172. package/tests/ddl/index-builder.spec.ts +0 -148
  173. package/tests/ddl/procedure-builder.expected.ts +0 -52
  174. package/tests/ddl/procedure-builder.spec.ts +0 -128
  175. package/tests/ddl/relation-builder.expected.ts +0 -36
  176. package/tests/ddl/relation-builder.spec.ts +0 -171
  177. package/tests/ddl/table-builder.expected.ts +0 -113
  178. package/tests/ddl/table-builder.spec.ts +0 -399
  179. package/tests/ddl/view-builder.expected.ts +0 -38
  180. package/tests/ddl/view-builder.spec.ts +0 -116
  181. package/tests/dml/delete.expected.ts +0 -96
  182. package/tests/dml/delete.spec.ts +0 -127
  183. package/tests/dml/insert.expected.ts +0 -192
  184. package/tests/dml/insert.spec.ts +0 -210
  185. package/tests/dml/update.expected.ts +0 -176
  186. package/tests/dml/update.spec.ts +0 -222
  187. package/tests/dml/upsert.expected.ts +0 -215
  188. package/tests/dml/upsert.spec.ts +0 -190
  189. package/tests/errors/queryable-errors.spec.ts +0 -126
  190. package/tests/escape.spec.ts +0 -59
  191. package/tests/examples/pivot.expected.ts +0 -211
  192. package/tests/examples/pivot.spec.ts +0 -200
  193. package/tests/examples/sampling.expected.ts +0 -69
  194. package/tests/examples/sampling.spec.ts +0 -42
  195. package/tests/examples/unpivot.expected.ts +0 -120
  196. package/tests/examples/unpivot.spec.ts +0 -161
  197. package/tests/exec/search-parser.spec.ts +0 -267
  198. package/tests/executable/basic.expected.ts +0 -18
  199. package/tests/executable/basic.spec.ts +0 -54
  200. package/tests/expr/comparison.expected.ts +0 -282
  201. package/tests/expr/comparison.spec.ts +0 -334
  202. package/tests/expr/conditional.expected.ts +0 -134
  203. package/tests/expr/conditional.spec.ts +0 -249
  204. package/tests/expr/date.expected.ts +0 -332
  205. package/tests/expr/date.spec.ts +0 -459
  206. package/tests/expr/math.expected.ts +0 -62
  207. package/tests/expr/math.spec.ts +0 -59
  208. package/tests/expr/string.expected.ts +0 -218
  209. package/tests/expr/string.spec.ts +0 -300
  210. package/tests/expr/utility.expected.ts +0 -147
  211. package/tests/expr/utility.spec.ts +0 -155
  212. package/tests/select/basic.expected.ts +0 -322
  213. package/tests/select/basic.spec.ts +0 -433
  214. package/tests/select/filter.expected.ts +0 -357
  215. package/tests/select/filter.spec.ts +0 -954
  216. package/tests/select/group.expected.ts +0 -169
  217. package/tests/select/group.spec.ts +0 -159
  218. package/tests/select/join.expected.ts +0 -582
  219. package/tests/select/join.spec.ts +0 -692
  220. package/tests/select/order.expected.ts +0 -150
  221. package/tests/select/order.spec.ts +0 -140
  222. package/tests/select/recursive-cte.expected.ts +0 -244
  223. package/tests/select/recursive-cte.spec.ts +0 -514
  224. package/tests/select/result-meta.spec.ts +0 -270
  225. package/tests/select/subquery.expected.ts +0 -363
  226. package/tests/select/subquery.spec.ts +0 -441
  227. package/tests/select/view.expected.ts +0 -155
  228. package/tests/select/view.spec.ts +0 -235
  229. package/tests/select/window.expected.ts +0 -345
  230. package/tests/select/window.spec.ts +0 -433
  231. package/tests/setup/MockExecutor.ts +0 -18
  232. package/tests/setup/TestDbContext.ts +0 -59
  233. package/tests/setup/models/Company.ts +0 -13
  234. package/tests/setup/models/Employee.ts +0 -10
  235. package/tests/setup/models/MonthlySales.ts +0 -11
  236. package/tests/setup/models/Post.ts +0 -16
  237. package/tests/setup/models/Sales.ts +0 -10
  238. package/tests/setup/models/User.ts +0 -19
  239. package/tests/setup/procedure/GetAllUsers.ts +0 -9
  240. package/tests/setup/procedure/GetUserById.ts +0 -12
  241. package/tests/setup/test-utils.ts +0 -72
  242. package/tests/setup/views/ActiveUsers.ts +0 -8
  243. package/tests/setup/views/UserSummary.ts +0 -11
  244. package/tests/types/nullable-queryable-record.spec.ts +0 -97
  245. package/tests/utils/result-parser-perf.spec.ts +0 -143
  246. package/tests/utils/result-parser.spec.ts +0 -667
@@ -1,464 +1,515 @@
1
1
  import { QueryBuilderBase } from "../base/query-builder-base.js";
2
2
  import { PostgresqlExprRenderer } from "./postgresql-expr-renderer.js";
3
- class PostgresqlQueryBuilder extends QueryBuilderBase {
4
- expr = new PostgresqlExprRenderer((def) => this.select(def).sql);
5
- //#region ========== Utilities ==========
6
- /** Render table name (PostgreSQL: database is handled by connection, uses schema.table only) */
7
- tableName(obj) {
8
- const schema = obj.schema ?? "public";
9
- return `${this.expr.wrap(schema)}.${this.expr.wrap(obj.name)}`;
10
- }
11
- /** Render LIMIT...OFFSET clause */
12
- renderLimit(limit, top) {
13
- if (limit != null) {
14
- const [offset, count] = limit;
15
- return ` LIMIT ${count} OFFSET ${offset}`;
16
- }
17
- if (top != null) {
18
- return ` LIMIT ${top}`;
19
- }
20
- return "";
21
- }
22
- renderJoin(join) {
23
- const alias = this.expr.wrap(join.as);
24
- if (this.needsLateral(join)) {
25
- const from2 = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
26
- return ` LEFT OUTER JOIN LATERAL ${from2} AS ${alias} ON TRUE`;
27
- }
28
- const from = this.renderFrom(join.from);
29
- const where = join.where != null && join.where.length > 0 ? ` ON ${this.expr.renderWhere(join.where)}` : " ON TRUE";
30
- return ` LEFT OUTER JOIN ${from} AS ${alias}${where}`;
31
- }
32
- //#endregion
33
- //#region ========== DML - SELECT ==========
34
- select(def) {
35
- let sql = "";
36
- if (def.with != null) {
37
- const { name, base, recursive } = def.with;
38
- sql += `WITH RECURSIVE ${this.expr.wrap(name)} AS (${this.select(base).sql} UNION ALL ${this.select(recursive).sql}) `;
39
- }
40
- sql += "SELECT";
41
- if (def.distinct) {
42
- sql += " DISTINCT";
43
- }
44
- if (def.select != null) {
45
- const cols = Object.entries(def.select).map(
46
- ([alias, expr]) => `${this.expr.render(expr)} AS ${this.expr.wrap(alias)}`
47
- );
48
- sql += ` ${cols.join(", ")}`;
49
- } else {
50
- sql += " *";
51
- }
52
- if (def.from != null) {
53
- const from = this.renderFrom(def.from);
54
- sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
55
- }
56
- sql += this.renderJoins(def.joins);
57
- sql += this.renderWhere(def.where);
58
- sql += this.renderGroupBy(def.groupBy);
59
- sql += this.renderHaving(def.having);
60
- sql += this.renderOrderBy(def.orderBy);
61
- sql += this.renderLimit(def.limit, def.top);
62
- if (def.lock) {
63
- sql += " FOR UPDATE";
64
- }
65
- return { sql };
66
- }
67
- //#endregion
68
- //#region ========== DML - INSERT ==========
69
- insert(def) {
70
- const table = this.tableName(def.table);
71
- if (def.records.length === 0) {
72
- throw new Error("INSERT requires at least one record.");
73
- }
74
- const columns = Object.keys(def.records[0]);
75
- const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
76
- const valuesList = def.records.map((record) => {
77
- const values = columns.map((c) => this.expr.escapeValue(record[c]));
78
- return `(${values.join(", ")})`;
79
- });
80
- let sql = `INSERT INTO ${table} (${colList})`;
81
- sql += ` VALUES ${valuesList.join(", ")}`;
82
- if (def.output != null) {
83
- const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
84
- sql += ` RETURNING ${outputCols}`;
85
- }
86
- return { sql };
87
- }
88
- insertIfNotExists(def) {
89
- const table = this.tableName(def.table);
90
- const columns = Object.keys(def.record);
91
- const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
92
- const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
93
- const existsQuerySql = this.select({
94
- ...def.existsSelectQuery,
95
- select: { _: { type: "value", value: 1 } }
96
- }).sql;
97
- let sql = `INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
98
- if (def.output != null) {
99
- const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
100
- sql += ` RETURNING ${outputCols}`;
101
- }
102
- return { sql };
103
- }
104
- insertInto(def) {
105
- const table = this.tableName(def.table);
106
- const selectSql = this.select(def.recordsSelectQuery).sql;
107
- const selectDef = def.recordsSelectQuery;
108
- const colList = selectDef.select != null ? Object.keys(selectDef.select).map((c) => this.expr.wrap(c)).join(", ") : "*";
109
- let sql = `INSERT INTO ${table} (${colList}) ${selectSql}`;
110
- if (def.output != null) {
111
- const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
112
- sql += ` RETURNING ${outputCols}`;
113
- }
114
- return { sql };
115
- }
116
- //#endregion
117
- //#region ========== DML - UPDATE ==========
118
- update(def) {
119
- const table = this.tableName(def.table);
120
- const alias = this.expr.wrap(def.as);
121
- const setParts = Object.entries(def.record).map(
122
- ([col, e]) => `${this.expr.wrap(col)} = ${this.expr.render(e)}`
123
- );
124
- let sql = `UPDATE ${table} AS ${alias} SET ${setParts.join(", ")}`;
125
- if (def.joins != null && def.joins.length > 0) {
126
- const joinTables = def.joins.map((j) => {
127
- const from = this.renderFrom(j.from);
128
- return `${from} AS ${this.expr.wrap(j.as)}`;
129
- });
130
- sql += ` FROM ${joinTables.join(", ")}`;
131
- const joinConditions = def.joins.filter((j) => j.where != null && j.where.length > 0).map((j) => this.expr.renderWhere(j.where));
132
- if (joinConditions.length > 0) {
133
- const whereCondition = def.where != null && def.where.length > 0 ? this.expr.renderWhere(def.where) : null;
134
- const allConditions = whereCondition != null ? [whereCondition, ...joinConditions] : joinConditions;
135
- sql += ` WHERE ${allConditions.join(" AND ")}`;
136
- } else {
137
- sql += this.renderWhere(def.where);
138
- }
139
- } else {
140
- sql += this.renderWhere(def.where);
141
- }
142
- if (def.output != null) {
143
- const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
144
- sql += ` RETURNING ${outputCols}`;
145
- }
146
- return { sql };
147
- }
148
- //#endregion
149
- //#region ========== DML - DELETE ==========
150
- delete(def) {
151
- const table = this.tableName(def.table);
152
- const alias = this.expr.wrap(def.as);
153
- let sql = `DELETE FROM ${table} AS ${alias}`;
154
- if (def.joins != null && def.joins.length > 0) {
155
- const joinTables = def.joins.map((j) => {
156
- const from = this.renderFrom(j.from);
157
- return `${from} AS ${this.expr.wrap(j.as)}`;
158
- });
159
- sql += ` USING ${joinTables.join(", ")}`;
160
- const joinConditions = def.joins.filter((j) => j.where != null && j.where.length > 0).map((j) => this.expr.renderWhere(j.where));
161
- if (joinConditions.length > 0) {
162
- const whereCondition = def.where != null && def.where.length > 0 ? this.expr.renderWhere(def.where) : null;
163
- const allConditions = whereCondition != null ? [whereCondition, ...joinConditions] : joinConditions;
164
- sql += ` WHERE ${allConditions.join(" AND ")}`;
165
- } else {
3
+ /**
4
+ * PostgreSQL QueryBuilder
5
+ *
6
+ * PostgreSQL 고유 사항:
7
+ * - OUTPUT: RETURNING 절 사용 (네이티브 지원)
8
+ * - TRUNCATE: RESTART IDENTITY 옵션 필요
9
+ * - UPSERT: CTE 방식 (INSERT ... ON CONFLICT는 단일 유니크 제약조건만 지원)
10
+ * - AUTO_INCREMENT: GENERATED BY DEFAULT AS IDENTITY (명시적 값 할당 가능)
11
+ * - FK 추가 시 별도 인덱스 생성 필요 (MySQL과 다름)
12
+ */
13
+ export class PostgresqlQueryBuilder extends QueryBuilderBase {
14
+ expr = new PostgresqlExprRenderer((def) => this.select(def).sql);
15
+ //#region ========== Utilities ==========
16
+ /** 테이블명 렌더링 (PostgreSQL: database는 연결에서 처리, schema.table만 사용) */
17
+ tableName(obj) {
18
+ const schema = obj.schema ?? "public";
19
+ return `${this.expr.wrap(schema)}.${this.expr.wrap(obj.name)}`;
20
+ }
21
+ /** LIMIT...OFFSET 절 렌더링 */
22
+ renderLimit(limit, top) {
23
+ if (limit != null) {
24
+ const [offset, count] = limit;
25
+ return ` LIMIT ${count} OFFSET ${offset}`;
26
+ }
27
+ if (top != null) {
28
+ return ` LIMIT ${top}`;
29
+ }
30
+ return "";
31
+ }
32
+ renderJoin(join) {
33
+ const alias = this.expr.wrap(join.as);
34
+ // LATERAL JOIN이 필요한지 감지
35
+ if (this.needsLateral(join)) {
36
+ // from이 배열(UNION ALL)이면 renderFrom(join.from) 사용,
37
+ // 외(orderBy, top, select 등)이면 renderFrom(join)으로 서브쿼리 생성
38
+ const from = Array.isArray(join.from) ? this.renderFrom(join.from) : this.renderFrom(join);
39
+ return ` LEFT OUTER JOIN LATERAL ${from} AS ${alias} ON TRUE`;
40
+ }
41
+ // 일반 JOIN
42
+ const from = this.renderFrom(join.from);
43
+ const where = join.where != null && join.where.length > 0
44
+ ? ` ON ${this.expr.renderWhere(join.where)}`
45
+ : " ON TRUE";
46
+ return ` LEFT OUTER JOIN ${from} AS ${alias}${where}`;
47
+ }
48
+ //#endregion
49
+ //#region ========== DML - SELECT ==========
50
+ select(def) {
51
+ // WITH (CTE)
52
+ let sql = "";
53
+ if (def.with != null) {
54
+ const { name, base, recursive } = def.with;
55
+ sql += `WITH RECURSIVE ${this.expr.wrap(name)} AS (${this.select(base).sql} UNION ALL ${this.select(recursive).sql}) `;
56
+ }
57
+ // SELECT
58
+ sql += "SELECT";
59
+ if (def.distinct) {
60
+ sql += " DISTINCT";
61
+ }
62
+ // columns
63
+ if (def.select != null) {
64
+ const cols = Object.entries(def.select).map(([alias, expr]) => `${this.expr.render(expr)} AS ${this.expr.wrap(alias)}`);
65
+ sql += ` ${cols.join(", ")}`;
66
+ }
67
+ else {
68
+ sql += " *";
69
+ }
70
+ // FROM
71
+ if (def.from != null) {
72
+ const from = this.renderFrom(def.from);
73
+ sql += ` FROM ${from} AS ${this.expr.wrap(def.as)}`;
74
+ }
75
+ // JOINs
76
+ sql += this.renderJoins(def.joins);
77
+ // WHERE
166
78
  sql += this.renderWhere(def.where);
167
- }
168
- } else {
169
- sql += this.renderWhere(def.where);
170
- }
171
- if (def.output != null) {
172
- const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
173
- sql += ` RETURNING ${outputCols}`;
174
- }
175
- return { sql };
176
- }
177
- //#endregion
178
- //#region ========== DML - UPSERT ==========
179
- upsert(def) {
180
- const table = this.tableName(def.table);
181
- const alias = this.expr.wrap(def.existsSelectQuery.as);
182
- const updateSetParts = Object.entries(def.updateRecord).map(
183
- ([col, e]) => `${this.expr.wrap(col)} = ${this.expr.render(e)}`
184
- );
185
- const insertColumns = Object.keys(def.insertRecord);
186
- const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
187
- const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
188
- const whereCondition = def.existsSelectQuery.where != null && def.existsSelectQuery.where.length > 0 ? this.expr.renderWhere(def.existsSelectQuery.where) : "TRUE";
189
- const outputCols = def.output != null ? def.output.columns.map((c) => this.expr.wrap(c)).join(", ") : "*";
190
- let sql = `WITH matched AS (
191
- `;
192
- sql += ` SELECT ${alias}.* FROM ${table} AS ${alias} WHERE ${whereCondition}
193
- `;
194
- sql += `),
195
- `;
196
- sql += `updated AS (
197
- `;
198
- sql += ` UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")}
199
- `;
200
- sql += ` WHERE ${whereCondition}
201
- `;
202
- sql += ` RETURNING ${outputCols}
203
- `;
204
- sql += `),
205
- `;
206
- sql += `inserted AS (
207
- `;
208
- sql += ` INSERT INTO ${table} (${insertColList})
209
- `;
210
- sql += ` SELECT ${insertValues}
211
- `;
212
- sql += ` WHERE NOT EXISTS (SELECT 1 FROM matched)
213
- `;
214
- sql += ` RETURNING ${outputCols}
215
- `;
216
- sql += `)
217
- `;
218
- sql += `SELECT * FROM updated UNION ALL SELECT * FROM inserted`;
219
- return { sql };
220
- }
221
- //#endregion
222
- //#region ========== DDL - Table ==========
223
- createTable(def) {
224
- const table = this.tableName(def.table);
225
- const colDefs = def.columns.map((col) => {
226
- let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
227
- if (col.nullable === true) {
228
- colSql += " NULL";
229
- } else {
230
- colSql += " NOT NULL";
231
- }
232
- if (col.autoIncrement) {
233
- colSql += " GENERATED BY DEFAULT AS IDENTITY";
234
- }
235
- if (col.default !== void 0) {
236
- colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
237
- }
238
- return colSql;
239
- });
240
- if (def.primaryKey != null && def.primaryKey.length > 0) {
241
- const pkCols = def.primaryKey.map((c) => this.expr.wrap(c)).join(", ");
242
- const pkName = this.expr.wrap(`PK_${def.table.name}`);
243
- colDefs.push(`CONSTRAINT ${pkName} PRIMARY KEY (${pkCols})`);
244
- }
245
- return { sql: `CREATE TABLE ${table} (
246
- ${colDefs.join(",\n ")}
247
- )` };
248
- }
249
- dropTable(def) {
250
- return { sql: `DROP TABLE ${this.tableName(def.table)}` };
251
- }
252
- renameTable(def) {
253
- return {
254
- sql: `ALTER TABLE ${this.tableName(def.table)} RENAME TO ${this.expr.wrap(def.newName)}`
255
- };
256
- }
257
- truncate(def) {
258
- return { sql: `TRUNCATE TABLE ${this.tableName(def.table)} RESTART IDENTITY` };
259
- }
260
- //#endregion
261
- //#region ========== DDL - Column ==========
262
- addColumn(def) {
263
- const table = this.tableName(def.table);
264
- const col = def.column;
265
- let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
266
- if (col.nullable === true) {
267
- colSql += " NULL";
268
- } else {
269
- colSql += " NOT NULL";
270
- }
271
- if (col.autoIncrement) {
272
- colSql += " GENERATED BY DEFAULT AS IDENTITY";
273
- }
274
- if (col.default !== void 0) {
275
- colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
276
- }
277
- return { sql: `ALTER TABLE ${table} ADD COLUMN ${colSql}` };
278
- }
279
- dropColumn(def) {
280
- return {
281
- sql: `ALTER TABLE ${this.tableName(def.table)} DROP COLUMN ${this.expr.wrap(def.column)}`
282
- };
283
- }
284
- modifyColumn(def) {
285
- const table = this.tableName(def.table);
286
- const col = def.column;
287
- const parts = [];
288
- parts.push(
289
- `ALTER COLUMN ${this.expr.wrap(col.name)} TYPE ${this.expr.renderDataType(col.dataType)}`
290
- );
291
- if (col.nullable === false) {
292
- parts.push(`ALTER COLUMN ${this.expr.wrap(col.name)} SET NOT NULL`);
293
- } else {
294
- parts.push(`ALTER COLUMN ${this.expr.wrap(col.name)} DROP NOT NULL`);
295
- }
296
- if (col.default !== void 0) {
297
- parts.push(
298
- `ALTER COLUMN ${this.expr.wrap(col.name)} SET DEFAULT ${this.expr.escapeValue(col.default)}`
299
- );
300
- }
301
- return { sql: `ALTER TABLE ${table} ${parts.join(", ")}` };
302
- }
303
- renameColumn(def) {
304
- const table = this.tableName(def.table);
305
- return {
306
- sql: `ALTER TABLE ${table} RENAME COLUMN ${this.expr.wrap(def.column)} TO ${this.expr.wrap(def.newName)}`
307
- };
308
- }
309
- //#endregion
310
- //#region ========== DDL - Constraint ==========
311
- addPrimaryKey(def) {
312
- const table = this.tableName(def.table);
313
- const cols = def.columns.map((c) => this.expr.wrap(c)).join(", ");
314
- const pkName = `PK_${def.table.name}`;
315
- return {
316
- sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(pkName)} PRIMARY KEY (${cols})`
317
- };
318
- }
319
- dropPrimaryKey(def) {
320
- const table = this.tableName(def.table);
321
- const pkName = `PK_${def.table.name}`;
322
- return { sql: `ALTER TABLE ${table} DROP CONSTRAINT ${this.expr.wrap(pkName)}` };
323
- }
324
- addForeignKey(def) {
325
- const table = this.tableName(def.table);
326
- const fk = def.foreignKey;
327
- const fkCols = fk.fkColumns.map((c) => this.expr.wrap(c)).join(", ");
328
- const targetTable = this.tableName(fk.targetTable);
329
- const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
330
- let sql = `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`;
331
- const idxName = `IDX_${def.table.name}_${fk.name.replace(/^FK_/, "")}`;
332
- sql += `;
333
- CREATE INDEX ${this.expr.wrap(idxName)} ON ${table} (${fkCols});`;
334
- return { sql };
335
- }
336
- dropForeignKey(def) {
337
- return {
338
- sql: `ALTER TABLE ${this.tableName(def.table)} DROP CONSTRAINT ${this.expr.wrap(def.foreignKey)}`
339
- };
340
- }
341
- addIndex(def) {
342
- const table = this.tableName(def.table);
343
- const idx = def.index;
344
- const cols = idx.columns.map((c) => `${this.expr.wrap(c.name)} ${c.orderBy}`).join(", ");
345
- const unique = idx.unique ? "UNIQUE " : "";
346
- return { sql: `CREATE ${unique}INDEX ${this.expr.wrap(idx.name)} ON ${table} (${cols})` };
347
- }
348
- dropIndex(def) {
349
- const schema = def.table.schema ?? "public";
350
- return { sql: `DROP INDEX ${this.expr.wrap(schema)}.${this.expr.wrap(def.index)}` };
351
- }
352
- //#endregion
353
- //#region ========== DDL - View/Procedure ==========
354
- createView(def) {
355
- const view = this.tableName(def.view);
356
- const selectSql = this.select(def.queryDef).sql;
357
- return { sql: `CREATE OR REPLACE VIEW ${view} AS ${selectSql}` };
358
- }
359
- dropView(def) {
360
- return { sql: `DROP VIEW IF EXISTS ${this.tableName(def.view)}` };
361
- }
362
- createProc(def) {
363
- var _a;
364
- const proc = this.tableName(def.procedure);
365
- const paramList = ((_a = def.params) == null ? void 0 : _a.map((p) => {
366
- let sql2 = `${this.expr.wrap(p.name)} ${this.expr.renderDataType(p.dataType)}`;
367
- if (p.default !== void 0) {
368
- sql2 += ` DEFAULT ${this.expr.escapeValue(p.default)}`;
369
- }
370
- return sql2;
371
- }).join(", ")) ?? "";
372
- let returnClause = "VOID";
373
- if (def.returns && def.returns.length > 0) {
374
- const returnFields = def.returns.map((r) => `${this.expr.wrap(r.name)} ${this.expr.renderDataType(r.dataType)}`).join(", ");
375
- returnClause = `TABLE(${returnFields})`;
376
- }
377
- let sql = `CREATE OR REPLACE FUNCTION ${proc}(${paramList})
378
- `;
379
- sql += `RETURNS ${returnClause} AS $$
380
- `;
381
- sql += `BEGIN
382
- `;
383
- sql += def.query;
384
- if (!def.query.trim().endsWith(";")) {
385
- sql += ";";
386
- }
387
- sql += `
388
- END;
389
- `;
390
- sql += `$$ LANGUAGE plpgsql`;
391
- return { sql };
392
- }
393
- dropProc(def) {
394
- return { sql: `DROP FUNCTION IF EXISTS ${this.tableName(def.procedure)}()` };
395
- }
396
- execProc(def) {
397
- const proc = this.tableName(def.procedure);
398
- if (def.params == null || Object.keys(def.params).length === 0) {
399
- return { sql: `SELECT ${proc}()` };
400
- }
401
- const params = Object.values(def.params).map((p) => this.expr.render(p)).join(", ");
402
- return { sql: `SELECT ${proc}(${params})` };
403
- }
404
- //#endregion
405
- //#region ========== Utils ==========
406
- clearSchema(def) {
407
- const schemaName = def.schema ?? "public";
408
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaName)) {
409
- throw new Error(`Invalid schema name: ${schemaName}`);
410
- }
411
- const schema = this.expr.escapeString(schemaName);
412
- return {
413
- sql: `
414
- DO $$
415
- DECLARE
416
- r RECORD;
417
- BEGIN
418
- -- Drop FK constraints
419
- FOR r IN (SELECT conname, conrelid::regclass AS tablename
420
- FROM pg_constraint
421
- WHERE contype = 'f' AND connamespace = '${schema}'::regnamespace)
422
- LOOP
423
- EXECUTE 'ALTER TABLE ' || r.tablename || ' DROP CONSTRAINT ' || quote_ident(r.conname);
424
- END LOOP;
425
-
426
- -- Drop tables
427
- FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = '${schema}')
428
- LOOP
429
- EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
430
- END LOOP;
431
-
432
- -- Drop views
433
- FOR r IN (SELECT viewname FROM pg_views WHERE schemaname = '${schema}')
434
- LOOP
435
- EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.viewname) || ' CASCADE';
436
- END LOOP;
437
-
438
- -- Drop functions
439
- FOR r IN (SELECT proname, pg_get_function_identity_arguments(oid) AS args
440
- FROM pg_proc WHERE pronamespace = '${schema}'::regnamespace)
441
- LOOP
442
- EXECUTE 'DROP FUNCTION IF EXISTS ' || quote_ident(r.proname) || '(' || r.args || ') CASCADE';
443
- END LOOP;
444
- END $$`
445
- };
446
- }
447
- schemaExists(def) {
448
- const schemaName = def.schema ?? "public";
449
- const schema = this.expr.escapeString(schemaName);
450
- return { sql: `SELECT nspname FROM pg_namespace WHERE nspname = '${schema}'` };
451
- }
452
- switchFk(def) {
453
- const table = this.tableName(def.table);
454
- if (def.enabled) {
455
- return { sql: `ALTER TABLE ${table} ENABLE TRIGGER ALL` };
456
- }
457
- return { sql: `ALTER TABLE ${table} DISABLE TRIGGER ALL` };
458
- }
459
- //#endregion
79
+ // GROUP BY
80
+ sql += this.renderGroupBy(def.groupBy);
81
+ // HAVING
82
+ sql += this.renderHaving(def.having);
83
+ // ORDER BY
84
+ sql += this.renderOrderBy(def.orderBy);
85
+ // LIMIT
86
+ sql += this.renderLimit(def.limit, def.top);
87
+ // LOCK (끝에 FOR UPDATE 추가)
88
+ if (def.lock) {
89
+ sql += " FOR UPDATE";
90
+ }
91
+ return { sql };
92
+ }
93
+ //#endregion
94
+ //#region ========== DML - INSERT ==========
95
+ insert(def) {
96
+ const table = this.tableName(def.table);
97
+ if (def.records.length === 0) {
98
+ throw new Error("INSERT에는 최소 1개의 레코드가 필요합니다.");
99
+ }
100
+ const columns = Object.keys(def.records[0]);
101
+ const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
102
+ const valuesList = def.records.map((record) => {
103
+ const values = columns.map((c) => this.expr.escapeValue(record[c]));
104
+ return `(${values.join(", ")})`;
105
+ });
106
+ let sql = `INSERT INTO ${table} (${colList})`;
107
+ // GENERATED BY DEFAULT AS IDENTITY이므로 명시적 값 삽입에 추가 구문 불필요
108
+ // overrideIdentity 파라미터는 MSSQL (SET IDENTITY_INSERT) 호환성을 위해 유지하지만
109
+ // PostgreSQL의 GENERATED BY DEFAULT는 자동으로 명시적 값을 허용
110
+ // (참고: GENERATED ALWAYS를 사용했다면 OVERRIDING SYSTEM VALUE가 필요)
111
+ sql += ` VALUES ${valuesList.join(", ")}`;
112
+ // RETURNING (PostgreSQL 네이티브 지원)
113
+ if (def.output != null) {
114
+ const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
115
+ sql += ` RETURNING ${outputCols}`;
116
+ }
117
+ return { sql };
118
+ }
119
+ insertIfNotExists(def) {
120
+ const table = this.tableName(def.table);
121
+ const columns = Object.keys(def.record);
122
+ const colList = columns.map((c) => this.expr.wrap(c)).join(", ");
123
+ const values = columns.map((c) => this.expr.escapeValue(def.record[c])).join(", ");
124
+ // existsSelectQuery를 SELECT 1 AS _로 렌더링
125
+ const existsQuerySql = this.select({
126
+ ...def.existsSelectQuery,
127
+ select: { _: { type: "value", value: 1 } },
128
+ }).sql;
129
+ let sql = `INSERT INTO ${table} (${colList}) SELECT ${values} WHERE NOT EXISTS (${existsQuerySql})`;
130
+ // RETURNING
131
+ if (def.output != null) {
132
+ const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
133
+ sql += ` RETURNING ${outputCols}`;
134
+ }
135
+ return { sql };
136
+ }
137
+ insertInto(def) {
138
+ const table = this.tableName(def.table);
139
+ const selectSql = this.select(def.recordsSelectQuery).sql;
140
+ // INSERT INTO SELECT에서 column 추출
141
+ const selectDef = def.recordsSelectQuery;
142
+ const colList = selectDef.select != null
143
+ ? Object.keys(selectDef.select)
144
+ .map((c) => this.expr.wrap(c))
145
+ .join(", ")
146
+ : "*";
147
+ let sql = `INSERT INTO ${table} (${colList}) ${selectSql}`;
148
+ // RETURNING
149
+ if (def.output != null) {
150
+ const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
151
+ sql += ` RETURNING ${outputCols}`;
152
+ }
153
+ return { sql };
154
+ }
155
+ //#endregion
156
+ //#region ========== DML - UPDATE ==========
157
+ update(def) {
158
+ const table = this.tableName(def.table);
159
+ const alias = this.expr.wrap(def.as);
160
+ // SET
161
+ const setParts = Object.entries(def.record).map(([col, e]) => `${this.expr.wrap(col)} = ${this.expr.render(e)}`);
162
+ let sql = `UPDATE ${table} AS ${alias} SET ${setParts.join(", ")}`;
163
+ // PostgreSQL: JOIN은 FROM 절로 처리
164
+ if (def.joins != null && def.joins.length > 0) {
165
+ const joinTables = def.joins.map((j) => {
166
+ const from = this.renderFrom(j.from);
167
+ return `${from} AS ${this.expr.wrap(j.as)}`;
168
+ });
169
+ sql += ` FROM ${joinTables.join(", ")}`;
170
+ // JOIN ON 조건을 WHERE에 추가
171
+ const joinConditions = def.joins
172
+ .filter((j) => j.where != null && j.where.length > 0)
173
+ .map((j) => this.expr.renderWhere(j.where));
174
+ if (joinConditions.length > 0) {
175
+ const whereCondition = def.where != null && def.where.length > 0 ? this.expr.renderWhere(def.where) : null;
176
+ const allConditions = whereCondition != null ? [whereCondition, ...joinConditions] : joinConditions;
177
+ sql += ` WHERE ${allConditions.join(" AND ")}`;
178
+ }
179
+ else {
180
+ sql += this.renderWhere(def.where);
181
+ }
182
+ }
183
+ else {
184
+ sql += this.renderWhere(def.where);
185
+ }
186
+ // RETURNING
187
+ if (def.output != null) {
188
+ const outputCols = def.output.columns.map((c) => `${alias}.${this.expr.wrap(c)}`).join(", ");
189
+ sql += ` RETURNING ${outputCols}`;
190
+ }
191
+ return { sql };
192
+ }
193
+ //#endregion
194
+ //#region ========== DML - DELETE ==========
195
+ delete(def) {
196
+ const table = this.tableName(def.table);
197
+ const alias = this.expr.wrap(def.as);
198
+ let sql = `DELETE FROM ${table} AS ${alias}`;
199
+ // PostgreSQL: JOIN은 USING 절로 처리
200
+ if (def.joins != null && def.joins.length > 0) {
201
+ const joinTables = def.joins.map((j) => {
202
+ const from = this.renderFrom(j.from);
203
+ return `${from} AS ${this.expr.wrap(j.as)}`;
204
+ });
205
+ sql += ` USING ${joinTables.join(", ")}`;
206
+ // JOIN ON 조건을 WHERE에 추가
207
+ const joinConditions = def.joins
208
+ .filter((j) => j.where != null && j.where.length > 0)
209
+ .map((j) => this.expr.renderWhere(j.where));
210
+ if (joinConditions.length > 0) {
211
+ const whereCondition = def.where != null && def.where.length > 0 ? this.expr.renderWhere(def.where) : null;
212
+ const allConditions = whereCondition != null ? [whereCondition, ...joinConditions] : joinConditions;
213
+ sql += ` WHERE ${allConditions.join(" AND ")}`;
214
+ }
215
+ else {
216
+ sql += this.renderWhere(def.where);
217
+ }
218
+ }
219
+ else {
220
+ sql += this.renderWhere(def.where);
221
+ }
222
+ // RETURNING (PostgreSQL: DELETE에서도 지원)
223
+ if (def.output != null) {
224
+ const outputCols = def.output.columns.map((c) => this.expr.wrap(c)).join(", ");
225
+ sql += ` RETURNING ${outputCols}`;
226
+ }
227
+ return { sql };
228
+ }
229
+ //#endregion
230
+ //#region ========== DML - UPSERT ==========
231
+ upsert(def) {
232
+ // PostgreSQL: CTE 방식 (ON CONFLICT는 단일 유니크 제약조건만 지원하므로 범용성을 위해 CTE 사용)
233
+ const table = this.tableName(def.table);
234
+ const alias = this.expr.wrap(def.existsSelectQuery.as);
235
+ // UPDATE SET 부분
236
+ const updateSetParts = Object.entries(def.updateRecord).map(([col, e]) => `${this.expr.wrap(col)} = ${this.expr.render(e)}`);
237
+ // INSERT 부분
238
+ const insertColumns = Object.keys(def.insertRecord);
239
+ const insertColList = insertColumns.map((c) => this.expr.wrap(c)).join(", ");
240
+ const insertValues = insertColumns.map((c) => this.expr.render(def.insertRecord[c])).join(", ");
241
+ // WHERE 조건
242
+ const whereCondition = def.existsSelectQuery.where != null && def.existsSelectQuery.where.length > 0
243
+ ? this.expr.renderWhere(def.existsSelectQuery.where)
244
+ : "TRUE";
245
+ // OUTPUT column 정의
246
+ const outputCols = def.output != null ? def.output.columns.map((c) => this.expr.wrap(c)).join(", ") : "*";
247
+ // CTE 기반 UPSERT
248
+ let sql = `WITH matched AS (\n`;
249
+ sql += ` SELECT ${alias}.* FROM ${table} AS ${alias} WHERE ${whereCondition}\n`;
250
+ sql += `),\n`;
251
+ sql += `updated AS (\n`;
252
+ sql += ` UPDATE ${table} AS ${alias} SET ${updateSetParts.join(", ")}\n`;
253
+ sql += ` WHERE ${whereCondition}\n`;
254
+ sql += ` RETURNING ${outputCols}\n`;
255
+ sql += `),\n`;
256
+ sql += `inserted AS (\n`;
257
+ sql += ` INSERT INTO ${table} (${insertColList})\n`;
258
+ sql += ` SELECT ${insertValues}\n`;
259
+ sql += ` WHERE NOT EXISTS (SELECT 1 FROM matched)\n`;
260
+ sql += ` RETURNING ${outputCols}\n`;
261
+ sql += `)\n`;
262
+ sql += `SELECT * FROM updated UNION ALL SELECT * FROM inserted`;
263
+ return { sql };
264
+ }
265
+ //#endregion
266
+ //#region ========== DDL - Table ==========
267
+ createTable(def) {
268
+ const table = this.tableName(def.table);
269
+ const colDefs = def.columns.map((col) => {
270
+ let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
271
+ // nullable: true → NULL, 아니면 → NOT NULL
272
+ if (col.nullable === true) {
273
+ colSql += " NULL";
274
+ }
275
+ else {
276
+ colSql += " NOT NULL";
277
+ }
278
+ if (col.autoIncrement) {
279
+ colSql += " GENERATED BY DEFAULT AS IDENTITY";
280
+ }
281
+ if (col.default !== undefined) {
282
+ colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
283
+ }
284
+ return colSql;
285
+ });
286
+ // CONSTRAINT 이름이 포함된 Primary Key
287
+ if (def.primaryKey != null && def.primaryKey.length > 0) {
288
+ const pkCols = def.primaryKey.map((c) => this.expr.wrap(c)).join(", ");
289
+ const pkName = this.expr.wrap(`PK_${def.table.name}`);
290
+ colDefs.push(`CONSTRAINT ${pkName} PRIMARY KEY (${pkCols})`);
291
+ }
292
+ return { sql: `CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n)` };
293
+ }
294
+ dropTable(def) {
295
+ return { sql: `DROP TABLE ${this.tableName(def.table)}` };
296
+ }
297
+ renameTable(def) {
298
+ return {
299
+ sql: `ALTER TABLE ${this.tableName(def.table)} RENAME TO ${this.expr.wrap(def.newName)}`,
300
+ };
301
+ }
302
+ truncate(def) {
303
+ // PostgreSQL: RESTART IDENTITY로 시퀀스 초기화
304
+ return { sql: `TRUNCATE TABLE ${this.tableName(def.table)} RESTART IDENTITY` };
305
+ }
306
+ //#endregion
307
+ //#region ========== DDL - Column ==========
308
+ addColumn(def) {
309
+ const table = this.tableName(def.table);
310
+ const col = def.column;
311
+ let colSql = `${this.expr.wrap(col.name)} ${this.expr.renderDataType(col.dataType)}`;
312
+ // nullable: true → NULL, else → NOT NULL
313
+ if (col.nullable === true) {
314
+ colSql += " NULL";
315
+ }
316
+ else {
317
+ colSql += " NOT NULL";
318
+ }
319
+ if (col.autoIncrement) {
320
+ colSql += " GENERATED BY DEFAULT AS IDENTITY";
321
+ }
322
+ if (col.default !== undefined) {
323
+ colSql += ` DEFAULT ${this.expr.escapeValue(col.default)}`;
324
+ }
325
+ return { sql: `ALTER TABLE ${table} ADD COLUMN ${colSql}` };
326
+ }
327
+ dropColumn(def) {
328
+ return {
329
+ sql: `ALTER TABLE ${this.tableName(def.table)} DROP COLUMN ${this.expr.wrap(def.column)}`,
330
+ };
331
+ }
332
+ modifyColumn(def) {
333
+ const table = this.tableName(def.table);
334
+ const col = def.column;
335
+ // PostgreSQL: ALTER COLUMN은 여러 ALTER 문이 필요
336
+ const parts = [];
337
+ // TYPE 변경
338
+ parts.push(`ALTER COLUMN ${this.expr.wrap(col.name)} TYPE ${this.expr.renderDataType(col.dataType)}`);
339
+ // NULL 제약조건 변경
340
+ if (col.nullable === false) {
341
+ parts.push(`ALTER COLUMN ${this.expr.wrap(col.name)} SET NOT NULL`);
342
+ }
343
+ else {
344
+ parts.push(`ALTER COLUMN ${this.expr.wrap(col.name)} DROP NOT NULL`);
345
+ }
346
+ // DEFAULT 변경
347
+ if (col.default !== undefined) {
348
+ parts.push(`ALTER COLUMN ${this.expr.wrap(col.name)} SET DEFAULT ${this.expr.escapeValue(col.default)}`);
349
+ }
350
+ return { sql: `ALTER TABLE ${table} ${parts.join(", ")}` };
351
+ }
352
+ renameColumn(def) {
353
+ const table = this.tableName(def.table);
354
+ return {
355
+ sql: `ALTER TABLE ${table} RENAME COLUMN ${this.expr.wrap(def.column)} TO ${this.expr.wrap(def.newName)}`,
356
+ };
357
+ }
358
+ //#endregion
359
+ //#region ========== DDL - Constraint ==========
360
+ addPrimaryKey(def) {
361
+ const table = this.tableName(def.table);
362
+ const cols = def.columns.map((c) => this.expr.wrap(c)).join(", ");
363
+ const pkName = `PK_${def.table.name}`;
364
+ return {
365
+ sql: `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(pkName)} PRIMARY KEY (${cols})`,
366
+ };
367
+ }
368
+ dropPrimaryKey(def) {
369
+ const table = this.tableName(def.table);
370
+ const pkName = `PK_${def.table.name}`;
371
+ return { sql: `ALTER TABLE ${table} DROP CONSTRAINT ${this.expr.wrap(pkName)}` };
372
+ }
373
+ addForeignKey(def) {
374
+ const table = this.tableName(def.table);
375
+ const fk = def.foreignKey;
376
+ const fkCols = fk.fkColumns.map((c) => this.expr.wrap(c)).join(", ");
377
+ const targetTable = this.tableName(fk.targetTable);
378
+ const targetCols = fk.targetPkColumns.map((c) => this.expr.wrap(c)).join(", ");
379
+ let sql = `ALTER TABLE ${table} ADD CONSTRAINT ${this.expr.wrap(fk.name)} FOREIGN KEY (${fkCols}) REFERENCES ${targetTable} (${targetCols})`;
380
+ // PostgreSQL: FK에 대한 별도 인덱스 생성 필요
381
+ const idxName = `IDX_${def.table.name}_${fk.name.replace(/^FK_/, "")}`;
382
+ sql += `;\nCREATE INDEX ${this.expr.wrap(idxName)} ON ${table} (${fkCols});`;
383
+ return { sql };
384
+ }
385
+ dropForeignKey(def) {
386
+ return {
387
+ sql: `ALTER TABLE ${this.tableName(def.table)} DROP CONSTRAINT ${this.expr.wrap(def.foreignKey)}`,
388
+ };
389
+ }
390
+ addIndex(def) {
391
+ const table = this.tableName(def.table);
392
+ const idx = def.index;
393
+ const cols = idx.columns.map((c) => `${this.expr.wrap(c.name)} ${c.orderBy}`).join(", ");
394
+ const unique = idx.unique ? "UNIQUE " : "";
395
+ return { sql: `CREATE ${unique}INDEX ${this.expr.wrap(idx.name)} ON ${table} (${cols})` };
396
+ }
397
+ dropIndex(def) {
398
+ // PostgreSQL: 인덱스는 스키마 수준에서 고유하므로 테이블명은 불필요하지만 스키마 지정 필요
399
+ const schema = def.table.schema ?? "public";
400
+ return { sql: `DROP INDEX ${this.expr.wrap(schema)}.${this.expr.wrap(def.index)}` };
401
+ }
402
+ //#endregion
403
+ //#region ========== DDL - View/Procedure ==========
404
+ createView(def) {
405
+ const view = this.tableName(def.view);
406
+ const selectSql = this.select(def.queryDef).sql;
407
+ return { sql: `CREATE OR REPLACE VIEW ${view} AS ${selectSql}` };
408
+ }
409
+ dropView(def) {
410
+ return { sql: `DROP VIEW IF EXISTS ${this.tableName(def.view)}` };
411
+ }
412
+ createProc(def) {
413
+ const proc = this.tableName(def.procedure);
414
+ // 파라미터 처리
415
+ const paramList = def.params
416
+ ?.map((p) => {
417
+ let sql = `${this.expr.wrap(p.name)} ${this.expr.renderDataType(p.dataType)}`;
418
+ if (p.default !== undefined) {
419
+ sql += ` DEFAULT ${this.expr.escapeValue(p.default)}`;
420
+ }
421
+ return sql;
422
+ })
423
+ .join(", ") ?? "";
424
+ // 반환 타입 처리
425
+ let returnClause = "VOID";
426
+ if (def.returns && def.returns.length > 0) {
427
+ const returnFields = def.returns
428
+ .map((r) => `${this.expr.wrap(r.name)} ${this.expr.renderDataType(r.dataType)}`)
429
+ .join(", ");
430
+ returnClause = `TABLE(${returnFields})`;
431
+ }
432
+ let sql = `CREATE OR REPLACE FUNCTION ${proc}(${paramList})\n`;
433
+ sql += `RETURNS ${returnClause} AS $$\n`;
434
+ sql += `BEGIN\n`;
435
+ sql += def.query;
436
+ if (!def.query.trim().endsWith(";")) {
437
+ sql += ";";
438
+ }
439
+ sql += `\nEND;\n`;
440
+ sql += `$$ LANGUAGE plpgsql`;
441
+ return { sql };
442
+ }
443
+ dropProc(def) {
444
+ return { sql: `DROP FUNCTION IF EXISTS ${this.tableName(def.procedure)}()` };
445
+ }
446
+ execProc(def) {
447
+ const proc = this.tableName(def.procedure);
448
+ if (def.params == null || Object.keys(def.params).length === 0) {
449
+ return { sql: `SELECT ${proc}()` };
450
+ }
451
+ const params = Object.values(def.params)
452
+ .map((p) => this.expr.render(p))
453
+ .join(", ");
454
+ return { sql: `SELECT ${proc}(${params})` };
455
+ }
456
+ //#endregion
457
+ //#region ========== Utils ==========
458
+ clearSchema(def) {
459
+ const schemaName = def.schema ?? "public";
460
+ // SQL 인젝션 방지: schema 이름 유효성 검사
461
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schemaName)) {
462
+ throw new Error(`잘못된 schema 이름: ${schemaName}`);
463
+ }
464
+ const schema = this.expr.escapeString(schemaName);
465
+ return {
466
+ sql: `
467
+ DO $$
468
+ DECLARE
469
+ r RECORD;
470
+ BEGIN
471
+ -- FK 제약조건 삭제
472
+ FOR r IN (SELECT conname, conrelid::regclass AS tablename
473
+ FROM pg_constraint
474
+ WHERE contype = 'f' AND connamespace = '${schema}'::regnamespace)
475
+ LOOP
476
+ EXECUTE 'ALTER TABLE ' || r.tablename || ' DROP CONSTRAINT ' || quote_ident(r.conname);
477
+ END LOOP;
478
+
479
+ -- 테이블 삭제
480
+ FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = '${schema}')
481
+ LOOP
482
+ EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
483
+ END LOOP;
484
+
485
+ -- 뷰 삭제
486
+ FOR r IN (SELECT viewname FROM pg_views WHERE schemaname = '${schema}')
487
+ LOOP
488
+ EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.viewname) || ' CASCADE';
489
+ END LOOP;
490
+
491
+ -- 함수 삭제
492
+ FOR r IN (SELECT proname, pg_get_function_identity_arguments(oid) AS args
493
+ FROM pg_proc WHERE pronamespace = '${schema}'::regnamespace)
494
+ LOOP
495
+ EXECUTE 'DROP FUNCTION IF EXISTS ' || quote_ident(r.proname) || '(' || r.args || ') CASCADE';
496
+ END LOOP;
497
+ END $$`,
498
+ };
499
+ }
500
+ schemaExists(def) {
501
+ const schemaName = def.schema ?? "public";
502
+ const schema = this.expr.escapeString(schemaName);
503
+ return { sql: `SELECT nspname FROM pg_namespace WHERE nspname = '${schema}'` };
504
+ }
505
+ switchFk(def) {
506
+ const table = this.tableName(def.table);
507
+ if (def.enabled) {
508
+ // PostgreSQL: 테이블의 모든 FK 트리거 활성화
509
+ return { sql: `ALTER TABLE ${table} ENABLE TRIGGER ALL` };
510
+ }
511
+ // PostgreSQL: 테이블의 모든 FK 트리거 비활성화
512
+ return { sql: `ALTER TABLE ${table} DISABLE TRIGGER ALL` };
513
+ }
460
514
  }
461
- export {
462
- PostgresqlQueryBuilder
463
- };
464
- //# sourceMappingURL=postgresql-query-builder.js.map
515
+ //# sourceMappingURL=postgresql-query-builder.js.map