@rdbms-erd/core 0.1.1 → 0.1.2
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/README.md +134 -67
- package/package.json +19 -11
- package/src/__tests__/alignment.test.ts +19 -19
- package/src/__tests__/benchmark.test.ts +14 -14
- package/src/__tests__/db-meta-adapter.test.ts +166 -0
- package/src/__tests__/ddl-diagnostics.test.ts +52 -52
- package/src/__tests__/ddl-snapshot.test.ts +91 -91
- package/src/__tests__/designDocument.test.ts +129 -78
- package/src/__tests__/logical-physical.test.ts +94 -37
- package/src/alignment.ts +71 -71
- package/src/index.ts +1166 -400
package/src/index.ts
CHANGED
|
@@ -1,334 +1,1047 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type BuiltinRdbmsDialect =
|
|
2
|
+
| "mssql"
|
|
3
|
+
| "oracle"
|
|
4
|
+
| "mysql"
|
|
5
|
+
| "postgres"
|
|
6
|
+
| "sqlite";
|
|
7
|
+
export type RdbmsDialect = BuiltinRdbmsDialect | (string & {});
|
|
2
8
|
|
|
3
9
|
export { alignNodePositions, type AlignCommand } from "./alignment";
|
|
4
10
|
|
|
5
|
-
export
|
|
11
|
+
export const LOGICAL_DATA_TYPES = [
|
|
12
|
+
"TEXT",
|
|
13
|
+
"DATE",
|
|
14
|
+
"TIME",
|
|
15
|
+
"DATETIME",
|
|
16
|
+
"NUMBER",
|
|
17
|
+
"DECIMAL",
|
|
18
|
+
"FLOAT",
|
|
19
|
+
"BOOLEAN",
|
|
20
|
+
"JSON",
|
|
21
|
+
"UUID",
|
|
22
|
+
"BINARY",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export type LogicalDataType = (typeof LOGICAL_DATA_TYPES)[number];
|
|
6
26
|
|
|
7
27
|
export interface ColumnModel {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
id: string;
|
|
29
|
+
logicalName: string;
|
|
30
|
+
physicalName: string;
|
|
31
|
+
/** 컬럼 설명(업무/도메인 메모). */
|
|
32
|
+
description?: string;
|
|
33
|
+
logicalType: LogicalDataType;
|
|
34
|
+
physicalType: string;
|
|
35
|
+
defaultValue?: string;
|
|
36
|
+
nullable: boolean;
|
|
37
|
+
isPrimaryKey?: boolean;
|
|
38
|
+
isForeignKey?: boolean;
|
|
39
|
+
referencesPrimaryColumnId?: string;
|
|
40
|
+
/** 테이블 노드에서 해당 컬럼 행 배경색 */
|
|
41
|
+
color?: string;
|
|
22
42
|
}
|
|
23
43
|
|
|
24
44
|
export interface TableModel {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
id: string;
|
|
46
|
+
logicalName: string;
|
|
47
|
+
physicalName: string;
|
|
48
|
+
/** 테이블 설명(업무/도메인 메모). */
|
|
49
|
+
description?: string;
|
|
50
|
+
/** Dialect-dependent schema/catalog qualifier (e.g. `public`, `dbo`). */
|
|
51
|
+
schemaName?: string;
|
|
52
|
+
color?: string;
|
|
53
|
+
columns: ColumnModel[];
|
|
30
54
|
}
|
|
31
55
|
|
|
32
56
|
export interface RelationshipModel {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
id: string;
|
|
58
|
+
/** 참조 원본(PK) 테이블 */
|
|
59
|
+
sourceTableId: string;
|
|
60
|
+
/** 참조 대상(FK) 테이블 */
|
|
61
|
+
targetTableId: string;
|
|
62
|
+
/** source(PK) 컬럼 id */
|
|
63
|
+
sourceColumnId?: string;
|
|
64
|
+
/** target(FK) 컬럼 id */
|
|
65
|
+
targetColumnId?: string;
|
|
66
|
+
/** 연결 시 생성된 FK 컬럼인지 여부(삭제 시 컬럼 정리용) */
|
|
67
|
+
autoCreatedTargetColumn?: boolean;
|
|
68
|
+
/** FK가 최초로 연결된 PK 컬럼 id(컬럼명 변경과 무관) */
|
|
69
|
+
originPkColumnId?: string;
|
|
70
|
+
/** 타깃 cardinality. 기본 1:N */
|
|
71
|
+
cardinality?: "1:1" | "1:N";
|
|
72
|
+
/** 출발(소스) 엣지의 테이블 내부 절대 Y 좌표(px). */
|
|
73
|
+
sourceLineY?: number;
|
|
74
|
+
/** @deprecated legacy ratio(0~1). sourceLineY가 없을 때만 fallback으로 사용 */
|
|
75
|
+
sourceLineRatio?: number;
|
|
76
|
+
/** 관계선의 중간 꺾임 위치 비율(0~1). 두 테이블 앵커 사이의 비율로 저장한다. */
|
|
77
|
+
linePivotRatio?: number;
|
|
78
|
+
/** true이면 캔버스에서 해당 관계선을 기본적으로 숨긴다. 툴의「숨긴 관계선 보기」로만 표시 가능. */
|
|
79
|
+
canvasLineHidden?: boolean;
|
|
46
80
|
}
|
|
47
81
|
|
|
48
82
|
/**
|
|
49
|
-
* 캔버스에 관계선을 그릴지
|
|
83
|
+
* 캔버스에 관계선을 그릴지 여부.
|
|
84
|
+
* - `revealHiddenLines === false`: `canvasLineHidden`인 관계는 제외.
|
|
85
|
+
* - `revealHiddenLines === true`: 숨김 처리된 관계도 함께 그린다(표시만, 속성은 바꾸지 않음).
|
|
50
86
|
*/
|
|
51
87
|
export function isRelationshipLineRenderable(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
globalLinesVisible: boolean
|
|
88
|
+
rel: RelationshipModel,
|
|
89
|
+
revealHiddenLines: boolean,
|
|
55
90
|
): boolean {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
if (rel.canvasLineHidden === true) return revealHiddenLines;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 구버전 `ColumnModel.showFkRelationLine === false`를 관계의 `canvasLineHidden`으로 옮긴 뒤 컬럼 플래그를 제거한다. */
|
|
96
|
+
export function migrateLegacyFkLineVisibility(model: DesignModel): void {
|
|
97
|
+
for (const table of model.tables) {
|
|
98
|
+
for (const col of table.columns) {
|
|
99
|
+
const legacy = (col as { showFkRelationLine?: boolean })
|
|
100
|
+
.showFkRelationLine;
|
|
101
|
+
if (legacy === false && col.isForeignKey) {
|
|
102
|
+
for (const rel of model.relationships) {
|
|
103
|
+
if (
|
|
104
|
+
rel.targetTableId === table.id &&
|
|
105
|
+
rel.targetColumnId === col.id
|
|
106
|
+
) {
|
|
107
|
+
rel.canvasLineHidden = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if ("showFkRelationLine" in col) {
|
|
112
|
+
delete (col as { showFkRelationLine?: boolean })
|
|
113
|
+
.showFkRelationLine;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
63
117
|
}
|
|
64
118
|
|
|
65
119
|
export interface IndexModel {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
120
|
+
id: string;
|
|
121
|
+
tableId: string;
|
|
122
|
+
name: string;
|
|
123
|
+
columns: string[];
|
|
124
|
+
unique: boolean;
|
|
71
125
|
}
|
|
72
126
|
|
|
73
127
|
export interface DiagramLayout {
|
|
74
|
-
|
|
128
|
+
nodePositions: Record<string, { x: number; y: number }>;
|
|
75
129
|
}
|
|
76
130
|
|
|
77
131
|
export interface DesignModel {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
132
|
+
dialect: RdbmsDialect;
|
|
133
|
+
tables: TableModel[];
|
|
134
|
+
relationships: RelationshipModel[];
|
|
135
|
+
indexes: IndexModel[];
|
|
82
136
|
}
|
|
83
137
|
|
|
84
138
|
export interface DesignDocument {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
139
|
+
schemaVersion: number;
|
|
140
|
+
model: DesignModel;
|
|
141
|
+
layout: DiagramLayout;
|
|
142
|
+
settings?: Record<string, unknown>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface DialectCapability {
|
|
146
|
+
supportsSchema: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type DdlStyleQuote = "double" | "backtick" | "bracket";
|
|
150
|
+
export type DdlStyleBooleanLiteral = "trueFalse" | "oneZero";
|
|
151
|
+
|
|
152
|
+
export interface DdlStyle {
|
|
153
|
+
quote: DdlStyleQuote;
|
|
154
|
+
boolLiteral?: DdlStyleBooleanLiteral;
|
|
155
|
+
nowKeyword?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface LogicalTypeMeta {
|
|
159
|
+
id: LogicalDataType;
|
|
160
|
+
label?: string;
|
|
161
|
+
defaultPhysicalType: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface DialectMetaJson {
|
|
165
|
+
id: RdbmsDialect;
|
|
166
|
+
label: string;
|
|
167
|
+
supportsSchema: boolean;
|
|
168
|
+
logicalTypes: LogicalTypeMeta[];
|
|
169
|
+
ddlStyle?: DdlStyle;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface DialectMeta {
|
|
173
|
+
id: RdbmsDialect;
|
|
174
|
+
label: string;
|
|
175
|
+
capabilities: DialectCapability;
|
|
176
|
+
logicalTypes: readonly LogicalDataType[];
|
|
177
|
+
defaultPhysicalTypeMap: Partial<Record<LogicalDataType, string>>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export type DdlScope =
|
|
181
|
+
| { kind: "all" }
|
|
182
|
+
| { kind: "selected"; tableIds: string[] };
|
|
183
|
+
|
|
184
|
+
export interface DdlGenerateInput {
|
|
185
|
+
doc: DesignDocument;
|
|
186
|
+
dialectId: RdbmsDialect;
|
|
187
|
+
scope: DdlScope;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface DdlGenerateOutput {
|
|
191
|
+
sql: string;
|
|
192
|
+
diagnostics?: DdlDiagnostic[];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export type DdlGeneratorHook = (
|
|
196
|
+
input: DdlGenerateInput,
|
|
197
|
+
) => DdlGenerateOutput | Promise<DdlGenerateOutput>;
|
|
198
|
+
|
|
199
|
+
export interface DdlGeneratorRules {
|
|
200
|
+
quoteIdentifier?: (dialect: RdbmsDialect, identifier: string) => string;
|
|
201
|
+
toDefaultExpression?: (
|
|
202
|
+
dialect: RdbmsDialect,
|
|
203
|
+
col: ColumnModel,
|
|
204
|
+
) => string | null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface DbMetaAdapter {
|
|
208
|
+
listDialects: () => DialectMeta[];
|
|
209
|
+
getDialectMeta: (dialect: RdbmsDialect) => DialectMeta | undefined;
|
|
210
|
+
getDefaultPhysicalType: (
|
|
211
|
+
dialect: RdbmsDialect,
|
|
212
|
+
logicalType: LogicalDataType,
|
|
213
|
+
) => string;
|
|
214
|
+
getDdlRules: (dialect: RdbmsDialect) => DdlGeneratorRules;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface CoreDbMetaOptions {
|
|
218
|
+
dbMetaAdapter?: DbMetaAdapter;
|
|
219
|
+
hostMetas?: DialectMetaJson[];
|
|
220
|
+
hostDdlGenerators?: Record<string, DdlGeneratorHook>;
|
|
221
|
+
fallbackOnHookError?: boolean;
|
|
89
222
|
}
|
|
90
223
|
|
|
91
224
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
225
|
+
return typeof value === "object" && value !== null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const BUILTIN_DIALECTS: readonly BuiltinRdbmsDialect[] = [
|
|
229
|
+
"mssql",
|
|
230
|
+
"oracle",
|
|
231
|
+
"mysql",
|
|
232
|
+
"postgres",
|
|
233
|
+
"sqlite",
|
|
234
|
+
] as const;
|
|
235
|
+
|
|
236
|
+
const DIALECT_DEFAULT_TYPE_MAP: Record<
|
|
237
|
+
BuiltinRdbmsDialect,
|
|
238
|
+
Record<LogicalDataType, string>
|
|
239
|
+
> = {
|
|
240
|
+
mssql: {
|
|
241
|
+
TEXT: "NVARCHAR(255)",
|
|
242
|
+
DATE: "DATE",
|
|
243
|
+
TIME: "TIME",
|
|
244
|
+
DATETIME: "DATETIME2",
|
|
245
|
+
NUMBER: "INT",
|
|
246
|
+
DECIMAL: "DECIMAL(10,2)",
|
|
247
|
+
FLOAT: "FLOAT",
|
|
248
|
+
BOOLEAN: "BIT",
|
|
249
|
+
JSON: "NVARCHAR(MAX)",
|
|
250
|
+
UUID: "UNIQUEIDENTIFIER",
|
|
251
|
+
BINARY: "VARBINARY(255)",
|
|
252
|
+
},
|
|
253
|
+
oracle: {
|
|
254
|
+
TEXT: "VARCHAR2(255)",
|
|
255
|
+
DATE: "DATE",
|
|
256
|
+
TIME: "TIMESTAMP",
|
|
257
|
+
DATETIME: "TIMESTAMP",
|
|
258
|
+
NUMBER: "NUMBER(10)",
|
|
259
|
+
DECIMAL: "NUMBER(10,2)",
|
|
260
|
+
FLOAT: "BINARY_FLOAT",
|
|
261
|
+
BOOLEAN: "NUMBER(1)",
|
|
262
|
+
JSON: "CLOB",
|
|
263
|
+
UUID: "RAW(16)",
|
|
264
|
+
BINARY: "RAW(255)",
|
|
265
|
+
},
|
|
266
|
+
mysql: {
|
|
267
|
+
TEXT: "VARCHAR(255)",
|
|
268
|
+
DATE: "DATE",
|
|
269
|
+
TIME: "TIME",
|
|
270
|
+
DATETIME: "DATETIME",
|
|
271
|
+
NUMBER: "INT",
|
|
272
|
+
DECIMAL: "DECIMAL(10,2)",
|
|
273
|
+
FLOAT: "FLOAT",
|
|
274
|
+
BOOLEAN: "BOOLEAN",
|
|
275
|
+
JSON: "JSON",
|
|
276
|
+
UUID: "CHAR(36)",
|
|
277
|
+
BINARY: "VARBINARY(255)",
|
|
278
|
+
},
|
|
279
|
+
postgres: {
|
|
280
|
+
TEXT: "VARCHAR(255)",
|
|
281
|
+
DATE: "DATE",
|
|
282
|
+
TIME: "TIME",
|
|
283
|
+
DATETIME: "TIMESTAMP",
|
|
284
|
+
NUMBER: "INTEGER",
|
|
285
|
+
DECIMAL: "NUMERIC(10,2)",
|
|
286
|
+
FLOAT: "REAL",
|
|
287
|
+
BOOLEAN: "BOOLEAN",
|
|
288
|
+
JSON: "JSONB",
|
|
289
|
+
UUID: "UUID",
|
|
290
|
+
BINARY: "BYTEA",
|
|
291
|
+
},
|
|
292
|
+
sqlite: {
|
|
293
|
+
TEXT: "TEXT",
|
|
294
|
+
DATE: "TEXT",
|
|
295
|
+
TIME: "TEXT",
|
|
296
|
+
DATETIME: "TEXT",
|
|
297
|
+
NUMBER: "INTEGER",
|
|
298
|
+
DECIMAL: "NUMERIC(10,2)",
|
|
299
|
+
FLOAT: "REAL",
|
|
300
|
+
BOOLEAN: "INTEGER",
|
|
301
|
+
JSON: "TEXT",
|
|
302
|
+
UUID: "TEXT",
|
|
303
|
+
BINARY: "BLOB",
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const DIALECT_CAPABILITIES: Record<
|
|
308
|
+
BuiltinRdbmsDialect,
|
|
309
|
+
{ supportsSchema: boolean }
|
|
310
|
+
> = {
|
|
311
|
+
mssql: { supportsSchema: true },
|
|
312
|
+
oracle: { supportsSchema: true },
|
|
313
|
+
mysql: { supportsSchema: true },
|
|
314
|
+
postgres: { supportsSchema: true },
|
|
315
|
+
sqlite: { supportsSchema: false },
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const DIALECT_LABELS: Record<BuiltinRdbmsDialect, string> = {
|
|
319
|
+
mssql: "MS SQL Server",
|
|
320
|
+
oracle: "Oracle",
|
|
321
|
+
mysql: "MySQL",
|
|
322
|
+
postgres: "PostgreSQL",
|
|
323
|
+
sqlite: "SQLite",
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const DIALECT_DDL_STYLE: Record<BuiltinRdbmsDialect, DdlStyle> = {
|
|
327
|
+
mssql: {
|
|
328
|
+
quote: "bracket",
|
|
329
|
+
boolLiteral: "oneZero",
|
|
330
|
+
nowKeyword: "GETDATE()",
|
|
331
|
+
},
|
|
332
|
+
oracle: {
|
|
333
|
+
quote: "double",
|
|
334
|
+
boolLiteral: "oneZero",
|
|
335
|
+
nowKeyword: "CURRENT_TIMESTAMP",
|
|
336
|
+
},
|
|
337
|
+
mysql: {
|
|
338
|
+
quote: "backtick",
|
|
339
|
+
boolLiteral: "oneZero",
|
|
340
|
+
nowKeyword: "CURRENT_TIMESTAMP",
|
|
341
|
+
},
|
|
342
|
+
postgres: {
|
|
343
|
+
quote: "double",
|
|
344
|
+
boolLiteral: "trueFalse",
|
|
345
|
+
nowKeyword: "CURRENT_TIMESTAMP",
|
|
346
|
+
},
|
|
347
|
+
sqlite: {
|
|
348
|
+
quote: "double",
|
|
349
|
+
boolLiteral: "oneZero",
|
|
350
|
+
nowKeyword: "CURRENT_TIMESTAMP",
|
|
351
|
+
},
|
|
124
352
|
};
|
|
125
353
|
|
|
126
|
-
|
|
127
|
-
|
|
354
|
+
function toLogicalTypeMetas(dialect: BuiltinRdbmsDialect): LogicalTypeMeta[] {
|
|
355
|
+
return LOGICAL_DATA_TYPES.map((id) => ({
|
|
356
|
+
id,
|
|
357
|
+
defaultPhysicalType: DIALECT_DEFAULT_TYPE_MAP[dialect][id],
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export const BUILTIN_DIALECT_METAS_JSON: DialectMetaJson[] =
|
|
362
|
+
BUILTIN_DIALECTS.map((dialect) => ({
|
|
363
|
+
id: dialect,
|
|
364
|
+
label: DIALECT_LABELS[dialect],
|
|
365
|
+
supportsSchema: DIALECT_CAPABILITIES[dialect].supportsSchema,
|
|
366
|
+
logicalTypes: toLogicalTypeMetas(dialect),
|
|
367
|
+
ddlStyle: DIALECT_DDL_STYLE[dialect],
|
|
368
|
+
}));
|
|
369
|
+
|
|
370
|
+
function defaultDdlRules(): DdlGeneratorRules {
|
|
371
|
+
return {
|
|
372
|
+
quoteIdentifier: (dialect, identifier) => {
|
|
373
|
+
if (dialect === "mssql") return `[${identifier}]`;
|
|
374
|
+
if (dialect === "mysql") return `\`${identifier}\``;
|
|
375
|
+
return `"${identifier}"`;
|
|
376
|
+
},
|
|
377
|
+
toDefaultExpression: (dialect, col) => {
|
|
378
|
+
const raw = col.defaultValue?.trim();
|
|
379
|
+
if (!raw) return null;
|
|
380
|
+
const upper = raw.toUpperCase();
|
|
381
|
+
const isFunctionLike =
|
|
382
|
+
/[()]/.test(raw) ||
|
|
383
|
+
upper === "NULL" ||
|
|
384
|
+
upper === "CURRENT_TIMESTAMP" ||
|
|
385
|
+
upper === "CURRENT_DATE";
|
|
386
|
+
if (isFunctionLike) return raw;
|
|
387
|
+
if (
|
|
388
|
+
col.logicalType === "NUMBER" ||
|
|
389
|
+
col.logicalType === "DECIMAL" ||
|
|
390
|
+
col.logicalType === "FLOAT"
|
|
391
|
+
)
|
|
392
|
+
return raw;
|
|
393
|
+
if (col.logicalType === "BOOLEAN") {
|
|
394
|
+
if (upper === "TRUE" || upper === "FALSE") {
|
|
395
|
+
if (
|
|
396
|
+
dialect === "mssql" ||
|
|
397
|
+
dialect === "oracle" ||
|
|
398
|
+
dialect === "sqlite"
|
|
399
|
+
)
|
|
400
|
+
return upper === "TRUE" ? "1" : "0";
|
|
401
|
+
return upper;
|
|
402
|
+
}
|
|
403
|
+
if (raw === "1" || raw === "0") return raw;
|
|
404
|
+
return quoteSqlString(raw);
|
|
405
|
+
}
|
|
406
|
+
if (col.logicalType === "DATE" || col.logicalType === "DATETIME") {
|
|
407
|
+
if (dialect === "mssql" && upper === "NOW") return "GETDATE()";
|
|
408
|
+
if (upper === "NOW") return "CURRENT_TIMESTAMP";
|
|
409
|
+
return quoteSqlString(raw);
|
|
410
|
+
}
|
|
411
|
+
if (
|
|
412
|
+
(raw.startsWith("'") && raw.endsWith("'")) ||
|
|
413
|
+
(raw.startsWith('"') && raw.endsWith('"'))
|
|
414
|
+
)
|
|
415
|
+
return raw;
|
|
416
|
+
return quoteSqlString(raw);
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function toAdapterDialectMeta(meta: DialectMetaJson): DialectMeta {
|
|
422
|
+
const typeMap: Partial<Record<LogicalDataType, string>> = {};
|
|
423
|
+
for (const lt of meta.logicalTypes) typeMap[lt.id] = lt.defaultPhysicalType;
|
|
424
|
+
return {
|
|
425
|
+
id: meta.id,
|
|
426
|
+
label: meta.label,
|
|
427
|
+
capabilities: { supportsSchema: meta.supportsSchema },
|
|
428
|
+
logicalTypes: meta.logicalTypes.map((lt) => lt.id),
|
|
429
|
+
defaultPhysicalTypeMap: typeMap,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function mergeDialectMetas(
|
|
434
|
+
base: DialectMetaJson[],
|
|
435
|
+
host: DialectMetaJson[] = [],
|
|
436
|
+
): DialectMetaJson[] {
|
|
437
|
+
const byId = new Map<string, DialectMetaJson>(
|
|
438
|
+
base.map((meta) => [meta.id, meta]),
|
|
439
|
+
);
|
|
440
|
+
for (const meta of host) byId.set(meta.id, meta);
|
|
441
|
+
return Array.from(byId.values());
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function resolveDialectMetas(
|
|
445
|
+
options?: CoreDbMetaOptions,
|
|
446
|
+
): DialectMetaJson[] {
|
|
447
|
+
return mergeDialectMetas(
|
|
448
|
+
BUILTIN_DIALECT_METAS_JSON,
|
|
449
|
+
options?.hostMetas ?? [],
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function buildDbMetaAdapterFromMetas(metas: DialectMetaJson[]): DbMetaAdapter {
|
|
454
|
+
const byId = new Map<RdbmsDialect, DialectMeta>(
|
|
455
|
+
metas.map((meta) => [meta.id, toAdapterDialectMeta(meta)]),
|
|
456
|
+
);
|
|
457
|
+
const defaultRules = defaultDdlRules();
|
|
458
|
+
return {
|
|
459
|
+
listDialects: () => Array.from(byId.values()),
|
|
460
|
+
getDialectMeta: (dialect) => byId.get(dialect),
|
|
461
|
+
getDefaultPhysicalType: (dialect, logicalType) => {
|
|
462
|
+
const mapped =
|
|
463
|
+
byId.get(dialect)?.defaultPhysicalTypeMap[logicalType];
|
|
464
|
+
return mapped ?? DIALECT_DEFAULT_TYPE_MAP.postgres[logicalType];
|
|
465
|
+
},
|
|
466
|
+
getDdlRules: (dialect) => {
|
|
467
|
+
const json = metas.find((m) => m.id === dialect);
|
|
468
|
+
const style = json?.ddlStyle;
|
|
469
|
+
if (!style) return defaultRules;
|
|
470
|
+
const quoteIdentifier: DdlGeneratorRules["quoteIdentifier"] = (
|
|
471
|
+
_d,
|
|
472
|
+
identifier,
|
|
473
|
+
) => {
|
|
474
|
+
if (style.quote === "bracket") return `[${identifier}]`;
|
|
475
|
+
if (style.quote === "backtick") return `\`${identifier}\``;
|
|
476
|
+
return `"${identifier}"`;
|
|
477
|
+
};
|
|
478
|
+
const toDefaultExpression: DdlGeneratorRules["toDefaultExpression"] =
|
|
479
|
+
(_d, col) => {
|
|
480
|
+
const raw = col.defaultValue?.trim();
|
|
481
|
+
if (!raw) return null;
|
|
482
|
+
const upper = raw.toUpperCase();
|
|
483
|
+
const isFunctionLike =
|
|
484
|
+
/[()]/.test(raw) ||
|
|
485
|
+
upper === "NULL" ||
|
|
486
|
+
upper === "CURRENT_TIMESTAMP" ||
|
|
487
|
+
upper === "CURRENT_DATE";
|
|
488
|
+
if (isFunctionLike) return raw;
|
|
489
|
+
if (
|
|
490
|
+
col.logicalType === "NUMBER" ||
|
|
491
|
+
col.logicalType === "DECIMAL" ||
|
|
492
|
+
col.logicalType === "FLOAT"
|
|
493
|
+
)
|
|
494
|
+
return raw;
|
|
495
|
+
if (col.logicalType === "BOOLEAN") {
|
|
496
|
+
if (upper === "TRUE" || upper === "FALSE") {
|
|
497
|
+
if ((style.boolLiteral ?? "oneZero") === "oneZero")
|
|
498
|
+
return upper === "TRUE" ? "1" : "0";
|
|
499
|
+
return upper;
|
|
500
|
+
}
|
|
501
|
+
if (raw === "1" || raw === "0") return raw;
|
|
502
|
+
return quoteSqlString(raw);
|
|
503
|
+
}
|
|
504
|
+
if (
|
|
505
|
+
col.logicalType === "DATE" ||
|
|
506
|
+
col.logicalType === "DATETIME"
|
|
507
|
+
) {
|
|
508
|
+
if (upper === "NOW")
|
|
509
|
+
return style.nowKeyword ?? "CURRENT_TIMESTAMP";
|
|
510
|
+
return quoteSqlString(raw);
|
|
511
|
+
}
|
|
512
|
+
if (
|
|
513
|
+
(raw.startsWith("'") && raw.endsWith("'")) ||
|
|
514
|
+
(raw.startsWith('"') && raw.endsWith('"'))
|
|
515
|
+
)
|
|
516
|
+
return raw;
|
|
517
|
+
return quoteSqlString(raw);
|
|
518
|
+
};
|
|
519
|
+
return { quoteIdentifier, toDefaultExpression };
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function createDefaultDbMetaAdapter(
|
|
525
|
+
overrides?: Partial<DbMetaAdapter>,
|
|
526
|
+
): DbMetaAdapter {
|
|
527
|
+
const base = buildDbMetaAdapterFromMetas(BUILTIN_DIALECT_METAS_JSON);
|
|
528
|
+
return { ...base, ...overrides };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export const defaultDbMetaAdapter = createDefaultDbMetaAdapter();
|
|
532
|
+
|
|
533
|
+
function resolveDbMetaAdapter(options?: CoreDbMetaOptions): DbMetaAdapter {
|
|
534
|
+
if (options?.dbMetaAdapter) return options.dbMetaAdapter;
|
|
535
|
+
if (options?.hostMetas && options.hostMetas.length > 0) {
|
|
536
|
+
return buildDbMetaAdapterFromMetas(resolveDialectMetas(options));
|
|
537
|
+
}
|
|
538
|
+
return defaultDbMetaAdapter;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function defaultPhysicalType(
|
|
542
|
+
dialect: RdbmsDialect,
|
|
543
|
+
logicalType: LogicalDataType,
|
|
544
|
+
options?: CoreDbMetaOptions,
|
|
545
|
+
): string {
|
|
546
|
+
return resolveDbMetaAdapter(options).getDefaultPhysicalType(
|
|
547
|
+
dialect,
|
|
548
|
+
logicalType,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function getRdbmsDialectCapability(
|
|
553
|
+
dialect: RdbmsDialect,
|
|
554
|
+
options?: CoreDbMetaOptions,
|
|
555
|
+
): DialectCapability {
|
|
556
|
+
const meta = resolveDbMetaAdapter(options).getDialectMeta(dialect);
|
|
557
|
+
return meta?.capabilities ?? { supportsSchema: false };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function dialectSupportsSchema(
|
|
561
|
+
dialect: RdbmsDialect,
|
|
562
|
+
options?: CoreDbMetaOptions,
|
|
563
|
+
): boolean {
|
|
564
|
+
return getRdbmsDialectCapability(dialect, options).supportsSchema;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function parseTypeArguments(physicalType: string): number[] | null {
|
|
568
|
+
const m = physicalType.match(/\(([^)]+)\)/);
|
|
569
|
+
if (!m) return null;
|
|
570
|
+
const values = m[1]
|
|
571
|
+
.split(",")
|
|
572
|
+
.map((v) => Number.parseInt(v.trim(), 10))
|
|
573
|
+
.filter((v) => Number.isFinite(v));
|
|
574
|
+
return values.length > 0 ? values : null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function convertPhysicalTypeByLogicalType(
|
|
578
|
+
physicalType: string,
|
|
579
|
+
logicalType: LogicalDataType,
|
|
580
|
+
nextDialect: RdbmsDialect,
|
|
581
|
+
options?: CoreDbMetaOptions,
|
|
582
|
+
): string {
|
|
583
|
+
const args = parseTypeArguments(physicalType);
|
|
584
|
+
const precision = args?.[0];
|
|
585
|
+
const scale = args?.[1];
|
|
586
|
+
const length = args?.[0];
|
|
587
|
+
|
|
588
|
+
if (logicalType === "NUMBER") {
|
|
589
|
+
if (nextDialect === "oracle") {
|
|
590
|
+
if (precision && scale !== undefined)
|
|
591
|
+
return `NUMBER(${precision},${scale})`;
|
|
592
|
+
if (precision) return `NUMBER(${precision})`;
|
|
593
|
+
return "NUMBER(10)";
|
|
594
|
+
}
|
|
595
|
+
if (nextDialect === "mssql") {
|
|
596
|
+
if (precision && scale !== undefined)
|
|
597
|
+
return `NUMERIC(${precision},${scale})`;
|
|
598
|
+
if (precision) return `NUMERIC(${precision},0)`;
|
|
599
|
+
return "INT";
|
|
600
|
+
}
|
|
601
|
+
if (nextDialect === "mysql") {
|
|
602
|
+
if (precision && scale !== undefined)
|
|
603
|
+
return `DECIMAL(${precision},${scale})`;
|
|
604
|
+
if (precision) return `DECIMAL(${precision},0)`;
|
|
605
|
+
return "INT";
|
|
606
|
+
}
|
|
607
|
+
if (nextDialect === "postgres") {
|
|
608
|
+
if (precision && scale !== undefined)
|
|
609
|
+
return `NUMERIC(${precision},${scale})`;
|
|
610
|
+
if (precision) return `NUMERIC(${precision},0)`;
|
|
611
|
+
return "INTEGER";
|
|
612
|
+
}
|
|
613
|
+
return "INTEGER";
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (logicalType === "DECIMAL") {
|
|
617
|
+
if (precision && scale !== undefined) {
|
|
618
|
+
if (nextDialect === "oracle")
|
|
619
|
+
return `NUMBER(${precision},${scale})`;
|
|
620
|
+
if (nextDialect === "postgres")
|
|
621
|
+
return `NUMERIC(${precision},${scale})`;
|
|
622
|
+
return `DECIMAL(${precision},${scale})`;
|
|
623
|
+
}
|
|
624
|
+
return defaultPhysicalType(nextDialect, logicalType, options);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (logicalType === "FLOAT") {
|
|
628
|
+
if (nextDialect === "oracle") return "BINARY_FLOAT";
|
|
629
|
+
if (nextDialect === "sqlite") return "REAL";
|
|
630
|
+
return "FLOAT";
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (logicalType === "TEXT") {
|
|
634
|
+
if (nextDialect === "oracle")
|
|
635
|
+
return length ? `VARCHAR2(${length})` : "VARCHAR2(255)";
|
|
636
|
+
if (nextDialect === "mssql")
|
|
637
|
+
return length ? `NVARCHAR(${length})` : "NVARCHAR(255)";
|
|
638
|
+
if (nextDialect === "mysql")
|
|
639
|
+
return length ? `VARCHAR(${length})` : "VARCHAR(255)";
|
|
640
|
+
if (nextDialect === "postgres")
|
|
641
|
+
return length ? `VARCHAR(${length})` : "VARCHAR(255)";
|
|
642
|
+
return "TEXT";
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (logicalType === "BINARY") {
|
|
646
|
+
if (nextDialect === "mssql")
|
|
647
|
+
return length ? `VARBINARY(${length})` : "VARBINARY(255)";
|
|
648
|
+
if (nextDialect === "oracle")
|
|
649
|
+
return length ? `RAW(${length})` : "RAW(255)";
|
|
650
|
+
if (nextDialect === "mysql")
|
|
651
|
+
return length ? `VARBINARY(${length})` : "VARBINARY(255)";
|
|
652
|
+
if (nextDialect === "postgres") return "BYTEA";
|
|
653
|
+
return "BLOB";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return defaultPhysicalType(nextDialect, logicalType, options);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export function convertDesignDialect(
|
|
660
|
+
doc: DesignDocument,
|
|
661
|
+
nextDialect: RdbmsDialect,
|
|
662
|
+
options?: CoreDbMetaOptions,
|
|
663
|
+
): DesignDocument {
|
|
664
|
+
if (doc.model.dialect === nextDialect) return doc;
|
|
665
|
+
const supportsSchema = dialectSupportsSchema(nextDialect, options);
|
|
666
|
+
return {
|
|
667
|
+
...doc,
|
|
668
|
+
model: {
|
|
669
|
+
...doc.model,
|
|
670
|
+
dialect: nextDialect,
|
|
671
|
+
tables: doc.model.tables.map((table) => ({
|
|
672
|
+
...table,
|
|
673
|
+
schemaName: supportsSchema ? table.schemaName : undefined,
|
|
674
|
+
columns: table.columns.map((col) => ({
|
|
675
|
+
...col,
|
|
676
|
+
physicalType: convertPhysicalTypeByLogicalType(
|
|
677
|
+
col.physicalType,
|
|
678
|
+
col.logicalType,
|
|
679
|
+
nextDialect,
|
|
680
|
+
options,
|
|
681
|
+
),
|
|
682
|
+
})),
|
|
683
|
+
})),
|
|
684
|
+
},
|
|
685
|
+
};
|
|
128
686
|
}
|
|
129
687
|
|
|
130
688
|
export function applyLogicalTypeChange(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
689
|
+
column: ColumnModel,
|
|
690
|
+
nextLogicalType: LogicalDataType,
|
|
691
|
+
dialect: RdbmsDialect,
|
|
692
|
+
options?: CoreDbMetaOptions,
|
|
134
693
|
): ColumnModel {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
694
|
+
return {
|
|
695
|
+
...column,
|
|
696
|
+
logicalType: nextLogicalType,
|
|
697
|
+
physicalType: defaultPhysicalType(dialect, nextLogicalType, options),
|
|
698
|
+
};
|
|
140
699
|
}
|
|
141
700
|
|
|
142
701
|
export function createColumn(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
): ColumnModel {
|
|
159
|
-
const physicalName = params.physicalName ?? params.logicalName;
|
|
160
|
-
const isPrimaryKey = params.isPrimaryKey ?? false;
|
|
161
|
-
const isForeignKey = params.isForeignKey ?? false;
|
|
162
|
-
const col: ColumnModel = {
|
|
163
|
-
id: params.id,
|
|
164
|
-
logicalName: params.logicalName,
|
|
165
|
-
physicalName,
|
|
166
|
-
logicalType: params.logicalType,
|
|
167
|
-
physicalType: defaultPhysicalType(dialect, params.logicalType),
|
|
168
|
-
defaultValue: params.defaultValue,
|
|
169
|
-
nullable: isPrimaryKey ? false : (params.nullable ?? true),
|
|
170
|
-
isPrimaryKey,
|
|
171
|
-
isForeignKey,
|
|
172
|
-
referencesPrimaryColumnId: params.referencesPrimaryColumnId
|
|
173
|
-
};
|
|
174
|
-
if (params.color !== undefined) col.color = params.color;
|
|
175
|
-
if (isForeignKey && params.showFkRelationLine === false) col.showFkRelationLine = false;
|
|
176
|
-
return col;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function createEmptyDesign(dialect: RdbmsDialect = "mssql"): DesignDocument {
|
|
180
|
-
return {
|
|
181
|
-
schemaVersion: 1,
|
|
182
|
-
model: {
|
|
183
|
-
dialect,
|
|
184
|
-
tables: [],
|
|
185
|
-
relationships: [],
|
|
186
|
-
indexes: []
|
|
702
|
+
dialect: RdbmsDialect,
|
|
703
|
+
params: {
|
|
704
|
+
id: string;
|
|
705
|
+
logicalName: string;
|
|
706
|
+
/** 생략 시 논리 이름과 동일한 물리 이름으로 생성된다. */
|
|
707
|
+
physicalName?: string;
|
|
708
|
+
description?: string;
|
|
709
|
+
logicalType: LogicalDataType;
|
|
710
|
+
defaultValue?: string;
|
|
711
|
+
nullable?: boolean;
|
|
712
|
+
isPrimaryKey?: boolean;
|
|
713
|
+
isForeignKey?: boolean;
|
|
714
|
+
referencesPrimaryColumnId?: string;
|
|
715
|
+
color?: string;
|
|
187
716
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
717
|
+
options?: CoreDbMetaOptions,
|
|
718
|
+
): ColumnModel {
|
|
719
|
+
const physicalName = params.physicalName ?? params.logicalName;
|
|
720
|
+
const isPrimaryKey = params.isPrimaryKey ?? false;
|
|
721
|
+
const isForeignKey = params.isForeignKey ?? false;
|
|
722
|
+
const col: ColumnModel = {
|
|
723
|
+
id: params.id,
|
|
724
|
+
logicalName: params.logicalName,
|
|
725
|
+
physicalName,
|
|
726
|
+
description: params.description,
|
|
727
|
+
logicalType: params.logicalType,
|
|
728
|
+
physicalType: defaultPhysicalType(dialect, params.logicalType, options),
|
|
729
|
+
defaultValue: params.defaultValue,
|
|
730
|
+
nullable: isPrimaryKey ? false : (params.nullable ?? true),
|
|
731
|
+
isPrimaryKey,
|
|
732
|
+
isForeignKey,
|
|
733
|
+
referencesPrimaryColumnId: params.referencesPrimaryColumnId,
|
|
734
|
+
};
|
|
735
|
+
if (params.color !== undefined) col.color = params.color;
|
|
736
|
+
return col;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export function createEmptyDesign(
|
|
740
|
+
dialect: RdbmsDialect = "mssql",
|
|
741
|
+
): DesignDocument {
|
|
742
|
+
return {
|
|
743
|
+
schemaVersion: 1,
|
|
744
|
+
model: {
|
|
745
|
+
dialect,
|
|
746
|
+
tables: [],
|
|
747
|
+
relationships: [],
|
|
748
|
+
indexes: [],
|
|
749
|
+
},
|
|
750
|
+
layout: {
|
|
751
|
+
nodePositions: {},
|
|
752
|
+
},
|
|
753
|
+
};
|
|
192
754
|
}
|
|
193
755
|
|
|
194
756
|
export function serializeDesign(doc: DesignDocument): string {
|
|
195
|
-
|
|
757
|
+
return JSON.stringify(doc, null, 2);
|
|
196
758
|
}
|
|
197
759
|
|
|
198
|
-
export function parseDesign(
|
|
199
|
-
|
|
200
|
-
|
|
760
|
+
export function parseDesign(
|
|
761
|
+
json: string,
|
|
762
|
+
options?: CoreDbMetaOptions,
|
|
763
|
+
): DesignDocument {
|
|
764
|
+
const parsed = JSON.parse(json) as unknown;
|
|
765
|
+
return validateDesignDocument(parsed, options);
|
|
201
766
|
}
|
|
202
767
|
|
|
203
|
-
export function validateDesignDocument(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
768
|
+
export function validateDesignDocument(
|
|
769
|
+
input: unknown,
|
|
770
|
+
options?: CoreDbMetaOptions,
|
|
771
|
+
): DesignDocument {
|
|
772
|
+
if (!isObject(input)) {
|
|
773
|
+
throw new Error("Invalid design document: root must be object");
|
|
774
|
+
}
|
|
207
775
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
776
|
+
if (input.schemaVersion !== 1) {
|
|
777
|
+
throw new Error("Unsupported schemaVersion");
|
|
778
|
+
}
|
|
211
779
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
780
|
+
if (
|
|
781
|
+
!isObject(input.model) ||
|
|
782
|
+
!Array.isArray(input.model.tables) ||
|
|
783
|
+
!Array.isArray(input.model.relationships) ||
|
|
784
|
+
!Array.isArray(input.model.indexes)
|
|
785
|
+
) {
|
|
786
|
+
throw new Error("Invalid design document: model is malformed");
|
|
787
|
+
}
|
|
215
788
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
789
|
+
const dialects = new Set(
|
|
790
|
+
resolveDbMetaAdapter(options)
|
|
791
|
+
.listDialects()
|
|
792
|
+
.map((d) => d.id),
|
|
793
|
+
);
|
|
794
|
+
const modelDialect = (input.model as unknown as { dialect?: unknown })
|
|
795
|
+
.dialect;
|
|
796
|
+
if (
|
|
797
|
+
typeof modelDialect !== "string" ||
|
|
798
|
+
!dialects.has(modelDialect as RdbmsDialect)
|
|
799
|
+
) {
|
|
800
|
+
throw new Error("Invalid design document: model.dialect is invalid");
|
|
801
|
+
}
|
|
221
802
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
803
|
+
if (!isObject(input.layout) || !isObject(input.layout.nodePositions)) {
|
|
804
|
+
throw new Error("Invalid design document: layout is malformed");
|
|
805
|
+
}
|
|
225
806
|
|
|
226
|
-
|
|
807
|
+
const doc = input as unknown as DesignDocument;
|
|
808
|
+
migrateLegacyFkLineVisibility(doc.model);
|
|
809
|
+
return doc;
|
|
227
810
|
}
|
|
228
811
|
|
|
229
812
|
export function roundTripDesign(doc: DesignDocument): DesignDocument {
|
|
230
|
-
|
|
813
|
+
return parseDesign(serializeDesign(doc));
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function quoteIdentifier(
|
|
817
|
+
dialect: RdbmsDialect,
|
|
818
|
+
identifier: string,
|
|
819
|
+
options?: CoreDbMetaOptions,
|
|
820
|
+
): string {
|
|
821
|
+
const quote =
|
|
822
|
+
resolveDbMetaAdapter(options).getDdlRules(dialect).quoteIdentifier ??
|
|
823
|
+
defaultDdlRules().quoteIdentifier!;
|
|
824
|
+
return quote(dialect, identifier);
|
|
231
825
|
}
|
|
232
826
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
827
|
+
function qualifiedTableName(
|
|
828
|
+
table: TableModel,
|
|
829
|
+
dialect: RdbmsDialect,
|
|
830
|
+
options?: CoreDbMetaOptions,
|
|
831
|
+
): string {
|
|
832
|
+
const schema = table.schemaName?.trim();
|
|
833
|
+
if (dialectSupportsSchema(dialect, options) && schema) {
|
|
834
|
+
return `${quoteIdentifier(dialect, schema, options)}.${quoteIdentifier(dialect, table.physicalName, options)}`;
|
|
835
|
+
}
|
|
836
|
+
return quoteIdentifier(dialect, table.physicalName, options);
|
|
237
837
|
}
|
|
238
838
|
|
|
239
839
|
function quoteSqlString(value: string): string {
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function toDefaultExpression(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
840
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function toDefaultExpression(
|
|
844
|
+
dialect: RdbmsDialect,
|
|
845
|
+
col: ColumnModel,
|
|
846
|
+
options?: CoreDbMetaOptions,
|
|
847
|
+
): string | null {
|
|
848
|
+
const handler =
|
|
849
|
+
resolveDbMetaAdapter(options).getDdlRules(dialect)
|
|
850
|
+
.toDefaultExpression ?? defaultDdlRules().toDefaultExpression!;
|
|
851
|
+
return handler(dialect, col);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function joinColumnDefs(
|
|
855
|
+
table: TableModel,
|
|
856
|
+
dialect: RdbmsDialect,
|
|
857
|
+
options?: CoreDbMetaOptions,
|
|
858
|
+
): string[] {
|
|
859
|
+
const defs = table.columns.map((col) => {
|
|
860
|
+
const nullable = col.nullable ? "NULL" : "NOT NULL";
|
|
861
|
+
const defaultExpr = toDefaultExpression(dialect, col, options);
|
|
862
|
+
const defaultSql = defaultExpr ? ` DEFAULT ${defaultExpr}` : "";
|
|
863
|
+
return ` ${quoteIdentifier(dialect, col.physicalName, options)} ${col.physicalType}${defaultSql} ${nullable}`;
|
|
864
|
+
});
|
|
865
|
+
const pkColumns = table.columns
|
|
866
|
+
.filter((col) => col.isPrimaryKey)
|
|
867
|
+
.map((col) => quoteIdentifier(dialect, col.physicalName, options));
|
|
868
|
+
if (pkColumns.length > 0) {
|
|
869
|
+
defs.push(` PRIMARY KEY (${pkColumns.join(", ")})`);
|
|
870
|
+
}
|
|
871
|
+
return defs;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function createTableSql(
|
|
875
|
+
table: TableModel,
|
|
876
|
+
dialect: RdbmsDialect,
|
|
877
|
+
options?: CoreDbMetaOptions,
|
|
878
|
+
): string {
|
|
879
|
+
const columns = joinColumnDefs(table, dialect, options);
|
|
880
|
+
return `CREATE TABLE ${qualifiedTableName(table, dialect, options)} (\n${columns.join(",\n")}\n);`;
|
|
278
881
|
}
|
|
279
882
|
|
|
280
883
|
function createRelationshipSql(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
884
|
+
rel: RelationshipModel,
|
|
885
|
+
model: DesignModel,
|
|
886
|
+
dialect: RdbmsDialect,
|
|
887
|
+
index: number,
|
|
888
|
+
options?: CoreDbMetaOptions,
|
|
285
889
|
): string | null {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
.
|
|
331
|
-
|
|
890
|
+
const sourceTable = model.tables.find(
|
|
891
|
+
(table) => table.id === rel.sourceTableId,
|
|
892
|
+
);
|
|
893
|
+
const targetTable = model.tables.find(
|
|
894
|
+
(table) => table.id === rel.targetTableId,
|
|
895
|
+
);
|
|
896
|
+
if (
|
|
897
|
+
!sourceTable ||
|
|
898
|
+
!targetTable ||
|
|
899
|
+
!rel.sourceColumnId ||
|
|
900
|
+
!rel.targetColumnId
|
|
901
|
+
) {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const sourceColumn = sourceTable.columns.find(
|
|
906
|
+
(col) => col.id === rel.sourceColumnId,
|
|
907
|
+
);
|
|
908
|
+
const targetColumn = targetTable.columns.find(
|
|
909
|
+
(col) => col.id === rel.targetColumnId,
|
|
910
|
+
);
|
|
911
|
+
if (!sourceColumn || !targetColumn) {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const fkName = `FK_${targetTable.physicalName}_${sourceTable.physicalName}_${index + 1}`;
|
|
916
|
+
return [
|
|
917
|
+
`ALTER TABLE ${qualifiedTableName(targetTable, dialect, options)}`,
|
|
918
|
+
` ADD CONSTRAINT ${quoteIdentifier(dialect, fkName, options)}`,
|
|
919
|
+
` FOREIGN KEY (${quoteIdentifier(dialect, targetColumn.physicalName, options)})`,
|
|
920
|
+
` REFERENCES ${qualifiedTableName(sourceTable, dialect, options)} (${quoteIdentifier(dialect, sourceColumn.physicalName, options)});`,
|
|
921
|
+
].join("\n");
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function createIndexSql(
|
|
925
|
+
indexModel: IndexModel,
|
|
926
|
+
model: DesignModel,
|
|
927
|
+
dialect: RdbmsDialect,
|
|
928
|
+
options?: CoreDbMetaOptions,
|
|
929
|
+
): string | null {
|
|
930
|
+
const table = model.tables.find((item) => item.id === indexModel.tableId);
|
|
931
|
+
if (!table || indexModel.columns.length === 0) {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
const unique = indexModel.unique ? "UNIQUE " : "";
|
|
935
|
+
const columns = indexModel.columns
|
|
936
|
+
.map((col) => quoteIdentifier(dialect, col, options))
|
|
937
|
+
.join(", ");
|
|
938
|
+
return `CREATE ${unique}INDEX ${quoteIdentifier(dialect, indexModel.name, options)} ON ${qualifiedTableName(table, dialect, options)} (${columns});`;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function buildDdlSql(doc: DesignDocument, options?: CoreDbMetaOptions): string {
|
|
942
|
+
const { model } = doc;
|
|
943
|
+
const tableSql = model.tables.map((table) =>
|
|
944
|
+
createTableSql(table, model.dialect, options),
|
|
945
|
+
);
|
|
946
|
+
const relSql = model.relationships
|
|
947
|
+
.map((rel, index) =>
|
|
948
|
+
createRelationshipSql(rel, model, model.dialect, index, options),
|
|
949
|
+
)
|
|
950
|
+
.filter((item): item is string => Boolean(item));
|
|
951
|
+
return [...tableSql, ...relSql].join("\n\n");
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function buildIndexDdlSql(
|
|
955
|
+
doc: DesignDocument,
|
|
956
|
+
options?: CoreDbMetaOptions,
|
|
957
|
+
): string {
|
|
958
|
+
const { model } = doc;
|
|
959
|
+
const statements = model.indexes
|
|
960
|
+
.map((index) => createIndexSql(index, model, model.dialect, options))
|
|
961
|
+
.filter((item): item is string => Boolean(item));
|
|
962
|
+
return statements.join("\n\n");
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function sliceDocByScope(doc: DesignDocument, scope: DdlScope): DesignDocument {
|
|
966
|
+
if (scope.kind === "all") return doc;
|
|
967
|
+
const selected = new Set(scope.tableIds);
|
|
968
|
+
return {
|
|
969
|
+
...doc,
|
|
970
|
+
model: {
|
|
971
|
+
...doc.model,
|
|
972
|
+
tables: doc.model.tables.filter((t) => selected.has(t.id)),
|
|
973
|
+
relationships: doc.model.relationships.filter(
|
|
974
|
+
(r) =>
|
|
975
|
+
selected.has(r.sourceTableId) &&
|
|
976
|
+
selected.has(r.targetTableId),
|
|
977
|
+
),
|
|
978
|
+
indexes: doc.model.indexes.filter((i) => selected.has(i.tableId)),
|
|
979
|
+
},
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function styleBasedDdlGenerator(
|
|
984
|
+
input: DdlGenerateInput,
|
|
985
|
+
options?: CoreDbMetaOptions,
|
|
986
|
+
): DdlGenerateOutput {
|
|
987
|
+
const scoped = sliceDocByScope(input.doc, input.scope);
|
|
988
|
+
return {
|
|
989
|
+
sql: buildDdlSql(scoped, options),
|
|
990
|
+
diagnostics: analyzeDdlDocument(scoped, options),
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function hasBuiltinDialect(
|
|
995
|
+
dialectId: RdbmsDialect,
|
|
996
|
+
): dialectId is BuiltinRdbmsDialect {
|
|
997
|
+
return (BUILTIN_DIALECTS as readonly string[]).includes(dialectId);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function getBuiltinDdlGenerator(
|
|
1001
|
+
dialectId: RdbmsDialect,
|
|
1002
|
+
_options?: CoreDbMetaOptions,
|
|
1003
|
+
): DdlGeneratorHook | undefined {
|
|
1004
|
+
if (!hasBuiltinDialect(dialectId)) return undefined;
|
|
1005
|
+
return (input) => styleBasedDdlGenerator(input, _options);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function invokeDdlGeneratorSync(
|
|
1009
|
+
generator: DdlGeneratorHook,
|
|
1010
|
+
input: DdlGenerateInput,
|
|
1011
|
+
): DdlGenerateOutput {
|
|
1012
|
+
const result = generator(input);
|
|
1013
|
+
if (
|
|
1014
|
+
result &&
|
|
1015
|
+
typeof (result as Promise<DdlGenerateOutput>).then === "function"
|
|
1016
|
+
) {
|
|
1017
|
+
throw new Error("Async DDL generator is not supported in sync API");
|
|
1018
|
+
}
|
|
1019
|
+
return result as DdlGenerateOutput;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function runDdlGenerator(
|
|
1023
|
+
doc: DesignDocument,
|
|
1024
|
+
scope: DdlScope,
|
|
1025
|
+
options?: CoreDbMetaOptions,
|
|
1026
|
+
): DdlGenerateOutput {
|
|
1027
|
+
const input: DdlGenerateInput = {
|
|
1028
|
+
doc,
|
|
1029
|
+
dialectId: doc.model.dialect,
|
|
1030
|
+
scope,
|
|
1031
|
+
};
|
|
1032
|
+
const hostGenerator = options?.hostDdlGenerators?.[doc.model.dialect];
|
|
1033
|
+
const builtinGenerator = getBuiltinDdlGenerator(doc.model.dialect, options);
|
|
1034
|
+
const fallback = () => styleBasedDdlGenerator(input, options);
|
|
1035
|
+
const fallbackOnError = options?.fallbackOnHookError ?? true;
|
|
1036
|
+
|
|
1037
|
+
const selectedGenerator = hostGenerator ?? builtinGenerator;
|
|
1038
|
+
if (!selectedGenerator) return fallback();
|
|
1039
|
+
try {
|
|
1040
|
+
return invokeDdlGeneratorSync(selectedGenerator, input);
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
if (!fallbackOnError) throw error;
|
|
1043
|
+
return fallback();
|
|
1044
|
+
}
|
|
332
1045
|
}
|
|
333
1046
|
|
|
334
1047
|
/** DDL/인덱스 분석 공통 심각도 */
|
|
@@ -336,11 +1049,11 @@ export type DdlDiagnosticSeverity = "error" | "warning";
|
|
|
336
1049
|
|
|
337
1050
|
/** 구조화된 DDL 관련 진단(에러·경고) */
|
|
338
1051
|
export interface DdlDiagnostic {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
1052
|
+
severity: DdlDiagnosticSeverity;
|
|
1053
|
+
code: string;
|
|
1054
|
+
message: string;
|
|
1055
|
+
/** 예: relationship:rel1, index:ix1, table:t1 */
|
|
1056
|
+
context?: string;
|
|
344
1057
|
}
|
|
345
1058
|
|
|
346
1059
|
/**
|
|
@@ -348,159 +1061,212 @@ export interface DdlDiagnostic {
|
|
|
348
1061
|
* 예: `[WARN][DDL_REL_MISSING_COLUMNS] ... | relationship:rel1`
|
|
349
1062
|
*/
|
|
350
1063
|
export function formatDdlDiagnostic(d: DdlDiagnostic): string {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
1064
|
+
const level = d.severity === "error" ? "ERROR" : "WARN";
|
|
1065
|
+
const tail = d.context ? ` | ${d.context}` : "";
|
|
1066
|
+
return `[${level}][${d.code}] ${d.message}${tail}`;
|
|
354
1067
|
}
|
|
355
1068
|
|
|
356
1069
|
export function formatDdlDiagnostics(diagnostics: DdlDiagnostic[]): string {
|
|
357
|
-
|
|
1070
|
+
return diagnostics.map(formatDdlDiagnostic).join("\n");
|
|
358
1071
|
}
|
|
359
1072
|
|
|
360
1073
|
/**
|
|
361
1074
|
* 모델 전체에 대한 DDL·인덱스 관련 진단(테이블/관계/인덱스).
|
|
362
1075
|
* 검증 예외(Invalid design document)와는 별도로, 생성 가능 여부와 경고를 나열한다.
|
|
363
1076
|
*/
|
|
364
|
-
export function analyzeDdlDocument(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
for (const rel of model.relationships) {
|
|
396
|
-
const ctx = `relationship:${rel.id}`;
|
|
397
|
-
const sourceTable = tableById.get(rel.sourceTableId);
|
|
398
|
-
const targetTable = tableById.get(rel.targetTableId);
|
|
399
|
-
if (!sourceTable || !targetTable) {
|
|
400
|
-
out.push({
|
|
401
|
-
severity: "error",
|
|
402
|
-
code: "DDL_REL_UNKNOWN_TABLE",
|
|
403
|
-
message: "관계의 소스 또는 타겟 테이블을 찾을 수 없어 FK DDL을 생성할 수 없다.",
|
|
404
|
-
context: ctx
|
|
405
|
-
});
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
if (!rel.sourceColumnId || !rel.targetColumnId) {
|
|
409
|
-
out.push({
|
|
410
|
-
severity: "warning",
|
|
411
|
-
code: "DDL_REL_MISSING_COLUMNS",
|
|
412
|
-
message: "소스/타겟 컬럼이 지정되지 않아 FK DDL을 생략한다.",
|
|
413
|
-
context: ctx
|
|
414
|
-
});
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
const sourceColumn = sourceTable.columns.find((c) => c.id === rel.sourceColumnId);
|
|
418
|
-
const targetColumn = targetTable.columns.find((c) => c.id === rel.targetColumnId);
|
|
419
|
-
if (!sourceColumn || !targetColumn) {
|
|
420
|
-
out.push({
|
|
421
|
-
severity: "warning",
|
|
422
|
-
code: "DDL_REL_UNKNOWN_COLUMN",
|
|
423
|
-
message: "관계에 지정된 컬럼 id를 테이블에서 찾을 수 없어 FK DDL을 생략한다.",
|
|
424
|
-
context: ctx
|
|
425
|
-
});
|
|
1077
|
+
export function analyzeDdlDocument(
|
|
1078
|
+
doc: DesignDocument,
|
|
1079
|
+
options?: CoreDbMetaOptions,
|
|
1080
|
+
): DdlDiagnostic[] {
|
|
1081
|
+
const out: DdlDiagnostic[] = [];
|
|
1082
|
+
const { model } = doc;
|
|
1083
|
+
const tableById = new Map(model.tables.map((t) => [t.id, t]));
|
|
1084
|
+
const physicalTableNames = new Map<string, string[]>();
|
|
1085
|
+
|
|
1086
|
+
for (const table of model.tables) {
|
|
1087
|
+
const qualifiedName =
|
|
1088
|
+
dialectSupportsSchema(model.dialect, options) &&
|
|
1089
|
+
table.schemaName?.trim()
|
|
1090
|
+
? `${table.schemaName.trim()}.${table.physicalName}`
|
|
1091
|
+
: table.physicalName;
|
|
1092
|
+
const list = physicalTableNames.get(qualifiedName) ?? [];
|
|
1093
|
+
list.push(table.id);
|
|
1094
|
+
physicalTableNames.set(qualifiedName, list);
|
|
1095
|
+
if (table.columns.length === 0) {
|
|
1096
|
+
out.push({
|
|
1097
|
+
severity: "warning",
|
|
1098
|
+
code: "DDL_EMPTY_TABLE",
|
|
1099
|
+
message:
|
|
1100
|
+
"컬럼이 없는 테이블은 CREATE TABLE 구문이 비어 있거나 무의미할 수 있다.",
|
|
1101
|
+
context: `table:${table.id}`,
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
426
1104
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
context: ctx
|
|
438
|
-
});
|
|
439
|
-
continue;
|
|
1105
|
+
|
|
1106
|
+
for (const [name, ids] of physicalTableNames) {
|
|
1107
|
+
if (ids.length > 1) {
|
|
1108
|
+
out.push({
|
|
1109
|
+
severity: "warning",
|
|
1110
|
+
code: "DDL_DUPLICATE_TABLE_NAME",
|
|
1111
|
+
message: `동일한 물리 테이블명 "${name}"이 ${ids.length}개 테이블에 사용되었다.`,
|
|
1112
|
+
context: `tables:${ids.join(",")}`,
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
440
1115
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
1116
|
+
|
|
1117
|
+
for (const rel of model.relationships) {
|
|
1118
|
+
const ctx = `relationship:${rel.id}`;
|
|
1119
|
+
const sourceTable = tableById.get(rel.sourceTableId);
|
|
1120
|
+
const targetTable = tableById.get(rel.targetTableId);
|
|
1121
|
+
if (!sourceTable || !targetTable) {
|
|
1122
|
+
out.push({
|
|
1123
|
+
severity: "error",
|
|
1124
|
+
code: "DDL_REL_UNKNOWN_TABLE",
|
|
1125
|
+
message:
|
|
1126
|
+
"관계의 소스 또는 타겟 테이블을 찾을 수 없어 FK DDL을 생성할 수 없다.",
|
|
1127
|
+
context: ctx,
|
|
1128
|
+
});
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (!rel.sourceColumnId || !rel.targetColumnId) {
|
|
1132
|
+
out.push({
|
|
1133
|
+
severity: "warning",
|
|
1134
|
+
code: "DDL_REL_MISSING_COLUMNS",
|
|
1135
|
+
message: "소스/타겟 컬럼이 지정되지 않아 FK DDL을 생략한다.",
|
|
1136
|
+
context: ctx,
|
|
1137
|
+
});
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
const sourceColumn = sourceTable.columns.find(
|
|
1141
|
+
(c) => c.id === rel.sourceColumnId,
|
|
1142
|
+
);
|
|
1143
|
+
const targetColumn = targetTable.columns.find(
|
|
1144
|
+
(c) => c.id === rel.targetColumnId,
|
|
1145
|
+
);
|
|
1146
|
+
if (!sourceColumn || !targetColumn) {
|
|
1147
|
+
out.push({
|
|
1148
|
+
severity: "warning",
|
|
1149
|
+
code: "DDL_REL_UNKNOWN_COLUMN",
|
|
1150
|
+
message:
|
|
1151
|
+
"관계에 지정된 컬럼 id를 테이블에서 찾을 수 없어 FK DDL을 생략한다.",
|
|
1152
|
+
context: ctx,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
449
1155
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
1156
|
+
|
|
1157
|
+
for (const indexModel of model.indexes) {
|
|
1158
|
+
const ctx = `index:${indexModel.id}`;
|
|
1159
|
+
const table = tableById.get(indexModel.tableId);
|
|
1160
|
+
if (!table) {
|
|
1161
|
+
out.push({
|
|
1162
|
+
severity: "warning",
|
|
1163
|
+
code: "IDX_UNKNOWN_TABLE",
|
|
1164
|
+
message:
|
|
1165
|
+
"인덱스가 가리키는 테이블을 찾을 수 없어 인덱스 DDL을 생략한다.",
|
|
1166
|
+
context: ctx,
|
|
1167
|
+
});
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
if (indexModel.columns.length === 0) {
|
|
1171
|
+
out.push({
|
|
1172
|
+
severity: "warning",
|
|
1173
|
+
code: "IDX_EMPTY_COLUMNS",
|
|
1174
|
+
message: "인덱스 컬럼 목록이 비어 있어 인덱스 DDL을 생략한다.",
|
|
1175
|
+
context: ctx,
|
|
1176
|
+
});
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
for (const colPhys of indexModel.columns) {
|
|
1180
|
+
if (!table.columns.some((c) => c.physicalName === colPhys)) {
|
|
1181
|
+
out.push({
|
|
1182
|
+
severity: "warning",
|
|
1183
|
+
code: "IDX_UNKNOWN_COLUMN",
|
|
1184
|
+
message: `인덱스가 참조하는 물리 컬럼명 "${colPhys}"을(를) 테이블 "${table.physicalName}"에서 찾을 수 없다.`,
|
|
1185
|
+
context: ctx,
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
459
1189
|
}
|
|
460
|
-
}
|
|
461
1190
|
|
|
462
|
-
|
|
1191
|
+
return out;
|
|
463
1192
|
}
|
|
464
1193
|
|
|
465
|
-
export function generateDdlWithDiagnostics(
|
|
466
|
-
|
|
1194
|
+
export function generateDdlWithDiagnostics(
|
|
1195
|
+
doc: DesignDocument,
|
|
1196
|
+
options?: CoreDbMetaOptions,
|
|
1197
|
+
): { sql: string; diagnostics: DdlDiagnostic[] } {
|
|
1198
|
+
const out = runDdlGenerator(doc, { kind: "all" }, options);
|
|
1199
|
+
return {
|
|
1200
|
+
sql: out.sql,
|
|
1201
|
+
diagnostics: out.diagnostics ?? analyzeDdlDocument(doc, options),
|
|
1202
|
+
};
|
|
467
1203
|
}
|
|
468
1204
|
|
|
469
|
-
export function generateIndexDdlWithDiagnostics(
|
|
470
|
-
|
|
1205
|
+
export function generateIndexDdlWithDiagnostics(
|
|
1206
|
+
doc: DesignDocument,
|
|
1207
|
+
options?: CoreDbMetaOptions,
|
|
1208
|
+
): { sql: string; diagnostics: DdlDiagnostic[] } {
|
|
1209
|
+
return {
|
|
1210
|
+
sql: buildIndexDdlSql(doc, options),
|
|
1211
|
+
diagnostics: analyzeDdlDocument(doc, options),
|
|
1212
|
+
};
|
|
471
1213
|
}
|
|
472
1214
|
|
|
473
|
-
export function generateDdl(
|
|
474
|
-
|
|
1215
|
+
export function generateDdl(
|
|
1216
|
+
doc: DesignDocument,
|
|
1217
|
+
options?: CoreDbMetaOptions,
|
|
1218
|
+
): string {
|
|
1219
|
+
return runDdlGenerator(doc, { kind: "all" }, options).sql;
|
|
475
1220
|
}
|
|
476
1221
|
|
|
477
|
-
export function
|
|
478
|
-
|
|
1222
|
+
export function generateDdlForSelection(
|
|
1223
|
+
doc: DesignDocument,
|
|
1224
|
+
tableIds: string[],
|
|
1225
|
+
options?: CoreDbMetaOptions,
|
|
1226
|
+
): { sql: string; diagnostics: DdlDiagnostic[] } {
|
|
1227
|
+
const out = runDdlGenerator(doc, { kind: "selected", tableIds }, options);
|
|
1228
|
+
const diagnostics =
|
|
1229
|
+
out.diagnostics ??
|
|
1230
|
+
analyzeDdlDocument(
|
|
1231
|
+
sliceDocByScope(doc, { kind: "selected", tableIds }),
|
|
1232
|
+
options,
|
|
1233
|
+
);
|
|
1234
|
+
return { sql: out.sql, diagnostics };
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
export function generateIndexDdl(
|
|
1238
|
+
doc: DesignDocument,
|
|
1239
|
+
options?: CoreDbMetaOptions,
|
|
1240
|
+
): string {
|
|
1241
|
+
return buildIndexDdlSql(doc, options);
|
|
479
1242
|
}
|
|
480
1243
|
|
|
481
1244
|
/** 성능·부하 테스트용: 지정 개수의 빈 테이블과 격자 레이아웃을 생성한다. */
|
|
482
|
-
export function createLargeDesign(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
1245
|
+
export function createLargeDesign(
|
|
1246
|
+
tableCount: number,
|
|
1247
|
+
dialect: RdbmsDialect = "postgres",
|
|
1248
|
+
): DesignDocument {
|
|
1249
|
+
const doc = createEmptyDesign(dialect);
|
|
1250
|
+
const cols = 20;
|
|
1251
|
+
for (let i = 0; i < tableCount; i++) {
|
|
1252
|
+
const id = `t-${i}`;
|
|
1253
|
+
doc.model.tables.push({
|
|
1254
|
+
id,
|
|
1255
|
+
logicalName: `엔티티${i}`,
|
|
1256
|
+
physicalName: `TB_T${i}`,
|
|
1257
|
+
columns: [
|
|
1258
|
+
createColumn(dialect, {
|
|
1259
|
+
id: `${id}-pk`,
|
|
1260
|
+
logicalName: "식별자",
|
|
1261
|
+
logicalType: "NUMBER",
|
|
1262
|
+
nullable: false,
|
|
1263
|
+
}),
|
|
1264
|
+
],
|
|
1265
|
+
});
|
|
1266
|
+
doc.layout.nodePositions[id] = {
|
|
1267
|
+
x: (i % cols) * 220,
|
|
1268
|
+
y: Math.floor(i / cols) * 120,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
return doc;
|
|
506
1272
|
}
|