refactor(state): collapse telegram topic v3 migration into one table-driven rebuild
Same behavior as the salvaged #76487 migration (fresh installs get the v3 shape; v1/v2 tables rebuild with profile_name leading the PK, legacy rows into 'default' only, CASCADE FK supplied on the way), with the per-table DDL written once instead of three times and the now-redundant v1->v2 CASCADE-only rebuild dropped (the v3 rebuild subsumes it). Co-authored-by: Celio Monteiro <crdesign8@hotmail.com>
This commit is contained in:
+51
-101
@@ -16111,20 +16111,16 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
|
||||
gateways (shared ``state.db``) isolate topic mode/bindings
|
||||
per Hermes profile (issue #76423).
|
||||
"""
|
||||
def _table_columns(conn, table: str) -> set:
|
||||
try:
|
||||
return {
|
||||
row[1]
|
||||
for row in conn.execute(f"PRAGMA table_info('{table}')").fetchall()
|
||||
}
|
||||
except sqlite3.OperationalError:
|
||||
return set()
|
||||
|
||||
def _do(conn):
|
||||
# Fresh installs get the v3 shape immediately.
|
||||
conn.executescript(
|
||||
# (table, column list, DDL body). ``profile_name`` leads the primary
|
||||
# key so multiplexed profiles sharing one state.db never collide on a
|
||||
# private chat_id (which is the user id, identical across bots).
|
||||
tables = (
|
||||
(
|
||||
"telegram_dm_topic_mode",
|
||||
"profile_name, chat_id, user_id, enabled, activated_at, updated_at, "
|
||||
"has_topics_enabled, allows_users_to_create_topics, "
|
||||
"capability_checked_at, intro_message_id, pinned_message_id",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS telegram_dm_topic_mode (
|
||||
profile_name TEXT NOT NULL DEFAULT 'default',
|
||||
chat_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
@@ -16137,9 +16133,13 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
|
||||
intro_message_id TEXT,
|
||||
pinned_message_id TEXT,
|
||||
PRIMARY KEY (profile_name, chat_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS telegram_dm_topic_bindings (
|
||||
""",
|
||||
),
|
||||
(
|
||||
"telegram_dm_topic_bindings",
|
||||
"profile_name, chat_id, thread_id, user_id, session_key, "
|
||||
"session_id, managed_mode, linked_at, updated_at",
|
||||
"""
|
||||
profile_name TEXT NOT NULL DEFAULT 'default',
|
||||
chat_id TEXT NOT NULL,
|
||||
thread_id TEXT NOT NULL,
|
||||
@@ -16150,93 +16150,43 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
|
||||
linked_at REAL NOT NULL,
|
||||
updated_at REAL NOT NULL,
|
||||
PRIMARY KEY (profile_name, chat_id, thread_id)
|
||||
);
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
def _do(conn):
|
||||
for table, columns, ddl in tables:
|
||||
# Fresh installs get the v3 shape immediately.
|
||||
conn.execute(f"CREATE TABLE IF NOT EXISTS {table} ({ddl})")
|
||||
have = {row[1] for row in conn.execute(f"PRAGMA table_info('{table}')")}
|
||||
if "profile_name" in have:
|
||||
continue
|
||||
# Pre-profile shape (v1 or v2) → v3. SQLite can't ALTER a
|
||||
# primary key (or a foreign key), so rebuild; this also
|
||||
# supplies the v2 ON DELETE CASCADE for v1 bindings tables.
|
||||
# Legacy rows land in the "default" namespace only — never
|
||||
# replicated across profiles.
|
||||
legacy_columns = columns.replace("profile_name, ", "", 1)
|
||||
conn.executescript(
|
||||
f"""
|
||||
CREATE TABLE {table}_new ({ddl});
|
||||
INSERT INTO {table}_new ({columns})
|
||||
SELECT 'default', {legacy_columns} FROM {table};
|
||||
DROP TABLE {table};
|
||||
ALTER TABLE {table}_new RENAME TO {table};
|
||||
"""
|
||||
)
|
||||
|
||||
# Indexes after any rebuild so they always target the v3 shape
|
||||
# (a legacy table lacking profile_name can't take the user index).
|
||||
conn.executescript(
|
||||
"""
|
||||
)
|
||||
# Indexes are created after any rebuild: a legacy table still
|
||||
# lacking profile_name cannot accept the v3 user index.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_telegram_dm_topic_bindings_session
|
||||
ON telegram_dm_topic_bindings(session_id);
|
||||
|
||||
mode_cols = _table_columns(conn, "telegram_dm_topic_mode")
|
||||
bind_cols = _table_columns(conn, "telegram_dm_topic_bindings")
|
||||
|
||||
# Any pre-profile shape (v1 or v2) → v3: rebuild both tables with
|
||||
# profile_name in the primary key. SQLite can't ALTER a primary key
|
||||
# (or a foreign key), so we rebuild; the bindings rebuild also
|
||||
# supplies the v2 ON DELETE CASCADE for v1 tables. Legacy rows
|
||||
# migrate into the "default" namespace only — never replicated
|
||||
# across profiles (collision-contaminated data would multiply).
|
||||
if "profile_name" not in mode_cols:
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE telegram_dm_topic_mode_new (
|
||||
profile_name TEXT NOT NULL DEFAULT 'default',
|
||||
chat_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
activated_at REAL NOT NULL,
|
||||
updated_at REAL NOT NULL,
|
||||
has_topics_enabled INTEGER,
|
||||
allows_users_to_create_topics INTEGER,
|
||||
capability_checked_at REAL,
|
||||
intro_message_id TEXT,
|
||||
pinned_message_id TEXT,
|
||||
PRIMARY KEY (profile_name, chat_id)
|
||||
);
|
||||
INSERT INTO telegram_dm_topic_mode_new (
|
||||
profile_name, chat_id, user_id, enabled, activated_at,
|
||||
updated_at, has_topics_enabled, allows_users_to_create_topics,
|
||||
capability_checked_at, intro_message_id, pinned_message_id
|
||||
)
|
||||
SELECT
|
||||
'default', chat_id, user_id, enabled, activated_at,
|
||||
updated_at, has_topics_enabled, allows_users_to_create_topics,
|
||||
capability_checked_at, intro_message_id, pinned_message_id
|
||||
FROM telegram_dm_topic_mode;
|
||||
DROP TABLE telegram_dm_topic_mode;
|
||||
ALTER TABLE telegram_dm_topic_mode_new
|
||||
RENAME TO telegram_dm_topic_mode;
|
||||
"""
|
||||
)
|
||||
|
||||
if "profile_name" not in bind_cols:
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE telegram_dm_topic_bindings_new (
|
||||
profile_name TEXT NOT NULL DEFAULT 'default',
|
||||
chat_id TEXT NOT NULL,
|
||||
thread_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
session_key TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
||||
managed_mode TEXT NOT NULL DEFAULT 'auto',
|
||||
linked_at REAL NOT NULL,
|
||||
updated_at REAL NOT NULL,
|
||||
PRIMARY KEY (profile_name, chat_id, thread_id)
|
||||
);
|
||||
INSERT INTO telegram_dm_topic_bindings_new (
|
||||
profile_name, chat_id, thread_id, user_id, session_key,
|
||||
session_id, managed_mode, linked_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
'default', chat_id, thread_id, user_id, session_key,
|
||||
session_id, managed_mode, linked_at, updated_at
|
||||
FROM telegram_dm_topic_bindings;
|
||||
DROP TABLE telegram_dm_topic_bindings;
|
||||
ALTER TABLE telegram_dm_topic_bindings_new
|
||||
RENAME TO telegram_dm_topic_bindings;
|
||||
"""
|
||||
)
|
||||
|
||||
# Indexes after any rebuild so they always target the v3 shape.
|
||||
conn.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS "
|
||||
"idx_telegram_dm_topic_bindings_session "
|
||||
"ON telegram_dm_topic_bindings(session_id)"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS "
|
||||
"idx_telegram_dm_topic_bindings_user "
|
||||
"ON telegram_dm_topic_bindings(profile_name, user_id, chat_id)"
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_dm_topic_bindings_user
|
||||
ON telegram_dm_topic_bindings(profile_name, user_id, chat_id);
|
||||
"""
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
|
||||
@@ -15,13 +15,14 @@ def _session(db, sid, profile_name=None):
|
||||
db.create_session(session_id=sid, source="telegram", user_id=CHAT, profile_name=profile_name)
|
||||
|
||||
|
||||
def test_legacy_v2_rows_migrate_only_to_default(tmp_path: Path):
|
||||
def test_legacy_rows_migrate_only_to_default(tmp_path: Path):
|
||||
"""v1 shape (no CASCADE, old user index) → v3: rows land in 'default' only."""
|
||||
db_path = tmp_path / "legacy.db"
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.executescript(
|
||||
f"""
|
||||
CREATE TABLE state_meta (key TEXT PRIMARY KEY, value TEXT);
|
||||
INSERT INTO state_meta(key, value) VALUES ('telegram_dm_topic_schema_version', '2');
|
||||
INSERT INTO state_meta(key, value) VALUES ('telegram_dm_topic_schema_version', '1');
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY, source TEXT, user_id TEXT, model TEXT,
|
||||
model_config TEXT, system_prompt TEXT, parent_session_id TEXT,
|
||||
@@ -43,11 +44,13 @@ def test_legacy_v2_rows_migrate_only_to_default(tmp_path: Path):
|
||||
CREATE TABLE telegram_dm_topic_bindings (
|
||||
chat_id TEXT NOT NULL, thread_id TEXT NOT NULL, user_id TEXT NOT NULL,
|
||||
session_key TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
||||
session_id TEXT NOT NULL REFERENCES sessions(id),
|
||||
managed_mode TEXT NOT NULL DEFAULT 'auto',
|
||||
linked_at REAL NOT NULL, updated_at REAL NOT NULL,
|
||||
PRIMARY KEY (chat_id, thread_id)
|
||||
);
|
||||
CREATE INDEX idx_telegram_dm_topic_bindings_user
|
||||
ON telegram_dm_topic_bindings(user_id, chat_id);
|
||||
INSERT INTO telegram_dm_topic_bindings
|
||||
VALUES ('{CHAT}', '99', '{CHAT}', 'k', 'legacy-sess', 'auto', 1.0, 1.0);
|
||||
"""
|
||||
@@ -69,6 +72,8 @@ def test_legacy_v2_rows_migrate_only_to_default(tmp_path: Path):
|
||||
assert db.get_telegram_topic_binding(
|
||||
chat_id=CHAT, thread_id="99", profile_name="coder",
|
||||
) is None
|
||||
fk = db._conn.execute("PRAGMA foreign_key_list('telegram_dm_topic_bindings')").fetchall()
|
||||
assert any(row[2] == "sessions" and row[6] == "CASCADE" for row in fk)
|
||||
db.close()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user