sonamu 0.7.3 → 0.7.5

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 (133) hide show
  1. package/dist/api/config.d.ts +1 -4
  2. package/dist/api/config.d.ts.map +1 -1
  3. package/dist/api/config.js +1 -1
  4. package/dist/api/sonamu.d.ts +2 -0
  5. package/dist/api/sonamu.d.ts.map +1 -1
  6. package/dist/api/sonamu.js +19 -47
  7. package/dist/bin/cli.js +6 -6
  8. package/dist/database/base-model.d.ts +1 -1
  9. package/dist/database/base-model.d.ts.map +1 -1
  10. package/dist/database/base-model.js +15 -4
  11. package/dist/database/code-generator.d.ts.map +1 -1
  12. package/dist/database/code-generator.js +3 -3
  13. package/dist/database/db.d.ts.map +1 -1
  14. package/dist/database/db.js +1 -1
  15. package/dist/database/puri-wrapper.d.ts +11 -11
  16. package/dist/database/puri-wrapper.d.ts.map +1 -1
  17. package/dist/database/puri-wrapper.js +7 -11
  18. package/dist/database/puri.d.ts +36 -17
  19. package/dist/database/puri.d.ts.map +1 -1
  20. package/dist/database/puri.js +54 -7
  21. package/dist/database/puri.types.d.ts +54 -17
  22. package/dist/database/puri.types.d.ts.map +1 -1
  23. package/dist/database/puri.types.js +2 -4
  24. package/dist/database/puri.types.test-d.js +129 -0
  25. package/dist/database/upsert-builder.d.ts +16 -10
  26. package/dist/database/upsert-builder.d.ts.map +1 -1
  27. package/dist/database/upsert-builder.js +10 -19
  28. package/dist/entity/entity-manager.d.ts +113 -22
  29. package/dist/entity/entity-manager.d.ts.map +1 -1
  30. package/dist/entity/entity-manager.js +1 -1
  31. package/dist/entity/entity.d.ts +34 -0
  32. package/dist/entity/entity.d.ts.map +1 -1
  33. package/dist/entity/entity.js +110 -37
  34. package/dist/index.d.ts +5 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +8 -2
  37. package/dist/migration/code-generation.d.ts.map +1 -1
  38. package/dist/migration/code-generation.js +341 -149
  39. package/dist/migration/migration-set.d.ts.map +1 -1
  40. package/dist/migration/migration-set.js +21 -5
  41. package/dist/migration/migrator.d.ts.map +1 -1
  42. package/dist/migration/migrator.js +7 -1
  43. package/dist/migration/postgresql-schema-reader.d.ts +11 -1
  44. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
  45. package/dist/migration/postgresql-schema-reader.js +111 -10
  46. package/dist/syncer/syncer.d.ts.map +1 -1
  47. package/dist/syncer/syncer.js +5 -4
  48. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  49. package/dist/template/implementations/generated.template.js +12 -2
  50. package/dist/template/implementations/generated_sso.template.d.ts +3 -3
  51. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
  52. package/dist/template/implementations/generated_sso.template.js +50 -2
  53. package/dist/template/implementations/model.template.js +6 -6
  54. package/dist/template/implementations/model_test.template.js +4 -4
  55. package/dist/template/implementations/view_enums_dropdown.template.js +2 -2
  56. package/dist/template/implementations/view_enums_select.template.js +2 -2
  57. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  58. package/dist/template/implementations/view_form.template.js +12 -9
  59. package/dist/template/implementations/view_id_async_select.template.js +4 -4
  60. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  61. package/dist/template/implementations/view_list.template.js +12 -9
  62. package/dist/template/implementations/view_search_input.template.js +2 -2
  63. package/dist/template/template.js +2 -2
  64. package/dist/template/zod-converter.d.ts.map +1 -1
  65. package/dist/template/zod-converter.js +17 -2
  66. package/dist/testing/fixture-manager.d.ts +2 -1
  67. package/dist/testing/fixture-manager.d.ts.map +1 -1
  68. package/dist/testing/fixture-manager.js +29 -29
  69. package/dist/types/types.d.ts +593 -68
  70. package/dist/types/types.d.ts.map +1 -1
  71. package/dist/types/types.js +113 -9
  72. package/dist/vector/chunking.d.ts +25 -0
  73. package/dist/vector/chunking.d.ts.map +1 -0
  74. package/dist/vector/chunking.js +97 -0
  75. package/dist/vector/config.d.ts +12 -0
  76. package/dist/vector/config.d.ts.map +1 -0
  77. package/dist/vector/config.js +83 -0
  78. package/dist/vector/embedding.d.ts +42 -0
  79. package/dist/vector/embedding.d.ts.map +1 -0
  80. package/dist/vector/embedding.js +147 -0
  81. package/dist/vector/types.d.ts +105 -0
  82. package/dist/vector/types.d.ts.map +1 -0
  83. package/dist/vector/types.js +5 -0
  84. package/dist/vector/vector-search.d.ts +47 -0
  85. package/dist/vector/vector-search.d.ts.map +1 -0
  86. package/dist/vector/vector-search.js +176 -0
  87. package/package.json +11 -11
  88. package/src/api/config.ts +0 -4
  89. package/src/api/sonamu.ts +21 -36
  90. package/src/bin/cli.ts +5 -5
  91. package/src/database/base-model.ts +20 -11
  92. package/src/database/code-generator.ts +6 -2
  93. package/src/database/db.ts +1 -0
  94. package/src/database/puri-wrapper.ts +22 -16
  95. package/src/database/puri.ts +150 -27
  96. package/src/database/puri.types.test-d.ts +457 -0
  97. package/src/database/puri.types.ts +231 -33
  98. package/src/database/upsert-builder.ts +43 -34
  99. package/src/entity/entity-manager.ts +2 -2
  100. package/src/entity/entity.ts +134 -44
  101. package/src/index.ts +6 -0
  102. package/src/migration/code-generation.ts +377 -174
  103. package/src/migration/migration-set.ts +22 -3
  104. package/src/migration/migrator.ts +6 -0
  105. package/src/migration/postgresql-schema-reader.ts +121 -21
  106. package/src/syncer/syncer.ts +4 -3
  107. package/src/template/implementations/generated.template.ts +51 -9
  108. package/src/template/implementations/generated_sso.template.ts +71 -2
  109. package/src/template/implementations/model.template.ts +5 -5
  110. package/src/template/implementations/model_test.template.ts +3 -3
  111. package/src/template/implementations/view_enums_dropdown.template.ts +1 -1
  112. package/src/template/implementations/view_enums_select.template.ts +1 -1
  113. package/src/template/implementations/view_form.template.ts +11 -8
  114. package/src/template/implementations/view_id_async_select.template.ts +3 -3
  115. package/src/template/implementations/view_list.template.ts +11 -8
  116. package/src/template/implementations/view_search_input.template.ts +1 -1
  117. package/src/template/template.ts +1 -1
  118. package/src/template/zod-converter.ts +20 -0
  119. package/src/testing/fixture-manager.ts +31 -30
  120. package/src/types/types.ts +226 -48
  121. package/src/vector/chunking.ts +115 -0
  122. package/src/vector/config.ts +68 -0
  123. package/src/vector/embedding.ts +193 -0
  124. package/src/vector/types.ts +122 -0
  125. package/src/vector/vector-search.ts +261 -0
  126. package/dist/template/implementations/view_enums_buttonset.template.d.ts +0 -17
  127. package/dist/template/implementations/view_enums_buttonset.template.d.ts.map +0 -1
  128. package/dist/template/implementations/view_enums_buttonset.template.js +0 -31
  129. package/dist/template/implementations/view_list_columns.template.d.ts +0 -17
  130. package/dist/template/implementations/view_list_columns.template.d.ts.map +0 -1
  131. package/dist/template/implementations/view_list_columns.template.js +0 -49
  132. package/src/template/implementations/view_enums_buttonset.template.ts +0 -34
  133. package/src/template/implementations/view_list_columns.template.ts +0 -53
