duckerd 0.1.0
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 +38 -0
- package/dist/commands/erd.js +14 -0
- package/dist/index.js +78 -0
- package/dist/lib/metadata.js +104 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# DuckERD CLI
|
|
2
|
+
|
|
3
|
+
A CLI tool for generating ERD diagrams from DuckDB databases.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js (v18.19.0 or later)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
To install the CLI tool, run:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
npm install -g duckerd
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
duckerd [options]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Generate an ERD diagram of the database schemas.
|
|
24
|
+
|
|
25
|
+
#### Options:
|
|
26
|
+
|
|
27
|
+
- `-d, --database <path>`: Path to the database file
|
|
28
|
+
- `-t, --theme [theme]`: Theme of the chart (choices: `default`, `forest`, `dark`, `neutral`, default: `default`)
|
|
29
|
+
- `-o, --output [output]`: Path to the output file
|
|
30
|
+
- `-w, --width [width]`: Width of the page (default: `1024`)
|
|
31
|
+
- `-H, --height [height]`: Height of the page (default: `768`)
|
|
32
|
+
- `-f, --outputFormat [format]`: Output format for the generated image (choices: `svg`, `png`, `pdf`, default: `png`)
|
|
33
|
+
|
|
34
|
+
#### Example:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
duckerd -d ./mydb.duckdb -o ./erd.png -f png -t neutral -w 1600
|
|
38
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createERD = void 0;
|
|
4
|
+
const duckdb_async_1 = require("duckdb-async");
|
|
5
|
+
const metadata_1 = require("../lib/metadata");
|
|
6
|
+
const createERD = async (databasePath) => {
|
|
7
|
+
const db = await duckdb_async_1.Database.create(databasePath);
|
|
8
|
+
const conn = await db.connect();
|
|
9
|
+
const metadata = await (0, metadata_1.getMetadata)(conn);
|
|
10
|
+
await db.close();
|
|
11
|
+
const mermaidCode = (0, metadata_1.generateMermaidCodeForAllDBs)(metadata);
|
|
12
|
+
return mermaidCode;
|
|
13
|
+
};
|
|
14
|
+
exports.createERD = createERD;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
20
|
+
if (mod && mod.__esModule) return mod;
|
|
21
|
+
var result = {};
|
|
22
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
23
|
+
__setModuleDefault(result, mod);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
const commander_1 = require("commander");
|
|
28
|
+
const path = __importStar(require("path"));
|
|
29
|
+
const child_process_1 = require("child_process");
|
|
30
|
+
const fs = __importStar(require("fs"));
|
|
31
|
+
const erd_1 = require("./commands/erd");
|
|
32
|
+
const program = new commander_1.Command();
|
|
33
|
+
program
|
|
34
|
+
.version('0.1.0')
|
|
35
|
+
.description('A CLI tool for generating ERD diagrams from DuckDB databases')
|
|
36
|
+
.option('-d, --database <path>', 'Path to the database file')
|
|
37
|
+
.option('-t, --theme [theme]', 'Theme of the chart (choices: "default", "forest", "dark", "neutral", default: "default")')
|
|
38
|
+
.option('-o, --output <path>', 'Path to the output file')
|
|
39
|
+
.option('-w, --width [width]', 'Width of the page (default: 1024)')
|
|
40
|
+
.option('-H, --height [height]', 'Height of the page (default: 768)')
|
|
41
|
+
.option('-f, --outputFormat [format]', 'Output format for the generated image. (choices: "svg", "png", "pdf")')
|
|
42
|
+
.action((options) => {
|
|
43
|
+
const dbPath = options.database ? path.resolve(options.database) : ':memory:';
|
|
44
|
+
console.log('Generating ERD diagram...');
|
|
45
|
+
// Check if the database file exists
|
|
46
|
+
if (dbPath !== ':memory:' && !fs.existsSync(dbPath)) {
|
|
47
|
+
console.error(`Error: Database file not found at ${dbPath}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
runErd(dbPath, options);
|
|
51
|
+
});
|
|
52
|
+
const runErd = async (dbPath, options) => {
|
|
53
|
+
const mermaidDiagram = await (0, erd_1.createERD)(dbPath);
|
|
54
|
+
const mermaidFile = path.resolve('schema_erd.mmd');
|
|
55
|
+
if (mermaidDiagram) {
|
|
56
|
+
fs.writeFileSync(mermaidFile, mermaidDiagram);
|
|
57
|
+
// Extract the database name from the dbPath
|
|
58
|
+
const dbName = dbPath === ':memory:' ? 'memory_db' : path.basename(dbPath, path.extname(dbPath));
|
|
59
|
+
const outputFile = options.output || `${dbName}_erd.${options.outputFormat || 'png'}`;
|
|
60
|
+
const width = options.width || 1024;
|
|
61
|
+
const height = options.height || 768;
|
|
62
|
+
const theme = options.theme || 'default';
|
|
63
|
+
const command = `npx mmdc -i ${mermaidFile} -o ${outputFile} -t ${theme} -w ${width} -H ${height}`;
|
|
64
|
+
(0, child_process_1.exec)(command, (error, stdout, stderr) => {
|
|
65
|
+
if (error) {
|
|
66
|
+
console.error(`Error generating ERD: ${error.message}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (stderr) {
|
|
70
|
+
console.error(`ERD generation stderr: ${stderr}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.log(`ERD diagram generated: ${outputFile}`);
|
|
74
|
+
fs.unlinkSync(mermaidFile);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeDataType = exports.generateMermaidCodeForAllDBs = exports.getMetadata = void 0;
|
|
4
|
+
// Database metadata query
|
|
5
|
+
const databaseMetadataQuery = `SELECT database_name as databaseName FROM duckdb_databases() WHERE database_name NOT IN ('system', 'temp') ORDER BY database_name`;
|
|
6
|
+
// Schema metadata query
|
|
7
|
+
const schemaMetadataQuery = `SELECT database_name as databaseName, schema_name as schemaName FROM duckdb_schemas() WHERE database_name NOT IN ('system', 'temp') and schema_name NOT IN ('information_schema', 'pg_catalog') ORDER BY database_name, schema_name`;
|
|
8
|
+
// Table metadata query
|
|
9
|
+
const tablesMetadataQuery = `SELECT database_name as databaseName, schema_name as schemaName, table_name as name, has_primary_key as hasPrimaryKey, estimated_size as estimatedRowCount, column_count as columnCount, index_count as indexCount, check_constraint_count as checkConstraintCount, sql FROM duckdb_tables() WHERE internal = false ORDER BY database_name, schema_name, table_name`;
|
|
10
|
+
// Column metadata query
|
|
11
|
+
const columnsMetadataQuery = `SELECT database_name as databaseName, schema_name as schemaName, table_name as tableName, column_name as name, data_type as dataType, numeric_precision as precision, numeric_scale as scale, is_nullable as isNullable FROM duckdb_columns() WHERE internal = false ORDER BY database_name, schema_name, table_name, column_index`;
|
|
12
|
+
// Index metadata query
|
|
13
|
+
const indexesMetadataQuery = `SELECT database_name as databaseName, schema_name as schemaName, table_name as tableName, index_name as name, is_unique as isUnique, sql FROM duckdb_indexes() ORDER BY database_name, schema_name, table_name, index_name`;
|
|
14
|
+
// Constraints metadata query
|
|
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
|
+
// Constraints metadata query
|
|
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 getMetadata = async (conn) => {
|
|
19
|
+
const databaseRaw = await conn.all(databaseMetadataQuery);
|
|
20
|
+
const schemas = await conn.all(schemaMetadataQuery);
|
|
21
|
+
const tables = await conn.all(tablesMetadataQuery);
|
|
22
|
+
const columns = await conn.all(columnsMetadataQuery);
|
|
23
|
+
const indexes = await conn.all(indexesMetadataQuery);
|
|
24
|
+
const constraints = await conn.all(contraintsMetadataQuery);
|
|
25
|
+
const sequences = await conn.all(sequencesMetadataQuery);
|
|
26
|
+
const databases = databaseRaw.map(database => ({
|
|
27
|
+
name: database.databaseName,
|
|
28
|
+
schemas: [...new Set(schemas.filter(schema => database.databaseName === schema.databaseName).map(schema => ({
|
|
29
|
+
name: schema.schemaName,
|
|
30
|
+
tables: [...new Set(tables.filter(table => schema.databaseName === table.databaseName && schema.schemaName === table.schemaName).map(table => ({
|
|
31
|
+
databaseName: database.databaseName,
|
|
32
|
+
schemaName: schema.schemaName,
|
|
33
|
+
name: table.name,
|
|
34
|
+
hasPrimaryKey: table.hasPrimaryKey,
|
|
35
|
+
estimatedRowCount: table.estimatedRowCount,
|
|
36
|
+
columnCount: table.columnCount,
|
|
37
|
+
indexCount: table.indexCount,
|
|
38
|
+
checkConstraintCount: table.checkConstraintCount,
|
|
39
|
+
sql: table.sql,
|
|
40
|
+
columns: [...new Set(columns.filter(column => column.databaseName === table.databaseName && column.schemaName === table.schemaName && column.tableName === table.name).map(column => ({
|
|
41
|
+
name: column.name,
|
|
42
|
+
dataType: column.dataType,
|
|
43
|
+
precision: column.precision,
|
|
44
|
+
scale: column.scale,
|
|
45
|
+
isNullable: column.isNullable,
|
|
46
|
+
})))],
|
|
47
|
+
indexes: [...new Set(indexes.filter(index => index.databaseName === table.databaseName && index.schemaName === table.schemaName && index.tableName === table.name).map(index => ({
|
|
48
|
+
name: index.name,
|
|
49
|
+
isUnique: index.isUnique,
|
|
50
|
+
sql: index.sql,
|
|
51
|
+
})))],
|
|
52
|
+
constraints: [...new Set(constraints.filter((constraint) => constraint.databaseName === table.databaseName && constraint.schemaName === table.schemaName && constraint.tableName === table.name).map(constraint => ({
|
|
53
|
+
columnName: constraint.columnName,
|
|
54
|
+
constraintType: constraint.constraintType,
|
|
55
|
+
sql: constraint.sql,
|
|
56
|
+
})))],
|
|
57
|
+
})))],
|
|
58
|
+
sequences: [...new Set(sequences.filter(sequence => sequence.databaseName === schema.databaseName && sequence.schemaName === schema.schemaName).map(sequence => ({
|
|
59
|
+
name: sequence.name,
|
|
60
|
+
isTemporary: sequence.isTemporary,
|
|
61
|
+
startValue: sequence.startValue,
|
|
62
|
+
lastValue: sequence.lastValue,
|
|
63
|
+
minValue: sequence.minValue,
|
|
64
|
+
maxValue: sequence.maxValue,
|
|
65
|
+
incrementBy: sequence.incrementBy,
|
|
66
|
+
sql: sequence.sql,
|
|
67
|
+
})))],
|
|
68
|
+
})))],
|
|
69
|
+
}));
|
|
70
|
+
const metadata = {
|
|
71
|
+
databases,
|
|
72
|
+
};
|
|
73
|
+
return metadata;
|
|
74
|
+
};
|
|
75
|
+
exports.getMetadata = getMetadata;
|
|
76
|
+
const generateMermaidCodeForAllDBs = (metadata) => {
|
|
77
|
+
let mermaidCode = `erDiagram
|
|
78
|
+
|
|
79
|
+
`;
|
|
80
|
+
if (metadata.databases && metadata.databases.length > 0 && metadata.databases[0].schemas && metadata.databases[0].schemas?.length > 0) {
|
|
81
|
+
// Get tables
|
|
82
|
+
const tables = metadata.databases.map((db) => db.schemas.map((s) => s.tables).flat()).flat();
|
|
83
|
+
if (tables && tables.length > 0) {
|
|
84
|
+
// Add tables, columns and constraints
|
|
85
|
+
mermaidCode += tables.map((table) => {
|
|
86
|
+
return `"${table.databaseName}.${table.name}" {
|
|
87
|
+
${table.columns.map((column) => ` ${(0, exports.sanitizeDataType)(column.dataType.toUpperCase())} ${column.name} ${table.constraints?.filter((constraint) => constraint.columnName === column.name).map((constraint) => ((constraint.constraintType === "PRIMARY KEY" ? "PK" : "") || (constraint.constraintType === "FOREIGN KEY" ? "FK" : "") || "")).filter(str => str)}`).join("\n")}
|
|
88
|
+
}\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
|
+
}).join("\n ");
|
|
90
|
+
return mermaidCode;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
exports.generateMermaidCodeForAllDBs = generateMermaidCodeForAllDBs;
|
|
101
|
+
const sanitizeDataType = (dataType) => {
|
|
102
|
+
return dataType.startsWith("STRUCT(") ? "STRUCT" : dataType;
|
|
103
|
+
};
|
|
104
|
+
exports.sanitizeDataType = sanitizeDataType;
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "duckerd",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A CLI tool for generating ERD diagrams from DuckDB databases",
|
|
5
|
+
"author": "TobiLG <tobilg@gmail.com>",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/tobilg/duckerd.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/tobilg/duckerd/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/tobilg/duckerd#readme",
|
|
15
|
+
"bin": {
|
|
16
|
+
"duckerd": "dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"start": "ts-node src/index.ts"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": "^18.19 || >=20.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": ["duckdb", "erd", "diagram", "cli"],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@mermaid-js/mermaid-cli": "^11.1.1",
|
|
28
|
+
"commander": "^12.1.0",
|
|
29
|
+
"duckdb-async": "^1.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^18.11.9",
|
|
33
|
+
"ts-node": "^10.9.1",
|
|
34
|
+
"typescript": "^5.6.2"
|
|
35
|
+
}
|
|
36
|
+
}
|