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
+27 -4
View File
@@ -838,6 +838,19 @@ async fn refresh_schema_cache(
database: &Database,
event_tx: &mpsc::Sender<AppEvent>,
) {
let cache = build_schema_cache(database).await;
let _ = event_tx.send(AppEvent::SchemaCacheRefreshed(cache)).await;
}
/// Build a `SchemaCache` snapshot from the live database.
///
/// Shared by `refresh_schema_cache` (interactive path — wraps
/// the result in a `SchemaCacheRefreshed` event) and the replay
/// path (which re-snapshots per line because the schema mutates
/// as replayed `create table` / `add column` commands run).
/// Best-effort: a failed query leaves that list empty and the
/// walker falls back to schemaless behaviour.
async fn build_schema_cache(database: &Database) -> crate::completion::SchemaCache {
use crate::completion::{SchemaCache, TableColumn};
use crate::dsl::grammar::IdentSource;
let mut cache = SchemaCache::default();
@@ -872,7 +885,7 @@ async fn refresh_schema_cache(
cache.table_columns.insert(name, cols);
}
}
let _ = event_tx.send(AppEvent::SchemaCacheRefreshed(cache)).await;
cache
}
/// Read `project.yaml` + `data/` to compute the rebuild
@@ -1440,9 +1453,19 @@ pub async fn run_replay(
continue;
}
// Parse the line through the same DSL parser the
// interactive path uses. A failure here is structural
// (bad syntax) — report and stop without dispatching.
let command = match crate::dsl::parse_command(trimmed) {
// interactive path uses. The schema is re-snapshotted
// every line because earlier replayed commands
// (`create table`, `add column`, …) mutate it — so
// Phase D typed-slot rejections (wrong-count value
// lists, wrong-type column values) fire at replay
// parse time, matching the interactive path, rather
// than only at bind time. A failure here is structural
// (bad syntax / typed-slot reject) — report and stop
// without dispatching.
let schema = build_schema_cache(database).await;
let command = match crate::dsl::parser::parse_command_with_schema(
trimmed, &schema,
) {
Ok(c) => c,
Err(e) => {
events.push(AppEvent::ReplayFailed {