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
+36
View File
@@ -466,6 +466,7 @@ async fn handle_project_switch(
if let Ok(tables) = session.database().list_tables().await {
let _ = event_tx.send(AppEvent::TablesRefreshed(tables)).await;
}
refresh_schema_cache(session.database(), event_tx).await;
}
Err(e) => {
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");
}
}
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
@@ -905,6 +936,7 @@ fn spawn_rebuild(
if let Ok(tables) = database.list_tables().await {
let _ = event_tx.send(AppEvent::TablesRefreshed(tables)).await;
}
refresh_schema_cache(&database, &event_tx).await;
}
Err(DbError::PersistenceFatal {
operation,
@@ -1003,6 +1035,9 @@ fn spawn_dsl_dispatch(
}
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"),
}
refresh_schema_cache(&database, &event_tx).await;
});
}