quickdbd-cli 0.1.2__tar.gz → 0.1.4__tar.gz

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.
Files changed (57) hide show
  1. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/PKG-INFO +103 -21
  2. quickdbd_cli-0.1.4/README.md +222 -0
  3. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/pyproject.toml +1 -1
  4. quickdbd_cli-0.1.4/src/quickdbd_cli/__init__.py +1 -0
  5. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/render.js +159 -2
  6. quickdbd_cli-0.1.4/src/quickdbd_cli/assets/style.css +167 -0
  7. quickdbd_cli-0.1.4/src/quickdbd_cli/assets/viewer.js +663 -0
  8. quickdbd_cli-0.1.4/src/quickdbd_cli/cli.py +326 -0
  9. quickdbd_cli-0.1.4/src/quickdbd_cli/dbt.py +412 -0
  10. quickdbd_cli-0.1.4/src/quickdbd_cli/diff.py +247 -0
  11. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/export.py +9 -0
  12. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/html.py +27 -4
  13. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/measure.py +22 -2
  14. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/project.py +42 -2
  15. quickdbd_cli-0.1.4/tests/fixtures/jaffle/LICENSE +201 -0
  16. quickdbd_cli-0.1.4/tests/fixtures/jaffle/README.md +2 -0
  17. quickdbd_cli-0.1.4/tests/fixtures/jaffle/catalog.json +227 -0
  18. quickdbd_cli-0.1.4/tests/fixtures/jaffle/manifest.json +880 -0
  19. quickdbd_cli-0.1.4/tests/test_dbt.py +231 -0
  20. quickdbd_cli-0.1.4/tests/test_diff.py +127 -0
  21. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/tests/test_pipeline.py +51 -0
  22. quickdbd_cli-0.1.4/tests/test_select.py +143 -0
  23. quickdbd_cli-0.1.2/README.md +0 -140
  24. quickdbd_cli-0.1.2/src/quickdbd_cli/__init__.py +0 -1
  25. quickdbd_cli-0.1.2/src/quickdbd_cli/assets/style.css +0 -92
  26. quickdbd_cli-0.1.2/src/quickdbd_cli/assets/viewer.js +0 -380
  27. quickdbd_cli-0.1.2/src/quickdbd_cli/cli.py +0 -169
  28. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/.gitignore +0 -0
  29. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/LICENSE +0 -0
  30. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/NOTICE +0 -0
  31. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/examples/quickdbd-sample.txt +0 -0
  32. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/examples/synthetic/finance.txt +0 -0
  33. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/examples/synthetic/hr.txt +0 -0
  34. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/examples/synthetic/inventory.txt +0 -0
  35. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/examples/synthetic/marketing.txt +0 -0
  36. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/examples/synthetic/qdbd.layout.json +0 -0
  37. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/examples/synthetic/sales.txt +0 -0
  38. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/scripts/gen_font_metrics.py +0 -0
  39. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/scripts/gen_schema.py +0 -0
  40. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/scripts/release.sh +0 -0
  41. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/__main__.py +0 -0
  42. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-Bold.ttf +0 -0
  43. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-Bold.woff2 +0 -0
  44. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-LICENSE.txt +0 -0
  45. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-Regular.ttf +0 -0
  46. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-Regular.woff2 +0 -0
  47. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/editor.js +0 -0
  48. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/elk-LICENSE.md +0 -0
  49. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/elk.bundled.js +0 -0
  50. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/font-metrics.json +0 -0
  51. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/jsrt.py +0 -0
  52. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/layout.py +0 -0
  53. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/model.py +0 -0
  54. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/parser.py +0 -0
  55. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/src/quickdbd_cli/serve.py +0 -0
  56. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/tests/test_parser.py +0 -0
  57. {quickdbd_cli-0.1.2 → quickdbd_cli-0.1.4}/tests/test_serve.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quickdbd-cli
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Compile QuickDBD-syntax schema files into interactive HTML ER diagrams, with no table limit.
5
5
  Project-URL: Homepage, https://github.com/MarkPhamm/quickdbd-cli
6
6
  Author: Minh (Mark) Pham
@@ -44,32 +44,96 @@ pipx install quickdbd-cli # or: uvx --from quickdbd-cli qdbd ...
44
44
  qdbd build schema.txt -o schema.html
45
45
  ```
46
46
 
47
+ **Live demo (200 tables):** https://markphamm.github.io/quickdbd-cli/
48
+
47
49
  Not affiliated with QuickDBD. It reads the same text syntax, so schemas move freely between the two.
48
50
 
49
51
  ## Features
50
52
 
51
53
  - **Same syntax as QuickDBD:** tables, aliases, quoted names, all 9 relationship operators, `rel` lines, PK/FK/NULL/UNIQUE/INDEX/IDENTITY, `default=`, doc comments, `[settings]` and `[layout]` sections. Error messages are QuickDBD's own.
52
54
  - **Interactive page:**
53
- - search and jump to a table (`/`)
55
+ - explorer sidebar: tables grouped by file or dbt folder, with search (`/`, Enter jumps to the best match)
56
+ - table detail panel (dbt-docs style, for relationships): description, columns with type/keys/docs, and clickable **References** / **Referenced by** lists
54
57
  - minimap
55
58
  - click a table to focus on it and its neighbours
56
59
  - collapse tables: full / keys only / names only
57
60
  - hover a column to highlight its relationship
58
61
  - hover a dotted-underlined table or column to read its `#` doc comments
