fix(app): hold input until the schema-cache refresh lands (#39)

A simple-mode Form-B insert (`insert into T values (…)`) submitted faster
than the post-DDL schema-cache refresh was validated against the stale
cache and wrongly rejected as "trying to write SQL?". The cache is
refreshed asynchronously after the worker applies a command, posting
`SchemaCacheRefreshed` on the same FIFO channel as key events; under
faster-than-human input the next Enter is processed before the refresh.

Add a submission gate: `dispatch_dsl` arms `awaiting_schema_refresh` on
every `ExecuteDsl` dispatch, holds further DSL submissions in a
`held_submissions` FIFO while armed, and the `SchemaCacheRefreshed`
handler drains them against the fresh cache in submission order.
App-lifecycle commands bypass the gate. Arm-on-every-dispatch keeps
exactly one DSL command in flight, so the boolean is provably correct.
Interactive-only: the replay/batch path already re-snapshots the schema
synchronously per line. No interactive-user impact; held input is never
lost (a refresh always follows a dispatch).

Tests (both verified red→green): a Tier-1 deterministic ordering test and
a Tier-4 PTY back-to-back regression (the unpaced inverse of flow 3).
Records the design as ADR-0022 Amendment 8.
This commit is contained in:
claude@clouddev1
2026-06-22 16:59:40 +00:00
parent 010dbf8e9e
commit 07575da983
5 changed files with 254 additions and 2 deletions
+27
View File
@@ -377,6 +377,33 @@ fn undo_after_drop_table_restores_it() {
app.quit();
}
/// Flow 5 — issue #39 regression: commands submitted **back-to-back**, with
/// no wait between them, must still execute correctly. This is the inverse of
/// flow 3 (which paces each command on purpose). Pre-fix, a Form-B insert sent
/// immediately after `add column` was validated against the *stale* schema
/// cache — the worker hadn't yet refreshed it — and wrongly rejected as
/// "trying to write SQL?", so the row never landed. The schema-refresh gate
/// (`App::awaiting_schema_refresh`) now holds each submission until the prior
/// command's refresh arrives, making the outcome independent of input speed.
#[test]
fn back_to_back_insert_after_ddl_still_succeeds() {
let mut app = PtyApp::launch(&[]);
app.wait_for_no_tables(); // fresh project
// Fire all three with no readiness wait in between — the faster-than-human
// input that triggered issue #39 (paste / script / unpaced driver).
app.submit("create table Customers with pk id(serial)");
app.submit("add column to Customers: Name (text)");
app.submit("insert into Customers values ('Alice')");
// The insert's OWN success echo (value + ✓) — proof the row reached the
// database, not the "trying to write SQL?" rejection. If the gate
// regressed, the insert would misparse against the stale schema and this
// would time out.
app.wait_for("('Alice') ✓");
app.quit();
}
// ===================== NFR perf (measured, generous) ===================
//
// These run against the DEBUG binary, so the bounds are loose
+6
View File
@@ -170,6 +170,12 @@ fn colon_escape_in_simple_mode_is_one_shot() {
echoed.text,
);
// Issue #39: dispatching `:select 1` arms the schema-refresh gate (a
// cache refresh is conceptually in flight). In the real runtime that
// refresh lands before the user types the next line; model it here so
// the follow-up submission is processed rather than held.
app.update(AppEvent::SchemaCacheRefreshed(app.schema_cache.clone()));
// Subsequent submission (unrecognised in simple mode) parse-errors,
// not echoes — confirming the mode reverted.
type_str(&mut app, "list things");