From c6742c6f6e87e4ad771cf8f3a96124ff99b62c90 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 01:28:54 +0000 Subject: [PATCH] fix(store): MySQL workspaces migration fails on populated tables under strict mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MySQL `projects` migration added `workspaces TEXT NOT NULL` with no DEFAULT, then backfilled in a separate UPDATE. Under strict SQL mode (MySQL 8 default STRICT_TRANS_TABLES) the ADD COLUMN itself aborts on a populated table because TEXT has no implicit default, so the migration fails before the backfill ever runs. Add the column nullable first, backfill, then MODIFY to NOT NULL — works across MySQL 5.7/8.x regardless of sql_mode while preserving the NOT NULL invariant. Mirrors the safe nullable-then-backfill pattern already used for the `automation` column. Also add the missing `ephemeral_tenants` helper: it was referenced from the Postgres and MySQL arms of `connect_all` but defined nowhere, so the crate failed to compile with either SQL feature. Adds the in-memory TenantStore fallback the call sites intended, mirroring the channel-store fallback in the same arms. Fixes #53 https://claude.ai/code/session_01TGN22RyFNGoC1KCPCrWKXn --- crates/harness-store/src/lib.rs | 15 +++++++++++++++ crates/harness-store/src/mysql.rs | 11 +++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/harness-store/src/lib.rs b/crates/harness-store/src/lib.rs index d418dfa..1d78956 100644 --- a/crates/harness-store/src/lib.rs +++ b/crates/harness-store/src/lib.rs @@ -140,6 +140,21 @@ pub struct StoreBundle { pub channel_instances: Arc, } +/// In-memory `TenantStore` fallback for the SQL backends. +/// +/// There's no SQL `TenantStore` impl yet (only JSON-file and SQLite), +/// so the Postgres / MySQL arms fall back to in-memory — tenant rows +/// won't survive restarts under those deployments until a SQL impl +/// lands. Mirrors the channel-store fallback used in the same arms. +#[cfg(any(feature = "postgres", feature = "mysql"))] +fn ephemeral_tenants(backend: &str) -> Arc { + tracing::warn!( + backend, + "no SQL TenantStore impl yet; tenants fall back to in-memory and won't survive restarts" + ); + Arc::new(MemoryTenantStore::new()) as Arc +} + /// Open both stores for a given database URL. The scheme selects the /// backend (see [module docs](crate)). /// diff --git a/crates/harness-store/src/mysql.rs b/crates/harness-store/src/mysql.rs index 3962759..25dbf2d 100644 --- a/crates/harness-store/src/mysql.rs +++ b/crates/harness-store/src/mysql.rs @@ -118,15 +118,22 @@ async fn migrate(pool: &MySqlPool) -> Result<(), StoreError> { .await? > 0; if !has_workspaces { - sqlx::query("ALTER TABLE projects ADD COLUMN workspaces TEXT NOT NULL") + // Add the column nullable first: under strict SQL mode (MySQL 8 + // default STRICT_TRANS_TABLES) an `ADD COLUMN ... TEXT NOT NULL` + // with no default aborts on a populated table because TEXT has no + // implicit default. So add it nullable, backfill, then tighten to + // NOT NULL — this works across MySQL 5.7/8.x regardless of sql_mode. + sqlx::query("ALTER TABLE projects ADD COLUMN workspaces TEXT") .execute(pool) .await?; - // Backfill so the NOT NULL constraint is satisfied for legacy rows. sqlx::query( "UPDATE projects SET workspaces = '[]' WHERE workspaces IS NULL OR workspaces = ''", ) .execute(pool) .await?; + sqlx::query("ALTER TABLE projects MODIFY COLUMN workspaces TEXT NOT NULL") + .execute(pool) + .await?; } let has_columns: bool = sqlx::query_scalar::<_, i64>( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS