sonamu 0.7.4 → 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.
- package/dist/api/config.d.ts +1 -4
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/sonamu.d.ts +2 -0
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +19 -47
- package/dist/bin/cli.js +6 -6
- package/dist/database/base-model.d.ts +1 -1
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +15 -4
- package/dist/database/code-generator.d.ts.map +1 -1
- package/dist/database/code-generator.js +3 -3
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +1 -1
- package/dist/database/puri-wrapper.d.ts +11 -11
- package/dist/database/puri-wrapper.d.ts.map +1 -1
- package/dist/database/puri-wrapper.js +7 -11
- package/dist/database/puri.d.ts +36 -17
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +54 -7
- package/dist/database/puri.types.d.ts +54 -17
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +2 -4
- package/dist/database/puri.types.test-d.js +129 -0
- package/dist/database/upsert-builder.d.ts +16 -10
- package/dist/database/upsert-builder.d.ts.map +1 -1
- package/dist/database/upsert-builder.js +10 -19
- package/dist/entity/entity-manager.d.ts +113 -22
- package/dist/entity/entity-manager.d.ts.map +1 -1
- package/dist/entity/entity-manager.js +1 -1
- package/dist/entity/entity.d.ts +34 -0
- package/dist/entity/entity.d.ts.map +1 -1
- package/dist/entity/entity.js +110 -37
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +341 -149
- package/dist/migration/migration-set.d.ts.map +1 -1
- package/dist/migration/migration-set.js +21 -5
- package/dist/migration/migrator.d.ts.map +1 -1
- package/dist/migration/migrator.js +7 -1
- package/dist/migration/postgresql-schema-reader.d.ts +11 -1
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/postgresql-schema-reader.js +111 -10
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/syncer/syncer.js +4 -3
- package/dist/template/implementations/generated.template.d.ts.map +1 -1
- package/dist/template/implementations/generated.template.js +12 -2
- package/dist/template/implementations/generated_sso.template.d.ts +3 -3
- package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
- package/dist/template/implementations/generated_sso.template.js +50 -2
- package/dist/template/implementations/model.template.js +6 -6
- package/dist/template/implementations/model_test.template.js +4 -4
- package/dist/template/implementations/view_enums_dropdown.template.js +2 -2
- package/dist/template/implementations/view_enums_select.template.js +2 -2
- package/dist/template/implementations/view_form.template.d.ts.map +1 -1
- package/dist/template/implementations/view_form.template.js +12 -9
- package/dist/template/implementations/view_id_async_select.template.js +4 -4
- package/dist/template/implementations/view_list.template.d.ts.map +1 -1
- package/dist/template/implementations/view_list.template.js +12 -9
- package/dist/template/implementations/view_search_input.template.js +2 -2
- package/dist/template/template.js +2 -2
- package/dist/template/zod-converter.d.ts.map +1 -1
- package/dist/template/zod-converter.js +17 -2
- package/dist/testing/fixture-manager.d.ts +2 -1
- package/dist/testing/fixture-manager.d.ts.map +1 -1
- package/dist/testing/fixture-manager.js +29 -29
- package/dist/types/types.d.ts +593 -68
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +113 -9
- package/dist/vector/chunking.d.ts +25 -0
- package/dist/vector/chunking.d.ts.map +1 -0
- package/dist/vector/chunking.js +97 -0
- package/dist/vector/config.d.ts +12 -0
- package/dist/vector/config.d.ts.map +1 -0
- package/dist/vector/config.js +83 -0
- package/dist/vector/embedding.d.ts +42 -0
- package/dist/vector/embedding.d.ts.map +1 -0
- package/dist/vector/embedding.js +147 -0
- package/dist/vector/types.d.ts +105 -0
- package/dist/vector/types.d.ts.map +1 -0
- package/dist/vector/types.js +5 -0
- package/dist/vector/vector-search.d.ts +47 -0
- package/dist/vector/vector-search.d.ts.map +1 -0
- package/dist/vector/vector-search.js +176 -0
- package/package.json +9 -8
- package/src/api/config.ts +0 -4
- package/src/api/sonamu.ts +21 -36
- package/src/bin/cli.ts +5 -5
- package/src/database/base-model.ts +20 -11
- package/src/database/code-generator.ts +6 -2
- package/src/database/db.ts +1 -0
- package/src/database/puri-wrapper.ts +22 -16
- package/src/database/puri.ts +150 -27
- package/src/database/puri.types.test-d.ts +457 -0
- package/src/database/puri.types.ts +231 -33
- package/src/database/upsert-builder.ts +43 -34
- package/src/entity/entity-manager.ts +2 -2
- package/src/entity/entity.ts +134 -44
- package/src/index.ts +6 -0
- package/src/migration/code-generation.ts +377 -174
- package/src/migration/migration-set.ts +22 -3
- package/src/migration/migrator.ts +6 -0
- package/src/migration/postgresql-schema-reader.ts +121 -21
- package/src/syncer/syncer.ts +3 -2
- package/src/template/implementations/generated.template.ts +51 -9
- package/src/template/implementations/generated_sso.template.ts +71 -2
- package/src/template/implementations/model.template.ts +5 -5
- package/src/template/implementations/model_test.template.ts +3 -3
- package/src/template/implementations/view_enums_dropdown.template.ts +1 -1
- package/src/template/implementations/view_enums_select.template.ts +1 -1
- package/src/template/implementations/view_form.template.ts +11 -8
- package/src/template/implementations/view_id_async_select.template.ts +3 -3
- package/src/template/implementations/view_list.template.ts +11 -8
- package/src/template/implementations/view_search_input.template.ts +1 -1
- package/src/template/template.ts +1 -1
- package/src/template/zod-converter.ts +20 -0
- package/src/testing/fixture-manager.ts +31 -30
- package/src/types/types.ts +226 -48
- package/src/vector/chunking.ts +115 -0
- package/src/vector/config.ts +68 -0
- package/src/vector/embedding.ts +193 -0
- package/src/vector/types.ts +122 -0
- package/src/vector/vector-search.ts +261 -0
- package/dist/template/implementations/view_enums_buttonset.template.d.ts +0 -17
- package/dist/template/implementations/view_enums_buttonset.template.d.ts.map +0 -1
- package/dist/template/implementations/view_enums_buttonset.template.js +0 -31
- package/dist/template/implementations/view_list_columns.template.d.ts +0 -17
- package/dist/template/implementations/view_list_columns.template.d.ts.map +0 -1
- package/dist/template/implementations/view_list_columns.template.js +0 -49
- package/src/template/implementations/view_enums_buttonset.template.ts +0 -34
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
...genColumnDefinitions(columns),
|
|
36
|
-
"",
|
|
37
|
-
"// indexes",
|
|
38
|
-
...standardIndexes.map((index) => genIndexDefinition(index, table)),
|
|
40
|
+
...columnDefs.builder,
|
|
39
41
|
"});",
|
|
40
|
-
//
|
|
41
|
-
...
|
|
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[]):
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
147
|
+
// nullable
|
|
148
|
+
chains.push(column.nullable ? "nullable()" : "notNullable()");
|
|
105
149
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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: "
|
|
144
|
-
fulltext: "
|
|
145
|
-
unique: "
|
|
220
|
+
index: "INDEX",
|
|
221
|
+
fulltext: "INDEX",
|
|
222
|
+
unique: "UNIQUE INDEX",
|
|
146
223
|
};
|
|
147
224
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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((
|
|
281
|
-
alterColumnsTo.drop.map((col) => col.name).includes(
|
|
388
|
+
index.columns.every(({ name }) =>
|
|
389
|
+
alterColumnsTo.drop.map((col) => col.name).includes(name),
|
|
282
390
|
) === false,
|
|
283
391
|
);
|
|
284
392
|
|
|
285
393
|
// 빈 코드 생성 방지
|
|
286
|
-
|
|
287
|
-
alterColumnLinesTo.add.up.length
|
|
288
|
-
alterColumnLinesTo.
|
|
289
|
-
alterColumnLinesTo.
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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((
|
|
343
|
-
alterColumnsTo.add.map((col) => col.name).includes(
|
|
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(
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
|
517
|
-
entity: diff(entityIndexes, dbIndexes,
|
|
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
|
-
|
|
534
|
-
|
|
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
|
-
|
|
728
|
-
|
|
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(
|
|
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) {
|