@@ -1,5 +1,5 @@
1
1
  import equal from "fast-deep-equal";
2
- import { alphabetical, diff, fork, omit } from "radashi";
2
+ import { alphabetical, diff, omit } from "radashi";
3
3
  import { Naite } from "..";
4
4
  import type {
5
5
  GenMigrationCode,
@@ -11,6 +11,16 @@ import type {
11
11
  import { formatCode } from "../utils/formatter";
12
12
  import { differenceWith, intersectionBy } from "../utils/utils";
13
13
 
14
+ /**
15
+ * 컬럼 정의 결과 타입
16
+ * - builder: Knex table builder 메서드로 실행할 구문 (table.xxx())
17
+ * - raw: knex.raw()로 실행할 구문
18
+ */
19
+ type ColumnDefinitionResult = {
20
+ builder: string[];
21
+ raw: string[];
22
+ };
23
+
14
24
  /**
15
25
  * 테이블 생성하는 케이스 - 컬럼/인덱스 생성
16
26
  */
@@ -19,11 +29,7 @@ async function generateCreateCode_ColumnAndIndexes(
19
29
  columns: MigrationColumn[],
20
30
  indexes: MigrationIndex[],
21
31
  ): Promise<GenMigrationCode> {
22
- // fulltext index 분리
23
- const [ngramIndexes, standardIndexes] = fork(
24
- indexes,
25
- (i) => i.type === "fulltext" && i.parser === "ngram",
26
- );
32
+ const columnDefs = genColumnDefinitions(table, columns);
27
33
 
28
34
  // 컬럼, 인덱스 처리
29
35
  const lines: string[] = [
@@ -31,14 +37,12 @@ async function generateCreateCode_ColumnAndIndexes(
31
37
  "",
32
38
  "export async function up(knex: Knex): Promise<void> {",
33
39
  `await knex.schema.createTable("${table}", (table) => {`,
34
- "// columns",
35
- ...genColumnDefinitions(columns),
36
- "",
37
- "// indexes",
38
- ...standardIndexes.map((index) => genIndexDefinition(index, table)),
40
+ ...columnDefs.builder,
39
41
  "});",
40
- // ngram은 knex.raw 처리하므로 createTable 밖에서 실행
41
- ...ngramIndexes.map((index) => genIndexDefinition(index, table)),
42
+ // raw 구문 (Generated Column 등)
43
+ ...columnDefs.raw,
44
+ // index는 knex.raw로 처리하므로 createTable 밖에서 실행
45
+ ...indexes.map((index) => genIndexDefinition(index, table)),
42
46
  "}",
43
47
  "",
44
48
  "export async function down(knex: Knex): Promise<void> {",
@@ -55,65 +59,133 @@ async function generateCreateCode_ColumnAndIndexes(
55
59
 
56
60
  /**
57
61
  * MigrationColumn[] 읽어서 컬럼 정의하는 구문 생성
62
+ * @returns builder: table builder 메서드, raw: knex.raw() 구문
58
63
  */
59
- function genColumnDefinitions(columns: MigrationColumn[]): string[] {
60
- return columns.map((column) => {
61
- const chains: string[] = [];
62
- if (column.name === "id") {
63
- return `table.increments().primary();`;
64
+ function genColumnDefinitions(table: string, columns: MigrationColumn[]): ColumnDefinitionResult {
65
+ const result: ColumnDefinitionResult = {
66
+ builder: [],
67
+ raw: [],
68
+ };
69
+
70
+ for (const column of columns) {
71
+ // Generated Column은 raw로 처리
72
+ if (column.generated) {
73
+ result.raw.push(genGeneratedColumnDefinition(table, column));
74
+ continue;
64
75
  }
65
76
 
66
- // 배열 타입 처리
67
- if (column.type.endsWith("[]")) {
68
- const elementType = column.type.slice(0, -2); // "integer[]" -> "integer"
69
- const pgType = getPgArrayType(column, elementType);
70
- chains.push(`specificType('${column.name}', '${pgType}')`);
71
- } else if (column.type === "numberOrNumeric") {
72
- // number
73
- if (column.numberType === "real") {
74
- chains.push(`float('${column.name}')`);
75
- } else if (column.numberType === "double precision") {
76
- chains.push(`double('${column.name}')`);
77
- } else if ((column.numberType ?? "numeric") === "numeric") {
78
- chains.push(`decimal('${column.name}', ${column.precision}, ${column.scale})`);
79
- }
80
- } else if (column.type === "string") {
81
- // string
82
- if (column.length !== undefined) {
83
- chains.push(`string('${column.name}', ${column.length})`);
84
- } else {
85
- chains.push(`text('${column.name}')`);
86
- }
87
- } else if (column.type === "date") {
88
- // date
89
- chains.push(`timestamp('${column.name}', { useTz: true })`);
90
- } else if (column.type === "json") {
91
- // json
92
- chains.push(`jsonb('${column.name}')`);
77
+ // 일반 컬럼은 builder로 처리
78
+ result.builder.push(genNormalColumnDefinition(column));
79
+ }
80
+
81
+ return result;
82
+ }
83
+
84
+ /**
85
+ * Generated Column 정의 생성 (ALTER TABLE ADD COLUMN 사용)
86
+ */
87
+ function genGeneratedColumnDefinition(table: string, column: MigrationColumn): string {
88
+ if (!column.generated) {
89
+ throw new Error("Generated column definition required");
90
+ }
91
+ const pgType = getPgTypeForColumn(column);
92
+ const storageType = column.generated.type === "VIRTUAL" ? " VIRTUAL" : " STORED";
93
+ const nullableClause = column.nullable ? "" : " NOT NULL";
94
+ return `await knex.raw(\`ALTER TABLE "${table}" ADD COLUMN "${column.name}" ${pgType} GENERATED ALWAYS AS (${column.generated.expression})${storageType}${nullableClause}\`);`;
95
+ }
96
+
97
+ /**
98
+ * 일반 컬럼 정의 생성 (table.xxx() 체인)
99
+ */
100
+ function genNormalColumnDefinition(column: MigrationColumn): string {
101
+ const chains: string[] = [];
102
+
103
+ if (column.name === "id") {
104
+ return `table.increments().primary();`;
105
+ }
106
+
107
+ // 배열 타입 처리
108
+ if (column.type.endsWith("[]")) {
109
+ const elementType = column.type.slice(0, -2); // "integer[]" -> "integer"
110
+ const pgType = getPgArrayType(column, elementType);
111
+ chains.push(`specificType('${column.name}', '${pgType}')`);
112
+ } else if (column.type === "vector") {
113
+ // Knex는 vector 타입을 직접 지원하지 않으므로 specificType 사용
114
+ chains.push(`specificType('${column.name}', 'vector(${column.dimensions})')`);
115
+ } else if (column.type === "numberOrNumeric") {
116
+ // number
117
+ if (column.numberType === "real") {
118
+ chains.push(`float('${column.name}')`);
119
+ } else if (column.numberType === "double precision") {
120
+ chains.push(`double('${column.name}')`);
121
+ } else if ((column.numberType ?? "numeric") === "numeric") {
122
+ chains.push(`decimal('${column.name}', ${column.precision}, ${column.scale})`);
123
+ }
124
+ } else if (column.type === "string") {
125
+ // string
126
+ if (column.length !== undefined) {
127
+ chains.push(`string('${column.name}', ${column.length})`);
93
128
  } else {
94
- // type, length
95
- let extraType: string | undefined;
96
- chains.push(
97
- `${column.type}('${column.name}'${
98
- column.length ? `, ${column.length}` : ""
99
- }${extraType ? `, '${extraType}'` : ""})`,
100
- );
129
+ chains.push(`text('${column.name}')`);
101
130
  }
131
+ } else if (column.type === "date") {
132
+ // date
133
+ chains.push(`timestamp('${column.name}', { useTz: true })`);
134
+ } else if (column.type === "json") {
135
+ // json
136
+ chains.push(`jsonb('${column.name}')`);
137
+ } else {
138
+ // type, length
139
+ let extraType: string | undefined;
140
+ chains.push(
141
+ `${column.type}('${column.name}'${
142
+ column.length ? `, ${column.length}` : ""
143
+ }${extraType ? `, '${extraType}'` : ""})`,
144
+ );
145
+ }
102
146
 
103
- // nullable
104
- chains.push(column.nullable ? "nullable()" : "notNullable()");
147
+ // nullable
148
+ chains.push(column.nullable ? "nullable()" : "notNullable()");
105
149
 
106
- // defaultTo
107
- if (column.defaultTo !== undefined) {
108
- if (typeof column.defaultTo === "string" && column.defaultTo.startsWith(`"`)) {
109
- chains.push(`defaultTo(${column.defaultTo})`);
110
- } else {
111
- chains.push(`defaultTo(knex.raw('${column.defaultTo}'))`);
112
- }
150
+ // defaultTo
151
+ if (column.defaultTo !== undefined) {
152
+ if (typeof column.defaultTo === "string" && column.defaultTo.startsWith(`"`)) {
153
+ chains.push(`defaultTo(${column.defaultTo})`);
154
+ } else {
155
+ chains.push(`defaultTo(knex.raw('${column.defaultTo}'))`);
113
156
  }
157
+ }
114
158
 
115
- return `table.${chains.join(".")};`;
116
- });
159
+ return `table.${chains.join(".")};`;
160
+ }
161
+
162
+ /**
163
+ * MigrationColumn의 타입을 PostgreSQL 타입 문자열로 변환
164
+ */
165
+ function getPgTypeForColumn(column: MigrationColumn): string {
166
+ if (column.type.endsWith("[]")) {
167
+ const elementType = column.type.slice(0, -2);
168
+ return getPgArrayType(column, elementType);
169
+ }
170
+
171
+ switch (column.type) {
172
+ case "string":
173
+ return column.length !== undefined ? `varchar(${column.length})` : "text";
174
+ case "bigInteger":
175
+ return "bigint";
176
+ case "numberOrNumeric":
177
+ if (column.numberType === "real") return "real";
178
+ if (column.numberType === "double precision") return "double precision";
179
+ return `numeric(${column.precision}, ${column.scale})`;
180
+ case "date":
181
+ return "timestamptz";
182
+ case "json":
183
+ return "jsonb";
184
+ case "vector":
185
+ return `vector(${column.dimensions})`;
186
+ default:
187
+ return column.type;
188
+ }
117
189
  }
118
190
 
119
191
  function getPgArrayType(column: MigrationColumn, elementType: string): string {
@@ -131,6 +203,7 @@ function getPgArrayType(column: MigrationColumn, elementType: string): string {
131
203
  if (elementType === "boolean") return "boolean[]";
132
204
  if (elementType === "uuid") return "uuid[]";
133
205
  if (elementType === "enum") return "text[]";
206
+ if (elementType === "vector") return `vector(${column.dimensions})[]`;
134
207
 
135
208
  throw new Error(`Unknown array element type: ${elementType}`);
136
209
  }
@@ -138,23 +211,64 @@ function getPgArrayType(column: MigrationColumn, elementType: string): string {
138
211
  /**
139
212
  * 개별 인덱스 정의 생성
140
213
  */
141
- function genIndexDefinition(index: MigrationIndex, table: string) {
214
+ function genIndexDefinition(index: MigrationIndex, table: string): string {
215
+ if (index.type === "hnsw" || index.type === "ivfflat") {
216
+ return genVectorIndexDefinition(index, table);
217
+ }
218
+
142
219
  const methodMap = {
143
- index: "index",
144
- fulltext: "index",
145
- unique: "unique",
220
+ index: "INDEX",
221
+ fulltext: "INDEX",
222
+ unique: "UNIQUE INDEX",
146
223
  };
147
224
 
148
- if (index.type === "fulltext" && index.parser === "ngram") {
149
- return `await knex.raw(\`ALTER TABLE ${table} ADD FULLTEXT INDEX ${index.name} (${index.columns.join(
150
- ", ",
151
- )}) WITH PARSER ngram\`);`;
225
+ const nullsNotDistinctClause =
226
+ index.nullsNotDistinct === undefined
227
+ ? ""
228
+ : ` NULLS ${index.nullsNotDistinct ? "NOT DISTINCT" : "DISTINCT"}`;
229
+
230
+ return `await knex.raw(
231
+ \`CREATE ${methodMap[index.type]} ${index.name} ON ${table} (${index.columns
232
+ .map((col) => {
233
+ const sortOrderClause = col.sortOrder === undefined ? "" : ` ${col.sortOrder}`;
234
+ const nullsFirstClause =
235
+ col.nullsFirst === undefined ? "" : ` NULLS ${col.nullsFirst ? "FIRST" : "LAST"}`;
236
+ return `${col.name}${sortOrderClause}${nullsFirstClause}`;
237
+ })
238
+ .join(", ")})${nullsNotDistinctClause};\`
239
+ );`;
240
+ }
241
+
242
+ /**
243
+ * @description
244
+ * - HNSW (Hierarchical Navigable Small World): 느린 빌드, 빠른 검색 속도, 높은 메모리 및 정확도
245
+ * - IVFFlat (Inverted File with Flat Compression): 빠른 빌드, 중간 검색 속도, 낮은 메모리
246
+ *
247
+ * @example
248
+ * // HNSW 인덱스 (권장 - 빠른 검색, 높은 정확도)
249
+ * CREATE INDEX idx_embedding ON items USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
250
+ *
251
+ * // IVFFlat 인덱스 (대용량 데이터, 비용 중요 시)
252
+ * CREATE INDEX idx_embedding ON items USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
253
+ */
254
+ function genVectorIndexDefinition(index: MigrationIndex, table: string): string {
255
+ const column = index.columns[0];
256
+ const vectorOps = column.vectorOps ?? "vector_cosine_ops";
257
+
258
+ // HNSW (Hierarchical Navigable Small World) - 권장: 빠른 검색, 높은 정확도
259
+ if (index.type === "hnsw") {
260
+ const m = index.m ?? 16;
261
+ const efConstruction = index.efConstruction ?? 64;
262
+ return `await knex.raw(\`CREATE INDEX ${index.name} ON ${table} USING hnsw (${column.name} ${vectorOps}) WITH (m = ${m}, ef_construction = ${efConstruction})\`);`;
152
263
  }
153
264
 
154
- return `table.${methodMap[index.type]}([${index.columns
155
- .map((col) => `'${col}'`)
156
- .join(",")}], '${index.name}'${index.type === "fulltext" ? ", 'FULLTEXT'" : ""}
157
- );`;
265
+ // IVFFlat (Inverted File with Flat Compression) - 대용량, 비용 중요 시
266
+ if (index.type === "ivfflat") {
267
+ const lists = index.lists ?? 100;
268
+ return `await knex.raw(\`CREATE INDEX ${index.name} ON ${table} USING ivfflat (${column.name} ${vectorOps}) WITH (lists = ${lists})\`);`;
269
+ }
270
+
271
+ throw new Error(`Unknown raw SQL index type: ${index.type}`);
158
272
  }
159
273
 
160
274
  /**
@@ -268,28 +382,23 @@ async function generateAlterCode_ColumnAndIndexes(
268
382
  // 인덱스의 add, drop 여부 확인
269
383
  const alterIndexesTo = getAlterIndexesTo(entityIndexes, dbIndexes);
270
384
 
271
- // fulltext index 분리
272
- const [ngramIndexes, standardIndexes] = fork(
273
- alterIndexesTo.add,
274
- (i) => i.type === "fulltext" && i.parser === "ngram",
275
- );
276
-
277
385
  // 인덱스가 삭제되는 경우, 컬럼과 같이 삭제된 케이스에는 drop에서 제외해야함!
278
386
  const indexNeedsToDrop = alterIndexesTo.drop.filter(
279
387
  (index) =>
280
- index.columns.every((colName) =>
281
- alterColumnsTo.drop.map((col) => col.name).includes(colName),
388
+ index.columns.every(({ name }) =>
389
+ alterColumnsTo.drop.map((col) => col.name).includes(name),
282
390
  ) === false,
283
391
  );
284
392
 
285
393
  // 빈 코드 생성 방지
286
- if (
287
- alterColumnLinesTo.add.up.length === 0 &&
288
- alterColumnLinesTo.drop.up.length === 0 &&
289
- alterColumnLinesTo.alter.up.length === 0 &&
290
- standardIndexes.length === 0 &&
291
- indexNeedsToDrop.length === 0
292
- ) {
394
+ const hasUpChanges =
395
+ alterColumnLinesTo.add.up.builder.length > 0 ||
396
+ alterColumnLinesTo.add.up.raw.length > 0 ||
397
+ alterColumnLinesTo.drop.up.builder.length > 0 ||
398
+ alterColumnLinesTo.alter.up.builder.length > 0 ||
399
+ alterIndexesTo.add.length > 0 ||
400
+ indexNeedsToDrop.length > 0;
401
+ if (!hasUpChanges) {
293
402
  Naite.t("migrator:generateAlterCode_ColumnAndIndexes:emptyCodeGenerationError", {
294
403
  entityColumns,
295
404
  dbColumns,
@@ -304,48 +413,65 @@ async function generateAlterCode_ColumnAndIndexes(
304
413
  "alterColumnsTo.alter.length": alterColumnsTo.alter.length,
305
414
  "alterIndexesTo.add.length": alterIndexesTo.add.length,
306
415
  "alterIndexesTo.drop.length": alterIndexesTo.drop.length,
307
- "standardIndexes.length": standardIndexes.length,
308
416
  "indexNeedsToDrop.length": indexNeedsToDrop.length,
309
417
  });
310
418
  // Naite.t("migrator:generateAlterCode_ColumnAndIndexes:alterColumnsTo", alterColumnsTo);
311
419
 
312
420
  // TODO: 인덱스명 변경된 경우 처리
313
421
 
314
- const lines: string[] = [
315
- 'import { Knex } from "knex";',
316
- "",
317
- "export async function up(knex: Knex): Promise<void> {",
318
- `await knex.schema.alterTable("${table}", (table) => {`,
319
- // 1. add column
320
- ...(alterColumnsTo.add.length > 0 ? alterColumnLinesTo.add.up : []),
321
- // 2. drop column
322
- ...(alterColumnsTo.drop.length > 0 ? alterColumnLinesTo.drop.up : []),
323
- // 3. alter column
324
- ...(alterColumnsTo.alter.length > 0 ? alterColumnLinesTo.alter.up : []),
325
- // 4. add index
326
- ...standardIndexes.map((index) => genIndexDefinition(index, table)),
327
- // 5. drop index
422
+ // table builder 메서드로 실행할 코드 (drop → add → alter 순서)
423
+ const upBuilderLines = [
424
+ ...(alterColumnLinesTo.drop.up.builder.length > 0 ? alterColumnLinesTo.drop.up.builder : []),
425
+ ...(alterColumnLinesTo.add.up.builder.length > 0 ? alterColumnLinesTo.add.up.builder : []),
426
+ ...(alterColumnLinesTo.alter.up.builder.length > 0 ? alterColumnLinesTo.alter.up.builder : []),
328
427
  ...indexNeedsToDrop.map(genIndexDropDefinition),
329
- "});",
330
- // ngram은 knex.raw로 처리하므로 alterTable 밖에서 실행
331
- ...ngramIndexes.map((index) => genIndexDefinition(index, table)),
332
- "}",
333
- "",
334
- "export async function down(knex: Knex): Promise<void> {",
335
- `return knex.schema.alterTable("${table}", (table) => {`,
336
- ...(alterColumnsTo.add.length > 0 ? alterColumnLinesTo.add.down : []),
337
- ...(alterColumnsTo.drop.length > 0 ? alterColumnLinesTo.drop.down : []),
338
- ...(alterColumnsTo.alter.length > 0 ? alterColumnLinesTo.alter.down : []),
428
+ ];
429
+
430
+ // knex.raw() 실행할 코드
431
+ const upRawLines = [
432
+ ...(alterColumnLinesTo.add.up.raw.length > 0 ? alterColumnLinesTo.add.up.raw : []),
433
+ ...alterIndexesTo.add.map((index) => genIndexDefinition(index, table)),
434
+ ];
435
+
436
+ // down은 up의 역순 (add.down = drop rollback, drop.down = add rollback)
437
+ const downBuilderLines = [
438
+ ...(alterColumnLinesTo.add.down.builder.length > 0 ? alterColumnLinesTo.add.down.builder : []),
439
+ ...(alterColumnLinesTo.alter.down.builder.length > 0
440
+ ? alterColumnLinesTo.alter.down.builder
441
+ : []),
442
+ ...(alterColumnLinesTo.drop.down.builder.length > 0
443
+ ? alterColumnLinesTo.drop.down.builder
444
+ : []),
339
445
  ...alterIndexesTo.add
340
446
  .filter(
341
447
  (index) =>
342
- index.columns.every((colName) =>
343
- alterColumnsTo.add.map((col) => col.name).includes(colName),
448
+ index.columns.every((indexCol) =>
449
+ alterColumnsTo.add.map((col) => col.name).includes(indexCol.name),
344
450
  ) === false,
345
451
  )
346
452
  .map(genIndexDropDefinition),
453
+ ];
454
+
455
+ const downRawLines = [
456
+ ...(alterColumnLinesTo.drop.down.raw.length > 0 ? alterColumnLinesTo.drop.down.raw : []),
347
457
  ...indexNeedsToDrop.map((index) => genIndexDefinition(index, table)),
348
- "});",
458
+ ];
459
+
460
+ const lines: string[] = [
461
+ 'import { Knex } from "knex";',
462
+ "",
463
+ "export async function up(knex: Knex): Promise<void> {",
464
+ ...(upBuilderLines.length > 0
465
+ ? [`await knex.schema.alterTable("${table}", (table) => {`, ...upBuilderLines, "});"]
466
+ : []),
467
+ ...upRawLines,
468
+ "}",
469
+ "",
470
+ "export async function down(knex: Knex): Promise<void> {",
471
+ ...(downBuilderLines.length > 0
472
+ ? [`await knex.schema.alterTable("${table}", (table) => {`, ...downBuilderLines, "});"]
473
+ : []),
474
+ ...downRawLines,
349
475
  "}",
350
476
  ];
351
477
 
@@ -374,6 +500,22 @@ async function generateAlterCode_ColumnAndIndexes(
374
500
  ];
375
501
  }
376
502
 
503
+ /**
504
+ * 컬럼 비교를 위해 Generated Column의 expression을 제외한 객체를 생성
505
+ */
506
+ function normalizeColumnForComparison(col: MigrationColumn): MigrationColumn {
507
+ if (col.generated) {
508
+ return {
509
+ ...col,
510
+ generated: {
511
+ type: col.generated.type,
512
+ expression: "",
513
+ },
514
+ };
515
+ }
516
+ return col;
517
+ }
518
+
377
519
  /**
378
520
  * 각 컬럼 이름 기준으로 add, drop, alter 여부 확인
379
521
  */
@@ -386,8 +528,8 @@ function getAlterColumnsTo(entityColumns: MigrationColumn[], dbColumns: Migratio
386
528
 
387
529
  // 컬럼명 기준 비교
388
530
  const extraColumns = {
389
- db: diff(dbColumns, entityColumns, (col) => col.name),
390
- entity: diff(entityColumns, dbColumns, (col) => col.name),
531
+ db: diff(dbColumns, entityColumns, (col) => [col.name, col.generated?.type].join("///")),
532
+ entity: diff(entityColumns, dbColumns, (col) => [col.name, col.generated?.type].join("///")),
391
533
  };
392
534
  if (extraColumns.entity.length > 0) {
393
535
  columnsTo.add = columnsTo.add.concat(extraColumns.entity);
@@ -396,10 +538,14 @@ function getAlterColumnsTo(entityColumns: MigrationColumn[], dbColumns: Migratio
396
538
  columnsTo.drop = columnsTo.drop.concat(extraColumns.db);
397
539
  }
398
540
 
399
- // 동일 컬럼명의 세부 필드 비교
541
+ // 동일 컬럼명의 세부 필드 비교 (Generated Column expression 제외)
400
542
  const sameDbColumns = intersectionBy(dbColumns, entityColumns, (col) => col.name);
401
543
  const sameMdColumns = intersectionBy(entityColumns, dbColumns, (col) => col.name);
402
- columnsTo.alter = differenceWith(sameDbColumns, sameMdColumns, (a, b) => equal(a, b));
544
+ columnsTo.alter = differenceWith(
545
+ sameDbColumns,
546
+ sameMdColumns,
547
+ (a, b) => equal({ ...a, generated: undefined }, { ...b, generated: undefined }), // generated 컬럼은 alter로 처리하지 않음
548
+ );
403
549
 
404
550
  return columnsTo;
405
551
  }
@@ -415,25 +561,34 @@ function getAlterColumnLinesTo(
415
561
  ) {
416
562
  const linesTo = {
417
563
  add: {
418
- up: [] as string[],
419
- down: [] as string[],
564
+ up: { builder: [] as string[], raw: [] as string[] },
565
+ down: { builder: [] as string[], raw: [] as string[] },
420
566
  },
421
567
  drop: {
422
- up: [] as string[],
423
- down: [] as string[],
568
+ up: { builder: [] as string[], raw: [] as string[] },
569
+ down: { builder: [] as string[], raw: [] as string[] },
424
570
  },
425
571
  alter: {
426
- up: [] as string[],
427
- down: [] as string[],
572
+ up: { builder: [] as string[], raw: [] as string[] },
573
+ down: { builder: [] as string[], raw: [] as string[] },
428
574
  },
429
575
  };
430
576
 
431
- linesTo.add = {
432
- up: ["// add", ...genColumnDefinitions(columnsTo.add)],
433
- down: [
434
- "// rollback - add",
435
- `table.dropColumns(${columnsTo.add.map((col) => `'${col.name}'`).join(", ")})`,
436
- ],
577
+ // add columns
578
+ const addColumnDefs = genColumnDefinitions(table, columnsTo.add);
579
+ linesTo.add.up = {
580
+ builder: addColumnDefs.builder.length > 0 ? ["// add", ...addColumnDefs.builder] : [],
581
+ raw: addColumnDefs.raw.length > 0 ? ["// add (generated)", ...addColumnDefs.raw] : [],
582
+ };
583
+ linesTo.add.down = {
584
+ builder:
585
+ columnsTo.add.length > 0
586
+ ? [
587
+ "// rollback - add",
588
+ `table.dropColumns(${columnsTo.add.map((col) => `'${col.name}'`).join(", ")})`,
589
+ ]
590
+ : [],
591
+ raw: [],
437
592
  };
438
593
 
439
594
  // drop할 컬럼에 걸린 FK 찾기
@@ -449,20 +604,38 @@ function getAlterColumnLinesTo(
449
604
 
450
605
  const restoreFkLines = genForeignDefinitions(table, fkToDropBeforeColumn).up;
451
606
 
607
+ // drop의 rollback시에는 generated column도 복원해야 함
608
+ const dropColumnDefs = genColumnDefinitions(table, columnsTo.drop);
452
609
  linesTo.drop = {
453
- up: [
454
- ...(dropFkLines.length > 0
455
- ? ["// drop foreign keys on columns to be dropped", ...dropFkLines]
456
- : []),
457
- "// drop columns",
458
- `table.dropColumns(${columnsTo.drop.map((col) => `'${col.name}'`).join(", ")})`,
459
- ],
460
- down: [
461
- "// rollback - drop columns",
462
- ...genColumnDefinitions(columnsTo.drop),
463
- ...(restoreFkLines.length > 0 ? ["// restore foreign keys", ...restoreFkLines] : []),
464
- ],
610
+ up: {
611
+ builder: [
612
+ ...(dropFkLines.length > 0
613
+ ? ["// drop foreign keys on columns to be dropped", ...dropFkLines]
614
+ : []),
615
+ ...(columnsTo.drop.length > 0
616
+ ? [
617
+ "// drop columns",
618
+ `table.dropColumns(${columnsTo.drop.map((col) => `'${col.name}'`).join(", ")})`,
619
+ ]
620
+ : []),
621
+ ],
622
+ raw: [],
623
+ },
624
+ down: {
625
+ builder: [
626
+ ...(dropColumnDefs.builder.length > 0
627
+ ? ["// rollback - drop columns", ...dropColumnDefs.builder]
628
+ : []),
629
+ ...(restoreFkLines.length > 0 ? ["// restore foreign keys", ...restoreFkLines] : []),
630
+ ],
631
+ raw:
632
+ dropColumnDefs.raw.length > 0
633
+ ? ["// rollback - drop columns (generated)", ...dropColumnDefs.raw]
634
+ : [],
635
+ },
465
636
  };
637
+
638
+ // alter columns (Generated Column은 ALTER 불가하므로 drop 후 재생성)
466
639
  linesTo.alter = columnsTo.alter.reduce(
467
640
  (r, dbColumn) => {
468
641
  const entityColumn = entityColumns.find((col) => col.name === dbColumn.name);
@@ -472,21 +645,21 @@ function getAlterColumnLinesTo(
472
645
 
473
646
  // 컬럼 변경사항
474
647
  const columnDiffUp = diff(
475
- genColumnDefinitions([entityColumn]),
476
- genColumnDefinitions([dbColumn]),
648
+ genColumnDefinitions(table, [entityColumn]).builder,
649
+ genColumnDefinitions(table, [dbColumn]).builder,
477
650
  );
478
651
  const columnDiffDown = diff(
479
- genColumnDefinitions([dbColumn]),
480
- genColumnDefinitions([entityColumn]),
652
+ genColumnDefinitions(table, [dbColumn]).builder,
653
+ genColumnDefinitions(table, [entityColumn]).builder,
481
654
  );
482
655
  if (columnDiffUp.length > 0) {
483
- r.up = [
484
- ...r.up,
656
+ r.up.builder = [
657
+ ...r.up.builder,
485
658
  "// alter column",
486
659
  ...columnDiffUp.map((l) => `${l.replace(";", "")}.alter();`),
487
660
  ];
488
- r.down = [
489
- ...r.down,
661
+ r.down.builder = [
662
+ ...r.down.builder,
490
663
  "// rollback - alter column",
491
664
  ...columnDiffDown.map((l) => `${l.replace(";", "")}.alter();`),
492
665
  ];
@@ -495,8 +668,8 @@ function getAlterColumnLinesTo(
495
668
  return r;
496
669
  },
497
670
  {
498
- up: [] as string[],
499
- down: [] as string[],
671
+ up: { builder: [] as string[], raw: [] as string[] },
672
+ down: { builder: [] as string[], raw: [] as string[] },
500
673
  },
501
674
  );
502
675
 
@@ -512,9 +685,29 @@ function getAlterIndexesTo(entityIndexes: MigrationIndex[], dbIndexes: Migration
512
685
  add: [] as MigrationIndex[],
513
686
  drop: [] as MigrationIndex[],
514
687
  };
688
+
689
+ // 인덱스 고유 식별자 생성 (name을 제외한 모든 필드를 문자열로 변환하여 조합)
690
+ const identity = <T extends Record<string, unknown>>(index: T): string => {
691
+ const keys = Object.keys(index)
692
+ .filter((key) => key !== "name")
693
+ .sort();
694
+
695
+ return keys
696
+ .map((key) => {
697
+ if (key === "name") {
698
+ return undefined;
699
+ }
700
+ if (key === "columns") {
701
+ return (index[key] as MigrationIndex["columns"]).flatMap(identity);
702
+ }
703
+ return `${key}=${index[key as keyof MigrationIndex]}`;
704
+ })
705
+ .join("//");
706
+ };
707
+
515
708
  const extraIndexes = {
516
- db: diff(dbIndexes, entityIndexes, (col) => [col.type, col.columns.join("-")].join("//")),
517
- entity: diff(entityIndexes, dbIndexes, (col) => [col.type, col.columns.join("-")].join("//")),
709
+ db: diff(dbIndexes, entityIndexes.map(setMigrationIndexDefaults), identity),
710
+ entity: diff(entityIndexes.map(setMigrationIndexDefaults), dbIndexes, identity),
518
711
  };
519
712
  if (extraIndexes.entity.length > 0) {
520
713
  indexesTo.add = indexesTo.add.concat(extraIndexes.entity);
@@ -530,17 +723,27 @@ function getAlterIndexesTo(entityIndexes: MigrationIndex[], dbIndexes: Migration
530
723
  * 인덱스 삭제 정의 생성
531
724
  */
532
725
  function genIndexDropDefinition(index: MigrationIndex) {
533
- const methodMap = {
534
- index: "Index",
535
- fulltext: "Index",
536
- unique: "Unique",
537
- };
538
-
539
- return `table.drop${methodMap[index.type]}([${index.columns
540
- .map((columnName) => `'${columnName}'`)
726
+ return `table.dropIndex([${index.columns
727
+ .map((column) => `'${column.name}'`)
541
728
  .join(",")}], '${index.name}')`;
542
729
  }
543
730
 
731
+ /**
732
+ * DB 조회 결과와 비교하기 위한 인덱스 기본값 설정
733
+ */
734
+ function setMigrationIndexDefaults(index: MigrationIndex): MigrationIndex {
735
+ return {
736
+ ...index,
737
+ columns: index.columns.map((col) => ({
738
+ ...col,
739
+ sortOrder: col.sortOrder ?? "ASC",
740
+ // sortOrder에 따라 nullsFirst의 default 값 설정
741
+ nullsFirst: col.nullsFirst ?? col.sortOrder === "DESC",
742
+ })),
743
+ nullsNotDistinct: index.nullsNotDistinct ?? false,
744
+ };
745
+ }
746
+
544
747
  /**
545
748
  * 테이블 변경 케이스 - Foreign Key 변경
546
749
  */
@@ -724,12 +927,9 @@ export async function generateAlterCode(
724
927
  console.debug({ entityColumn, dbColumn });
725
928
  */
726
929
 
727
- const entityIndexes = alphabetical(entitySet.indexes, (a) =>
728
- [a.type, ...a.columns].join("-"),
729
- );
730
- const dbIndexes = alphabetical(dbSet.indexes, (a) =>
731
- [a.type, ...a.columns].join("-"),
732
- );
930
+ // ?
931
+ const entityIndexes = alphabetical(entitySet.indexes, (a) => [a.type, ...a.columns].join("-"));
932
+ const dbIndexes = alphabetical(dbSet.indexes, (a) => [a.type, ...a.columns].join("-"));
733
933
 
734
934
  const replaceNoActionOnMySQL = (f: MigrationForeign) => {
735
935
  // MySQL에서 RESTRICT와 NO ACTION은 동일함
@@ -754,9 +954,12 @@ export async function generateAlterCode(
754
954
  const alterCodes: (GenMigrationCode | GenMigrationCode[] | null)[] = [];
755
955
 
756
956
  // 1. columnsAndIndexes 처리
757
- const isEqualColumns = equal(entityColumns, dbColumns);
957
+ const isEqualColumns = equal(
958
+ entityColumns.map(normalizeColumnForComparison),
959
+ dbColumns.map(normalizeColumnForComparison),
960
+ );
758
961
  const isEqualIndexes = equal(
759
- entityIndexes.map((index) => omit(index, ["parser"])),
962
+ entityIndexes.map((index) => omit(index, ["parser"])).map(setMigrationIndexDefaults),
760
963
  dbIndexes,
761
964
  );
762
965
  if (!isEqualColumns || !isEqualIndexes) {