From 33e7107637f0c612bfe8d3d1a1494ff5583f4b63 Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Thu, 30 Apr 2026 18:35:18 +0200 Subject: [PATCH] =?UTF-8?q?feat(db):=20schema=20v15=20=E2=80=94=20welcome?= =?UTF-8?q?=5Ftemplate=20singleton=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/db.py | 32 +++++++++++++++++++++- tests/test_welcome_template_migration.py | 35 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tests/test_welcome_template_migration.py diff --git a/src/db.py b/src/db.py index 854703c..adb0cf0 100644 --- a/src/db.py +++ b/src/db.py @@ -39,7 +39,7 @@ def _maybe_instrument(con, db_tag: str): _SAFE_IDENTIFIER = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]{0,63}$") -SCHEMA_VERSION = 20 +SCHEMA_VERSION = 21 _SYSTEM_SCHEMA = """ CREATE TABLE IF NOT EXISTS schema_version ( @@ -405,6 +405,18 @@ CREATE TABLE IF NOT EXISTS resource_grants ( assigned_by VARCHAR, UNIQUE (group_id, resource_type, resource_id) ); + +-- v21: customizable analyst-bootstrap welcome prompt. +-- Singleton row (id=1). NULL content means "use the default template +-- shipped at config/claude_md_template.txt"; admin-edited override +-- stores the raw Jinja2 source string. +CREATE TABLE IF NOT EXISTS welcome_template ( + id INTEGER PRIMARY KEY DEFAULT 1, + content TEXT, + updated_at TIMESTAMP, + updated_by VARCHAR, + CONSTRAINT singleton CHECK (id = 1) +); """ @@ -1614,6 +1626,17 @@ _V3_TO_V4_MIGRATIONS = [ """, ] +_V20_TO_V21_MIGRATIONS = [ + """CREATE TABLE IF NOT EXISTS welcome_template ( + id INTEGER PRIMARY KEY DEFAULT 1, + content TEXT, + updated_at TIMESTAMP, + updated_by VARCHAR, + CONSTRAINT singleton CHECK (id = 1) + )""", + "INSERT INTO welcome_template (id, content) VALUES (1, NULL) ON CONFLICT (id) DO NOTHING", +] + def _ensure_schema(conn: duckdb.DuckDBPyConnection) -> None: """Create tables if they don't exist. Apply migrations if schema version changed. @@ -1672,6 +1695,10 @@ def _ensure_schema(conn: duckdb.DuckDBPyConnection) -> None: "INSERT INTO schema_version (version) VALUES (?)", [SCHEMA_VERSION], ) + conn.execute( + "INSERT INTO welcome_template (id, content) VALUES (1, NULL) " + "ON CONFLICT (id) DO NOTHING" + ) # Fresh-install seed is handled by the unconditional # _seed_core_roles call at the bottom of _ensure_schema — # left as a no-op branch here so the migration ladder still @@ -1749,6 +1776,9 @@ def _ensure_schema(conn: duckdb.DuckDBPyConnection) -> None: if current < 20: for sql in _V19_TO_V20_MIGRATIONS: conn.execute(sql) + if current < 21: + for sql in _V20_TO_V21_MIGRATIONS: + conn.execute(sql) conn.execute( "UPDATE schema_version SET version = ?, applied_at = current_timestamp", [SCHEMA_VERSION], diff --git a/tests/test_welcome_template_migration.py b/tests/test_welcome_template_migration.py new file mode 100644 index 0000000..a728209 --- /dev/null +++ b/tests/test_welcome_template_migration.py @@ -0,0 +1,35 @@ +"""v20 → v21 migration: adds welcome_template singleton table.""" + +from pathlib import Path + +import duckdb +import pytest + +from src.db import SCHEMA_VERSION, _ensure_schema, get_schema_version + + +def _open(path: Path) -> duckdb.DuckDBPyConnection: + return duckdb.connect(str(path)) + + +def test_v21_creates_welcome_template_table(tmp_path): + db_path = tmp_path / "system.duckdb" + conn = _open(db_path) + # Pretend we're on v20: write a v20-shaped DB by running schema then + # rolling the version row back. + _ensure_schema(conn) + conn.execute("UPDATE schema_version SET version = 20") + conn.execute("DROP TABLE IF EXISTS welcome_template") + conn.close() + + # Re-open: migration ladder runs. + conn = _open(db_path) + _ensure_schema(conn) + assert get_schema_version(conn) == SCHEMA_VERSION + # Singleton row must exist with NULL content (= use shipped default). + rows = conn.execute( + "SELECT id, content, updated_at, updated_by FROM welcome_template" + ).fetchall() + assert len(rows) == 1 + assert rows[0][0] == 1 # singleton id + assert rows[0][1] is None # NULL = default