erdscope 0.4.1__tar.gz → 0.6.0__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 (25) hide show
  1. {erdscope-0.4.1/erdscope.egg-info → erdscope-0.6.0}/PKG-INFO +176 -8
  2. {erdscope-0.4.1 → erdscope-0.6.0}/README.md +175 -7
  3. {erdscope-0.4.1 → erdscope-0.6.0}/erd.py +1808 -101
  4. {erdscope-0.4.1 → erdscope-0.6.0/erdscope.egg-info}/PKG-INFO +176 -8
  5. {erdscope-0.4.1 → erdscope-0.6.0}/erdscope.egg-info/SOURCES.txt +5 -1
  6. {erdscope-0.4.1 → erdscope-0.6.0}/pyproject.toml +1 -1
  7. {erdscope-0.4.1 → erdscope-0.6.0}/tests/test_config_validation.py +481 -0
  8. {erdscope-0.4.1 → erdscope-0.6.0}/tests/test_e2e.py +521 -0
  9. {erdscope-0.4.1 → erdscope-0.6.0}/tests/test_erd.py +116 -0
  10. erdscope-0.6.0/tests/test_groups.py +325 -0
  11. erdscope-0.6.0/tests/test_notes.py +556 -0
  12. {erdscope-0.4.1 → erdscope-0.6.0}/tests/test_pipeline.py +188 -0
  13. erdscope-0.6.0/tests/test_provider_contract.py +221 -0
  14. erdscope-0.6.0/tests/test_rails_schema.py +900 -0
  15. {erdscope-0.4.1 → erdscope-0.6.0}/LICENSE +0 -0
  16. {erdscope-0.4.1 → erdscope-0.6.0}/erdscope.egg-info/dependency_links.txt +0 -0
  17. {erdscope-0.4.1 → erdscope-0.6.0}/erdscope.egg-info/entry_points.txt +0 -0
  18. {erdscope-0.4.1 → erdscope-0.6.0}/erdscope.egg-info/requires.txt +0 -0
  19. {erdscope-0.4.1 → erdscope-0.6.0}/erdscope.egg-info/top_level.txt +0 -0
  20. {erdscope-0.4.1 → erdscope-0.6.0}/setup.cfg +0 -0
  21. {erdscope-0.4.1 → erdscope-0.6.0}/tests/test_build.py +0 -0
  22. {erdscope-0.4.1 → erdscope-0.6.0}/tests/test_characterization.py +0 -0
  23. {erdscope-0.4.1 → erdscope-0.6.0}/tests/test_db_integration.py +0 -0
  24. {erdscope-0.4.1 → erdscope-0.6.0}/tests/test_demo.py +0 -0
  25. {erdscope-0.4.1 → erdscope-0.6.0}/tests/test_merge_ir.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: erdscope
3
- Version: 0.4.1
3
+ Version: 0.6.0
4
4
  Summary: Interactive, self-contained ER-diagram HTML and Excel table definitions from a MySQL, PostgreSQL, or SQLite database and/or application code (Rails / Prisma / Django) — single file, zero required dependencies
5
5
  Author: tas6
6
6
  License-Expression: MIT
