Iteration 4a: rebuild command with confirmation modal
Adds the explicit `rebuild` app-level command (ADR-0015 §7, §11)
and a modal UI infrastructure to host its confirmation dialog.
Typing `rebuild` emits Action::PrepareRebuild; the runtime reads
project.yaml + data/ to compute a summary ("3 tables and 47 rows
will be reconstructed; the existing playground.db will be
replaced") and posts AppEvent::RebuildPrepared, which opens the
modal. Y confirms, N/Esc cancels. While the modal is open,
normal input is gated.
The worker's do_rebuild_from_text now wipes existing user tables
and metadata before reloading from text, so it works on both
fresh and populated databases. Source text is plumbed through
rebuild_from_text so the explicit rebuild logs to history.log
while the silent on-load rebuild from Iteration 3 stays silent.
Modal infrastructure (App.modal field + key routing + centered
overlay rendering + word-wrap) is reused by Iteration 4b's save
/ save as / load / new flows.
Tests: 314 passing (268 lib + 9 + 5 + 6 new + 9 + 17),
0 failing, 0 skipped. Clippy clean.
This commit is contained in:
@@ -322,10 +322,11 @@ enum Request {
|
||||
},
|
||||
/// Rebuild the database from `project.yaml` + `data/`
|
||||
/// (ADR-0015 §7). Used by the runtime when the `.db` file
|
||||
/// is missing on project open. Iteration 4's `rebuild`
|
||||
/// app-level command will reuse the same request.
|
||||
/// is missing on project open and by the explicit
|
||||
/// `rebuild` app-level command (Iteration 4).
|
||||
RebuildFromText {
|
||||
project_path: std::path::PathBuf,
|
||||
source: Option<String>,
|
||||
reply: oneshot::Sender<Result<(), DbError>>,
|
||||
},
|
||||
}
|
||||
@@ -543,16 +544,22 @@ impl Database {
|
||||
}
|
||||
|
||||
/// Rebuild the database from `project.yaml` + `data/`
|
||||
/// (ADR-0015 §7). Called by the runtime on a missing `.db`
|
||||
/// at startup; Iteration 4 will also expose this via the
|
||||
/// `rebuild` app-level command.
|
||||
/// (ADR-0015 §7).
|
||||
///
|
||||
/// Called by the runtime on a missing `.db` at startup
|
||||
/// (with `source = None`, no history entry) and by the
|
||||
/// explicit `rebuild` app-level command (with
|
||||
/// `source = Some("rebuild")`, which appends to
|
||||
/// `history.log` on success).
|
||||
pub async fn rebuild_from_text(
|
||||
&self,
|
||||
project_path: std::path::PathBuf,
|
||||
source: Option<String>,
|
||||
) -> Result<(), DbError> {
|
||||
let (reply, recv) = oneshot::channel();
|
||||
self.send(Request::RebuildFromText {
|
||||
project_path,
|
||||
source,
|
||||
reply,
|
||||
})
|
||||
.await?;
|
||||
@@ -821,8 +828,17 @@ fn handle_request(conn: &Connection, persistence: Option<&Persistence>, req: Req
|
||||
&table,
|
||||
));
|
||||
}
|
||||
Request::RebuildFromText { project_path, reply } => {
|
||||
let _ = reply.send(do_rebuild_from_text(conn, &project_path));
|
||||
Request::RebuildFromText {
|
||||
project_path,
|
||||
source,
|
||||
reply,
|
||||
} => {
|
||||
let _ = reply.send(do_rebuild_from_text(
|
||||
conn,
|
||||
persistence,
|
||||
source.as_deref(),
|
||||
&project_path,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2437,14 +2453,29 @@ fn read_relationships_inbound(
|
||||
///
|
||||
/// The on-disk text is the authoritative source: this function
|
||||
/// recreates schema, metadata, and rows so the resulting `.db`
|
||||
/// reflects them exactly. Persistence callbacks are NOT invoked;
|
||||
/// we're loading, not changing user-visible state.
|
||||
/// reflects them exactly. Persistence callbacks are NOT invoked
|
||||
/// for the schema/data writes; we're loading, not changing
|
||||
/// user-visible state. The exception is `history.log`: when
|
||||
/// `source` is `Some`, the rebuild was user-initiated (via the
|
||||
/// `rebuild` app-level command) and is appended as a successful
|
||||
/// command per ADR-0015 §5.
|
||||
///
|
||||
/// Existing user tables and metadata rows are wiped at the
|
||||
/// start of the rebuild so this function works on both fresh
|
||||
/// and populated databases — the silent on-load case (empty
|
||||
/// db) sees a no-op wipe; the explicit `rebuild` command
|
||||
/// replaces whatever was there.
|
||||
///
|
||||
/// FK enforcement is disabled for the load and re-enabled at
|
||||
/// the end (regardless of success). A `foreign_key_check`
|
||||
/// before commit verifies the loaded data is consistent — any
|
||||
/// violation aborts with a fatal error.
|
||||
fn do_rebuild_from_text(conn: &Connection, project_path: &Path) -> Result<(), DbError> {
|
||||
fn do_rebuild_from_text(
|
||||
conn: &Connection,
|
||||
persistence: Option<&Persistence>,
|
||||
source: Option<&str>,
|
||||
project_path: &Path,
|
||||
) -> Result<(), DbError> {
|
||||
let yaml_path = project_path.join(PROJECT_YAML);
|
||||
let data_dir = project_path.join(DATA_DIR);
|
||||
|
||||
@@ -2468,6 +2499,25 @@ fn do_rebuild_from_text(conn: &Connection, project_path: &Path) -> Result<(), Db
|
||||
.unchecked_transaction()
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
|
||||
// 0. Wipe any existing user tables + metadata so the
|
||||
// rebuild can start from a clean slate. This step
|
||||
// is a no-op on a freshly-created database (the
|
||||
// silent rebuild on missing-`.db`); on the explicit
|
||||
// `rebuild` command it replaces the live state with
|
||||
// the text-source state per ADR-0015 §7.
|
||||
let existing_tables = do_list_tables(&tx)?;
|
||||
for name in &existing_tables {
|
||||
tx.execute_batch(&format!(
|
||||
"DROP TABLE {ident};",
|
||||
ident = quote_ident(name)
|
||||
))
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
}
|
||||
tx.execute_batch(&format!(
|
||||
"DELETE FROM {META_TABLE}; DELETE FROM {REL_TABLE};"
|
||||
))
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
|
||||
// 1. Recreate user tables with FK constraints inline.
|
||||
for table in &snapshot.tables {
|
||||
let read_schema = build_read_schema(table, &snapshot.relationships);
|
||||
@@ -2554,6 +2604,14 @@ fn do_rebuild_from_text(conn: &Connection, project_path: &Path) -> Result<(), Db
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Append `history.log` if this rebuild was
|
||||
// user-initiated (the silent on-load case has
|
||||
// `source = None`).
|
||||
if let (Some(p), Some(text)) = (persistence, source) {
|
||||
p.append_history(text)
|
||||
.map_err(DbError::from_persistence)?;
|
||||
}
|
||||
|
||||
tx.commit().map_err(DbError::from_rusqlite)?;
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user