@rdbms-erd/core 0.1.0 → 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 -0
- 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
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { createColumn, createEmptyDesign, generateDdl, generateIndexDdl } from "../index";
|
|
3
|
-
|
|
4
|
-
function minimalDoc(dialect: "mssql" | "oracle" | "mysql" | "postgres") {
|
|
5
|
-
const doc = createEmptyDesign(dialect);
|
|
6
|
-
doc.model.tables.push({
|
|
7
|
-
id: "parent",
|
|
8
|
-
logicalName: "부모",
|
|
9
|
-
physicalName: "TB_PARENT",
|
|
10
|
-
columns: [
|
|
11
|
-
createColumn(dialect, {
|
|
12
|
-
id: "pk",
|
|
13
|
-
logicalName: "식별자",
|
|
14
|
-
logicalType: "NUMBER",
|
|
15
|
-
nullable: false
|
|
16
|
-
})
|
|
17
|
-
]
|
|
18
|
-
});
|
|
19
|
-
doc.model.tables.push({
|
|
20
|
-
id: "child",
|
|
21
|
-
logicalName: "자식",
|
|
22
|
-
physicalName: "TB_CHILD",
|
|
23
|
-
columns: [
|
|
24
|
-
createColumn(dialect, {
|
|
25
|
-
id: "fk",
|
|
26
|
-
logicalName: "부모참조",
|
|
27
|
-
logicalType: "NUMBER",
|
|
28
|
-
nullable: false
|
|
29
|
-
})
|
|
30
|
-
]
|
|
31
|
-
});
|
|
32
|
-
doc.model.relationships.push({
|
|
33
|
-
id: "rel1",
|
|
34
|
-
sourceTableId: "parent",
|
|
35
|
-
targetTableId: "child",
|
|
36
|
-
sourceColumnId: "pk",
|
|
37
|
-
targetColumnId: "fk"
|
|
38
|
-
});
|
|
39
|
-
doc.model.indexes.push({
|
|
40
|
-
id: "ix1",
|
|
41
|
-
tableId: "child",
|
|
42
|
-
name: "IX_CHILD_PARENT",
|
|
43
|
-
columns: ["부모참조"],
|
|
44
|
-
unique: false
|
|
45
|
-
});
|
|
46
|
-
return doc;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
describe("generateDdl / generateIndexDdl", () => {
|
|
50
|
-
it.each(["postgres", "mysql", "oracle", "mssql"] as const)("dialect %s emits CREATE TABLE and FK", (dialect) => {
|
|
51
|
-
const sql = generateDdl(minimalDoc(dialect));
|
|
52
|
-
expect(sql).toContain("CREATE TABLE");
|
|
53
|
-
expect(sql.toUpperCase()).toContain("FOREIGN KEY");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("emits CREATE INDEX", () => {
|
|
57
|
-
const ix = generateIndexDdl(minimalDoc("postgres"));
|
|
58
|
-
expect(ix.toUpperCase()).toContain("CREATE ");
|
|
59
|
-
expect(ix.toUpperCase()).toContain("INDEX");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("includes PK, NOT NULL, and DEFAULT in table DDL", () => {
|
|
63
|
-
const doc = createEmptyDesign("postgres");
|
|
64
|
-
doc.model.tables.push({
|
|
65
|
-
id: "t1",
|
|
66
|
-
logicalName: "사용자",
|
|
67
|
-
physicalName: "TB_USER",
|
|
68
|
-
columns: [
|
|
69
|
-
createColumn("postgres", {
|
|
70
|
-
id: "id",
|
|
71
|
-
logicalName: "아이디",
|
|
72
|
-
physicalName: "USER_ID",
|
|
73
|
-
logicalType: "NUMBER",
|
|
74
|
-
isPrimaryKey: true
|
|
75
|
-
}),
|
|
76
|
-
createColumn("postgres", {
|
|
77
|
-
id: "name",
|
|
78
|
-
logicalName: "이름",
|
|
79
|
-
physicalName: "USER_NAME",
|
|
80
|
-
logicalType: "TEXT",
|
|
81
|
-
nullable: false,
|
|
82
|
-
defaultValue: "UNKNOWN"
|
|
83
|
-
})
|
|
84
|
-
]
|
|
85
|
-
});
|
|
86
|
-
const sql = generateDdl(doc);
|
|
87
|
-
expect(sql).toContain("PRIMARY KEY");
|
|
88
|
-
expect(sql).toContain("NOT NULL");
|
|
89
|
-
expect(sql).toContain("DEFAULT 'UNKNOWN'");
|
|
90
|
-
});
|
|
91
|
-
});
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createColumn, createEmptyDesign, generateDdl, generateIndexDdl } from "../index";
|
|
3
|
+
|
|
4
|
+
function minimalDoc(dialect: "mssql" | "oracle" | "mysql" | "postgres" | "sqlite") {
|
|
5
|
+
const doc = createEmptyDesign(dialect);
|
|
6
|
+
doc.model.tables.push({
|
|
7
|
+
id: "parent",
|
|
8
|
+
logicalName: "부모",
|
|
9
|
+
physicalName: "TB_PARENT",
|
|
10
|
+
columns: [
|
|
11
|
+
createColumn(dialect, {
|
|
12
|
+
id: "pk",
|
|
13
|
+
logicalName: "식별자",
|
|
14
|
+
logicalType: "NUMBER",
|
|
15
|
+
nullable: false
|
|
16
|
+
})
|
|
17
|
+
]
|
|
18
|
+
});
|
|
19
|
+
doc.model.tables.push({
|
|
20
|
+
id: "child",
|
|
21
|
+
logicalName: "자식",
|
|
22
|
+
physicalName: "TB_CHILD",
|
|
23
|
+
columns: [
|
|
24
|
+
createColumn(dialect, {
|
|
25
|
+
id: "fk",
|
|
26
|
+
logicalName: "부모참조",
|
|
27
|
+
logicalType: "NUMBER",
|
|
28
|
+
nullable: false
|
|
29
|
+
})
|
|
30
|
+
]
|
|
31
|
+
});
|
|
32
|
+
doc.model.relationships.push({
|
|
33
|
+
id: "rel1",
|
|
34
|
+
sourceTableId: "parent",
|
|
35
|
+
targetTableId: "child",
|
|
36
|
+
sourceColumnId: "pk",
|
|
37
|
+
targetColumnId: "fk"
|
|
38
|
+
});
|
|
39
|
+
doc.model.indexes.push({
|
|
40
|
+
id: "ix1",
|
|
41
|
+
tableId: "child",
|
|
42
|
+
name: "IX_CHILD_PARENT",
|
|
43
|
+
columns: ["부모참조"],
|
|
44
|
+
unique: false
|
|
45
|
+
});
|
|
46
|
+
return doc;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("generateDdl / generateIndexDdl", () => {
|
|
50
|
+
it.each(["postgres", "mysql", "oracle", "mssql", "sqlite"] as const)("dialect %s emits CREATE TABLE and FK", (dialect) => {
|
|
51
|
+
const sql = generateDdl(minimalDoc(dialect));
|
|
52
|
+
expect(sql).toContain("CREATE TABLE");
|
|
53
|
+
expect(sql.toUpperCase()).toContain("FOREIGN KEY");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("emits CREATE INDEX", () => {
|
|
57
|
+
const ix = generateIndexDdl(minimalDoc("postgres"));
|
|
58
|
+
expect(ix.toUpperCase()).toContain("CREATE ");
|
|
59
|
+
expect(ix.toUpperCase()).toContain("INDEX");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("includes PK, NOT NULL, and DEFAULT in table DDL", () => {
|
|
63
|
+
const doc = createEmptyDesign("postgres");
|
|
64
|
+
doc.model.tables.push({
|
|
65
|
+
id: "t1",
|
|
66
|
+
logicalName: "사용자",
|
|
67
|
+
physicalName: "TB_USER",
|
|
68
|
+
columns: [
|
|
69
|
+
createColumn("postgres", {
|
|
70
|
+
id: "id",
|
|
71
|
+
logicalName: "아이디",
|
|
72
|
+
physicalName: "USER_ID",
|
|
73
|
+
logicalType: "NUMBER",
|
|
74
|
+
isPrimaryKey: true
|
|
75
|
+
}),
|
|
76
|
+
createColumn("postgres", {
|
|
77
|
+
id: "name",
|
|
78
|
+
logicalName: "이름",
|
|
79
|
+
physicalName: "USER_NAME",
|
|
80
|
+
logicalType: "TEXT",
|
|
81
|
+
nullable: false,
|
|
82
|
+
defaultValue: "UNKNOWN"
|
|
83
|
+
})
|
|
84
|
+
]
|
|
85
|
+
});
|
|
86
|
+
const sql = generateDdl(doc);
|
|
87
|
+
expect(sql).toContain("PRIMARY KEY");
|
|
88
|
+
expect(sql).toContain("NOT NULL");
|
|
89
|
+
expect(sql).toContain("DEFAULT 'UNKNOWN'");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -1,78 +1,129 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
createColumn,
|
|
4
|
-
createEmptyDesign,
|
|
5
|
-
isRelationshipLineRenderable,
|
|
6
|
-
parseDesign,
|
|
7
|
-
roundTripDesign,
|
|
8
|
-
serializeDesign,
|
|
9
|
-
validateDesignDocument
|
|
10
|
-
} from "../index";
|
|
11
|
-
|
|
12
|
-
describe("DesignDocument", () => {
|
|
13
|
-
it("round-trips", () => {
|
|
14
|
-
const doc = createEmptyDesign("mysql");
|
|
15
|
-
doc.model.tables.push({
|
|
16
|
-
id: "t1",
|
|
17
|
-
logicalName: "엔티티",
|
|
18
|
-
physicalName: "TB_T1",
|
|
19
|
-
columns: []
|
|
20
|
-
});
|
|
21
|
-
const again = roundTripDesign(doc);
|
|
22
|
-
expect(again.model.dialect).toBe("mysql");
|
|
23
|
-
expect(again.model.tables).toHaveLength(1);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("rejects invalid dialect", () => {
|
|
27
|
-
const raw = JSON.parse(serializeDesign(createEmptyDesign()));
|
|
28
|
-
raw.model.dialect = "
|
|
29
|
-
expect(() => validateDesignDocument(raw)).toThrow(/dialect/);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("parseDesign validates", () => {
|
|
33
|
-
const json = serializeDesign(createEmptyDesign("oracle"));
|
|
34
|
-
expect(parseDesign(json).model.dialect).toBe("oracle");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("createEmptyDesign defaults to mssql", () => {
|
|
38
|
-
expect(createEmptyDesign().model.dialect).toBe("mssql");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("isRelationshipLineRenderable respects
|
|
42
|
-
const doc = createEmptyDesign("postgres");
|
|
43
|
-
doc.model.tables.push(
|
|
44
|
-
{
|
|
45
|
-
id: "a",
|
|
46
|
-
logicalName: "A",
|
|
47
|
-
physicalName: "TA",
|
|
48
|
-
columns: [createColumn("postgres", { id: "apk", logicalName: "id", logicalType: "NUMBER", nullable: false, isPrimaryKey: true })]
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: "b",
|
|
52
|
-
logicalName: "B",
|
|
53
|
-
physicalName: "TB",
|
|
54
|
-
columns: [
|
|
55
|
-
createColumn("postgres", {
|
|
56
|
-
id: "bfk",
|
|
57
|
-
logicalName: "aid",
|
|
58
|
-
logicalType: "NUMBER",
|
|
59
|
-
isForeignKey: true
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
72
|
-
doc.model.relationships.push(rel);
|
|
73
|
-
expect(isRelationshipLineRenderable(rel,
|
|
74
|
-
expect(isRelationshipLineRenderable(rel,
|
|
75
|
-
|
|
76
|
-
expect(isRelationshipLineRenderable(rel,
|
|
77
|
-
});
|
|
78
|
-
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createColumn,
|
|
4
|
+
createEmptyDesign,
|
|
5
|
+
isRelationshipLineRenderable,
|
|
6
|
+
parseDesign,
|
|
7
|
+
roundTripDesign,
|
|
8
|
+
serializeDesign,
|
|
9
|
+
validateDesignDocument,
|
|
10
|
+
} from "../index";
|
|
11
|
+
|
|
12
|
+
describe("DesignDocument", () => {
|
|
13
|
+
it("round-trips", () => {
|
|
14
|
+
const doc = createEmptyDesign("mysql");
|
|
15
|
+
doc.model.tables.push({
|
|
16
|
+
id: "t1",
|
|
17
|
+
logicalName: "엔티티",
|
|
18
|
+
physicalName: "TB_T1",
|
|
19
|
+
columns: []
|
|
20
|
+
});
|
|
21
|
+
const again = roundTripDesign(doc);
|
|
22
|
+
expect(again.model.dialect).toBe("mysql");
|
|
23
|
+
expect(again.model.tables).toHaveLength(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("rejects invalid dialect", () => {
|
|
27
|
+
const raw = JSON.parse(serializeDesign(createEmptyDesign()));
|
|
28
|
+
raw.model.dialect = "mariadb";
|
|
29
|
+
expect(() => validateDesignDocument(raw)).toThrow(/dialect/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("parseDesign validates", () => {
|
|
33
|
+
const json = serializeDesign(createEmptyDesign("oracle"));
|
|
34
|
+
expect(parseDesign(json).model.dialect).toBe("oracle");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("createEmptyDesign defaults to mssql", () => {
|
|
38
|
+
expect(createEmptyDesign().model.dialect).toBe("mssql");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("isRelationshipLineRenderable respects relationship canvasLineHidden and reveal toggle", () => {
|
|
42
|
+
const doc = createEmptyDesign("postgres");
|
|
43
|
+
doc.model.tables.push(
|
|
44
|
+
{
|
|
45
|
+
id: "a",
|
|
46
|
+
logicalName: "A",
|
|
47
|
+
physicalName: "TA",
|
|
48
|
+
columns: [createColumn("postgres", { id: "apk", logicalName: "id", logicalType: "NUMBER", nullable: false, isPrimaryKey: true })]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "b",
|
|
52
|
+
logicalName: "B",
|
|
53
|
+
physicalName: "TB",
|
|
54
|
+
columns: [
|
|
55
|
+
createColumn("postgres", {
|
|
56
|
+
id: "bfk",
|
|
57
|
+
logicalName: "aid",
|
|
58
|
+
logicalType: "NUMBER",
|
|
59
|
+
isForeignKey: true
|
|
60
|
+
})
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
const rel = {
|
|
65
|
+
id: "r1",
|
|
66
|
+
sourceTableId: "a",
|
|
67
|
+
targetTableId: "b",
|
|
68
|
+
sourceColumnId: "apk",
|
|
69
|
+
targetColumnId: "bfk",
|
|
70
|
+
canvasLineHidden: true as const
|
|
71
|
+
};
|
|
72
|
+
doc.model.relationships.push(rel);
|
|
73
|
+
expect(isRelationshipLineRenderable(rel, false)).toBe(false);
|
|
74
|
+
expect(isRelationshipLineRenderable(rel, true)).toBe(true);
|
|
75
|
+
delete rel.canvasLineHidden;
|
|
76
|
+
expect(isRelationshipLineRenderable(rel, false)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("migrates legacy column showFkRelationLine false to relationship canvasLineHidden", () => {
|
|
80
|
+
const doc = createEmptyDesign("postgres");
|
|
81
|
+
doc.model.tables.push(
|
|
82
|
+
{
|
|
83
|
+
id: "a",
|
|
84
|
+
logicalName: "A",
|
|
85
|
+
physicalName: "TA",
|
|
86
|
+
columns: [
|
|
87
|
+
createColumn("postgres", {
|
|
88
|
+
id: "apk",
|
|
89
|
+
logicalName: "id",
|
|
90
|
+
logicalType: "NUMBER",
|
|
91
|
+
nullable: false,
|
|
92
|
+
isPrimaryKey: true,
|
|
93
|
+
}),
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "b",
|
|
98
|
+
logicalName: "B",
|
|
99
|
+
physicalName: "TB",
|
|
100
|
+
columns: [
|
|
101
|
+
createColumn("postgres", {
|
|
102
|
+
id: "bfk",
|
|
103
|
+
logicalName: "aid",
|
|
104
|
+
logicalType: "NUMBER",
|
|
105
|
+
isForeignKey: true,
|
|
106
|
+
}),
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
doc.model.relationships.push({
|
|
111
|
+
id: "r1",
|
|
112
|
+
sourceTableId: "a",
|
|
113
|
+
targetTableId: "b",
|
|
114
|
+
sourceColumnId: "apk",
|
|
115
|
+
targetColumnId: "bfk",
|
|
116
|
+
});
|
|
117
|
+
const raw = JSON.parse(serializeDesign(doc)) as Record<string, unknown>;
|
|
118
|
+
const tables = (raw.model as { tables: { id: string; columns: Record<string, unknown>[] }[] }).tables;
|
|
119
|
+
const bTable = tables.find((t) => t.id === "b");
|
|
120
|
+
const fkCol = bTable?.columns.find((c) => c.id === "bfk");
|
|
121
|
+
if (fkCol) fkCol.showFkRelationLine = false;
|
|
122
|
+
const out = validateDesignDocument(raw);
|
|
123
|
+
expect(out.model.relationships.find((r) => r.id === "r1")?.canvasLineHidden).toBe(true);
|
|
124
|
+
const migratedFk = out.model.tables.find((t) => t.id === "b")?.columns.find((c) => c.id === "bfk");
|
|
125
|
+
expect(
|
|
126
|
+
migratedFk && "showFkRelationLine" in migratedFk ? (migratedFk as { showFkRelationLine?: boolean }).showFkRelationLine : undefined,
|
|
127
|
+
).toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -1,37 +1,94 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { applyLogicalTypeChange, createColumn, defaultPhysicalType } from "../index";
|
|
3
|
-
|
|
4
|
-
describe("logical / physical types", () => {
|
|
5
|
-
it("createColumn defaults physical name to logical name and sets default physical type", () => {
|
|
6
|
-
const col = createColumn("postgres", {
|
|
7
|
-
id: "c1",
|
|
8
|
-
logicalName: "이름",
|
|
9
|
-
logicalType: "TEXT"
|
|
10
|
-
});
|
|
11
|
-
expect(col.physicalName).toBe("이름");
|
|
12
|
-
expect(col.physicalType).toBe(defaultPhysicalType("postgres", "TEXT"));
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("createColumn uses explicit physical name when provided", () => {
|
|
16
|
-
const col = createColumn("postgres", {
|
|
17
|
-
id: "c1",
|
|
18
|
-
logicalName: "이름",
|
|
19
|
-
physicalName: "Name",
|
|
20
|
-
logicalType: "TEXT"
|
|
21
|
-
});
|
|
22
|
-
expect(col.physicalName).toBe("Name");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("applyLogicalTypeChange resets physical to default", () => {
|
|
26
|
-
let col = createColumn("mssql", {
|
|
27
|
-
id: "c1",
|
|
28
|
-
logicalName: "수량",
|
|
29
|
-
physicalName: "Qty",
|
|
30
|
-
logicalType: "NUMBER"
|
|
31
|
-
});
|
|
32
|
-
col = { ...col, physicalType: "BIGINT" };
|
|
33
|
-
col = applyLogicalTypeChange(col, "FLOAT", "mssql");
|
|
34
|
-
expect(col.logicalType).toBe("FLOAT");
|
|
35
|
-
expect(col.physicalType).toBe(defaultPhysicalType("mssql", "FLOAT"));
|
|
36
|
-
});
|
|
37
|
-
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { applyLogicalTypeChange, convertDesignDialect, createColumn, createEmptyDesign, defaultPhysicalType } from "../index";
|
|
3
|
+
|
|
4
|
+
describe("logical / physical types", () => {
|
|
5
|
+
it("createColumn defaults physical name to logical name and sets default physical type", () => {
|
|
6
|
+
const col = createColumn("postgres", {
|
|
7
|
+
id: "c1",
|
|
8
|
+
logicalName: "이름",
|
|
9
|
+
logicalType: "TEXT"
|
|
10
|
+
});
|
|
11
|
+
expect(col.physicalName).toBe("이름");
|
|
12
|
+
expect(col.physicalType).toBe(defaultPhysicalType("postgres", "TEXT"));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("createColumn uses explicit physical name when provided", () => {
|
|
16
|
+
const col = createColumn("postgres", {
|
|
17
|
+
id: "c1",
|
|
18
|
+
logicalName: "이름",
|
|
19
|
+
physicalName: "Name",
|
|
20
|
+
logicalType: "TEXT"
|
|
21
|
+
});
|
|
22
|
+
expect(col.physicalName).toBe("Name");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("applyLogicalTypeChange resets physical to default", () => {
|
|
26
|
+
let col = createColumn("mssql", {
|
|
27
|
+
id: "c1",
|
|
28
|
+
logicalName: "수량",
|
|
29
|
+
physicalName: "Qty",
|
|
30
|
+
logicalType: "NUMBER"
|
|
31
|
+
});
|
|
32
|
+
col = { ...col, physicalType: "BIGINT" };
|
|
33
|
+
col = applyLogicalTypeChange(col, "FLOAT", "mssql");
|
|
34
|
+
expect(col.logicalType).toBe("FLOAT");
|
|
35
|
+
expect(col.physicalType).toBe(defaultPhysicalType("mssql", "FLOAT"));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("convertDesignDialect preserves numeric precision/scale when possible", () => {
|
|
39
|
+
const doc = createEmptyDesign("mssql");
|
|
40
|
+
doc.model.tables.push({
|
|
41
|
+
id: "t1",
|
|
42
|
+
logicalName: "주문",
|
|
43
|
+
physicalName: "orders",
|
|
44
|
+
columns: [
|
|
45
|
+
{
|
|
46
|
+
...createColumn("mssql", {
|
|
47
|
+
id: "c1",
|
|
48
|
+
logicalName: "금액",
|
|
49
|
+
physicalName: "amount",
|
|
50
|
+
logicalType: "NUMBER"
|
|
51
|
+
}),
|
|
52
|
+
physicalType: "NUMERIC(10,5)"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const oracleDoc = convertDesignDialect(doc, "oracle");
|
|
58
|
+
expect(oracleDoc.model.tables[0].columns[0].physicalType).toBe("NUMBER(10,5)");
|
|
59
|
+
|
|
60
|
+
const pgDoc = convertDesignDialect(doc, "postgres");
|
|
61
|
+
expect(pgDoc.model.tables[0].columns[0].physicalType).toBe("NUMERIC(10,5)");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("convertDesignDialect preserves varchar length when possible", () => {
|
|
65
|
+
const doc = createEmptyDesign("postgres");
|
|
66
|
+
doc.model.tables.push({
|
|
67
|
+
id: "t1",
|
|
68
|
+
logicalName: "상품",
|
|
69
|
+
physicalName: "products",
|
|
70
|
+
columns: [
|
|
71
|
+
{
|
|
72
|
+
...createColumn("postgres", {
|
|
73
|
+
id: "c1",
|
|
74
|
+
logicalName: "코드",
|
|
75
|
+
physicalName: "code",
|
|
76
|
+
logicalType: "TEXT"
|
|
77
|
+
}),
|
|
78
|
+
physicalType: "VARCHAR(120)"
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const mssqlDoc = convertDesignDialect(doc, "mssql");
|
|
84
|
+
expect(mssqlDoc.model.tables[0].columns[0].physicalType).toBe("NVARCHAR(120)");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("defaultPhysicalType includes newly added logical types", () => {
|
|
88
|
+
expect(defaultPhysicalType("postgres", "BOOLEAN")).toBe("BOOLEAN");
|
|
89
|
+
expect(defaultPhysicalType("mysql", "JSON")).toBe("JSON");
|
|
90
|
+
expect(defaultPhysicalType("oracle", "UUID")).toBe("RAW(16)");
|
|
91
|
+
expect(defaultPhysicalType("sqlite", "BINARY")).toBe("BLOB");
|
|
92
|
+
expect(defaultPhysicalType("mssql", "DECIMAL")).toBe("DECIMAL(10,2)");
|
|
93
|
+
});
|
|
94
|
+
});
|
package/src/alignment.ts
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
export type AlignCommand =
|
|
2
|
-
| "left"
|
|
3
|
-
| "h-center"
|
|
4
|
-
| "right"
|
|
5
|
-
| "top"
|
|
6
|
-
| "v-center"
|
|
7
|
-
| "bottom"
|
|
8
|
-
| "h-gap"
|
|
9
|
-
| "v-gap";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 멀티 선택된 노드 위치를 bounding box 기준으로 정렬/분배한다.
|
|
13
|
-
* positions는 id -> 좌표 맵이며, 반환값은 동일 키에 갱신된 좌표만 포함한다.
|
|
14
|
-
*/
|
|
15
|
-
export function alignNodePositions(
|
|
16
|
-
selectedIds: string[],
|
|
17
|
-
positions: Record<string, { x: number; y: number }>,
|
|
18
|
-
command: AlignCommand
|
|
19
|
-
): Record<string, { x: number; y: number }> {
|
|
20
|
-
const entries = selectedIds
|
|
21
|
-
.map((id) => {
|
|
22
|
-
const pos = positions[id];
|
|
23
|
-
return pos ? { id, pos: { ...pos } } : null;
|
|
24
|
-
})
|
|
25
|
-
.filter((e): e is { id: string; pos: { x: number; y: number } } => Boolean(e));
|
|
26
|
-
|
|
27
|
-
if (entries.length < 2) {
|
|
28
|
-
return {};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const xs = entries.map((e) => e.pos.x);
|
|
32
|
-
const ys = entries.map((e) => e.pos.y);
|
|
33
|
-
const minX = Math.min(...xs);
|
|
34
|
-
const maxX = Math.max(...xs);
|
|
35
|
-
const minY = Math.min(...ys);
|
|
36
|
-
const maxY = Math.max(...ys);
|
|
37
|
-
const centerX = (minX + maxX) / 2;
|
|
38
|
-
const centerY = (minY + maxY) / 2;
|
|
39
|
-
|
|
40
|
-
const sortByX = [...entries].sort((a, b) => a.pos.x - b.pos.x);
|
|
41
|
-
const sortByY = [...entries].sort((a, b) => a.pos.y - b.pos.y);
|
|
42
|
-
|
|
43
|
-
const out: Record<string, { x: number; y: number }> = {};
|
|
44
|
-
|
|
45
|
-
for (const e of entries) {
|
|
46
|
-
const next = { ...e.pos };
|
|
47
|
-
if (command === "left") next.x = minX;
|
|
48
|
-
if (command === "h-center") next.x = centerX;
|
|
49
|
-
if (command === "right") next.x = maxX;
|
|
50
|
-
if (command === "top") next.y = minY;
|
|
51
|
-
if (command === "v-center") next.y = centerY;
|
|
52
|
-
if (command === "bottom") next.y = maxY;
|
|
53
|
-
out[e.id] = next;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (command === "h-gap") {
|
|
57
|
-
const gap = entries.length > 1 ? (maxX - minX) / (entries.length - 1) : 0;
|
|
58
|
-
sortByX.forEach((e, index) => {
|
|
59
|
-
out[e.id] = { ...e.pos, x: minX + gap * index };
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (command === "v-gap") {
|
|
64
|
-
const gap = entries.length > 1 ? (maxY - minY) / (entries.length - 1) : 0;
|
|
65
|
-
sortByY.forEach((e, index) => {
|
|
66
|
-
out[e.id] = { ...e.pos, y: minY + gap * index };
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return out;
|
|
71
|
-
}
|
|
1
|
+
export type AlignCommand =
|
|
2
|
+
| "left"
|
|
3
|
+
| "h-center"
|
|
4
|
+
| "right"
|
|
5
|
+
| "top"
|
|
6
|
+
| "v-center"
|
|
7
|
+
| "bottom"
|
|
8
|
+
| "h-gap"
|
|
9
|
+
| "v-gap";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 멀티 선택된 노드 위치를 bounding box 기준으로 정렬/분배한다.
|
|
13
|
+
* positions는 id -> 좌표 맵이며, 반환값은 동일 키에 갱신된 좌표만 포함한다.
|
|
14
|
+
*/
|
|
15
|
+
export function alignNodePositions(
|
|
16
|
+
selectedIds: string[],
|
|
17
|
+
positions: Record<string, { x: number; y: number }>,
|
|
18
|
+
command: AlignCommand
|
|
19
|
+
): Record<string, { x: number; y: number }> {
|
|
20
|
+
const entries = selectedIds
|
|
21
|
+
.map((id) => {
|
|
22
|
+
const pos = positions[id];
|
|
23
|
+
return pos ? { id, pos: { ...pos } } : null;
|
|
24
|
+
})
|
|
25
|
+
.filter((e): e is { id: string; pos: { x: number; y: number } } => Boolean(e));
|
|
26
|
+
|
|
27
|
+
if (entries.length < 2) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const xs = entries.map((e) => e.pos.x);
|
|
32
|
+
const ys = entries.map((e) => e.pos.y);
|
|
33
|
+
const minX = Math.min(...xs);
|
|
34
|
+
const maxX = Math.max(...xs);
|
|
35
|
+
const minY = Math.min(...ys);
|
|
36
|
+
const maxY = Math.max(...ys);
|
|
37
|
+
const centerX = (minX + maxX) / 2;
|
|
38
|
+
const centerY = (minY + maxY) / 2;
|
|
39
|
+
|
|
40
|
+
const sortByX = [...entries].sort((a, b) => a.pos.x - b.pos.x);
|
|
41
|
+
const sortByY = [...entries].sort((a, b) => a.pos.y - b.pos.y);
|
|
42
|
+
|
|
43
|
+
const out: Record<string, { x: number; y: number }> = {};
|
|
44
|
+
|
|
45
|
+
for (const e of entries) {
|
|
46
|
+
const next = { ...e.pos };
|
|
47
|
+
if (command === "left") next.x = minX;
|
|
48
|
+
if (command === "h-center") next.x = centerX;
|
|
49
|
+
if (command === "right") next.x = maxX;
|
|
50
|
+
if (command === "top") next.y = minY;
|
|
51
|
+
if (command === "v-center") next.y = centerY;
|
|
52
|
+
if (command === "bottom") next.y = maxY;
|
|
53
|
+
out[e.id] = next;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (command === "h-gap") {
|
|
57
|
+
const gap = entries.length > 1 ? (maxX - minX) / (entries.length - 1) : 0;
|
|
58
|
+
sortByX.forEach((e, index) => {
|
|
59
|
+
out[e.id] = { ...e.pos, x: minX + gap * index };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (command === "v-gap") {
|
|
64
|
+
const gap = entries.length > 1 ? (maxY - minY) / (entries.length - 1) : 0;
|
|
65
|
+
sortByY.forEach((e, index) => {
|
|
66
|
+
out[e.id] = { ...e.pos, y: minY + gap * index };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return out;
|
|
71
|
+
}
|