diff --git a/src/sqlalchemy_erd/html_renderer.py b/src/sqlalchemy_erd/html_renderer.py
index 112dac8..ad3f7de 100644
--- a/src/sqlalchemy_erd/html_renderer.py
+++ b/src/sqlalchemy_erd/html_renderer.py
@@ -235,6 +235,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);
@@ -353,18 +355,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 674cb93..943d603 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 03df1c4..d1e5f98 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -307,3 +307,19 @@ def multi_fk_base():
@pytest.fixture
def cardinality_metadata_fixture():
return cardinality_metadata
+
+
+# -- 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..ab93f96 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,12 @@ 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
+ # A title attribute renders no tooltip in SVG; el() must emit a child.
+ assert "el('title', { textContent: v }, e)" in html
diff --git a/tests/test_introspect.py b/tests/test_introspect.py
index 5d6c3e2..eaff3a0 100644
--- a/tests/test_introspect.py
+++ b/tests/test_introspect.py
@@ -373,3 +373,13 @@ def test_nullable_fk_marks_optional_parent(self, cardinality_metadata_fixture):
rel = next(r for r in rels if r.to_table == "tasks")
assert rel.from_card == "0..1"
assert rel.to_card == "N"
+
+
+# -- 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"