@@ -37,9 +37,15 @@ Dynamic: license-file
37
37
  [![CI](https://github.com/orapli/erdscope/actions/workflows/ci.yml/badge.svg)](https://github.com/orapli/erdscope/actions/workflows/ci.yml)
38
38
  [![PyPI](https://img.shields.io/pypi/v/erdscope)](https://pypi.org/project/erdscope/)
39
39
 
40
- Generate a **self-contained, interactive ER diagram** and an **Excel table-definition
40
+ **Interactive ER diagrams and documented schema definitions.** Generate a
41
+ **self-contained, interactive ER diagram** — and an **Excel table-definition
41
42
  workbook** — from a live MySQL, PostgreSQL, or SQLite database, with a single-file,
42
- zero-dependency Python CLI.
43
+ zero-dependency Python CLI. Turn an ER diagram into a documented schema: attach design
44
+ decisions, operational rules, and ADR links to tables and relationships with config
45
+ [`notes:`](#notes-attach-design-decisions-to-the-diagram) — plain text and http(s) links
46
+ only, validated against the real schema so a note can never point at something that
47
+ doesn't exist — and draw a rounded, titled frame around a set of related tables with config
48
+ [`groups:`](#groups-draw-a-frame-around-related-tables).
43
49
 
44
50
  ```bash
45
51
  pip install erdscope
@@ -54,9 +60,10 @@ erdscope sqlite:///path/to/app.db -o erd.html
54
60
 
55
61
  - **Database** (MySQL / PostgreSQL / SQLite) — the source of truth for tables, columns,
56
62
  comments, indexes, and real foreign keys.
57
- - **Application code** (`--models`: Rails / Prisma / Django) adds association semantics
58
- the database cannot express (`has_many :through`, polymorphic, ...), and can stand on
59
- its own when there is no DB to point at.
63
+ - **Application code** (`--models`: Rails / Prisma / Django, or a Rails `db/schema.rb` —
64
+ see [Typed input sources](#typed-input-sources-sources) below) adds association
65
+ semantics the database cannot express (`has_many :through`, polymorphic, ...), and can
66
+ stand on its own when there is no DB to point at.
60
67
  - **Config file** (`tables:`) — declare or patch a schema by hand: add tables, columns,
61
68
  indexes, and associations, or override and delete what the DB or code got wrong.
62
69
 
@@ -225,6 +232,138 @@ something that actually exists once all sources are merged) — typos never pass
225
232
  The full `tables:` schema, the `drop`/`replace` operations, and the precedence rules are
226
233
  documented in the [manual](https://orapli.github.io/erdscope/manual.html#config-file).
227
234
 
235
+ ### Typed input sources (`sources:`)
236
+
237
+ `--models`/config `models` auto-detect what kind of project a path is. The config-only
238
+ **`sources:`** list is the typed alternative: each entry names its own `type`, so nothing
239
+ needs detecting and several inputs can be declared explicitly and unambiguously —
240
+ including **`rails.schema`**, which statically parses a Rails `db/schema.rb` file
241
+ (columns, indexes, real foreign keys) with **no live database and no Ruby execution**:
242
+
243
+ ```yaml
244
+ version: 1
245
+ sources:
246
+ - id: schema
247
+ type: rails.schema
248
+ path: db/schema.rb
249
+ - id: app
250
+ type: rails.models # every registered overlay gets its own <name>.models type
251
+ path: app/models
252
+ ```
253
+
254
+ (The optional top-level `version: 1` is a config-format marker — currently the only
255
+ supported value, with no other effect; it's for future config-format changes to key off.)
256
+
257
+ A typed source that parses **nothing** — say, a Prisma project accidentally declared as
258
+ `rails.models` — is a hard error naming the source id and the layout the type expected,
259
+ never a silently empty diagram. If an empty result is genuinely intended (a scaffolded
260
+ but still-empty `app/models`, for example), opt in per source with `allow_empty: true`;
261
+ a `rails.project` entry passes the flag down to both of its expanded halves.
262
+
263
+ A **`rails.project`** entry is a macro for a whole Rails app root: it expands to both the
264
+ `rails.schema` (`<root>/db/schema.rb`) and `rails.models` (`<root>/app/models`) halves,
265
+ whichever exist —
266
+
267
+ ```yaml
268
+ sources:
269
+ - id: app
270
+ type: rails.project
271
+ path: ../myapp
272
+ ```
273
+
274
+ is equivalent to declaring both `rails.schema` and `rails.models` entries above. A
275
+ `schema.rb`-derived foreign key merges as a **schema FK** (a teal badge in the viewer,
276
+ distinct from a live `DB FK`), and the merge authority order extends to
277
+ **Config > live DB > rails.schema > models** (physical facts) — a `schema.rb` dump is
278
+ closer to the real database than code is, but a live DB read still wins when both are
279
+ present. See the [Input sources chapter of the manual](https://orapli.github.io/erdscope/manual.html#input-sources)
280
+ for the full `sources[]` reference.
281
+
282
+ ### Notes: attach design decisions to the diagram
283
+
284
+ Config **`notes:`** attaches short, plain-text write-ups — design decisions, operational
285
+ rules, ADR links — to a table, a specific relation, or the whole diagram. Notes never
286
+ touch the schema itself (no effect on columns, associations, or merge precedence); they're
287
+ a read-only sidecar validated against the final merged schema and rendered next to it:
288
+
289
+ ```yaml
290
+ notes:
291
+ - id: user-retention
292
+ target: { type: table, table: users }
293
+ title: Retention policy
294
+ text: Suspended accounts are kept for 1 year, then anonymized.
295
+ links:
296
+ - { label: ADR-004, url: https://example.com/adr/004 }
297
+
298
+ - id: order-ownership
299
+ target: { type: relation, source_table: orders, target_table: users, foreign_key: user_id }
300
+ text: Orders are kept after a user is anonymized (financial record-keeping).
301
+
302
+ - id: diagram-conventions
303
+ target: { type: global }
304
+ title: How to read this diagram
305
+ text: The dotted amber edge is an inferred relation, not a real FK.
306
+ ```
307
+
308
+ - Every note needs a config-unique `id` and non-empty `text`; `title` and `links` are
309
+ optional. Link `url`s must be `http://` or `https://` — anything else (`javascript:`,
310
+ `data:`, a bare string) is rejected at load time.
311
+ - `target.type` is `table`, `relation`, or `global`. A `relation` note is identified by
312
+ `source_table` (the side that **holds** the association — the `belongs_to`/FK-holding
313
+ side for a `belongs_to`, the owning side for a `has_many`) and `target_table`;
314
+ `foreign_key`/`name`/`assoc_type`/`through`/`polymorphic` are optional narrowing keys for
315
+ when a table has more than one relation to the same target (`assoc_type` is the
316
+ association kind — `has_many`/`belongs_to`/`has_one`/`has_and_belongs_to_many` — which
317
+ tells apart, say, a `has_many` and a `has_one` that share a name and target). A note that
318
+ matches no relation, or more than one, is a hard error naming the note's `id` — never a
319
+ silent guess.
320
+ - Validation runs **twice**, like `tables:` above: syntax at load time, then semantically
321
+ against the schema that results **after** DB/`--models`/config `tables:` are all merged
322
+ — so a note can reference a table or relation that config itself adds, and a note on
323
+ something config *removes* is correctly an error.
324
+ - Rendered as plain, HTML-escaped text only — no Markdown, no raw HTML, no scripts — in
325
+ the table's detail panel, next to the matching relation, or in the diagram's legend for
326
+ a `global` note. Notes are also searchable (title, text, link labels). A note whose
327
+ table is hidden from the current view simply doesn't appear in the (now absent) detail
328
+ panel; a `global` note's legend entry is unaffected.
329
+ - `write_excel` accepts the same notes but doesn't use them yet in this release — an Excel
330
+ Notes sheet is a possible future addition, not implemented today.
331
+
332
+ See the [Notes chapter of the manual](https://orapli.github.io/erdscope/manual.html#notes)
333
+ for the full reference.
334
+
335
+ ### Groups: draw a frame around related tables
336
+
337
+ Config **`groups:`** draws a rounded frame with a title around a set of related tables in
338
+ the diagram — a lightweight way to call out a domain ("Billing", "Orders") without changing
339
+ the schema or the layout. Like `notes:`, it's a read-only sidecar validated against the
340
+ final merged schema:
341
+
342
+ ```yaml
343
+ groups:
344
+ - id: billing
345
+ title: Billing
346
+ tables: [invoices, payments, coupons]
347
+ color: "#0d9488"
348
+
349
+ - id: catalog
350
+ tables: [products, categories, product_categories]
351
+ ```
352
+
353
+ - Every group needs a config-unique `id` and non-empty `tables`; `title` (defaults to `id`)
354
+ and `color` (a hex string, e.g. `#0d9488`) are optional.
355
+ - A table may belong to **at most one** group — claiming the same table from two groups is
356
+ a hard error naming both group `id`s and the table, not a silently-picked winner. There's
357
+ no support for overlapping/nested groups in this release.
358
+ - Validation runs twice, like `notes:`/`tables:` above: syntax at load time, then
359
+ semantically against the final merged schema, so a group can reference a table config
360
+ itself adds, and a group naming something config *removes* is correctly an error.
361
+ - Purely visual: groups never affect layout, merge precedence, or associations — the frame
362
+ is drawn around wherever its member tables already ended up. In the viewer, drag a
363
+ group's title to move every member together; a "Groups" toolbar toggle shows/hides the
364
+ frames, and both PNG and SVG exports include them. `--only`/`--exclude` narrow a group's
365
+ membership down to the surviving tables, dropping the group entirely if none remain.
366
+
228
367
  ## Dependencies
229
368
 
230
369
  `erd.py` runs with **zero required dependencies** — everything below is optional, and
@@ -250,6 +389,24 @@ Test-only, and only if you run that particular suite:
250
389
  | [openpyxl](https://pypi.org/project/openpyxl/) | Roundtrip-verifying `--excel` output in the unit tests (`tests/test_erd.py`); that one test skips itself if it's missing |
251
390
  | [Playwright](https://playwright.dev/python/) | The browser E2E suite (`tests/test_e2e.py`) — see [Tests](#tests) below |
252
391
 
392
+ ## Verified versions
393
+
394
+ The input formats erdscope parses change rarely, so exact versions matter less than
395
+ they might seem — newer releases are expected to keep working, though constructs
396
+ beyond the tested ones may be ignored rather than parsed. For the record, this is what
397
+ each input is actually verified against:
398
+
399
+ | Input | Verified against |
400
+ |---|---|
401
+ | MySQL | **8.4** — real-server integration tests in CI (`information_schema`); developed against 8.x |
402
+ | PostgreSQL | **16** — real-server integration tests in CI (`pg_catalog`/`information_schema`) |
403
+ | SQLite | the `sqlite3` module bundled with CPython (any supported 3.x) |
404
+ | Rails `schema.rb` | the format Rails **7.x / 8.x** writes (`ActiveRecord::Schema[7.x]`, and the classic un-versioned header) |
405
+ | Rails models | the association DSL as of Rails **7.x** — `has_many`/`has_one`/`belongs_to`/`has_and_belongs_to_many`, `through:`, `polymorphic:`, STI, concerns, custom base classes; also exercised against Mastodon's real codebase. Dynamically computed definitions (and `structure.sql`) are out of scope |
406
+ | Prisma | the schema language as of Prisma **5 / 6** — `@map`/`@@map`, enums, named relations, implicit and explicit m2m, self-relations, composite `@@id`/`@@unique`, `@@schema` |
407
+ | Django | models as of Django **4.2 / 5.x** — FK / OneToOne / M2M (incl. `through=`), abstract bases, `db_table`/`db_column`, `GenericForeignKey` (kept as a polymorphic marker); a swappable `AUTH_USER_MODEL` FK keeps its column and skips the edge |
408
+ | Python | 3.9+ (`requires-python`); CI runs the latest CPython 3.x |
409
+
253
410
  ## What you get
254
411
 
255
412
  Feature highlights — each link goes to the relevant [manual](https://orapli.github.io/erdscope/manual.html) chapter:
@@ -258,7 +415,8 @@ Feature highlights — each link goes to the relevant [manual](https://orapli.gi
258
415
  column comments, indexes, and real FK constraints, read from the database catalog
259
416
  (`information_schema` on MySQL, `pg_catalog` on PostgreSQL)
260
417
  - **Code semantics on top** — `--models` merges Rails / Prisma / Django associations;
261
- declared, DB-FK, and inferred edges stay [visually distinct](https://orapli.github.io/erdscope/manual.html#viewer-edges)
418
+ declared, DB-FK, schema FK (a statically-parsed `rails.schema` foreign key), and
419
+ inferred edges stay [visually distinct](https://orapli.github.io/erdscope/manual.html#viewer-edges)
262
420
  - **[Interactive exploration](https://orapli.github.io/erdscope/manual.html#viewer-guide)** — focus with depth and dependency
263
421
  direction, two-level hiding, table *and column* search (with regex/case toggles), a
264
422
  non-filtering Highlight search that survives into exports, named views, share links
@@ -273,6 +431,12 @@ Feature highlights — each link goes to the relevant [manual](https://orapli.gi
273
431
  the live view and exports
274
432
  - **Extras** — a built-in `?` shortcuts/help popup, dark mode, print stylesheet,
275
433
  resizable/collapsible panes
434
+ - **[Notes](#notes-attach-design-decisions-to-the-diagram)** — attach design decisions,
435
+ operational rules, and ADR links to a table, a relation, or the whole diagram, validated
436
+ against the real schema and searchable alongside tables/columns
437
+ - **[Groups](#groups-draw-a-frame-around-related-tables)** — draw a rounded, titled frame
438
+ around a set of related tables to call out a domain, purely visual, draggable by its
439
+ title, with its own toolbar toggle and export support
276
440
 
277
441
  ## Tests
278
442
 
@@ -281,7 +445,11 @@ python3 -m unittest discover -s tests -v
281
445
  ```
282
446
 
283
447
  The IR builders and the Excel writer are covered by pure unit tests; the overlay
284
- parsers have minimal fixtures under `tests/fixture_*`. No database is required to
448
+ parsers have fixtures under `tests/fixture_*` basic plus advanced constructs per
449
+ framework — and every input provider is additionally held to one shared contract
450
+ (`tests/test_provider_contract.py`): typed dispatch, standalone HTML/Excel generation,
451
+ merging over a DB layer, 1:N / 1:1 / M:N / self-reference, association provenance, and
452
+ empty-input diagnostics, all over the same small domain. No database is required to
285
453
  run the tests.
286
454
 
287
455
  `tests/test_e2e.py` drives the generated HTML's client-side JS (grid layout,
@@ -3,9 +3,15 @@
3
3
  [![CI](https://github.com/orapli/erdscope/actions/workflows/ci.yml/badge.svg)](https://github.com/orapli/erdscope/actions/workflows/ci.yml)
4
4
  [![PyPI](https://img.shields.io/pypi/v/erdscope)](https://pypi.org/project/erdscope/)
5
5
 
6
- Generate a **self-contained, interactive ER diagram** and an **Excel table-definition
6
+ **Interactive ER diagrams and documented schema definitions.** Generate a
7
+ **self-contained, interactive ER diagram** — and an **Excel table-definition
7
8
  workbook** — from a live MySQL, PostgreSQL, or SQLite database, with a single-file,
8
- zero-dependency Python CLI.
9
+ zero-dependency Python CLI. Turn an ER diagram into a documented schema: attach design
10
+ decisions, operational rules, and ADR links to tables and relationships with config
11
+ [`notes:`](#notes-attach-design-decisions-to-the-diagram) — plain text and http(s) links
12
+ only, validated against the real schema so a note can never point at something that
13
+ doesn't exist — and draw a rounded, titled frame around a set of related tables with config
14
+ [`groups:`](#groups-draw-a-frame-around-related-tables).
9
15
 
10
16
  ```bash
11
17
  pip install erdscope
@@ -20,9 +26,10 @@ erdscope sqlite:///path/to/app.db -o erd.html
20
26
 
21
27
  - **Database** (MySQL / PostgreSQL / SQLite) — the source of truth for tables, columns,
22
28
  comments, indexes, and real foreign keys.
23
- - **Application code** (`--models`: Rails / Prisma / Django) adds association semantics
24
- the database cannot express (`has_many :through`, polymorphic, ...), and can stand on
25
- its own when there is no DB to point at.
29
+ - **Application code** (`--models`: Rails / Prisma / Django, or a Rails `db/schema.rb` —
30
+ see [Typed input sources](#typed-input-sources-sources) below) adds association
31
+ semantics the database cannot express (`has_many :through`, polymorphic, ...), and can
32
+ stand on its own when there is no DB to point at.
26
33
  - **Config file** (`tables:`) — declare or patch a schema by hand: add tables, columns,
27
34
  indexes, and associations, or override and delete what the DB or code got wrong.
28
35
 
@@ -191,6 +198,138 @@ something that actually exists once all sources are merged) — typos never pass
191
198
  The full `tables:` schema, the `drop`/`replace` operations, and the precedence rules are
192
199
  documented in the [manual](https://orapli.github.io/erdscope/manual.html#config-file).
193
200
 
201
+ ### Typed input sources (`sources:`)
202
+
203
+ `--models`/config `models` auto-detect what kind of project a path is. The config-only
204
+ **`sources:`** list is the typed alternative: each entry names its own `type`, so nothing
205
+ needs detecting and several inputs can be declared explicitly and unambiguously —
206
+ including **`rails.schema`**, which statically parses a Rails `db/schema.rb` file
207
+ (columns, indexes, real foreign keys) with **no live database and no Ruby execution**:
208
+
209
+ ```yaml
210
+ version: 1
211
+ sources:
212
+ - id: schema
213
+ type: rails.schema
214
+ path: db/schema.rb
215
+ - id: app
216
+ type: rails.models # every registered overlay gets its own <name>.models type
217
+ path: app/models
218
+ ```
219
+
220
+ (The optional top-level `version: 1` is a config-format marker — currently the only
221
+ supported value, with no other effect; it's for future config-format changes to key off.)
222
+
223
+ A typed source that parses **nothing** — say, a Prisma project accidentally declared as
224
+ `rails.models` — is a hard error naming the source id and the layout the type expected,
225
+ never a silently empty diagram. If an empty result is genuinely intended (a scaffolded
226
+ but still-empty `app/models`, for example), opt in per source with `allow_empty: true`;
227
+ a `rails.project` entry passes the flag down to both of its expanded halves.
228
+
229
+ A **`rails.project`** entry is a macro for a whole Rails app root: it expands to both the
230
+ `rails.schema` (`<root>/db/schema.rb`) and `rails.models` (`<root>/app/models`) halves,
231
+ whichever exist —
232
+
233
+ ```yaml
234
+ sources:
235
+ - id: app
236
+ type: rails.project
237
+ path: ../myapp
238
+ ```
239
+
240
+ is equivalent to declaring both `rails.schema` and `rails.models` entries above. A
241
+ `schema.rb`-derived foreign key merges as a **schema FK** (a teal badge in the viewer,
242
+ distinct from a live `DB FK`), and the merge authority order extends to
243
+ **Config > live DB > rails.schema > models** (physical facts) — a `schema.rb` dump is
244
+ closer to the real database than code is, but a live DB read still wins when both are
245
+ present. See the [Input sources chapter of the manual](https://orapli.github.io/erdscope/manual.html#input-sources)
246
+ for the full `sources[]` reference.
247
+
248
+ ### Notes: attach design decisions to the diagram
249
+
250
+ Config **`notes:`** attaches short, plain-text write-ups — design decisions, operational
251
+ rules, ADR links — to a table, a specific relation, or the whole diagram. Notes never
252
+ touch the schema itself (no effect on columns, associations, or merge precedence); they're
253
+ a read-only sidecar validated against the final merged schema and rendered next to it:
254
+
255
+ ```yaml
256
+ notes:
257
+ - id: user-retention
258
+ target: { type: table, table: users }
259
+ title: Retention policy
260
+ text: Suspended accounts are kept for 1 year, then anonymized.
261
+ links:
262
+ - { label: ADR-004, url: https://example.com/adr/004 }
263
+
264
+ - id: order-ownership
265
+ target: { type: relation, source_table: orders, target_table: users, foreign_key: user_id }
266
+ text: Orders are kept after a user is anonymized (financial record-keeping).
267
+
268
+ - id: diagram-conventions
269
+ target: { type: global }
270
+ title: How to read this diagram
271
+ text: The dotted amber edge is an inferred relation, not a real FK.
272
+ ```
273
+
274
+ - Every note needs a config-unique `id` and non-empty `text`; `title` and `links` are
275
+ optional. Link `url`s must be `http://` or `https://` — anything else (`javascript:`,
276
+ `data:`, a bare string) is rejected at load time.
277
+ - `target.type` is `table`, `relation`, or `global`. A `relation` note is identified by
278
+ `source_table` (the side that **holds** the association — the `belongs_to`/FK-holding
279
+ side for a `belongs_to`, the owning side for a `has_many`) and `target_table`;
280
+ `foreign_key`/`name`/`assoc_type`/`through`/`polymorphic` are optional narrowing keys for
281
+ when a table has more than one relation to the same target (`assoc_type` is the
282
+ association kind — `has_many`/`belongs_to`/`has_one`/`has_and_belongs_to_many` — which
283
+ tells apart, say, a `has_many` and a `has_one` that share a name and target). A note that
284
+ matches no relation, or more than one, is a hard error naming the note's `id` — never a
285
+ silent guess.
286
+ - Validation runs **twice**, like `tables:` above: syntax at load time, then semantically
287
+ against the schema that results **after** DB/`--models`/config `tables:` are all merged
288
+ — so a note can reference a table or relation that config itself adds, and a note on
289
+ something config *removes* is correctly an error.
290
+ - Rendered as plain, HTML-escaped text only — no Markdown, no raw HTML, no scripts — in
291
+ the table's detail panel, next to the matching relation, or in the diagram's legend for
292
+ a `global` note. Notes are also searchable (title, text, link labels). A note whose
293
+ table is hidden from the current view simply doesn't appear in the (now absent) detail
294
+ panel; a `global` note's legend entry is unaffected.
295
+ - `write_excel` accepts the same notes but doesn't use them yet in this release — an Excel
296
+ Notes sheet is a possible future addition, not implemented today.
297
+
298
+ See the [Notes chapter of the manual](https://orapli.github.io/erdscope/manual.html#notes)
299
+ for the full reference.
300
+
301
+ ### Groups: draw a frame around related tables
302
+
303
+ Config **`groups:`** draws a rounded frame with a title around a set of related tables in
304
+ the diagram — a lightweight way to call out a domain ("Billing", "Orders") without changing
305
+ the schema or the layout. Like `notes:`, it's a read-only sidecar validated against the
306
+ final merged schema:
307
+
308
+ ```yaml
309
+ groups:
310
+ - id: billing
311
+ title: Billing
312
+ tables: [invoices, payments, coupons]
313
+ color: "#0d9488"
314
+
315
+ - id: catalog
316
+ tables: [products, categories, product_categories]
317
+ ```
318
+
319
+ - Every group needs a config-unique `id` and non-empty `tables`; `title` (defaults to `id`)
320
+ and `color` (a hex string, e.g. `#0d9488`) are optional.
321
+ - A table may belong to **at most one** group — claiming the same table from two groups is
322
+ a hard error naming both group `id`s and the table, not a silently-picked winner. There's
323
+ no support for overlapping/nested groups in this release.
324
+ - Validation runs twice, like `notes:`/`tables:` above: syntax at load time, then
325
+ semantically against the final merged schema, so a group can reference a table config
326
+ itself adds, and a group naming something config *removes* is correctly an error.
327
+ - Purely visual: groups never affect layout, merge precedence, or associations — the frame
328
+ is drawn around wherever its member tables already ended up. In the viewer, drag a
329
+ group's title to move every member together; a "Groups" toolbar toggle shows/hides the
330
+ frames, and both PNG and SVG exports include them. `--only`/`--exclude` narrow a group's
331
+ membership down to the surviving tables, dropping the group entirely if none remain.
332
+
194
333
  ## Dependencies
195
334
 
196
335
  `erd.py` runs with **zero required dependencies** — everything below is optional, and
@@ -216,6 +355,24 @@ Test-only, and only if you run that particular suite:
216
355
  | [openpyxl](https://pypi.org/project/openpyxl/) | Roundtrip-verifying `--excel` output in the unit tests (`tests/test_erd.py`); that one test skips itself if it's missing |
217
356
  | [Playwright](https://playwright.dev/python/) | The browser E2E suite (`tests/test_e2e.py`) — see [Tests](#tests) below |
218
357
 
358
+ ## Verified versions
359
+
360
+ The input formats erdscope parses change rarely, so exact versions matter less than
361
+ they might seem — newer releases are expected to keep working, though constructs
362
+ beyond the tested ones may be ignored rather than parsed. For the record, this is what
363
+ each input is actually verified against:
364
+
365
+ | Input | Verified against |
366
+ |---|---|
367
+ | MySQL | **8.4** — real-server integration tests in CI (`information_schema`); developed against 8.x |
368
+ | PostgreSQL | **16** — real-server integration tests in CI (`pg_catalog`/`information_schema`) |
369
+ | SQLite | the `sqlite3` module bundled with CPython (any supported 3.x) |
370
+ | Rails `schema.rb` | the format Rails **7.x / 8.x** writes (`ActiveRecord::Schema[7.x]`, and the classic un-versioned header) |
371
+ | Rails models | the association DSL as of Rails **7.x** — `has_many`/`has_one`/`belongs_to`/`has_and_belongs_to_many`, `through:`, `polymorphic:`, STI, concerns, custom base classes; also exercised against Mastodon's real codebase. Dynamically computed definitions (and `structure.sql`) are out of scope |
372
+ | Prisma | the schema language as of Prisma **5 / 6** — `@map`/`@@map`, enums, named relations, implicit and explicit m2m, self-relations, composite `@@id`/`@@unique`, `@@schema` |
373
+ | Django | models as of Django **4.2 / 5.x** — FK / OneToOne / M2M (incl. `through=`), abstract bases, `db_table`/`db_column`, `GenericForeignKey` (kept as a polymorphic marker); a swappable `AUTH_USER_MODEL` FK keeps its column and skips the edge |
374
+ | Python | 3.9+ (`requires-python`); CI runs the latest CPython 3.x |
375
+
219
376
  ## What you get
220
377
 
221
378
  Feature highlights — each link goes to the relevant [manual](https://orapli.github.io/erdscope/manual.html) chapter:
@@ -224,7 +381,8 @@ Feature highlights — each link goes to the relevant [manual](https://orapli.gi
224
381
  column comments, indexes, and real FK constraints, read from the database catalog
225
382
  (`information_schema` on MySQL, `pg_catalog` on PostgreSQL)
226
383
  - **Code semantics on top** — `--models` merges Rails / Prisma / Django associations;
227
- declared, DB-FK, and inferred edges stay [visually distinct](https://orapli.github.io/erdscope/manual.html#viewer-edges)
384
+ declared, DB-FK, schema FK (a statically-parsed `rails.schema` foreign key), and
385
+ inferred edges stay [visually distinct](https://orapli.github.io/erdscope/manual.html#viewer-edges)
228
386
  - **[Interactive exploration](https://orapli.github.io/erdscope/manual.html#viewer-guide)** — focus with depth and dependency
229
387
  direction, two-level hiding, table *and column* search (with regex/case toggles), a
230
388
  non-filtering Highlight search that survives into exports, named views, share links
@@ -239,6 +397,12 @@ Feature highlights — each link goes to the relevant [manual](https://orapli.gi
239
397
  the live view and exports
240
398
  - **Extras** — a built-in `?` shortcuts/help popup, dark mode, print stylesheet,
241
399
  resizable/collapsible panes
400
+ - **[Notes](#notes-attach-design-decisions-to-the-diagram)** — attach design decisions,
401
+ operational rules, and ADR links to a table, a relation, or the whole diagram, validated
402
+ against the real schema and searchable alongside tables/columns
403
+ - **[Groups](#groups-draw-a-frame-around-related-tables)** — draw a rounded, titled frame
404
+ around a set of related tables to call out a domain, purely visual, draggable by its
405
+ title, with its own toolbar toggle and export support
242
406
 
243
407
  ## Tests
244
408
 
@@ -247,7 +411,11 @@ python3 -m unittest discover -s tests -v
247
411
  ```
248
412
 
249
413
  The IR builders and the Excel writer are covered by pure unit tests; the overlay
250
- parsers have minimal fixtures under `tests/fixture_*`. No database is required to
414
+ parsers have fixtures under `tests/fixture_*` basic plus advanced constructs per
415
+ framework — and every input provider is additionally held to one shared contract
416
+ (`tests/test_provider_contract.py`): typed dispatch, standalone HTML/Excel generation,
417
+ merging over a DB layer, 1:N / 1:1 / M:N / self-reference, association provenance, and
418
+ empty-input diagnostics, all over the same small domain. No database is required to
251
419
  run the tests.
252
420
 
253
421
  `tests/test_e2e.py` drives the generated HTML's client-side JS (grid layout,