ADR-0022 stage 8d: schema cache refresh wiring

New `AppEvent::SchemaCacheRefreshed(SchemaCache)` event +
App handler that stores it on `app.schema_cache`.

Runtime helper `refresh_schema_cache(database, event_tx)`
fetches table / column / relationship names via the
`list_names_for` worker request (added in stage 7) and posts
the assembled cache. Wired into every site that already
posts `TablesRefreshed`:
  - `seed_initial_tables` (initial project load).
  - Project-switch path in `handle_project_switch`.
  - `RebuildSucceeded` path.
  - Post-DDL path (`spawn_command`).
  - Post-replay path.

Result: schema-aware identifier completion (added in 8c)
becomes live — Tab on `show data ` offers the actual table
names from the current project, `drop column from T: ` (or
similar) offers existing columns, etc. The cache stays
fresh across DDL and rebuild without per-keystroke worker
round-trips (one refresh per schema-mutating action is
amortised across many subsequent keystrokes).

Best-effort: a failed `list_names_for` for any individual
slot kind leaves that field empty in the cache rather than
suppressing the whole refresh — partial completion beats
no completion.

Tests: 738 passing, 0 failing, 1 ignored (unchanged
total — this stage is wiring, not new test surface; the
synthetic-cache tests from stage 8c remain the regression
net for the completion logic itself). Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-11 20:57:09 +00:00
parent 51a8d9ac44
commit 7a32c13bd5
3 changed files with 51 additions and 0 deletions
+10
View File
@@ -356,6 +356,16 @@ impl App {
self.tables = tables; self.tables = tables;
Vec::new() Vec::new()
} }
AppEvent::SchemaCacheRefreshed(cache) => {
trace!(
tables = cache.tables.len(),
columns = cache.columns.len(),
relationships = cache.relationships.len(),
"schema cache refreshed",
);
self.schema_cache = cache;
Vec::new()
}
AppEvent::PersistenceFatal { AppEvent::PersistenceFatal {
operation, operation,
path, path,
+5
View File
@@ -69,6 +69,11 @@ pub enum AppEvent {
}, },
/// Refreshed list of tables in the database. /// Refreshed list of tables in the database.
TablesRefreshed(Vec<String>), TablesRefreshed(Vec<String>),
/// Refreshed schema lookup cache feeding Tab completion
/// for identifier slots (ADR-0022 §9 + stage 8d). Runtime
/// posts this alongside `TablesRefreshed` after project
/// load and after every successful DDL.
SchemaCacheRefreshed(crate::completion::SchemaCache),
/// A persistence failure occurred (ADR-0015 §8). The /// A persistence failure occurred (ADR-0015 §8). The
/// application surfaces a fatal banner and exits cleanly so /// application surfaces a fatal banner and exits cleanly so
/// the message remains above the shell prompt. /// the message remains above the shell prompt.
+36
View File
@@ -466,6 +466,7 @@ async fn handle_project_switch(
if let Ok(tables) = session.database().list_tables().await { if let Ok(tables) = session.database().list_tables().await {
let _ = event_tx.send(AppEvent::TablesRefreshed(tables)).await; let _ = event_tx.send(AppEvent::TablesRefreshed(tables)).await;
} }
refresh_schema_cache(session.database(), event_tx).await;
} }
Err(e) => { Err(e) => {
let _ = event_tx let _ = event_tx
@@ -817,6 +818,36 @@ async fn seed_initial_tables(database: &Database, event_tx: &mpsc::Sender<AppEve
error!(error = %e, "failed to seed initial table list"); error!(error = %e, "failed to seed initial table list");
} }
} }
refresh_schema_cache(database, event_tx).await;
}
/// Fetch the three identifier lists (tables / columns /
/// relationships) and post them as `SchemaCacheRefreshed`
/// (ADR-0022 §9 + stage 8d). Always sends an event, even on
/// partial failure — best-effort completion is better than
/// no completion. Called wherever `TablesRefreshed` is sent
/// today; the schema cache lives on the App and feeds Tab
/// completion for identifier slots.
async fn refresh_schema_cache(
database: &Database,
event_tx: &mpsc::Sender<AppEvent>,
) {
use crate::completion::SchemaCache;
use crate::dsl::ident_slot::IdentSlot;
let mut cache = SchemaCache::default();
if let Ok(tables) = database.list_names_for(IdentSlot::TableName).await {
cache.tables = tables;
}
if let Ok(columns) = database.list_names_for(IdentSlot::Column).await {
cache.columns = columns;
}
if let Ok(rels) = database
.list_names_for(IdentSlot::RelationshipName)
.await
{
cache.relationships = rels;
}
let _ = event_tx.send(AppEvent::SchemaCacheRefreshed(cache)).await;
} }
/// Read `project.yaml` + `data/` to compute the rebuild /// Read `project.yaml` + `data/` to compute the rebuild
@@ -905,6 +936,7 @@ fn spawn_rebuild(
if let Ok(tables) = database.list_tables().await { if let Ok(tables) = database.list_tables().await {
let _ = event_tx.send(AppEvent::TablesRefreshed(tables)).await; let _ = event_tx.send(AppEvent::TablesRefreshed(tables)).await;
} }
refresh_schema_cache(&database, &event_tx).await;
} }
Err(DbError::PersistenceFatal { Err(DbError::PersistenceFatal {
operation, operation,
@@ -1003,6 +1035,9 @@ fn spawn_dsl_dispatch(
} }
Err(e) => warn!(error = %e, "post-list_tables failed"), Err(e) => warn!(error = %e, "post-list_tables failed"),
} }
// Refresh the schema cache feeding Tab completion
// (ADR-0022 §9). Same timing as TablesRefreshed.
refresh_schema_cache(&database, &event_tx).await;
}); });
} }
@@ -1337,6 +1372,7 @@ fn spawn_replay(
} }
Err(e) => warn!(error = %e, "post-replay list_tables failed"), Err(e) => warn!(error = %e, "post-replay list_tables failed"),
} }
refresh_schema_cache(&database, &event_tx).await;
}); });
} }