Skip to content

Commit 8940f56

Browse files
ilblackdragonclaude
authored andcommitted
fix(db): resolve V15 migration numbering conflict (#1923)
* fix(db): resolve V15 migration numbering conflict between user_identities and conversation_source_channel A merge conflict left two PostgreSQL migrations at V15. This renumbers them (V15=user_identities, V16=conversation_source_channel, V17=document_versions) to match the libSQL incremental ordering. Adds user_identities, document_versions, and source_channel to the libSQL base schema so fresh databases get all tables. Includes a one-time repair for existing databases where V15 was mis-recorded. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix rustfmt formatting in repair_misnumbered_v15 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(db): address PR review — proper error handling and tighter repair condition - Replace .ok().flatten() with explicit error propagation via .map_err()? so DB errors during V15 repair are surfaced, not silently swallowed - Tighten repair condition from `!= "user_identities"` to `== "document_versions"` to only fix the specific known-bad case from the merge conflict Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d2c2150 commit 8940f56

4 files changed

Lines changed: 142 additions & 1 deletion

File tree

File renamed without changes.

src/db/libsql/identities.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,4 +415,58 @@ mod tests {
415415
.unwrap();
416416
assert!(found_identity.is_some());
417417
}
418+
419+
/// Regression: an earlier release recorded V15 as "document_versions"
420+
/// instead of "user_identities", so the table was never created.
421+
/// Verify that `run_migrations` repairs this and creates the table.
422+
#[tokio::test]
423+
async fn test_v15_misnumbered_repair() {
424+
let dir = tempfile::tempdir().unwrap();
425+
let db_path = dir.path().join("test_v15_repair.db");
426+
let backend = LibSqlBackend::new_local(&db_path).await.unwrap();
427+
backend.run_migrations().await.unwrap();
428+
429+
// Simulate the bug: drop user_identities and re-record V15 with wrong name
430+
let conn = backend.connect().await.unwrap();
431+
conn.execute_batch("DROP TABLE IF EXISTS user_identities")
432+
.await
433+
.unwrap();
434+
conn.execute(
435+
"UPDATE _migrations SET name = 'document_versions' WHERE version = 15",
436+
libsql::params![],
437+
)
438+
.await
439+
.unwrap();
440+
441+
// Confirm the table is gone
442+
let err = conn
443+
.query("SELECT 1 FROM user_identities LIMIT 1", ())
444+
.await;
445+
assert!(err.is_err(), "user_identities should not exist yet");
446+
447+
// Re-run migrations — the repair should fix V15
448+
drop(conn);
449+
backend.run_migrations().await.unwrap();
450+
451+
// Table should now exist and be queryable
452+
let conn = backend.connect().await.unwrap();
453+
let mut rows = conn
454+
.query("SELECT 1 FROM user_identities LIMIT 1", ())
455+
.await
456+
.unwrap();
457+
// No rows is fine — just verifying the table exists without error
458+
let _ = rows.next().await;
459+
460+
// Verify V15 is now recorded correctly
461+
let mut rows = conn
462+
.query(
463+
"SELECT name FROM _migrations WHERE version = 15",
464+
libsql::params![],
465+
)
466+
.await
467+
.unwrap();
468+
let row = rows.next().await.unwrap().unwrap();
469+
let name: String = row.get(0).unwrap();
470+
assert_eq!(name, "user_identities");
471+
}
418472
}

