|
| 1 | +--- |
| 2 | +ai_summary: "Fix DiagramView union semantics: always expand TableGroups wildcard to concrete names, rewrite syncDiagramView generation to follow 6 rules from filter-dbml-examples.md" |
| 3 | +ai_warnings: |
| 4 | + - "Only dbml changes needed — no dbx-utils changes" |
| 5 | +ai_generated_at: "2026-04-15" |
| 6 | +attached_repos: |
| 7 | + - dbml |
| 8 | +--- |
| 9 | + |
| 10 | +# Solutions: DiagramView Union Semantics Fix |
| 11 | + |
| 12 | +## Overview |
| 13 | + |
| 14 | +```mermaid |
| 15 | +flowchart LR |
| 16 | + subgraph "syncDiagramView (generation)" |
| 17 | + A["UI FilterConfig"] -->|"6 rules"| B["DBML: correct representation"] |
| 18 | + end |
| 19 | +
|
| 20 | + subgraph "Parser (interpretation)" |
| 21 | + B -->|"Trinity omit rule"| C["omitted dims → [] (show all)"] |
| 22 | + B -->|"wildcard expansion"| D["TableGroups: * → concrete names"] |
| 23 | + B -->|"body-level * → all []"| E["no expansion needed"] |
| 24 | + end |
| 25 | +
|
| 26 | + subgraph "filterNormalizedModel (rendering)" |
| 27 | + C --> F["[] treated as show-all"] |
| 28 | + D --> G["concrete names → name lookup works"] |
| 29 | + F --> H["Union: all matching tables"] |
| 30 | + G --> H |
| 31 | + end |
| 32 | +``` |
| 33 | + |
| 34 | +Two changes, both in dbml, working together: |
| 35 | + |
| 36 | +1. **syncDiagramView** — follows 6 generation rules to produce correct DBML. Handles legacy null cases and normal cases via omit-empty and union rules. Never emits unnecessary `*` for dimensions the user didn't filter. |
| 37 | + |
| 38 | +2. **expandDiagramViewWildcards** — for human-written DBML with `TableGroups: *`, always expands to concrete group names regardless of sibling dims. Body-level `{ * }` is unaffected. |
| 39 | + |
| 40 | +3. **No dbx-utils changes needed** — expanding TableGroups `*` to concrete names gives `filterNormalizedModel` non-empty arrays, so the existing `hasTableGroupFilters` check correctly returns `true`. |
| 41 | + |
| 42 | +### Round-trip Truth Tables |
| 43 | + |
| 44 | +> Shorthand: `(T, TG, S, N)` = FilterConfig. `n` = null, `[]` = empty array, `[x]` = has items. |
| 45 | +> Delta column shows what changed in round-trip (blank = 1:1). |
| 46 | +
|
| 47 | +#### Table 1: FilterConfig → DBML → FilterConfig (Generation Stability) |
| 48 | + |
| 49 | +Tests: does `generate(filterConfig)` produce DBML that re-parses back to the same FilterConfig? |
| 50 | + |
| 51 | +| Case | Input FC | → DBML | → FC (re-parse) | Delta | |
| 52 | +|------|----------|--------|-----------------|-------| |
| 53 | +| **A1** | `n, n, n, n` | `{ }` | `n, n, n, n` | — | |
| 54 | +| **A2** | `[T], n, [], []` | `Tables {T}` | `[T], [], [], n` | TG:`n→[]`, N:`[]→n` | |
| 55 | +| **A3** | `[T], n, [S], []` | `Tables {T} Schemas {S}` | `[T], [], [S], n` | TG:`n→[]`, N:`[]→n` | |
| 56 | +| **A5** | `n, [], [], []` | `Notes { * }` | `n, n, n, []` | TG:`[]→n`, S:`[]→n` | |
| 57 | +| **A6** | `[], [], n, []` | `Notes { * }` | `n, n, n, []` | T:`[]→n`, TG:`[]→n` | |
| 58 | +| **A7** | `n, [], n, []` | `Notes { * }` | `n, n, n, []` | TG:`[]→n` | |
| 59 | +| **A8** | `n, [TG], [S], []` | `TableGroups {TG} Schemas {S}` | `[], [TG], [S], n` | T:`n→[]`, N:`[]→n` | |
| 60 | +| **A9** | `[T], [TG], n, []` | `Tables {T} TableGroups {TG}` | `[T], [TG], [], n` | S:`n→[]`, N:`[]→n` | |
| 61 | +| **A10** | `n, n, n, []` | `Notes { * }` | `n, n, n, []` | — | |
| 62 | +| **B1** | `[], [], [], []` | `{ * }` | `[], [], [], []` | — | |
| 63 | +| **B2** | `[T], [], [], []` | `Tables {T}` | `[T], [], [], n` | N:`[]→n` | |
| 64 | +| **B3** | `[], [TG], [], []` | `TableGroups {TG}` | `[], [TG], [], n` | N:`[]→n` | |
| 65 | +| **B4** | `[], [], [S], []` | `Schemas {S}` | `[], [], [S], n` | N:`[]→n` | |
| 66 | +| **B5** | `[T], [], [S], []` | `Tables {T} Schemas {S}` | `[T], [], [S], n` | N:`[]→n` | |
| 67 | +| **B6** | `[T], [TG], [], []` | `Tables {T} TableGroups {TG}` | `[T], [TG], [], n` | N:`[]→n` | |
| 68 | +| **B7** | `[], [TG], [S], []` | `TableGroups {TG} Schemas {S}` | `[], [TG], [S], n` | N:`[]→n` | |
| 69 | +| **B8** | `[T], [TG], [S], []` | `Tables {T} TableGroups {TG} Schemas {S}` | `[T], [TG], [S], n` | N:`[]→n` | |
| 70 | +| **C1** | `[], [], [], [N]` | `Tables { * } Notes {N}` | `[], [], [], [N]` | — | |
| 71 | +| **C2** | `[T], [], [], [N]` | `Tables {T} Notes {N}` | `[T], [], [], [N]` | — | |
| 72 | + |
| 73 | +**Non-1:1 patterns:** |
| 74 | + |
| 75 | +| Pattern | Cause | Why acceptable | |
| 76 | +|---------|-------|----------------| |
| 77 | +| N:`[]→n` | When no Notes block emitted, parser leaves stickyNotes as `null` | Both `[]` and `null` mean "don't filter notes" in practice; filterNormalizedModel handles both | |
| 78 | +| TG:`n→[]` | Trinity omit: when other Trinity dims are set, omitted tableGroups promoted to `[]` | `null` tableGroups is legacy; after round-trip it becomes `[]` (show all) — functionally equivalent | |
| 79 | +| S:`n→[]` / T:`n→[]` | Same Trinity omit rule | Same reason — `null` dims only exist in legacy data | |
| 80 | +| T:`[]→n` / TG:`[]→n` / S:`[]→n` | `Notes { * }` generation: only Notes block emitted, no Trinity set → no Trinity omit | Legacy "show only notes" case; empty arrays → null is functionally equivalent | |
| 81 | + |
| 82 | +#### Table 2: DBML → FilterConfig → DBML (Parse Stability) |
| 83 | + |
| 84 | +Tests: does parsing DBML produce a FilterConfig that generates back to the same DBML? |
| 85 | + |
| 86 | +| Case | Input DBML | → FC | → DBML (re-generate) | Delta | |
| 87 | +|------|-----------|------|---------------------|-------| |
| 88 | +| **D1** | `Tables { users, orders }` | `[T], [], [], n` | `Tables { users, orders }` | — | |
| 89 | +| **D2** | `TableGroups { Inv, Rep }` | `[], [TG], [], n` | `TableGroups { Inv, Rep }` | — | |
| 90 | +| **D3** | `Schemas { sales }` | `[], [], [S], n` | `Schemas { sales }` | — | |
| 91 | +| **D4** | `Tables { users } Schemas { sales }` | `[T], [], [S], n` | `Tables { users } Schemas { sales }` | — | |
| 92 | +| **D5** | `Tables { users } TableGroups { Inv }` | `[T], [TG], [], n` | `Tables { users } TableGroups { Inv }` | — | |
| 93 | +| **D6** | `TableGroups { Inv } Schemas { sales }` | `[], [TG], [S], n` | `TableGroups { Inv } Schemas { sales }` | — | |
| 94 | +| **D7** | All three have items | `[T], [TG], [S], n` | All three blocks | — | |
| 95 | +| **E1** | `Tables{T} TableGroups{*} Schemas{S}` | `[T], [TG], [S], n` | `Tables{T} TableGroups{TG} Schemas{S}` | `*` expanded to concrete names | |
| 96 | +| **E2** | `Tables { * }` | `[], [], [], n` | `{ * }` | `Tables{*}` → body `{*}` (stickyNotes null) | |
| 97 | +| **E3** | `Schemas { * }` | `[], [], [], n` | `{ * }` | `Schemas{*}` → body `{*}` | |
| 98 | +| **E4** | `TableGroups { * }` | `[], [TG], [], n` | `TableGroups {TG}` | `*` expanded to concrete names | |
| 99 | +| **F1** | `{ * }` | `[], [], [], []` | `{ * }` | — | |
| 100 | +| **F2** | `Tables{*} Notes{Note1}` | `[], [], [], [N]` | `Tables{*} Notes{Note1}` | — | |
| 101 | +| **G1** | `{ }` (empty body) | `n, n, n, n` | `{ }` | — | |
| 102 | +| **G2** | `Tables { }` (empty sub-block) | `n, n, n, n` | `{ }` | `Tables{}` → `{ }` (empty sub-block = all null) | |
| 103 | +| **H1** | `Notes { Note1, Note2 }` | `n, n, n, [N]` | `Notes { Note1, Note2 }` | — | |
| 104 | + |
| 105 | +**Non-1:1 patterns:** |
| 106 | + |
| 107 | +| Pattern | Cause | Why acceptable | |
| 108 | +|---------|-------|----------------| |
| 109 | +| `TableGroups{*}` → concrete names | Wildcard expanded to concrete group names | Expanded form is functionally identical; needed for union semantics | |
| 110 | +| `Tables{*}` → body `{*}` | When only `Tables{*}` (no other dims), FC is `[],[],[],n` → all Trinity empty → body-level `{*}` | Semantically equivalent; body `{*}` = show all | |
| 111 | +| `Tables{}` → `{ }` | Empty sub-block = all null → generates empty body | Correct — empty block is meaningless, same as no block | |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +## Sub-Problems |
| 116 | + |
| 117 | +### Sub-Problem 1: `expandDiagramViewWildcards` — wildcard expansion rules |
| 118 | + |
| 119 | +```mermaid |
| 120 | +flowchart TD |
| 121 | + A["Wildcard parsed"] --> B["expandDiagramViewWildcards runs"] |
| 122 | + B --> C{"wildcards.has('tables') OR wildcards.has('schemas')?"} |
| 123 | + C -->|yes| D["All Trinity dims → [] (show all)"] |
| 124 | + D --> E["Union covers everything — specific items overridden"] |
| 125 | + C -->|no| F{"wildcards.has('tableGroups')?"} |
| 126 | + F -->|yes| G["Expand to concrete group names"] |
| 127 | + G --> H["filterNormalizedModel receives concrete list"] |
| 128 | + H --> I["Union: tables + group members"] |
| 129 | + F -->|no| J["No expansion needed"] |
| 130 | +``` |
| 131 | + |
| 132 | +**Two wildcard rules:** |
| 133 | + |
| 134 | +1. **Tables `*` or Schemas `*`** → all Trinity dims become `[]` (show all). Union with "show all tables" or "show all schemas" = show everything. Specific items in other dims are overridden. |
| 135 | + |
| 136 | +2. **TableGroups `*`** → expand to concrete group names. Does NOT collapse other dims because not all tables belong to groups — specific items in Tables/Schemas are still meaningful. |
| 137 | + |
| 138 | +**Why the difference:** |
| 139 | +- `Tables: *` = show all tables → union with anything = everything |
| 140 | +- `Schemas: *` = show all schemas → union with anything = everything |
| 141 | +- `TableGroups: *` needs concrete names because some tables DON'T belong to any group. Specific tables/schemas alongside group wildcard still filter meaningfully. |
| 142 | + |
| 143 | +### Sub-Problem 2: `syncDiagramView` — rewrite generation rules |
| 144 | + |
| 145 | +```mermaid |
| 146 | +flowchart TD |
| 147 | + A["FilterConfig from UI"] --> B{"All null?"} |
| 148 | + B -->|yes| C["{ }"] |
| 149 | + B -->|no| D{"Any Trinity null?"} |
| 150 | + D -->|yes, Trinity has items| E["Union: emit only items dims"] |
| 151 | + D -->|yes, Trinity empty, notes has items| F["Notes { items }"] |
| 152 | + D -->|yes, Trinity empty, notes empty| G["Notes { * }"] |
| 153 | + D -->|no| H{"All Trinity []?"} |
| 154 | + H -->|yes, notes empty| I["{ * }"] |
| 155 | + H -->|yes, notes has items| J["Tables { * } Notes { items }"] |
| 156 | + H -->|no| K["Emit only dims with items"] |
| 157 | +``` |
| 158 | + |
| 159 | +**6 generation rules:** |
| 160 | + |
| 161 | +1. **`tableGroups: null`** → frontend backfills standalone tables into tables array; emit tables as-is, omit tableGroups |
| 162 | +2. **`tables: null` or `schemas: null` + rest empty** → `Notes { * }` |
| 163 | +3. **`tables: null` or `schemas: null` + rest has items** → union rule, omit null dim |
| 164 | +4. **All Trinity `[]`** → body-level `{ * }` (or `Tables { * } + Notes` if notes) |
| 165 | +5. **Some items + rest `[]`** → emit only items dims |
| 166 | +6. **All items** → emit all |
| 167 | + |
| 168 | +**Safety guarantee:** Trinity omit rule ensures omitted dims → `[]` on re-parse, so omitting `[]` dims is always safe. |
0 commit comments