Enable state store foreign key cascades#3444
Conversation
Turn on SQLite foreign key enforcement for every StateStore connection so delete_thread honors the schema's cascading child-row cleanup. Add a regression test that persists messages, dynamic tools, checkpoints, and thread goals before deleting the parent thread.
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
|
Closing this narrower branch because the same state-store foreign-key enforcement is already covered in the broader v0.8.65 local-state hardening PR #3433. |
There was a problem hiding this comment.
Code Review
This pull request enables foreign key constraints on the SQLite state database connection and adds a test to verify that deleting a thread cascades to delete associated child rows. Feedback suggests using 'execute' instead of 'execute_batch' for the single 'PRAGMA' statement to improve efficiency, and validating the table name against an allowlist in the 'table_count' test helper to avoid unsafe SQL interpolation patterns.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| conn.execute_batch("PRAGMA foreign_keys = ON;") | ||
| .context("failed to enable state db foreign keys")?; |
There was a problem hiding this comment.
Using execute_batch for a single, simple PRAGMA statement is less efficient because it parses the input as a batch of multiple SQL statements. It is more idiomatic and efficient to use execute with empty parameters.
| conn.execute_batch("PRAGMA foreign_keys = ON;") | |
| .context("failed to enable state db foreign keys")?; | |
| conn.execute("PRAGMA foreign_keys = ON;", []) | |
| .context("failed to enable state db foreign keys")?; |
| fn table_count(store: &StateStore, table: &str, thread_id: &str) -> i64 { | ||
| let conn = store.conn().expect("open test connection"); | ||
| let query = format!("SELECT COUNT(*) FROM {table} WHERE thread_id = ?1"); | ||
| conn.query_row(&query, params![thread_id], |row| row.get(0)) | ||
| .expect("count rows") | ||
| } |
There was a problem hiding this comment.
Constructing SQL queries via string interpolation (format!) is a dangerous pattern that can lead to SQL injection vulnerabilities. Although this is a test helper and currently only called with static string literals, it is highly recommended to validate the table parameter against an allowlist of expected table names to prevent unsafe patterns from spreading or being copied into production code.
| fn table_count(store: &StateStore, table: &str, thread_id: &str) -> i64 { | |
| let conn = store.conn().expect("open test connection"); | |
| let query = format!("SELECT COUNT(*) FROM {table} WHERE thread_id = ?1"); | |
| conn.query_row(&query, params![thread_id], |row| row.get(0)) | |
| .expect("count rows") | |
| } | |
| fn table_count(store: &StateStore, table: &str, thread_id: &str) -> i64 { | |
| let allowed_tables = ["messages", "thread_dynamic_tools", "checkpoints", "thread_goals"]; | |
| assert!(allowed_tables.contains(&table), "invalid table name: {table}"); | |
| let conn = store.conn().expect("open test connection"); | |
| let query = format!("SELECT COUNT(*) FROM {table} WHERE thread_id = ?1"); | |
| conn.query_row(&query, params![thread_id], |row| row.get(0)) | |
| .expect("count rows") | |
| } |
Goal
Make thread deletion honor the state schema existing cascading child-row cleanup.
Changes
Verification
Risks
Low. The schema already declares the cascade relationships; this makes SQLite enforce them consistently for each connection.