src/db/libsql_migrations.rs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ CREATE TABLE IF NOT EXISTS conversations (
3838
thread_id TEXT,
3939
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
4040
last_activity TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
41-
metadata TEXT NOT NULL DEFAULT '{}'
41+
metadata TEXT NOT NULL DEFAULT '{}',
42+
source_channel TEXT
4243
);
4344
4445
CREATE INDEX IF NOT EXISTS idx_conversations_channel ON conversations(channel);
@@ -609,6 +610,41 @@ CREATE TABLE IF NOT EXISTS api_tokens (
609610
CREATE INDEX IF NOT EXISTS idx_api_tokens_user ON api_tokens(user_id);
610611
CREATE INDEX IF NOT EXISTS idx_api_tokens_hash ON api_tokens(token_hash);
611612
613+
-- ==================== User identities (V15) ====================
614+
615+
CREATE TABLE IF NOT EXISTS user_identities (
616+
id TEXT PRIMARY KEY,
617+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
618+
provider TEXT NOT NULL,
619+
provider_user_id TEXT NOT NULL,
620+
email TEXT,
621+
email_verified INTEGER NOT NULL DEFAULT 0,
622+
display_name TEXT,
623+
avatar_url TEXT,
624+
raw_profile TEXT NOT NULL DEFAULT '{}',
625+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
626+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
627+
UNIQUE (provider, provider_user_id)
628+
);
629+
CREATE INDEX IF NOT EXISTS idx_user_identities_user ON user_identities(user_id);
630+
CREATE INDEX IF NOT EXISTS idx_user_identities_email ON user_identities(email) WHERE email IS NOT NULL;
631+
632+
-- ==================== Document versions (V17) ====================
633+
634+
CREATE TABLE IF NOT EXISTS memory_document_versions (
635+
id TEXT PRIMARY KEY,
636+
document_id TEXT NOT NULL REFERENCES memory_documents(id) ON DELETE CASCADE,
637+
version INTEGER NOT NULL,
638+
content TEXT NOT NULL,
639+
content_hash TEXT NOT NULL,
640+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
641+
changed_by TEXT,
642+
UNIQUE(document_id, version)
643+
);
644+
645+
CREATE INDEX IF NOT EXISTS idx_doc_versions_lookup
646+
ON memory_document_versions(document_id, version DESC);
647+
612648
"#;
613649

614650
/// Incremental migrations applied after the base schema.
@@ -868,13 +904,64 @@ async fn column_exists(
868904
Ok(rows.next().await.ok().flatten().is_some())
869905
}
870906

907+
/// Repair databases where V15 was recorded as "document_versions" instead of
908+
/// "user_identities" due to a migration numbering error in an earlier release.
909+
/// Deletes the stale _migrations row so V15 reruns with the correct SQL.
910+
async fn repair_misnumbered_v15(
911+
conn: &libsql::Connection,
912+
) -> Result<(), crate::error::DatabaseError> {
913+
use crate::error::DatabaseError;
914+
915+
let mut rows = conn
916+
.query(
917+
"SELECT name FROM _migrations WHERE version = 15",
918+
libsql::params![],
919+
)
920+
.await
921+
.map_err(|e| DatabaseError::Migration(format!("V15 repair check failed: {e}")))?;
922+
923+
let maybe_row = rows
924+
.next()
925+
.await
926+
.map_err(|e| DatabaseError::Migration(format!("V15 repair: failed to fetch row: {e}")))?;
927+
if let Some(row) = maybe_row {
928+
let name: String = row.get(0).map_err(|e| {
929+
DatabaseError::Migration(format!("V15 repair: failed to read name: {e}"))
930+
})?;
931+
if name == "document_versions" {
932+
// V15 was recorded with the wrong name due to a merge-conflict
933+
// misnumbering — the user_identities CREATE TABLE never ran.
934+
// Delete the stale record so the migration loop will reapply it.
935+
tracing::warn!(
936+
recorded_name = %name,
937+
"libSQL: V15 was mis-recorded as document_versions; deleting stale _migrations row to reapply"
938+
);
939+
conn.execute(
940+
"DELETE FROM _migrations WHERE version = 15",
941+
libsql::params![],
942+
)
943+
.await
944+
.map_err(|e| {
945+
DatabaseError::Migration(format!("V15 repair: failed to delete stale row: {e}"))
946+
})?;
947+
}
948+
}
949+
Ok(())
950+
}
951+
871952
/// Run incremental migrations that haven't been applied yet.
872953
///
873954
/// Each migration is wrapped in a transaction. On success the version is
874955
/// recorded in `_migrations` so it won't run again.
875956
pub async fn run_incremental(conn: &libsql::Connection) -> Result<(), crate::error::DatabaseError> {
876957
use crate::error::DatabaseError;
877958

959+
// Repair: an earlier release mis-numbered V15 as "document_versions"
960+
// instead of "user_identities", so the user_identities CREATE TABLE
961+
// never ran. If V15 is recorded but the table doesn't exist, delete
962+
// the stale record so V15 reruns with the correct SQL.
963+
repair_misnumbered_v15(conn).await?;
964+
878965
let mut applied_count = 0;
879966
for &(version, name, sql) in INCREMENTAL_MIGRATIONS {
880967
// Check if already applied

0 commit comments

Comments
 (0)