Skip to content

Commit d5610b3

Browse files
committed
fix: migrations bug
1 parent b3ed002 commit d5610b3

File tree

4 files changed

+97
-23
lines changed

4 files changed

+97
-23
lines changed

crates/chat-cli/src/database/mod.rs

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -352,15 +352,12 @@ impl Database {
352352
let mut conn = self.pool.get()?;
353353
let transaction = conn.transaction()?;
354354

355-
// select the max migration id
356-
let max_id = max_migration(&transaction);
355+
let max_version = max_migration_version(&transaction);
357356

358357
for (version, migration) in MIGRATIONS.iter().enumerate() {
359-
// skip migrations that already exist
360-
match max_id {
361-
Some(max_id) if max_id >= version as i64 => continue,
362-
_ => (),
363-
};
358+
if has_migration(&transaction, version, max_version)? {
359+
continue;
360+
}
364361

365362
// execute the migration
366363
transaction.execute_batch(migration.sql)?;
@@ -443,11 +440,51 @@ impl Database {
443440
}
444441
}
445442

446-
fn max_migration<C: Deref<Target = Connection>>(conn: &C) -> Option<i64> {
447-
let mut stmt = conn.prepare("SELECT MAX(id) FROM migrations").ok()?;
443+
fn max_migration_version<C: Deref<Target = Connection>>(conn: &C) -> Option<i64> {
444+
let mut stmt = conn.prepare("SELECT MAX(version) FROM migrations").ok()?;
448445
stmt.query_row([], |row| row.get(0)).ok()
449446
}
450447

448+
fn has_migration<C: Deref<Target = Connection>>(
449+
conn: &C,
450+
version: usize,
451+
max_version: Option<i64>,
452+
) -> Result<bool, DatabaseError> {
453+
// IMPORTANT: Due to a bug with the first 7 migrations, we have to check manually
454+
//
455+
// Background: the migrations table stores two identifying keys: the sqlite auto-generated
456+
// auto-incrementing key `id`, and the `version` which is the index of the `MIGRATIONS`
457+
// constant.
458+
//
459+
// Checking whether a migration exists would compare id with version, but since id is 1-indexed
460+
// and version is 0-indexed, we would actually skip the last migration! Therefore, it's
461+
// possible users are missing a critical migration (namely, auth_kv table creation) when
462+
// upgrading to the qchat build (which includes two new migrations). Hence, we have to check
463+
// all migrations until version 7 to make sure that nothing is missed.
464+
if version <= 7 {
465+
let mut stmt = match conn.prepare("SELECT COUNT(*) FROM migrations WHERE version = ?1") {
466+
Ok(stmt) => stmt,
467+
// If the migrations table does not exist, then we can reasonably say no migrations
468+
// will exist.
469+
Err(Error::SqliteFailure(_, Some(msg))) if msg.contains("no such table") => {
470+
return Ok(false);
471+
},
472+
Err(err) => return Err(err.into()),
473+
};
474+
let count: i32 = stmt.query_row([version], |row| row.get(0))?;
475+
return Ok(count >= 1);
476+
}
477+
478+
// Continuing from the previously implemented logic - any migrations after the 7th can have a simple
479+
// maximum version check, since we can reasonably assume if any version >=7 will have all
480+
// migrations prior to it.
481+
#[allow(clippy::match_like_matches_macro)]
482+
Ok(match max_version {
483+
Some(max_version) if max_version >= version as i64 => true,
484+
_ => false,
485+
})
486+
}
487+
451488
#[cfg(test)]
452489
mod tests {
453490
use super::*;
@@ -477,7 +514,7 @@ mod tests {
477514
let db = Database::new().await.unwrap();
478515

479516
// assert migration count is correct
480-
let max_migration = max_migration(&&*db.pool.get().unwrap());
517+
let max_migration = max_migration_version(&&*db.pool.get().unwrap());
481518
assert_eq!(max_migration, Some(MIGRATIONS.len() as i64));
482519
}
483520

crates/chat-cli/src/util/consts.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#[cfg(windows)]
22
pub const APP_PROCESS_NAME: &str = "q_desktop.exe";
33

4-
/// TODO(brandonskiser): revert back to "q" for prompting login after standalone releases.
4+
/// TODO(brandonskiser): revert back to "qchat" for prompting login after standalone releases.
55
pub const CLI_BINARY_NAME: &str = "q";
66
#[allow(dead_code)]
77
pub const CHAT_BINARY_NAME: &str = "qchat";

crates/fig_settings/src/sqlite/mod.rs

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,12 @@ impl Db {
119119
let mut conn = self.pool.get()?;
120120
let transaction = conn.transaction()?;
121121

122-
// select the max migration id
123-
let max_id = max_migration(&transaction);
122+
let max_version = max_migration_version(&transaction);
124123

125124
for (version, migration) in MIGRATIONS.iter().enumerate() {
126-
// skip migrations that already exist
127-
match max_id {
128-
Some(max_id) if max_id >= version as i64 => continue,
129-
_ => (),
130-
};
125+
if has_migration(&transaction, version, max_version)? {
126+
continue;
127+
}
131128

132129
// execute the migration
133130
transaction.execute_batch(migration.sql)?;
@@ -289,11 +286,47 @@ impl Db {
289286
}
290287
}
291288

292-
fn max_migration<C: Deref<Target = Connection>>(conn: &C) -> Option<i64> {
293-
let mut stmt = conn.prepare("SELECT MAX(id) FROM migrations").ok()?;
289+
fn max_migration_version<C: Deref<Target = Connection>>(conn: &C) -> Option<i64> {
290+
let mut stmt = conn.prepare("SELECT MAX(version) FROM migrations").ok()?;
294291
stmt.query_row([], |row| row.get(0)).ok()
295292
}
296293

294+
fn has_migration<C: Deref<Target = Connection>>(conn: &C, version: usize, max_version: Option<i64>) -> Result<bool> {
295+
// IMPORTANT: Due to a bug with the first 7 migrations, we have to check manually
296+
//
297+
// Background: the migrations table stores two identifying keys: the sqlite auto-generated
298+
// auto-incrementing key `id`, and the `version` which is the index of the `MIGRATIONS`
299+
// constant.
300+
//
301+
// Checking whether a migration exists would compare id with version, but since id is 1-indexed
302+
// and version is 0-indexed, we would actually skip the last migration! Therefore, it's
303+
// possible users are missing a critical migration (namely, auth_kv table creation) when
304+
// upgrading to the qchat build (which includes two new migrations). Hence, we have to check
305+
// all migrations until version 7 to make sure that nothing is missed.
306+
if version <= 7 {
307+
let mut stmt = match conn.prepare("SELECT COUNT(*) FROM migrations WHERE version = ?1") {
308+
Ok(stmt) => stmt,
309+
// If the migrations table does not exist, then we can reasonably say no migrations
310+
// will exist.
311+
Err(Error::SqliteFailure(_, Some(msg))) if msg.contains("no such table") => {
312+
return Ok(false);
313+
},
314+
Err(err) => return Err(err.into()),
315+
};
316+
let count: i32 = stmt.query_row([version], |row| row.get(0))?;
317+
return Ok(count >= 1);
318+
}
319+
320+
// Continuing from the previously implemented logic - any migrations after the 7th can have a simple
321+
// maximum version check, since we can reasonably assume if any version >=7 will have all
322+
// migrations prior to it.
323+
#[allow(clippy::match_like_matches_macro)]
324+
Ok(match max_version {
325+
Some(max_version) if max_version >= version as i64 => true,
326+
_ => false,
327+
})
328+
}
329+
297330
#[cfg(test)]
298331
mod tests {
299332
use super::*;
@@ -309,7 +342,7 @@ mod tests {
309342
let db = mock();
310343

311344
// assert migration count is correct
312-
let max_migration = max_migration(&&*db.pool.get().unwrap());
345+
let max_migration = max_migration_version(&&*db.pool.get().unwrap());
313346
assert_eq!(max_migration, Some(MIGRATIONS.len() as i64));
314347
}
315348

crates/q_cli/src/cli/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ use tokio::signal::ctrl_c;
7070
use tracing::{
7171
Level,
7272
debug,
73+
error,
7374
};
7475

7576
use self::integrations::IntegrationsSubcommands;
@@ -381,10 +382,13 @@ impl Cli {
381382

382383
let secret_store = SecretStore::new().await.ok();
383384
if let Some(secret_store) = secret_store {
384-
if let Ok(database) = database() {
385+
if let Ok(database) = database().map_err(|err| error!(?err, "failed to open database")) {
385386
if let Ok(token) = BuilderIdToken::load(&secret_store, false).await {
386387
if let Ok(token) = serde_json::to_string(&token) {
387-
database.set_auth_value("codewhisperer:odic:token", token).ok();
388+
database
389+
.set_auth_value("codewhisperer:odic:token", token)
390+
.map_err(|err| error!(?err, "failed to write credentials to auth db"))
391+
.ok();
388392
}
389393
}
390394
}

0 commit comments

Comments
 (0)