@cuongph.dev/dbdocgen 0.1.0 → 0.1.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/dist/cli/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { Command } from "commander";
5
- import { readFile as readFile2, writeFile as writeFile5, rm } from "fs/promises";
5
+ import { readFile as readFile2, readdir, rm, writeFile as writeFile6 } from "fs/promises";
6
6
  import { existsSync } from "fs";
7
7
  import { resolve } from "path";
8
8
 
@@ -30,7 +30,7 @@ var outputLanguageSchema = z.enum(["en", "jp"]);
30
30
  var dbdocgenConfigSchema = z.object({
31
31
  schema: z.string().default("./schema.sql"),
32
32
  dialect: dialectSchema.optional(),
33
- outDir: z.string().default("./docs/db"),
33
+ outDir: z.string().default("./output"),
34
34
  output: z.object({
35
35
  formats: z.array(outputFormatSchema).default(["excel", "markdown", "html", "diagram", "word"]),
36
36
  language: outputLanguageSchema.default("en")
@@ -113,7 +113,7 @@ function sanitizeType(type) {
113
113
  }
114
114
 
115
115
  // src/exporters/excel/excel-exporter.ts
116
- import { mkdir as mkdir2 } from "fs/promises";
116
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
117
117
  import { join as join2 } from "path";
118
118
  import ExcelJS from "exceljs";
119
119
 
@@ -137,6 +137,10 @@ var LABELS = {
137
137
  type: "Type",
138
138
  required: "Required",
139
139
  defaultValue: "Default Value",
140
+ size: "Size",
141
+ minValue: "Min",
142
+ maxValue: "Max",
143
+ unique: "Unique",
140
144
  notes: "Notes",
141
145
  yes: "Yes",
142
146
  no: "No",
@@ -165,7 +169,15 @@ var LABELS = {
165
169
  rowNo: "#",
166
170
  backToOverview: "\u2190 Overview",
167
171
  pkMarker: "PK",
168
- fkMarker: "FK"
172
+ fkMarker: "FK",
173
+ erDiagramHeading: "ER Diagram",
174
+ erDiagramSheet: "ER Diagram",
175
+ viewErDiagram: "View interactive ER diagram (html/er-diagram.html)",
176
+ zoomIn: "Zoom in",
177
+ zoomOut: "Zoom out",
178
+ zoomReset: "Reset",
179
+ zoomFit: "Fit",
180
+ panZoomHint: "Drag to pan \xB7 Scroll to zoom"
169
181
  },
170
182
  jp: {
171
183
  docTitle: "Database Documentation",
@@ -185,6 +197,10 @@ var LABELS = {
185
197
  type: "\u578B",
186
198
  required: "\u5FC5\u9808",
187
199
  defaultValue: "\u30C7\u30D5\u30A9\u30EB\u30C8\u5024",
200
+ size: "\u6841\u6570",
201
+ minValue: "\u6700\u5C0F\u5024",
202
+ maxValue: "\u6700\u5927\u5024",
203
+ unique: "\u4E00\u610F",
188
204
  notes: "\u5099\u8003",
189
205
  yes: "Yes",
190
206
  no: "No",
@@ -213,13 +229,533 @@ var LABELS = {
213
229
  rowNo: "No.",
214
230
  backToOverview: "\u2190 \u4E00\u89A7",
215
231
  pkMarker: "PK",
216
- fkMarker: "FK"
232
+ fkMarker: "FK",
233
+ erDiagramHeading: "ER Diagram",
234
+ erDiagramSheet: "ER Diagram",
235
+ viewErDiagram: "\u30A4\u30F3\u30BF\u30E9\u30AF\u30C6\u30A3\u30D6ER\u56F3 (html/er-diagram.html)",
236
+ zoomIn: "\u62E1\u5927",
237
+ zoomOut: "\u7E2E\u5C0F",
238
+ zoomReset: "\u30EA\u30BB\u30C3\u30C8",
239
+ zoomFit: "\u5168\u4F53\u8868\u793A",
240
+ panZoomHint: "\u30C9\u30E9\u30C3\u30B0\u3067\u79FB\u52D5 \xB7 \u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u62E1\u5927\u7E2E\u5C0F"
217
241
  }
218
242
  };
219
243
  function getOutputLabels(language = "en") {
220
244
  return LABELS[language];
221
245
  }
222
246
 
247
+ // src/exporters/shared/column-definition.ts
248
+ var A5_COLUMN_COUNT = 10;
249
+ function columnDefinitionHeaders(labels) {
250
+ return [
251
+ labels.physicalName,
252
+ labels.logicalName,
253
+ labels.type,
254
+ labels.size,
255
+ labels.required,
256
+ labels.defaultValue,
257
+ labels.minValue,
258
+ labels.maxValue,
259
+ labels.unique,
260
+ labels.notes
261
+ ];
262
+ }
263
+ function formatColumnNotes(column, labels) {
264
+ const parts = [];
265
+ if (column.isPrimaryKey) parts.push(labels.pkMarker);
266
+ if (column.isForeignKey) parts.push(labels.fkMarker);
267
+ if (column.constraintNotes?.length) parts.push(...column.constraintNotes);
268
+ if (column.description?.value) parts.push(column.description.value);
269
+ return parts.join(", ") || labels.none;
270
+ }
271
+ function columnDefinitionRow(column, labels) {
272
+ return [
273
+ column.name,
274
+ column.comment ?? "",
275
+ column.type,
276
+ column.size ?? labels.none,
277
+ column.nullable ? labels.no : labels.yes,
278
+ column.defaultValue ?? labels.none,
279
+ column.minValue ?? labels.none,
280
+ column.maxValue ?? labels.none,
281
+ column.isUnique ? labels.yes : labels.no,
282
+ formatColumnNotes(column, labels)
283
+ ];
284
+ }
285
+
286
+ // src/exporters/diagram/er-diagram-embed.ts
287
+ function getErDiagramMermaid(doc) {
288
+ return renderMermaid(doc);
289
+ }
290
+ function renderErDiagramHtmlPage(mermaidSource, labels) {
291
+ const escaped = mermaidSource.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
292
+ return `<!DOCTYPE html>
293
+ <html lang="en">
294
+ <head>
295
+ <meta charset="UTF-8">
296
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
297
+ <title>${esc(labels.erDiagramHeading)}</title>
298
+ <style>
299
+ body { margin: 0; font-family: "Yu Gothic UI", "Meiryo", Arial, sans-serif; background: #f3f4f6; }
300
+ .toolbar {
301
+ background: #4472c4; color: #fff; padding: 10px 16px;
302
+ display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
303
+ }
304
+ .toolbar a { color: #fff; text-decoration: underline; }
305
+ .toolbar .spacer { flex: 1; }
306
+ .toolbar .hint { opacity: 0.9; font-size: 13px; }
307
+ .toolbar button {
308
+ background: #fff; color: #2f5597; border: none; border-radius: 4px;
309
+ padding: 6px 12px; font-size: 13px; cursor: pointer; font-weight: 600;
310
+ }
311
+ .toolbar button:hover { background: #e8eef8; }
312
+ .viewport {
313
+ position: relative; height: calc(100vh - 52px); margin: 12px;
314
+ background: #fff; border: 1px solid #bfc7d4; border-radius: 4px;
315
+ overflow: hidden; cursor: grab; touch-action: none;
316
+ }
317
+ .viewport.dragging { cursor: grabbing; }
318
+ .canvas {
319
+ position: absolute; left: 0; top: 0; transform-origin: 0 0;
320
+ padding: 24px;
321
+ }
322
+ .mermaid { min-width: 320px; }
323
+ .mermaid svg { max-width: none !important; height: auto !important; }
324
+ </style>
325
+ </head>
326
+ <body>
327
+ <div class="toolbar">
328
+ <strong>${esc(labels.erDiagramHeading)}</strong>
329
+ <a href="index.html">\u2190 ${esc(labels.tableListHeading)}</a>
330
+ <span class="spacer"></span>
331
+ <span class="hint">${esc(labels.panZoomHint)}</span>
332
+ <button type="button" id="zoom-out" title="${esc(labels.zoomOut)}">\u2212</button>
333
+ <button type="button" id="zoom-reset" title="${esc(labels.zoomReset)}">${esc(labels.zoomReset)}</button>
334
+ <button type="button" id="zoom-in" title="${esc(labels.zoomIn)}">+</button>
335
+ <button type="button" id="zoom-fit" title="${esc(labels.zoomFit)}">${esc(labels.zoomFit)}</button>
336
+ </div>
337
+ <div class="viewport" id="viewport">
338
+ <div class="canvas" id="canvas">
339
+ <pre class="mermaid">${escaped}</pre>
340
+ </div>
341
+ </div>
342
+ <script type="module">
343
+ import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
344
+
345
+ mermaid.initialize({
346
+ startOnLoad: false,
347
+ theme: "default",
348
+ er: { useMaxWidth: false }
349
+ });
350
+
351
+ await mermaid.run({ querySelector: ".mermaid" });
352
+ setupPanZoom(
353
+ document.getElementById("viewport"),
354
+ document.getElementById("canvas")
355
+ );
356
+
357
+ function setupPanZoom(viewport, canvas) {
358
+ let scale = 1;
359
+ let tx = 40;
360
+ let ty = 40;
361
+ let dragging = false;
362
+ let lastX = 0;
363
+ let lastY = 0;
364
+
365
+ function apply() {
366
+ canvas.style.transform = "translate(" + tx + "px," + ty + "px) scale(" + scale + ")";
367
+ }
368
+
369
+ function zoomAt(factor, cx, cy) {
370
+ const next = Math.min(4, Math.max(0.15, scale * factor));
371
+ const ratio = next / scale;
372
+ tx = cx - (cx - tx) * ratio;
373
+ ty = cy - (cy - ty) * ratio;
374
+ scale = next;
375
+ apply();
376
+ }
377
+
378
+ function fitToView() {
379
+ const svg = canvas.querySelector("svg");
380
+ if (!svg) return;
381
+ const box = svg.getBBox();
382
+ const pad = 32;
383
+ const vw = viewport.clientWidth;
384
+ const vh = viewport.clientHeight;
385
+ scale = Math.min(
386
+ (vw - pad * 2) / Math.max(box.width, 1),
387
+ (vh - pad * 2) / Math.max(box.height, 1),
388
+ 1.5
389
+ );
390
+ tx = (vw - box.width * scale) / 2 - box.x * scale;
391
+ ty = (vh - box.height * scale) / 2 - box.y * scale;
392
+ apply();
393
+ }
394
+
395
+ viewport.addEventListener("wheel", (e) => {
396
+ e.preventDefault();
397
+ const rect = viewport.getBoundingClientRect();
398
+ const cx = e.clientX - rect.left;
399
+ const cy = e.clientY - rect.top;
400
+ zoomAt(e.deltaY < 0 ? 1.12 : 0.89, cx, cy);
401
+ }, { passive: false });
402
+
403
+ viewport.addEventListener("mousedown", (e) => {
404
+ if (e.button !== 0) return;
405
+ dragging = true;
406
+ lastX = e.clientX;
407
+ lastY = e.clientY;
408
+ viewport.classList.add("dragging");
409
+ });
410
+
411
+ window.addEventListener("mousemove", (e) => {
412
+ if (!dragging) return;
413
+ tx += e.clientX - lastX;
414
+ ty += e.clientY - lastY;
415
+ lastX = e.clientX;
416
+ lastY = e.clientY;
417
+ apply();
418
+ });
419
+
420
+ window.addEventListener("mouseup", () => {
421
+ dragging = false;
422
+ viewport.classList.remove("dragging");
423
+ });
424
+
425
+ document.getElementById("zoom-in").addEventListener("click", () => {
426
+ zoomAt(1.2, viewport.clientWidth / 2, viewport.clientHeight / 2);
427
+ });
428
+ document.getElementById("zoom-out").addEventListener("click", () => {
429
+ zoomAt(1 / 1.2, viewport.clientWidth / 2, viewport.clientHeight / 2);
430
+ });
431
+ document.getElementById("zoom-reset").addEventListener("click", () => {
432
+ scale = 1;
433
+ tx = 40;
434
+ ty = 40;
435
+ apply();
436
+ });
437
+ document.getElementById("zoom-fit").addEventListener("click", fitToView);
438
+
439
+ fitToView();
440
+ }
441
+ </script>
442
+ </body>
443
+ </html>`;
444
+ }
445
+ function renderErDiagramMarkdown(mermaidSource, labels) {
446
+ return [
447
+ `# ${labels.erDiagramHeading}`,
448
+ "",
449
+ "```mermaid",
450
+ mermaidSource.trimEnd(),
451
+ "```",
452
+ ""
453
+ ].join("\n");
454
+ }
455
+ function esc(text) {
456
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
457
+ }
458
+
459
+ // src/exporters/diagram/er-diagram-layout.ts
460
+ import ELK from "elkjs/lib/elk.bundled.js";
461
+ var BOX_W = 200;
462
+ var HEADER_H = 28;
463
+ var LINE_H = 14;
464
+ var MAX_COLS_SHOWN = 6;
465
+ var COMPACT_THRESHOLD = 18;
466
+ var PAD = 24;
467
+ var CLUSTER_GAP = 56;
468
+ function isCompactLayout(tableCount) {
469
+ return tableCount >= COMPACT_THRESHOLD;
470
+ }
471
+ function getVisibleErColumns(table) {
472
+ const prioritized = [
473
+ ...table.columns.filter((column) => column.isPrimaryKey),
474
+ ...table.columns.filter(
475
+ (column) => column.isForeignKey && !column.isPrimaryKey
476
+ ),
477
+ ...table.columns.filter(
478
+ (column) => !column.isPrimaryKey && !column.isForeignKey
479
+ )
480
+ ];
481
+ const unique = prioritized.filter(
482
+ (column, index, columns) => columns.findIndex((item) => item.name === column.name) === index
483
+ );
484
+ return unique.slice(0, MAX_COLS_SHOWN);
485
+ }
486
+ function measureTableBox(table, _compact = false) {
487
+ const visible = getVisibleErColumns(table);
488
+ const extra = table.columns.length > visible.length ? 1 : 0;
489
+ return {
490
+ w: BOX_W,
491
+ h: HEADER_H + (visible.length + extra) * LINE_H + 8
492
+ };
493
+ }
494
+ function buildAdjacency(doc) {
495
+ const names = new Set(doc.tables.map((t) => t.name));
496
+ const adj = /* @__PURE__ */ new Map();
497
+ for (const name of names) adj.set(name, /* @__PURE__ */ new Set());
498
+ for (const rel of doc.relationships.filter((r) => r.source === "schema")) {
499
+ if (!names.has(rel.fromTable) || !names.has(rel.toTable)) continue;
500
+ adj.get(rel.fromTable).add(rel.toTable);
501
+ adj.get(rel.toTable).add(rel.fromTable);
502
+ }
503
+ return adj;
504
+ }
505
+ function connectedComponents(tableNames, adj) {
506
+ const visited = /* @__PURE__ */ new Set();
507
+ const components = [];
508
+ for (const name of tableNames) {
509
+ if (visited.has(name)) continue;
510
+ const stack = [name];
511
+ const component = [];
512
+ visited.add(name);
513
+ while (stack.length > 0) {
514
+ const current = stack.pop();
515
+ component.push(current);
516
+ for (const next of adj.get(current) ?? []) {
517
+ if (!visited.has(next)) {
518
+ visited.add(next);
519
+ stack.push(next);
520
+ }
521
+ }
522
+ }
523
+ component.sort();
524
+ components.push(component);
525
+ }
526
+ return components.sort((a, b) => b.length - a.length);
527
+ }
528
+ function sectionToPoints(section) {
529
+ return [section.startPoint, ...section.bendPoints ?? [], section.endPoint];
530
+ }
531
+ function extractEdges(layouted) {
532
+ const edges = [];
533
+ for (const edge of layouted.edges ?? []) {
534
+ for (const section of edge.sections ?? []) {
535
+ edges.push({
536
+ id: edge.id,
537
+ points: sectionToPoints(section)
538
+ });
539
+ }
540
+ }
541
+ return edges;
542
+ }
543
+ async function layoutComponent(doc, tableNames, compact) {
544
+ const elk = new ELK();
545
+ const nameSet = new Set(tableNames);
546
+ const tables = doc.tables.filter((t) => nameSet.has(t.name));
547
+ const direction = tables.length >= 8 ? "DOWN" : "RIGHT";
548
+ const children = tables.map((table) => {
549
+ const { w, h } = measureTableBox(table, compact);
550
+ return { id: table.name, width: w, height: h };
551
+ });
552
+ const edges = [];
553
+ const seen = /* @__PURE__ */ new Set();
554
+ for (const rel of doc.relationships.filter((r) => r.source === "schema")) {
555
+ if (!nameSet.has(rel.fromTable) || !nameSet.has(rel.toTable)) continue;
556
+ const key = `${rel.fromTable}->${rel.toTable}`;
557
+ if (seen.has(key)) continue;
558
+ seen.add(key);
559
+ edges.push({
560
+ id: key,
561
+ sources: [rel.fromTable],
562
+ targets: [rel.toTable]
563
+ });
564
+ }
565
+ const graph = {
566
+ id: "root",
567
+ layoutOptions: {
568
+ "elk.algorithm": "layered",
569
+ "elk.direction": direction,
570
+ "elk.edgeRouting": "ORTHOGONAL",
571
+ "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
572
+ "elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
573
+ "elk.layered.spacing.nodeNodeBetweenLayers": compact ? "56" : "80",
574
+ "elk.spacing.nodeNode": compact ? "32" : "48",
575
+ "elk.spacing.edgeNode": "24",
576
+ "elk.padding": `[top=${PAD},left=${PAD},bottom=${PAD},right=${PAD}]`
577
+ },
578
+ children,
579
+ edges
580
+ };
581
+ const layouted = await elk.layout(graph);
582
+ const boxes = /* @__PURE__ */ new Map();
583
+ let minX = Infinity;
584
+ let minY = Infinity;
585
+ let maxX = -Infinity;
586
+ let maxY = -Infinity;
587
+ for (const child of layouted.children ?? []) {
588
+ const table = tables.find((t) => t.name === child.id);
589
+ if (!table) continue;
590
+ const { w, h } = measureTableBox(table, compact);
591
+ const x = child.x ?? 0;
592
+ const y = child.y ?? 0;
593
+ const box = { x, y, w, h };
594
+ boxes.set(child.id, box);
595
+ minX = Math.min(minX, x);
596
+ minY = Math.min(minY, y);
597
+ maxX = Math.max(maxX, x + w);
598
+ maxY = Math.max(maxY, y + h);
599
+ }
600
+ const width = Math.ceil(maxX - minX + PAD * 2);
601
+ const height = Math.ceil(maxY - minY + PAD * 2);
602
+ const dx = PAD - minX;
603
+ const dy = PAD - minY;
604
+ for (const [name, box] of boxes) {
605
+ boxes.set(name, { x: box.x + dx, y: box.y + dy, w: box.w, h: box.h });
606
+ }
607
+ const shiftedEdges = extractEdges(layouted).map((edge) => ({
608
+ ...edge,
609
+ points: edge.points.map((p) => ({ x: p.x + dx, y: p.y + dy }))
610
+ }));
611
+ return { boxes, edges: shiftedEdges, width, height };
612
+ }
613
+ function shiftLayout(boxes, edges, offsetX, offsetY) {
614
+ for (const [name, box] of boxes) {
615
+ boxes.set(name, { ...box, x: box.x + offsetX, y: box.y + offsetY });
616
+ }
617
+ for (const edge of edges) {
618
+ for (const p of edge.points) {
619
+ p.x += offsetX;
620
+ p.y += offsetY;
621
+ }
622
+ }
623
+ }
624
+ async function layoutErDiagram(doc) {
625
+ const tables = doc.tables;
626
+ if (tables.length === 0) {
627
+ return {
628
+ boxes: /* @__PURE__ */ new Map(),
629
+ edges: [],
630
+ compact: false,
631
+ width: 400,
632
+ height: 80
633
+ };
634
+ }
635
+ const compact = isCompactLayout(tables.length);
636
+ const adj = buildAdjacency(doc);
637
+ const components = connectedComponents(
638
+ tables.map((t) => t.name),
639
+ adj
640
+ );
641
+ const mergedBoxes = /* @__PURE__ */ new Map();
642
+ const mergedEdges = [];
643
+ const clusterCols = components.length <= 1 ? 1 : components.length <= 4 ? 2 : 3;
644
+ let tileX = 0;
645
+ let tileY = 0;
646
+ let rowHeight = 0;
647
+ let maxWidth = PAD;
648
+ let maxHeight = PAD;
649
+ for (const [i, component] of components.entries()) {
650
+ const laid = await layoutComponent(doc, component, compact);
651
+ shiftLayout(laid.boxes, laid.edges, tileX, tileY);
652
+ for (const [name, box] of laid.boxes) mergedBoxes.set(name, box);
653
+ mergedEdges.push(...laid.edges);
654
+ rowHeight = Math.max(rowHeight, laid.height);
655
+ tileX += laid.width + CLUSTER_GAP;
656
+ maxWidth = Math.max(maxWidth, tileX);
657
+ maxHeight = Math.max(maxHeight, tileY + laid.height);
658
+ if ((i + 1) % clusterCols === 0) {
659
+ tileX = 0;
660
+ tileY += rowHeight + CLUSTER_GAP;
661
+ rowHeight = 0;
662
+ }
663
+ }
664
+ return {
665
+ boxes: mergedBoxes,
666
+ edges: mergedEdges,
667
+ compact,
668
+ width: Math.ceil(maxWidth),
669
+ height: Math.ceil(maxHeight + PAD)
670
+ };
671
+ }
672
+
673
+ // src/exporters/diagram/er-diagram-svg.ts
674
+ async function renderErDiagramSvg(doc) {
675
+ const tables = doc.tables;
676
+ if (tables.length === 0) {
677
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="80"><text x="10" y="40" font-family="Arial,sans-serif" font-size="14">No tables</text></svg>`;
678
+ }
679
+ const layout = await layoutErDiagram(doc);
680
+ return buildErDiagramSvg(doc, layout);
681
+ }
682
+ async function renderErDiagramPng(doc) {
683
+ const tables = doc.tables;
684
+ if (tables.length === 0) {
685
+ const svg2 = await renderErDiagramSvg(doc);
686
+ const sharp2 = (await import("sharp")).default;
687
+ const buffer2 = await sharp2(Buffer.from(svg2)).png().toBuffer();
688
+ return { buffer: buffer2, width: 400, height: 80 };
689
+ }
690
+ const layout = await layoutErDiagram(doc);
691
+ const svg = buildErDiagramSvg(doc, layout);
692
+ const sharp = (await import("sharp")).default;
693
+ const buffer = await sharp(Buffer.from(svg)).png().toBuffer();
694
+ return { buffer, width: layout.width, height: layout.height };
695
+ }
696
+ function buildErDiagramSvg(doc, layout) {
697
+ const { boxes, edges, width, height } = layout;
698
+ const parts = [
699
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" font-family="Arial,sans-serif" font-size="11">`,
700
+ `<rect width="100%" height="100%" fill="#ffffff"/>`,
701
+ `<defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#5b7aa6"/></marker></defs>`,
702
+ `<g class="edges">`
703
+ ];
704
+ for (const edge of edges) {
705
+ if (edge.points.length < 2) continue;
706
+ const d = pointsToPath(edge.points);
707
+ parts.push(
708
+ `<path d="${d}" fill="none" stroke="#7d96b8" stroke-width="1.25" marker-end="url(#arrow)"/>`
709
+ );
710
+ }
711
+ parts.push(`</g><g class="nodes">`);
712
+ for (const table of doc.tables) {
713
+ const box = boxes.get(table.name);
714
+ parts.push(...renderTableBox(table, box));
715
+ }
716
+ parts.push("</g></svg>");
717
+ return parts.join("");
718
+ }
719
+ function pointsToPath(points) {
720
+ return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
721
+ }
722
+ function renderTableBox(table, box) {
723
+ const parts = [
724
+ `<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${box.h}" fill="#f8fafc" stroke="#4472c4" stroke-width="1.5" rx="4"/>`,
725
+ `<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${HEADER_H}" fill="#4472c4" rx="4"/>`,
726
+ `<rect x="${box.x}" y="${box.y + HEADER_H - 4}" width="${box.w}" height="4" fill="#4472c4"/>`,
727
+ `<text x="${box.x + 8}" y="${box.y + 18}" fill="#ffffff" font-weight="bold">${escapeXml(table.name)}</text>`
728
+ ];
729
+ let cy = box.y + HEADER_H + 14;
730
+ const visible = getVisibleErColumns(table);
731
+ for (const col of visible) {
732
+ const marker = col.isPrimaryKey ? " PK" : col.isForeignKey ? " FK" : "";
733
+ parts.push(
734
+ `<text x="${box.x + 8}" y="${cy}" fill="#333333">${escapeXml(col.name)} : ${escapeXml(shortType(col.type))}${marker}</text>`
735
+ );
736
+ cy += LINE_H;
737
+ }
738
+ if (table.columns.length > visible.length) {
739
+ parts.push(
740
+ `<text x="${box.x + 8}" y="${cy}" fill="#666666">... +${table.columns.length - visible.length} more</text>`
741
+ );
742
+ }
743
+ return parts;
744
+ }
745
+ function shortType(type) {
746
+ return type.length > 18 ? `${type.slice(0, 15)}...` : type;
747
+ }
748
+ function escapeXml(text) {
749
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
750
+ }
751
+ function fitErDiagramToBox(width, height, maxWidth, maxHeight) {
752
+ const scale = Math.min(maxWidth / width, maxHeight / height, 1);
753
+ return {
754
+ width: Math.max(1, Math.round(width * scale)),
755
+ height: Math.max(1, Math.round(height * scale))
756
+ };
757
+ }
758
+
223
759
  // src/exporters/excel/excel-exporter.ts
224
760
  var COLOR = {
225
761
  headerBg: "FF4472C4",
@@ -245,6 +781,9 @@ async function exportExcelDictionary(doc, options) {
245
781
  sheetNames.set(table.name, buildSheetName(table.name, sheetNames));
246
782
  }
247
783
  addOverviewSheet(workbook, doc, labels, sheetNames);
784
+ if (doc.tables.length > 0) {
785
+ await addErDiagramSheet(workbook, doc, labels, options.outDir);
786
+ }
248
787
  for (const table of doc.tables) {
249
788
  const sheetName = sheetNames.get(table.name);
250
789
  const sheet = workbook.addWorksheet(sheetName);
@@ -332,6 +871,51 @@ function addOverviewSheet(workbook, doc, labels, sheetNames) {
332
871
  };
333
872
  sheet.views = [{ state: "frozen", ySplit: headerRowNum }];
334
873
  }
874
+ async function addErDiagramSheet(workbook, doc, labels, outDir) {
875
+ const sheet = workbook.addWorksheet(labels.erDiagramSheet);
876
+ sheet.mergeCells(1, 1, 1, 6);
877
+ const titleCell = sheet.getCell(1, 1);
878
+ titleCell.value = labels.erDiagramHeading;
879
+ titleCell.font = { bold: true, size: 14, color: { argb: COLOR.overviewFg } };
880
+ titleCell.fill = solidFill(COLOR.overviewBg);
881
+ titleCell.alignment = { horizontal: "center", vertical: "middle" };
882
+ sheet.getRow(1).height = 28;
883
+ let nextRow = 3;
884
+ try {
885
+ const { buffer: png, width, height } = await renderErDiagramPng(doc);
886
+ await writeFile2(join2(outDir, "er_diagram.png"), png);
887
+ const imageId = workbook.addImage({
888
+ base64: png.toString("base64"),
889
+ extension: "png"
890
+ });
891
+ const fitted = fitErDiagramToBox(width, height, 1100, 1200);
892
+ sheet.addImage(imageId, {
893
+ tl: { col: 0, row: 2 },
894
+ ext: fitted
895
+ });
896
+ nextRow = Math.max(28, Math.ceil(fitted.height / 18) + 4);
897
+ } catch {
898
+ sheet.getCell(3, 1).value = labels.viewErDiagram;
899
+ nextRow = 5;
900
+ }
901
+ const mermaid = getErDiagramMermaid(doc);
902
+ sheet.getCell(nextRow, 1).value = "Mermaid source";
903
+ sheet.getCell(nextRow, 1).font = { bold: true, color: { argb: COLOR.metaFg } };
904
+ nextRow += 1;
905
+ sheet.mergeCells(nextRow, 1, nextRow + 20, 6);
906
+ const sourceCell = sheet.getCell(nextRow, 1);
907
+ sourceCell.value = mermaid;
908
+ sourceCell.alignment = { wrapText: true, vertical: "top" };
909
+ sourceCell.font = { name: "Courier New", size: 9 };
910
+ sheet.columns = [
911
+ { width: 24 },
912
+ { width: 24 },
913
+ { width: 24 },
914
+ { width: 24 },
915
+ { width: 24 },
916
+ { width: 24 }
917
+ ];
918
+ }
335
919
  function populateTableSheet(sheet, table, doc, labels) {
336
920
  const indexes = collectTableIndexes(table, doc);
337
921
  sheet.mergeCells(1, 1, 1, 6);
@@ -372,48 +956,39 @@ function populateTableSheet(sheet, table, doc, labels) {
372
956
  row.getCell(2).alignment = { wrapText: true, vertical: "top" };
373
957
  }
374
958
  sheet.addRow([]);
375
- const headerRow = sheet.addRow([
376
- labels.physicalName,
377
- labels.logicalName,
378
- labels.type,
379
- labels.required,
380
- labels.defaultValue,
381
- labels.notes
382
- ]);
959
+ const headerRow = sheet.addRow(columnDefinitionHeaders(labels));
383
960
  styleColorRow(headerRow, COLOR.headerBg, COLOR.headerFg);
384
- applyBorderToRow(headerRow, 6);
961
+ applyBorderToRow(headerRow, A5_COLUMN_COUNT);
385
962
  const headerRowNum = headerRow.number;
386
963
  for (const [i, column] of table.columns.entries()) {
387
- const markers = [];
388
- if (column.isPrimaryKey) markers.push(labels.pkMarker);
389
- if (column.isForeignKey) markers.push(labels.fkMarker);
390
- const notes = [markers.join(", "), column.description?.value ?? ""].filter(Boolean).join(" | ");
391
- const row = sheet.addRow([
392
- column.name,
393
- displayValue(column.comment, labels),
394
- column.type,
395
- column.nullable ? labels.no : labels.yes,
396
- column.defaultValue ?? "-",
397
- notes || "-"
398
- ]);
964
+ const row = sheet.addRow(
965
+ columnDefinitionRow(column, labels).map(
966
+ (value, index) => index === 1 ? displayValue(value, labels) : value
967
+ )
968
+ );
399
969
  if (column.isPrimaryKey) {
400
- shadeRow(row, 6, COLOR.pkBg);
970
+ shadeRow(row, A5_COLUMN_COUNT, COLOR.pkBg);
401
971
  row.getCell(1).font = { bold: true };
402
972
  } else if (column.isForeignKey) {
403
- shadeRow(row, 6, COLOR.fkBg);
973
+ shadeRow(row, A5_COLUMN_COUNT, COLOR.fkBg);
404
974
  } else if (i % 2 === 1) {
405
- shadeRow(row, 6, COLOR.altRow);
975
+ shadeRow(row, A5_COLUMN_COUNT, COLOR.altRow);
406
976
  }
407
- row.getCell(4).alignment = { horizontal: "center" };
408
- applyBorderToRow(row, 6);
977
+ row.getCell(5).alignment = { horizontal: "center" };
978
+ row.getCell(9).alignment = { horizontal: "center" };
979
+ applyBorderToRow(row, A5_COLUMN_COUNT);
409
980
  }
410
981
  sheet.columns = [
982
+ { width: 22 },
411
983
  { width: 24 },
412
- { width: 28 },
413
- { width: 18 },
984
+ { width: 16 },
414
985
  { width: 10 },
415
- { width: 18 },
416
- { width: 36 }
986
+ { width: 8 },
987
+ { width: 14 },
988
+ { width: 8 },
989
+ { width: 8 },
990
+ { width: 8 },
991
+ { width: 28 }
417
992
  ];
418
993
  sheet.views = [{ state: "frozen", ySplit: headerRowNum }];
419
994
  }
@@ -484,7 +1059,7 @@ function collectTableIndexes(table, doc) {
484
1059
  }
485
1060
 
486
1061
  // src/exporters/markdown/markdown-exporter.ts
487
- import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
1062
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
488
1063
  import { join as join3 } from "path";
489
1064
 
490
1065
  // src/core/sanitize.ts
@@ -498,8 +1073,16 @@ async function exportMarkdownDocs(doc, options) {
498
1073
  const tablesDir = join3(options.outDir, "tables");
499
1074
  await mkdir3(tablesDir, { recursive: true });
500
1075
  const labels = getOutputLabels(options.language);
1076
+ if (doc.tables.length > 0) {
1077
+ const mermaid = getErDiagramMermaid(doc);
1078
+ await writeFile3(
1079
+ join3(options.outDir, "ER_DIAGRAM.md"),
1080
+ renderErDiagramMarkdown(mermaid, labels),
1081
+ "utf8"
1082
+ );
1083
+ }
501
1084
  for (const table of doc.tables) {
502
- await writeFile2(
1085
+ await writeFile3(
503
1086
  join3(tablesDir, `${sanitizeFilename(table.name)}.md`),
504
1087
  renderTableDoc(table, doc, labels),
505
1088
  "utf8"
@@ -546,12 +1129,12 @@ function renderTableDoc(table, doc, labels) {
546
1129
  lines.push(`## ${labels.columnsHeading}`);
547
1130
  lines.push("");
548
1131
  lines.push(
549
- `| ${labels.physicalName} | ${labels.logicalName} | ${labels.type} | ${labels.required} | ${labels.defaultValue} | ${labels.notes} |`
1132
+ `| ${columnDefinitionHeaders(labels).join(" | ")} |`
550
1133
  );
551
- lines.push("|--------|--------|----|------|--------------|------|");
1134
+ lines.push(`|${columnDefinitionHeaders(labels).map(() => "--------").join("|")}|`);
552
1135
  for (const col of table.columns) {
553
1136
  lines.push(
554
- `| ${escapeMd(col.name)} | ${escapeMd(col.comment ?? "")} | ${escapeMd(col.type)} | ${col.nullable ? labels.no : labels.yes} | ${escapeMd(col.defaultValue ?? "-")} | ${escapeMd(col.description?.value ?? "")} |`
1137
+ `| ${columnDefinitionRow(col, labels).map((value) => escapeMd(value)).join(" | ")} |`
555
1138
  );
556
1139
  }
557
1140
  lines.push("");
@@ -570,7 +1153,7 @@ function escapeMd(text) {
570
1153
  }
571
1154
 
572
1155
  // src/exporters/html/html-exporter.ts
573
- import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
1156
+ import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
574
1157
  import { join as join4 } from "path";
575
1158
  async function exportHtmlDocs(doc, options) {
576
1159
  try {
@@ -578,13 +1161,21 @@ async function exportHtmlDocs(doc, options) {
578
1161
  const tablesDir = join4(htmlDir, "tables");
579
1162
  await mkdir4(tablesDir, { recursive: true });
580
1163
  const labels = getOutputLabels(options.language);
581
- await writeFile3(
1164
+ await writeFile4(
582
1165
  join4(htmlDir, "index.html"),
583
1166
  renderIndexPage(doc, labels),
584
1167
  "utf8"
585
1168
  );
1169
+ if (doc.tables.length > 0) {
1170
+ const mermaid = getErDiagramMermaid(doc);
1171
+ await writeFile4(
1172
+ join4(htmlDir, "er-diagram.html"),
1173
+ renderErDiagramHtmlPage(mermaid, labels),
1174
+ "utf8"
1175
+ );
1176
+ }
586
1177
  for (const table of doc.tables) {
587
- await writeFile3(
1178
+ await writeFile4(
588
1179
  join4(tablesDir, `${sanitizeFilename(table.name)}.html`),
589
1180
  renderTablePage(table, doc, labels),
590
1181
  "utf8"
@@ -666,7 +1257,7 @@ function pageShell(title, body, fromSubdir = false) {
666
1257
  <head>
667
1258
  <meta charset="UTF-8">
668
1259
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
669
- <title>${esc(title)}</title>
1260
+ <title>${esc2(title)}</title>
670
1261
  <style>${CSS} </style>
671
1262
  </head>
672
1263
  <body>
@@ -683,34 +1274,35 @@ function renderIndexPage(doc, labels) {
683
1274
  const fkCount = table.foreignKeys.length;
684
1275
  const fileName = sanitizeFilename(table.name);
685
1276
  tableRows += ` <tr>
686
- <td><a href="tables/${fileName}.html">${esc(table.name)}</a></td>
687
- <td>${esc(table.comment ?? "")}</td>
1277
+ <td><a href="tables/${fileName}.html">${esc2(table.name)}</a></td>
1278
+ <td>${esc2(table.comment ?? "")}</td>
688
1279
  <td style="text-align:center">${table.columns.length}</td>
689
- <td>${esc(pkCols)}</td>
1280
+ <td>${esc2(pkCols)}</td>
690
1281
  <td style="text-align:center">${fkCount}</td>
691
1282
  </tr>
692
1283
  `;
693
1284
  }
694
1285
  const body = `
695
- <h1>${esc(labels.docTitle)}</h1>
1286
+ <h1>${esc2(labels.docTitle)}</h1>
696
1287
  <div class="summary">
697
- <div class="summary-item"><div class="num">${doc.tables.length}</div><div class="lbl">${esc(labels.tablesLabel)}</div></div>
698
- <div class="summary-item"><div class="num">${doc.relationships.length}</div><div class="lbl">${esc(labels.relationshipsLabel)}</div></div>
699
- <div class="summary-item"><div class="num">${doc.dialect}</div><div class="lbl">${esc(labels.dialectLabel)}</div></div>
1288
+ <div class="summary-item"><div class="num">${doc.tables.length}</div><div class="lbl">${esc2(labels.tablesLabel)}</div></div>
1289
+ <div class="summary-item"><div class="num">${doc.relationships.length}</div><div class="lbl">${esc2(labels.relationshipsLabel)}</div></div>
1290
+ <div class="summary-item"><div class="num">${doc.dialect}</div><div class="lbl">${esc2(labels.dialectLabel)}</div></div>
700
1291
  </div>
701
- <h2>${esc(labels.tableListHeading)}</h2>
1292
+ <p class="back"><a href="er-diagram.html">${esc2(labels.erDiagramHeading)} \u2192</a></p>
1293
+ <h2>${esc2(labels.tableListHeading)}</h2>
702
1294
  <table class="table-list">
703
1295
  <thead><tr>
704
- <th>${esc(labels.tableLabel)}</th>
705
- <th>${esc(labels.tableLogicalName)}</th>
1296
+ <th>${esc2(labels.tableLabel)}</th>
1297
+ <th>${esc2(labels.tableLogicalName)}</th>
706
1298
  <th style="width:70px;text-align:center">Cols</th>
707
- <th>${esc(labels.primaryKey)}</th>
1299
+ <th>${esc2(labels.primaryKey)}</th>
708
1300
  <th style="width:50px;text-align:center">FK</th>
709
1301
  </tr></thead>
710
1302
  <tbody>
711
1303
  ${tableRows} </tbody>
712
1304
  </table>
713
- <p class="note">${esc(labels.generatedNote)}</p>
1305
+ <p class="note">${esc2(labels.generatedNote)}</p>
714
1306
  `;
715
1307
  return pageShell(labels.docTitle, body);
716
1308
  }
@@ -728,39 +1320,34 @@ function renderTablePage(table, doc, labels) {
728
1320
  const pkBadge = col.isPrimaryKey ? `<span class="badge badge-pk">PK</span>` : "";
729
1321
  const fkBadge = col.isForeignKey ? `<span class="badge badge-fk">FK</span>` : "";
730
1322
  const rowClass = col.isPrimaryKey ? "pk" : col.isForeignKey ? "fk" : "";
731
- const required = col.nullable ? labels.no : labels.yes;
732
- colRows += ` <tr${rowClass ? ` class="${rowClass}"` : ""}><td>${esc(col.name)}${pkBadge}${fkBadge}</td><td>${esc(col.comment ?? "")}</td><td>${esc(col.type)}</td><td>${required}</td><td>${esc(col.defaultValue ?? "-")}</td><td>${esc(col.description?.value ?? "")}</td></tr>
1323
+ const cells = columnDefinitionRow(col, labels);
1324
+ colRows += ` <tr${rowClass ? ` class="${rowClass}"` : ""}><td>${esc2(cells[0] ?? "")}${pkBadge}${fkBadge}</td>` + cells.slice(1).map((cell) => `<td>${esc2(cell)}</td>`).join("") + `</tr>
733
1325
  `;
734
1326
  }
735
1327
  const body = `
736
- <p class="back"><a href="../index.html">\u2190 ${esc(labels.tableListHeading)}</a></p>
737
- <h1>${esc(table.name)}</h1>
738
- <h2>${esc(labels.tableInfoHeading)}</h2>
1328
+ <p class="back"><a href="../index.html">\u2190 ${esc2(labels.tableListHeading)}</a></p>
1329
+ <h1>${esc2(table.name)}</h1>
1330
+ <h2>${esc2(labels.tableInfoHeading)}</h2>
739
1331
  <table class="meta">
740
1332
  <tbody>
741
- <tr><th>${esc(labels.tablePhysicalName)}</th><td>${esc(table.name)}</td></tr>
742
- <tr><th>${esc(labels.tableLogicalName)}</th><td>${esc(table.comment ?? "")}</td></tr>
743
- <tr><th>${esc(labels.schema)}</th><td>${esc(table.schema ?? "")}</td></tr>
744
- <tr><th>${esc(labels.primaryKey)}</th><td>${esc(table.primaryKeys.join(", ") || labels.none)}</td></tr>
745
- <tr><th>${esc(labels.foreignKeys)}</th><td>${foreignKeys}</td></tr>
746
- <tr><th>${esc(labels.indexes)}</th><td>${indexText}</td></tr>
1333
+ <tr><th>${esc2(labels.tablePhysicalName)}</th><td>${esc2(table.name)}</td></tr>
1334
+ <tr><th>${esc2(labels.tableLogicalName)}</th><td>${esc2(table.comment ?? "")}</td></tr>
1335
+ <tr><th>${esc2(labels.schema)}</th><td>${esc2(table.schema ?? "")}</td></tr>
1336
+ <tr><th>${esc2(labels.primaryKey)}</th><td>${esc2(table.primaryKeys.join(", ") || labels.none)}</td></tr>
1337
+ <tr><th>${esc2(labels.foreignKeys)}</th><td>${foreignKeys}</td></tr>
1338
+ <tr><th>${esc2(labels.indexes)}</th><td>${indexText}</td></tr>
747
1339
  </tbody>
748
1340
  </table>
749
1341
 
750
- <h2>${esc(labels.columnsHeading)}</h2>
1342
+ <h2>${esc2(labels.columnsHeading)}</h2>
751
1343
  <table class="columns">
752
1344
  <thead><tr>
753
- <th>${esc(labels.physicalName)}</th>
754
- <th>${esc(labels.logicalName)}</th>
755
- <th>${esc(labels.type)}</th>
756
- <th>${esc(labels.required)}</th>
757
- <th>${esc(labels.defaultValue)}</th>
758
- <th>${esc(labels.notes)}</th>
1345
+ ${columnDefinitionHeaders(labels).map((header) => `<th>${esc2(header)}</th>`).join("\n ")}
759
1346
  </tr></thead>
760
1347
  <tbody>
761
1348
  ${colRows} </tbody>
762
1349
  </table>
763
- <p class="note">${esc(labels.generatedNote)}</p>
1350
+ <p class="note">${esc2(labels.generatedNote)}</p>
764
1351
  `;
765
1352
  return pageShell(table.name, body);
766
1353
  }
@@ -772,12 +1359,12 @@ function collectTableIndexes3(table, doc) {
772
1359
  )
773
1360
  ];
774
1361
  }
775
- function esc(text) {
1362
+ function esc2(text) {
776
1363
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
777
1364
  }
778
1365
 
779
1366
  // src/exporters/word/word-exporter.ts
780
- import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
1367
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
781
1368
  import { join as join5 } from "path";
782
1369
  import {
783
1370
  Document,
@@ -787,7 +1374,8 @@ import {
787
1374
  TableCell,
788
1375
  TableRow,
789
1376
  TextRun,
790
- HeadingLevel
1377
+ HeadingLevel,
1378
+ ImageRun
791
1379
  } from "docx";
792
1380
  async function exportWordDocument(doc, options) {
793
1381
  try {
@@ -821,6 +1409,53 @@ async function exportWordDocument(doc, options) {
821
1409
  children: [new TextRun(`${labels.relationshipsLabel}: ${doc.relationships.length}`)]
822
1410
  })
823
1411
  );
1412
+ if (doc.tables.length > 0) {
1413
+ children.push(
1414
+ new Paragraph({
1415
+ heading: HeadingLevel.HEADING_2,
1416
+ children: [new TextRun(labels.erDiagramHeading)]
1417
+ })
1418
+ );
1419
+ try {
1420
+ const { buffer: png, width, height } = await renderErDiagramPng(doc);
1421
+ await writeFile5(join5(options.outDir, "er_diagram.png"), png);
1422
+ const fitted = fitErDiagramToBox(width, height, 620, 900);
1423
+ children.push(
1424
+ new Paragraph({
1425
+ children: [
1426
+ new ImageRun({
1427
+ data: png,
1428
+ transformation: fitted,
1429
+ type: "png"
1430
+ })
1431
+ ]
1432
+ })
1433
+ );
1434
+ } catch {
1435
+ children.push(
1436
+ new Paragraph({
1437
+ children: [new TextRun(labels.viewErDiagram)]
1438
+ })
1439
+ );
1440
+ }
1441
+ const mermaid = getErDiagramMermaid(doc);
1442
+ children.push(
1443
+ new Paragraph({
1444
+ children: [new TextRun({ text: "Mermaid source", bold: true })]
1445
+ })
1446
+ );
1447
+ children.push(
1448
+ new Paragraph({
1449
+ children: [
1450
+ new TextRun({
1451
+ text: mermaid,
1452
+ font: "Courier New",
1453
+ size: 18
1454
+ })
1455
+ ]
1456
+ })
1457
+ );
1458
+ }
824
1459
  children.push(
825
1460
  new Paragraph({
826
1461
  heading: HeadingLevel.HEADING_2,
@@ -1021,7 +1656,7 @@ async function exportWordDocument(doc, options) {
1021
1656
  sections: [{ children }]
1022
1657
  });
1023
1658
  const buffer = await Packer.toBuffer(wordDoc);
1024
- await writeFile4(join5(options.outDir, "database_document.docx"), buffer);
1659
+ await writeFile5(join5(options.outDir, "database_document.docx"), buffer);
1025
1660
  } catch (err) {
1026
1661
  throw new Error(
1027
1662
  `Failed to export Word document: ${err instanceof Error ? err.message : String(err)}`,
@@ -1080,14 +1715,7 @@ function renderTableDetail(table, doc, labels) {
1080
1715
  return items;
1081
1716
  }
1082
1717
  function renderColumnsTable(table, labels) {
1083
- const headerCells = [
1084
- labels.physicalName,
1085
- labels.logicalName,
1086
- labels.type,
1087
- labels.required,
1088
- labels.defaultValue,
1089
- labels.notes
1090
- ].map(
1718
+ const headerCells = columnDefinitionHeaders(labels).map(
1091
1719
  (h) => new TableCell({
1092
1720
  children: [
1093
1721
  new Paragraph({ children: [new TextRun({ text: h, bold: true })] })
@@ -1098,40 +1726,11 @@ function renderColumnsTable(table, labels) {
1098
1726
  for (const col of table.columns) {
1099
1727
  colRows.push(
1100
1728
  new TableRow({
1101
- children: [
1102
- new TableCell({
1103
- children: [new Paragraph({ children: [new TextRun(col.name)] })]
1104
- }),
1105
- new TableCell({
1106
- children: [
1107
- new Paragraph({ children: [new TextRun(col.comment ?? "")] })
1108
- ]
1109
- }),
1110
- new TableCell({
1111
- children: [new Paragraph({ children: [new TextRun(col.type)] })]
1112
- }),
1113
- new TableCell({
1114
- children: [
1115
- new Paragraph({
1116
- children: [new TextRun(col.nullable ? labels.no : labels.yes)]
1117
- })
1118
- ]
1119
- }),
1120
- new TableCell({
1121
- children: [
1122
- new Paragraph({
1123
- children: [new TextRun(col.defaultValue ?? "-")]
1124
- })
1125
- ]
1126
- }),
1127
- new TableCell({
1128
- children: [
1129
- new Paragraph({
1130
- children: [new TextRun(col.description?.value ?? "")]
1131
- })
1132
- ]
1729
+ children: columnDefinitionRow(col, labels).map(
1730
+ (value) => new TableCell({
1731
+ children: [new Paragraph({ children: [new TextRun(value)] })]
1133
1732
  })
1134
- ]
1733
+ )
1135
1734
  })
1136
1735
  );
1137
1736
  }
@@ -1179,6 +1778,221 @@ function createWarning(code, message, target) {
1179
1778
  };
1180
1779
  }
1181
1780
 
1781
+ // src/parsers/sql/column-meta.ts
1782
+ function normalizeColumnType(definition) {
1783
+ if (typeof definition !== "object" || definition === null) {
1784
+ return { type: String(definition ?? "unknown").toLowerCase() };
1785
+ }
1786
+ const def = definition;
1787
+ const base = String(def.dataType ?? def.type ?? def.name ?? "unknown").toLowerCase();
1788
+ if (String(def.dataType ?? "").toUpperCase() === "ENUM") {
1789
+ const values = extractEnumValues(def.expr);
1790
+ if (values.length > 0) {
1791
+ const joined = values.join(", ");
1792
+ return {
1793
+ type: `${base}(${values.map((v) => `'${v}'`).join(",")})`,
1794
+ size: String(values.length)
1795
+ };
1796
+ }
1797
+ }
1798
+ if (def.length !== void 0 && def.length !== null) {
1799
+ const length = formatAstValue(def.length);
1800
+ if (def.scale !== void 0 && def.scale !== null) {
1801
+ const scale = formatAstValue(def.scale);
1802
+ return {
1803
+ type: `${base}(${length},${scale})`,
1804
+ size: `${length},${scale}`
1805
+ };
1806
+ }
1807
+ return {
1808
+ type: `${base}(${length})`,
1809
+ size: length
1810
+ };
1811
+ }
1812
+ const suffix = Array.isArray(def.suffix) ? def.suffix.map((item) => formatAstValue(item)).filter(Boolean).join(" ") : def.suffix ? formatAstValue(def.suffix) : "";
1813
+ return {
1814
+ type: suffix ? `${base} ${suffix}`.trim() : base
1815
+ };
1816
+ }
1817
+ function extractColumnComment(definition) {
1818
+ const comment = definition.comment;
1819
+ if (!comment) return void 0;
1820
+ const value = comment.value;
1821
+ if (value?.value !== void 0) return String(value.value);
1822
+ if (comment.value !== void 0 && typeof comment.value === "string") {
1823
+ return comment.value;
1824
+ }
1825
+ return void 0;
1826
+ }
1827
+ function hasColumnUnique(definition) {
1828
+ return definition.unique === "unique" || definition.unique === true;
1829
+ }
1830
+ function extractColumnConstraintNotes(definition) {
1831
+ const notes = [];
1832
+ if (definition.auto_increment === "auto_increment" || definition.auto_increment === true) {
1833
+ notes.push("AUTO_INCREMENT");
1834
+ }
1835
+ if (definition.on_update) {
1836
+ notes.push(`ON UPDATE ${formatOnUpdate(definition.on_update)}`);
1837
+ }
1838
+ const generated = definition.generated;
1839
+ if (generated) {
1840
+ const storage = String(generated.storage_type ?? "virtual").toUpperCase();
1841
+ const expression = stringifyGeneratedExpression(generated.expr);
1842
+ notes.push(
1843
+ expression ? `GENERATED ALWAYS ${storage}: ${expression}` : `GENERATED ALWAYS ${storage}`
1844
+ );
1845
+ }
1846
+ const enumValues = extractEnumValues(definition.definition?.expr);
1847
+ if (enumValues.length > 0) {
1848
+ notes.push(`ENUM: ${enumValues.join(", ")}`);
1849
+ }
1850
+ return notes;
1851
+ }
1852
+ function extractEnumValues(expr) {
1853
+ if (!expr || typeof expr !== "object") return [];
1854
+ const object = expr;
1855
+ if (object.type !== "expr_list" || !Array.isArray(object.value)) return [];
1856
+ return object.value.map((item) => {
1857
+ if (!item || typeof item !== "object") return "";
1858
+ const entry = item;
1859
+ return entry.value !== void 0 ? String(entry.value) : "";
1860
+ }).filter(Boolean);
1861
+ }
1862
+ function formatOnUpdate(value) {
1863
+ if (!value || typeof value !== "object") return String(value ?? "");
1864
+ const object = value;
1865
+ if (object.type === "function" && object.name) {
1866
+ const name = object.name;
1867
+ const parts = Array.isArray(name.name) ? name.name : [];
1868
+ return parts.map((part) => String(part.value ?? "")).join("") || "CURRENT_TIMESTAMP";
1869
+ }
1870
+ return formatAstValue(value);
1871
+ }
1872
+ function stringifyGeneratedExpression(expr) {
1873
+ if (!expr || typeof expr !== "object") return void 0;
1874
+ const text = stringifyExpression(expr);
1875
+ return text === "check" ? void 0 : text;
1876
+ }
1877
+ function extractCheckBounds(expression, columnName) {
1878
+ const result = {};
1879
+ walkCheckExpression(expression, columnName, result);
1880
+ return result;
1881
+ }
1882
+ function walkCheckExpression(expression, columnName, result) {
1883
+ if (!expression || typeof expression !== "object") return;
1884
+ const expr = expression;
1885
+ if (expr.type === "binary_expr") {
1886
+ const operator = String(expr.operator ?? "").toUpperCase();
1887
+ if (operator === "AND" || operator === "OR") {
1888
+ walkCheckExpression(expr.left, columnName, result);
1889
+ walkCheckExpression(expr.right, columnName, result);
1890
+ return;
1891
+ }
1892
+ const columnRef = findColumnRef(expr.left) ?? findColumnRef(expr.right);
1893
+ if (columnRef !== columnName) return;
1894
+ const bound = readBound(expr, columnName);
1895
+ if (!bound) return;
1896
+ if (bound.kind === "min") {
1897
+ result.minValue = mergeBound(result.minValue, bound.value, "max");
1898
+ } else {
1899
+ result.maxValue = mergeBound(result.maxValue, bound.value, "min");
1900
+ }
1901
+ return;
1902
+ }
1903
+ result.expression ??= stringifyExpression(expr);
1904
+ }
1905
+ function readBound(expr, columnName) {
1906
+ const operator = String(expr.operator ?? "");
1907
+ const left = expr.left;
1908
+ const right = expr.right;
1909
+ if (findColumnRef(left) === columnName) {
1910
+ if (operator === ">=" || operator === ">") {
1911
+ return { kind: "min", value: formatAstValue(right) };
1912
+ }
1913
+ if (operator === "<=" || operator === "<") {
1914
+ return { kind: "max", value: formatAstValue(right) };
1915
+ }
1916
+ }
1917
+ if (findColumnRef(right) === columnName) {
1918
+ if (operator === ">=" || operator === ">") {
1919
+ return { kind: "max", value: formatAstValue(left) };
1920
+ }
1921
+ if (operator === "<=" || operator === "<") {
1922
+ return { kind: "min", value: formatAstValue(left) };
1923
+ }
1924
+ }
1925
+ return void 0;
1926
+ }
1927
+ function mergeBound(current, next, pick) {
1928
+ if (!current) return next;
1929
+ const currentNum = Number(current);
1930
+ const nextNum = Number(next);
1931
+ if (!Number.isNaN(currentNum) && !Number.isNaN(nextNum)) {
1932
+ return pick === "min" ? String(Math.max(currentNum, nextNum)) : String(Math.min(currentNum, nextNum));
1933
+ }
1934
+ return next;
1935
+ }
1936
+ function findColumnRef(value) {
1937
+ if (!value || typeof value !== "object") return void 0;
1938
+ const expr = value;
1939
+ if (expr.type === "column_ref" && expr.column) {
1940
+ return String(expr.column);
1941
+ }
1942
+ return void 0;
1943
+ }
1944
+ function formatAstValue(value) {
1945
+ if (value === null || value === void 0) return "";
1946
+ if (typeof value === "object") {
1947
+ const object = value;
1948
+ if (object.value !== void 0) return String(object.value);
1949
+ if (object.dataType) return normalizeColumnType(object).type;
1950
+ }
1951
+ return String(value);
1952
+ }
1953
+ function stringifyExpression(expr) {
1954
+ if (expr.type === "binary_expr") {
1955
+ const left = stringifyExpression(expr.left ?? {});
1956
+ const right = stringifyExpression(expr.right ?? {});
1957
+ return `${left} ${expr.operator} ${right}`.trim();
1958
+ }
1959
+ if (expr.type === "column_ref") return String(expr.column ?? "");
1960
+ if (expr.value !== void 0) return formatAstValue(expr);
1961
+ return "check";
1962
+ }
1963
+ function extractConstraintColumnNames(definition) {
1964
+ return extractDeepColumnNames(definition);
1965
+ }
1966
+ function extractDeepColumnNames(value) {
1967
+ if (!Array.isArray(value)) return [];
1968
+ return value.map((item) => {
1969
+ if (typeof item !== "object" || item === null) return String(item ?? "unknown");
1970
+ const object = item;
1971
+ if (object.column !== void 0) return String(object.column);
1972
+ if (object.expr) return extractDeepColumnName(object.expr);
1973
+ return String(object.name ?? "unknown");
1974
+ });
1975
+ }
1976
+ function extractDeepColumnName(value) {
1977
+ if (typeof value !== "object" || value === null) {
1978
+ return String(value ?? "unknown");
1979
+ }
1980
+ const object = value;
1981
+ if (object.expr) return extractDeepColumnName(object.expr);
1982
+ if (object.column && typeof object.column === "object") {
1983
+ return extractDeepColumnName(object.column);
1984
+ }
1985
+ if (object.column !== void 0) return String(object.column);
1986
+ return String(object.name ?? "unknown");
1987
+ }
1988
+ function stringifyCheckDefinition(definition) {
1989
+ if (!Array.isArray(definition) || definition.length === 0) return void 0;
1990
+ if (definition.length === 1) {
1991
+ return stringifyExpression(definition[0]);
1992
+ }
1993
+ return definition.map((item) => stringifyExpression(item)).filter(Boolean).join("; ");
1994
+ }
1995
+
1182
1996
  // src/parsers/sql/sql-normalizer.ts
1183
1997
  function normalizeSqlAst(ast, dialect) {
1184
1998
  const statements = Array.isArray(ast) ? ast : [ast];
@@ -1200,7 +2014,11 @@ function normalizeSqlAst(ast, dialect) {
1200
2014
  }
1201
2015
  for (const index of indexes) {
1202
2016
  const table = tables.find((candidate) => candidate.name === index.table);
1203
- table?.indexes.push(index);
2017
+ if (!table) continue;
2018
+ table.indexes.push(index);
2019
+ if (index.unique) {
2020
+ markColumnsUnique(table, index.columns, `INDEX ${index.name}`);
2021
+ }
1204
2022
  }
1205
2023
  return {
1206
2024
  dialect,
@@ -1222,31 +2040,49 @@ function normalizeCreateTable(statement) {
1222
2040
  reviewTodos: []
1223
2041
  };
1224
2042
  for (const definition of createDefinitions) {
1225
- if (definition.resource === "column") {
1226
- const columnName = extractDeepColumnName(definition.column);
1227
- const isPrimaryKey = hasPrimaryKey(definition);
1228
- const isNotNull = hasNotNull(definition);
1229
- table.columns.push({
1230
- name: columnName,
1231
- type: normalizeType(definition.definition),
1232
- nullable: !isNotNull && !isPrimaryKey,
1233
- defaultValue: extractDefaultFromDef(definition),
1234
- isPrimaryKey,
1235
- isForeignKey: false
1236
- });
1237
- if (isPrimaryKey) table.primaryKeys.push(columnName);
2043
+ if (definition.resource !== "column") continue;
2044
+ const columnName = extractDeepColumnName2(definition.column);
2045
+ const isPrimaryKey = hasPrimaryKey(definition);
2046
+ const isNotNull = hasNotNull(definition);
2047
+ const { type, size } = normalizeColumnType(definition.definition);
2048
+ const check = definition.check;
2049
+ const bounds = check?.definition ? extractCheckBounds(check.definition, columnName) : {};
2050
+ const constraintNotes = extractColumnConstraintNotes(definition);
2051
+ if (check?.definition) {
2052
+ const expression = stringifyCheckDefinition(check.definition);
2053
+ if (expression && (!bounds.minValue || !bounds.maxValue)) {
2054
+ constraintNotes.push(`CHECK: ${expression}`);
2055
+ }
1238
2056
  }
1239
- if (definition.resource === "constraint" && isConstraintType(definition.constraint_type, "PRIMARY KEY")) {
1240
- table.primaryKeys = extractDeepColumnNames(definition.definition);
2057
+ table.columns.push({
2058
+ name: columnName,
2059
+ type,
2060
+ size,
2061
+ nullable: !isNotNull && !isPrimaryKey,
2062
+ defaultValue: extractDefaultFromDef(definition),
2063
+ minValue: bounds.minValue,
2064
+ maxValue: bounds.maxValue,
2065
+ isUnique: hasColumnUnique(definition),
2066
+ isPrimaryKey,
2067
+ isForeignKey: false,
2068
+ comment: extractColumnComment(definition),
2069
+ constraintNotes: constraintNotes.length > 0 ? constraintNotes : void 0
2070
+ });
2071
+ if (isPrimaryKey) table.primaryKeys.push(columnName);
2072
+ }
2073
+ for (const definition of createDefinitions) {
2074
+ if (definition.resource !== "constraint") continue;
2075
+ if (isConstraintType(definition.constraint_type, "PRIMARY KEY")) {
2076
+ table.primaryKeys = extractDeepColumnNames2(definition.definition);
1241
2077
  for (const column of table.columns) {
1242
2078
  if (table.primaryKeys.includes(column.name)) column.isPrimaryKey = true;
1243
2079
  }
1244
2080
  }
1245
- if (definition.resource === "constraint" && isConstraintType(definition.constraint_type, "FOREIGN KEY")) {
1246
- const columns = extractDeepColumnNames(definition.definition);
2081
+ if (isConstraintType(definition.constraint_type, "FOREIGN KEY")) {
2082
+ const columns = extractDeepColumnNames2(definition.definition);
1247
2083
  const refDef = definition.reference_definition;
1248
2084
  const referencedTable = extractTableName(refDef?.table);
1249
- const referencedColumns = extractDeepColumnNames(refDef?.definition);
2085
+ const referencedColumns = extractDeepColumnNames2(refDef?.definition);
1250
2086
  table.foreignKeys.push({
1251
2087
  name: typeof definition.constraint === "string" ? definition.constraint : void 0,
1252
2088
  columns,
@@ -1257,14 +2093,71 @@ function normalizeCreateTable(statement) {
1257
2093
  if (columns.includes(column.name)) column.isForeignKey = true;
1258
2094
  }
1259
2095
  }
2096
+ if (isConstraintType(definition.constraint_type, "UNIQUE")) {
2097
+ const columns = extractConstraintColumnNames(definition.definition);
2098
+ const label = typeof definition.constraint === "string" ? definition.constraint : "UNIQUE";
2099
+ markColumnsUnique(table, columns, label);
2100
+ }
2101
+ if (isConstraintType(definition.constraint_type, "CHECK")) {
2102
+ applyTableCheckConstraint(table, definition);
2103
+ }
1260
2104
  }
1261
2105
  return table;
1262
2106
  }
2107
+ function markColumnsUnique(table, columns, label) {
2108
+ const composite = columns.length > 1;
2109
+ for (const columnName of columns) {
2110
+ const column = table.columns.find((item) => item.name === columnName);
2111
+ if (!column) continue;
2112
+ column.isUnique = true;
2113
+ if (composite) {
2114
+ addConstraintNote(
2115
+ column,
2116
+ `UNIQUE (${label}: ${columns.join(", ")})`
2117
+ );
2118
+ }
2119
+ }
2120
+ }
2121
+ function applyTableCheckConstraint(table, definition) {
2122
+ const expression = stringifyCheckDefinition(definition.definition);
2123
+ if (!expression) return;
2124
+ const referencedColumns = /* @__PURE__ */ new Set();
2125
+ for (const column of table.columns) {
2126
+ const bounds = extractCheckBounds(definition.definition, column.name);
2127
+ if (bounds.minValue) column.minValue = bounds.minValue;
2128
+ if (bounds.maxValue) column.maxValue = bounds.maxValue;
2129
+ if (bounds.minValue || bounds.maxValue) {
2130
+ referencedColumns.add(column.name);
2131
+ continue;
2132
+ }
2133
+ if (expression.includes(column.name)) {
2134
+ referencedColumns.add(column.name);
2135
+ }
2136
+ }
2137
+ if (referencedColumns.size === 0) {
2138
+ for (const column of table.columns) {
2139
+ addConstraintNote(column, `CHECK: ${expression}`);
2140
+ }
2141
+ return;
2142
+ }
2143
+ for (const columnName of referencedColumns) {
2144
+ const column = table.columns.find((item) => item.name === columnName);
2145
+ if (!column) continue;
2146
+ if (!column.minValue && !column.maxValue) {
2147
+ addConstraintNote(column, `CHECK: ${expression}`);
2148
+ }
2149
+ }
2150
+ }
2151
+ function addConstraintNote(column, note) {
2152
+ const notes = column.constraintNotes ?? [];
2153
+ if (!notes.includes(note)) notes.push(note);
2154
+ column.constraintNotes = notes;
2155
+ }
1263
2156
  function normalizeCreateIndex(statement) {
1264
2157
  return {
1265
2158
  name: String(statement.index ?? statement.index_name ?? "unnamed_index"),
1266
2159
  table: extractTableName(statement.table),
1267
- columns: extractDeepColumnNames(
2160
+ columns: extractDeepColumnNames2(
1268
2161
  statement.index_columns ?? statement.columns
1269
2162
  ),
1270
2163
  unique: Boolean(statement.unique)
@@ -1318,15 +2211,15 @@ function extractTableName(value) {
1318
2211
  }
1319
2212
  return String(value ?? "unknown");
1320
2213
  }
1321
- function extractDeepColumnName(value) {
2214
+ function extractDeepColumnName2(value) {
1322
2215
  if (typeof value !== "object" || value === null)
1323
2216
  return String(value ?? "unknown");
1324
2217
  const object = value;
1325
2218
  if (object.expr && typeof object.expr === "object") {
1326
- return extractDeepColumnName(object.expr);
2219
+ return extractDeepColumnName2(object.expr);
1327
2220
  }
1328
2221
  if (object.column && typeof object.column === "object") {
1329
- return extractDeepColumnName(object.column);
2222
+ return extractDeepColumnName2(object.column);
1330
2223
  }
1331
2224
  if (object.value !== void 0) {
1332
2225
  return String(object.value);
@@ -1336,18 +2229,9 @@ function extractDeepColumnName(value) {
1336
2229
  }
1337
2230
  return String(object.name ?? object.tableName ?? "unknown");
1338
2231
  }
1339
- function extractDeepColumnNames(value) {
2232
+ function extractDeepColumnNames2(value) {
1340
2233
  if (!Array.isArray(value)) return [];
1341
- return value.map((item) => extractDeepColumnName(item));
1342
- }
1343
- function normalizeType(value) {
1344
- if (typeof value === "object" && value !== null) {
1345
- const object = value;
1346
- return String(
1347
- object.dataType ?? object.type ?? object.name ?? "unknown"
1348
- ).toLowerCase();
1349
- }
1350
- return String(value ?? "unknown").toLowerCase();
2234
+ return value.map((item) => extractDeepColumnName2(item));
1351
2235
  }
1352
2236
  function hasPrimaryKey(def) {
1353
2237
  if (def.primary_key) return true;
@@ -1627,6 +2511,27 @@ async function generateDbDocs(options) {
1627
2511
  return doc;
1628
2512
  }
1629
2513
 
2514
+ // src/core/output-path.ts
2515
+ import { basename, join as join6 } from "path";
2516
+ var OUTPUT_RUN_DIR_PREFIX = "db_doc_gen_";
2517
+ function createTimestampedRunName(date = /* @__PURE__ */ new Date()) {
2518
+ const year = String(date.getFullYear()).slice(-2);
2519
+ const month = String(date.getMonth() + 1).padStart(2, "0");
2520
+ const day = String(date.getDate()).padStart(2, "0");
2521
+ const hours = String(date.getHours()).padStart(2, "0");
2522
+ const minutes = String(date.getMinutes()).padStart(2, "0");
2523
+ return `${OUTPUT_RUN_DIR_PREFIX}${year}${month}${day}${hours}${minutes}`;
2524
+ }
2525
+ function resolveGenerateOutDir(parentDir, date = /* @__PURE__ */ new Date()) {
2526
+ return join6(parentDir, createTimestampedRunName(date));
2527
+ }
2528
+ function isOutputRunDir(path) {
2529
+ return basename(path).startsWith(OUTPUT_RUN_DIR_PREFIX);
2530
+ }
2531
+ function isOutputRunDirName(name) {
2532
+ return name.startsWith(OUTPUT_RUN_DIR_PREFIX);
2533
+ }
2534
+
1630
2535
  // src/cli/index.ts
1631
2536
  var DEFAULT_CONFIG_PATH = "dbdocgen.config.json";
1632
2537
  var program = new Command();
@@ -1639,14 +2544,17 @@ program.command("init").description("Create a default config file").option("-f,
1639
2544
  }
1640
2545
  const defaultConfig = {
1641
2546
  schema: "./database/schema.sql",
2547
+ outDir: "./output",
1642
2548
  output: {
1643
2549
  formats: ["excel", "markdown", "html", "diagram", "word"],
1644
2550
  language: "en"
1645
2551
  }
1646
2552
  };
1647
- await writeFile5(configPath, JSON.stringify(defaultConfig, null, 2), "utf8");
2553
+ await writeFile6(configPath, JSON.stringify(defaultConfig, null, 2), "utf8");
1648
2554
  console.log(`Created config at ${configPath}`);
1649
- console.log("Default generate output directory is ./output/db_doc_gen_{yymmddhhmm} unless you pass --out.");
2555
+ console.log(
2556
+ "Each generate run writes to {outDir}/db_doc_gen_{yymmddhhmm} (outDir defaults to ./output)."
2557
+ );
1650
2558
  console.log("Edit the file to configure your database schema path and output formats.");
1651
2559
  });
1652
2560
  var configCommand = program.command("config").description("Manage configuration");
@@ -1676,7 +2584,7 @@ configCommand.command("validate").description("Validate config file").option("--
1676
2584
  process.exitCode = 1;
1677
2585
  }
1678
2586
  });
1679
- program.command("generate").description("Generate database documentation").option("--schema <path>", "Path to schema.sql").option("--out <path>", "Output directory").option("--format <formats>", "Comma-separated output formats").option("--config <path>", "Config file path").action(async (rawOptions) => {
2587
+ program.command("generate").description("Generate database documentation").option("--schema <path>", "Path to schema.sql").option("--out <path>", "Parent output directory (run folder: db_doc_gen_{yymmddhhmm})").option("--format <formats>", "Comma-separated output formats").option("--config <path>", "Config file path").action(async (rawOptions) => {
1680
2588
  console.log("[dbdocgen] Loading configuration...");
1681
2589
  const config = await loadConfig({
1682
2590
  cwd: process.cwd(),
@@ -1687,9 +2595,11 @@ program.command("generate").description("Generate database documentation").optio
1687
2595
  configPath: rawOptions.config
1688
2596
  }
1689
2597
  });
1690
- const outDir = rawOptions.out ?? createTimestampedOutputDir();
2598
+ const parentOutDir = config.outDir;
2599
+ const outDir = resolveGenerateOutDir(parentOutDir);
1691
2600
  console.log("[dbdocgen] Configuration loaded");
1692
2601
  console.log(` schema: ${config.schema}`);
2602
+ console.log(` outputParent: ${parentOutDir}`);
1693
2603
  console.log(` outDir: ${outDir}`);
1694
2604
  console.log(` formats: ${config.output.formats.join(", ")}`);
1695
2605
  console.log(` language: ${config.output.language}`);
@@ -1759,7 +2669,7 @@ Found ${doc.tables.length} table(s):
1759
2669
  }
1760
2670
  console.log("Schema validation passed.");
1761
2671
  });
1762
- program.command("clean").description("Clean output directory").option("--out <path>", "Output directory to clean").option("--config <path>", "Config file path").action(async (rawOptions) => {
2672
+ program.command("clean").description("Clean output directory").option("--out <path>", "Parent output directory or a specific db_doc_gen_* run folder").option("--config <path>", "Config file path").action(async (rawOptions) => {
1763
2673
  const config = await loadConfig({
1764
2674
  cwd: process.cwd(),
1765
2675
  cliOptions: {
@@ -1767,13 +2677,29 @@ program.command("clean").description("Clean output directory").option("--out <pa
1767
2677
  configPath: rawOptions.config
1768
2678
  }
1769
2679
  });
1770
- const outDir = resolve(config.outDir);
1771
- if (!existsSync(outDir)) {
1772
- console.log(`Output directory ${outDir} does not exist. Nothing to clean.`);
2680
+ const target = resolve(config.outDir);
2681
+ if (!existsSync(target)) {
2682
+ console.log(`Output path ${target} does not exist. Nothing to clean.`);
2683
+ return;
2684
+ }
2685
+ if (isOutputRunDir(target)) {
2686
+ console.log(`Cleaning ${target}...`);
2687
+ await rm(target, { recursive: true, force: true });
2688
+ console.log("Done.");
1773
2689
  return;
1774
2690
  }
1775
- console.log(`Cleaning ${outDir}...`);
1776
- await rm(outDir, { recursive: true, force: true });
2691
+ const entries = await readdir(target, { withFileTypes: true });
2692
+ const runDirs = entries.filter(
2693
+ (entry) => entry.isDirectory() && isOutputRunDirName(entry.name)
2694
+ );
2695
+ if (runDirs.length === 0) {
2696
+ console.log(`No db_doc_gen_* folders found under ${target}.`);
2697
+ return;
2698
+ }
2699
+ console.log(`Cleaning ${runDirs.length} run folder(s) under ${target}...`);
2700
+ for (const entry of runDirs) {
2701
+ await rm(resolve(target, entry.name), { recursive: true, force: true });
2702
+ }
1777
2703
  console.log("Done.");
1778
2704
  });
1779
2705
  program.command("info").description("Show project info and supported features").action(() => {
@@ -1819,12 +2745,4 @@ function parseFormats(value) {
1819
2745
  }
1820
2746
  return valid.length > 0 ? valid : void 0;
1821
2747
  }
1822
- function createTimestampedOutputDir(date = /* @__PURE__ */ new Date()) {
1823
- const year = String(date.getFullYear()).slice(-2);
1824
- const month = String(date.getMonth() + 1).padStart(2, "0");
1825
- const day = String(date.getDate()).padStart(2, "0");
1826
- const hours = String(date.getHours()).padStart(2, "0");
1827
- const minutes = String(date.getMinutes()).padStart(2, "0");
1828
- return `./output/db_doc_gen_${year}${month}${day}${hours}${minutes}`;
1829
- }
1830
2748
  //# sourceMappingURL=index.js.map