duckerd 0.1.4 → 0.2.1
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 +2 -0
- package/dist/commands/erd.js +12 -8
- package/dist/index.js +6 -1
- package/dist/lib/metadata.js +73 -16
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -27,9 +27,11 @@ Generate an ERD diagram of the database schemas.
|
|
|
27
27
|
- `-d, --database <path>`: Path to the database file
|
|
28
28
|
- `-t, --theme [theme]`: Theme of the chart (choices: `default`, `forest`, `dark`, `neutral`, default: `default`)
|
|
29
29
|
- `-o, --output <path>`: Path to the output file
|
|
30
|
+
- `-m, --mmd-output <path>`: Path to write the Mermaid source (`.mmd`) file (default: cleaned up after rendering)
|
|
30
31
|
- `-w, --width [width]`: Width of the page (default: `1024`)
|
|
31
32
|
- `-H, --height [height]`: Height of the page (default: `768`)
|
|
32
33
|
- `-f, --outputFormat [format]`: Output format for the generated image (choices: `svg`, `png`, `pdf`, default: `png`)
|
|
34
|
+
- `-e, --expand-structs`: Expand `STRUCT` columns into individual sub-field rows (e.g. `full_name STRUCT(given VARCHAR, family VARCHAR)` becomes `full_name__given` and `full_name__family`)
|
|
33
35
|
|
|
34
36
|
#### Example:
|
|
35
37
|
|
package/dist/commands/erd.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createERD = void 0;
|
|
4
|
-
const
|
|
4
|
+
const node_api_1 = require("@duckdb/node-api");
|
|
5
5
|
const metadata_1 = require("../lib/metadata");
|
|
6
|
-
const createERD = async (databasePath) => {
|
|
7
|
-
const
|
|
8
|
-
const conn = await
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
const createERD = async (databasePath, expandStructs = false) => {
|
|
7
|
+
const instance = await node_api_1.DuckDBInstance.create(databasePath);
|
|
8
|
+
const conn = await instance.connect();
|
|
9
|
+
try {
|
|
10
|
+
const metadata = await (0, metadata_1.getMetadata)(conn);
|
|
11
|
+
return (0, metadata_1.generateMermaidCodeForAllDBs)(metadata, expandStructs);
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
conn.closeSync();
|
|
15
|
+
instance.closeSync();
|
|
16
|
+
}
|
|
13
17
|
};
|
|
14
18
|
exports.createERD = createERD;
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,8 @@ program
|
|
|
47
47
|
.option('-d, --database <path>', 'Path to the database file')
|
|
48
48
|
.option('-t, --theme [theme]', 'Theme of the chart (choices: "default", "forest", "dark", "neutral", default: "default")')
|
|
49
49
|
.option('-o, --output <path>', 'Path to the output file')
|
|
50
|
+
.option('-m, --mmd-output <path>', 'Path to write the Mermaid source (.mmd) file (default: cleaned up after rendering)')
|
|
51
|
+
.option('-e, --expand-structs', 'Expand STRUCT type columns into individual sub-field rows')
|
|
50
52
|
.option('-w, --width [width]', 'Width of the page (default: 1024)')
|
|
51
53
|
.option('-H, --height [height]', 'Height of the page (default: 768)')
|
|
52
54
|
.option('-f, --outputFormat [format]', 'Output format for the generated image. (choices: "svg", "png", "pdf")')
|
|
@@ -61,10 +63,13 @@ program
|
|
|
61
63
|
run(dbPath, options);
|
|
62
64
|
});
|
|
63
65
|
const run = async (dbPath, options) => {
|
|
64
|
-
const mermaidDiagram = await (0, erd_1.createERD)(dbPath);
|
|
66
|
+
const mermaidDiagram = await (0, erd_1.createERD)(dbPath, options.expandStructs ?? false);
|
|
65
67
|
const mermaidFile = path.resolve('schema_erd.mmd');
|
|
66
68
|
if (mermaidDiagram) {
|
|
67
69
|
fs.writeFileSync(mermaidFile, mermaidDiagram);
|
|
70
|
+
if (options.mmdOutput) {
|
|
71
|
+
fs.copyFileSync(mermaidFile, path.resolve(options.mmdOutput));
|
|
72
|
+
}
|
|
68
73
|
// Extract the database name from the dbPath
|
|
69
74
|
const dbName = dbPath === ':memory:' ? 'memory_db' : path.basename(dbPath, path.extname(dbPath));
|
|
70
75
|
const outputFile = options.output || `${dbName}_erd.${options.outputFormat || 'png'}`;
|
package/dist/lib/metadata.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.addCommentIfNecessary = exports.sanitizeDataType = exports.generateMermaidCodeForAllDBs = exports.getMetadata = void 0;
|
|
3
|
+
exports.addCommentIfNecessary = exports.sanitizeDataType = exports.expandStructFields = exports.parseStructFields = exports.generateMermaidCodeForAllDBs = exports.getMetadata = void 0;
|
|
4
4
|
// Database metadata query
|
|
5
5
|
const databaseMetadataQuery = `SELECT database_name as databaseName FROM duckdb_databases() WHERE database_name NOT IN ('system', 'temp') ORDER BY database_name`;
|
|
6
6
|
// Schema metadata query
|
|
@@ -15,14 +15,18 @@ const indexesMetadataQuery = `SELECT database_name as databaseName, schema_name
|
|
|
15
15
|
const contraintsMetadataQuery = `SELECT database_name as databaseName, schema_name as schemaName, table_name as tableName, unnest(constraint_column_names) as columnName, constraint_type as constraintType, constraint_text as sql FROM duckdb_constraints() ORDER BY database_name, schema_name, table_name`;
|
|
16
16
|
// Constraints metadata query
|
|
17
17
|
const sequencesMetadataQuery = `SELECT database_name as databaseName, schema_name as schemaName, sequence_name as name, temporary as isTemporary, start_value as startValue, last_value as lastValue, min_value as minValue, max_value as maxValue, increment_by as incrementBy, sql FROM duckdb_sequences() ORDER BY database_name, schema_name, sequence_name`;
|
|
18
|
+
const queryAll = async (conn, sql) => {
|
|
19
|
+
const reader = await conn.runAndReadAll(sql);
|
|
20
|
+
return reader.getRowObjectsJson();
|
|
21
|
+
};
|
|
18
22
|
const getMetadata = async (conn) => {
|
|
19
|
-
const databaseRaw = await conn
|
|
20
|
-
const schemas = await conn
|
|
21
|
-
const tables = await conn
|
|
22
|
-
const columns = await conn
|
|
23
|
-
const indexes = await conn
|
|
24
|
-
const constraints = await conn
|
|
25
|
-
const sequences = await conn
|
|
23
|
+
const databaseRaw = await queryAll(conn, databaseMetadataQuery);
|
|
24
|
+
const schemas = await queryAll(conn, schemaMetadataQuery);
|
|
25
|
+
const tables = await queryAll(conn, tablesMetadataQuery);
|
|
26
|
+
const columns = await queryAll(conn, columnsMetadataQuery);
|
|
27
|
+
const indexes = await queryAll(conn, indexesMetadataQuery);
|
|
28
|
+
const constraints = await queryAll(conn, contraintsMetadataQuery);
|
|
29
|
+
const sequences = await queryAll(conn, sequencesMetadataQuery);
|
|
26
30
|
const databases = databaseRaw.map(database => ({
|
|
27
31
|
name: database.databaseName,
|
|
28
32
|
schemas: [...new Set(schemas.filter(schema => database.databaseName === schema.databaseName).map(schema => ({
|
|
@@ -73,7 +77,7 @@ const getMetadata = async (conn) => {
|
|
|
73
77
|
return metadata;
|
|
74
78
|
};
|
|
75
79
|
exports.getMetadata = getMetadata;
|
|
76
|
-
const generateMermaidCodeForAllDBs = (metadata) => {
|
|
80
|
+
const generateMermaidCodeForAllDBs = (metadata, expandStructs = false) => {
|
|
77
81
|
let mermaidCode = `erDiagram
|
|
78
82
|
|
|
79
83
|
`;
|
|
@@ -84,7 +88,14 @@ const generateMermaidCodeForAllDBs = (metadata) => {
|
|
|
84
88
|
// Add tables, columns and constraints
|
|
85
89
|
mermaidCode += tables.map((table) => {
|
|
86
90
|
return `"${table.databaseName}.${table.name}" {
|
|
87
|
-
${table.columns.
|
|
91
|
+
${table.columns.flatMap((column) => {
|
|
92
|
+
const constraintMarkers = table.constraints?.filter((constraint) => constraint.columnName === column.name).map((constraint) => ((constraint.constraintType === "PRIMARY KEY" ? "PK" : "") || (constraint.constraintType === "FOREIGN KEY" ? "FK" : "") || "")).filter(str => str) ?? [];
|
|
93
|
+
const structFields = expandStructs ? (0, exports.expandStructFields)(column.name, column.dataType) : [];
|
|
94
|
+
if (structFields.length > 0) {
|
|
95
|
+
return structFields.map((field) => ` ${(0, exports.sanitizeDataType)(field.type.toUpperCase())} ${field.name}`);
|
|
96
|
+
}
|
|
97
|
+
return [` ${(0, exports.sanitizeDataType)(column.dataType.toUpperCase())} ${column.name} ${constraintMarkers}${(0, exports.addCommentIfNecessary)(column.dataType)}`];
|
|
98
|
+
}).join("\n")}
|
|
88
99
|
}\n${table.constraints?.filter((constraint) => constraint.constraintType === "FOREIGN KEY").map((constraint) => ` "${table.databaseName}.${constraint.sql.match(/(?:^|)REFERENCES\s([^*]+?)\b\(/i)[1]}" ||--o{ "${table.databaseName}.${table.name}" : has`).join("\n")}`;
|
|
89
100
|
}).join("\n ");
|
|
90
101
|
return mermaidCode;
|
|
@@ -98,15 +109,61 @@ ${table.columns.map((column) => ` ${(0, exports.sanitizeDataType)(column.da
|
|
|
98
109
|
}
|
|
99
110
|
};
|
|
100
111
|
exports.generateMermaidCodeForAllDBs = generateMermaidCodeForAllDBs;
|
|
101
|
-
const
|
|
102
|
-
if (dataType.startsWith("STRUCT(")) {
|
|
103
|
-
return
|
|
112
|
+
const parseStructFields = (dataType) => {
|
|
113
|
+
if (!dataType.toUpperCase().startsWith("STRUCT(")) {
|
|
114
|
+
return [];
|
|
104
115
|
}
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
const inner = dataType.slice(dataType.indexOf("(") + 1, dataType.lastIndexOf(")"));
|
|
117
|
+
const fields = [];
|
|
118
|
+
let depth = 0;
|
|
119
|
+
let current = "";
|
|
120
|
+
for (const ch of inner) {
|
|
121
|
+
if (ch === "(" || ch === "<")
|
|
122
|
+
depth++;
|
|
123
|
+
else if (ch === ")" || ch === ">")
|
|
124
|
+
depth--;
|
|
125
|
+
else if (ch === "," && depth === 0) {
|
|
126
|
+
const parts = current.trim().split(/\s+/);
|
|
127
|
+
if (parts.length >= 2) {
|
|
128
|
+
fields.push({ name: parts[0].replace(/['"]/g, ""), type: parts.slice(1).join(" ") });
|
|
129
|
+
}
|
|
130
|
+
current = "";
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
current += ch;
|
|
134
|
+
}
|
|
135
|
+
if (current.trim()) {
|
|
136
|
+
const parts = current.trim().split(/\s+/);
|
|
137
|
+
if (parts.length >= 2) {
|
|
138
|
+
fields.push({ name: parts[0].replace(/['"]/g, ""), type: parts.slice(1).join(" ") });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return fields;
|
|
142
|
+
};
|
|
143
|
+
exports.parseStructFields = parseStructFields;
|
|
144
|
+
const expandStructFields = (prefix, dataType) => {
|
|
145
|
+
const fields = (0, exports.parseStructFields)(dataType);
|
|
146
|
+
if (fields.length === 0)
|
|
147
|
+
return [];
|
|
148
|
+
return fields.flatMap((field) => {
|
|
149
|
+
const nestedFields = (0, exports.parseStructFields)(field.type);
|
|
150
|
+
if (nestedFields.length > 0) {
|
|
151
|
+
return (0, exports.expandStructFields)(`${prefix}__${field.name}`, field.type);
|
|
152
|
+
}
|
|
153
|
+
return [{ name: `${prefix}__${field.name}`, type: field.type }];
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
exports.expandStructFields = expandStructFields;
|
|
157
|
+
const COLLAPSIBLE_TYPES = ["STRUCT", "MAP", "ENUM"];
|
|
158
|
+
const sanitizeDataType = (dataType) => {
|
|
159
|
+
const upper = dataType.toUpperCase();
|
|
160
|
+
for (const t of COLLAPSIBLE_TYPES) {
|
|
161
|
+
if (upper.startsWith(`${t}(`)) {
|
|
162
|
+
return t;
|
|
163
|
+
}
|
|
107
164
|
}
|
|
108
165
|
if (dataType.includes(",")) {
|
|
109
|
-
return dataType.replace(
|
|
166
|
+
return dataType.replace(/,/g, "_");
|
|
110
167
|
}
|
|
111
168
|
return dataType;
|
|
112
169
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "duckerd",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A CLI tool for generating ERD diagrams from DuckDB databases",
|
|
5
5
|
"author": "TobiLG <tobilg@gmail.com>",
|
|
6
6
|
"repository": {
|
|
@@ -17,19 +17,20 @@
|
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "rm -rf dist && tsc",
|
|
20
|
-
"start": "ts-node src/index.ts"
|
|
20
|
+
"start": "ts-node src/index.ts",
|
|
21
|
+
"test": "ts-node src/lib/metadata.test.ts"
|
|
21
22
|
},
|
|
22
23
|
"engines": {
|
|
23
|
-
"node": "
|
|
24
|
+
"node": ">=22.0"
|
|
24
25
|
},
|
|
25
26
|
"keywords": ["duckdb", "erd", "diagram", "cli"],
|
|
26
27
|
"dependencies": {
|
|
27
|
-
"
|
|
28
|
-
"
|
|
28
|
+
"@duckdb/node-api": "^1.5.2-r.1",
|
|
29
|
+
"commander": "^14.0.3"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
|
-
"@types/node": "^
|
|
32
|
+
"@types/node": "^22",
|
|
32
33
|
"ts-node": "^10.9.2",
|
|
33
|
-
"typescript": "^
|
|
34
|
+
"typescript": "^6.0.3"
|
|
34
35
|
}
|
|
35
36
|
}
|