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 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
 
@@ -1,14 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createERD = void 0;
4
- const duckdb_async_1 = require("duckdb-async");
4
+ const node_api_1 = require("@duckdb/node-api");
5
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;
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'}`;
@@ -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.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);
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.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)}${(0, exports.addCommentIfNecessary)(column.dataType)}`).join("\n")}
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 sanitizeDataType = (dataType) => {
102
- if (dataType.startsWith("STRUCT(")) {
103
- return dataType.replace("STRUCT(", "STRUCT").replace(")", "");
112
+ const parseStructFields = (dataType) => {
113
+ if (!dataType.toUpperCase().startsWith("STRUCT(")) {
114
+ return [];
104
115
  }
105
- if (dataType.startsWith("ENUM(")) {
106
- return "ENUM";
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.4",
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": "^18.19 || >=20.0"
24
+ "node": ">=22.0"
24
25
  },
25
26
  "keywords": ["duckdb", "erd", "diagram", "cli"],
26
27
  "dependencies": {
27
- "commander": "^13.1.0",
28
- "duckdb-async": "^1.2.0"
28
+ "@duckdb/node-api": "^1.5.2-r.1",
29
+ "commander": "^14.0.3"
29
30
  },
30
31
  "devDependencies": {
31
- "@types/node": "^18.11.9",
32
+ "@types/node": "^22",
32
33
  "ts-node": "^10.9.2",
33
- "typescript": "^5.8.2"
34
+ "typescript": "^6.0.3"
34
35
  }
35
36
  }