59
- - PNG/SVG export buttons
62
+ - PNG/SVG export buttons, and **draw.io**: an editable diagrams.net file using draw.io's ER table shapes and crow's-foot arrows
63
+ - shareable links: the URL keeps the filter, selected table, collapse mode and zoom; **Link** copies it
60
64
  - **Live editing:** `qdbd serve` reloads when you save, has a browser editor pane with autosave, and remembers where you drag tables.
61
65
  - **Many files, one diagram:** point it at a folder and foreign keys can cross files.
62
66
  - **Pip-only install:** no Graphviz, Node or browser needed. Layout uses [ELK](https://eclipse.dev/elk/), running in an embedded V8.
63
67
  - **CI-friendly:** builds are deterministic, and errors are `file:line: error: …` with exit code 1.
64
68
 
69
+ ## From a dbt project
70
+
71
+ ```bash
72
+ dbt docs generate # optional: adds real column types (catalog.json)
73
+ qdbd import dbt target/manifest.json -o erd/ # one .txt per folder (staging, marts, sources, ...)
74
+ qdbd build erd/ -o erd.html
75
+ ```
76
+
77
+ - **Tables:** models, seeds and snapshots of your project, plus sources.
78
+ - **Relationships:** `relationships` tests (including dbt_utils `relationships_where`) and `foreign_key` constraints.
79
+ - **Primary keys:** `primary_key` constraints, or else a column with both `unique` and `not_null` tests.
80
+ - **Docs:** model and column descriptions become doc comments, shown as tooltips.
81
+ - **Options:**
82
+ - `--select 'marts.*'` narrows by name, fqn or path (repeatable)
83
+ - `--package all` includes installed packages
84
+ - `--no-sources` drops sources
85
+ - `-o schema.txt` writes a single file
86
+
87
+ Re-running the import overwrites the generated files. Table positions survive because they live in the layout file, keyed by table name.
88
+
89
+ ## Selecting tables
90
+
91
+ Like dbt's `--select`, but it follows relationships instead of lineage. It works in the page's filter box and as `-s/--select` / `--exclude` on `build` and `export`.
92
+
93
+ | Selector | Tables |
94
+ |---|---|
95
+ | `orders` / `stg_*` | by name or alias (glob, case-insensitive) |
96
+ | `+orders` | `orders` and every table it references (any depth); `1+orders` limits to one hop |
97
+ | `orders+` | `orders` and every table that references it; `orders+2` limits to two hops |
98
+ | `+orders+1` | both directions |
99
+ | `group:marts` | a file or dbt folder (and its subfolders) |
100
+ | `file:core.txt` | a schema file (glob) |
101
+ | `col:customer_id` | tables with a matching column |
102
+ | `diff:modified` / `diff:added` / `diff:changed` | changed since `--diff-base` (like dbt `state:modified`) |
103
+ | `a b` / `a,b` / `!a` | union / intersection / exclude |
104
+
105
+ ```bash
106
+ qdbd build erd/ -s 'group:marts' --exclude '*_tmp' -o marts.html
107
+ qdbd export erd/ -s '+orders+1' -o orders.png
108
+ ```
109
+
110
+ ## Schema diffs
111
+
112
+ ```bash
113
+ qdbd diff old.txt new.txt # two versions
114
+ qdbd diff --base origin/main erd/ # git ref vs working tree
115
+ qdbd diff --base origin/main erd/ --format markdown -o diff.md # for a PR comment
116
+ qdbd build erd/ --diff-base origin/main -o erd.html # colour what changed
117
+ ```
118
+
119
+ `diff` reports tables added and removed, column changes (added, removed, type and flag changes) and relationships added and removed. `--format json` is available, and `--exit-code` exits 1 when anything changed.
120
+
121
+ With `--diff-base`, the page:
122
+ - marks new tables green and changed tables amber, with a bar on each changed column;
123
+ - shows a summary banner, and its **Show changes** link filters to the changes plus their neighbours;
124
+ - lists each table's changes in the detail panel.
125
+
126
+ The selector `diff:modified` (or `diff:added` / `diff:changed`) works like dbt's `state:modified`, e.g. `-s 'diff:modified+1'`.
127
+
65
128
  ## Commands
66
129
 
67
130
  ```bash
68
131
  qdbd check PATH... [--strict] # validate; "No errors found. Good job!"
69
132
  qdbd build PATH... -o diagram.html # shareable interactive HTML (positions locked)
70
133
  qdbd serve PATH... [--port 8765] # live editor + drag-to-arrange
71
- qdbd export PATH... -o out.png [--focus Customer --depth 1] # PNG or SVG image
134
+ qdbd export PATH... -o out.png [--focus Customer --depth 1] # PNG, SVG or .drawio
72
135
  qdbd layout PATH... [--reset] # (re)write the layout file
136
+ qdbd import dbt target/manifest.json -o erd/ # generate schema files from a dbt project
73
137
  ```
74
138
 
75
139
  `PATH` is a `.txt` file or a folder. Every `*.txt` under a folder is read recursively.
@@ -102,27 +166,45 @@ Once a layout exists, a new table is placed next to the tables it's related to,
102
166
 
103
167
  ## CI
104
168
 
169
+ **GitHub Action:** checks the schema and builds the diagram. On pull requests it also posts one schema-diff comment, updated on every push, and highlights the changes in the diagram artifact.
170
+
171
+ ```yaml
172
+ name: ERD
173
+ on: [pull_request, push]
174
+ jobs:
175
+ erd:
176
+ runs-on: ubuntu-latest
177
+ permissions: { contents: read, pull-requests: write }
178
+ steps:
179
+ - uses: actions/checkout@v4
180
+ - uses: MarkPhamm/quickdbd-cli@v0.1.4
181
+ with:
182
+ path: erd/ # schema file(s) or folder(s)
183
+ output: site/index.html
184
+ ```
185
+
186
+ | Input | Default | |
187
+ |---|---|---|
188
+ | `path` | (required) | schema files or folders |
189
+ | `output` | `erd/index.html` | HTML diagram path |
190
+ | `check` / `strict` | `true` / `false` | fail on errors / also on QuickDBD-compatibility warnings |
191
+ | `comment` | `true` | post the diff comment on PRs |
192
+ | `select` | | dbt-style selector for the diagram |
193
+ | `qdbd-version` | `latest` | PyPI version |
194
+
195
+ Outputs are `html`, `changed` and `diff-file`.
196
+
197
+ **Publishing:** to publish the diagram on GitHub Pages, add `actions/upload-pages-artifact` + `actions/deploy-pages` after it. See [`.github/workflows/erd.yml`](.github/workflows/erd.yml), which publishes this repo's demo.
198
+
105
199
  **pre-commit:**
106
200
 
107
201
  ```yaml
108
202
  repos:
109
203
  - repo: https://github.com/MarkPhamm/quickdbd-cli
110
- rev: v0.1.0
204
+ rev: v0.1.4
111
205
  hooks:
112
206
  - id: qdbd-check
113
- files: ^schema/.*\.txt$ # point at your schema files
114
- ```
115
-
116
- **GitHub Actions:**
117
-
118
- ```yaml
119
- - uses: actions/setup-python@v5
120
- with: { python-version: "3.12" }
121
- - run: pip install quickdbd-cli
122
- - run: qdbd check schema/
123
- - run: qdbd build schema/ -o site/schema.html
124
- - uses: actions/upload-artifact@v4
125
- with: { name: schema-diagram, path: site/schema.html }
207
+ files: ^erd/.*\.txt$ # point at your schema files
126
208
  ```
127
209
 
128
210
  **GitLab CI:**
@@ -132,10 +214,10 @@ erd:
132
214
  image: python:3.12
133
215
  script:
134
216
  - pip install quickdbd-cli
135
- - qdbd check schema/
136
- - qdbd build schema/ -o public/schema.html
217
+ - qdbd check erd/
218
+ - qdbd build erd/ -o public/index.html
137
219
  artifacts:
138
- paths: [public/schema.html]
220
+ paths: [public/index.html]
139
221
  ```
140
222
 
141
223
  ## Development
@@ -0,0 +1,222 @@
1
+ # quickdbd-cli (`qdbd`)
2
+
3
+ Compile [QuickDBD](https://www.quickdatabasediagrams.com)-syntax schema files into **interactive HTML ER diagrams**, with no table limit, from the command line. Built to stay readable at 200+ tables.
4
+
5
+ ```
6
+ Customer
7
+ -
8
+ CustomerId PK int
9
+ Name string
10
+
11
+ Order
12
+ -
13
+ OrderId PK int
14
+ CustomerId int FK >- Customer.CustomerId
15
+ ```
16
+
17
+ ```bash
18
+ pipx install quickdbd-cli # or: uvx --from quickdbd-cli qdbd ...
19
+ qdbd build schema.txt -o schema.html
20
+ ```
21
+
22
+ **Live demo (200 tables):** https://markphamm.github.io/quickdbd-cli/
23
+
24
+ Not affiliated with QuickDBD. It reads the same text syntax, so schemas move freely between the two.
25
+
26
+ ## Features
27
+
28
+ - **Same syntax as QuickDBD:** tables, aliases, quoted names, all 9 relationship operators, `rel` lines, PK/FK/NULL/UNIQUE/INDEX/IDENTITY, `default=`, doc comments, `[settings]` and `[layout]` sections. Error messages are QuickDBD's own.
29
+ - **Interactive page:**
30
+ - explorer sidebar: tables grouped by file or dbt folder, with search (`/`, Enter jumps to the best match)
31
+ - table detail panel (dbt-docs style, for relationships): description, columns with type/keys/docs, and clickable **References** / **Referenced by** lists
32
+ - minimap
33
+ - click a table to focus on it and its neighbours
34
+ - collapse tables: full / keys only / names only
35
+ - hover a column to highlight its relationship
36
+ - hover a dotted-underlined table or column to read its `#` doc comments
37
+ - PNG/SVG export buttons, and **draw.io**: an editable diagrams.net file using draw.io's ER table shapes and crow's-foot arrows
38
+ - shareable links: the URL keeps the filter, selected table, collapse mode and zoom; **Link** copies it
39
+ - **Live editing:** `qdbd serve` reloads when you save, has a browser editor pane with autosave, and remembers where you drag tables.
40
+ - **Many files, one diagram:** point it at a folder and foreign keys can cross files.
41
+ - **Pip-only install:** no Graphviz, Node or browser needed. Layout uses [ELK](https://eclipse.dev/elk/), running in an embedded V8.
42
+ - **CI-friendly:** builds are deterministic, and errors are `file:line: error: …` with exit code 1.
43
+
44
+ ## From a dbt project
45
+
46
+ ```bash
47
+ dbt docs generate # optional: adds real column types (catalog.json)
48
+ qdbd import dbt target/manifest.json -o erd/ # one .txt per folder (staging, marts, sources, ...)
49
+ qdbd build erd/ -o erd.html
50
+ ```
51
+
52
+ - **Tables:** models, seeds and snapshots of your project, plus sources.
53
+ - **Relationships:** `relationships` tests (including dbt_utils `relationships_where`) and `foreign_key` constraints.
54
+ - **Primary keys:** `primary_key` constraints, or else a column with both `unique` and `not_null` tests.
55
+ - **Docs:** model and column descriptions become doc comments, shown as tooltips.
56
+ - **Options:**
57
+ - `--select 'marts.*'` narrows by name, fqn or path (repeatable)
58
+ - `--package all` includes installed packages
59
+ - `--no-sources` drops sources
60
+ - `-o schema.txt` writes a single file
61
+
62
+ Re-running the import overwrites the generated files. Table positions survive because they live in the layout file, keyed by table name.
63
+
64
+ ## Selecting tables
65
+
66
+ Like dbt's `--select`, but it follows relationships instead of lineage. It works in the page's filter box and as `-s/--select` / `--exclude` on `build` and `export`.
67
+
68
+ | Selector | Tables |
69
+ |---|---|
70
+ | `orders` / `stg_*` | by name or alias (glob, case-insensitive) |
71
+ | `+orders` | `orders` and every table it references (any depth); `1+orders` limits to one hop |
72
+ | `orders+` | `orders` and every table that references it; `orders+2` limits to two hops |
73
+ | `+orders+1` | both directions |
74
+ | `group:marts` | a file or dbt folder (and its subfolders) |
75
+ | `file:core.txt` | a schema file (glob) |
76
+ | `col:customer_id` | tables with a matching column |
77
+ | `diff:modified` / `diff:added` / `diff:changed` | changed since `--diff-base` (like dbt `state:modified`) |
78
+ | `a b` / `a,b` / `!a` | union / intersection / exclude |
79
+
80
+ ```bash
81
+ qdbd build erd/ -s 'group:marts' --exclude '*_tmp' -o marts.html
82
+ qdbd export erd/ -s '+orders+1' -o orders.png
83
+ ```
84
+
85
+ ## Schema diffs
86
+
87
+ ```bash
88
+ qdbd diff old.txt new.txt # two versions
89
+ qdbd diff --base origin/main erd/ # git ref vs working tree
90
+ qdbd diff --base origin/main erd/ --format markdown -o diff.md # for a PR comment
91
+ qdbd build erd/ --diff-base origin/main -o erd.html # colour what changed
92
+ ```
93
+
94
+ `diff` reports tables added and removed, column changes (added, removed, type and flag changes) and relationships added and removed. `--format json` is available, and `--exit-code` exits 1 when anything changed.
95
+
96
+ With `--diff-base`, the page:
97
+ - marks new tables green and changed tables amber, with a bar on each changed column;
98
+ - shows a summary banner, and its **Show changes** link filters to the changes plus their neighbours;
99
+ - lists each table's changes in the detail panel.
100
+
101
+ The selector `diff:modified` (or `diff:added` / `diff:changed`) works like dbt's `state:modified`, e.g. `-s 'diff:modified+1'`.
102
+
103
+ ## Commands
104
+
105
+ ```bash
106
+ qdbd check PATH... [--strict] # validate; "No errors found. Good job!"
107
+ qdbd build PATH... -o diagram.html # shareable interactive HTML (positions locked)
108
+ qdbd serve PATH... [--port 8765] # live editor + drag-to-arrange
109
+ qdbd export PATH... -o out.png [--focus Customer --depth 1] # PNG, SVG or .drawio
110
+ qdbd layout PATH... [--reset] # (re)write the layout file
111
+ qdbd import dbt target/manifest.json -o erd/ # generate schema files from a dbt project
112
+ ```
113
+
114
+ `PATH` is a `.txt` file or a folder. Every `*.txt` under a folder is read recursively.
115
+
116
+ ### Table positions
117
+
118
+ The first build auto-arranges the tables. In `qdbd serve`, drag tables where you want them. Positions are saved to a sidecar file you can commit:
119
+
120
+ - `schema.txt` → `schema.layout.json`
121
+ - a folder → `<folder>/qdbd.layout.json`
122
+ - override either with `--layout FILE`
123
+
124
+ Once a layout exists, a new table is placed next to the tables it's related to, and nothing else moves. Run `qdbd layout PATH --reset` to re-arrange everything from scratch. A pasted schema's QuickDBD `[layout]` section is used for any table the sidecar doesn't cover.
125
+
126
+ `qdbd build` never writes the sidecar, so CI builds leave the working tree clean.
127
+
128
+ ### Differences from QuickDBD
129
+
130
+ `qdbd` accepts a few things QuickDBD rejects, and prints a warning for each one:
131
+
132
+ - tab characters
133
+ - `decimal(10, 2)` (a space inside a type)
134
+ - an explicit `NOT NULL`
135
+ - indented table names
136
+ - `[layout]` entries for unknown tables
137
+
138
+ `qdbd check --strict` turns these warnings into errors, so a file that passes is guaranteed to paste into QuickDBD.
139
+
140
+ `default=` values keep their case, where QuickDBD lowercases them.
141
+
142
+ ## CI
143
+
144
+ **GitHub Action:** checks the schema and builds the diagram. On pull requests it also posts one schema-diff comment, updated on every push, and highlights the changes in the diagram artifact.
145
+
146
+ ```yaml
147
+ name: ERD
148
+ on: [pull_request, push]
149
+ jobs:
150
+ erd:
151
+ runs-on: ubuntu-latest
152
+ permissions: { contents: read, pull-requests: write }
153
+ steps:
154
+ - uses: actions/checkout@v4
155
+ - uses: MarkPhamm/quickdbd-cli@v0.1.4
156
+ with:
157
+ path: erd/ # schema file(s) or folder(s)
158
+ output: site/index.html
159
+ ```
160
+
161
+ | Input | Default | |
162
+ |---|---|---|
163
+ | `path` | (required) | schema files or folders |
164
+ | `output` | `erd/index.html` | HTML diagram path |
165
+ | `check` / `strict` | `true` / `false` | fail on errors / also on QuickDBD-compatibility warnings |
166
+ | `comment` | `true` | post the diff comment on PRs |
167
+ | `select` | | dbt-style selector for the diagram |
168
+ | `qdbd-version` | `latest` | PyPI version |
169
+
170
+ Outputs are `html`, `changed` and `diff-file`.
171
+
172
+ **Publishing:** to publish the diagram on GitHub Pages, add `actions/upload-pages-artifact` + `actions/deploy-pages` after it. See [`.github/workflows/erd.yml`](.github/workflows/erd.yml), which publishes this repo's demo.
173
+
174
+ **pre-commit:**
175
+
176
+ ```yaml
177
+ repos:
178
+ - repo: https://github.com/MarkPhamm/quickdbd-cli
179
+ rev: v0.1.4
180
+ hooks:
181
+ - id: qdbd-check
182
+ files: ^erd/.*\.txt$ # point at your schema files
183
+ ```
184
+
185
+ **GitLab CI:**
186
+
187
+ ```yaml
188
+ erd:
189
+ image: python:3.12
190
+ script:
191
+ - pip install quickdbd-cli
192
+ - qdbd check erd/
193
+ - qdbd build erd/ -o public/index.html
194
+ artifacts:
195
+ paths: [public/index.html]
196
+ ```
197
+
198
+ ## Development
199
+
200
+ ```bash
201
+ uv venv && uv pip install -e ".[dev]"
202
+ .venv/bin/pytest
203
+ python scripts/gen_schema.py # regenerate examples/synthetic (200 tables)
204
+ python scripts/gen_font_metrics.py # regenerate font width table
205
+ ```
206
+
207
+ Design notes: `docs/superpowers/specs/2026-10-17-qdbd-design.md`.
208
+
209
+ ### Releasing
210
+
211
+ Put PyPI credentials in `.env` (gitignored):
212
+
213
+ ```
214
+ TWINE_USERNAME=__token__
215
+ TWINE_PASSWORD=pypi-...
216
+ ```
217
+
218
+ Bump `version` in `pyproject.toml` and `src/quickdbd_cli/__init__.py`, commit, then run `PATH="$PWD/.venv/bin:$PATH" scripts/release.sh`. It builds, uploads to PyPI, tags `vX.Y.Z` and creates the GitHub release.
219
+
220
+ ## License
221
+
222
+ Apache-2.0. Bundled third-party files are listed in [NOTICE](NOTICE).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quickdbd-cli"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Compile QuickDBD-syntax schema files into interactive HTML ER diagrams, with no table limit."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.4"
@@ -18,6 +18,8 @@
18
18
  badgeBg: "#eef1f5",
19
19
  badgeText: "#7a8899",
20
20
  edge: "#b8bcc2",
21
+ added: "#2e7d32",
22
+ changed: "#d48806",
21
23
  };
22
24
  var FONT = "Inter, 'Helvetica Neue', Helvetica, Arial, sans-serif";
23
25
 
@@ -76,7 +78,12 @@
76
78
  var st = model.style, w = g.w, h = g.h, out = [];
77
79
  out.push('<g class="t" data-id="' + esc(t.id) + '" transform="translate(' + n(p[0]) + "," + n(p[1]) + ')">');
78
80
  out.push('<rect class="tb" width="' + w + '" height="' + h + '" rx="2" fill="' + C.body + '" stroke="' + C.border + '"/>');
79
- out.push('<path class="th" d="M0 2a2 2 0 0 1 2-2h' + (w - 4) + "a2 2 0 0 1 2 2v" + (st.headerH - 2) + "h-" + w + 'z" fill="' + C.header + '"/>');
81
+ var state = t.diff && t.diff.state, hcol = state === "added" ? C.added : state === "changed" ? C.changed : C.header;
82
+ out.push('<path class="th" d="M0 2a2 2 0 0 1 2-2h' + (w - 4) + "a2 2 0 0 1 2 2v" + (st.headerH - 2) + "h-" + w + 'z" fill="' + hcol + '"/>');
83
+ if (state) {
84
+ out.push('<text x="' + (w - st.padX) + '" y="' + n(st.headerH / 2 + 4) + '" text-anchor="end" fill="#fff" font-family="' + FONT +
85
+ '" font-size="10" font-weight="bold">' + (state === "added" ? "NEW" : "CHANGED") + "</text>");
86
+ }
80
87
  out.push('<text x="' + st.padX + '" y="' + n(st.headerH / 2 + st.headerSize * 0.36) + '" fill="' + C.headerText +
81
88
  '" font-family="' + FONT + '" font-size="' + st.headerSize + '">' + esc(t.name) + "</text>");
82
89
  if (docMarks && t.doc) {
@@ -88,6 +95,8 @@
88
95
  var color = c.pk ? C.pk : c.fk ? C.fk : C.text;
89
96
  out.push('<g class="r" data-c="' + esc(c.id) + '">');
90
97
  out.push('<rect class="rb" y="' + y + '" width="' + w + '" height="' + st.rowH + '" fill="' + C.body + '" fill-opacity="0"/>');
98
+ var cstate = t.diff && t.diff.cols && t.diff.cols[c.id];
99
+ if (cstate) out.push('<rect y="' + (y + 3) + '" width="3" height="' + (st.rowH - 6) + '" fill="' + C[cstate] + '"/>');
91
100
  out.push('<text x="' + st.padX + '" y="' + base + '" fill="' + color + '" font-family="' + FONT + '" font-size="' +
92
101
  st.rowSize + '"' + weight + ">" + esc(c.name) + "</text>");
93
102
  if (docMarks && c.doc) out.push(docMark(st.padX, cy + st.rowSize * 0.36 + 2.5, c.nameW, color));
@@ -99,7 +108,7 @@
99
108
  '" font-family="' + FONT + '" font-size="' + st.badgeSize + '">' + esc(b.t) + "</text>");
100
109
  bx += b.w + st.badgeGap;
101
110
  });
