From 6ebcf8deff4c8ea51fef87995c98f2dff51b7ad5 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Sun, 29 Mar 2026 22:51:15 +0100 Subject: [PATCH 1/5] Renumber migrations after the WIT default shift Make `V12__wasm_wit_default_0_3_0.sql` the PostgreSQL migration that updates the default WIT version, then move the token-budget and redundant-index migrations to `V13` and `V14` so the sequence stays strictly ordered. Align the libSQL incremental migration table and its tests with the new version numbers, and update the libSQL integration test to assert the renumbered migration marker. --- ..._0.sql => V12__wasm_wit_default_0_3_0.sql} | 0 ...n_budget.sql => V13__job_token_budget.sql} | 0 ..._drop_redundant_wasm_tools_name_index.sql} | 0 src/db/libsql_migrations.rs | 34 +++++++++---------- tests/db_integration/libsql_wit_defaults.rs | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) rename migrations/{V14__wasm_wit_default_0_3_0.sql => V12__wasm_wit_default_0_3_0.sql} (100%) rename migrations/{V12__job_token_budget.sql => V13__job_token_budget.sql} (100%) rename migrations/{V13__drop_redundant_wasm_tools_name_index.sql => V14__drop_redundant_wasm_tools_name_index.sql} (100%) diff --git a/migrations/V14__wasm_wit_default_0_3_0.sql b/migrations/V12__wasm_wit_default_0_3_0.sql similarity index 100% rename from migrations/V14__wasm_wit_default_0_3_0.sql rename to migrations/V12__wasm_wit_default_0_3_0.sql diff --git a/migrations/V12__job_token_budget.sql b/migrations/V13__job_token_budget.sql similarity index 100% rename from migrations/V12__job_token_budget.sql rename to migrations/V13__job_token_budget.sql diff --git a/migrations/V13__drop_redundant_wasm_tools_name_index.sql b/migrations/V14__drop_redundant_wasm_tools_name_index.sql similarity index 100% rename from migrations/V13__drop_redundant_wasm_tools_name_index.sql rename to migrations/V14__drop_redundant_wasm_tools_name_index.sql diff --git a/src/db/libsql_migrations.rs b/src/db/libsql_migrations.rs index ed1187eea..12db47f17 100644 --- a/src/db/libsql_migrations.rs +++ b/src/db/libsql_migrations.rs @@ -129,7 +129,7 @@ END; "#, ), ( - 10, + 12, "wasm_wit_default_0_3_0", // Update existing databases that still default newly inserted wasm tool // and channel rows to the historical 0.1.0 WIT version. This rebuilds @@ -137,10 +137,10 @@ END; // // `legacy_alter_table=ON` is required so child foreign keys keep pointing // at `wasm_tools` while we rename the old table out of the way. - "-- generated by v10_wasm_wit_default_migration_sql()", + "-- generated by v12_wasm_wit_default_migration_sql()", ), ( - 12, + 13, "job_token_budget", // Add token budget tracking columns to agent_jobs. // SQLite supports ALTER TABLE ADD COLUMN, so no table rebuild needed. @@ -150,9 +150,9 @@ ALTER TABLE agent_jobs ADD COLUMN total_tokens_used INTEGER NOT NULL DEFAULT 0; "#, ), ( - 13, + 14, "drop_redundant_wasm_tools_name_index", - include_str!("../../migrations/V13__drop_redundant_wasm_tools_name_index.sql"), + include_str!("../../migrations/V14__drop_redundant_wasm_tools_name_index.sql"), ), ]; @@ -187,11 +187,11 @@ pub async fn run_incremental(conn: &libsql::Connection) -> Result<(), crate::err tracing::info!(version, name, "libSQL: applying incremental migration"); - // V10 contains its own `BEGIN IMMEDIATE`/`COMMIT` block and sets + // V12 contains its own `BEGIN IMMEDIATE`/`COMMIT` block and sets // PRAGMAs that must execute outside a transaction, so bypass the // outer transaction wrapper. - if version == 10 { - let sql = v10_wasm_wit_default_migration_sql(); + if version == 12 { + let sql = v12_wasm_wit_default_migration_sql(); apply_non_transactional_migration(conn, version, name, &sql).await?; tracing::info!(version, name, "libSQL: migration applied successfully"); continue; @@ -302,7 +302,7 @@ fn append_table_rebuild_sql( sql.push_str("\n\n"); } -fn v10_wasm_wit_default_migration_sql() -> String { +fn v12_wasm_wit_default_migration_sql() -> String { let mut sql = String::from( "PRAGMA legacy_alter_table=ON;\nPRAGMA foreign_keys=OFF;\nBEGIN IMMEDIATE;\n\n", ); @@ -328,7 +328,7 @@ fn v10_wasm_wit_default_migration_sql() -> String { #[cfg(test)] mod tests { - use super::{INCREMENTAL_MIGRATIONS, SCHEMA, v10_wasm_wit_default_migration_sql}; + use super::{INCREMENTAL_MIGRATIONS, SCHEMA, v12_wasm_wit_default_migration_sql}; #[test] fn schema_uses_current_wit_defaults_for_new_wasm_records() { @@ -348,22 +348,22 @@ mod tests { fn incremental_migrations_upgrade_existing_wasm_wit_defaults_to_0_3_0() { let (_, _, sql_marker) = INCREMENTAL_MIGRATIONS .iter() - .find(|(version, _, _)| *version == 10) - .expect("expected a V10 libSQL migration for stale wasm wit_version defaults"); + .find(|(version, _, _)| *version == 12) + .expect("expected a V12 libSQL migration for stale wasm wit_version defaults"); assert_eq!( - *sql_marker, "-- generated by v10_wasm_wit_default_migration_sql()", - "expected V10 migration entry to use the shared SQL builder" + *sql_marker, "-- generated by v12_wasm_wit_default_migration_sql()", + "expected V12 migration entry to use the shared SQL builder" ); - let sql = v10_wasm_wit_default_migration_sql(); + let sql = v12_wasm_wit_default_migration_sql(); assert!( sql.contains("0.3.0"), - "expected V10 libSQL migration to set wasm wit_version defaults to 0.3.0" + "expected V12 libSQL migration to set wasm wit_version defaults to 0.3.0" ); assert!( !sql.contains("wit_version TEXT NOT NULL DEFAULT '0.1.0'"), - "expected V10 libSQL migration to remove stale 0.1.0 wit_version defaults" + "expected V12 libSQL migration to remove stale 0.1.0 wit_version defaults" ); assert!( !sql.contains("INSERT INTO _migrations"), diff --git a/tests/db_integration/libsql_wit_defaults.rs b/tests/db_integration/libsql_wit_defaults.rs index 78875bec0..d5918bea4 100644 --- a/tests/db_integration/libsql_wit_defaults.rs +++ b/tests/db_integration/libsql_wit_defaults.rs @@ -136,7 +136,7 @@ async fn libsql_run_migrations_upgrades_legacy_wasm_wit_defaults() { let mut migration_rows = conn .query( "SELECT name FROM _migrations WHERE version = ?1", - libsql::params![10], + libsql::params![12], ) .await .expect("query migration marker"); From a29a3d222648ad658f0844d42976528072050b7f Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Sun, 29 Mar 2026 23:23:37 +0100 Subject: [PATCH 2/5] Strengthen libSQL migration ordering assertions Expand the libSQL WIT-default integration test so it asserts the old `V10` migration marker is absent and the renumbered `V12`, `V13`, and `V14` markers all exist with the expected names. Keep the renumbering fix covered by a stronger regression check so a future divergence between PostgreSQL and libSQL migration sequences fails loudly. --- tests/db_integration/libsql_wit_defaults.rs | 46 ++++++++++++++++----- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/tests/db_integration/libsql_wit_defaults.rs b/tests/db_integration/libsql_wit_defaults.rs index d5918bea4..279513d04 100644 --- a/tests/db_integration/libsql_wit_defaults.rs +++ b/tests/db_integration/libsql_wit_defaults.rs @@ -133,18 +133,44 @@ async fn libsql_run_migrations_upgrades_legacy_wasm_wit_defaults() { let channel_wit_version: String = channel_row.get(0).expect("channel wit_version"); assert_eq!(channel_wit_version, "0.3.0"); - let mut migration_rows = conn + let mut old_version_rows = conn .query( "SELECT name FROM _migrations WHERE version = ?1", - libsql::params![12], + libsql::params![10], ) .await - .expect("query migration marker"); - let migration_row = migration_rows - .next() - .await - .expect("migration rows") - .expect("migration row"); - let migration_name: String = migration_row.get(0).expect("migration name"); - assert_eq!(migration_name, "wasm_wit_default_0_3_0"); + .expect("query migration marker for version 10"); + assert!( + old_version_rows + .next() + .await + .expect("read migration row for version 10") + .is_none(), + "unexpected _migrations row for version 10; migration numbering regressed" + ); + + for (version, expected_name) in [ + (12_i64, "wasm_wit_default_0_3_0"), + (13_i64, "job_token_budget"), + (14_i64, "drop_redundant_wasm_tools_name_index"), + ] { + let mut migration_rows = conn + .query( + "SELECT name FROM _migrations WHERE version = ?1", + libsql::params![version], + ) + .await + .expect("query migration marker"); + let migration_row = migration_rows + .next() + .await + .expect("read migration row for expected version") + .unwrap_or_else(|| { + panic!( + "expected _migrations row for version {version}; migration sequences diverged" + ) + }); + let migration_name: String = migration_row.get(0).expect("migration name"); + assert_eq!(migration_name, expected_name); + } } From df3e514f09cd76578738b8488397cfbdc88708ae Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Sun, 29 Mar 2026 23:37:30 +0100 Subject: [PATCH 3/5] Clarify libSQL migration version gaps Document why `INCREMENTAL_MIGRATIONS` jumps from `9` to `12` in the libSQL backend. Explain that `wasm_versioning` and `conversation_unique_indexes` are already baked into `libsql_schema.sql`, so maintainers do not try to add backend-specific placeholder entries when extending the migration sequence. --- src/db/libsql_migrations.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/db/libsql_migrations.rs b/src/db/libsql_migrations.rs index 12db47f17..1e418c094 100644 --- a/src/db/libsql_migrations.rs +++ b/src/db/libsql_migrations.rs @@ -65,6 +65,13 @@ const V10_WASM_CHANNELS_COPY_COLUMNS: &str = r#"id, user_id, name, version, wit_ /// /// Each entry is `(version, name, sql)`. Migrations are idempotent: the /// `_migrations` table tracks which versions have been applied. +/// +/// `INCREMENTAL_MIGRATIONS` intentionally jumps from versions 9 -> 12 for the +/// libSQL backend. The PostgreSQL `wasm_versioning` (V10) and +/// `conversation_unique_indexes` (V11) migrations are already baked into +/// `libsql_schema.sql` rather than applied incrementally, so the gap is +/// backend-specific and should not be "filled in" when editing +/// `INCREMENTAL_MIGRATIONS` or adding later versions. pub const INCREMENTAL_MIGRATIONS: &[(i64, &str, &str)] = &[ ( 9, From 736fef0b504cbc2cdf6f62f18f7c248c19b03453 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Mon, 30 Mar 2026 00:08:23 +0100 Subject: [PATCH 4/5] Rename libSQL v12 rebuild constants Rename the libSQL rebuild SQL constants that feed v12_wasm_wit_default_migration_sql so their names match the migration version that still uses them. This removes stale V10-prefixed identifiers that were left behind after migration renumbering and makes the v12 migration wiring easier to follow. --- src/db/libsql_migrations.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/db/libsql_migrations.rs b/src/db/libsql_migrations.rs index 1e418c094..fd07e5ec5 100644 --- a/src/db/libsql_migrations.rs +++ b/src/db/libsql_migrations.rs @@ -21,7 +21,7 @@ /// - PL/pgSQL functions -> SQLite triggers pub const SCHEMA: &str = include_str!("../../migrations/libsql_schema.sql"); -const V10_WASM_TOOLS_COLUMNS: &str = r#" id TEXT PRIMARY KEY, +const V12_WASM_TOOLS_COLUMNS: &str = r#" id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, version TEXT NOT NULL DEFAULT '1.0.0', @@ -37,14 +37,14 @@ const V10_WASM_TOOLS_COLUMNS: &str = r#" id TEXT PRIMARY KEY, updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), UNIQUE (user_id, name, version)"#; -const V10_WASM_TOOLS_COPY_COLUMNS: &str = r#"id, user_id, name, version, wit_version, description, wasm_binary, binary_hash, +const V12_WASM_TOOLS_COPY_COLUMNS: &str = r#"id, user_id, name, version, wit_version, description, wasm_binary, binary_hash, parameters_schema, source_url, trust_level, status, created_at, updated_at"#; -const V10_WASM_TOOLS_POST_REBUILD_SQL: &str = r#"CREATE INDEX IF NOT EXISTS idx_wasm_tools_user ON wasm_tools(user_id); +const V12_WASM_TOOLS_POST_REBUILD_SQL: &str = r#"CREATE INDEX IF NOT EXISTS idx_wasm_tools_user ON wasm_tools(user_id); CREATE INDEX IF NOT EXISTS idx_wasm_tools_status ON wasm_tools(status); CREATE INDEX IF NOT EXISTS idx_wasm_tools_trust ON wasm_tools(trust_level);"#; -const V10_WASM_CHANNELS_COLUMNS: &str = r#" id TEXT PRIMARY KEY, +const V12_WASM_CHANNELS_COLUMNS: &str = r#" id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, version TEXT NOT NULL DEFAULT '0.1.0', @@ -58,7 +58,7 @@ const V10_WASM_CHANNELS_COLUMNS: &str = r#" id TEXT PRIMARY KEY, updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), UNIQUE (user_id, name)"#; -const V10_WASM_CHANNELS_COPY_COLUMNS: &str = r#"id, user_id, name, version, wit_version, description, wasm_binary, binary_hash, +const V12_WASM_CHANNELS_COPY_COLUMNS: &str = r#"id, user_id, name, version, wit_version, description, wasm_binary, binary_hash, capabilities_json, status, created_at, updated_at"#; /// Incremental migrations applied after the base schema. @@ -317,15 +317,15 @@ fn v12_wasm_wit_default_migration_sql() -> String { append_table_rebuild_sql( &mut sql, "wasm_tools", - V10_WASM_TOOLS_COLUMNS, - V10_WASM_TOOLS_COPY_COLUMNS, - Some(V10_WASM_TOOLS_POST_REBUILD_SQL), + V12_WASM_TOOLS_COLUMNS, + V12_WASM_TOOLS_COPY_COLUMNS, + Some(V12_WASM_TOOLS_POST_REBUILD_SQL), ); append_table_rebuild_sql( &mut sql, "wasm_channels", - V10_WASM_CHANNELS_COLUMNS, - V10_WASM_CHANNELS_COPY_COLUMNS, + V12_WASM_CHANNELS_COLUMNS, + V12_WASM_CHANNELS_COPY_COLUMNS, None, ); From d76f4dc7c9f36b4d39920ed3ea7d7ac866664c34 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Mon, 30 Mar 2026 00:29:50 +0100 Subject: [PATCH 5/5] Extract libSQL WIT migration test helpers Extract the repeated insert and assertion steps from `libsql_run_migrations_upgrades_legacy_wasm_wit_defaults` into free async helpers in the same integration test file. This keeps the legacy schema fixture unchanged while shrinking the test body down to setup, migration execution, and the expected helper calls. --- tests/db_integration/libsql_wit_defaults.rs | 118 +++++++++++--------- 1 file changed, 64 insertions(+), 54 deletions(-) diff --git a/tests/db_integration/libsql_wit_defaults.rs b/tests/db_integration/libsql_wit_defaults.rs index 279513d04..62b246243 100644 --- a/tests/db_integration/libsql_wit_defaults.rs +++ b/tests/db_integration/libsql_wit_defaults.rs @@ -50,20 +50,7 @@ CREATE TABLE IF NOT EXISTS wasm_channels ( ); "#; -#[tokio::test] -async fn libsql_run_migrations_upgrades_legacy_wasm_wit_defaults() { - let dir = tempfile::tempdir().expect("temp dir"); - let db_path = dir.path().join("legacy-wit-defaults.db"); - let backend = LibSqlBackend::new_local(&db_path).await.expect("backend"); - - let conn = backend.connect().await.expect("connect"); - conn.execute_batch(LEGACY_WASM_WIT_SCHEMA) - .await - .expect("seed legacy schema"); - - backend.run_migrations().await.expect("run migrations"); - - let conn = backend.connect().await.expect("connect after migration"); +async fn insert_test_wasm_tool(conn: &libsql::Connection) { conn.execute( r#" INSERT INTO wasm_tools ( @@ -83,7 +70,9 @@ async fn libsql_run_migrations_upgrades_legacy_wasm_wit_defaults() { ) .await .expect("insert wasm tool without wit_version"); +} +async fn insert_test_wasm_channel(conn: &libsql::Connection) { conn.execute( r#" INSERT INTO wasm_channels ( @@ -102,75 +91,96 @@ async fn libsql_run_migrations_upgrades_legacy_wasm_wit_defaults() { ) .await .expect("insert wasm channel without wit_version"); +} - let mut tool_rows = conn - .query( - "SELECT wit_version FROM wasm_tools WHERE id = ?1", - libsql::params!["tool-1"], - ) +async fn assert_wit_version(conn: &libsql::Connection, table: &str, id: &str, expected: &str) { + let query = format!("SELECT wit_version FROM {table} WHERE id = ?1"); + let mut rows = conn + .query(&query, libsql::params![id]) .await - .expect("query tool"); - let tool_row = tool_rows + .unwrap_or_else(|error| panic!("query {table} wit_version: {error}")); + let row = rows .next() .await - .expect("tool rows") - .expect("tool row"); - let tool_wit_version: String = tool_row.get(0).expect("tool wit_version"); - assert_eq!(tool_wit_version, "0.3.0"); - - let mut channel_rows = conn - .query( - "SELECT wit_version FROM wasm_channels WHERE id = ?1", - libsql::params!["channel-1"], - ) - .await - .expect("query channel"); - let channel_row = channel_rows - .next() - .await - .expect("channel rows") - .expect("channel row"); - let channel_wit_version: String = channel_row.get(0).expect("channel wit_version"); - assert_eq!(channel_wit_version, "0.3.0"); + .unwrap_or_else(|error| panic!("read {table} wit_version row: {error}")) + .unwrap_or_else(|| panic!("missing {table} row for id {id}")); + let actual: String = row + .get(0) + .unwrap_or_else(|error| panic!("read {table} wit_version value: {error}")); + assert_eq!(actual, expected); +} +async fn assert_migration_absent(conn: &libsql::Connection, version: i64) { let mut old_version_rows = conn .query( "SELECT name FROM _migrations WHERE version = ?1", - libsql::params![10], + libsql::params![version], ) .await - .expect("query migration marker for version 10"); + .unwrap_or_else(|error| panic!("query migration marker for version {version}: {error}")); assert!( old_version_rows .next() .await - .expect("read migration row for version 10") + .unwrap_or_else(|error| panic!("read migration row for version {version}: {error}")) .is_none(), - "unexpected _migrations row for version 10; migration numbering regressed" + "unexpected _migrations row for version {version}; migration numbering regressed" ); +} - for (version, expected_name) in [ - (12_i64, "wasm_wit_default_0_3_0"), - (13_i64, "job_token_budget"), - (14_i64, "drop_redundant_wasm_tools_name_index"), - ] { +async fn assert_migration_entries(conn: &libsql::Connection, entries: &[(i64, &str)]) { + for (version, expected_name) in entries { let mut migration_rows = conn .query( "SELECT name FROM _migrations WHERE version = ?1", - libsql::params![version], + libsql::params![*version], ) .await - .expect("query migration marker"); + .unwrap_or_else(|error| { + panic!("query migration marker for version {version}: {error}") + }); let migration_row = migration_rows .next() .await - .expect("read migration row for expected version") + .unwrap_or_else(|error| panic!("read migration row for version {version}: {error}")) .unwrap_or_else(|| { panic!( "expected _migrations row for version {version}; migration sequences diverged" ) }); - let migration_name: String = migration_row.get(0).expect("migration name"); - assert_eq!(migration_name, expected_name); + let migration_name: String = migration_row + .get(0) + .unwrap_or_else(|error| panic!("read migration name for version {version}: {error}")); + assert_eq!(migration_name, *expected_name); } } + +#[tokio::test] +async fn libsql_run_migrations_upgrades_legacy_wasm_wit_defaults() { + let dir = tempfile::tempdir().expect("temp dir"); + let db_path = dir.path().join("legacy-wit-defaults.db"); + let backend = LibSqlBackend::new_local(&db_path).await.expect("backend"); + + let conn = backend.connect().await.expect("connect"); + conn.execute_batch(LEGACY_WASM_WIT_SCHEMA) + .await + .expect("seed legacy schema"); + + backend.run_migrations().await.expect("run migrations"); + + let conn = backend.connect().await.expect("connect after migration"); + insert_test_wasm_tool(&conn).await; + insert_test_wasm_channel(&conn).await; + assert_wit_version(&conn, "wasm_tools", "tool-1", "0.3.0").await; + assert_wit_version(&conn, "wasm_channels", "channel-1", "0.3.0").await; + assert_migration_absent(&conn, 10).await; + assert_migration_entries( + &conn, + &[ + (12_i64, "wasm_wit_default_0_3_0"), + (13_i64, "job_token_budget"), + (14_i64, "drop_redundant_wasm_tools_name_index"), + ], + ) + .await; +}