feat: DSL→SQL teaching echo — Phase 3 cat-3 caveat (ADR-0038)

Lands the only piece of category-3 prose not already covered by the
existing `client_side.*` notes infrastructure: the `change column …
--dont-convert` *caveat* (ADR-0038 §6, the only Bucket A caveat —
every other category-3 line is illuminating).

`--dont-convert` skips the client-side layer entirely, so the headline
SQL echo (`ALTER TABLE … SET DATA TYPE …`) is the nearest SQL but
*not* equivalent: running the line in advanced mode would convert the
stored values, but the playground left them as-is. The new caveat
states that divergence explicitly.

* New i18n key `client_side.dont_convert_caveat` (no placeholders) —
  registered in keys::KEYS_AND_PLACEHOLDERS.
* New `dont_convert_caveat: bool` field on DslChangeColumnSucceeded,
  set in the runtime when submission_mode is advanced *and* the
  command is ChangeColumnType { mode: DontConvert, .. }. Gated on
  advanced mode because the caveat references "the line above" — the
  echo, which only fires in advanced mode.
* App's handle_dsl_change_column_success emits the caveat line
  between the existing client-side notes and the structure render,
  so it reads alongside the echo, not after the table view.

The other two category-3 lines from §6 (shortid generation,
type-conversion transforms) were already in place via
`client_side.auto_fill_*` / `client_side.transformed*` — those notes
already render after the echo via handle_dsl_add_column_success /
handle_dsl_change_column_success, in the right position per the ADR.
This commit just adds the missing caveat.

Tests: 2014 passed / 0 failed / 1 ignored (pre-existing); clippy
clean. An App-level test pins the rendering order (caveat sits
after the echo, before the structure) and the simple-mode gate
(no caveat without an echo to refer to).

