Files
codex/codex-rs/cli/src/migrate_nfs_db.rs
T
Dmitriy Kovalenko f69b8d203d Report when NFS database migration finds no databases (#57357)
## Why

`codex migrate-nfs-db` reported a successful migration when the database directory existed but contained no databases.

## What changed

Count databases that were migrated or already use DELETE journal mode. If none are found, print `No databases found.` instead of the migration success message.

## Testing

Extend the CLI integration test to cover an empty directory, a failure on a corrupt logs database after an earlier conversion, and a successful rerun that recognizes the converted database as already in DELETE mode.

GitOrigin-RevId: 16b109645925164f3af5d35a909417872cfa004f
2026-10-27 04:46:24 +00:00

118 lines
4.7 KiB
Rust

//! Confirms explicit database migration and reports the shared migrator's results.
//! Other SQLite clients must remain stopped during replacement, and NFS must enforce
//! cross-client POSIX locks.
use anyhow::Context;
use anyhow::ensure;
use clap::Parser;
use codex_config::LoaderOverrides;
use codex_core::config::ConfigBuilder;
use codex_state::NfsDbMigration;
use codex_state::NfsDbMigrationOutcome;
use codex_utils_cli::CliConfigOverrides;
use owo_colors::OwoColorize;
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(
after_help = "Stop SQLite clients on every host and keep them stopped through cutover.\nThe NFS mount must support cross-client POSIX locks (no nolock/local_lock=all).\nThis command replaces configured databases in place and retains their WAL backups."
)]
pub(crate) struct MigrateNfsDbCommand {
/// Use a local directory with space for the database and WAL files.
/// The default is the system temporary directory.
#[arg(long, default_value_os_t = std::env::temp_dir())]
staging_dir: PathBuf,
}
pub(crate) async fn run(
command: MigrateNfsDbCommand,
config_overrides: CliConfigOverrides,
loader_overrides: LoaderOverrides,
) -> anyhow::Result<()> {
ensure!(
cfg!(unix),
"migrate-nfs-db currently requires Unix filesystem locking and directory fsync"
);
let warning = "WARNING: This operation is unsafe under concurrency.";
if supports_color::on(supports_color::Stream::Stderr).is_some() {
eprintln!("{}", warning.red().bold());
} else {
eprintln!("{warning}");
}
eprintln!(
"Stop all Codex instances on every host that uses these databases.\nKeep them stopped until the migration completes successfully."
);
let config = ConfigBuilder::default()
.cli_overrides(
config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?,
)
.loader_overrides(loader_overrides)
.build()
.await?;
// Check the merged config for an explicit log directory.
let effective_config = config.config_layer_stack.effective_config();
let skip_logs = effective_config.get("log_dir").is_some();
let sqlite = config.sqlite_config();
if !sqlite.home().try_exists()? {
println!("No databases found.");
return Ok(());
}
let mut needs_permission_confirmation = false;
for database in sqlite.runtime_db_paths() {
if skip_logs && database.path == sqlite.logs_db_path() {
continue;
}
needs_permission_confirmation |=
NfsDbMigration::needs_permission_confirmation(&database.path)?;
}
if needs_permission_confirmation {
eprintln!(
"Some database files have permissions other than 0600.\nMigration preserves file mode bits, including group and other access."
);
}
eprintln!(
"Replacement files belong to you; the original owner, group, and extended ACLs are not copied.\nThis can remove another user's access even when file permissions are 0600.\nOriginal files remain in a private backup directory.\nYou need write access to the database directory and its migration lock."
);
let prompt = if needs_permission_confirmation {
"Migrate databases, including files with permissions other than 0600? [y/N]:"
} else {
"Continue? [y/N]:"
};
ensure!(
crate::confirm(prompt)?,
"Migration cancelled. No databases were changed."
);
let mut migration = NfsDbMigration::lock(sqlite.home())?;
let mut databases_found = 0;
for database in sqlite.runtime_db_paths() {
if skip_logs && database.path == sqlite.logs_db_path() {
println!("Skipped logs database: log_dir is configured.");
continue;
}
let outcome = migration.migrate(&database.path, &command.staging_dir).await.with_context(|| format!(
"migration failed for {}. Keep all Codex instances stopped. Correct the error and rerun codex migrate-nfs-db",
database.path.display()
))?;
match outcome {
NfsDbMigrationOutcome::Missing => continue,
NfsDbMigrationOutcome::AlreadyDelete => {
println!("Already in DELETE mode: {}", database.path.display())
}
NfsDbMigrationOutcome::Migrated { backup_directory } => println!(
"Migrated database: {}\nWAL backup: {}",
database.path.display(),
backup_directory.display()
),
}
databases_found += 1;
}
if databases_found == 0 {
println!("No databases found.");
return Ok(());
}
println!("Migration completed successfully. You can restart Codex.");
Ok(())
}