From 4baa6edff07ebb87cd419d1b7ffa730c3bd96f49 Mon Sep 17 00:00:00 2001 From: zamax14 Date: Mon, 29 Jun 2026 02:03:04 -0600 Subject: [PATCH 1/2] feat: show column comments in html tooltips --- src/sqlalchemy_erd/html_renderer.py | 14 ++++++++++---- src/sqlalchemy_erd/introspect.py | 2 ++ src/sqlalchemy_erd/serialization.py | 1 + tests/conftest.py | 16 ++++++++++++++++ tests/test_html_renderer.py | 13 +++++++++++++ tests/test_introspect.py | 10 ++++++++++ 6 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/sqlalchemy_erd/html_renderer.py b/src/sqlalchemy_erd/html_renderer.py index eb507a7..ab01428 100644 --- a/src/sqlalchemy_erd/html_renderer.py +++ b/src/sqlalchemy_erd/html_renderer.py @@ -354,18 +354,24 @@ def render_html( const sy = HEADER_H + PAD_ + i * FIELD_H; el('line', {{ x1:'10', y1:sy, x2:NODE_W-10, y2:sy, stroke: THEME.separator, 'stroke-width':'1' }}, g); }} - el('text', {{ + const nameAttrs = {{ x:'10', y:fy, 'font-size':'10', 'font-family':"'Courier New', Courier, monospace", fill: field.nameColor, 'font-weight': field.nameWeight, 'dominant-baseline':'middle', textContent: field.name - }}, g); - el('text', {{ + }}; + const kindAttrs = {{ x: NODE_W-8, y:fy, 'font-size':'9', 'font-family':"'Courier New', Courier, monospace", fill: field.kindColor, 'text-anchor':'end', 'dominant-baseline':'middle', opacity:'0.9', textContent: field.kindLabel - }}, g); + }}; + if (field.comment) {{ + nameAttrs.title = field.comment; + kindAttrs.title = field.comment; + }} + el('text', nameAttrs, g); + el('text', kindAttrs, g); }} g.addEventListener('pointerdown', e => {{ diff --git a/src/sqlalchemy_erd/introspect.py b/src/sqlalchemy_erd/introspect.py index a98fae0..5d461d6 100644 --- a/src/sqlalchemy_erd/introspect.py +++ b/src/sqlalchemy_erd/introspect.py @@ -18,6 +18,7 @@ class ColumnInfo: nullable: bool is_pk: bool is_fk: bool + comment: str | None = None @dataclass @@ -147,6 +148,7 @@ def _build_table( nullable=col.nullable or False, is_pk=is_pk, is_fk=is_fk, + comment=col.comment, )) display_name = class_names.get(table_key, table.name) diff --git a/src/sqlalchemy_erd/serialization.py b/src/sqlalchemy_erd/serialization.py index 5d9d490..1a65c9c 100644 --- a/src/sqlalchemy_erd/serialization.py +++ b/src/sqlalchemy_erd/serialization.py @@ -33,6 +33,7 @@ def build_entities_json(tables: list[TableInfo], theme: Theme) -> str: "nameWeight": name_weight, "kindColor": kind_color, "kindLabel": kind_label, + "comment": col.comment, }) entities.append({ diff --git a/tests/conftest.py b/tests/conftest.py index a9500bb..f3ae395 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -273,3 +273,19 @@ class Task(MultiFkBase): @pytest.fixture def multi_fk_base(): return MultiFkBase + + +# -- Column comments schema --------------------------------------------------- + +comments_metadata = MetaData() + +Table( + "accounts", comments_metadata, + Column("id", Integer, primary_key=True), + Column("email", String(200), comment="Primary login email"), +) + + +@pytest.fixture +def comments_metadata_fixture(): + return comments_metadata diff --git a/tests/test_html_renderer.py b/tests/test_html_renderer.py index 1e4397d..d1249cf 100644 --- a/tests/test_html_renderer.py +++ b/tests/test_html_renderer.py @@ -95,6 +95,12 @@ def test_per_table_color_override_applied(self, blog_base): entities = json.loads(_build_entities_json(tables, theme)) assert _entity(entities, "users")["headerColor"] == "#ff0000" + def test_column_comment_is_serialized(self, comments_metadata_fixture): + tables, _ = introspect_models(comments_metadata_fixture) + entities = json.loads(_build_entities_json(tables, get_theme("default"))) + email = _field(_entity(entities, "accounts"), "email") + assert email["comment"] == "Primary login email" + # ── _build_relations_json ──────────────────────────────────────────────────── @@ -170,3 +176,10 @@ def test_empty_schema_still_renders(self, empty_base): html = render_html(tables, rels, positions, theme) assert "" in html assert _extract_js_object(html, "ENTITIES") == [] + + def test_html_contains_tooltip_binding(self, comments_metadata_fixture): + tables, rels = introspect_models(comments_metadata_fixture) + positions = force_directed_layout(tables, rels) + html = render_html(tables, rels, positions, get_theme("default")) + assert "field.comment" in html + assert "Primary login email" in html diff --git a/tests/test_introspect.py b/tests/test_introspect.py index ff1c65d..bdc3ad2 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -354,3 +354,13 @@ def test_schema_attribute_set(self, multi_schema_metadata_fixture): tables, _ = introspect_models(multi_schema_metadata_fixture) schemas = {t.schema for t in tables} assert schemas == {"auth", "billing"} + + +# -- Column comments ---------------------------------------------------------- + +class TestIntrospectColumnComments: + def test_column_comments_are_preserved(self, comments_metadata_fixture): + tables, _ = introspect_models(comments_metadata_fixture) + accounts = next(t for t in tables if t.name == "accounts") + email = next(c for c in accounts.columns if c.name == "email") + assert email.comment == "Primary login email" From 7acefb442f02339678a3100a15f6fac1f0e01d3d Mon Sep 17 00:00:00 2001 From: zamax14 Date: Thu, 30 Jul 2026 11:51:33 -0600 Subject: [PATCH 2/2] fix(html): render column comment tooltips as svg title elements --- src/sqlalchemy_erd/html_renderer.py | 2 ++ tests/test_html_renderer.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/sqlalchemy_erd/html_renderer.py b/src/sqlalchemy_erd/html_renderer.py index ab01428..77b2d6c 100644 --- a/src/sqlalchemy_erd/html_renderer.py +++ b/src/sqlalchemy_erd/html_renderer.py @@ -236,6 +236,8 @@ def render_html( const e = document.createElementNS(NS, tag); for (const [k,v] of Object.entries(attrs||{{}})) {{ if (k === 'textContent') e.textContent = v; + // SVG ignores the title attribute: the native tooltip needs a child. + else if (k === 'title') el('title', {{ textContent: v }}, e); else e.setAttribute(k, v); }} if (parent) parent.appendChild(e); diff --git a/tests/test_html_renderer.py b/tests/test_html_renderer.py index d1249cf..ab93f96 100644 --- a/tests/test_html_renderer.py +++ b/tests/test_html_renderer.py @@ -183,3 +183,5 @@ def test_html_contains_tooltip_binding(self, comments_metadata_fixture): html = render_html(tables, rels, positions, get_theme("default")) assert "field.comment" in html assert "Primary login email" in html + # A title attribute renders no tooltip in SVG; el() must emit a <title> child. + assert "el('title', { textContent: v }, e)" in html