Iteration 3: existence-only load + rebuild from text on missing .db
When the runtime opens a project whose playground.db is missing,
it now rebuilds the database from project.yaml + data/<table>.csv
per ADR-0015 §7. The rebuild path:
1. Parses project.yaml (serde_yml). Unknown versions / types /
actions surface as PersistenceFatal.
2. Recreates each user table with FK constraints inline
(PRAGMA foreign_keys=OFF), then populates the column-type,
relationship, and project metadata tables.
3. Loads each table's CSV via a hand-rolled reader that
preserves the NULL-vs-empty distinction (the csv crate
doesn't expose whether a field was quoted; ours does).
4. Runs PRAGMA foreign_key_check before commit; any violation
aborts.
5. Restores foreign_keys=ON regardless of success.
Row-level failures get DbError::RebuildRowFailed with row
number, file, table, and a friendly per-type detail. They land
in the runtime as a fatal stderr message ("unable to load row N
from `data/T.csv` into table `T`: ...") before the alternate
screen is entered.
created_at from project.yaml overwrites the configure-time
placeholder so timestamps round-trip stably.
Tests: 307 passing (267 lib + 9 + 5 new + 9 + 17), 0 failing,
0 skipped. Clippy clean with nursery lints.
This commit is contained in:
@@ -47,8 +47,29 @@ pub async fn run(args: Args) -> Result<()> {
|
||||
let db_path = project.db_path();
|
||||
let display_name = project.display_name().to_string();
|
||||
let persistence = crate::persistence::Persistence::new(project.path().to_path_buf());
|
||||
// Capture whether the .db file existed BEFORE we open it —
|
||||
// sqlite creates it on connect, so this is the only honest
|
||||
// signal that we need to rebuild from text (ADR-0015 §7).
|
||||
let db_existed = db_path.exists();
|
||||
let database = Database::open_with_persistence(db_path.as_path(), persistence)
|
||||
.context("open database")?;
|
||||
if !db_existed
|
||||
&& let Err(e) = database.rebuild_from_text(project.path().to_path_buf()).await
|
||||
{
|
||||
// The terminal is still in cooked mode here (we haven't
|
||||
// entered the alternate screen yet), so writing to
|
||||
// stderr lands directly in the user's shell. Drop the
|
||||
// project to release the lock first.
|
||||
drop(project);
|
||||
if matches!(
|
||||
e,
|
||||
DbError::PersistenceFatal { .. } | DbError::RebuildRowFailed { .. }
|
||||
) {
|
||||
eprintln!("rdbms-playground: {}", e.friendly_message());
|
||||
return Ok(());
|
||||
}
|
||||
return Err(anyhow::anyhow!(e.friendly_message())).context("rebuild from text");
|
||||
}
|
||||
|
||||
let mut terminal = setup_terminal().context("setup terminal")?;
|
||||
let result = run_loop(&mut terminal, args.theme, database, display_name).await;
|
||||
|
||||
Reference in New Issue
Block a user