--- name: Storage profiles and profile_terms CRUD type: architecture-map-page slice: 05-core-storage-hotkey-build last_verified: 2026/05/09 --- # Storage profiles and profile_terms CRUD > **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Storage profiles CRUD **Plain English summary.** Profiles are user-defined contexts (Work, Personal, Game). Each profile has its own initial prompt and a list of custom dictionary terms. The default profile (`00000000-0000-0000-0000-000000000001`) is seeded by migration v6 and protected by SQL triggers from rename or delete. ## At a glance - Source: `crates/storage/src/database.rs:776-1124`. - Public surface: `list_profiles`, `get_profile`, `create_profile`, `update_profile`, `delete_profile`, `list_profile_terms`, `add_profile_term`, `delete_profile_term`. - Public types: `ProfileRow`, `ProfileTermRow`. - Public constant: `lumotia_storage::DEFAULT_PROFILE_ID = "00000000-0000-0000-0000-000000000001"` at `crates/storage/src/lib.rs:7`. - Consumers: slice 2 profiles command, transcript inserts (every transcript carries a `profile_id`), the LLM prompt builder (slice 4) reads the profile's `initial_prompt` and `profile_terms` to condition cleanup. ## Public types ### `ProfileRow` — `crates/storage/src/database.rs:776` ```rust pub struct ProfileRow { pub id: String, pub name: String, pub initial_prompt: String, pub created_at: String, } ``` ### `ProfileTermRow` — `crates/storage/src/database.rs:784` ```rust pub struct ProfileTermRow { pub id: String, pub profile_id: String, pub term: String, pub note: String, pub created_at: String, } ``` ## Functions ### `list_profiles(pool) -> Result>` — `crates/storage/src/database.rs:950` `SELECT ... FROM profiles ORDER BY created_at ASC`. Default profile is always first by virtue of being the earliest-created. ### `get_profile(pool, id) -> Result>` — `crates/storage/src/database.rs:959` Single-row select. ### `create_profile(pool, id, name, initial_prompt) -> Result` — `crates/storage/src/database.rs:968` UUID is generated by the caller. `name` has a `UNIQUE` constraint at the column level (migration v6); a duplicate name returns `lumotia_storage::Error::Query { operation: "create_profile", source }` carrying the sqlx UNIQUE-constraint error. ### `update_profile(pool, id, name, initial_prompt)` — `crates/storage/src/database.rs:995` Renames the profile and / or rewrites the initial prompt. **Updating the default profile's name** is short-circuited in Rust before hitting sqlite, raising `lumotia_storage::Error::InvalidReference { entity: Entity::Profile, reason: "Default profile cannot be renamed" }`. The `trg_protect_default_profile_rename` trigger (migration v6) is the structural backstop. Updating only `initial_prompt` is allowed. ### `delete_profile(pool, id) -> Result<()>` — `crates/storage/src/database.rs:1024` Two guards before the DELETE: 1. **Application-layer guard:** if the profile owns any transcripts, refuse with a friendly error pointing the user at re-assignment. Implemented via `transcript_count_for_profile` (`database.rs:1103`). 2. **Database-layer guard:** `trg_protect_default_profile_delete` raises `RAISE(ABORT, 'cannot delete the default profile')` when the id matches. The cascade on `profile_terms.profile_id` removes the profile's custom terms automatically. ### `list_profile_terms(pool, profile_id) -> Result>` — `crates/storage/src/database.rs:1044` `SELECT ... FROM profile_terms WHERE profile_id = ? ORDER BY term ASC`. Used by the LLM prompt builder. ### `add_profile_term(pool, profile_id, term, note) -> Result` — `crates/storage/src/database.rs:1062` Generates a UUID v4 (per `crates/storage/Cargo.toml`'s `features = ["v4"]`), inserts the row, returns the new id. The `Cargo.toml` comment claims "v7 random" but the feature flag is `v4`. Practical effect is identical for our purposes (random unique ids); the comment is the inconsistency to clean up if anyone touches that line. ### `delete_profile_term(pool, id) -> Result<()>` — `crates/storage/src/database.rs:1085` `DELETE FROM profile_terms WHERE id = ?`. ## Data flow / contract - Every transcript references a profile via `transcripts.profile_id` (FK installed in migration v9). `insert_transcript` validates the FK at the application layer for a friendlier error. - The LLM prompt builder loads the active profile's `initial_prompt` and the matching `profile_terms` at cleanup time. - The default profile's id is a `pub const &str` so frontend code via Tauri commands can also reference it as a known constant (it is stamped into the JSON bridge by the Tauri preferences command). ## Watch-outs - **Default profile protection is enforced by triggers, not by code.** A future migration that drops or renames the trigger silently removes the protection. There is no head-schema test that asserts the triggers are still present; today only the v6 migration tests assert protection at DML time. Consider adding a head-schema integration test that explicitly attempts to delete the default profile and asserts the failure. - **Profile name `UNIQUE`-ness is case-sensitive.** "Work" and "work" are two different profiles. Worth a discussion about whether case-insensitive uniqueness is the right behaviour; today it is not enforced. - **`transcript_count_for_profile` is the only application-layer FK guard.** Tasks reference no profile (the table predates the profile concept). If task-to-profile FKs land later, `delete_profile` needs a parallel check. - **No `update_profile_term`.** A user fixing a typo in a term's note has to delete and re-add. Worth adding if the volume of profile terms grows. ## Tests Migration tests cover the v6 trigger protection at `crates/storage/src/migrations.rs:977-1015` (`migration_v6_trigger_rejects_default_profile_delete`, `migration_v6_trigger_rejects_default_profile_rename`). The CRUD functions themselves do not have dedicated unit tests in `database.rs`; coverage is via the slice-2 integration tests. ## See also - [Schema and migrations (v6, v7, v9)](storage-schema-and-migrations.md) - [Storage overview](storage-overview.md) - [Slice 4 LLM prompt builder](../04-llm-formatting-mcp/README.md) — consumer of `list_profile_terms`.