duckerd 0.2.0 → 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
 
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createERD = void 0;
4
4
  const node_api_1 = require("@duckdb/node-api");
5
5
  const metadata_1 = require("../lib/metadata");
6
- const createERD = async (databasePath) => {
6
+ const createERD = async (databasePath, expandStructs = false) => {
7
7
  const instance = await node_api_1.DuckDBInstance.create(databasePath);
8
8
  const conn = await instance.connect();
9
9
  try {
10
10
  const metadata = await (0, metadata_1.getMetadata)(conn);
11
- return (0, metadata_1.generateMermaidCodeForAllDBs)(metadata);
11
+ return (0, metadata_1.generateMermaidCodeForAllDBs)(metadata, expandStructs);
12
12
  }
13
13
  finally {
14
14
  conn.closeSync();
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
@@ -77,7 +77,7 @@ const getMetadata = async (conn) => {
77
77
  return metadata;
78
78
  };
79
79
  exports.getMetadata = getMetadata;
80
- const generateMermaidCodeForAllDBs = (metadata) => {
80
+ const generateMermaidCodeForAllDBs = (metadata, expandStructs = false) => {
81
81
  let mermaidCode = `erDiagram
82
82
 
83
83
  `;
@@ -88,7 +88,14 @@ const generateMermaidCodeForAllDBs = (metadata) => {
88
88
  // Add tables, columns and constraints
89
89
  mermaidCode += tables.map((table) => {
90
90
  return `"${table.databaseName}.${table.name}" {
91
- ${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")}
92
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")}`;
93
100
  }).join("\n ");
94
101
  return mermaidCode;
@@ -102,15 +109,61 @@ ${table.columns.map((column) => ` ${(0, exports.sanitizeDataType)(column.da
102
109
  }
103
110
  };
104
111
  exports.generateMermaidCodeForAllDBs = generateMermaidCodeForAllDBs;
105
- const sanitizeDataType = (dataType) => {
106
- if (dataType.startsWith("STRUCT(")) {
107
- return dataType.replace("STRUCT(", "STRUCT").replace(")", "");
112
+ const parseStructFields = (dataType) => {
113
+ if (!dataType.toUpperCase().startsWith("STRUCT(")) {
114
+ return [];
108
115
  }
109
- if (dataType.startsWith("ENUM(")) {
110
- 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
+ }
111
164
  }
112
165
  if (dataType.includes(",")) {
113
- return dataType.replace(",", "_");
166
+ return dataType.replace(/,/g, "_");
114
167
  }
115
168
  return dataType;
116
169
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "duckerd",
3
- "version": "0.2.0",
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,7 +17,8 @@
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
24
  "node": ">=22.0"