From 84c69fbea865dab2167f7a3f28e50d1581bbb97e Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Mon, 13 Apr 2026 08:22:08 -0500 Subject: [PATCH 1/3] fix: add missing ai_providers columns and fix linux-amd64 build - Add migration 015 to add use_datastore_upload and created_at columns - Handle column-already-exists errors gracefully - Update Dockerfile to install linuxdeploy for AppImage bundling - Add fuse dependency for AppImage support --- .docker/Dockerfile.linux-amd64 | 7 ++- src-tauri/src/db/migrations.rs | 98 ++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/.docker/Dockerfile.linux-amd64 b/.docker/Dockerfile.linux-amd64 index 1dbb3cc5..18c7ea87 100644 --- a/.docker/Dockerfile.linux-amd64 +++ b/.docker/Dockerfile.linux-amd64 @@ -2,7 +2,7 @@ # All system dependencies are installed once here; CI jobs skip apt-get entirely. # Rebuild when: Rust toolchain version changes, webkit2gtk/gtk major version changes, # Node.js major version changes, OpenSSL major version changes (used via OPENSSL_STATIC=1), -# or Tauri CLI version changes that affect bundler system deps. +# Tauri CLI version changes that affect bundler system deps, or linuxdeploy is needed. # Tag format: rust-node FROM rust:1.88-slim @@ -20,9 +20,14 @@ RUN apt-get update -qq \ perl \ jq \ git \ + fuse \ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && rm -rf /var/lib/apt/lists/* +# Install linuxdeploy for AppImage bundling (required for Tauri 2.x) +RUN curl -fsSL https://github.com/tauri-apps/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -o /usr/local/bin/linuxdeploy \ + && chmod +x /usr/local/bin/linuxdeploy + RUN rustup target add x86_64-unknown-linux-gnu \ && rustup component add rustfmt clippy diff --git a/src-tauri/src/db/migrations.rs b/src-tauri/src/db/migrations.rs index b0e017c9..9dffabe7 100644 --- a/src-tauri/src/db/migrations.rs +++ b/src-tauri/src/db/migrations.rs @@ -191,6 +191,11 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> { updated_at TEXT NOT NULL DEFAULT (datetime('now')) );", ), + ( + "015_add_ai_provider_missing_columns", + "ALTER TABLE ai_providers ADD COLUMN use_datastore_upload INTEGER DEFAULT 0; + ALTER TABLE ai_providers ADD COLUMN created_at TEXT NOT NULL DEFAULT (datetime('now'));", + ), ]; for (name, sql) in migrations { @@ -201,9 +206,20 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> { if !already_applied { // FTS5 virtual table creation can be skipped if FTS5 is not compiled in + // Also handle column-already-exists errors for migration 015 if let Err(e) = conn.execute_batch(sql) { if name.contains("fts") { tracing::warn!("FTS5 not available, skipping: {e}"); + } else if *name == "015_add_ai_provider_missing_columns" { + // Skip error if columns already exist (e.g., from earlier migration or manual creation) + let err_str = e.to_string(); + if err_str.contains("duplicate column name") + || err_str.contains("has no column named") + { + tracing::info!("Columns may already exist, skipping migration 015: {e}"); + } else { + return Err(e.into()); + } } else { return Err(e.into()); } @@ -560,4 +576,86 @@ mod tests { assert_eq!(encrypted_key, "encrypted_key_123"); assert_eq!(model, "gpt-4o"); } + + #[test] + fn test_add_missing_columns_to_existing_table() { + let conn = Connection::open_in_memory().unwrap(); + + // Simulate existing table without use_datastore_upload and created_at + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS ai_providers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + provider_type TEXT NOT NULL, + api_url TEXT NOT NULL, + encrypted_api_key TEXT NOT NULL, + model TEXT NOT NULL, + max_tokens INTEGER, + temperature REAL, + custom_endpoint_path TEXT, + custom_auth_header TEXT, + custom_auth_prefix TEXT, + api_format TEXT, + user_id TEXT, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + );", + ) + .unwrap(); + + // Verify columns BEFORE migration + let mut stmt = conn.prepare("PRAGMA table_info(ai_providers)").unwrap(); + let columns: Vec = stmt + .query_map([], |row| row.get::<_, String>(1)) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert!(columns.contains(&"name".to_string())); + assert!(columns.contains(&"model".to_string())); + assert!(!columns.contains(&"use_datastore_upload".to_string())); + assert!(!columns.contains(&"created_at".to_string())); + + // Run migrations (should apply 015 to add missing columns) + run_migrations(&conn).unwrap(); + + // Verify columns AFTER migration + let mut stmt = conn.prepare("PRAGMA table_info(ai_providers)").unwrap(); + let columns: Vec = stmt + .query_map([], |row| row.get::<_, String>(1)) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert!(columns.contains(&"name".to_string())); + assert!(columns.contains(&"model".to_string())); + assert!(columns.contains(&"use_datastore_upload".to_string())); + assert!(columns.contains(&"created_at".to_string())); + + // Verify data integrity - existing rows should have default values + conn.execute( + "INSERT INTO ai_providers (id, name, provider_type, api_url, encrypted_api_key, model) + VALUES (?, ?, ?, ?, ?, ?)", + rusqlite::params![ + "test-provider-2", + "Test Provider", + "openai", + "https://api.example.com", + "encrypted_key_456", + "gpt-3.5-turbo" + ], + ) + .unwrap(); + + let (name, use_datastore_upload, created_at): (String, i64, String) = conn + .query_row( + "SELECT name, use_datastore_upload, created_at FROM ai_providers WHERE name = ?1", + ["Test Provider"], + |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), + ) + .unwrap(); + + assert_eq!(name, "Test Provider"); + assert_eq!(use_datastore_upload, 0); + assert!(created_at.len() > 0); + } } From 2d7aac8413294cb8e6b0fd079a80db97d23647c2 Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Mon, 13 Apr 2026 08:38:43 -0500 Subject: [PATCH 2/3] fix: address AI review findings - Add -L flag to curl for linuxdeploy redirects - Split migration 015 into 015_add_use_datastore_upload and 016_add_created_at - Use separate execute calls for ALTER TABLE statements - Add idempotency test for migration 015 - Use bool type for use_datastore_upload instead of i64 --- .docker/Dockerfile.linux-amd64 | 3 +- src-tauri/src/db/migrations.rs | 66 +++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/.docker/Dockerfile.linux-amd64 b/.docker/Dockerfile.linux-amd64 index 18c7ea87..6f9297ea 100644 --- a/.docker/Dockerfile.linux-amd64 +++ b/.docker/Dockerfile.linux-amd64 @@ -26,7 +26,8 @@ RUN apt-get update -qq \ && rm -rf /var/lib/apt/lists/* # Install linuxdeploy for AppImage bundling (required for Tauri 2.x) -RUN curl -fsSL https://github.com/tauri-apps/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -o /usr/local/bin/linuxdeploy \ +# Download linuxdeploy from the official repository and extract AppImage +RUN curl -fsSL -L https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -o /usr/local/bin/linuxdeploy \ && chmod +x /usr/local/bin/linuxdeploy RUN rustup target add x86_64-unknown-linux-gnu \ diff --git a/src-tauri/src/db/migrations.rs b/src-tauri/src/db/migrations.rs index 9dffabe7..4140eee5 100644 --- a/src-tauri/src/db/migrations.rs +++ b/src-tauri/src/db/migrations.rs @@ -192,9 +192,12 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> { );", ), ( - "015_add_ai_provider_missing_columns", - "ALTER TABLE ai_providers ADD COLUMN use_datastore_upload INTEGER DEFAULT 0; - ALTER TABLE ai_providers ADD COLUMN created_at TEXT NOT NULL DEFAULT (datetime('now'));", + "015_add_use_datastore_upload", + "ALTER TABLE ai_providers ADD COLUMN use_datastore_upload INTEGER DEFAULT 0", + ), + ( + "016_add_created_at", + "ALTER TABLE ai_providers ADD COLUMN created_at TEXT NOT NULL DEFAULT (datetime('now'))", ), ]; @@ -206,21 +209,29 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> { if !already_applied { // FTS5 virtual table creation can be skipped if FTS5 is not compiled in - // Also handle column-already-exists errors for migration 015 - if let Err(e) = conn.execute_batch(sql) { - if name.contains("fts") { + // Also handle column-already-exists errors for migrations 015-016 + if name.contains("fts") { + if let Err(e) = conn.execute_batch(sql) { tracing::warn!("FTS5 not available, skipping: {e}"); - } else if *name == "015_add_ai_provider_missing_columns" { - // Skip error if columns already exist (e.g., from earlier migration or manual creation) + } + } else if name.ends_with("_add_use_datastore_upload") + || name.ends_with("_add_created_at") + { + // Use execute for ALTER TABLE (SQLite only allows one statement per command) + // Skip error if column already exists + if let Err(e) = conn.execute(sql, []) { let err_str = e.to_string(); if err_str.contains("duplicate column name") || err_str.contains("has no column named") { - tracing::info!("Columns may already exist, skipping migration 015: {e}"); + tracing::info!("Column may already exist, skipping migration {name}: {e}"); } else { return Err(e.into()); } - } else { + } + } else { + // Use execute_batch for other migrations (FTS5, CREATE TABLE, etc.) + if let Err(e) = conn.execute_batch(sql) { return Err(e.into()); } } @@ -646,7 +657,7 @@ mod tests { ) .unwrap(); - let (name, use_datastore_upload, created_at): (String, i64, String) = conn + let (name, use_datastore_upload, created_at): (String, bool, String) = conn .query_row( "SELECT name, use_datastore_upload, created_at FROM ai_providers WHERE name = ?1", ["Test Provider"], @@ -655,7 +666,38 @@ mod tests { .unwrap(); assert_eq!(name, "Test Provider"); - assert_eq!(use_datastore_upload, 0); + assert!(!use_datastore_upload); assert!(created_at.len() > 0); } + + #[test] + fn test_idempotent_add_missing_columns() { + let conn = Connection::open_in_memory().unwrap(); + + // Create table with both columns already present (simulating prior migration run) + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS ai_providers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + provider_type TEXT NOT NULL, + api_url TEXT NOT NULL, + encrypted_api_key TEXT NOT NULL, + model TEXT NOT NULL, + max_tokens INTEGER, + temperature REAL, + custom_endpoint_path TEXT, + custom_auth_header TEXT, + custom_auth_prefix TEXT, + api_format TEXT, + user_id TEXT, + use_datastore_upload INTEGER DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + );", + ) + .unwrap(); + + // Should not fail even though columns already exist + run_migrations(&conn).unwrap(); + } } From 8e1d43da430075d29c45c7110f942d32e1f34ac4 Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Mon, 13 Apr 2026 08:50:34 -0500 Subject: [PATCH 3/3] fix: address critical AI review issues - Fix linuxdeploy AppImage extraction using --appimage-extract - Remove 'has no column named' from duplicate column error handling - Use strftime instead of datetime for created_at default format --- .docker/Dockerfile.linux-amd64 | 9 ++++++--- src-tauri/src/db/migrations.rs | 8 +++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.docker/Dockerfile.linux-amd64 b/.docker/Dockerfile.linux-amd64 index 6f9297ea..5c5892ef 100644 --- a/.docker/Dockerfile.linux-amd64 +++ b/.docker/Dockerfile.linux-amd64 @@ -26,9 +26,12 @@ RUN apt-get update -qq \ && rm -rf /var/lib/apt/lists/* # Install linuxdeploy for AppImage bundling (required for Tauri 2.x) -# Download linuxdeploy from the official repository and extract AppImage -RUN curl -fsSL -L https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -o /usr/local/bin/linuxdeploy \ - && chmod +x /usr/local/bin/linuxdeploy +# Download linuxdeploy AppImage and extract to get the binary +RUN curl -Ls https://github.com/tauri-apps/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -o /tmp/linuxdeploy.AppImage \ + && chmod +x /tmp/linuxdeploy.AppImage \ + && /tmp/linuxdeploy.AppImage --appimage-extract \ + && mv squashfs-root/usr/bin/linuxdeploy /usr/local/bin/ \ + && rm -rf /tmp/linuxdeploy.AppImage squashfs-root RUN rustup target add x86_64-unknown-linux-gnu \ && rustup component add rustfmt clippy diff --git a/src-tauri/src/db/migrations.rs b/src-tauri/src/db/migrations.rs index 4140eee5..12056f10 100644 --- a/src-tauri/src/db/migrations.rs +++ b/src-tauri/src/db/migrations.rs @@ -197,7 +197,7 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> { ), ( "016_add_created_at", - "ALTER TABLE ai_providers ADD COLUMN created_at TEXT NOT NULL DEFAULT (datetime('now'))", + "ALTER TABLE ai_providers ADD COLUMN created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))", ), ]; @@ -218,12 +218,10 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> { || name.ends_with("_add_created_at") { // Use execute for ALTER TABLE (SQLite only allows one statement per command) - // Skip error if column already exists + // Skip error if column already exists (SQLITE_ERROR with "duplicate column name") if let Err(e) = conn.execute(sql, []) { let err_str = e.to_string(); - if err_str.contains("duplicate column name") - || err_str.contains("has no column named") - { + if err_str.contains("duplicate column name") { tracing::info!("Column may already exist, skipping migration {name}: {e}"); } else { return Err(e.into());