102
- if (c.pk) out.push(keyIcon(w - st.padX - c.typeW - st.keyW + 1, cy));
111
+ if (c.pk) out.push(keyIcon(w - st.padX - c.typeW - st.keyW, cy));
103
112
  out.push('<text x="' + (w - st.padX) + '" y="' + base + '" text-anchor="end" fill="' + C.type + '" font-family="' + FONT +
104
113
  '" font-size="' + st.rowSize + '">' + esc(c.type) + "</text>");
105
114
  out.push("</g>");
@@ -225,7 +234,153 @@
225
234
  return out;
226
235
  }
227
236
 
237
+ /* ------------------------------------------------------------------ draw.io export
238
+ * mxGraph XML using draw.io's own Entity Relation table shapes, so tables stay editable
239
+ * (rows can be added, lines re-attached) and crow's-foot arrows are draw.io's ER arrows. */
240
+ var DRAWIO_ARROW = { one: "ERmandOne", many: "ERmany", zeroOne: "ERzeroToOne", zeroMany: "ERzeroToMany" };
241
+ function renderDrawio(model, pos, opts) {
242
+ opts = opts || {};
243
+ var mode = opts.mode || "full", m = subset(model, opts.only), geo = geometry(m, mode);
244
+ var KEYW = 44, ROWH = 26, HEADH = 32, cells = [], rowId = {}, n = 2;
245
+ function id() { return "q" + n++; }
246
+ function cell(attrs, geom) {
247
+ var a = Object.keys(attrs).map(function (k) { return k + '="' + esc(attrs[k]) + '"'; }).join(" ");
248
+ return "<mxCell " + a + ">" + geom + "</mxCell>";
249
+ }
250
+ function g(x, y, w, h) {
251
+ return '<mxGeometry x="' + n2(x) + '" y="' + n2(y) + '" width="' + n2(w) + '" height="' + n2(h) + '" as="geometry"/>';
252
+ }
253
+ function n2(v) { return Math.round(v); }
254
+ var b = bounds(m, pos, mode);
255
+ m.tables.forEach(function (t) {
256
+ var p = pos[t.id];
257
+ if (!p) return;
258
+ var tg = geo[t.id], w = tg.w + KEYW, tid = id();
259
+ rowId[t.id] = { _table: tid };
260
+ cells.push(cell({
261
+ id: tid, value: t.name, vertex: "1", parent: "1",
262
+ style: "shape=table;startSize=" + HEADH + ";container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;" +
263
+ "fontStyle=1;align=left;spacingLeft=10;resizeLast=1;html=0;whiteSpace=wrap;fillColor=#2b5797;fontColor=#ffffff;" +
264
+ "strokeColor=#dfe3e8;swimlaneFillColor=#ffffff;fontSize=14;",
265
+ }, g(p[0] - b[0] + 40, p[1] - b[1] + 40, w, HEADH + Math.max(tg.cols.length, 0) * ROWH)));
266
+ tg.cols.forEach(function (c, i) {
267
+ var rid = id(), key = c.pk && c.fk ? "PK,FK" : c.pk ? "PK" : c.fk ? "FK" : "";
268
+ rowId[t.id][c.id] = rid;
269
+ cells.push(cell({
270
+ id: rid, value: "", vertex: "1", parent: tid,
271
+ style: "shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;" +
272
+ "dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;",
273
+ }, g(0, HEADH + i * ROWH, w, ROWH)));
274
+ var base = "shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;html=0;";
275
+ var flags = c.badges.map(function (x) { return x.t; }).join(" ");
276
+ cells.push(cell({ id: id(), value: key, vertex: "1", parent: rid, style: base + "fontStyle=1;fontColor=#2b5797;align=center;fontSize=11;" },
277
+ g(0, 0, KEYW, ROWH)));
278
+ cells.push(cell({
279
+ id: id(), value: c.name + (flags ? " [" + flags + "]" : "") + (c.type ? " : " + c.type : ""), vertex: "1", parent: rid,
280
+ style: base + "align=left;spacingLeft=6;fontSize=12;" + (c.pk ? "fontStyle=5;" : c.fk ? "fontStyle=1;" : ""),
281
+ }, g(KEYW, 0, w - KEYW, ROWH)));
282
+ });
283
+ });
284
+ m.rels.forEach(function (r) {
285
+ var ra = rowId[r.a.t], rb = rowId[r.b.t];
286
+ if (!ra || !rb) return;
287
+ var src = ra[r.a.c] || ra._table, dst = rb[r.b.c] || rb._table;
288
+ cells.push(cell({
289
+ id: id(), value: "", edge: "1", parent: "1", source: src, target: dst,
290
+ style: "edgeStyle=entityRelationEdgeStyle;fontSize=12;html=0;endFill=0;startFill=0;strokeColor=#9aa0a6;" +
291
+ "startArrow=" + DRAWIO_ARROW[r.a.card] + ";endArrow=" + DRAWIO_ARROW[r.b.card] + ";",
292
+ }, '<mxGeometry relative="1" as="geometry"/>'));
293
+ });
294
+ return '<mxfile host="qdbd" type="device"><diagram id="erd" name="' + esc(opts.title || "ERD") + '">' +
295
+ '<mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" ' +
296
+ 'page="0" pageScale="1" math="0" shadow="0"><root><mxCell id="0"/><mxCell id="1" parent="0"/>' +
297
+ cells.join("") + "</root></mxGraphModel></diagram></mxfile>";
298
+ }
299
+
300
+ /* ------------------------------------------------------------------ selectors (dbt-style, over relationships)
301
+ * Space-separated atoms are unioned; a comma inside an atom intersects (a,b); a leading ! excludes.
302
+ * Atom: [N]+ method:pattern +[N]
303
+ * method: name (default; also matches aliases), group, file, col
304
+ * pattern: glob with * and ?
305
+ * +x x plus the tables it references (parents), any depth; 2+x limits to 2 hops
306
+ * x+ x plus the tables referencing it (children); x+1 limits to 1 hop
307
+ */
308
+ function globRe(p) {
309
+ return new RegExp("^" + p.replace(/[.^$|()[\]{}\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$", "i");
310
+ }
311
+ function edges(model) {
312
+ var up = {}, down = {};
313
+ model.rels.forEach(function (r) {
314
+ var aMany = r.a.card === "many" || r.a.card === "zeroMany", bMany = r.b.card === "many" || r.b.card === "zeroMany";
315
+ var aIsChild = aMany || !bMany, child = aIsChild ? r.a.t : r.b.t, parent = aIsChild ? r.b.t : r.a.t;
316
+ if (child === parent) return;
317
+ (up[child] = up[child] || []).push(parent);
318
+ (down[parent] = down[parent] || []).push(child);
319
+ });
320
+ return { up: up, down: down };
321
+ }
322
+ function walk(start, adj, depth, into) {
323
+ var frontier = start, d = 0;
324
+ while (frontier.length && d < depth) {
325
+ var next = [];
326
+ frontier.forEach(function (id) {
327
+ (adj[id] || []).forEach(function (o) { if (!into[o]) { into[o] = true; next.push(o); } });
328
+ });
329
+ frontier = next;
330
+ d++;
331
+ }
332
+ }
333
+ function atomSet(model, atom, g) {
334
+ var m = /^(\d*\+)?(.*?)(\+\d*)?$/.exec(atom), up = m[1], body = m[2], down = m[3];
335
+ var method = "name", pat = body, i = body.indexOf(":");
336
+ if (i > 0) { method = body.slice(0, i).toLowerCase(); pat = body.slice(i + 1); }
337
+ var re = globRe(pat || "*"), out = {}, base = [];
338
+ model.tables.forEach(function (t) {
339
+ var ok;
340
+ if (method === "name") ok = re.test(t.name) || t.aliases.some(function (a) { return re.test(a); });
341
+ else if (method === "group") ok = re.test(t.group || "") || (t.group || "").toLowerCase().indexOf(pat.toLowerCase() + "/") === 0;
342
+ else if (method === "file") ok = re.test(t.file || "") || re.test((t.file || "").split("/").pop());
343
+ else if (method === "col" || method === "column") ok = t.cols.some(function (c) { return re.test(c.name); });
344
+ else if (method === "diff" || method === "state") {
345
+ var ds = t.diff && t.diff.state, want = pat.toLowerCase();
346
+ ok = !!ds && (want === "modified" || want === "*" || want === ds || (want === "new" && ds === "added"));
347
+ }
348
+ else throw new Error("unknown selector method '" + method + "' (use name, group, file, col, diff)");
349
+ if (ok) { out[t.id] = true; base.push(t.id); }
350
+ });
351
+ function hops(op, before) {
352
+ var n = before ? op.slice(0, -1) : op.slice(1);
353
+ return n ? parseInt(n, 10) : Infinity;
354
+ }
355
+ if (up) walk(base, g.up, hops(up, true), out);
356
+ if (down) walk(base, g.down, hops(down, false), out);
357
+ return out;
358
+ }
359
+ /* Returns the ids matching `expr`, in model order, or null when expr is blank (= everything). */
360
+ function select(model, expr) {
361
+ var tokens = String(expr || "").trim().split(/\s+/).filter(Boolean);
362
+ if (!tokens.length) return null;
363
+ var g = edges(model), inc = null, exc = {};
364
+ tokens.forEach(function (tok) {
365
+ var neg = tok.charAt(0) === "!";
366
+ if (neg) tok = tok.slice(1);
367
+ var parts = tok.split(",").filter(Boolean), set = null;
368
+ parts.forEach(function (p) {
369
+ var s = atomSet(model, p, g);
370
+ if (set === null) set = s;
371
+ else { var k = {}; Object.keys(set).forEach(function (id) { if (s[id]) k[id] = true; }); set = k; }
372
+ });
373
+ set = set || {};
374
+ if (neg) Object.keys(set).forEach(function (id) { exc[id] = true; });
375
+ else { inc = inc || {}; Object.keys(set).forEach(function (id) { inc[id] = true; }); }
376
+ });
377
+ return model.tables.filter(function (t) { return (inc === null || inc[t.id]) && !exc[t.id]; }).map(function (t) { return t.id; });
378
+ }
379
+
228
380
  root.qdbd = {
381
+ renderDrawio: renderDrawio,
382
+ select: select,
383
+ subset: subset,
229
384
  colors: C,
230
385
  geometry: geometry,
231
386
  renderTable: renderTable,
@@ -237,5 +392,7 @@
237
392
  esc: esc,
238
393
  };
239
394
  root.qdbdRenderSvg = renderSvg;
395
+ root.qdbdSelect = select;
396
+ root.qdbdRenderDrawio = renderDrawio;
240
397
  root.qdbdNeighbourhood = neighbourhood;
241
398
  })(typeof window !== "undefined" ? window : globalThis);