Runtime: schema-aware replay parsing

run_replay parsed each line with the schemaless parse_command, so
Phase D typed-slot rejections (wrong-count value lists, wrong-type
column values) fired only at bind time during replay — inconsistent
with the interactive path (handoff-12 §2.1).

run_replay now re-snapshots the schema per line (the schema mutates
as replayed create-table / add-column commands run) and parses with
parse_command_with_schema. Extracted build_schema_cache, shared with
the interactive refresh_schema_cache.

Added a replay integration test asserting a typed-slot violation is
caught at parse time (through the replay.error_parse wrapper).
This commit is contained in:
claude@clouddev1
2026-05-15 22:31:19 +00:00
parent 90e3f5dbfb
commit f46606b12e
2 changed files with 76 additions and 4 deletions
+49
View File
@@ -227,6 +227,55 @@ fn replay_aborts_on_first_parse_failure_and_reports_line() {
);
}
#[test]
fn replay_rejects_typed_slot_violation_at_parse_time() {
// Schema-aware replay (handoff-13 §2.1 fix): run_replay
// re-snapshots the schema per line and parses with
// parse_command_with_schema. So a wrong-type value in a
// value list is caught at *parse* time during replay —
// surfaced through the `replay.error_parse` wrapper ("parse
// error …") — exactly as the interactive path would, rather
// than only at bind time.
//
// `'not a number'` (a string) lands in the int `count`
// slot. The schemaless parser would accept it (a string is
// a value literal) and only bind-time would reject; the
// schema-aware parser rejects it at parse time. Asserting
// the error went through the parse wrapper proves the
// schema was threaded.
let data = tempdir();
let (project, db) = open_project_db(data.path());
write_script(
project.path(),
"typed.commands",
"create table T with pk id:int\n\
add column T: count (int)\n\
insert into T values (1, 'not a number')\n",
);
let events = rt().block_on(async {
run_replay(&db, project.path(), "typed.commands").await
});
let failed = assert_failed_at(&events, 3);
let AppEvent::ReplayFailed { error, .. } = failed else {
unreachable!()
};
assert!(
error.contains("parse error"),
"typed-slot violation should be caught at parse time, got: {error}",
);
// The earlier two lines stayed applied; the failing insert
// did not run.
let data_result = rt()
.block_on(async { db.query_data("T".to_string(), None).await })
.expect("query_data");
assert!(
data_result.rows.is_empty(),
"the rejected insert must not have dispatched",
);
}
#[test]
fn replay_aborts_on_first_runtime_failure_and_reports_line() {
let data = tempdir();