From 4449623af8ad64bf5c8929d57828f078a97cc84a Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Thu, 30 Apr 2026 18:56:44 +0200 Subject: [PATCH] fix(renderer): tolerate missing optional tables; document tzinfo --- src/welcome_template.py | 38 +++++++++++++++++-------- tests/test_welcome_template_renderer.py | 23 +++++++++++++++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/welcome_template.py b/src/welcome_template.py index 7de4e0d..ab31935 100644 --- a/src/welcome_template.py +++ b/src/welcome_template.py @@ -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 { diff --git a/tests/test_welcome_template_renderer.py b/tests/test_welcome_template_renderer.py index 4620a90..386ddfe 100644 --- a/tests/test_welcome_template_renderer.py +++ b/tests/test_welcome_template_renderer.py @@ -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