@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/README.md
CHANGED
|
@@ -1,67 +1,134 @@
|
|
|
1
|
-
# @rdbms-erd/core
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- `
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
1
|
+
# @rdbms-erd/core
|
|
2
|
+
|
|
3
|
+
`@rdbms-erd/core` is the headless model/DDL package for rdbms-erd.
|
|
4
|
+
It provides document types, validation/serialization, logical->physical type defaults,
|
|
5
|
+
DDL generation, diagnostics, and host-extensible database metadata.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @rdbms-erd/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createEmptyDesign, createColumn, generateDdl } from "@rdbms-erd/core";
|
|
17
|
+
|
|
18
|
+
const doc = createEmptyDesign("postgres");
|
|
19
|
+
doc.model.tables.push({
|
|
20
|
+
id: "table-users",
|
|
21
|
+
logicalName: "Users",
|
|
22
|
+
physicalName: "users",
|
|
23
|
+
columns: [
|
|
24
|
+
createColumn("postgres", {
|
|
25
|
+
id: "col-users-id",
|
|
26
|
+
logicalName: "ID",
|
|
27
|
+
physicalName: "id",
|
|
28
|
+
logicalType: "NUMBER",
|
|
29
|
+
nullable: false,
|
|
30
|
+
isPrimaryKey: true,
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
console.log(generateDdl(doc));
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Core Concepts
|
|
39
|
+
|
|
40
|
+
- `DesignDocument`: ER JSON source of truth
|
|
41
|
+
- `RdbmsDialect`: dialect id stored in document
|
|
42
|
+
- `LogicalDataType`: normalized logical type ids
|
|
43
|
+
- `DialectMetaJson`: host-overridable per-dialect metadata
|
|
44
|
+
- `DdlGeneratorHook`: optional per-dialect SQL generation hook
|
|
45
|
+
|
|
46
|
+
## Relationship Model Notes
|
|
47
|
+
|
|
48
|
+
`RelationshipModel` supports canvas-specific rendering state used by the designer:
|
|
49
|
+
|
|
50
|
+
- `cardinality?: "1:1" | "1:N"`
|
|
51
|
+
- `canvasLineHidden?: boolean`
|
|
52
|
+
- `linePivotRatio?: number` (middle vertical segment ratio)
|
|
53
|
+
- `sourceLineY?: number` (source edge absolute Y inside table)
|
|
54
|
+
- `sourceLineRatio?: number` (legacy fallback; kept for compatibility)
|
|
55
|
+
|
|
56
|
+
## Host-Extensible DB Metadata
|
|
57
|
+
|
|
58
|
+
### 1) Override/append dialect metadata with JSON
|
|
59
|
+
|
|
60
|
+
Use `hostMetas` in options. Merge policy:
|
|
61
|
+
|
|
62
|
+
- same `id`: override builtin
|
|
63
|
+
- new `id`: append
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { resolveDialectMetas } from "@rdbms-erd/core";
|
|
67
|
+
|
|
68
|
+
const metas = resolveDialectMetas({
|
|
69
|
+
hostMetas: [
|
|
70
|
+
{
|
|
71
|
+
id: "acme",
|
|
72
|
+
label: "AcmeDB",
|
|
73
|
+
supportsSchema: true,
|
|
74
|
+
logicalTypes: [
|
|
75
|
+
{ id: "TEXT", defaultPhysicalType: "STRING(255)" },
|
|
76
|
+
{ id: "NUMBER", defaultPhysicalType: "INT64" },
|
|
77
|
+
],
|
|
78
|
+
ddlStyle: { quote: "double", boolLiteral: "oneZero" },
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2) Optional per-dialect DDL function hook
|
|
85
|
+
|
|
86
|
+
If a dialect DDL is complex, provide a function in `hostDdlGenerators`.
|
|
87
|
+
If omitted, style-based builtin SQL generation is used.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import {
|
|
91
|
+
generateDdlForSelection,
|
|
92
|
+
type DdlGenerateInput,
|
|
93
|
+
} from "@rdbms-erd/core";
|
|
94
|
+
|
|
95
|
+
const sql = generateDdlForSelection(doc, ["table-users"], {
|
|
96
|
+
hostDdlGenerators: {
|
|
97
|
+
acme: (input: DdlGenerateInput) => {
|
|
98
|
+
// input.scope.kind: "all" | "selected"
|
|
99
|
+
return { sql: "-- custom acme ddl" };
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
fallbackOnHookError: true,
|
|
103
|
+
}).sql;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Main APIs
|
|
107
|
+
|
|
108
|
+
- Document lifecycle:
|
|
109
|
+
- `createEmptyDesign`
|
|
110
|
+
- `serializeDesign`, `parseDesign`, `validateDesignDocument`, `roundTripDesign`
|
|
111
|
+
- Type defaults:
|
|
112
|
+
- `defaultPhysicalType`
|
|
113
|
+
- `createColumn`
|
|
114
|
+
- `applyLogicalTypeChange`
|
|
115
|
+
- `convertDesignDialect`
|
|
116
|
+
- DDL:
|
|
117
|
+
- `generateDdl`
|
|
118
|
+
- `generateDdlForSelection`
|
|
119
|
+
- `generateIndexDdl`
|
|
120
|
+
- `generateDdlWithDiagnostics`
|
|
121
|
+
- `generateIndexDdlWithDiagnostics`
|
|
122
|
+
- Metadata:
|
|
123
|
+
- `resolveDialectMetas`
|
|
124
|
+
- `mergeDialectMetas`
|
|
125
|
+
- `createDefaultDbMetaAdapter` (legacy/advanced adapter path)
|
|
126
|
+
|
|
127
|
+
## Notes
|
|
128
|
+
|
|
129
|
+
- DDL function hooks are runtime values and are **not serialized** in `DesignDocument`.
|
|
130
|
+
- `DesignDocument` stores only dialect id and model data.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@rdbms-erd/core",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"license": "MIT",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@rdbms-erd/core",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"erd",
|
|
7
|
+
"database",
|
|
8
|
+
"schema",
|
|
9
|
+
"ddl",
|
|
10
|
+
"sql",
|
|
11
|
+
"rdbms"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./src/index.ts",
|
|
15
|
+
"types": "./src/index.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./src/index.ts"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { alignNodePositions } from "../alignment";
|
|
3
|
-
|
|
4
|
-
describe("alignNodePositions", () => {
|
|
5
|
-
it("aligns left to min x", () => {
|
|
6
|
-
const positions = { a: { x: 10, y: 0 }, b: { x: 50, y: 0 } };
|
|
7
|
-
const out = alignNodePositions(["a", "b"], positions, "left");
|
|
8
|
-
expect(out.a.x).toBe(10);
|
|
9
|
-
expect(out.b.x).toBe(10);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("distributes horizontal gap", () => {
|
|
13
|
-
const positions = { a: { x: 0, y: 0 }, b: { x: 100, y: 0 }, c: { x: 200, y: 0 } };
|
|
14
|
-
const out = alignNodePositions(["a", "b", "c"], positions, "h-gap");
|
|
15
|
-
expect(out.a.x).toBe(0);
|
|
16
|
-
expect(out.b.x).toBe(100);
|
|
17
|
-
expect(out.c.x).toBe(200);
|
|
18
|
-
});
|
|
19
|
-
});
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { alignNodePositions } from "../alignment";
|
|
3
|
+
|
|
4
|
+
describe("alignNodePositions", () => {
|
|
5
|
+
it("aligns left to min x", () => {
|
|
6
|
+
const positions = { a: { x: 10, y: 0 }, b: { x: 50, y: 0 } };
|
|
7
|
+
const out = alignNodePositions(["a", "b"], positions, "left");
|
|
8
|
+
expect(out.a.x).toBe(10);
|
|
9
|
+
expect(out.b.x).toBe(10);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("distributes horizontal gap", () => {
|
|
13
|
+
const positions = { a: { x: 0, y: 0 }, b: { x: 100, y: 0 }, c: { x: 200, y: 0 } };
|
|
14
|
+
const out = alignNodePositions(["a", "b", "c"], positions, "h-gap");
|
|
15
|
+
expect(out.a.x).toBe(0);
|
|
16
|
+
expect(out.b.x).toBe(100);
|
|
17
|
+
expect(out.c.x).toBe(200);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { createLargeDesign, roundTripDesign, serializeDesign } from "../index";
|
|
3
|
-
|
|
4
|
-
describe("large model (500 tables)", () => {
|
|
5
|
-
it("serializes and round-trips within reasonable time", () => {
|
|
6
|
-
const doc = createLargeDesign(500, "postgres");
|
|
7
|
-
const t0 = performance.now();
|
|
8
|
-
const json = serializeDesign(doc);
|
|
9
|
-
const parsed = roundTripDesign(JSON.parse(json) as typeof doc);
|
|
10
|
-
const ms = performance.now() - t0;
|
|
11
|
-
expect(parsed.model.tables).toHaveLength(500);
|
|
12
|
-
expect(ms).toBeLessThan(8000);
|
|
13
|
-
});
|
|
14
|
-
});
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createLargeDesign, roundTripDesign, serializeDesign } from "../index";
|
|
3
|
+
|
|
4
|
+
describe("large model (500 tables)", () => {
|
|
5
|
+
it("serializes and round-trips within reasonable time", () => {
|
|
6
|
+
const doc = createLargeDesign(500, "postgres");
|
|
7
|
+
const t0 = performance.now();
|
|
8
|
+
const json = serializeDesign(doc);
|
|
9
|
+
const parsed = roundTripDesign(JSON.parse(json) as typeof doc);
|
|
10
|
+
const ms = performance.now() - t0;
|
|
11
|
+
expect(parsed.model.tables).toHaveLength(500);
|
|
12
|
+
expect(ms).toBeLessThan(8000);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createColumn,
|
|
4
|
+
createDefaultDbMetaAdapter,
|
|
5
|
+
createEmptyDesign,
|
|
6
|
+
generateDdl,
|
|
7
|
+
generateDdlForSelection,
|
|
8
|
+
LOGICAL_DATA_TYPES,
|
|
9
|
+
resolveDialectMetas,
|
|
10
|
+
serializeDesign,
|
|
11
|
+
validateDesignDocument,
|
|
12
|
+
type DdlGenerateInput,
|
|
13
|
+
type DdlGenerateOutput,
|
|
14
|
+
type DbMetaAdapter,
|
|
15
|
+
type DialectMetaJson,
|
|
16
|
+
type DialectMeta,
|
|
17
|
+
type LogicalDataType
|
|
18
|
+
} from "../index";
|
|
19
|
+
|
|
20
|
+
function createAcmeAdapter(): DbMetaAdapter {
|
|
21
|
+
const base = createDefaultDbMetaAdapter();
|
|
22
|
+
const acmeTypeMap: Record<LogicalDataType, string> = {
|
|
23
|
+
TEXT: "STRING(255)",
|
|
24
|
+
DATE: "DATE",
|
|
25
|
+
TIME: "TIME",
|
|
26
|
+
DATETIME: "TIMESTAMP",
|
|
27
|
+
NUMBER: "INT64",
|
|
28
|
+
DECIMAL: "DECIMAL(10,2)",
|
|
29
|
+
FLOAT: "DOUBLE",
|
|
30
|
+
BOOLEAN: "BOOL",
|
|
31
|
+
JSON: "JSON",
|
|
32
|
+
UUID: "UUID",
|
|
33
|
+
BINARY: "BYTES"
|
|
34
|
+
};
|
|
35
|
+
const acmeMeta: DialectMeta = {
|
|
36
|
+
id: "acme",
|
|
37
|
+
label: "AcmeDB",
|
|
38
|
+
capabilities: { supportsSchema: true },
|
|
39
|
+
logicalTypes: LOGICAL_DATA_TYPES,
|
|
40
|
+
defaultPhysicalTypeMap: acmeTypeMap
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
listDialects: () => [...base.listDialects(), acmeMeta],
|
|
44
|
+
getDialectMeta: (dialect) => (dialect === "acme" ? acmeMeta : base.getDialectMeta(dialect)),
|
|
45
|
+
getDefaultPhysicalType: (dialect, logicalType) =>
|
|
46
|
+
dialect === "acme" ? acmeTypeMap[logicalType] : base.getDefaultPhysicalType(dialect, logicalType),
|
|
47
|
+
getDdlRules: (dialect) =>
|
|
48
|
+
dialect === "acme"
|
|
49
|
+
? {
|
|
50
|
+
...base.getDdlRules(dialect),
|
|
51
|
+
quoteIdentifier: (_d, id) => `<${id}>`
|
|
52
|
+
}
|
|
53
|
+
: base.getDdlRules(dialect)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("db meta adapter", () => {
|
|
58
|
+
it("allows host dialect for createColumn default physical type", () => {
|
|
59
|
+
const adapter = createAcmeAdapter();
|
|
60
|
+
const col = createColumn(
|
|
61
|
+
"acme",
|
|
62
|
+
{
|
|
63
|
+
id: "c1",
|
|
64
|
+
logicalName: "name",
|
|
65
|
+
logicalType: "TEXT"
|
|
66
|
+
},
|
|
67
|
+
{ dbMetaAdapter: adapter }
|
|
68
|
+
);
|
|
69
|
+
expect(col.physicalType).toBe("STRING(255)");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("applies host quote rule and schema support to DDL", () => {
|
|
73
|
+
const adapter = createAcmeAdapter();
|
|
74
|
+
const doc = createEmptyDesign("acme");
|
|
75
|
+
doc.model.tables.push({
|
|
76
|
+
id: "t1",
|
|
77
|
+
logicalName: "사용자",
|
|
78
|
+
physicalName: "users",
|
|
79
|
+
schemaName: "core",
|
|
80
|
+
columns: [
|
|
81
|
+
createColumn(
|
|
82
|
+
"acme",
|
|
83
|
+
{
|
|
84
|
+
id: "id1",
|
|
85
|
+
logicalName: "id",
|
|
86
|
+
physicalName: "id",
|
|
87
|
+
logicalType: "NUMBER",
|
|
88
|
+
nullable: false,
|
|
89
|
+
isPrimaryKey: true
|
|
90
|
+
},
|
|
91
|
+
{ dbMetaAdapter: adapter }
|
|
92
|
+
)
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const sql = generateDdl(doc, { dbMetaAdapter: adapter });
|
|
97
|
+
expect(sql).toContain("CREATE TABLE <core>.<users>");
|
|
98
|
+
expect(sql).toContain("<id> INT64");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("validates custom dialect when adapter provides it", () => {
|
|
102
|
+
const adapter = createAcmeAdapter();
|
|
103
|
+
const doc = createEmptyDesign("acme");
|
|
104
|
+
const raw = JSON.parse(serializeDesign(doc));
|
|
105
|
+
expect(() => validateDesignDocument(raw, { dbMetaAdapter: adapter })).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("merges host metas by id (override/append)", () => {
|
|
109
|
+
const hostMetas: DialectMetaJson[] = [
|
|
110
|
+
{
|
|
111
|
+
id: "postgres",
|
|
112
|
+
label: "Postgres Override",
|
|
113
|
+
supportsSchema: true,
|
|
114
|
+
logicalTypes: [{ id: "TEXT", defaultPhysicalType: "TEXT" }]
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "acme_json",
|
|
118
|
+
label: "Acme JSON",
|
|
119
|
+
supportsSchema: true,
|
|
120
|
+
logicalTypes: [{ id: "TEXT", defaultPhysicalType: "STRING(255)" }]
|
|
121
|
+
}
|
|
122
|
+
];
|
|
123
|
+
const resolved = resolveDialectMetas({ hostMetas });
|
|
124
|
+
expect(resolved.find((m) => m.id === "postgres")?.label).toBe("Postgres Override");
|
|
125
|
+
expect(resolved.some((m) => m.id === "acme_json")).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("calls host ddl generator with selected scope", () => {
|
|
129
|
+
const doc = createEmptyDesign("postgres");
|
|
130
|
+
doc.model.tables.push(
|
|
131
|
+
{ id: "t1", logicalName: "A", physicalName: "a", columns: [createColumn("postgres", { id: "c1", logicalName: "id", logicalType: "NUMBER", nullable: false, isPrimaryKey: true })] },
|
|
132
|
+
{ id: "t2", logicalName: "B", physicalName: "b", columns: [createColumn("postgres", { id: "c2", logicalName: "id", logicalType: "NUMBER", nullable: false, isPrimaryKey: true })] }
|
|
133
|
+
);
|
|
134
|
+
const calls: DdlGenerateInput[] = [];
|
|
135
|
+
const hostGenerator = (input: DdlGenerateInput): DdlGenerateOutput => {
|
|
136
|
+
calls.push(input);
|
|
137
|
+
return { sql: "-- custom" };
|
|
138
|
+
};
|
|
139
|
+
const out = generateDdlForSelection(doc, ["t1"], {
|
|
140
|
+
hostDdlGenerators: { postgres: hostGenerator }
|
|
141
|
+
});
|
|
142
|
+
expect(out.sql).toBe("-- custom");
|
|
143
|
+
expect(calls).toHaveLength(1);
|
|
144
|
+
expect(calls[0].scope.kind).toBe("selected");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("falls back to style generator when host hook throws", () => {
|
|
148
|
+
const doc = createEmptyDesign("postgres");
|
|
149
|
+
doc.model.tables.push({
|
|
150
|
+
id: "t1",
|
|
151
|
+
logicalName: "사용자",
|
|
152
|
+
physicalName: "users",
|
|
153
|
+
columns: [createColumn("postgres", { id: "c1", logicalName: "id", logicalType: "NUMBER", nullable: false, isPrimaryKey: true })]
|
|
154
|
+
});
|
|
155
|
+
const sql = generateDdl(doc, {
|
|
156
|
+
hostDdlGenerators: {
|
|
157
|
+
postgres: () => {
|
|
158
|
+
throw new Error("boom");
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
fallbackOnHookError: true
|
|
162
|
+
});
|
|
163
|
+
expect(sql).toContain("CREATE TABLE");
|
|
164
|
+
expect(sql).toContain("\"users\"");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { analyzeDdlDocument, createColumn, createEmptyDesign, formatDdlDiagnostic, generateDdlWithDiagnostics } from "../index";
|
|
3
|
-
|
|
4
|
-
describe("analyzeDdlDocument / formatDdlDiagnostic", () => {
|
|
5
|
-
it("formats diagnostics with unified template", () => {
|
|
6
|
-
const line = formatDdlDiagnostic({
|
|
7
|
-
severity: "warning",
|
|
8
|
-
code: "DDL_REL_MISSING_COLUMNS",
|
|
9
|
-
message: "소스/타겟 컬럼이 지정되지 않아 FK DDL을 생략한다.",
|
|
10
|
-
context: "relationship:r1"
|
|
11
|
-
});
|
|
12
|
-
expect(line).toBe(
|
|
13
|
-
"[WARN][DDL_REL_MISSING_COLUMNS] 소스/타겟 컬럼이 지정되지 않아 FK DDL을 생략한다. | relationship:r1"
|
|
14
|
-
);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("warns on relationship without column ids", () => {
|
|
18
|
-
const doc = createEmptyDesign("postgres");
|
|
19
|
-
doc.model.tables.push({
|
|
20
|
-
id: "a",
|
|
21
|
-
logicalName: "A",
|
|
22
|
-
physicalName: "TA",
|
|
23
|
-
columns: [createColumn("postgres", { id: "c1", logicalName: "x", logicalType: "TEXT" })]
|
|
24
|
-
});
|
|
25
|
-
doc.model.tables.push({
|
|
26
|
-
id: "b",
|
|
27
|
-
logicalName: "B",
|
|
28
|
-
physicalName: "TB",
|
|
29
|
-
columns: [createColumn("postgres", { id: "c2", logicalName: "y", logicalType: "TEXT" })]
|
|
30
|
-
});
|
|
31
|
-
doc.model.relationships.push({
|
|
32
|
-
id: "r1",
|
|
33
|
-
sourceTableId: "a",
|
|
34
|
-
targetTableId: "b"
|
|
35
|
-
});
|
|
36
|
-
const d = analyzeDdlDocument(doc);
|
|
37
|
-
expect(d.some((x) => x.code === "DDL_REL_MISSING_COLUMNS")).toBe(true);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("generateDdlWithDiagnostics returns sql and same analyze list", () => {
|
|
41
|
-
const doc = createEmptyDesign("mysql");
|
|
42
|
-
doc.model.tables.push({
|
|
43
|
-
id: "t1",
|
|
44
|
-
logicalName: "T",
|
|
45
|
-
physicalName: "TT",
|
|
46
|
-
columns: [createColumn("mysql", { id: "c1", logicalName: "id", logicalType: "NUMBER", nullable: false })]
|
|
47
|
-
});
|
|
48
|
-
const { sql, diagnostics } = generateDdlWithDiagnostics(doc);
|
|
49
|
-
expect(sql).toContain("CREATE TABLE");
|
|
50
|
-
expect(diagnostics.length).toBeGreaterThanOrEqual(0);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { analyzeDdlDocument, createColumn, createEmptyDesign, formatDdlDiagnostic, generateDdlWithDiagnostics } from "../index";
|
|
3
|
+
|
|
4
|
+
describe("analyzeDdlDocument / formatDdlDiagnostic", () => {
|
|
5
|
+
it("formats diagnostics with unified template", () => {
|
|
6
|
+
const line = formatDdlDiagnostic({
|
|
7
|
+
severity: "warning",
|
|
8
|
+
code: "DDL_REL_MISSING_COLUMNS",
|
|
9
|
+
message: "소스/타겟 컬럼이 지정되지 않아 FK DDL을 생략한다.",
|
|
10
|
+
context: "relationship:r1"
|
|
11
|
+
});
|
|
12
|
+
expect(line).toBe(
|
|
13
|
+
"[WARN][DDL_REL_MISSING_COLUMNS] 소스/타겟 컬럼이 지정되지 않아 FK DDL을 생략한다. | relationship:r1"
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("warns on relationship without column ids", () => {
|
|
18
|
+
const doc = createEmptyDesign("postgres");
|
|
19
|
+
doc.model.tables.push({
|
|
20
|
+
id: "a",
|
|
21
|
+
logicalName: "A",
|
|
22
|
+
physicalName: "TA",
|
|
23
|
+
columns: [createColumn("postgres", { id: "c1", logicalName: "x", logicalType: "TEXT" })]
|
|
24
|
+
});
|
|
25
|
+
doc.model.tables.push({
|
|
26
|
+
id: "b",
|
|
27
|
+
logicalName: "B",
|
|
28
|
+
physicalName: "TB",
|
|
29
|
+
columns: [createColumn("postgres", { id: "c2", logicalName: "y", logicalType: "TEXT" })]
|
|
30
|
+
});
|
|
31
|
+
doc.model.relationships.push({
|
|
32
|
+
id: "r1",
|
|
33
|
+
sourceTableId: "a",
|
|
34
|
+
targetTableId: "b"
|
|
35
|
+
});
|
|
36
|
+
const d = analyzeDdlDocument(doc);
|
|
37
|
+
expect(d.some((x) => x.code === "DDL_REL_MISSING_COLUMNS")).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("generateDdlWithDiagnostics returns sql and same analyze list", () => {
|
|
41
|
+
const doc = createEmptyDesign("mysql");
|
|
42
|
+
doc.model.tables.push({
|
|
43
|
+
id: "t1",
|
|
44
|
+
logicalName: "T",
|
|
45
|
+
physicalName: "TT",
|
|
46
|
+
columns: [createColumn("mysql", { id: "c1", logicalName: "id", logicalType: "NUMBER", nullable: false })]
|
|
47
|
+
});
|
|
48
|
+
const { sql, diagnostics } = generateDdlWithDiagnostics(doc);
|
|
49
|
+
expect(sql).toContain("CREATE TABLE");
|
|
50
|
+
expect(diagnostics.length).toBeGreaterThanOrEqual(0);
|
|
51
|
+
});
|
|
52
|
+
});
|