The §4 de-emphasised styled-runs rendering polish remains —
the echo + caveat lines are still plain `[system]` lines.
This commit is contained in:
claude@clouddev1
2026-05-28 08:50:08 +00:00
parent 275c726ad4
commit e6ad1aec3d
5 changed files with 130 additions and 8 deletions
+90 -1
View File
@@ -539,9 +539,10 @@ impl App {
command, command,
result, result,
echo, echo,
dont_convert_caveat,
} => { } => {
self.pending_echo = echo; self.pending_echo = echo;
self.handle_dsl_change_column_success(&command, result); self.handle_dsl_change_column_success(&command, result, dont_convert_caveat);
Vec::new() Vec::new()
} }
AppEvent::DslAddColumnSucceeded { AppEvent::DslAddColumnSucceeded {
@@ -1529,6 +1530,7 @@ impl App {
&mut self, &mut self,
command: &Command, command: &Command,
result: ChangeColumnTypeResult, result: ChangeColumnTypeResult,
dont_convert_caveat: bool,
) { ) {
self.note_ok_summary(command); self.note_ok_summary(command);
if let Some(note) = result.client_side { if let Some(note) = result.client_side {
@@ -1567,6 +1569,15 @@ impl App {
)); ));
} }
} }
// ADR-0038 §6 category 3 caveat: `--dont-convert` skips the
// client-side layer entirely, so the headline echo is the
// nearest SQL but *not* equivalent (the only Bucket A caveat —
// every other category-3 line is illuminating). Sits between
// the client-side notes and the structure render so it reads
// alongside the echo, not after the table view.
if dont_convert_caveat {
self.note_system(crate::t!("client_side.dont_convert_caveat"));
}
for line in crate::output_render::render_structure(&result.description) { for line in crate::output_render::render_structure(&result.description) {
self.note_system(line); self.note_system(line);
} }
@@ -3050,10 +3061,88 @@ mod tests {
client_side: None, client_side: None,
}, },
echo: Some(vec!["ALTER TABLE T ALTER COLUMN c SET DATA TYPE text".to_string()]), echo: Some(vec!["ALTER TABLE T ALTER COLUMN c SET DATA TYPE text".to_string()]),
dont_convert_caveat: false,
}); });
assert_echo_beneath_ok(&app, "ALTER TABLE T ALTER COLUMN c SET DATA TYPE text"); assert_echo_beneath_ok(&app, "ALTER TABLE T ALTER COLUMN c SET DATA TYPE text");
} }
#[test]
fn change_column_dont_convert_renders_the_caveat_between_notes_and_structure() {
// ADR-0038 §6 category 3 (Phase 3): when `change column …
// --dont-convert` ran in an advanced effective mode, the runtime
// sets `dont_convert_caveat = true`; the App emits the prose
// caveat between the existing client-side notes (none here, since
// --dont-convert skips that layer entirely) and the structure
// render, so it reads alongside the echo. Simple mode ⇒
// `dont_convert_caveat = false` ⇒ no caveat line.
use crate::db::ChangeColumnTypeResult;
use crate::dsl::ChangeColumnMode;
// Advanced mode + DontConvert → caveat fires.
let mut app = App::new();
app.update(AppEvent::DslChangeColumnSucceeded {
command: Command::ChangeColumnType {
table: "T".to_string(),
column: "c".to_string(),
ty: Type::Int,
mode: ChangeColumnMode::DontConvert,
},
result: ChangeColumnTypeResult {
description: sample_description("T"),
// --dont-convert skips the client-side layer → None.
client_side: None,
},
echo: Some(vec![
"ALTER TABLE T ALTER COLUMN c SET DATA TYPE int".to_string(),
]),
dont_convert_caveat: true,
});
let texts: Vec<&str> = app.output.iter().map(|l| l.text.as_str()).collect();
let echo_idx = texts
.iter()
.position(|t| t.contains("Executing SQL:"))
.expect("echo line");
let caveat_idx = texts
.iter()
.position(|t| t.contains("`--dont-convert` kept the stored values"))
.expect("caveat line");
assert!(
caveat_idx > echo_idx,
"caveat sits after the echo (reads alongside the line above): {texts:?}",
);
// And before the structure render (the structure shows column names).
if let Some(structure_idx) = texts.iter().position(|t| t.contains("Columns")) {
assert!(
caveat_idx < structure_idx,
"caveat sits before the structure render: {texts:?}",
);
}
// Simple mode → no echo, no caveat.
let mut app = App::new();
app.update(AppEvent::DslChangeColumnSucceeded {
command: Command::ChangeColumnType {
table: "T".to_string(),
column: "c".to_string(),
ty: Type::Int,
mode: ChangeColumnMode::DontConvert,
},
result: ChangeColumnTypeResult {
description: sample_description("T"),
client_side: None,
},
echo: None,
dont_convert_caveat: false,
});
assert!(
!app.output
.iter()
.any(|l| l.text.contains("--dont-convert")),
"no caveat in simple mode (no echo to refer to)",
);
}
#[test] #[test]
fn bucket_b_multi_line_echo_renders_one_line_per_statement_beneath_ok() { fn bucket_b_multi_line_echo_renders_one_line_per_statement_beneath_ok() {
// ADR-0038 §6 category 2 / §4 / Phase 2 Slice 2b: a `drop column // ADR-0038 §6 category 2 / §4 / Phase 2 Slice 2b: a `drop column
+8 -2
View File
@@ -100,9 +100,15 @@ pub enum AppEvent {
command: Command, command: Command,
result: ChangeColumnTypeResult, result: ChangeColumnTypeResult,
/// The DSL → SQL teaching echo (ADR-0038): `ALTER TABLE T ALTER /// The DSL → SQL teaching echo (ADR-0038): `ALTER TABLE T ALTER
/// COLUMN c SET DATA TYPE …`. `None` in simple mode. (The /// COLUMN c SET DATA TYPE …`. `None` in simple mode.
/// `--dont-convert` caveat line is category-3, a later slice.)
echo: Option<Vec<String>>, echo: Option<Vec<String>>,
/// The `--dont-convert` caveat (ADR-0038 §6 category 3): set by the
/// runtime to `true` when the command ran with
/// `ChangeColumnMode::DontConvert` in an advanced effective mode
/// (the only case where it is meaningful — the line references
/// "the line above," i.e. the echo). The App renders the prose
/// caveat between the existing client-side notes and the structure.
dont_convert_caveat: bool,
}, },
/// An `add column …` succeeded. `result` carries the /// An `add column …` succeeded. `result` carries the
/// post-add description plus any `[client-side]` notes /// post-add description plus any `[client-side]` notes
+1
View File
@@ -503,6 +503,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
("client_side.auto_fill_add_serial", &["count"]), ("client_side.auto_fill_add_serial", &["count"]),
("client_side.auto_fill_add_shortid", &["count"]), ("client_side.auto_fill_add_shortid", &["count"]),
("client_side.auto_fill_transition", &["count", "kind"]), ("client_side.auto_fill_transition", &["count", "kind"]),
("client_side.dont_convert_caveat", &[]),
("client_side.transformed", &["count"]), ("client_side.transformed", &["count"]),
("client_side.transformed_lossy", &["count", "lossy"]), ("client_side.transformed_lossy", &["count", "lossy"]),
// ---- Replay command surfaces (ADR-0019 §9 sweep) ---- // ---- Replay command surfaces (ADR-0019 §9 sweep) ----
+9
View File
@@ -918,6 +918,15 @@ client_side:
# non-empty table. # non-empty table.
auto_fill_add_shortid: |- auto_fill_add_shortid: |-
[client-side] {count} row(s) given auto-generated shortid values. In raw SQL this would need an explicit UPDATE to populate. [client-side] {count} row(s) given auto-generated shortid values. In raw SQL this would need an explicit UPDATE to populate.
# `change column … --dont-convert` caveat (ADR-0038 §6 category 3,
# Phase 3). The only category-3 *caveat* — the others are illuminating
# (the headline echo already matches the effect). Here the headline
# SQL (`ALTER TABLE … SET DATA TYPE …`) *would* convert if run, but
# `--dont-convert` skipped the client-side layer, so the line above is
# not equivalent. Fires only in an advanced effective mode (the line
# references "the line above," i.e. the echo).
dont_convert_caveat: |-
[client-side] `--dont-convert` kept the stored values as-is; standard SQL always converts, so running the line above would transform them instead.
# ---- Replay command surfaces (ADR-0019 §9 sweep) --------------------- # ---- Replay command surfaces (ADR-0019 §9 sweep) ---------------------
replay: replay:
+19 -2
View File
@@ -1344,11 +1344,28 @@ fn spawn_dsl_dispatch(
result, result,
echo, echo,
}, },
Ok(CommandOutcome::ChangeColumn(result)) => AppEvent::DslChangeColumnSucceeded { Ok(CommandOutcome::ChangeColumn(result)) => {
// ADR-0038 §6 category 3 caveat: `--dont-convert` skips
// the client-side layer entirely, so the headline echo
// (`ALTER TABLE … SET DATA TYPE …`) is the *nearest*
// SQL but not equivalent. The caveat only makes sense
// next to the echo, so gate on advanced mode (the line
// references "the line above").
let dont_convert_caveat = submission_mode.is_advanced()
&& matches!(
&command,
Command::ChangeColumnType {
mode: ChangeColumnMode::DontConvert,
..
}
);
AppEvent::DslChangeColumnSucceeded {
command: command.clone(), command: command.clone(),
result, result,
echo, echo,
}, dont_convert_caveat,
}
}
Ok(CommandOutcome::AddColumn(result)) => AppEvent::DslAddColumnSucceeded { Ok(CommandOutcome::AddColumn(result)) => AppEvent::DslAddColumnSucceeded {
command: command.clone(), command: command.clone(),
result, result,