feat: DSL→SQL teaching echo — Bucket A renderer (ADR-0038 Phase 1)
Expands the renderer skeleton from ADR-0038's first slice to the full single-statement catalogue. Every Bucket A row round-trips through the advanced-mode walker (the §1 copy-paste contract): add column / drop column (non-cascade) / rename column / change column (SET DATA TYPE) / add constraint (not null, default, unique, check) / drop constraint (not null, default) / show data [where] [limit] / delete --all-rows / update --all-rows Adds the Expr→SQL and Value→SQL-literal renderers (ADR-0038 §5) — bare identifiers, inlined literals, NULL uppercase, standard <> for inequality — and threads `echo: Option<String>` onto the six remaining success events (DslAddColumn/DropColumn/ChangeColumn/Data/Update/Delete Succeeded) with matching runtime construction and App stash arms. `show data` is the one Bucket A row whose echo needs schema info beyond the Command (the `ORDER BY <pk>` for a limited query): the pure renderer takes the primary key as a parameter, and the runtime sources it post-execution via describe_table — gated on advanced mode + limit present, mirroring the enrich_dsl_failure describe pattern. An end-to-end test pins the describe→PK→ORDER BY glue against a real worker; the simple-mode gate and unlimited-no-lookup paths are covered too. Also fixes a contract gap surfaced while completing the catalogue: the existing create-table echo silently dropped per-column DEFAULT / CHECK, which simple-mode `create table … with pk c(ty) check (…)` does parse (ADR-0029) — so the echo was non-equivalent. The render now emits the full ADR-0029 column-constraint suffix, sharing one append_constraints helper with `add column`. Phase 2 (Bucket B — resolved-name + multi-line echoes, including `add index`), Phase 3 (category-3 prose), and the de-emphasised styled-runs polish remain deferred per ADR-0038 §8 phasing. Tests: 2000 passed / 0 failed / 1 ignored (pre-existing); clippy clean (`--all-targets -D warnings`, nursery).
This commit is contained in:
+118
-4
@@ -1292,10 +1292,19 @@ fn spawn_dsl_dispatch(
|
||||
name,
|
||||
}
|
||||
}
|
||||
Ok(CommandOutcome::Query(data)) => AppEvent::DslDataSucceeded {
|
||||
command: command.clone(),
|
||||
data,
|
||||
},
|
||||
Ok(CommandOutcome::Query(data)) => {
|
||||
// ADR-0038: `show data` is the only DSL-form query that
|
||||
// echoes; its limited form orders by the table's primary
|
||||
// key, which is not on the Command — so the echo is built
|
||||
// post-execution from the schema (handoff §5). A
|
||||
// SQL-entered `SELECT` (also a Query outcome) has no echo.
|
||||
let echo = build_show_data_echo(&database, &command, submission_mode).await;
|
||||
AppEvent::DslDataSucceeded {
|
||||
command: command.clone(),
|
||||
data,
|
||||
echo,
|
||||
}
|
||||
}
|
||||
Ok(CommandOutcome::QueryPlan(plan)) => AppEvent::DslExplainSucceeded {
|
||||
command: command.clone(),
|
||||
plan,
|
||||
@@ -1307,22 +1316,27 @@ fn spawn_dsl_dispatch(
|
||||
Ok(CommandOutcome::Update(result)) => AppEvent::DslUpdateSucceeded {
|
||||
command: command.clone(),
|
||||
result,
|
||||
echo,
|
||||
},
|
||||
Ok(CommandOutcome::Delete(result)) => AppEvent::DslDeleteSucceeded {
|
||||
command: command.clone(),
|
||||
result,
|
||||
echo,
|
||||
},
|
||||
Ok(CommandOutcome::ChangeColumn(result)) => AppEvent::DslChangeColumnSucceeded {
|
||||
command: command.clone(),
|
||||
result,
|
||||
echo,
|
||||
},
|
||||
Ok(CommandOutcome::AddColumn(result)) => AppEvent::DslAddColumnSucceeded {
|
||||
command: command.clone(),
|
||||
result,
|
||||
echo,
|
||||
},
|
||||
Ok(CommandOutcome::DropColumn(result)) => AppEvent::DslDropColumnSucceeded {
|
||||
command: command.clone(),
|
||||
result,
|
||||
echo,
|
||||
},
|
||||
Err(DbError::PersistenceFatal {
|
||||
operation,
|
||||
@@ -1365,6 +1379,50 @@ fn spawn_dsl_dispatch(
|
||||
});
|
||||
}
|
||||
|
||||
/// Build the `show data` DSL → SQL teaching echo (ADR-0038).
|
||||
///
|
||||
/// `show data` is the one Bucket A row whose echo needs schema info beyond
|
||||
/// the `Command`: the limited form (`show data T limit n`) orders by the
|
||||
/// table's primary key for a stable "first n" (the worker's
|
||||
/// `build_query_data_sql`), and that column list is not on the `Command`.
|
||||
/// So when — and only when — the query is limited, this resolves the
|
||||
/// primary key via `describe_table` (the same best-effort schema lookup
|
||||
/// `enrich_dsl_failure` uses) and feeds it to [`crate::echo::echo_for_query`].
|
||||
///
|
||||
/// Silent in simple mode (gated before any lookup) and for a SQL-entered
|
||||
/// `SELECT` (not a `ShowData`). A describe failure or a primary-key-less
|
||||
/// table simply drops the `ORDER BY`, exactly as the worker does.
|
||||
async fn build_show_data_echo(
|
||||
database: &Database,
|
||||
command: &Command,
|
||||
submission_mode: crate::app::EffectiveMode,
|
||||
) -> Option<String> {
|
||||
if !submission_mode.is_advanced() {
|
||||
return None;
|
||||
}
|
||||
// The primary key is needed only for the `ORDER BY` of a limited query;
|
||||
// skip the lookup otherwise so the common case stays round-trip-free.
|
||||
let primary_key = match command {
|
||||
Command::ShowData {
|
||||
name,
|
||||
limit: Some(_),
|
||||
..
|
||||
} => database
|
||||
.describe_table(name.clone(), None)
|
||||
.await
|
||||
.map(|desc| {
|
||||
desc.columns
|
||||
.iter()
|
||||
.filter(|c| c.primary_key)
|
||||
.map(|c| c.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
crate::echo::echo_for_query(command, submission_mode, &primary_key)
|
||||
}
|
||||
|
||||
/// Build schema-resolved enrichment for a DSL failure (ADR-0019 §6).
|
||||
///
|
||||
/// Best-effort: every lookup is independently fallible and a
|
||||
@@ -2516,4 +2574,60 @@ mod tests {
|
||||
assert_eq!(d.visible(), None);
|
||||
assert!(!d.is_armed());
|
||||
}
|
||||
|
||||
// --- ADR-0038: the `show data` teaching echo's primary-key sourcing ---
|
||||
|
||||
/// End-to-end cover for `build_show_data_echo` against a real worker:
|
||||
/// the limited `show data` echo orders by the table's primary key,
|
||||
/// resolved from the schema post-execution (handoff §5 / ADR-0038 §4).
|
||||
/// The pure renderer is unit-tested in `echo`; this pins the describe →
|
||||
/// PK → `ORDER BY` glue, plus the simple-mode gate and the
|
||||
/// unlimited-no-lookup path.
|
||||
#[tokio::test]
|
||||
async fn show_data_echo_orders_by_resolved_primary_key_when_limited() {
|
||||
use crate::app::EffectiveMode;
|
||||
use crate::db::Database;
|
||||
use crate::dsl::Command;
|
||||
use crate::dsl::command::ColumnSpec;
|
||||
use crate::dsl::types::Type;
|
||||
|
||||
let db = Database::open(":memory:").expect("open in-memory");
|
||||
db.create_table(
|
||||
"Customers".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id", Type::Serial),
|
||||
ColumnSpec::new("name", Type::Text),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("create table");
|
||||
|
||||
let limited = Command::ShowData {
|
||||
name: "Customers".to_string(),
|
||||
filter: None,
|
||||
limit: Some(5),
|
||||
};
|
||||
// Limited → ORDER BY the resolved primary key.
|
||||
assert_eq!(
|
||||
super::build_show_data_echo(&db, &limited, EffectiveMode::AdvancedPersistent).await,
|
||||
Some("SELECT * FROM Customers ORDER BY id LIMIT 5".to_string()),
|
||||
);
|
||||
// Simple mode → silent, gated before any lookup.
|
||||
assert_eq!(
|
||||
super::build_show_data_echo(&db, &limited, EffectiveMode::Simple).await,
|
||||
None,
|
||||
);
|
||||
// Unlimited → no describe, no ORDER BY.
|
||||
let unlimited = Command::ShowData {
|
||||
name: "Customers".to_string(),
|
||||
filter: None,
|
||||
limit: None,
|
||||
};
|
||||
assert_eq!(
|
||||
super::build_show_data_echo(&db, &unlimited, EffectiveMode::AdvancedPersistent).await,
|
||||
Some("SELECT * FROM Customers".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user