feat(history): mode-tagged history + top-of-chain journaling (#30)
Record the submission mode per history entry so advanced commands are reusable in simple mode, and fix the bug where a ':'-one-shot command lost its ':' across sessions (ADR-0052, closing #30). Format: the history.log status token gains an optional ':adv' suffix (ok / ok:adv / err / err:adv); 'source' stays last and canonical, so replay is unaffected. The in-memory ring (still Vec<String>) stores advanced entries ': '-prefixed; recall strips the ':' in advanced mode and keeps it in simple; hydration reconstructs the prefix from the tag. Journaling moved from the worker to the dispatch layer (spawn_dsl_- dispatch / run_replay / app-command sites), where the mode is in scope with no worker plumbing; finalize_persistence writes only yaml/csv (commit-db-last still atomic for state). The journal write is now best-effort (command already committed), consistent with the failure path. App commands journal simple, so they recall bare. Journaling is now uniform (every successful command, per ADR-0034) — closing a gap where show tables/relationships/explain didn't journal. Amends ADR-0034 (status tag + journaling location), ADR-0015 §6 (history.log out of the worker tx), ADR-0040 (journal-write best-effort). 15 worker-level journaling tests retired, re-covered at the new layer (history.rs format, app.rs recall matrix, iteration6 cross-session regression, replay). 2471 pass / 0 fail / 0 skip, clippy clean.
This commit is contained in:
@@ -2262,12 +2262,10 @@ fn handle_request(
|
||||
// (`show table`), it belongs in the complete journal
|
||||
// (ADR-0034). ADR-0035 §4.
|
||||
if if_not_exists && user_table_exists(conn, &name).unwrap_or(false) {
|
||||
let result = do_describe_table(conn, &name).and_then(|desc| {
|
||||
if let (Some(p), Some(text)) = (persistence, source.as_deref()) {
|
||||
p.append_history(text).map_err(DbError::from_persistence)?;
|
||||
}
|
||||
Ok(CreateOutcome::Skipped(desc))
|
||||
});
|
||||
// ADR-0052: journaling moved to the dispatch layer; this
|
||||
// no-op skip is an `Ok` outcome there and is journalled by
|
||||
// the spawn like any other.
|
||||
let result = do_describe_table(conn, &name).map(CreateOutcome::Skipped);
|
||||
let _ = reply.send(result);
|
||||
} else {
|
||||
snapshot_then(snap, batch, conn, source.as_deref(), reply, || {
|
||||
@@ -2306,12 +2304,8 @@ fn handle_request(
|
||||
// line is still journalled — like the `CREATE TABLE IF NOT
|
||||
// EXISTS` skip and other no-ops (ADR-0034). ADR-0035 §4.
|
||||
if if_exists && !user_table_exists(conn, &name).unwrap_or(false) {
|
||||
let result = (|| {
|
||||
if let (Some(p), Some(text)) = (persistence, source.as_deref()) {
|
||||
p.append_history(text).map_err(DbError::from_persistence)?;
|
||||
}
|
||||
Ok(DropOutcome::Skipped)
|
||||
})();
|
||||
// ADR-0052: journaling moved to the dispatch layer.
|
||||
let result: Result<DropOutcome, DbError> = Ok(DropOutcome::Skipped);
|
||||
let _ = reply.send(result);
|
||||
} else {
|
||||
snapshot_then(snap, batch, conn, source.as_deref(), reply, || {
|
||||
@@ -2519,12 +2513,9 @@ fn handle_request(
|
||||
// ADR-0035 §4). Existence uses the same user-index lookup as
|
||||
// `do_drop_index` (`sql IS NOT NULL`).
|
||||
if if_exists && !index_exists(conn, &name, true).unwrap_or(false) {
|
||||
let result = (|| {
|
||||
if let (Some(p), Some(text)) = (persistence, source.as_deref()) {
|
||||
p.append_history(text).map_err(DbError::from_persistence)?;
|
||||
}
|
||||
Ok(DropIndexOutcome::Skipped)
|
||||
})();
|
||||
// ADR-0052: journaling moved to the dispatch layer.
|
||||
let result: Result<DropIndexOutcome, DbError> =
|
||||
Ok(DropIndexOutcome::Skipped);
|
||||
let _ = reply.send(result);
|
||||
} else {
|
||||
snapshot_then(snap, batch, conn, source.as_deref(), reply, || {
|
||||
@@ -2555,12 +2546,9 @@ fn handle_request(
|
||||
// hits `do_add_index`'s redundant-set refusal (ADR-0025).
|
||||
let resolved = resolve_index_name(name.as_deref(), &table, &columns);
|
||||
if if_not_exists && index_exists(conn, &resolved, false).unwrap_or(false) {
|
||||
let result = (|| {
|
||||
if let (Some(p), Some(text)) = (persistence, source.as_deref()) {
|
||||
p.append_history(text).map_err(DbError::from_persistence)?;
|
||||
}
|
||||
Ok(CreateIndexOutcome::Skipped(resolved.clone()))
|
||||
})();
|
||||
// ADR-0052: journaling moved to the dispatch layer.
|
||||
let result: Result<CreateIndexOutcome, DbError> =
|
||||
Ok(CreateIndexOutcome::Skipped(resolved));
|
||||
let _ = reply.send(result);
|
||||
} else {
|
||||
snapshot_then(snap, batch, conn, source.as_deref(), reply, || {
|
||||
@@ -3065,10 +3053,21 @@ struct Changes {
|
||||
/// Read-only requests (no schema change, no row writes, no
|
||||
/// drops) still use this to append `history.log` if `source`
|
||||
/// is set; they pass an empty `Changes`.
|
||||
// Persist the **state** sources (project.yaml + data/*.csv) for a
|
||||
// committed mutation, inside the worker transaction (ADR-0015 §6
|
||||
// commit-db-last). `history.log` is NOT written here — ADR-0052 moved
|
||||
// journaling to the dispatch layer (runtime), so the command's mode is
|
||||
// available without plumbing it through the worker, and a journal-write
|
||||
// failure no longer rolls back a committed command (it is best-effort,
|
||||
// like the failure path).
|
||||
fn finalize_persistence(
|
||||
conn: &Connection,
|
||||
persistence: Option<&Persistence>,
|
||||
source: Option<&str>,
|
||||
// Vestigial since ADR-0052 (the `history.log` write that used it moved
|
||||
// to the dispatch layer). Retained so the ~28 worker handlers that
|
||||
// thread `source` to here keep a use for it, rather than orphaning the
|
||||
// param across all of them; a later cleanup could unwind that plumbing.
|
||||
_source: Option<&str>,
|
||||
changes: &Changes,
|
||||
) -> Result<(), DbError> {
|
||||
let Some(p) = persistence else {
|
||||
@@ -3093,10 +3092,6 @@ fn finalize_persistence(
|
||||
p.delete_table_data(table)
|
||||
.map_err(DbError::from_persistence)?;
|
||||
}
|
||||
if let Some(text) = source {
|
||||
p.append_history(text)
|
||||
.map_err(DbError::from_persistence)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8361,18 +8356,18 @@ fn do_drop_index(
|
||||
/// Read-only wrapper around `do_describe_table` that runs an
|
||||
/// auxiliary `history.log` append for user-issued
|
||||
/// `show table` commands.
|
||||
// ADR-0052: journaling moved to the dispatch layer, so this read-only
|
||||
// `show table` wrapper no longer appends to `history.log` — the spawn
|
||||
// journals the `Ok` outcome. Kept as a thin delegate (a later cleanup
|
||||
// could inline `do_describe_table` at the one call site); `_persistence`
|
||||
// / `_source` are vestigial.
|
||||
fn do_describe_table_request(
|
||||
conn: &Connection,
|
||||
persistence: Option<&Persistence>,
|
||||
source: Option<&str>,
|
||||
_persistence: Option<&Persistence>,
|
||||
_source: Option<&str>,
|
||||
name: &str,
|
||||
) -> Result<TableDescription, DbError> {
|
||||
let description = do_describe_table(conn, name)?;
|
||||
if let (Some(p), Some(text)) = (persistence, source) {
|
||||
p.append_history(text)
|
||||
.map_err(DbError::from_persistence)?;
|
||||
}
|
||||
Ok(description)
|
||||
do_describe_table(conn, name)
|
||||
}
|
||||
|
||||
fn do_describe_table(conn: &Connection, name: &str) -> Result<TableDescription, DbError> {
|
||||
@@ -9981,40 +9976,32 @@ fn do_delete(
|
||||
})
|
||||
}
|
||||
|
||||
/// Read-only wrapper that adds the `history.log` append for
|
||||
/// `show data` user commands.
|
||||
/// Read-only `show data` wrapper. ADR-0052: journaling moved to the
|
||||
/// dispatch layer (`_persistence` / `_source` vestigial).
|
||||
fn do_query_data_request(
|
||||
conn: &Connection,
|
||||
persistence: Option<&Persistence>,
|
||||
source: Option<&str>,
|
||||
_persistence: Option<&Persistence>,
|
||||
_source: Option<&str>,
|
||||
table: &str,
|
||||
filter: Option<&Expr>,
|
||||
limit: Option<u64>,
|
||||
) -> Result<DataResult, DbError> {
|
||||
let data = do_query_data(conn, table, filter, limit)?;
|
||||
if let (Some(p), Some(text)) = (persistence, source) {
|
||||
p.append_history(text)
|
||||
.map_err(DbError::from_persistence)?;
|
||||
}
|
||||
Ok(data)
|
||||
// ADR-0052: journaling moved to the dispatch layer (`_persistence` /
|
||||
// `_source` vestigial; the spawn journals the `Ok` outcome).
|
||||
do_query_data(conn, table, filter, limit)
|
||||
}
|
||||
|
||||
/// Worker handler for `Request::RunSelect` (ADR-0030 §6,
|
||||
/// ADR-0031). Mirrors `do_query_data_request`: run the
|
||||
/// statement, append the literal line to `history.log` so a
|
||||
/// Worker handler for `Request::RunSelect` (ADR-0030 §6, ADR-0031).
|
||||
/// ADR-0052: journaling moved to the dispatch layer, so this no longer
|
||||
/// appends to `history.log` — the spawn journals the literal line so a
|
||||
/// replay re-runs it (ADR-0030 §11).
|
||||
fn do_run_select_request(
|
||||
conn: &Connection,
|
||||
persistence: Option<&Persistence>,
|
||||
source: Option<&str>,
|
||||
_persistence: Option<&Persistence>,
|
||||
_source: Option<&str>,
|
||||
sql: &str,
|
||||
) -> Result<DataResult, DbError> {
|
||||
let data = do_run_select(conn, sql)?;
|
||||
if let (Some(p), Some(text)) = (persistence, source) {
|
||||
p.append_history(text)
|
||||
.map_err(DbError::from_persistence)?;
|
||||
}
|
||||
Ok(data)
|
||||
do_run_select(conn, sql)
|
||||
}
|
||||
|
||||
/// Currently-stored non-NULL values of one column, for shortid
|
||||
@@ -11119,8 +11106,10 @@ fn read_relationships_inbound(
|
||||
/// violation aborts with a fatal error.
|
||||
fn do_rebuild_from_text(
|
||||
conn: &Connection,
|
||||
persistence: Option<&Persistence>,
|
||||
source: Option<&str>,
|
||||
// Vestigial since ADR-0052: `rebuild` is journalled at the dispatch
|
||||
// layer (`spawn_rebuild`), not here.
|
||||
_persistence: Option<&Persistence>,
|
||||
_source: Option<&str>,
|
||||
project_path: &Path,
|
||||
) -> Result<(), DbError> {
|
||||
debug!(path = %project_path.display(), "rebuild_from_text");
|
||||
@@ -11320,10 +11309,8 @@ fn do_rebuild_from_text(
|
||||
// 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)?;
|
||||
}
|
||||
// ADR-0052: `rebuild` is journalled at the dispatch layer
|
||||
// (`spawn_rebuild`), not here — journaling left the worker.
|
||||
|
||||
tx.commit().map_err(DbError::from_rusqlite)?;
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user