fix(renderer): tolerate missing optional tables; document tzinfo

This commit is contained in:
ZdenekSrotyr 2026-04-30 18:56:44 +02:00
parent 51f287a81a
commit 4449623af8
2 changed files with 49 additions and 12 deletions

View file

@ -47,11 +47,14 @@ def _load_default_template() -> str:
def _list_tables(conn: duckdb.DuckDBPyConnection) -> list[dict[str, Any]]:
rows = conn.execute(
"""SELECT name, description, query_mode
FROM table_registry
ORDER BY name"""
).fetchall()
try:
rows = conn.execute(
"""SELECT name, description, query_mode
FROM table_registry
ORDER BY name"""
).fetchall()
except duckdb.CatalogException:
return []
return [
{"name": r[0], "description": r[1] or "", "query_mode": r[2] or "local"}
for r in rows
@ -81,18 +84,24 @@ def _marketplaces_for_user(
Results are grouped by marketplace slug; display names are fetched
from marketplace_registry in a single query.
"""
allowed = resolve_allowed_plugins(conn, user)
try:
allowed = resolve_allowed_plugins(conn, user)
except duckdb.CatalogException:
return []
if not allowed:
return []
# Build slug → display name lookup from registry
slugs = list({p["marketplace_slug"] for p in allowed})
placeholders = ",".join(["?"] * len(slugs))
name_rows = conn.execute(
f"SELECT id, name FROM marketplace_registry WHERE id IN ({placeholders})",
slugs,
).fetchall()
slug_to_name: dict[str, str] = {r[0]: r[1] for r in name_rows}
try:
name_rows = conn.execute(
f"SELECT id, name FROM marketplace_registry WHERE id IN ({placeholders})",
slugs,
).fetchall()
slug_to_name: dict[str, str] = {r[0]: r[1] for r in name_rows}
except duckdb.CatalogException:
slug_to_name = {}
grouped: dict[str, dict[str, Any]] = {}
for plugin in allowed:
@ -116,7 +125,12 @@ def build_context(
user: dict[str, Any],
server_url: str,
) -> dict[str, Any]:
"""Compose the Jinja2 render context. Pure, no side effects."""
"""Compose the Jinja2 render context. Pure, no side effects.
Note: ``now`` is tz-aware UTC; DB-sourced timestamps elsewhere in the
codebase are naive (DuckDB stores ``TIMESTAMP``, not ``TIMESTAMPTZ``).
Don't subtract or compare them inside templates without normalising.
"""
now = datetime.now(timezone.utc)
parsed = urlparse(server_url)
return {

View file

@ -54,3 +54,26 @@ def test_context_exposes_documented_keys(conn):
for top in ("instance", "server", "sync_interval", "data_source",
"tables", "metrics", "marketplaces", "user", "now", "today"):
assert top in ctx, f"missing top-level key: {top}"
def test_render_tolerates_missing_optional_tables(tmp_path, monkeypatch):
"""A bare DuckDB without table_registry / marketplace_registry must still render."""
monkeypatch.setenv("DATA_DIR", str(tmp_path))
db_path = tmp_path / "bare.duckdb"
bare = duckdb.connect(str(db_path))
# Only seed the welcome_template singleton manually; no other tables.
bare.execute(
"""CREATE TABLE welcome_template (
id INTEGER PRIMARY KEY DEFAULT 1,
content TEXT,
updated_at TIMESTAMP,
updated_by VARCHAR
)"""
)
bare.execute("INSERT INTO welcome_template (id, content) VALUES (1, NULL)")
out = render_welcome(bare, user=_user(), server_url="https://example.com")
bare.close()
assert "AI Data Analyst" in out # default template still renders
# No tables → "_No tables registered yet_" branch from the default template
assert "No tables registered yet" in out