quickdbd-cli 0.1.3__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.
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/PKG-INFO +82 -21
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/README.md +81 -20
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/pyproject.toml +1 -1
- quickdbd_cli-0.1.4/src/quickdbd_cli/__init__.py +1 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/render.js +158 -1
- quickdbd_cli-0.1.4/src/quickdbd_cli/assets/style.css +167 -0
- quickdbd_cli-0.1.4/src/quickdbd_cli/assets/viewer.js +663 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/cli.py +106 -8
- quickdbd_cli-0.1.4/src/quickdbd_cli/diff.py +247 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/export.py +9 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/html.py +27 -4
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/measure.py +21 -1
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/project.py +42 -2
- quickdbd_cli-0.1.4/tests/test_diff.py +127 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/test_pipeline.py +51 -0
- quickdbd_cli-0.1.4/tests/test_select.py +143 -0
- quickdbd_cli-0.1.3/src/quickdbd_cli/__init__.py +0 -1
- quickdbd_cli-0.1.3/src/quickdbd_cli/assets/style.css +0 -92
- quickdbd_cli-0.1.3/src/quickdbd_cli/assets/viewer.js +0 -380
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/.gitignore +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/LICENSE +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/NOTICE +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/quickdbd-sample.txt +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/synthetic/finance.txt +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/synthetic/hr.txt +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/synthetic/inventory.txt +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/synthetic/marketing.txt +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/synthetic/qdbd.layout.json +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/synthetic/sales.txt +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/scripts/gen_font_metrics.py +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/scripts/gen_schema.py +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/scripts/release.sh +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/__main__.py +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-Bold.ttf +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-Bold.woff2 +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-LICENSE.txt +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-Regular.ttf +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-Regular.woff2 +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/editor.js +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/elk-LICENSE.md +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/elk.bundled.js +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/font-metrics.json +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/dbt.py +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/jsrt.py +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/layout.py +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/model.py +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/parser.py +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/serve.py +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/fixtures/jaffle/LICENSE +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/fixtures/jaffle/README.md +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/fixtures/jaffle/catalog.json +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/fixtures/jaffle/manifest.json +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/test_dbt.py +0 -0
- {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/test_parser.py +0 -0
- {quickdbd_cli-0.1.3 → 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.
|
|
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,19 +44,23 @@ 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
|
|
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.
|
|
@@ -82,13 +86,52 @@ qdbd build erd/ -o erd.html
|
|
|
82
86
|
|
|
83
87
|
Re-running the import overwrites the generated files. Table positions survive because they live in the layout file, keyed by table name.
|
|
84
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
|
+
|
|
85
128
|
## Commands
|
|
86
129
|
|
|
87
130
|
```bash
|
|
88
131
|
qdbd check PATH... [--strict] # validate; "No errors found. Good job!"
|
|
89
132
|
qdbd build PATH... -o diagram.html # shareable interactive HTML (positions locked)
|
|
90
133
|
qdbd serve PATH... [--port 8765] # live editor + drag-to-arrange
|
|
91
|
-
qdbd export PATH... -o out.png [--focus Customer --depth 1] # PNG or
|
|
134
|
+
qdbd export PATH... -o out.png [--focus Customer --depth 1] # PNG, SVG or .drawio
|
|
92
135
|
qdbd layout PATH... [--reset] # (re)write the layout file
|
|
93
136
|
qdbd import dbt target/manifest.json -o erd/ # generate schema files from a dbt project
|
|
94
137
|
```
|
|
@@ -123,27 +166,45 @@ Once a layout exists, a new table is placed next to the tables it's related to,
|
|
|
123
166
|
|
|
124
167
|
## CI
|
|
125
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
|
+
|
|
126
199
|
**pre-commit:**
|
|
127
200
|
|
|
128
201
|
```yaml
|
|
129
202
|
repos:
|
|
130
203
|
- repo: https://github.com/MarkPhamm/quickdbd-cli
|
|
131
|
-
rev: v0.1.
|
|
204
|
+
rev: v0.1.4
|
|
132
205
|
hooks:
|
|
133
206
|
- id: qdbd-check
|
|
134
|
-
files: ^
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
**GitHub Actions:**
|
|
138
|
-
|
|
139
|
-
```yaml
|
|
140
|
-
- uses: actions/setup-python@v5
|
|
141
|
-
with: { python-version: "3.12" }
|
|
142
|
-
- run: pip install quickdbd-cli
|
|
143
|
-
- run: qdbd check schema/
|
|
144
|
-
- run: qdbd build schema/ -o site/schema.html
|
|
145
|
-
- uses: actions/upload-artifact@v4
|
|
146
|
-
with: { name: schema-diagram, path: site/schema.html }
|
|
207
|
+
files: ^erd/.*\.txt$ # point at your schema files
|
|
147
208
|
```
|
|
148
209
|
|
|
149
210
|
**GitLab CI:**
|
|
@@ -153,10 +214,10 @@ erd:
|
|
|
153
214
|
image: python:3.12
|
|
154
215
|
script:
|
|
155
216
|
- pip install quickdbd-cli
|
|
156
|
-
- qdbd check
|
|
157
|
-
- qdbd build
|
|
217
|
+
- qdbd check erd/
|
|
218
|
+
- qdbd build erd/ -o public/index.html
|
|
158
219
|
artifacts:
|
|
159
|
-
paths: [public/
|
|
220
|
+
paths: [public/index.html]
|
|
160
221
|
```
|
|
161
222
|
|
|
162
223
|
## Development
|
|
@@ -19,19 +19,23 @@ pipx install quickdbd-cli # or: uvx --from quickdbd-cli qdbd ...
|
|
|
19
19
|
qdbd build schema.txt -o schema.html
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
**Live demo (200 tables):** https://markphamm.github.io/quickdbd-cli/
|
|
23
|
+
|
|
22
24
|
Not affiliated with QuickDBD. It reads the same text syntax, so schemas move freely between the two.
|
|
23
25
|
|
|
24
26
|
## Features
|
|
25
27
|
|
|
26
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.
|
|
27
29
|
- **Interactive page:**
|
|
28
|
-
- search
|
|
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
|
|
29
32
|
- minimap
|
|
30
33
|
- click a table to focus on it and its neighbours
|
|
31
34
|
- collapse tables: full / keys only / names only
|
|
32
35
|
- hover a column to highlight its relationship
|
|
33
36
|
- hover a dotted-underlined table or column to read its `#` doc comments
|
|
34
|
-
- PNG/SVG export buttons
|
|
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
|
|
35
39
|
- **Live editing:** `qdbd serve` reloads when you save, has a browser editor pane with autosave, and remembers where you drag tables.
|
|
36
40
|
- **Many files, one diagram:** point it at a folder and foreign keys can cross files.
|
|
37
41
|
- **Pip-only install:** no Graphviz, Node or browser needed. Layout uses [ELK](https://eclipse.dev/elk/), running in an embedded V8.
|
|
@@ -57,13 +61,52 @@ qdbd build erd/ -o erd.html
|
|
|
57
61
|
|
|
58
62
|
Re-running the import overwrites the generated files. Table positions survive because they live in the layout file, keyed by table name.
|
|
59
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
|
+
|
|
60
103
|
## Commands
|
|
61
104
|
|
|
62
105
|
```bash
|
|
63
106
|
qdbd check PATH... [--strict] # validate; "No errors found. Good job!"
|
|
64
107
|
qdbd build PATH... -o diagram.html # shareable interactive HTML (positions locked)
|
|
65
108
|
qdbd serve PATH... [--port 8765] # live editor + drag-to-arrange
|
|
66
|
-
qdbd export PATH... -o out.png [--focus Customer --depth 1] # PNG or
|
|
109
|
+
qdbd export PATH... -o out.png [--focus Customer --depth 1] # PNG, SVG or .drawio
|
|
67
110
|
qdbd layout PATH... [--reset] # (re)write the layout file
|
|
68
111
|
qdbd import dbt target/manifest.json -o erd/ # generate schema files from a dbt project
|
|
69
112
|
```
|
|
@@ -98,27 +141,45 @@ Once a layout exists, a new table is placed next to the tables it's related to,
|
|
|
98
141
|
|
|
99
142
|
## CI
|
|
100
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
|
+
|
|
101
174
|
**pre-commit:**
|
|
102
175
|
|
|
103
176
|
```yaml
|
|
104
177
|
repos:
|
|
105
178
|
- repo: https://github.com/MarkPhamm/quickdbd-cli
|
|
106
|
-
rev: v0.1.
|
|
179
|
+
rev: v0.1.4
|
|
107
180
|
hooks:
|
|
108
181
|
- id: qdbd-check
|
|
109
|
-
files: ^
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
**GitHub Actions:**
|
|
113
|
-
|
|
114
|
-
```yaml
|
|
115
|
-
- uses: actions/setup-python@v5
|
|
116
|
-
with: { python-version: "3.12" }
|
|
117
|
-
- run: pip install quickdbd-cli
|
|
118
|
-
- run: qdbd check schema/
|
|
119
|
-
- run: qdbd build schema/ -o site/schema.html
|
|
120
|
-
- uses: actions/upload-artifact@v4
|
|
121
|
-
with: { name: schema-diagram, path: site/schema.html }
|
|
182
|
+
files: ^erd/.*\.txt$ # point at your schema files
|
|
122
183
|
```
|
|
123
184
|
|
|
124
185
|
**GitLab CI:**
|
|
@@ -128,10 +189,10 @@ erd:
|
|
|
128
189
|
image: python:3.12
|
|
129
190
|
script:
|
|
130
191
|
- pip install quickdbd-cli
|
|
131
|
-
- qdbd check
|
|
132
|
-
- qdbd build
|
|
192
|
+
- qdbd check erd/
|
|
193
|
+
- qdbd build erd/ -o public/index.html
|
|
133
194
|
artifacts:
|
|
134
|
-
paths: [public/
|
|
195
|
+
paths: [public/index.html]
|
|
135
196
|
```
|
|
136
197
|
|
|
137
198
|
## Development
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quickdbd-cli"
|
|
7
|
-
version = "0.1.
|
|
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
|
-
|
|
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));
|
|
@@ -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);
|