diff --git a/src/app.rs b/src/app.rs index 6590e84..baf9477 100644 --- a/src/app.rs +++ b/src/app.rs @@ -539,9 +539,10 @@ impl App { command, result, echo, + dont_convert_caveat, } => { 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() } AppEvent::DslAddColumnSucceeded { @@ -1529,6 +1530,7 @@ impl App { &mut self, command: &Command, result: ChangeColumnTypeResult, + dont_convert_caveat: bool, ) { self.note_ok_summary(command); 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) { self.note_system(line); } @@ -3050,10 +3061,88 @@ mod tests { client_side: None, }, 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"); } + #[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] 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 diff --git a/src/event.rs b/src/event.rs index 499c77a..b5ffe94 100644 --- a/src/event.rs +++ b/src/event.rs @@ -100,9 +100,15 @@ pub enum AppEvent { command: Command, result: ChangeColumnTypeResult, /// The DSL → SQL teaching echo (ADR-0038): `ALTER TABLE T ALTER - /// COLUMN c SET DATA TYPE …`. `None` in simple mode. (The - /// `--dont-convert` caveat line is category-3, a later slice.) + /// COLUMN c SET DATA TYPE …`. `None` in simple mode. echo: Option>, + /// 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 /// post-add description plus any `[client-side]` notes diff --git a/src/friendly/keys.rs b/src/friendly/keys.rs index aa36793..0f8ac4c 100644 --- a/src/friendly/keys.rs +++ b/src/friendly/keys.rs @@ -503,6 +503,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("client_side.auto_fill_add_serial", &["count"]), ("client_side.auto_fill_add_shortid", &["count"]), ("client_side.auto_fill_transition", &["count", "kind"]), + ("client_side.dont_convert_caveat", &[]), ("client_side.transformed", &["count"]), ("client_side.transformed_lossy", &["count", "lossy"]), // ---- Replay command surfaces (ADR-0019 §9 sweep) ---- diff --git a/src/friendly/strings/en-US.yaml b/src/friendly/strings/en-US.yaml index ba65ed4..1111fb8 100644 --- a/src/friendly/strings/en-US.yaml +++ b/src/friendly/strings/en-US.yaml @@ -918,6 +918,15 @@ client_side: # non-empty table. 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. + # `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: diff --git a/src/runtime.rs b/src/runtime.rs index cf6541a..62de196 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1344,11 +1344,28 @@ fn spawn_dsl_dispatch( result, echo, }, - Ok(CommandOutcome::ChangeColumn(result)) => AppEvent::DslChangeColumnSucceeded { - command: command.clone(), - result, - echo, - }, + 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(), + result, + echo, + dont_convert_caveat, + } + } Ok(CommandOutcome::AddColumn(result)) => AppEvent::DslAddColumnSucceeded { command: command.clone(), result,