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.
Files changed (55) hide show
  1. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/PKG-INFO +82 -21
  2. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/README.md +81 -20
  3. {quickdbd_cli-0.1.3 → 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.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/render.js +158 -1
  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.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/cli.py +106 -8
  9. quickdbd_cli-0.1.4/src/quickdbd_cli/diff.py +247 -0
  10. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/export.py +9 -0
  11. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/html.py +27 -4
  12. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/measure.py +21 -1
  13. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/project.py +42 -2
  14. quickdbd_cli-0.1.4/tests/test_diff.py +127 -0
  15. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/test_pipeline.py +51 -0
  16. quickdbd_cli-0.1.4/tests/test_select.py +143 -0
  17. quickdbd_cli-0.1.3/src/quickdbd_cli/__init__.py +0 -1
  18. quickdbd_cli-0.1.3/src/quickdbd_cli/assets/style.css +0 -92
  19. quickdbd_cli-0.1.3/src/quickdbd_cli/assets/viewer.js +0 -380
  20. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/.gitignore +0 -0
  21. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/LICENSE +0 -0
  22. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/NOTICE +0 -0
  23. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/quickdbd-sample.txt +0 -0
  24. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/synthetic/finance.txt +0 -0
  25. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/synthetic/hr.txt +0 -0
  26. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/synthetic/inventory.txt +0 -0
  27. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/synthetic/marketing.txt +0 -0
  28. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/synthetic/qdbd.layout.json +0 -0
  29. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/examples/synthetic/sales.txt +0 -0
  30. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/scripts/gen_font_metrics.py +0 -0
  31. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/scripts/gen_schema.py +0 -0
  32. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/scripts/release.sh +0 -0
  33. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/__main__.py +0 -0
  34. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-Bold.ttf +0 -0
  35. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-Bold.woff2 +0 -0
  36. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-LICENSE.txt +0 -0
  37. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-Regular.ttf +0 -0
  38. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/Inter-Regular.woff2 +0 -0
  39. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/editor.js +0 -0
  40. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/elk-LICENSE.md +0 -0
  41. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/elk.bundled.js +0 -0
  42. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/assets/font-metrics.json +0 -0
  43. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/dbt.py +0 -0
  44. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/jsrt.py +0 -0
  45. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/layout.py +0 -0
  46. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/model.py +0 -0
  47. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/parser.py +0 -0
  48. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/src/quickdbd_cli/serve.py +0 -0
  49. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/fixtures/jaffle/LICENSE +0 -0
  50. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/fixtures/jaffle/README.md +0 -0
  51. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/fixtures/jaffle/catalog.json +0 -0
  52. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/fixtures/jaffle/manifest.json +0 -0
  53. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/test_dbt.py +0 -0
  54. {quickdbd_cli-0.1.3 → quickdbd_cli-0.1.4}/tests/test_parser.py +0 -0
  55. {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
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 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.
@@ -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 SVG image
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.0
204
+ rev: v0.1.4
132
205
  hooks:
133
206
  - id: qdbd-check
134
- files: ^schema/.*\.txt$ # point at your schema 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 schema/
157
- - qdbd build schema/ -o public/schema.html
217
+ - qdbd check erd/
218
+ - qdbd build erd/ -o public/index.html
158
219
  artifacts:
159
- paths: [public/schema.html]
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 and jump to a table (`/`)
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 SVG image
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.0
179
+ rev: v0.1.4
107
180
  hooks:
108
181
  - id: qdbd-check
109
- files: ^schema/.*\.txt$ # point at your schema 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 schema/
132
- - qdbd build schema/ -o public/schema.html
192
+ - qdbd check erd/
193
+ - qdbd build erd/ -o public/index.html
133
194
  artifacts:
134
- paths: [public/schema.html]
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.3"
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));
@@ -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);