From 41b7e9a04992cd9708f1775b57044de838b48b85 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Wed, 17 Jun 2026 21:39:19 +0000 Subject: [PATCH] style: format the whole tree with cargo fmt (stock defaults, #35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One-time, mechanical reformat — no functional changes. The tree was not rustfmt-clean (~1800 hunks across ~100 files); this brings it to stock `cargo fmt` defaults so a `cargo fmt --check` CI gate can follow. Behaviour-preserving: 2509 pass / 0 fail / 1 ignored (unchanged baseline), clippy clean. A .git-blame-ignore-revs entry follows so `git blame` skips this commit. --- src/app.rs | 398 ++-- src/archive.rs | 74 +- src/cli.rs | 68 +- src/completion.rs | 464 +++-- src/db.rs | 1936 +++++++++++------- src/dsl/command.rs | 21 +- src/dsl/grammar/app.rs | 71 +- src/dsl/grammar/data.rs | 179 +- src/dsl/grammar/ddl.rs | 412 ++-- src/dsl/grammar/expr.rs | 59 +- src/dsl/grammar/mod.rs | 44 +- src/dsl/grammar/shared.rs | 41 +- src/dsl/grammar/sql_create_table.rs | 94 +- src/dsl/grammar/sql_delete.rs | 14 +- src/dsl/grammar/sql_expr.rs | 94 +- src/dsl/grammar/sql_insert.rs | 39 +- src/dsl/grammar/sql_select.rs | 159 +- src/dsl/grammar/sql_update.rs | 14 +- src/dsl/mod.rs | 6 +- src/dsl/parser.rs | 43 +- src/dsl/shortid.rs | 3 +- src/dsl/sql_functions.rs | 30 +- src/dsl/types.rs | 11 +- src/dsl/value.rs | 55 +- src/dsl/walker/driver.rs | 298 ++- src/dsl/walker/highlight.rs | 69 +- src/dsl/walker/lex_helpers.rs | 4 +- src/dsl/walker/mod.rs | 989 ++++----- src/echo.rs | 148 +- src/event.rs | 15 +- src/friendly/format.rs | 14 +- src/friendly/keys.rs | 53 +- src/friendly/mod.rs | 4 +- src/friendly/translate.rs | 155 +- src/input_render.rs | 282 ++- src/logging.rs | 7 +- src/main.rs | 2 +- src/output_render.rs | 111 +- src/persistence/csv_io.rs | 42 +- src/persistence/history.rs | 51 +- src/persistence/migrations.rs | 80 +- src/persistence/mod.rs | 38 +- src/persistence/yaml.rs | 152 +- src/project/lock.rs | 10 +- src/project/mod.rs | 57 +- src/project/naming.rs | 45 +- src/project/prettifier.rs | 10 +- src/runtime.rs | 274 ++- src/seed/check.rs | 10 +- src/seed/generators.rs | 169 +- src/seed/heuristics.rs | 225 +- src/seed/mod.rs | 7 +- src/theme.rs | 67 +- src/type_change.rs | 39 +- src/ui.rs | 373 ++-- src/undo.rs | 18 +- tests/it/case_insensitive_names.rs | 60 +- tests/it/column_op_guards.rs | 30 +- tests/it/compound_fk.rs | 79 +- tests/it/engine_vocabulary_audit.rs | 13 +- tests/it/friendly_enrichment.rs | 84 +- tests/it/help_command.rs | 2 +- tests/it/iteration2_persistence.rs | 33 +- tests/it/iteration3_rebuild.rs | 65 +- tests/it/iteration4a_rebuild_command.rs | 16 +- tests/it/iteration4b_lifecycle_commands.rs | 9 +- tests/it/iteration5_export_import.rs | 18 +- tests/it/iteration6_resume_history.rs | 3 +- tests/it/m2n.rs | 210 +- tests/it/main.rs | 2 +- tests/it/parse_error_pedagogy.rs | 540 ++++- tests/it/project_lifecycle.rs | 33 +- tests/it/replay_command.rs | 113 +- tests/it/seed.rs | 469 ++++- tests/it/show_list.rs | 70 +- tests/it/sql_alter_table.rs | 256 ++- tests/it/sql_create_index.rs | 62 +- tests/it/sql_create_table.rs | 609 ++++-- tests/it/sql_delete.rs | 325 ++- tests/it/sql_dml_e2e.rs | 296 ++- tests/it/sql_drop_index.rs | 31 +- tests/it/sql_drop_table.rs | 85 +- tests/it/sql_insert.rs | 523 ++++- tests/it/sql_select.rs | 98 +- tests/it/sql_update.rs | 247 ++- tests/it/undo_snapshots.rs | 14 +- tests/it/walking_skeleton.rs | 79 +- tests/typing_surface/add_relationship.rs | 20 +- tests/typing_surface/create_m2n.rs | 5 +- tests/typing_surface/create_table.rs | 15 +- tests/typing_surface/delete_with_where.rs | 18 +- tests/typing_surface/drop_column.rs | 5 +- tests/typing_surface/drop_relationship.rs | 15 +- tests/typing_surface/explain.rs | 10 +- tests/typing_surface/insert_form_a.rs | 44 +- tests/typing_surface/insert_form_b.rs | 76 +- tests/typing_surface/insert_form_c.rs | 35 +- tests/typing_surface/mod.rs | 51 +- tests/typing_surface/rename_change_column.rs | 15 +- tests/typing_surface/update_all_rows.rs | 20 +- tests/typing_surface/update_with_where.rs | 37 +- tests/typing_surface/where_expression.rs | 5 +- 102 files changed, 8017 insertions(+), 4975 deletions(-) diff --git a/src/app.rs b/src/app.rs index efba2fa..ad721b8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -509,7 +509,10 @@ pub enum LoadPickerSubMode { /// Switched to via `b`. Same input/cursor surface as /// `PathEntryModal`; kept inline so the picker can flip /// back to List with `Esc`. - PathEntry { input: String, cursor: usize }, + PathEntry { + input: String, + cursor: usize, + }, } const PAGE_SCROLL_LINES: usize = 5; @@ -697,9 +700,7 @@ impl App { // `trimmed[1..].trim()`. let leading_ws = self.input.len() - self.input.trim_start().len(); let mut offset = leading_ws + 1; // past the `:` - while offset < self.input.len() - && self.input.as_bytes()[offset].is_ascii_whitespace() - { + while offset < self.input.len() && self.input.as_bytes()[offset].is_ascii_whitespace() { offset += 1; } let view = &self.input[offset..]; @@ -727,8 +728,7 @@ impl App { pub fn input_validity_verdict(&self) -> Option { let mode = match self.effective_mode() { EffectiveMode::Simple => Mode::Simple, - EffectiveMode::AdvancedPersistent - | EffectiveMode::AdvancedOneShot => Mode::Advanced, + EffectiveMode::AdvancedPersistent | EffectiveMode::AdvancedOneShot => Mode::Advanced, }; // Strip the `:` one-shot prefix so the walker verdicts the SQL // itself, not the escape marker (which it can't parse). @@ -1037,10 +1037,7 @@ impl App { Vec::new() } AppEvent::ExportSucceeded { path } => { - self.note_system(crate::t!( - "project.export_ok", - path = path.display() - )); + self.note_system(crate::t!("project.export_ok", path = path.display())); Vec::new() } AppEvent::ExportFailed { error } => { @@ -1056,11 +1053,7 @@ impl App { // `[ok] replay — N command(s)` summary is payload-bearing // (the count) and stays. self.mark_oldest_pending_echo(EchoStatus::Ok); - self.note_system(crate::t!( - "replay.completed", - path = path, - count = count - )); + self.note_system(crate::t!("replay.completed", path = path, count = count)); // ADR-0034: surface `[skip]` warnings for app-lifecycle // commands whose omission can leave the replayed state // incomplete (`import`, nested `replay`). @@ -1084,11 +1077,7 @@ impl App { // it, mirroring how the interactive `running: …` // path renders source-line context above an error. if line_number == 0 { - self.note_error(crate::t!( - "replay.failed_open", - path = path, - error = error - )); + self.note_error(crate::t!("replay.failed_open", path = path, error = error)); } else { self.note_error(crate::t!( "replay.failed_at_line", @@ -1097,10 +1086,7 @@ impl App { error = error )); if !command.is_empty() { - self.note_error(crate::t!( - "replay.command_echo", - command = command - )); + self.note_error(crate::t!("replay.command_echo", command = command)); } } Vec::new() @@ -1237,8 +1223,7 @@ impl App { // stays F1-only. let hint_key = key.code == KeyCode::F(1) || (self.demo_mode - && (key.code, key.modifiers) - == (KeyCode::Char('g'), KeyModifiers::CONTROL)); + && (key.code, key.modifiers) == (KeyCode::Char('g'), KeyModifiers::CONTROL)); if hint_key { if self.input.trim().is_empty() { self.note_hint_for_recent_error(); @@ -1369,8 +1354,8 @@ impl App { /// against crossterm 0.29). Only active in demo mode (the caller /// gates on `self.demo_mode`). fn handle_demo_caption_key(&mut self, key: KeyEvent) -> Option> { - let is_toggle = key.code == KeyCode::Char('5') - && key.modifiers.contains(KeyModifiers::CONTROL); + let is_toggle = + key.code == KeyCode::Char('5') && key.modifiers.contains(KeyModifiers::CONTROL); if self.demo_caption_capturing { if is_toggle { @@ -1379,8 +1364,7 @@ impl App { self.demo_caption_capturing = false; let text = std::mem::take(&mut self.demo_caption_buffer); let trimmed = text.trim(); - self.demo_caption = - (!trimmed.is_empty()).then(|| trimmed.to_string()); + self.demo_caption = (!trimmed.is_empty()).then(|| trimmed.to_string()); } else { match key.code { // Plain characters accumulate invisibly; the prompt @@ -1553,7 +1537,10 @@ impl App { &self.schema_cache, self.effective_mode().as_mode(), )?; - comp.replaced_range = (comp.replaced_range.0 + offset, comp.replaced_range.1 + offset); + comp.replaced_range = ( + comp.replaced_range.0 + offset, + comp.replaced_range.1 + offset, + ); Some(comp) } @@ -1581,8 +1568,7 @@ impl App { idx: usize, ) -> crate::completion::LastCompletion { let inserted = comp.candidates[idx].text.clone(); - let original_text = - self.input[comp.replaced_range.0..comp.replaced_range.1].to_string(); + let original_text = self.input[comp.replaced_range.0..comp.replaced_range.1].to_string(); self.input .replace_range(comp.replaced_range.0..comp.replaced_range.1, &inserted); let new_end = comp.replaced_range.0 + inserted.len(); @@ -1777,7 +1763,10 @@ impl App { // teaching echo (ADR-0038) on an advanced effective mode. let (submission_mode, effective_input) = if self.mode == Mode::Simple && trimmed.starts_with(':') { - (EffectiveMode::AdvancedOneShot, trimmed[1..].trim().to_string()) + ( + EffectiveMode::AdvancedOneShot, + trimmed[1..].trim().to_string(), + ) } else if self.mode == Mode::Advanced { (EffectiveMode::AdvancedPersistent, trimmed.to_string()) } else { @@ -1845,11 +1834,7 @@ impl App { /// simple and advanced modes; the parse-first refactor /// (round-5) routes app commands here before the /// mode-specific DSL/SQL paths. - fn dispatch_app_command( - &mut self, - cmd: crate::dsl::AppCommand, - source: &str, - ) -> Vec { + fn dispatch_app_command(&mut self, cmd: crate::dsl::AppCommand, source: &str) -> Vec { use crate::dsl::{AppCommand, MessagesValue, ModeValue}; debug!(command = ?cmd, "dispatch app command"); match cmd { @@ -2009,11 +1994,8 @@ impl App { // mode so the walker gates SQL-only forms — simple-mode // `select` returns the "this is SQL" hint as a normal // parse error and is rendered through the Err arm below. - match crate::dsl::parser::parse_command_with_schema_in_mode( - input, - &self.schema_cache, - mode, - ) { + match crate::dsl::parser::parse_command_with_schema_in_mode(input, &self.schema_cache, mode) + { Ok(Command::Replay { path }) => { // `replay` is parsed as a DSL command for the // sake of grammar uniformity, but its execution @@ -2127,15 +2109,9 @@ impl App { .get(..*position) .map_or(*position, |s| s.chars().count()); let pad = prefix.chars().count() + chars_before; - self.note_error(crate::t!( - "parse.caret", - padding = " ".repeat(pad) - )); + self.note_error(crate::t!("parse.caret", padding = " ".repeat(pad))); } - self.note_error(crate::t!( - "parse.error", - detail = parse_error_message(&err) - )); + self.note_error(crate::t!("parse.error", detail = parse_error_message(&err))); // ADR-0033 Amendment 3: combine the DSL error with a // pointer to advanced mode when the same line would // run as SQL there. Only in simple mode (a one-shot @@ -2228,7 +2204,11 @@ impl App { | Command::AddRelationship { .. } | Command::DropRelationship { .. } ) { - debug!(verb = command.verb(), width = self.last_output_width, "render: relationship diagrams (ADR-0044)"); + debug!( + verb = command.verb(), + width = self.last_output_width, + "render: relationship diagrams (ADR-0044)" + ); for line in crate::output_render::render_structure_with_diagrams( desc, self.last_output_width, @@ -2252,11 +2232,7 @@ impl App { } } - fn handle_dsl_explain_success( - &mut self, - command: &Command, - plan: &crate::db::QueryPlan, - ) { + fn handle_dsl_explain_success(&mut self, command: &Command, plan: &crate::db::QueryPlan) { self.note_ok_summary(command); // ADR-0028 §3: the display SQL, then the plan tree. // `render_explain_plan` returns ready-built `OutputLine`s @@ -2350,11 +2326,7 @@ impl App { } } - fn handle_dsl_add_column_success( - &mut self, - command: &Command, - result: AddColumnResult, - ) { + fn handle_dsl_add_column_success(&mut self, command: &Command, result: AddColumnResult) { self.note_ok_summary(command); // ADR-0018 §9 / ADR-0038 §6 category 3: emit auto-fill note(s) // before the structure render so the pedagogical "the tool did @@ -2369,19 +2341,12 @@ impl App { self.current_table = Some(result.description); } - fn handle_dsl_drop_column_success( - &mut self, - command: &Command, - result: DropColumnResult, - ) { + fn handle_dsl_drop_column_success(&mut self, command: &Command, result: DropColumnResult) { self.note_ok_summary(command); // ADR-0025: when `--cascade` removed covering indexes, // name each one so the learner sees the side effect. for index in &result.dropped_indexes { - self.note_system(crate::t!( - "ok.index_dropped_with_column", - index = index, - )); + self.note_system(crate::t!("ok.index_dropped_with_column", index = index,)); } for line in crate::output_render::render_structure(&result.description) { self.note_system(line); @@ -2415,10 +2380,7 @@ impl App { lossy = note.lossy ) } else { - crate::t!( - "client_side.transformed", - count = note.transformed - ) + crate::t!("client_side.transformed", count = note.transformed) }; self.push_category_three_prose(line); } @@ -2583,9 +2545,7 @@ impl App { (Operation::RenameTable, Some(table.as_str()), None) } }, - C::SqlCreateTable { name, .. } => { - (Operation::CreateTable, Some(name.as_str()), None) - } + C::SqlCreateTable { name, .. } => (Operation::CreateTable, Some(name.as_str()), None), C::DropTable { name } => (Operation::DropTable, Some(name.as_str()), None), C::SqlDropTable { name, .. } => (Operation::DropTable, Some(name.as_str()), None), C::AddColumn { table, column, .. } => ( @@ -2635,9 +2595,7 @@ impl App { // SQL `CREATE [UNIQUE] INDEX` shares the add-index operation // (it reuses `do_add_index`); route engine/validation errors // through it with the parsed table. - C::SqlCreateIndex { table, .. } => { - (Operation::AddIndex, Some(table.as_str()), None) - } + C::SqlCreateIndex { table, .. } => (Operation::AddIndex, Some(table.as_str()), None), C::AddConstraint { table, column, .. } => ( Operation::AddConstraint, Some(table.as_str()), @@ -2700,19 +2658,13 @@ impl App { // `dispatch_input` routes them through // `dispatch_app_command` before the DSL execution // pipeline that this context builder feeds. - C::App(_) => unreachable!( - "App commands are dispatched before reaching dsl execution" - ), + C::App(_) => unreachable!("App commands are dispatched before reaching dsl execution"), }; TranslateContext { operation: Some(operation), - table: facts - .table - .or_else(|| fallback_table.map(str::to_string)), - column: facts - .column - .or_else(|| fallback_column.map(str::to_string)), + table: facts.table.or_else(|| fallback_table.map(str::to_string)), + column: facts.column.or_else(|| fallback_column.map(str::to_string)), child_table: facts.child_table, parent_table: facts.parent_table, parent_column: facts.parent_column, @@ -2818,11 +2770,7 @@ impl App { } } - fn handle_path_entry_key( - &mut self, - key: KeyEvent, - mut state: PathEntryModal, - ) -> Vec { + fn handle_path_entry_key(&mut self, key: KeyEvent, mut state: PathEntryModal) -> Vec { match key.code { KeyCode::Esc => { self.modal = None; @@ -2904,11 +2852,7 @@ impl App { } } - fn handle_load_picker_key( - &mut self, - key: KeyEvent, - mut state: LoadPickerModal, - ) -> Vec { + fn handle_load_picker_key(&mut self, key: KeyEvent, mut state: LoadPickerModal) -> Vec { match &mut state.sub_mode { LoadPickerSubMode::List => match key.code { KeyCode::Esc => { @@ -3198,7 +3142,10 @@ impl App { .map(|c| c.text.clone()) .collect::>() .join(", "); - self.push_category_three_prose(crate::t!("hint.ambient_expected", expected = names)); + self.push_category_three_prose(crate::t!( + "hint.ambient_expected", + expected = names + )); } None => self.note_getting_started(), } @@ -3413,10 +3360,7 @@ fn render_usage_block(input: &str, mode: Mode) -> String { .into_iter() .map(|w| format!("`{w}`")) .collect(); - crate::t!( - "parse.available_commands", - commands = names.join(", ") - ) + crate::t!("parse.available_commands", commands = names.join(", ")) } fn render_cascade_effect(effect: &CascadeEffect) -> String { @@ -3424,9 +3368,7 @@ fn render_cascade_effect(effect: &CascadeEffect) -> String { let action_key = match effect.action { ReferentialAction::Cascade => "db.cascade.action_deleted", ReferentialAction::SetNull => "db.cascade.action_set_null", - ReferentialAction::Restrict | ReferentialAction::NoAction => { - "db.cascade.action_blocked" - } + ReferentialAction::Restrict | ReferentialAction::NoAction => "db.cascade.action_blocked", }; crate::t!( "db.cascade.summary", @@ -3464,7 +3406,10 @@ mod tests { fn demo_badge_label_maps_the_invisible_keys() { let none = KeyModifiers::NONE; assert_eq!(demo_badge_label(&ke(KeyCode::Tab, none)), Some("[TAB]")); - assert_eq!(demo_badge_label(&ke(KeyCode::BackTab, KeyModifiers::SHIFT)), Some("[SHIFT-TAB]")); + assert_eq!( + demo_badge_label(&ke(KeyCode::BackTab, KeyModifiers::SHIFT)), + Some("[SHIFT-TAB]") + ); assert_eq!(demo_badge_label(&ke(KeyCode::Enter, none)), Some("[ENTER]")); assert_eq!(demo_badge_label(&ke(KeyCode::Esc, none)), Some("[ESC]")); assert_eq!(demo_badge_label(&ke(KeyCode::Up, none)), Some("[UP]")); @@ -3474,8 +3419,14 @@ mod tests { assert_eq!(demo_badge_label(&ke(KeyCode::Home, none)), Some("[HOME]")); assert_eq!(demo_badge_label(&ke(KeyCode::End, none)), Some("[END]")); assert_eq!(demo_badge_label(&ke(KeyCode::PageUp, none)), Some("[PGUP]")); - assert_eq!(demo_badge_label(&ke(KeyCode::PageDown, none)), Some("[PGDN]")); - assert_eq!(demo_badge_label(&ke(KeyCode::Backspace, none)), Some("[BKSP]")); + assert_eq!( + demo_badge_label(&ke(KeyCode::PageDown, none)), + Some("[PGDN]") + ); + assert_eq!( + demo_badge_label(&ke(KeyCode::Backspace, none)), + Some("[BKSP]") + ); assert_eq!(demo_badge_label(&ke(KeyCode::Delete, none)), Some("[DEL]")); assert_eq!( demo_badge_label(&ke(KeyCode::Char('o'), KeyModifiers::CONTROL)), @@ -3493,12 +3444,24 @@ mod tests { #[test] fn demo_badge_label_none_for_glyphs_and_excluded_chords() { // Plain characters render their own glyph — no badge. - assert_eq!(demo_badge_label(&ke(KeyCode::Char('a'), KeyModifiers::NONE)), None); - assert_eq!(demo_badge_label(&ke(KeyCode::Char(' '), KeyModifiers::NONE)), None); + assert_eq!( + demo_badge_label(&ke(KeyCode::Char('a'), KeyModifiers::NONE)), + None + ); + assert_eq!( + demo_badge_label(&ke(KeyCode::Char(' '), KeyModifiers::NONE)), + None + ); // Quit and the (Phase C) caption toggle are deliberately excluded. - assert_eq!(demo_badge_label(&ke(KeyCode::Char('c'), KeyModifiers::CONTROL)), None); + assert_eq!( + demo_badge_label(&ke(KeyCode::Char('c'), KeyModifiers::CONTROL)), + None + ); // Ctrl+] decodes to Char('5')+CONTROL — must not badge. - assert_eq!(demo_badge_label(&ke(KeyCode::Char('5'), KeyModifiers::CONTROL)), None); + assert_eq!( + demo_badge_label(&ke(KeyCode::Char('5'), KeyModifiers::CONTROL)), + None + ); } #[test] @@ -3606,7 +3569,10 @@ mod tests { assert!(app.demo_caption_capturing, "still capturing"); assert_eq!(app.demo_caption_buffer, "note"); assert_eq!(app.input, ""); - assert_eq!(app.demo_badge, None, "inert keys raise no badge while capturing"); + assert_eq!( + app.demo_badge, None, + "inert keys raise no badge while capturing" + ); } #[test] @@ -4211,7 +4177,9 @@ mod tests { type_str(&mut app, "copy sideways"); let actions = submit(&mut app); assert!( - !actions.iter().any(|a| matches!(a, Action::CopyToClipboard(_))), + !actions + .iter() + .any(|a| matches!(a, Action::CopyToClipboard(_))), "an unknown target does not copy", ); let rendered = app @@ -4399,7 +4367,10 @@ mod tests { ); // … names the table's columns so the user can see what's needed … assert!( - out.contains("Name") && out.contains("Age") && out.contains("id") && out.contains("SerNo"), + out.contains("Name") + && out.contains("Age") + && out.contains("id") + && out.contains("SerNo"), "missing the column-name list in: {out}", ); // … and shows the column-list override targeting the non-auto columns. @@ -4423,9 +4394,15 @@ mod tests { let _ = submit(&mut app); let out = error_lines(&app); // The teaching line names the user-supplied columns … - assert!(out.contains("Name") && out.contains("Age"), "missing non-auto column names in: {out}"); + assert!( + out.contains("Name") && out.contains("Age"), + "missing non-auto column names in: {out}" + ); // … the auto-generated columns … - assert!(out.contains("id") && out.contains("SerNo"), "missing auto column names in: {out}"); + assert!( + out.contains("id") && out.contains("SerNo"), + "missing auto column names in: {out}" + ); // … signals the contract … assert!( out.contains("auto-generated"), @@ -4520,10 +4497,7 @@ mod tests { let mut app = App::new(); install_customers_schema_two_serials(&mut app); app.mode = Mode::Advanced; - type_str( - &mut app, - "insert into Customers values (13, 'Oli', 42, 13)", - ); + type_str(&mut app, "insert into Customers values (13, 'Oli', 42, 13)"); let actions = submit(&mut app); assert!( actions @@ -4552,7 +4526,9 @@ mod tests { type_str(&mut app, "insert into Customers values ('Oli', 52, 3)"); let actions = submit(&mut app); assert!( - !actions.iter().any(|a| matches!(a, Action::ExecuteDsl { .. })), + !actions + .iter() + .any(|a| matches!(a, Action::ExecuteDsl { .. })), "simple-mode Form B count mismatch must NOT dispatch; got: {actions:?}", ); } @@ -4565,7 +4541,9 @@ mod tests { type_str(&mut app, "insert into Customers values ('Oli')"); let actions = submit(&mut app); assert!( - !actions.iter().any(|a| matches!(a, Action::ExecuteDsl { .. })), + !actions + .iter() + .any(|a| matches!(a, Action::ExecuteDsl { .. })), "simple-mode Form B under-supply must NOT dispatch; got: {actions:?}", ); } @@ -4580,7 +4558,9 @@ mod tests { type_str(&mut app, "insert into Customers (Name, Age) values ('Oli')"); let actions = submit(&mut app); assert!( - !actions.iter().any(|a| matches!(a, Action::ExecuteDsl { .. })), + !actions + .iter() + .any(|a| matches!(a, Action::ExecuteDsl { .. })), "simple-mode Form A count mismatch must NOT dispatch; got: {actions:?}", ); } @@ -4616,9 +4596,7 @@ mod tests { for c in &tc { app.schema_cache.columns.push(c.name.clone()); } - app.schema_cache - .table_columns - .insert("T".to_string(), tc); + app.schema_cache.table_columns.insert("T".to_string(), tc); type_str(&mut app, "insert into T values (1, 2), (3, 4)"); let actions = submit(&mut app); assert!( @@ -4649,11 +4627,11 @@ mod tests { // advanced-mode hint at all, so we look for any line carrying // the "mode advanced" actionable fragment that the pointer // always emits. - let has_pointer = app - .output - .iter() - .any(|l| l.text.contains("mode advanced")); - assert!(!has_pointer, "unknown command must not point at advanced mode"); + let has_pointer = app.output.iter().any(|l| l.text.contains("mode advanced")); + assert!( + !has_pointer, + "unknown command must not point at advanced mode" + ); } #[test] @@ -4702,7 +4680,11 @@ mod tests { app.mode = mode; type_str(&mut app, input); match submit(&mut app).as_slice() { - [Action::ExecuteDsl { submission_mode, .. }] => *submission_mode, + [ + Action::ExecuteDsl { + submission_mode, .. + }, + ] => *submission_mode, other => panic!("expected one ExecuteDsl; got {other:?}"), } }; @@ -4733,7 +4715,9 @@ mod tests { app.update(AppEvent::DslSucceeded { command: cmd.clone(), description: None, - echo: Some(vec!["CREATE TABLE Other (id serial PRIMARY KEY)".to_string()]), + echo: Some(vec![ + "CREATE TABLE Other (id serial PRIMARY KEY)".to_string(), + ]), }); let texts: Vec<&str> = app.output.iter().map(|l| l.text.as_str()).collect(); // ADR-0040: no `[ok]` summary; with no preceding `running:` echo @@ -4792,7 +4776,10 @@ mod tests { .position(|t| t.contains("Executing SQL:")) .expect("an echo line"); assert_eq!(echo_idx, 0, "teaching echo leads the output: {texts:?}"); - assert!(texts[echo_idx].contains(expected), "echo carries the SQL: {texts:?}"); + assert!( + texts[echo_idx].contains(expected), + "echo carries the SQL: {texts:?}" + ); // ADR-0038 §4 polish: every success arm now wires the echo as // `OutputKind::TeachingEcho` so `ui::render_output_line` fires // the dim-prefix + advanced-lex custom branch. Pinning this @@ -4911,7 +4898,9 @@ mod tests { description: sample_description("T"), 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"); @@ -5126,9 +5115,7 @@ mod tests { dont_convert_caveat: false, }); assert!( - !app.output - .iter() - .any(|l| l.text.contains("--dont-convert")), + !app.output.iter().any(|l| l.text.contains("--dont-convert")), "no caveat in simple mode (no echo to refer to)", ); } @@ -5198,7 +5185,10 @@ mod tests { ); // Pin the `Executing SQL:` prefix repeats once per statement // (the plain-rendering shape until the styled-runs polish lands). - let exec_count = texts.iter().filter(|t| t.contains("Executing SQL:")).count(); + let exec_count = texts + .iter() + .filter(|t| t.contains("Executing SQL:")) + .count(); assert_eq!(exec_count, 3, "one Executing SQL: per statement: {texts:?}"); } @@ -5223,19 +5213,13 @@ mod tests { // (could be the caret line, the parse-error detail // line, or the usage line). Scan for the friendly // "unknown mode" anchor phrase. - let anywhere = app - .output - .iter() - .any(|l| l.text.contains("unknown mode")); + let anywhere = app.output.iter().any(|l| l.text.contains("unknown mode")); assert!( anywhere, "expected 'unknown mode' somewhere in output: {:?}", app.output.iter().map(|l| &l.text).collect::>(), ); - let any_error = app - .output - .iter() - .any(|l| l.kind == OutputKind::Error); + let any_error = app.output.iter().any(|l| l.kind == OutputKind::Error); assert!(any_error, "expected at least one Error line"); } @@ -5325,11 +5309,17 @@ mod tests { app.schema_cache.tables = vec!["Orders".into(), "Customers".into()]; app.schema_cache.table_columns.insert( "Orders".into(), - vec![TableColumn::new("id", Type::Serial), TableColumn::new("customer_id", Type::Int)], + vec![ + TableColumn::new("id", Type::Serial), + TableColumn::new("customer_id", Type::Int), + ], ); app.schema_cache.table_columns.insert( "Customers".into(), - vec![TableColumn::new("id", Type::Serial), TableColumn::new("name", Type::Text)], + vec![ + TableColumn::new("id", Type::Serial), + TableColumn::new("name", Type::Text), + ], ); for t in app.schema_cache.tables.clone() { for c in &app.schema_cache.table_columns[&t] { @@ -5490,10 +5480,7 @@ mod tests { detail: "SCAN Customers".to_string(), }], }; - app.update(AppEvent::DslExplainSucceeded { - command: cmd, - plan, - }); + app.update(AppEvent::DslExplainSucceeded { command: cmd, plan }); // ADR-0040: no `[ok] explain …` header — the (no-echo here) // command's success shows via the marker; the plan output // itself carries the content. @@ -5549,7 +5536,11 @@ mod tests { .iter() .find(|l| l.kind == OutputKind::Echo) .expect("dispatch pushed an echo"); - assert_eq!(echo.status, Some(EchoStatus::Pending), "pending before result"); + assert_eq!( + echo.status, + Some(EchoStatus::Pending), + "pending before result" + ); app.update(AppEvent::DslSucceeded { command: Command::CreateTable { name: "T".to_string(), @@ -5639,8 +5630,14 @@ mod tests { .collect::>() .join("\n"); assert!(text.contains("[ok] replay"), "summary present:\n{text}"); - assert!(text.contains("import a.zip"), "import skip warning rendered:\n{text}"); - assert!(text.contains("nested `replay x`"), "nested-replay skip warning rendered:\n{text}"); + assert!( + text.contains("import a.zip"), + "import skip warning rendered:\n{text}" + ); + assert!( + text.contains("nested `replay x`"), + "nested-replay skip warning rendered:\n{text}" + ); } #[test] @@ -5736,7 +5733,7 @@ mod tests { #[test] fn hint_command_parses_to_app_hint() { - use crate::dsl::{parse_command, AppCommand, Command}; + use crate::dsl::{AppCommand, Command, parse_command}; assert!(matches!( parse_command("hint"), Ok(Command::App(AppCommand::Hint)) @@ -5747,7 +5744,7 @@ mod tests { #[test] fn version_command_parses_to_app_version() { - use crate::dsl::{parse_command, AppCommand, Command}; + use crate::dsl::{AppCommand, Command, parse_command}; assert!(matches!( parse_command("version"), Ok(Command::App(AppCommand::Version)) @@ -5801,7 +5798,10 @@ mod tests { let mut app = App::new(); type_str(&mut app, "show "); app.update(key(KeyCode::Tab)); - assert!(app.last_completion.is_some(), "precondition: Tab sets the memo"); + assert!( + app.last_completion.is_some(), + "precondition: Tab sets the memo" + ); let input = app.input.clone(); f1(&mut app); assert!(app.last_completion.is_some(), "F1 must not clear the memo"); @@ -5827,8 +5827,14 @@ mod tests { let input = app.input.clone(); let before = app.output.len(); app.update(ctrl_g()); - assert_eq!(app.input, input, "Ctrl-G must not change the buffer (no `g` typed)"); - assert!(app.output.len() > before, "Ctrl-G must emit the same hint F1 does"); + assert_eq!( + app.input, input, + "Ctrl-G must not change the buffer (no `g` typed)" + ); + assert!( + app.output.len() > before, + "Ctrl-G must emit the same hint F1 does" + ); } #[test] @@ -5855,7 +5861,11 @@ mod tests { let before = app.output.len(); app.update(ctrl_g()); assert_eq!(app.input, input, "Ctrl-G must not insert a `g`"); - assert_eq!(app.output.len(), before, "Ctrl-G does nothing when demo mode is off"); + assert_eq!( + app.output.len(), + before, + "Ctrl-G does nothing when demo mode is off" + ); } #[test] @@ -5949,7 +5959,10 @@ mod tests { #[test] fn f1_on_add_relationship_renders_the_relationship_block() { let mut app = App::new(); - type_str(&mut app, "add 1:n relationship from Customers.id to Orders.cust "); + type_str( + &mut app, + "add 1:n relationship from Customers.id to Orders.cust ", + ); f1(&mut app); assert!( output_contains(&app, "one parent, many children"), @@ -6142,14 +6155,8 @@ mod tests { let mut app = App::new(); let cmd = Command::Update { table: "Customers".to_string(), - assignments: vec![( - "id".to_string(), - crate::dsl::Value::Number("7".to_string()), - )], - filter: crate::dsl::RowFilter::eq( - "name", - crate::dsl::Value::Text("Bob".to_string()), - ), + assignments: vec![("id".to_string(), crate::dsl::Value::Number("7".to_string()))], + filter: crate::dsl::RowFilter::eq("name", crate::dsl::Value::Text("Bob".to_string())), }; let err = crate::db::DbError::Sqlite { message: "UNIQUE constraint failed: Customers.id".to_string(), @@ -6709,7 +6716,10 @@ mod tests { app.update(key(KeyCode::Backspace)); let actions = app.update(key(KeyCode::Enter)); assert_eq!(app.input, "select", "input untouched in navigation mode"); - assert!(actions.is_empty(), "Enter does not submit in navigation mode"); + assert!( + actions.is_empty(), + "Enter does not submit in navigation mode" + ); } #[test] @@ -6720,7 +6730,10 @@ mod tests { app.update(key(KeyCode::Down)); app.update(key(KeyCode::Down)); assert_eq!(app.tables_scroll, 2); - assert_eq!(app.relationships_scroll, 0, "only the focused panel scrolls"); + assert_eq!( + app.relationships_scroll, 0, + "only the focused panel scrolls" + ); app.update(key(KeyCode::Up)); assert_eq!(app.tables_scroll, 1); // Up saturates at the top. @@ -6998,7 +7011,8 @@ mod tests { for round in 0..3 { app.update(key(KeyCode::Up)); assert_eq!( - app.input, "insert into Thing values (1)", + app.input, + "insert into Thing values (1)", "Up #{} should recall the newest entry", round + 1, ); @@ -7220,8 +7234,7 @@ mod tests { has_default: false, }], ); - app.input = - "select * from products where price like 5".to_string(); + app.input = "select * from products where price like 5".to_string(); assert_eq!( app.input_validity_verdict(), Some(crate::dsl::walker::Severity::Warning), @@ -7350,8 +7363,10 @@ mod tests { "directly-deleted count surfaced: {texts:?}", ); assert!( - texts.iter().any(|t| t.contains("2 row(s) deleted in `Orders`") - && t.contains("relationship `places`")), + texts + .iter() + .any(|t| t.contains("2 row(s) deleted in `Orders`") + && t.contains("relationship `places`")), "per-relationship cascade summary surfaced: {texts:?}", ); } @@ -7386,11 +7401,15 @@ mod tests { }); let texts: Vec = app.output.iter().map(|l| l.text.clone()).collect(); assert!( - texts.iter().any(|t| t.contains("20 row(s) seeded into users")), + texts + .iter() + .any(|t| t.contains("20 row(s) seeded into users")), "seeded-row count surfaced: {texts:?}", ); assert!( - texts.iter().any(|t| t.contains("status") && t.contains("generic text")), + texts + .iter() + .any(|t| t.contains("status") && t.contains("generic text")), "the advisory names the enum-ish column: {texts:?}", ); } @@ -7424,8 +7443,9 @@ mod tests { }); let texts: Vec = app.output.iter().map(|l| l.text.clone()).collect(); assert!( - texts.iter().any(|t| t.contains("4 row(s) seeded into J") - && t.contains("of 10 requested")), + texts + .iter() + .any(|t| t.contains("4 row(s) seeded into J") && t.contains("of 10 requested")), "the cap note surfaces requested vs produced: {texts:?}", ); } @@ -7464,7 +7484,9 @@ mod tests { }); let texts: Vec = app.output.iter().map(|l| l.text.clone()).collect(); assert!( - texts.iter().any(|t| t.contains("2 row(s) deleted in `Orders`")), + texts + .iter() + .any(|t| t.contains("2 row(s) deleted in `Orders`")), "cascade summary still surfaces alongside RETURNING: {texts:?}", ); assert!( diff --git a/src/archive.rs b/src/archive.rs index a128773..04cc787 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -30,9 +30,7 @@ use std::path::{Component, Path, PathBuf}; use tracing::{debug, info}; use zip::{CompressionMethod, ZipArchive, ZipWriter, write::SimpleFileOptions}; -use crate::project::{ - HISTORY_LOG, PLAYGROUND_DB, PROJECT_YAML, naming::today_local, -}; +use crate::project::{HISTORY_LOG, PLAYGROUND_DB, PROJECT_YAML, naming::today_local}; /// File names excluded from `export` zips. These are either /// derived (`playground.db`), per-process (`.lock`), @@ -118,20 +116,14 @@ impl std::fmt::Display for ArchiveError { limit = format_args!("{limit:02}"), )) } - Self::InvalidZip(detail) => f.write_str(&crate::t!( - "archive.invalid_zip", - detail = detail, - )), - Self::NotAProjectArchive => { - f.write_str(&crate::t!("archive.not_a_project_archive")) + Self::InvalidZip(detail) => { + f.write_str(&crate::t!("archive.invalid_zip", detail = detail,)) } - Self::MultipleTopFolders => { - f.write_str(&crate::t!("archive.multiple_top_folders")) + Self::NotAProjectArchive => f.write_str(&crate::t!("archive.not_a_project_archive")), + Self::MultipleTopFolders => f.write_str(&crate::t!("archive.multiple_top_folders")), + Self::UnsafeEntry(entry) => { + f.write_str(&crate::t!("archive.unsafe_entry", entry = entry,)) } - Self::UnsafeEntry(entry) => f.write_str(&crate::t!( - "archive.unsafe_entry", - entry = entry, - )), } } } @@ -216,13 +208,7 @@ pub fn export_project( .unix_permissions(0o644); add_directory_entry(&mut writer, project_name, dst_zip)?; - add_directory_recursive( - &mut writer, - project_path, - project_name, - &options, - dst_zip, - )?; + add_directory_recursive(&mut writer, project_path, project_name, &options, dst_zip)?; writer.finish().map_err(|e| ArchiveError::Zip { path: dst_zip.to_path_buf(), @@ -392,10 +378,7 @@ pub struct ZipInspection { /// /// Returns the resolved target path and the suffix that was /// applied (0 if the original name was free, 2..=99 otherwise). -pub fn resolve_import_target( - parent: &Path, - name: &str, -) -> Result<(PathBuf, u32), ArchiveError> { +pub fn resolve_import_target(parent: &Path, name: &str) -> Result<(PathBuf, u32), ArchiveError> { let direct = parent.join(name); if !direct.exists() { return Ok((direct, 0)); @@ -495,10 +478,12 @@ pub fn extract_into( source, })?; let mut buf = Vec::with_capacity(entry.size() as usize); - entry.read_to_end(&mut buf).map_err(|source| ArchiveError::Io { - path: dst_path.clone(), - source, - })?; + entry + .read_to_end(&mut buf) + .map_err(|source| ArchiveError::Io { + path: dst_path.clone(), + source, + })?; out.write_all(&buf).map_err(|source| ArchiveError::Io { path: dst_path.clone(), source, @@ -523,7 +508,11 @@ mod tests { fs::write(p.join(PROJECT_YAML), "version: 1\nproject:\n created_at: 2026-01-01T00:00:00Z\ntables: []\nrelationships: []\n").unwrap(); fs::create_dir_all(p.join("data")).unwrap(); fs::write(p.join("data/Customers.csv"), "Name\nAlice\nBob\n").unwrap(); - fs::write(p.join(HISTORY_LOG), "T|ok|create table Customers with pk id(serial)\n").unwrap(); + fs::write( + p.join(HISTORY_LOG), + "T|ok|create table Customers with pk id(serial)\n", + ) + .unwrap(); fs::write(p.join(PLAYGROUND_DB), [0u8; 32]).unwrap(); fs::write(p.join(GITIGNORE), "/playground.db\n").unwrap(); // Stray atomic-write staging file — must be excluded. @@ -536,7 +525,9 @@ mod tests { ) .unwrap(); fs::write( - p.join(crate::undo::SNAPSHOTS_DIR).join("0").join(PLAYGROUND_DB), + p.join(crate::undo::SNAPSHOTS_DIR) + .join("0") + .join(PLAYGROUND_DB), [0u8; 16], ) .unwrap(); @@ -618,11 +609,15 @@ mod tests { let zip_path = tmp.path().join("notaproject.zip"); let f = fs::File::create(&zip_path).unwrap(); let mut w = ZipWriter::new(f); - w.start_file("foo/bar.txt", SimpleFileOptions::default()).unwrap(); + w.start_file("foo/bar.txt", SimpleFileOptions::default()) + .unwrap(); w.write_all(b"hi").unwrap(); w.finish().unwrap(); let err = inspect_zip(&zip_path).expect_err("must refuse"); - assert!(matches!(err, ArchiveError::NotAProjectArchive), "got: {err:?}"); + assert!( + matches!(err, ArchiveError::NotAProjectArchive), + "got: {err:?}" + ); } #[test] @@ -631,13 +626,18 @@ mod tests { let zip_path = tmp.path().join("multi.zip"); let f = fs::File::create(&zip_path).unwrap(); let mut w = ZipWriter::new(f); - w.start_file("a/project.yaml", SimpleFileOptions::default()).unwrap(); + w.start_file("a/project.yaml", SimpleFileOptions::default()) + .unwrap(); w.write_all(b"x").unwrap(); - w.start_file("b/project.yaml", SimpleFileOptions::default()).unwrap(); + w.start_file("b/project.yaml", SimpleFileOptions::default()) + .unwrap(); w.write_all(b"x").unwrap(); w.finish().unwrap(); let err = inspect_zip(&zip_path).expect_err("must refuse"); - assert!(matches!(err, ArchiveError::MultipleTopFolders), "got: {err:?}"); + assert!( + matches!(err, ArchiveError::MultipleTopFolders), + "got: {err:?}" + ); } #[test] diff --git a/src/cli.rs b/src/cli.rs index 9796431..b3155a4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -96,10 +96,7 @@ pub enum ArgsError { impl std::fmt::Display for ArgsError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::MissingValue(flag) => f.write_str(&crate::t!( - "cli.missing_value", - flag = flag, - )), + Self::MissingValue(flag) => f.write_str(&crate::t!("cli.missing_value", flag = flag,)), Self::InvalidValue { flag, value, @@ -110,10 +107,7 @@ impl std::fmt::Display for ArgsError { value = value, expected = expected, )), - Self::Unknown(arg) => f.write_str(&crate::t!( - "cli.unknown_argument", - arg = arg, - )), + Self::Unknown(arg) => f.write_str(&crate::t!("cli.unknown_argument", arg = arg,)), Self::MultiplePaths { first, second } => f.write_str(&crate::t!( "cli.multiple_paths", first = first, @@ -261,7 +255,11 @@ fn default_theme() -> Theme { // Standard convention: 0..=6 and 8 are dark backgrounds, // 7 and 9..=15 are light. ITerm emits 15 for white-ish. let is_dark = matches!(code, 0..=6 | 8); - return if is_dark { Theme::dark() } else { Theme::light() }; + return if is_dark { + Theme::dark() + } else { + Theme::light() + }; } Theme::default() } @@ -314,10 +312,19 @@ mod tests { #[test] fn mode_flag_simple_and_advanced() { - assert_eq!(Args::parse(["--mode", "simple"]).unwrap().mode, Some(Mode::Simple)); - assert_eq!(Args::parse(["--mode", "advanced"]).unwrap().mode, Some(Mode::Advanced)); + assert_eq!( + Args::parse(["--mode", "simple"]).unwrap().mode, + Some(Mode::Simple) + ); + assert_eq!( + Args::parse(["--mode", "advanced"]).unwrap().mode, + Some(Mode::Advanced) + ); // Case-insensitive, like the `mode` command. - assert_eq!(Args::parse(["--mode", "ADVANCED"]).unwrap().mode, Some(Mode::Advanced)); + assert_eq!( + Args::parse(["--mode", "ADVANCED"]).unwrap().mode, + Some(Mode::Advanced) + ); } #[test] @@ -350,7 +357,10 @@ mod tests { #[test] fn data_dir_flag_parses() { let args = Args::parse(["--data-dir", "/tmp/playground-data"]).unwrap(); - assert_eq!(args.data_dir.as_deref(), Some(std::path::Path::new("/tmp/playground-data"))); + assert_eq!( + args.data_dir.as_deref(), + Some(std::path::Path::new("/tmp/playground-data")) + ); } #[test] @@ -370,13 +380,11 @@ mod tests { #[test] fn data_dir_and_positional_can_coexist() { - let args = Args::parse([ - "--data-dir", - "/tmp/data", - "/home/me/MyProject", - ]) - .unwrap(); - assert_eq!(args.data_dir.as_deref(), Some(std::path::Path::new("/tmp/data"))); + let args = Args::parse(["--data-dir", "/tmp/data", "/home/me/MyProject"]).unwrap(); + assert_eq!( + args.data_dir.as_deref(), + Some(std::path::Path::new("/tmp/data")) + ); assert_eq!( args.project_path.as_deref(), Some(std::path::Path::new("/home/me/MyProject")) @@ -386,7 +394,10 @@ mod tests { #[test] fn two_positional_paths_error() { let err = Args::parse(["/a", "/b"]).unwrap_err(); - assert!(matches!(err, ArgsError::MultiplePaths { .. }), "got: {err:?}"); + assert!( + matches!(err, ArgsError::MultiplePaths { .. }), + "got: {err:?}" + ); } #[test] @@ -455,7 +466,10 @@ mod tests { // Absent `--demo` (and absent env var in the test runner), // demo mode is off — zero footprint for real users. let args = Args::parse(std::iter::empty::<&str>()).unwrap(); - assert!(!args.demo, "demo is off unless --demo or the env var is given"); + assert!( + !args.demo, + "demo is off unless --demo or the env var is given" + ); } #[test] @@ -484,7 +498,10 @@ mod tests { } // Disabling values. for v in ["", " ", "0", "false", "False", "no", "off", "OFF"] { - assert!(!demo_value_is_truthy(v), "{v:?} should not enable demo mode"); + assert!( + !demo_value_is_truthy(v), + "{v:?} should not enable demo mode" + ); } } @@ -493,7 +510,10 @@ mod tests { // Make sure the path-vs-flag distinction is robust: // unknown flags don't get silently swallowed as paths. let err = Args::parse(["--bogus", "/some/path"]).unwrap_err(); - assert!(matches!(&err, ArgsError::Unknown(s) if s == "--bogus"), "got: {err:?}"); + assert!( + matches!(&err, ArgsError::Unknown(s) if s == "--bogus"), + "got: {err:?}" + ); } // ---- ADR-0054: --version / -V ---- diff --git a/src/completion.rs b/src/completion.rs index 10b319a..ad8eb42 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -15,10 +15,10 @@ //! `app.rs`; this module owns the candidate computation. use crate::dsl::grammar::IdentSource; +use crate::dsl::parser::parse_command_with_schema_in_mode; use crate::dsl::types::Type; use crate::dsl::walker::outcome::Expectation; use crate::dsl::{ParseError, parse_command}; -use crate::dsl::parser::parse_command_with_schema_in_mode; use crate::mode::Mode; /// Composite literal candidates whose lexed shape is more than @@ -275,11 +275,7 @@ pub struct Completion { /// (case-insensitive starts-with), combined, sorted, and /// deduplicated. #[must_use] -pub fn candidates_at_cursor( - input: &str, - cursor: usize, - cache: &SchemaCache, -) -> Option { +pub fn candidates_at_cursor(input: &str, cursor: usize, cache: &SchemaCache) -> Option { candidates_at_cursor_in_mode(input, cursor, cache, Mode::Advanced) } @@ -358,7 +354,11 @@ pub fn candidates_at_cursor_with_in_mode( let word_boundary = run == 0 || bytes[run - 1].is_ascii_whitespace(); if run < cursor && bytes[run] == b'-' && word_boundary && run < start { let pre = crate::dsl::walker::completion_probe_in_mode(&input[..run], cache, mode); - if pre.expected.iter().any(|e| matches!(e, Expectation::Flag(_))) { + if pre + .expected + .iter() + .any(|e| matches!(e, Expectation::Flag(_))) + { start = run; } } @@ -473,22 +473,19 @@ pub fn candidates_at_cursor_with_in_mode( // walk's `current_table_columns`; fall back to "the union of // the look-ahead from_scope's bindings' columns" when leading // produced no in-scope columns. Phase-1 DSL paths unaffected. - let lookahead_union_columns: Vec = - if probe.current_table_columns.is_none() { - let mut out: Vec = Vec::new(); - for binding in resolution_from_scope { - for col in &binding.columns { - if !out.iter().any(|c| { - c.name.eq_ignore_ascii_case(&col.name) - }) { - out.push(col.clone()); - } + let lookahead_union_columns: Vec = if probe.current_table_columns.is_none() { + let mut out: Vec = Vec::new(); + for binding in resolution_from_scope { + for col in &binding.columns { + if !out.iter().any(|c| c.name.eq_ignore_ascii_case(&col.name)) { + out.push(col.clone()); } } - out - } else { - Vec::new() - }; + } + out + } else { + Vec::new() + }; let lookahead_slice: Option<&[TableColumn]> = if lookahead_union_columns.is_empty() { None } else { @@ -507,30 +504,23 @@ pub fn candidates_at_cursor_with_in_mode( // column list (the structural error path surfaces the // unresolved-prefix message). let prefix_qualifier = peek_back_qualifier(input, start); - let qualified_columns: Option> = prefix_qualifier - .as_ref() - .map(|q| { - // ADR-0033 §9: `excluded.|` inside an `INSERT … ON - // CONFLICT … DO UPDATE` completes to the target table's - // columns — `excluded` mirrors the would-be-inserted row. - // The target's columns are the INSERT's - // `current_table_columns` (set by the target-table slot). - // The diagnostic pass enforces the strict DO-UPDATE - // byte-range; completion is the softer surface and offers - // the columns whenever the INSERT target is in hand. - if q.eq_ignore_ascii_case("excluded") - && let Some(cols) = current_table_columns - { - cols.iter().map(|c| c.name.clone()).collect() - } else { - resolve_qualifier_columns_in( - q, - resolution_from_scope, - resolution_cte_bindings, - cache, - ) - } - }); + let qualified_columns: Option> = prefix_qualifier.as_ref().map(|q| { + // ADR-0033 §9: `excluded.|` inside an `INSERT … ON + // CONFLICT … DO UPDATE` completes to the target table's + // columns — `excluded` mirrors the would-be-inserted row. + // The target's columns are the INSERT's + // `current_table_columns` (set by the target-table slot). + // The diagnostic pass enforces the strict DO-UPDATE + // byte-range; completion is the softer surface and offers + // the columns whenever the INSERT target is in hand. + if q.eq_ignore_ascii_case("excluded") + && let Some(cols) = current_table_columns + { + cols.iter().map(|c| c.name.clone()).collect() + } else { + resolve_qualifier_columns_in(q, resolution_from_scope, resolution_cte_bindings, cache) + } + }); let expected = if probe.expected.is_empty() { expected_at(leading, mode) @@ -574,8 +564,7 @@ pub fn candidates_at_cursor_with_in_mode( Some(crate::dsl::grammar::HintMode::ProseOnly(_)) ); if partial_prefix.is_empty() - && (prose_only_slot - || (is_value_literal_signature(&expected) && !has_schema_ident)) + && (prose_only_slot || (is_value_literal_signature(&expected) && !has_schema_ident)) { return None; } @@ -646,7 +635,13 @@ pub fn candidates_at_cursor_with_in_mode( // shortid). The walker surfaces this as // `Expectation::Ident { source: Types }`. let type_names: Vec = if expected.iter().any(|e| { - matches!(e, Expectation::Ident { source: IdentSource::Types, .. }) + matches!( + e, + Expectation::Ident { + source: IdentSource::Types, + .. + } + ) }) { Type::all() .iter() @@ -725,7 +720,13 @@ pub fn candidates_at_cursor_with_in_mode( // filtered like every other source; empty prefix offers the whole // set. Tagged `CandidateKind::Function` for its own colour. let has_sql_expr_slot = expected.iter().any(|e| { - matches!(e, Expectation::Ident { role: "sql_expr_ident", .. }) + matches!( + e, + Expectation::Ident { + role: "sql_expr_ident", + .. + } + ) }); let mut functions: Vec = if has_sql_expr_slot { crate::dsl::sql_functions::KNOWN_SQL_FUNCTIONS @@ -741,9 +742,15 @@ pub fn candidates_at_cursor_with_in_mode( // curated vocabulary is offered so a learner can discover `email` / // `product` / … by Tab. Same `Function` kind / `tok_function` colour // as SQL functions (no new theme colour — ADR-0048 §Grammar). - let has_generator_slot = expected - .iter() - .any(|e| matches!(e, Expectation::Ident { source: IdentSource::Generators, .. })); + let has_generator_slot = expected.iter().any(|e| { + matches!( + e, + Expectation::Ident { + source: IdentSource::Generators, + .. + } + ) + }); if has_generator_slot { functions.extend( crate::seed::KNOWN_GENERATORS @@ -765,38 +772,36 @@ pub fn candidates_at_cursor_with_in_mode( // (the `typing_over_diag` path) — keeps the alias from flashing as // a bogus "unknown column" while typing. Mixed into `identifiers` // so it sorts/dedups/colours uniformly with column candidates. - let alias_candidates: Vec = - if has_sql_expr_slot && prefix_qualifier.is_none() { - // Once the partial *exactly* matches an in-scope qualifier, - // discoverability is served — the learner has a whole alias - // in hand and now needs the "add `.column`" hint - // (`diagnostic.alias_used_as_column`), not sibling aliases - // that merely share the prefix. Offering them would also let - // the `typing_over_diag` path suppress that very hint. So in - // the exact-match case we emit no alias candidates and let - // the targeted diagnostic surface. - let partial_is_exact_alias = resolution_from_scope.iter().any(|b| { - let q = b.alias.as_deref().unwrap_or(b.table.as_str()); - q.eq_ignore_ascii_case(&partial_prefix) - }); - if partial_is_exact_alias { - Vec::new() - } else { - let mut out: Vec = Vec::new(); - for binding in resolution_from_scope { - let qualifier = - binding.alias.as_deref().unwrap_or(binding.table.as_str()); - if matches_prefix(qualifier) - && !out.iter().any(|q| q.eq_ignore_ascii_case(qualifier)) - { - out.push(qualifier.to_string()); - } - } - out - } - } else { + let alias_candidates: Vec = if has_sql_expr_slot && prefix_qualifier.is_none() { + // Once the partial *exactly* matches an in-scope qualifier, + // discoverability is served — the learner has a whole alias + // in hand and now needs the "add `.column`" hint + // (`diagnostic.alias_used_as_column`), not sibling aliases + // that merely share the prefix. Offering them would also let + // the `typing_over_diag` path suppress that very hint. So in + // the exact-match case we emit no alias candidates and let + // the targeted diagnostic surface. + let partial_is_exact_alias = resolution_from_scope.iter().any(|b| { + let q = b.alias.as_deref().unwrap_or(b.table.as_str()); + q.eq_ignore_ascii_case(&partial_prefix) + }); + if partial_is_exact_alias { Vec::new() - }; + } else { + let mut out: Vec = Vec::new(); + for binding in resolution_from_scope { + let qualifier = binding.alias.as_deref().unwrap_or(binding.table.as_str()); + if matches_prefix(qualifier) + && !out.iter().any(|q| q.eq_ignore_ascii_case(qualifier)) + { + out.push(qualifier.to_string()); + } + } + out + } + } else { + Vec::new() + }; // Source 2: schema identifiers — accumulated across every // matching schema-listable `Ident { source }` expectation. @@ -811,9 +816,7 @@ pub fn candidates_at_cursor_with_in_mode( let mut identifiers: Vec = expected .iter() .filter_map(|e| match e { - Expectation::Ident { source, .. } if source.completes_from_schema() => { - Some(*source) - } + Expectation::Ident { source, .. } if source.completes_from_schema() => Some(*source), _ => None, }) .flat_map(|source| { @@ -1007,11 +1010,7 @@ fn resolve_qualifier_columns_in( .iter() .find(|c| c.name.eq_ignore_ascii_case(&binding.table)) { - return cte - .columns - .iter() - .filter_map(|c| c.name.clone()) - .collect(); + return cte.columns.iter().filter_map(|c| c.name.clone()).collect(); } } // Second: table-name match in the active from_scope. @@ -1026,11 +1025,7 @@ fn resolve_qualifier_columns_in( .iter() .find(|c| c.name.eq_ignore_ascii_case(&binding.table)) { - return cte - .columns - .iter() - .filter_map(|c| c.name.clone()) - .collect(); + return cte.columns.iter().filter_map(|c| c.name.clone()).collect(); } } // Third: direct cte_bindings match (cte_alias.|). @@ -1038,11 +1033,7 @@ fn resolve_qualifier_columns_in( .iter() .find(|c| c.name.eq_ignore_ascii_case(qualifier)) { - return cte - .columns - .iter() - .filter_map(|c| c.name.clone()) - .collect(); + return cte.columns.iter().filter_map(|c| c.name.clone()).collect(); } // Fourth: a bare table name from the schema cache — DSL // paths reach this for `from .` shapes where @@ -1287,7 +1278,13 @@ pub fn invalid_ident_at_cursor_in_mode( // column. So `select Agx` warns at typing time again, while // `select sum` does not. let has_sql_expr_slot = expected.iter().any(|e| { - matches!(e, Expectation::Ident { role: "sql_expr_ident", .. }) + matches!( + e, + Expectation::Ident { + role: "sql_expr_ident", + .. + } + ) }); if has_sql_expr_slot && crate::dsl::sql_functions::is_known_function_prefix(partial) { return None; @@ -1318,9 +1315,15 @@ pub fn invalid_ident_at_cursor_in_mode( // schema-column check below would never see it. A partial that // prefix-matches a known generator is an in-progress name; anything // else is an unknown generator → flag it `[ERR]` while typing. - let has_generator_slot = expected - .iter() - .any(|e| matches!(e, Expectation::Ident { source: IdentSource::Generators, .. })); + let has_generator_slot = expected.iter().any(|e| { + matches!( + e, + Expectation::Ident { + source: IdentSource::Generators, + .. + } + ) + }); if has_generator_slot { if crate::seed::is_known_generator_prefix(partial) { return None; @@ -1335,9 +1338,7 @@ pub fn invalid_ident_at_cursor_in_mode( let sources: Vec = expected .iter() .filter_map(|e| match e { - Expectation::Ident { source, .. } if source.completes_from_schema() => { - Some(*source) - } + Expectation::Ident { source, .. } if source.completes_from_schema() => Some(*source), _ => None, }) .collect(); @@ -1412,13 +1413,15 @@ mod tests { use pretty_assertions::assert_eq; fn cands(input: &str, cursor: usize) -> Vec { - candidates_at_cursor(input, cursor, &SchemaCache::default()) - .map_or_else(Vec::new, |c| c.candidates.into_iter().map(|c| c.text).collect()) + candidates_at_cursor(input, cursor, &SchemaCache::default()).map_or_else(Vec::new, |c| { + c.candidates.into_iter().map(|c| c.text).collect() + }) } fn cands_with(input: &str, cursor: usize, cache: &SchemaCache) -> Vec { - candidates_at_cursor(input, cursor, cache) - .map_or_else(Vec::new, |c| c.candidates.into_iter().map(|c| c.text).collect()) + candidates_at_cursor(input, cursor, cache).map_or_else(Vec::new, |c| { + c.candidates.into_iter().map(|c| c.text).collect() + }) } /// Simple-mode completion candidates — the DSL surface @@ -1429,7 +1432,9 @@ mod tests { /// Advanced mode surfaces the SQL grammar's completions instead. fn cands_simple(input: &str, cursor: usize) -> Vec { candidates_at_cursor_in_mode(input, cursor, &SchemaCache::default(), Mode::Simple) - .map_or_else(Vec::new, |c| c.candidates.into_iter().map(|c| c.text).collect()) + .map_or_else(Vec::new, |c| { + c.candidates.into_iter().map(|c| c.text).collect() + }) } fn cand_kinds_with( @@ -1438,10 +1443,7 @@ mod tests { cache: &SchemaCache, ) -> Vec<(String, CandidateKind)> { candidates_at_cursor(input, cursor, cache).map_or_else(Vec::new, |c| { - c.candidates - .into_iter() - .map(|c| (c.text, c.kind)) - .collect() + c.candidates.into_iter().map(|c| (c.text, c.kind)).collect() }) } @@ -1503,12 +1505,21 @@ mod tests { // Simple-only (column, relationship, constraint). let cs = cands("drop ", 5); for kw in ["table", "index", "column", "relationship", "constraint"] { - assert!(cs.contains(&kw.to_string()), "`drop ` should offer `{kw}`; got {cs:?}"); + assert!( + cs.contains(&kw.to_string()), + "`drop ` should offer `{kw}`; got {cs:?}" + ); } // Both-mode continuations block before the simple-only ones. let pos = |k: &str| cs.iter().position(|c| c == k).unwrap(); - assert!(pos("table") < pos("column"), "Both block precedes Simple block: {cs:?}"); - assert!(pos("index") < pos("relationship"), "Both block precedes Simple block: {cs:?}"); + assert!( + pos("table") < pos("column"), + "Both block precedes Simple block: {cs:?}" + ); + assert!( + pos("index") < pos("relationship"), + "Both block precedes Simple block: {cs:?}" + ); } #[test] @@ -1631,8 +1642,14 @@ mod tests { let c = candidates_at_cursor(input, input.len(), &SchemaCache::default()) .expect("a `-` at a flag position offers candidates"); let texts: Vec<&str> = c.candidates.iter().map(|x| x.text.as_str()).collect(); - assert!(texts.contains(&"--create-fk"), "should offer --create-fk: {texts:?}"); - assert!(!texts.contains(&"on"), "must NOT offer `on` after a dash: {texts:?}"); + assert!( + texts.contains(&"--create-fk"), + "should offer --create-fk: {texts:?}" + ); + assert!( + !texts.contains(&"on"), + "must NOT offer `on` after a dash: {texts:?}" + ); assert_eq!( c.replaced_range, (input.len() - 1, input.len()), @@ -1643,13 +1660,9 @@ mod tests { #[test] fn double_dash_replaces_both_dashes_on_accept() { let input = "delete from T --"; - let c = candidates_at_cursor_in_mode( - input, - input.len(), - &SchemaCache::default(), - Mode::Simple, - ) - .expect("`--` offers the flag"); + let c = + candidates_at_cursor_in_mode(input, input.len(), &SchemaCache::default(), Mode::Simple) + .expect("`--` offers the flag"); assert!(c.candidates.iter().any(|x| x.text == "--all-rows")); assert_eq!( c.replaced_range, @@ -1668,9 +1681,7 @@ mod tests { s.tables.push("T".into()); s.columns.push("x".into()); let input = "show data T where x = -5"; - if let Some(c) = - candidates_at_cursor_in_mode(input, input.len(), &s, Mode::Simple) - { + if let Some(c) = candidates_at_cursor_in_mode(input, input.len(), &s, Mode::Simple) { assert!( !c.candidates.iter().any(|x| x.text.starts_with("--")), "no flags at a value position: {:?}", @@ -1715,8 +1726,8 @@ mod tests { // App-lifecycle commands now appear alongside DSL // commands in the entry-keyword set. for expected in &[ - "quit", "help", "rebuild", "save", "new", "load", "export", - "import", "mode", "messages", "undo", "redo", "copy", + "quit", "help", "rebuild", "save", "new", "load", "export", "import", "mode", + "messages", "undo", "redo", "copy", ] { assert!( cs.contains(&expected.to_string()), @@ -1943,7 +1954,10 @@ mod tests { // opening a sub-shape) becomes a Tab candidate. let input = "add column to table T"; let cs = cands(input, input.len()); - assert!(cs.is_empty(), "trailing-content punct should not surface: {cs:?}"); + assert!( + cs.is_empty(), + "trailing-content punct should not surface: {cs:?}" + ); } #[test] @@ -1957,10 +1971,7 @@ mod tests { assert!(cs.contains(&"(".to_string()), "got {cs:?}"); } - fn schema_with_table( - table: &str, - columns: &[(&str, crate::dsl::types::Type)], - ) -> SchemaCache { + fn schema_with_table(table: &str, columns: &[(&str, crate::dsl::types::Type)]) -> SchemaCache { let mut cache = SchemaCache::default(); cache.tables.push(table.to_string()); let cols: Vec = columns @@ -2002,8 +2013,14 @@ mod tests { let cache = two_table_alias_cache(); let input = "select a.id from a o join b z on o.id = z.id group by "; let cs = cands_with(input, input.len(), &cache); - assert!(cs.contains(&"o".to_string()), "alias `o` must be offered; got {cs:?}"); - assert!(cs.contains(&"z".to_string()), "alias `z` must be offered; got {cs:?}"); + assert!( + cs.contains(&"o".to_string()), + "alias `o` must be offered; got {cs:?}" + ); + assert!( + cs.contains(&"z".to_string()), + "alias `z` must be offered; got {cs:?}" + ); } #[test] @@ -2015,8 +2032,14 @@ mod tests { let cache = two_table_alias_cache(); let input = "select a.id from a aa join b ab on aa.id = ab.id group by a"; let cs = cands_with(input, input.len(), &cache); - assert!(cs.contains(&"aa".to_string()), "alias `aa` must be offered; got {cs:?}"); - assert!(cs.contains(&"ab".to_string()), "alias `ab` must be offered; got {cs:?}"); + assert!( + cs.contains(&"aa".to_string()), + "alias `aa` must be offered; got {cs:?}" + ); + assert!( + cs.contains(&"ab".to_string()), + "alias `ab` must be offered; got {cs:?}" + ); // Exact-alias partial: the alias source steps aside. let exact = "select aa.id from a aa join b ab on aa.id = ab.id group by aa"; @@ -2046,19 +2069,20 @@ mod tests { // SchemaCache.columns has columns from many tables, but // at `update Customers set ` only Customers' columns // should appear. - let mut cache = schema_with_table( - "Customers", - &[("id", Type::Int), ("Email", Type::Text)], - ); + let mut cache = schema_with_table("Customers", &[("id", Type::Int), ("Email", Type::Text)]); // Pretend the global flat list has columns from a second // table that aren't in Customers. cache.columns.push("OrderTotal".to_string()); cache.columns.push("Stock".to_string()); - cache - .table_columns - .insert("Orders".to_string(), vec![ - TableColumn { name: "OrderTotal".to_string(), user_type: Type::Real, not_null: false, has_default: false }, - ]); + cache.table_columns.insert( + "Orders".to_string(), + vec![TableColumn { + name: "OrderTotal".to_string(), + user_type: Type::Real, + not_null: false, + has_default: false, + }], + ); cache.tables.push("Orders".to_string()); let cs = cands_with("update Customers set ", 21, &cache); // Customers's columns should appear: @@ -2079,10 +2103,7 @@ mod tests { // *before* ORDER BY (the FROM's JOIN options, WHERE / // GROUP BY / HAVING, set-ops). Those used to shove the // columns off-screen. - let cache = schema_with_table( - "Things", - &[("Name", Type::Text), ("Qty", Type::Int)], - ); + let cache = schema_with_table("Things", &[("Name", Type::Text), ("Qty", Type::Int)]); let input = "select Name from Things order by "; let cs = cands_with(input, input.len(), &cache); // The columns the user wants are offered: @@ -2090,8 +2111,19 @@ mod tests { assert!(cs.contains(&"Qty".to_string()), "got {cs:?}"); // Preceding-clause keywords must not leak in: for kw in [ - "where", "group", "having", "join", "union", "intersect", - "except", "left", "right", "full", "cross", "inner", "as", + "where", + "group", + "having", + "join", + "union", + "intersect", + "except", + "left", + "right", + "full", + "cross", + "inner", + "as", ] { assert!( !cs.contains(&kw.to_string()), @@ -2108,10 +2140,7 @@ mod tests { // sort item the direction keywords surface as // continuations (previously discarded at the Repeated // boundary, so completion offered neither). - let cache = schema_with_table( - "Things", - &[("Name", Type::Text), ("Qty", Type::Int)], - ); + let cache = schema_with_table("Things", &[("Name", Type::Text), ("Qty", Type::Int)]); let input = "select Name from Things order by Name "; let cs = cands_with(input, input.len(), &cache); assert!(cs.contains(&"asc".to_string()), "got {cs:?}"); @@ -2123,10 +2152,7 @@ mod tests { use crate::dsl::types::Type; // walk_repeated trailing-optional fix: after a complete // projection item the `as` alias keyword surfaces. - let cache = schema_with_table( - "Things", - &[("Name", Type::Text), ("Qty", Type::Int)], - ); + let cache = schema_with_table("Things", &[("Name", Type::Text), ("Qty", Type::Int)]); let input = "select Name "; let cs = cands_with(input, input.len(), &cache); assert!(cs.contains(&"as".to_string()), "got {cs:?}"); @@ -2153,16 +2179,13 @@ mod tests { // ADR-0022 Amendment 2: at an expression position offering // both column names and keywords, every column precedes // every keyword so the names stay visible by default. - let cache = schema_with_table( - "Things", - &[("Name", Type::Text), ("Qty", Type::Int)], - ); + let cache = schema_with_table("Things", &[("Name", Type::Text), ("Qty", Type::Int)]); let input = "select * from Things where "; let cs = cands_with(input, input.len(), &cache); let pos = |needle: &str| { - cs.iter().position(|c| c == needle).unwrap_or_else(|| { - panic!("{needle:?} not in candidates: {cs:?}") - }) + cs.iter() + .position(|c| c == needle) + .unwrap_or_else(|| panic!("{needle:?} not in candidates: {cs:?}")) }; // Both columns come before any expression-start keyword. let last_ident = pos("Name").max(pos("Qty")); @@ -2176,13 +2199,9 @@ mod tests { #[test] fn update_where_offers_only_current_table_columns() { use crate::dsl::types::Type; - let mut cache = schema_with_table( - "Customers", - &[("id", Type::Int), ("Email", Type::Text)], - ); + let mut cache = schema_with_table("Customers", &[("id", Type::Int), ("Email", Type::Text)]); cache.columns.push("OrderTotal".to_string()); - let cs = - cands_with("update Customers set Email='x' where ", 37, &cache); + let cs = cands_with("update Customers set Email='x' where ", 37, &cache); assert!(cs.contains(&"id".to_string()), "got {cs:?}"); assert!(cs.contains(&"Email".to_string()), "got {cs:?}"); assert!(!cs.contains(&"OrderTotal".to_string()), "got {cs:?}"); @@ -2208,7 +2227,11 @@ mod tests { use crate::dsl::types::Type; let cache = schema_with_table( "Customers", - &[("id", Type::Int), ("Email", Type::Text), ("Name", Type::Text)], + &[ + ("id", Type::Int), + ("Email", Type::Text), + ("Name", Type::Text), + ], ); let cs = cands_with("insert into Customers (", 23, &cache); // The user is at Form A's column-list position. All @@ -2222,10 +2245,7 @@ mod tests { #[test] fn insert_into_open_paren_does_not_offer_unrelated_columns() { use crate::dsl::types::Type; - let mut cache = schema_with_table( - "Customers", - &[("id", Type::Int), ("Email", Type::Text)], - ); + let mut cache = schema_with_table("Customers", &[("id", Type::Int), ("Email", Type::Text)]); cache.columns.push("OrderTotal".to_string()); let cs = cands_with("insert into Customers (", 23, &cache); assert!(!cs.contains(&"OrderTotal".to_string()), "got {cs:?}"); @@ -2239,13 +2259,9 @@ mod tests { // table's columns. `OrderTotal` belongs to no table in // this cache's `table_columns`, so it must not leak. use crate::dsl::types::Type; - let mut cache = schema_with_table( - "Customers", - &[("id", Type::Int), ("Email", Type::Text)], - ); + let mut cache = schema_with_table("Customers", &[("id", Type::Int), ("Email", Type::Text)]); cache.columns.push("OrderTotal".to_string()); - let cs = - cands_with("drop column from Customers: ", 28, &cache); + let cs = cands_with("drop column from Customers: ", 28, &cache); assert!(cs.contains(&"Email".to_string()), "got {cs:?}"); assert!(cs.contains(&"id".to_string()), "got {cs:?}"); assert!( @@ -2271,8 +2287,8 @@ mod tests { #[test] fn cursor_mid_keyword_replaces_only_the_partial_prefix() { - let comp = candidates_at_cursor("cre", 3, &SchemaCache::default()) - .expect("some completion"); + let comp = + candidates_at_cursor("cre", 3, &SchemaCache::default()).expect("some completion"); assert_eq!(comp.replaced_range, (0, 3)); assert_eq!(comp.partial_prefix, "cre"); assert_eq!(comp.candidates.len(), 1); @@ -2282,8 +2298,8 @@ mod tests { #[test] fn cursor_at_word_boundary_has_empty_partial_prefix() { - let comp = candidates_at_cursor("create ", 7, &SchemaCache::default()) - .expect("some completion"); + let comp = + candidates_at_cursor("create ", 7, &SchemaCache::default()).expect("some completion"); assert_eq!(comp.replaced_range, (7, 7)); assert_eq!(comp.partial_prefix, ""); } @@ -2517,8 +2533,8 @@ mod tests { // inside `Name`, and substituting any name there // produces a complete command. No useful "next after // name" hint. - let t = typing_name_at_cursor("add column to table T: Name (text)", 27) - .expect("should fire"); + let t = + typing_name_at_cursor("add column to table T: Name (text)", 27).expect("should fire"); assert_eq!(t.next_after_name, None); } @@ -2534,8 +2550,8 @@ mod tests { assert!(invalid_ident_at_cursor("show data Cust", 14, &cache).is_none()); // `show data Cust` plus a typo: `show data Custp`. No // table starts with "Custp" → invalid. - let invalid = invalid_ident_at_cursor("show data Custp", 15, &cache) - .expect("should be invalid"); + let invalid = + invalid_ident_at_cursor("show data Custp", 15, &cache).expect("should be invalid"); assert_eq!(invalid.range, (10, 15)); assert_eq!(invalid.found, "Custp"); assert_eq!(invalid.source, IdentSource::Tables); @@ -2600,7 +2616,11 @@ mod tests { !cs.iter().any(|c| c == "Existing" || c == "AlsoExisting"), "NewName slot must not surface schema candidates; got {cs:?}" ); - assert_eq!(cs, vec!["if".to_string()], "only the advanced IF NOT EXISTS keyword"); + assert_eq!( + cs, + vec!["if".to_string()], + "only the advanced IF NOT EXISTS keyword" + ); } fn keyword_cand(text: &str) -> Candidate { @@ -2791,8 +2811,10 @@ mod tests { let cands = candidates_at_cursor(input, input.len(), &cache) .expect("some completion") .candidates; - let count_entries: Vec<_> = - cands.iter().filter(|c| c.text.eq_ignore_ascii_case("count")).collect(); + let count_entries: Vec<_> = cands + .iter() + .filter(|c| c.text.eq_ignore_ascii_case("count")) + .collect(); assert_eq!( count_entries.len(), 1, @@ -2805,7 +2827,9 @@ mod tests { ); // A non-colliding function at the same slot is unaffected. assert!( - cands.iter().any(|c| c.text == "coalesce" && c.kind == CandidateKind::Function), + cands + .iter() + .any(|c| c.text == "coalesce" && c.kind == CandidateKind::Function), "non-colliding functions still surface; got {cands:?}", ); } @@ -2875,8 +2899,10 @@ mod tests { let mut s = SchemaCache::default(); s.tables.push("OrderLines".into()); s.columns.push("count".into()); - s.table_columns - .insert("OrderLines".into(), vec![TableColumn::new("count", Type::Int)]); + s.table_columns.insert( + "OrderLines".into(), + vec![TableColumn::new("count", Type::Int)], + ); let input = "select sum(ol.count) from OrderLines ol"; let cursor = input.find("ol.count").unwrap() + 2; // right after `ol` assert!( @@ -2938,15 +2964,35 @@ mod tests { s.table_columns.insert( "a".to_string(), vec![ - TableColumn { name: "id".to_string(), user_type: Type::Int, not_null: false, has_default: false }, - TableColumn { name: "name".to_string(), user_type: Type::Text, not_null: false, has_default: false }, + TableColumn { + name: "id".to_string(), + user_type: Type::Int, + not_null: false, + has_default: false, + }, + TableColumn { + name: "name".to_string(), + user_type: Type::Text, + not_null: false, + has_default: false, + }, ], ); s.table_columns.insert( "b".to_string(), vec![ - TableColumn { name: "id".to_string(), user_type: Type::Int, not_null: false, has_default: false }, - TableColumn { name: "total".to_string(), user_type: Type::Real, not_null: false, has_default: false }, + TableColumn { + name: "id".to_string(), + user_type: Type::Int, + not_null: false, + has_default: false, + }, + TableColumn { + name: "total".to_string(), + user_type: Type::Real, + not_null: false, + has_default: false, + }, ], ); s @@ -3191,5 +3237,3 @@ mod tests { assert!(candidates_at_cursor_with("create ", 7, &cache, empty_ranker).is_none()); } } - - diff --git a/src/db.rs b/src/db.rs index 80ebb64..a230b55 100644 --- a/src/db.rs +++ b/src/db.rs @@ -30,24 +30,23 @@ use rusqlite::Connection; use tokio::sync::{mpsc, oneshot}; use tracing::{debug, info, warn}; +use crate::dsl::ColumnSpec; use crate::dsl::action::ReferentialAction; use crate::dsl::command::{ - ChangeColumnMode, Command, CompareOp, Constraint, ConstraintKind, Expr, IndexSelector, - Operand, Predicate, RelationshipSelector, RowFilter, SeedOverride, SeedOverrideKind, - SqlForeignKey, + ChangeColumnMode, Command, CompareOp, Constraint, ConstraintKind, Expr, IndexSelector, Operand, + Predicate, RelationshipSelector, RowFilter, SeedOverride, SeedOverrideKind, SqlForeignKey, }; -use crate::dsl::ColumnSpec; use crate::dsl::shortid; use crate::dsl::types::Type; use crate::dsl::value::{Bound, Value, ValueError}; use crate::mode::Mode; use crate::output_render::{Alignment, render_diagnostic_table}; -use crate::type_change; use crate::persistence::{ CellValue, ColumnSchema, IndexSchema, Persistence, PersistenceError, RelationshipSchema, SchemaSnapshot, TableCheck, TableSchema, TableSnapshot, decode_cell, parse_csv, parse_schema, }; use crate::project::{DATA_DIR, PROJECT_YAML}; +use crate::type_change; use crate::undo::{DEFAULT_RING_CAPACITY, SnapshotError, SnapshotMeta, SnapshotStore, Staged}; /// Inbox capacity. The worker is fast enough that this rarely @@ -120,8 +119,7 @@ pub struct IndexInfo { /// referenced) sides; the field meanings flip per side. /// `(outbound, inbound)` pair returned by /// [`Database::read_relationships`]. -pub type RelationshipsReply = - Result<(Vec, Vec), DbError>; +pub type RelationshipsReply = Result<(Vec, Vec), DbError>; #[derive(Debug, Clone, PartialEq, Eq)] pub struct RelationshipEnd { @@ -193,18 +191,15 @@ pub enum DbError { impl std::fmt::Display for DbError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Sqlite { message, .. } => f.write_str(&crate::t!( - "db.error.sqlite", - message = message, - )), - Self::Unsupported(detail) => f.write_str(&crate::t!( - "db.error.unsupported", - detail = detail, - )), - Self::InvalidValue(detail) => f.write_str(&crate::t!( - "db.error.invalid_value", - detail = detail, - )), + Self::Sqlite { message, .. } => { + f.write_str(&crate::t!("db.error.sqlite", message = message,)) + } + Self::Unsupported(detail) => { + f.write_str(&crate::t!("db.error.unsupported", detail = detail,)) + } + Self::InvalidValue(detail) => { + f.write_str(&crate::t!("db.error.invalid_value", detail = detail,)) + } Self::PersistenceFatal { operation, path, @@ -228,10 +223,7 @@ impl std::fmt::Display for DbError { detail = detail, )), Self::WorkerGone => f.write_str(&crate::t!("db.error.worker_gone")), - Self::Io(detail) => f.write_str(&crate::t!( - "db.error.io", - detail = detail, - )), + Self::Io(detail) => f.write_str(&crate::t!("db.error.io", detail = detail,)), } } } @@ -465,7 +457,10 @@ impl DbError { /// surfaces these as fatal banners. #[must_use] pub const fn is_fatal(&self) -> bool { - matches!(self, Self::PersistenceFatal { .. } | Self::RebuildRowFailed { .. }) + matches!( + self, + Self::PersistenceFatal { .. } | Self::RebuildRowFailed { .. } + ) } } @@ -902,9 +897,7 @@ enum Request { }, /// Close a batch: finalise the boundary snapshot into the ring if /// any mutation committed during the batch, else discard it. - EndBatch { - reply: oneshot::Sender<()>, - }, + EndBatch { reply: oneshot::Sender<()> }, /// Record the current input mode and persist it to /// `project.yaml` (ADR-0015 mode-restore amendment, issue /// #14). Sent at boot / project-switch to seed the mode and @@ -1043,7 +1036,12 @@ impl Database { pub async fn drop_table(&self, name: String, source: Option) -> Result<(), DbError> { let (reply, recv) = oneshot::channel(); - self.send(Request::DropTable { name, source, reply }).await?; + self.send(Request::DropTable { + name, + source, + reply, + }) + .await?; recv.await.map_err(|_| DbError::WorkerGone)? } @@ -1758,10 +1756,7 @@ impl Database { /// `EXPLAIN QUERY PLAN` only inspects how the engine would /// locate the rows — so this is safe even for `update` / /// `delete`. - pub async fn explain_query_plan( - &self, - query: Command, - ) -> Result { + pub async fn explain_query_plan(&self, query: Command) -> Result { let (reply, recv) = oneshot::channel(); self.send(Request::ExplainPlan { query, reply }).await?; recv.await.map_err(|_| DbError::WorkerGone)? @@ -1770,12 +1765,10 @@ impl Database { /// Read both directions of FK relationships for `table`. /// Used by the runtime's friendly-error enrichment to /// resolve parent / child table names (ADR-0019 §6). - pub async fn read_relationships( - &self, - table: String, - ) -> RelationshipsReply { + pub async fn read_relationships(&self, table: String) -> RelationshipsReply { let (reply, recv) = oneshot::channel(); - self.send(Request::ReadRelationships { table, reply }).await?; + self.send(Request::ReadRelationships { table, reply }) + .await?; recv.await.map_err(|_| DbError::WorkerGone)? } @@ -2215,16 +2208,18 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_create_table( - conn, - persistence, - &name, - &columns, - &primary_key, - &[], - &[], - &[], - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_create_table( + conn, + persistence, + &name, + &columns, + &primary_key, + &[], + &[], + &[], + ) + }); } Request::SqlCreateTable { name, @@ -2290,8 +2285,7 @@ fn handle_request( let _ = reply.send(result); } else { snapshot_then(snap, batch, conn, source.as_deref(), reply, || { - do_drop_table(conn, persistence, &name) - .map(|()| DropOutcome::Dropped) + do_drop_table(conn, persistence, &name).map(|()| DropOutcome::Dropped) }); } } @@ -2301,12 +2295,9 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_add_column( - conn, - persistence, - &table, - &column, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_add_column(conn, persistence, &table, &column) + }); } Request::DropColumn { table, @@ -2315,13 +2306,9 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_drop_column( - conn, - persistence, - &table, - &column, - cascade, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_drop_column(conn, persistence, &table, &column, cascade) + }); } Request::RenameColumn { table, @@ -2330,13 +2317,9 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_rename_column( - conn, - persistence, - &table, - &old, - &new, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_rename_column(conn, persistence, &table, &old, &new) + }); } Request::RenameTable { table, @@ -2344,12 +2327,9 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_rename_table( - conn, - persistence, - &table, - &new, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_rename_table(conn, persistence, &table, &new) + }); } Request::ChangeColumnType { table, @@ -2359,14 +2339,9 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_change_column_type( - conn, - persistence, - &table, - &column, - ty, - mode, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_change_column_type(conn, persistence, &table, &column, ty, mode) + }); } Request::ListTables { reply } => { let _ = reply.send(do_list_tables(conn)); @@ -2392,18 +2367,20 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_add_relationship( - conn, - persistence, - name.as_deref(), - &parent_table, - &parent_columns, - &child_table, - &child_columns, - on_delete, - on_update, - create_fk, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_add_relationship( + conn, + persistence, + name.as_deref(), + &parent_table, + &parent_columns, + &child_table, + &child_columns, + on_delete, + on_update, + create_fk, + ) + }); } Request::CreateM2nRelationship { t1, @@ -2413,13 +2390,7 @@ fn handle_request( reply, } => { snapshot_then(snap, batch, conn, source.as_deref(), reply, || { - do_create_m2n_relationship( - conn, - persistence, - &t1, - &t2, - name.as_deref(), - ) + do_create_m2n_relationship(conn, persistence, &t1, &t2, name.as_deref()) }); } Request::DropRelationship { @@ -2427,11 +2398,9 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_drop_relationship( - conn, - persistence, - &selector, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_drop_relationship(conn, persistence, &selector) + }); } Request::AddIndex { name, @@ -2440,28 +2409,28 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_add_index( - conn, - persistence, - name.as_deref(), - &table, - &columns, - // Simple-mode `add index` is always non-unique - // (ADR-0025); `add unique index` stays deferred. The SQL - // `CREATE UNIQUE INDEX` path passes `true` (ADR-0035 §4d). - false, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_add_index( + conn, + persistence, + name.as_deref(), + &table, + &columns, + // Simple-mode `add index` is always non-unique + // (ADR-0025); `add unique index` stays deferred. The SQL + // `CREATE UNIQUE INDEX` path passes `true` (ADR-0035 §4d). + false, + ) + }); } Request::DropIndex { selector, source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_drop_index( - conn, - persistence, - &selector, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_drop_index(conn, persistence, &selector) + }); } Request::SqlDropIndex { name, @@ -2476,8 +2445,7 @@ fn handle_request( // `do_drop_index` (`sql IS NOT NULL`). if if_exists && !index_exists(conn, &name, true).unwrap_or(false) { // ADR-0052: journaling moved to the dispatch layer. - let result: Result = - Ok(DropIndexOutcome::Skipped); + let result: Result = Ok(DropIndexOutcome::Skipped); let _ = reply.send(result); } else { snapshot_then(snap, batch, conn, source.as_deref(), reply, || { @@ -2513,15 +2481,8 @@ fn handle_request( let _ = reply.send(result); } else { snapshot_then(snap, batch, conn, source.as_deref(), reply, || { - do_add_index( - conn, - persistence, - name.as_deref(), - &table, - &columns, - unique, - ) - .map(CreateIndexOutcome::Created) + do_add_index(conn, persistence, name.as_deref(), &table, &columns, unique) + .map(CreateIndexOutcome::Created) }); } } @@ -2532,13 +2493,9 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_add_constraint( - conn, - persistence, - &table, - &column, - &constraint, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_add_constraint(conn, persistence, &table, &column, &constraint) + }); } Request::DropConstraint { table, @@ -2547,13 +2504,9 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_drop_constraint( - conn, - persistence, - &table, - &column, - kind, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_drop_constraint(conn, persistence, &table, &column, kind) + }); } Request::SetColumnDefault { table, @@ -2563,13 +2516,7 @@ fn handle_request( reply, } => { snapshot_then(snap, batch, conn, source.as_deref(), reply, || { - do_set_column_default( - conn, - persistence, - &table, - &column, - &default_sql, - ) + do_set_column_default(conn, persistence, &table, &column, &default_sql) }); } Request::AlterAddTableCheck { @@ -2580,13 +2527,7 @@ fn handle_request( reply, } => { snapshot_then(snap, batch, conn, source.as_deref(), reply, || { - do_alter_add_table_check( - conn, - persistence, - &table, - name.as_deref(), - &expr_sql, - ) + do_alter_add_table_check(conn, persistence, &table, name.as_deref(), &expr_sql) }); } Request::AlterAddUnique { @@ -2617,13 +2558,7 @@ fn handle_request( reply, } => { snapshot_then(snap, batch, conn, source.as_deref(), reply, || { - do_alter_add_foreign_key( - conn, - persistence, - &child_table, - name.as_deref(), - &fk, - ) + do_alter_add_foreign_key(conn, persistence, &child_table, name.as_deref(), &fk) }); } Request::Insert { @@ -2633,13 +2568,9 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_insert( - conn, - persistence, - &table, - columns.as_deref(), - &values, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_insert(conn, persistence, &table, columns.as_deref(), &values) + }); } Request::Seed { table, @@ -2652,15 +2583,17 @@ fn handle_request( } => { // One snapshot wraps the whole seed (ADR-0048 D15 — one undo // step), exactly like a single insert. - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_seed( - conn, - persistence, - &table, - target_column.as_deref(), - count, - &overrides, - rng_seed, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_seed( + conn, + persistence, + &table, + target_column.as_deref(), + count, + &overrides, + rng_seed, + ) + }); } Request::Update { table, @@ -2669,13 +2602,9 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_update( - conn, - persistence, - &table, - &assignments, - &filter, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_update(conn, persistence, &table, &assignments, &filter) + }); } Request::Delete { table, @@ -2683,12 +2612,9 @@ fn handle_request( source, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_delete( - conn, - persistence, - &table, - &filter, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_delete(conn, persistence, &table, &filter) + }); } Request::QueryData { table, @@ -2711,16 +2637,18 @@ fn handle_request( literal_rows, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_sql_insert( - conn, - persistence, - &sql, - &target_table, - &listed_columns, - &row_source, - returning, - &literal_rows, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_sql_insert( + conn, + persistence, + &sql, + &target_table, + &listed_columns, + &row_source, + returning, + &literal_rows, + ) + }); } Request::RunSqlUpdate { sql, @@ -2730,14 +2658,16 @@ fn handle_request( set_literals, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_sql_update( - conn, - persistence, - &sql, - &target_table, - returning, - &set_literals, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_sql_update( + conn, + persistence, + &sql, + &target_table, + returning, + &set_literals, + ) + }); } Request::RunSqlDelete { sql, @@ -2746,13 +2676,9 @@ fn handle_request( returning, reply, } => { - snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_sql_delete( - conn, - persistence, - &sql, - &target_table, - returning, - )); + snapshot_then(snap, batch, conn, source.as_deref(), reply, || { + do_sql_delete(conn, persistence, &sql, &target_table, returning) + }); } Request::RebuildFromText { project_path, @@ -2835,9 +2761,7 @@ fn do_list_names_for( } IdentSource::Relationships => { let mut stmt = conn - .prepare(&format!( - "SELECT name FROM {REL_TABLE} ORDER BY name;" - )) + .prepare(&format!("SELECT name FROM {REL_TABLE} ORDER BY name;")) .map_err(DbError::from_rusqlite)?; let rows = stmt .query_map([], |row| row.get::<_, String>(0)) @@ -2869,10 +2793,9 @@ fn do_list_names_for( } Ok(out) } - IdentSource::NewName - | IdentSource::Types - | IdentSource::Generators - | IdentSource::Free => Ok(Vec::new()), + IdentSource::NewName | IdentSource::Types | IdentSource::Generators | IdentSource::Free => { + Ok(Vec::new()) + } } } @@ -2916,8 +2839,7 @@ fn do_find_rows_matching( .map_err(|e| DbError::InvalidValue(e.to_string()))?; let column_names: Vec = schema.columns.iter().map(|c| c.name.clone()).collect(); - let column_types: Vec> = - schema.columns.iter().map(|c| c.user_type).collect(); + let column_types: Vec> = schema.columns.iter().map(|c| c.user_type).collect(); let cols_csv = column_names .iter() .map(|c| quote_ident(c)) @@ -2935,8 +2857,7 @@ fn do_find_rows_matching( let mut stmt = conn.prepare(&sql).map_err(DbError::from_rusqlite)?; let rows_iter = stmt .query_map(rusqlite::params![bound_value], |row| { - let mut cells: Vec = - Vec::with_capacity(column_names.len()); + let mut cells: Vec = Vec::with_capacity(column_names.len()); for i in 0..column_names.len() { cells.push(row.get(i)?); } @@ -3139,10 +3060,7 @@ fn read_project_created_at(conn: &Connection) -> Result { /// Read a single table's full row data, returning `None` if /// the table no longer exists (e.g. a recent `drop_table`). -fn read_table_snapshot( - conn: &Connection, - table: &str, -) -> Result, DbError> { +fn read_table_snapshot(conn: &Connection, table: &str) -> Result, DbError> { if !user_table_exists(conn, table)? { return Ok(None); } @@ -3159,11 +3077,7 @@ fn read_table_snapshot( check: c.check.clone(), }) .collect(); - let column_idents: Vec = read - .columns - .iter() - .map(|c| quote_ident(&c.name)) - .collect(); + let column_idents: Vec = read.columns.iter().map(|c| quote_ident(&c.name)).collect(); let sql = format!( "SELECT {} FROM {} ORDER BY rowid", column_idents.join(", "), @@ -3234,7 +3148,9 @@ fn canonical_table_name(conn: &Connection, name: &str) -> Result, .map_err(DbError::from_rusqlite)?; let mut rows = stmt.query([name]).map_err(DbError::from_rusqlite)?; match rows.next().map_err(DbError::from_rusqlite)? { - Some(row) => Ok(Some(row.get::<_, String>(0).map_err(DbError::from_rusqlite)?)), + Some(row) => Ok(Some( + row.get::<_, String>(0).map_err(DbError::from_rusqlite)?, + )), None => Ok(None), } } @@ -3252,9 +3168,7 @@ fn row_value_to_cell(row: &rusqlite::Row<'_>, idx: usize) -> Result CellValue::Null, ValueRef::Integer(n) => CellValue::Integer(n), ValueRef::Real(f) => CellValue::Real(f), - ValueRef::Text(bytes) => { - CellValue::Text(String::from_utf8_lossy(bytes).into_owned()) - } + ValueRef::Text(bytes) => CellValue::Text(String::from_utf8_lossy(bytes).into_owned()), ValueRef::Blob(bytes) => CellValue::Blob(bytes.to_vec()), }) } @@ -3496,10 +3410,9 @@ fn do_create_table( // computes the next value itself (verified by the multi-column / // rebuild round-trip tests) — so the choice is purely about // matching the rebuild generator (ADR-0035 §6.4). - let inline_pk_col: Option<&str> = (primary_key.len() == 1 - && !columns.is_empty() - && primary_key[0] == columns[0].name) - .then(|| primary_key[0].as_str()); + let inline_pk_col: Option<&str> = + (primary_key.len() == 1 && !columns.is_empty() && primary_key[0] == columns[0].name) + .then(|| primary_key[0].as_str()); // Compile each column's CHECK once (ADR-0029 §4) — reused // by the DDL clause and the metadata insert below. The @@ -3511,9 +3424,11 @@ fn do_create_table( let check_sqls: Vec> = columns .iter() .map(|c| { - c.check_sql - .clone() - .or_else(|| c.check.as_ref().map(|e| compile_check_sql(e, &check_schema))) + c.check_sql.clone().or_else(|| { + c.check + .as_ref() + .map(|e| compile_check_sql(e, &check_schema)) + }) }) .collect(); @@ -3865,57 +3780,55 @@ fn do_add_auto_generated_column( let old_columns: Vec = old_schema.columns.iter().map(|c| c.name.clone()).collect(); let new_columns: Vec = new_schema.columns.iter().map(|c| c.name.clone()).collect(); - let copy_data = |tx: &rusqlite::Transaction<'_>, - temp_name: &str, - orig: &str| - -> Result<(), DbError> { - if row_count == 0 { - return Ok(()); - } - // Read all rows from old, append the auto-fill value, - // INSERT into temp. - let select_cols = old_columns - .iter() - .map(|c| quote_ident(c)) - .collect::>() - .join(", "); - let select_sql = format!( - "SELECT {select_cols} FROM {orig};", - orig = quote_ident(orig), - ); - let cols_csv = new_columns - .iter() - .map(|c| quote_ident(c)) - .collect::>() - .join(", "); - let placeholders = (1..=new_columns.len()) - .map(|i| format!("?{i}")) - .collect::>() - .join(", "); - let insert_sql = format!( - "INSERT INTO {temp} ({cols_csv}) VALUES ({placeholders});", - temp = quote_ident(temp_name), - ); - - let mut select_stmt = tx.prepare(&select_sql).map_err(DbError::from_rusqlite)?; - let mut rows = select_stmt.query([]).map_err(DbError::from_rusqlite)?; - let mut insert_stmt = tx.prepare(&insert_sql).map_err(DbError::from_rusqlite)?; - let mut row_idx = 0usize; - while let Some(r) = rows.next().map_err(DbError::from_rusqlite)? { - let mut values: Vec = Vec::with_capacity(new_columns.len()); - for i in 0..old_columns.len() { - values.push(r.get(i).map_err(DbError::from_rusqlite)?); + let copy_data = + |tx: &rusqlite::Transaction<'_>, temp_name: &str, orig: &str| -> Result<(), DbError> { + if row_count == 0 { + return Ok(()); } - values.push(auto_fill_values[row_idx].clone()); - let params: Vec<&dyn rusqlite::ToSql> = - values.iter().map(|v| v as &dyn rusqlite::ToSql).collect(); - insert_stmt - .execute(rusqlite::params_from_iter(params)) - .map_err(DbError::from_rusqlite)?; - row_idx += 1; - } - Ok(()) - }; + // Read all rows from old, append the auto-fill value, + // INSERT into temp. + let select_cols = old_columns + .iter() + .map(|c| quote_ident(c)) + .collect::>() + .join(", "); + let select_sql = format!( + "SELECT {select_cols} FROM {orig};", + orig = quote_ident(orig), + ); + let cols_csv = new_columns + .iter() + .map(|c| quote_ident(c)) + .collect::>() + .join(", "); + let placeholders = (1..=new_columns.len()) + .map(|i| format!("?{i}")) + .collect::>() + .join(", "); + let insert_sql = format!( + "INSERT INTO {temp} ({cols_csv}) VALUES ({placeholders});", + temp = quote_ident(temp_name), + ); + + let mut select_stmt = tx.prepare(&select_sql).map_err(DbError::from_rusqlite)?; + let mut rows = select_stmt.query([]).map_err(DbError::from_rusqlite)?; + let mut insert_stmt = tx.prepare(&insert_sql).map_err(DbError::from_rusqlite)?; + let mut row_idx = 0usize; + while let Some(r) = rows.next().map_err(DbError::from_rusqlite)? { + let mut values: Vec = Vec::with_capacity(new_columns.len()); + for i in 0..old_columns.len() { + values.push(r.get(i).map_err(DbError::from_rusqlite)?); + } + values.push(auto_fill_values[row_idx].clone()); + let params: Vec<&dyn rusqlite::ToSql> = + values.iter().map(|v| v as &dyn rusqlite::ToSql).collect(); + insert_stmt + .execute(rusqlite::params_from_iter(params)) + .map_err(DbError::from_rusqlite)?; + row_idx += 1; + } + Ok(()) + }; let metadata_updates = |tx: &rusqlite::Transaction<'_>| -> Result<(), DbError> { tx.execute( @@ -4015,10 +3928,11 @@ fn do_add_constrained_column_via_rebuild( // The CHECK: the raw advanced-mode text (`check_sql`) wins; otherwise // the typed `check` AST is compiled against the post-add schema (so // it may reference the new column itself). - let check_sql = spec - .check_sql - .clone() - .or_else(|| spec.check.as_ref().map(|e| compile_check_sql(e, &new_schema))); + let check_sql = spec.check_sql.clone().or_else(|| { + spec.check + .as_ref() + .map(|e| compile_check_sql(e, &new_schema)) + }); if let Some(last) = new_schema.columns.last_mut() { last.check.clone_from(&check_sql); } @@ -4100,9 +4014,7 @@ fn do_add_constraint( // ADR-0029 §6 — an auto-generated column fills its own // values, so a `default` would be a second, ambiguous // source of "the value when none is given". - Constraint::Default(_) - if matches!(col_user_type, Some(Type::Serial | Type::ShortId)) => - { + Constraint::Default(_) if matches!(col_user_type, Some(Type::Serial | Type::ShortId)) => { return Err(DbError::Unsupported(format!( "`{table}.{column}` is a {ty} column — it auto-fills its own \ values, so it cannot also carry a `default`.", @@ -4130,7 +4042,9 @@ fn do_add_constraint( &old_schema, table, column, - check_sql.as_deref().expect("check_sql set for a Check constraint"), + check_sql + .as_deref() + .expect("check_sql set for a Check constraint"), )?, Constraint::Default(_) => None, }; @@ -4374,8 +4288,7 @@ fn read_constraint_dry_run_rows( ) -> Result, DbError> { use rusqlite::types::Value as RV; let pk_columns = &schema.primary_key; - let mut select_idents: Vec = - pk_columns.iter().map(|c| quote_ident(c)).collect(); + let mut select_idents: Vec = pk_columns.iter().map(|c| quote_ident(c)).collect(); select_idents.push(quote_ident(column)); let sql = format!( "SELECT {cols} FROM {tbl} WHERE {pred};", @@ -4582,13 +4495,8 @@ fn dry_run_check( column: &str, check_sql: &str, ) -> Result, DbError> { - let rows = read_constraint_dry_run_rows( - conn, - schema, - table, - column, - &format!("NOT ({check_sql})"), - )?; + let rows = + read_constraint_dry_run_rows(conn, schema, table, column, &format!("NOT ({check_sql})"))?; if rows.is_empty() { return Ok(None); } @@ -4818,9 +4726,7 @@ fn do_drop_column( debug!(ddl = %ddl, "drop_column"); tx.execute_batch(&ddl).map_err(DbError::from_rusqlite)?; tx.execute( - &format!( - "DELETE FROM {META_TABLE} WHERE table_name = ?1 AND column_name = ?2;" - ), + &format!("DELETE FROM {META_TABLE} WHERE table_name = ?1 AND column_name = ?2;"), [table, column], ) .map_err(DbError::from_rusqlite)?; @@ -5055,7 +4961,9 @@ fn do_rename_table( )) .map_err(DbError::from_rusqlite)?; let rows = stmt - .query_map([new], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))) + .query_map([new], |r| { + Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)) + }) .map_err(DbError::from_rusqlite)?; let mut v = Vec::new(); for row in rows { @@ -5284,8 +5192,8 @@ fn do_change_column_type( // STRICT typing will accept or refuse cells; we wrap // any error in a friendly message (ADR-0017 §5, // ADR-0002 user-facing posture). - rebuild_table(conn, table, &old_schema, &new_schema, metadata_updates) - .map_err(|e| { + rebuild_table(conn, table, &old_schema, &new_schema, metadata_updates).map_err( + |e| { let ctx = crate::friendly::TranslateContext { operation: Some(crate::friendly::Operation::ChangeColumnType), table: Some(table.to_string()), @@ -5294,14 +5202,14 @@ fn do_change_column_type( target_type: Some(ty), ..crate::friendly::TranslateContext::default() }; - let rendered = - crate::friendly::translate_error(&e, &ctx).render(); + let rendered = crate::friendly::translate_error(&e, &ctx).render(); DbError::Unsupported(rendered) - })?; + }, + )?; None } - ChangeColumnMode::Default | ChangeColumnMode::ForceConversion => Some( - run_change_column_with_dry_run( + ChangeColumnMode::Default | ChangeColumnMode::ForceConversion => { + Some(run_change_column_with_dry_run( conn, table, column, @@ -5311,8 +5219,8 @@ fn do_change_column_type( &old_schema, &new_schema, metadata_updates, - )?, - ), + )?) + } }; // Client-side note fires when at least one cell was // materially transformed (ADR-0017 §6) OR at least one @@ -5378,7 +5286,12 @@ where .expect("column existence checked above"); let pk_indices: Vec = pk_columns .iter() - .map(|pk| all_columns.iter().position(|c| c == pk).expect("pk in cols")) + .map(|pk| { + all_columns + .iter() + .position(|c| c == pk) + .expect("pk in cols") + }) .collect(); let select_cols = all_columns @@ -5425,7 +5338,10 @@ where // Placeholder; replaced after the loop. (CellOutcome::Clean(RV::Null), true) } else { - (type_change::transform_cell(src_ty, target_ty, &original), false) + ( + type_change::transform_cell(src_ty, target_ty, &original), + false, + ) }; outcomes.push(Outcome { pk_values, @@ -5516,39 +5432,37 @@ where }) .collect(); - let copy_data = |tx: &rusqlite::Transaction<'_>, - temp_name: &str, - _orig: &str| - -> Result<(), DbError> { - if rows.is_empty() { - return Ok(()); - } - let cols_csv = all_columns - .iter() - .map(|c| quote_ident(c)) - .collect::>() - .join(", "); - let placeholders = (0..all_columns.len()) - .map(|i| format!("?{}", i + 1)) - .collect::>() - .join(", "); - let insert_sql = format!( - "INSERT INTO {temp} ({cols}) VALUES ({ph});", - temp = quote_ident(temp_name), - cols = cols_csv, - ph = placeholders, - ); - let mut stmt = tx.prepare(&insert_sql).map_err(DbError::from_rusqlite)?; - for (row_idx, row) in rows.iter().enumerate() { - let mut bound: Vec = row.clone(); - bound[target_idx] = transformed_values[row_idx].clone(); - let params: Vec<&dyn rusqlite::ToSql> = - bound.iter().map(|v| v as &dyn rusqlite::ToSql).collect(); - stmt.execute(rusqlite::params_from_iter(params)) - .map_err(DbError::from_rusqlite)?; - } - Ok(()) - }; + let copy_data = + |tx: &rusqlite::Transaction<'_>, temp_name: &str, _orig: &str| -> Result<(), DbError> { + if rows.is_empty() { + return Ok(()); + } + let cols_csv = all_columns + .iter() + .map(|c| quote_ident(c)) + .collect::>() + .join(", "); + let placeholders = (0..all_columns.len()) + .map(|i| format!("?{}", i + 1)) + .collect::>() + .join(", "); + let insert_sql = format!( + "INSERT INTO {temp} ({cols}) VALUES ({ph});", + temp = quote_ident(temp_name), + cols = cols_csv, + ph = placeholders, + ); + let mut stmt = tx.prepare(&insert_sql).map_err(DbError::from_rusqlite)?; + for (row_idx, row) in rows.iter().enumerate() { + let mut bound: Vec = row.clone(); + bound[target_idx] = transformed_values[row_idx].clone(); + let params: Vec<&dyn rusqlite::ToSql> = + bound.iter().map(|v| v as &dyn rusqlite::ToSql).collect(); + stmt.execute(rusqlite::params_from_iter(params)) + .map_err(DbError::from_rusqlite)?; + } + Ok(()) + }; rebuild_table_with_copy(conn, table, new_schema, copy_data, metadata_updates)?; @@ -5601,10 +5515,7 @@ where /// `MAX(non-null value) + 1`. For `ShortId` targets: generate /// fresh shortids that don't collide with existing values or /// with one another (5-retry budget per cell, ADR-0018 §3). -fn fill_auto_generated_cells( - target_ty: Type, - outcomes: &mut [Outcome], -) -> Result<(), DbError> { +fn fill_auto_generated_cells(target_ty: Type, outcomes: &mut [Outcome]) -> Result<(), DbError> { use rusqlite::types::Value as RV; use type_change::CellOutcome; @@ -5644,11 +5555,7 @@ fn fill_auto_generated_cells( .collect(); let auto_fill_count = outcomes.iter().filter(|o| o.is_auto_fill).count(); let new_values = generate_shortid_batch(auto_fill_count, &existing)?; - for (idx, o) in outcomes - .iter_mut() - .filter(|o| o.is_auto_fill) - .enumerate() - { + for (idx, o) in outcomes.iter_mut().filter(|o| o.is_auto_fill).enumerate() { o.outcome = CellOutcome::Clean(new_values[idx].clone()); } } @@ -5695,9 +5602,7 @@ fn render_lossy_diagnostic( for o in lossies.iter().take(visible) { let mut cells = pk_value_cells(&o.pk_values); let (new_str, reason) = match &o.outcome { - type_change::CellOutcome::Lossy { new, reason } => { - (render_value(new), reason.clone()) - } + type_change::CellOutcome::Lossy { new, reason } => (render_value(new), reason.clone()), _ => unreachable!("filtered to Lossy"), }; cells.push(render_value(&o.original)); @@ -5746,10 +5651,7 @@ fn render_incompatible_diagnostic( ]); let mut alignments = pk_header_alignments(pk_columns, old_schema); - alignments.extend([ - type_change::is_in_matrix_alignment(src_ty), - Alignment::Left, - ]); + alignments.extend([type_change::is_in_matrix_alignment(src_ty), Alignment::Left]); let total = incompatibles.len(); let visible = total.min(DIAGNOSTIC_ROW_CAP); @@ -5908,10 +5810,7 @@ struct Outcome { } fn pk_header_cells(pk_columns: &[String]) -> Vec { - pk_columns - .iter() - .map(|c| format!("{c} (PK)")) - .collect() + pk_columns.iter().map(|c| format!("{c} (PK)")).collect() } fn pk_header_alignments(pk_columns: &[String], schema: &ReadSchema) -> Vec { @@ -6382,7 +6281,9 @@ fn read_table_checks(conn: &Connection, table: &str) -> Result, fn check_table_has_name_column(conn: &Connection) -> Result { let count: i64 = conn .query_row( - &format!("SELECT COUNT(*) FROM pragma_table_info('{CHECK_TABLE}') WHERE name = 'name';"), + &format!( + "SELECT COUNT(*) FROM pragma_table_info('{CHECK_TABLE}') WHERE name = 'name';" + ), [], |row| row.get(0), ) @@ -6544,14 +6445,23 @@ mod rewrite_check_table_qualifier_tests { #[test] fn rewrites_a_bare_qualifier() { - assert_eq!(rewrite_check_table_qualifier("T.age > 0", "T", "U"), "U.age > 0"); + assert_eq!( + rewrite_check_table_qualifier("T.age > 0", "T", "U"), + "U.age > 0" + ); // multiple occurrences, table-level CHECK shape - assert_eq!(rewrite_check_table_qualifier("T.a <> T.b", "T", "U"), "U.a <> U.b"); + assert_eq!( + rewrite_check_table_qualifier("T.a <> T.b", "T", "U"), + "U.a <> U.b" + ); } #[test] fn is_case_insensitive_on_the_qualifier() { - assert_eq!(rewrite_check_table_qualifier("t.age > 0", "T", "U"), "U.age > 0"); + assert_eq!( + rewrite_check_table_qualifier("t.age > 0", "T", "U"), + "U.age > 0" + ); } #[test] @@ -6578,7 +6488,10 @@ mod rewrite_check_table_qualifier_tests { #[test] fn unqualified_check_is_a_no_op() { - assert_eq!(rewrite_check_table_qualifier("age > 0", "T", "U"), "age > 0"); + assert_eq!( + rewrite_check_table_qualifier("age > 0", "T", "U"), + "age > 0" + ); } #[test] @@ -6674,9 +6587,7 @@ fn read_table_indexes(conn: &Connection, table: &str) -> Result, /// The indexed columns of `index`, in index order. fn read_index_columns(conn: &Connection, index: &str) -> Result, DbError> { let mut stmt = conn - .prepare( - "SELECT name FROM pragma_index_info(?1) ORDER BY seqno;", - ) + .prepare("SELECT name FROM pragma_index_info(?1) ORDER BY seqno;") .map_err(DbError::from_rusqlite)?; let rows = stmt .query_map([index], |row| row.get::<_, String>(0)) @@ -6925,11 +6836,8 @@ where // column references stay valid. let captured_indexes = read_table_indexes(&tx, table)?; - tx.execute_batch(&format!( - "DROP TABLE {ident};", - ident = quote_ident(table) - )) - .map_err(DbError::from_rusqlite)?; + tx.execute_batch(&format!("DROP TABLE {ident};", ident = quote_ident(table))) + .map_err(DbError::from_rusqlite)?; tx.execute_batch(&format!( "ALTER TABLE {temp} RENAME TO {final_name};", temp = quote_ident(&temp_name), @@ -7275,7 +7183,10 @@ fn resolve_create_table_fks( if fk.parent_table == table_name { ( primary_key.to_vec(), - columns.iter().map(|c| (c.name.clone(), Some(c.ty))).collect(), + columns + .iter() + .map(|c| (c.name.clone(), Some(c.ty))) + .collect(), ) } else { let ps = read_schema(conn, &fk.parent_table)?; @@ -7625,7 +7536,15 @@ fn do_drop_relationship( FROM {REL_TABLE} WHERE name = ?1;" ), [name], - |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?)), + |row| { + Ok(( + row.get(0)?, + row.get(1)?, + row.get(2)?, + row.get(3)?, + row.get(4)?, + )) + }, ) .ok(), RelationshipSelector::Endpoints { @@ -7642,7 +7561,15 @@ fn do_drop_relationship( AND child_table = ?3 AND child_column = ?4;" ), [parent_table, parent_column, child_table, child_column], - |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?)), + |row| { + Ok(( + row.get(0)?, + row.get(1)?, + row.get(2)?, + row.get(3)?, + row.get(4)?, + )) + }, ) .ok(), }; @@ -7891,9 +7818,7 @@ fn do_drop_constraint_by_name( let (t, n) = (table.to_string(), name.to_string()); rebuild_table(conn, table, &old_schema, &new_schema, |tx| { tx.execute( - &format!( - "DELETE FROM {CHECK_TABLE} WHERE table_name = ?1 AND name = ?2;" - ), + &format!("DELETE FROM {CHECK_TABLE} WHERE table_name = ?1 AND name = ?2;"), [t.as_str(), n.as_str()], ) .map_err(DbError::from_rusqlite)?; @@ -8214,10 +8139,7 @@ fn do_drop_index( match matches.as_slice() { [] => { return Err(DbError::Sqlite { - message: format!( - "no index on {table} ({}) exists", - columns.join(", ") - ), + message: format!("no index on {table} ({}) exists", columns.join(", ")), kind: SqliteErrorKind::Other, }); } @@ -8298,11 +8220,7 @@ fn do_describe_table(conn: &Connection, name: &str) -> Result Result { +fn impl_value_for(schema: &ReadSchema, column: &str, value: &Value) -> Result { let col = schema .columns .iter() @@ -8542,8 +8460,7 @@ fn query_rows_by_rowid( ) -> Result { let schema = read_schema(conn, table)?; let column_names: Vec = schema.columns.iter().map(|c| c.name.clone()).collect(); - let column_types: Vec> = - schema.columns.iter().map(|c| c.user_type).collect(); + let column_types: Vec> = schema.columns.iter().map(|c| c.user_type).collect(); if rowids.is_empty() { return Ok(DataResult { @@ -8575,8 +8492,7 @@ fn query_rows_by_rowid( .collect(); let rows_iter = stmt .query_map(rusqlite::params_from_iter(params.iter()), |row| { - let mut cells: Vec = - Vec::with_capacity(column_names.len()); + let mut cells: Vec = Vec::with_capacity(column_names.len()); for i in 0..column_names.len() { cells.push(row.get(i)?); } @@ -8751,9 +8667,7 @@ fn do_seed( // Column-fill (D1 form 2) is a distinct UPDATE path. if let Some(col) = target_column { - return do_seed_column_fill( - conn, persistence, table, col, count, overrides, rng_seed, - ); + return do_seed_column_fill(conn, persistence, table, col, count, overrides, rng_seed); } let n = count.unwrap_or(DEFAULT_SEED_COUNT); @@ -8862,7 +8776,14 @@ fn do_seed( // up front rather than silently capped (DA finding). FK / type binding // still apply — a value that violates a constraint surfaces through the // existing FK-error guard. - apply_seed_overrides(&schema, overrides, n, &col_names, &mut plans, &mut advisory_columns)?; + apply_seed_overrides( + &schema, + overrides, + n, + &col_names, + &mut plans, + &mut advisory_columns, + )?; // Uniqueness groups (ADR-0048 D10): value tuples that must stay // distinct across the batch and against existing rows — the @@ -8987,8 +8908,7 @@ fn do_seed( let keys: Vec = unique_groups .iter() .map(|group| { - let projected: Vec = - group.iter().map(|&i| values[i].clone()).collect(); + let projected: Vec = group.iter().map(|&i| values[i].clone()).collect(); seed_value_list_key(&projected) }) .collect(); @@ -9003,8 +8923,7 @@ fn do_seed( for (gi, k) in keys.into_iter().enumerate() { used[gi].insert(k); } - let (_rows, rowid) = - insert_one_row(conn, table, &schema, Some(&col_names), &values)?; + let (_rows, rowid) = insert_one_row(conn, table, &schema, Some(&col_names), &values)?; break Some(rowid); }; match rowid { @@ -9132,8 +9051,7 @@ fn seed_override_capacity_guard( .iter() .find(|c| c.name.eq_ignore_ascii_case(column)) .is_some_and(|c| c.unique) - || (schema.primary_key.len() == 1 - && schema.primary_key[0].eq_ignore_ascii_case(column)); + || (schema.primary_key.len() == 1 && schema.primary_key[0].eq_ignore_ascii_case(column)); if single_unique { return Err(DbError::Unsupported(format!( "cannot fill {row_count} rows: `set {column} …` offers only {distinct} distinct \ @@ -9391,9 +9309,7 @@ fn do_seed_column_fill( ); let mut used: std::collections::HashSet = std::collections::HashSet::new(); if enforce_unique { - for tuple in - sample_parent_key_tuples(conn, table, std::slice::from_ref(&canonical_col))? - { + for tuple in sample_parent_key_tuples(conn, table, std::slice::from_ref(&canonical_col))? { used.insert(seed_value_list_key(&tuple)); } } @@ -9457,8 +9373,10 @@ fn do_seed_column_fill( break v; }; let bound = impl_value_for(&schema, &canonical_col, &value)?; - let params: Vec = - vec![bound_to_sqlite_value(&bound), rusqlite::types::Value::Integer(*rowid)]; + let params: Vec = vec![ + bound_to_sqlite_value(&bound), + rusqlite::types::Value::Integer(*rowid), + ]; execute_with_fk_enrichment(conn, table, &update_sql, ¶ms)?; produced += 1; let _ = offset; @@ -9550,10 +9468,7 @@ fn insert_one_row( // thread serialisation (ADR-0010) makes this read-then- // write sequence safe without explicit locking. for c in &schema.columns { - if c.user_type == Some(Type::Serial) - && !c.primary_key - && !provided.contains(&c.name) - { + if c.user_type == Some(Type::Serial) && !c.primary_key && !provided.contains(&c.name) { let next: i64 = conn .query_row( &format!( @@ -9597,8 +9512,10 @@ fn insert_one_row( ident = quote_ident(table), ); debug!(sql = %sql, "insert"); - let params: Vec = - bindings.iter().map(|(_, b)| bound_to_sqlite_value(b)).collect(); + let params: Vec = bindings + .iter() + .map(|(_, b)| bound_to_sqlite_value(b)) + .collect(); let rows_affected = execute_with_fk_enrichment(conn, table, &sql, ¶ms)?; let new_rowid = conn.last_insert_rowid(); Ok((rows_affected, new_rowid)) @@ -9628,7 +9545,10 @@ fn do_insert( }; finalize_persistence(conn, persistence, &changes)?; tx.commit().map_err(DbError::from_rusqlite)?; - Ok(InsertResult { rows_affected, data }) + Ok(InsertResult { + rows_affected, + data, + }) } /// Build the parameterised `UPDATE … SET … WHERE …` statement. @@ -9763,7 +9683,10 @@ fn build_delete_sql( format!(" WHERE {}", compile_expr(expr, schema, &mut params)) } }; - let sql = format!("DELETE FROM {ident}{where_sql};", ident = quote_ident(table)); + let sql = format!( + "DELETE FROM {ident}{where_sql};", + ident = quote_ident(table) + ); (sql, params) } @@ -9860,11 +9783,7 @@ fn do_delete( /// Currently-stored non-NULL values of one column, for shortid /// collision-avoidance (passed to `generate_shortid_batch`). -fn existing_shortids( - conn: &Connection, - table: &str, - column: &str, -) -> Result, DbError> { +fn existing_shortids(conn: &Connection, table: &str, column: &str) -> Result, DbError> { let mut stmt = conn .prepare(&format!( "SELECT {col} FROM {tbl} WHERE {col} IS NOT NULL;", @@ -9932,8 +9851,10 @@ fn plan_autogen_autofill( // Identifiers are case-preserving but matched case-insensitively // (ADR-0009): a column counts as omitted unless the user listed a // name equal to it ignoring ASCII case. - let listed_ci: Vec = - listed_columns.iter().map(|c| c.to_ascii_lowercase()).collect(); + let listed_ci: Vec = listed_columns + .iter() + .map(|c| c.to_ascii_lowercase()) + .collect(); let is_omitted = |c: &&ReadColumn| !listed_ci.contains(&c.name.to_ascii_lowercase()); let omitted_shortids: Vec = schema .columns @@ -10020,8 +9941,11 @@ fn plan_autogen_autofill( |row| row.get(0), ) .map_err(DbError::from_rusqlite)?; - serial_batches - .push((1..=n as i64).map(|i| rusqlite::types::Value::Integer(max + i)).collect()); + serial_batches.push( + (1..=n as i64) + .map(|i| rusqlite::types::Value::Integer(max + i)) + .collect(), + ); } // Reconstruct: listed columns, then the omitted shortid columns, @@ -10156,7 +10080,13 @@ fn do_sql_insert( String::new() } else { sql.find(row_source) - .map(|i| sql[i + row_source.len()..].trim().trim_end_matches(';').trim().to_string()) + .map(|i| { + sql[i + row_source.len()..] + .trim() + .trim_end_matches(';') + .trim() + .to_string() + }) .unwrap_or_default() }; // Sub-phase 3d: when the user's column list omits one or more @@ -10165,8 +10095,14 @@ fn do_sql_insert( // rows. Returns the executable SQL + bound params; an empty // params vec with the original `sql` means "no auto-fill — // execute verbatim" (the 3b path). - let (exec_sql, params) = - plan_autogen_autofill(conn, target_table, sql, listed_columns, row_source, &trailing_tail)?; + let (exec_sql, params) = plan_autogen_autofill( + conn, + target_table, + sql, + listed_columns, + row_source, + &trailing_tail, + )?; let tx = conn .unchecked_transaction() .map_err(DbError::from_rusqlite)?; @@ -10277,7 +10213,10 @@ fn do_sql_update( }; finalize_persistence(conn, persistence, &changes)?; tx.commit().map_err(DbError::from_rusqlite)?; - Ok(UpdateResult { rows_affected, data }) + Ok(UpdateResult { + rows_affected, + data, + }) } /// Worker handler for `Request::RunSqlDelete` (ADR-0033 §1/§7, @@ -10440,8 +10379,7 @@ fn run_returning( table_name: &str, ) -> Result { let mut stmt = conn.prepare(sql).map_err(DbError::from_rusqlite)?; - let column_names: Vec = - stmt.column_names().into_iter().map(String::from).collect(); + let column_names: Vec = stmt.column_names().into_iter().map(String::from).collect(); let col_count = column_names.len(); let column_types = resolve_select_column_types(conn, &stmt); let rows_iter = stmt @@ -10475,11 +10413,7 @@ fn run_returning( fn do_run_select(conn: &Connection, sql: &str) -> Result { debug!(sql = %sql, "run_select"); let mut stmt = conn.prepare(sql).map_err(DbError::from_rusqlite)?; - let column_names: Vec = stmt - .column_names() - .into_iter() - .map(String::from) - .collect(); + let column_names: Vec = stmt.column_names().into_iter().map(String::from).collect(); let col_count = column_names.len(); let column_types = resolve_select_column_types(conn, &stmt); let rows_iter = stmt @@ -10521,10 +10455,7 @@ fn do_run_select(conn: &Connection, sql: &str) -> Result { /// `None` — Amendment 1 documents that recursive-CTE result /// columns and computed projections are the only structural /// classes that don't follow through. -fn resolve_select_column_types( - conn: &Connection, - stmt: &rusqlite::Statement, -) -> Vec> { +fn resolve_select_column_types(conn: &Connection, stmt: &rusqlite::Statement) -> Vec> { let metas = stmt.columns_with_metadata(); if metas.is_empty() { return Vec::new(); @@ -10614,8 +10545,7 @@ fn do_query_data( debug!(table = %table, limit = ?limit, "query_data"); let schema = read_schema(conn, table)?; let column_names: Vec = schema.columns.iter().map(|c| c.name.clone()).collect(); - let column_types: Vec> = - schema.columns.iter().map(|c| c.user_type).collect(); + let column_types: Vec> = schema.columns.iter().map(|c| c.user_type).collect(); let (sql, params) = build_query_data_sql(&schema, table, filter, limit); debug!(sql = %sql, "query_data"); @@ -10960,12 +10890,11 @@ fn do_rebuild_from_text(conn: &Connection, project_path: &Path) -> Result<(), Db let yaml_path = project_path.join(PROJECT_YAML); let data_dir = project_path.join(DATA_DIR); - let yaml_body = - std::fs::read_to_string(&yaml_path).map_err(|e| DbError::PersistenceFatal { - operation: "read", - path: yaml_path.clone(), - message: e.to_string(), - })?; + let yaml_body = std::fs::read_to_string(&yaml_path).map_err(|e| DbError::PersistenceFatal { + operation: "read", + path: yaml_path.clone(), + message: e.to_string(), + })?; let snapshot = parse_schema(&yaml_body).map_err(|e| DbError::PersistenceFatal { operation: "parse", path: yaml_path.clone(), @@ -10988,11 +10917,8 @@ fn do_rebuild_from_text(conn: &Connection, project_path: &Path) -> Result<(), Db // the text-source state per ADR-0015 §7. let existing_tables = do_list_tables(&tx)?; for name in &existing_tables { - tx.execute_batch(&format!( - "DROP TABLE {ident};", - ident = quote_ident(name) - )) - .map_err(DbError::from_rusqlite)?; + tx.execute_batch(&format!("DROP TABLE {ident};", ident = quote_ident(name))) + .map_err(DbError::from_rusqlite)?; } tx.execute_batch(&format!( "DELETE FROM {META_TABLE}; DELETE FROM {REL_TABLE};" @@ -11082,10 +11008,7 @@ fn do_rebuild_from_text(conn: &Connection, project_path: &Path) -> Result<(), Db for table in &snapshot.tables { for (seq, check) in table.check_constraints.iter().enumerate() { stmt.execute(rusqlite::params![ - table.name, - seq as i64, - check.expr, - check.name, + table.name, seq as i64, check.expr, check.name, ]) .map_err(DbError::from_rusqlite)?; } @@ -11402,7 +11325,8 @@ mod tests { name.to_string(), vec![col("id", Type::Serial)], vec!["id".to_string()], - None) + None, + ) .await .expect("create table") } @@ -11441,7 +11365,8 @@ mod tests { "Customers".to_string(), vec![col("email", Type::Text)], vec!["email".to_string()], - None) + None, + ) .await .unwrap(); assert_eq!(desc.columns.len(), 1); @@ -11459,7 +11384,8 @@ mod tests { "OrderLines".to_string(), vec![col("order_id", Type::Int), col("product_id", Type::Int)], vec!["order_id".to_string(), "product_id".to_string()], - None) + None, + ) .await .unwrap(); assert_eq!(desc.columns.len(), 2); @@ -11476,7 +11402,8 @@ mod tests { "T".to_string(), vec![col("flag", Type::Bool)], vec!["flag".to_string()], - None) + None, + ) .await .unwrap(); assert!(desc.columns[0].primary_key); @@ -11508,7 +11435,16 @@ mod tests { ) .await .unwrap_err(); - assert!(matches!(err, DbError::Sqlite { kind: SqliteErrorKind::NoSuchTable, .. }), "got {err:?}"); + assert!( + matches!( + err, + DbError::Sqlite { + kind: SqliteErrorKind::NoSuchTable, + .. + } + ), + "got {err:?}" + ); assert!(db.list_tables().await.unwrap().is_empty()); } @@ -11526,7 +11462,11 @@ mod tests { let db = db(); make_id_table(&db, "Customers").await; let result = db - .add_column("Customers".to_string(), ColumnSpec::new("Name".to_string(), Type::Text), None) + .add_column( + "Customers".to_string(), + ColumnSpec::new("Name".to_string(), Type::Text), + None, + ) .await .unwrap(); let desc = &result.description; @@ -11535,7 +11475,10 @@ mod tests { let name_col = desc.columns.iter().find(|c| c.name == "Name").unwrap(); assert_eq!(name_col.user_type, Some(Type::Text)); assert_eq!(name_col.sqlite_type.to_uppercase(), "TEXT"); - assert!(result.client_side_notes.is_empty(), "no auto-fill for plain types"); + assert!( + result.client_side_notes.is_empty(), + "no auto-fill for plain types" + ); } #[tokio::test] @@ -11546,9 +11489,13 @@ mod tests { // datetime, decimal — all backed by TEXT). make_id_table(&db, "T").await; for ty in [Type::Date, Type::DateTime, Type::Decimal, Type::ShortId] { - db.add_column("T".to_string(), ColumnSpec::new(format!("c_{ty}"), ty), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new(format!("c_{ty}"), ty), + None, + ) + .await + .unwrap(); } let desc = db.describe_table("T".to_string()).await.unwrap(); let id_col = desc.columns.iter().find(|c| c.name == "id").unwrap(); @@ -11578,7 +11525,8 @@ mod tests { "T".to_string(), vec![col("when", Type::Date)], vec!["when".to_string()], - None) + None, + ) .await .unwrap(); let before = db.describe_table("T".to_string()).await.unwrap(); @@ -11595,7 +11543,8 @@ mod tests { "T".to_string(), vec![col("when", Type::DateTime)], vec!["when".to_string()], - None) + None, + ) .await .unwrap(); let after = db.describe_table("T".to_string()).await.unwrap(); @@ -11625,7 +11574,11 @@ mod tests { let db = db(); make_id_table(&db, "T").await; let result = db - .add_column("T".to_string(), ColumnSpec::new("code".to_string(), Type::Serial), None) + .add_column( + "T".to_string(), + ColumnSpec::new("code".to_string(), Type::Serial), + None, + ) .await .unwrap(); let code = result @@ -11649,9 +11602,13 @@ mod tests { /// (text) and insert N rows, populating just `Name`. async fn make_table_with_n_rows(db: &Database, table: &str, count: usize) { make_id_table(db, table).await; - db.add_column(table.to_string(), ColumnSpec::new("Name".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + table.to_string(), + ColumnSpec::new("Name".to_string(), Type::Text), + None, + ) + .await + .unwrap(); for i in 0..count { db.insert( table.to_string(), @@ -11669,7 +11626,11 @@ mod tests { let db = db(); make_table_with_n_rows(&db, "T", 3).await; let result = db - .add_column("T".to_string(), ColumnSpec::new("seq".to_string(), Type::Serial), None) + .add_column( + "T".to_string(), + ColumnSpec::new("seq".to_string(), Type::Serial), + None, + ) .await .unwrap(); let seq = result @@ -11679,7 +11640,10 @@ mod tests { .find(|c| c.name == "seq") .unwrap(); assert_eq!(seq.user_type, Some(Type::Serial)); - assert!(!result.client_side_notes.is_empty(), "auto-fill note expected"); + assert!( + !result.client_side_notes.is_empty(), + "auto-fill note expected" + ); assert!( result.client_side_notes[0].contains("3 row(s) given auto-generated serial"), "unexpected note: {:?}", @@ -11702,7 +11666,11 @@ mod tests { let db = db(); make_table_with_n_rows(&db, "T", 3).await; let result = db - .add_column("T".to_string(), ColumnSpec::new("tag".to_string(), Type::ShortId), None) + .add_column( + "T".to_string(), + ColumnSpec::new("tag".to_string(), Type::ShortId), + None, + ) .await .unwrap(); let tag = result @@ -11712,7 +11680,10 @@ mod tests { .find(|c| c.name == "tag") .unwrap(); assert_eq!(tag.user_type, Some(Type::ShortId)); - assert!(!result.client_side_notes.is_empty(), "auto-fill note expected"); + assert!( + !result.client_side_notes.is_empty(), + "auto-fill note expected" + ); assert!( result.client_side_notes[0].contains("3 row(s) given auto-generated shortid"), "unexpected note: {:?}", @@ -11737,9 +11708,13 @@ mod tests { // columns continue to use SQLite's rowid alias. let db = db(); make_table_with_n_rows(&db, "T", 0).await; - db.add_column("T".to_string(), ColumnSpec::new("seq".to_string(), Type::Serial), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("seq".to_string(), Type::Serial), + None, + ) + .await + .unwrap(); // Insert three rows providing only `Name`. The seq // column should auto-fill 1, 2, 3. for n in ["a", "b", "c"] { @@ -11770,14 +11745,21 @@ mod tests { // accepted (ADR-0018 Resolution 2). let db = db(); make_table_with_n_rows(&db, "T", 0).await; - db.add_column("T".to_string(), ColumnSpec::new("seq".to_string(), Type::Serial), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("seq".to_string(), Type::Serial), + None, + ) + .await + .unwrap(); // Insert with explicit seq=100. db.insert( "T".to_string(), Some(vec!["Name".to_string(), "seq".to_string()]), - vec![Value::Text("a".to_string()), Value::Number("100".to_string())], + vec![ + Value::Text("a".to_string()), + Value::Number("100".to_string()), + ], None, ) .await @@ -11809,9 +11791,13 @@ mod tests { // confirming the engine refuses. let db = db(); make_table_with_n_rows(&db, "T", 2).await; - db.add_column("T".to_string(), ColumnSpec::new("seq".to_string(), Type::Serial), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("seq".to_string(), Type::Serial), + None, + ) + .await + .unwrap(); // Attempt to UPDATE one row to have the same `seq` value // as the other — should violate UNIQUE. let err = db @@ -11842,7 +11828,8 @@ mod tests { "T".to_string(), vec![col("id", Type::Serial)], vec!["id".to_string()], - None) + None, + ) .await .unwrap_err(); match err { @@ -11865,7 +11852,11 @@ mod tests { async fn add_column_to_missing_table_returns_no_such_table() { let db = db(); let err = db - .add_column("Ghost".to_string(), ColumnSpec::new("x".to_string(), Type::Text), None) + .add_column( + "Ghost".to_string(), + ColumnSpec::new("x".to_string(), Type::Text), + None, + ) .await .unwrap_err(); match err { @@ -11880,9 +11871,13 @@ mod tests { async fn drop_column_removes_column_and_data() { let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("Score".to_string(), Type::Int), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("Score".to_string(), Type::Int), + None, + ) + .await + .unwrap(); db.insert( "T".to_string(), None, @@ -11930,7 +11925,11 @@ mod tests { // Customers(id PK) ← Orders(cust_id FK) make_id_table(&db, "Customers").await; make_id_table(&db, "Orders").await; - db.add_column("Orders".to_string(), ColumnSpec::new("cust_id".to_string(), Type::Int), None) + db.add_column( + "Orders".to_string(), + ColumnSpec::new("cust_id".to_string(), Type::Int), + None, + ) .await .unwrap(); db.add_relationship( @@ -11975,9 +11974,13 @@ mod tests { /// something indexable. async fn make_indexable_table(db: &Database, name: &str) { make_id_table(db, name).await; - db.add_column(name.to_string(), ColumnSpec::new("Email".to_string(), Type::Text), None) - .await - .expect("add Email column"); + db.add_column( + name.to_string(), + ColumnSpec::new("Email".to_string(), Type::Text), + None, + ) + .await + .expect("add Email column"); } #[tokio::test] @@ -12003,7 +12006,12 @@ mod tests { let db = db(); make_indexable_table(&db, "Customers").await; let desc = db - .add_index(None, "Customers".to_string(), vec!["Email".to_string()], None) + .add_index( + None, + "Customers".to_string(), + vec!["Email".to_string()], + None, + ) .await .expect("add index"); assert_eq!(desc.indexes[0].name, "Customers_Email_idx"); @@ -12013,12 +12021,20 @@ mod tests { async fn add_index_composite_auto_name_joins_columns() { let db = db(); make_id_table(&db, "Orders").await; - db.add_column("Orders".to_string(), ColumnSpec::new("CustId".to_string(), Type::Int), None) - .await - .unwrap(); - db.add_column("Orders".to_string(), ColumnSpec::new("Day".to_string(), Type::Date), None) - .await - .unwrap(); + db.add_column( + "Orders".to_string(), + ColumnSpec::new("CustId".to_string(), Type::Int), + None, + ) + .await + .unwrap(); + db.add_column( + "Orders".to_string(), + ColumnSpec::new("Day".to_string(), Type::Date), + None, + ) + .await + .unwrap(); let desc = db .add_index( None, @@ -12039,9 +12055,13 @@ mod tests { async fn add_index_rejects_duplicate_name() { let db = db(); make_indexable_table(&db, "Customers").await; - db.add_column("Customers".to_string(), ColumnSpec::new("Nick".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + "Customers".to_string(), + ColumnSpec::new("Nick".to_string(), Type::Text), + None, + ) + .await + .unwrap(); db.add_index( Some("idx".to_string()), "Customers".to_string(), @@ -12091,7 +12111,12 @@ mod tests { let db = db(); make_indexable_table(&db, "Customers").await; let err = db - .add_index(None, "Customers".to_string(), vec!["Ghost".to_string()], None) + .add_index( + None, + "Customers".to_string(), + vec!["Ghost".to_string()], + None, + ) .await .unwrap_err(); assert!( @@ -12153,9 +12178,14 @@ mod tests { async fn drop_index_by_columns_removes_it() { let db = db(); make_indexable_table(&db, "Customers").await; - db.add_index(None, "Customers".to_string(), vec!["Email".to_string()], None) - .await - .unwrap(); + db.add_index( + None, + "Customers".to_string(), + vec!["Email".to_string()], + None, + ) + .await + .unwrap(); let desc = db .drop_index( IndexSelector::Columns { @@ -12223,13 +12253,7 @@ mod tests { .expect("drop column --cascade"); assert_eq!(result.dropped_indexes, vec!["idx_email".to_string()]); assert!(result.description.indexes.is_empty()); - assert!( - result - .description - .columns - .iter() - .all(|c| c.name != "Email"), - ); + assert!(result.description.columns.iter().all(|c| c.name != "Email"),); } #[tokio::test] @@ -12238,9 +12262,13 @@ mod tests { // unrelated column must survive the rebuild (ADR-0025). let db = db(); make_indexable_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("Score".to_string(), Type::Int), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("Score".to_string(), Type::Int), + None, + ) + .await + .unwrap(); db.add_index( Some("idx_email".to_string()), "T".to_string(), @@ -12271,9 +12299,13 @@ mod tests { async fn rename_column_updates_schema_and_metadata() { let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("Old".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("Old".to_string(), Type::Text), + None, + ) + .await + .unwrap(); let desc = db .rename_column("T".to_string(), "Old".to_string(), "New".to_string(), None) .await @@ -12290,7 +12322,11 @@ mod tests { let db = db(); make_id_table(&db, "Customers").await; make_id_table(&db, "Orders").await; - db.add_column("Orders".to_string(), ColumnSpec::new("cust_id".to_string(), Type::Int), None) + db.add_column( + "Orders".to_string(), + ColumnSpec::new("cust_id".to_string(), Type::Int), + None, + ) .await .unwrap(); db.add_relationship( @@ -12316,10 +12352,7 @@ mod tests { ) .await .unwrap(); - let orders = db - .describe_table("Orders".to_string()) - .await - .unwrap(); + let orders = db.describe_table("Orders".to_string()).await.unwrap(); let outbound = orders .outbound_relationships .iter() @@ -12331,10 +12364,7 @@ mod tests { ); // Same from the parent perspective via inbound. - let customers = db - .describe_table("Customers".to_string()) - .await - .unwrap(); + let customers = db.describe_table("Customers".to_string()).await.unwrap(); let inbound = customers .inbound_relationships .iter() @@ -12350,12 +12380,20 @@ mod tests { async fn rename_column_refuses_collision() { let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("A".to_string(), Type::Text), None) - .await - .unwrap(); - db.add_column("T".to_string(), ColumnSpec::new("B".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("A".to_string(), Type::Text), + None, + ) + .await + .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("B".to_string(), Type::Text), + None, + ) + .await + .unwrap(); let err = db .rename_column("T".to_string(), "A".to_string(), "B".to_string(), None) .await @@ -12367,9 +12405,13 @@ mod tests { async fn rename_column_refuses_identity_rename() { let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("A".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("A".to_string(), Type::Text), + None, + ) + .await + .unwrap(); let err = db .rename_column("T".to_string(), "A".to_string(), "A".to_string(), None) .await @@ -12381,9 +12423,13 @@ mod tests { async fn change_column_type_works_for_compatible_data() { let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("Score".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("Score".to_string(), Type::Text), + None, + ) + .await + .unwrap(); // Insert numeric-looking strings. for v in ["1", "2", "3"] { db.insert( @@ -12450,7 +12496,11 @@ mod tests { let db = db(); make_id_table(&db, "Customers").await; make_id_table(&db, "Orders").await; - db.add_column("Orders".to_string(), ColumnSpec::new("cust_id".to_string(), Type::Int), None) + db.add_column( + "Orders".to_string(), + ColumnSpec::new("cust_id".to_string(), Type::Int), + None, + ) .await .unwrap(); db.add_relationship( @@ -12494,7 +12544,11 @@ mod tests { let db = db(); make_id_table(&db, "Customers").await; make_id_table(&db, "Orders").await; - db.add_column("Orders".to_string(), ColumnSpec::new("cust_id".to_string(), Type::Int), None) + db.add_column( + "Orders".to_string(), + ColumnSpec::new("cust_id".to_string(), Type::Int), + None, + ) .await .unwrap(); db.add_relationship( @@ -12530,9 +12584,13 @@ mod tests { // table. let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("Score".to_string(), Type::Real), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("Score".to_string(), Type::Real), + None, + ) + .await + .unwrap(); for v in ["3.14", "2.71"] { db.insert( "T".to_string(), @@ -12581,9 +12639,13 @@ mod tests { // the lossy count. let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("Score".to_string(), Type::Real), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("Score".to_string(), Type::Real), + None, + ) + .await + .unwrap(); for v in ["3.14", "2.71", "5.0"] { db.insert( "T".to_string(), @@ -12616,9 +12678,13 @@ mod tests { // does NOT help (per ADR-0017 §5 / §2 step 3). let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("Note".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("Note".to_string(), Type::Text), + None, + ) + .await + .unwrap(); for v in ["abc", "123", "xyz"] { db.insert( "T".to_string(), @@ -12631,13 +12697,7 @@ mod tests { } for mode in [ChangeColumnMode::Default, ChangeColumnMode::ForceConversion] { let err = db - .change_column_type( - "T".to_string(), - "Note".to_string(), - Type::Int, - mode, - None, - ) + .change_column_type("T".to_string(), "Note".to_string(), Type::Int, mode, None) .await .unwrap_err(); match err { @@ -12665,9 +12725,13 @@ mod tests { // [client-side] note is expected. let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("Flag".to_string(), Type::Int), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("Flag".to_string(), Type::Int), + None, + ) + .await + .unwrap(); for v in ["0", "1", "0"] { db.insert( "T".to_string(), @@ -12713,9 +12777,13 @@ mod tests { // help (incompatible is not lossy). let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("Flag".to_string(), Type::Int), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("Flag".to_string(), Type::Int), + None, + ) + .await + .unwrap(); for v in ["0", "1", "2"] { db.insert( "T".to_string(), @@ -12728,13 +12796,7 @@ mod tests { } for mode in [ChangeColumnMode::Default, ChangeColumnMode::ForceConversion] { let err = db - .change_column_type( - "T".to_string(), - "Flag".to_string(), - Type::Bool, - mode, - None, - ) + .change_column_type("T".to_string(), "Flag".to_string(), Type::Bool, mode, None) .await .unwrap_err(); match err { @@ -12765,9 +12827,13 @@ mod tests { // engine coercion; the note is suppressed. let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("Score".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("Score".to_string(), Type::Text), + None, + ) + .await + .unwrap(); for v in ["1", "2", "3"] { db.insert( "T".to_string(), @@ -12850,9 +12916,13 @@ mod tests { async fn change_column_type_blob_target_refused_statically() { let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("Note".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("Note".to_string(), Type::Text), + None, + ) + .await + .unwrap(); let err = db .change_column_type( "T".to_string(), @@ -12873,7 +12943,11 @@ mod tests { let db = db(); make_id_table(&db, "Customers").await; make_id_table(&db, "Orders").await; - db.add_column("Orders".to_string(), ColumnSpec::new("cust_id".to_string(), Type::Int), None) + db.add_column( + "Orders".to_string(), + ColumnSpec::new("cust_id".to_string(), Type::Int), + None, + ) .await .unwrap(); db.add_relationship( @@ -12916,9 +12990,13 @@ mod tests { // note, and the structural change goes through. let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("Note".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("Note".to_string(), Type::Text), + None, + ) + .await + .unwrap(); let result = db .change_column_type( "T".to_string(), @@ -12949,9 +13027,13 @@ mod tests { // existing values are preserved. let db = db(); make_table_with_n_rows(&db, "T", 0).await; - db.add_column("T".to_string(), ColumnSpec::new("code".to_string(), Type::Int), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("code".to_string(), Type::Int), + None, + ) + .await + .unwrap(); // Insert a few rows with explicit code values. for (i, code) in [(1, 10), (2, 20), (3, 30)] { db.insert( @@ -12999,15 +13081,22 @@ mod tests { // uniqueness-collision diagnostic. let db = db(); make_table_with_n_rows(&db, "T", 0).await; - db.add_column("T".to_string(), ColumnSpec::new("code".to_string(), Type::Int), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("code".to_string(), Type::Int), + None, + ) + .await + .unwrap(); // Two rows with the same code. for i in 0..2 { db.insert( "T".to_string(), Some(vec!["Name".to_string(), "code".to_string()]), - vec![Value::Text(format!("row{i}")), Value::Number("7".to_string())], + vec![ + Value::Text(format!("row{i}")), + Value::Number("7".to_string()), + ], None, ) .await @@ -13041,9 +13130,13 @@ mod tests { // to route through int. let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("A".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("A".to_string(), Type::Text), + None, + ) + .await + .unwrap(); let err = db .change_column_type( "T".to_string(), @@ -13072,9 +13165,13 @@ mod tests { // [client-side] note reports the auto-fill count. let db = db(); make_table_with_n_rows(&db, "T", 0).await; - db.add_column("T".to_string(), ColumnSpec::new("code".to_string(), Type::Int), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("code".to_string(), Type::Int), + None, + ) + .await + .unwrap(); // Three rows: one with code=5, two with NULL. db.insert( "T".to_string(), @@ -13128,9 +13225,13 @@ mod tests { // text column get fresh shortids (ADR-0018 §3). let db = db(); make_table_with_n_rows(&db, "T", 0).await; - db.add_column("T".to_string(), ColumnSpec::new("tag".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("tag".to_string(), Type::Text), + None, + ) + .await + .unwrap(); // One row with a valid shortid value, two with NULL. db.insert( "T".to_string(), @@ -13182,7 +13283,11 @@ mod tests { let db = db(); make_id_table(&db, "Customers").await; make_id_table(&db, "Orders").await; - db.add_column("Orders".to_string(), ColumnSpec::new("cust_id".to_string(), Type::Int), None) + db.add_column( + "Orders".to_string(), + ColumnSpec::new("cust_id".to_string(), Type::Int), + None, + ) .await .unwrap(); db.add_relationship( @@ -13223,11 +13328,21 @@ mod tests { async fn change_column_type_no_op_to_same_type_errors() { let db = db(); make_id_table(&db, "T").await; - db.add_column("T".to_string(), ColumnSpec::new("A".to_string(), Type::Int), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("A".to_string(), Type::Int), + None, + ) + .await + .unwrap(); let err = db - .change_column_type("T".to_string(), "A".to_string(), Type::Int, ChangeColumnMode::Default, None) + .change_column_type( + "T".to_string(), + "A".to_string(), + Type::Int, + ChangeColumnMode::Default, + None, + ) .await .unwrap_err(); assert!(matches!(err, DbError::Unsupported(_)), "got {err:?}"); @@ -13250,19 +13365,25 @@ mod tests { "Customers".to_string(), vec![col("id", Type::Serial), col("Name", Type::Text)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); db.create_table( "Orders".to_string(), vec![col("id", Type::Serial)], vec!["id".to_string()], - None) + None, + ) + .await + .unwrap(); + db.add_column( + "Orders".to_string(), + ColumnSpec::new("CustId".to_string(), Type::Int), + None, + ) .await .unwrap(); - db.add_column("Orders".to_string(), ColumnSpec::new("CustId".to_string(), Type::Int), None) - .await - .unwrap(); } #[tokio::test] @@ -13278,7 +13399,8 @@ mod tests { ReferentialAction::NoAction, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap(); let orders = db.describe_table("Orders".to_string()).await.unwrap(); @@ -13303,7 +13425,8 @@ mod tests { ReferentialAction::NoAction, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap(); let customers = db.describe_table("Customers".to_string()).await.unwrap(); @@ -13327,7 +13450,8 @@ mod tests { ReferentialAction::Cascade, ReferentialAction::SetNull, false, - None) + None, + ) .await .unwrap(); let orders = db.describe_table("Orders".to_string()).await.unwrap(); @@ -13345,14 +13469,16 @@ mod tests { "Customers".to_string(), vec![col("id", Type::Serial)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); db.create_table( "Orders".to_string(), vec![col("id", Type::Serial)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); @@ -13388,14 +13514,16 @@ mod tests { "Customers".to_string(), vec![col("id", Type::Serial)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); db.create_table( "Orders".to_string(), vec![col("id", Type::Serial)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); let err = db @@ -13408,7 +13536,8 @@ mod tests { ReferentialAction::NoAction, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap_err(); assert!(matches!(err, DbError::Unsupported(_)), "got {err:?}"); @@ -13421,20 +13550,26 @@ mod tests { "Customers".to_string(), vec![col("id", Type::Serial)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); db.create_table( "Orders".to_string(), vec![col("id", Type::Serial)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); // Wrong type — text instead of int. - db.add_column("Orders".to_string(), ColumnSpec::new("CustId".to_string(), Type::Text), None) - .await - .unwrap(); + db.add_column( + "Orders".to_string(), + ColumnSpec::new("CustId".to_string(), Type::Text), + None, + ) + .await + .unwrap(); let err = db .add_relationship( @@ -13446,7 +13581,8 @@ mod tests { ReferentialAction::NoAction, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap_err(); match err { @@ -13466,19 +13602,25 @@ mod tests { "Customers".to_string(), vec![col("id", Type::Serial), col("Name", Type::Text)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); db.create_table( "Orders".to_string(), vec![col("id", Type::Serial)], vec!["id".to_string()], - None) + None, + ) + .await + .unwrap(); + db.add_column( + "Orders".to_string(), + ColumnSpec::new("CustName".to_string(), Type::Text), + None, + ) .await .unwrap(); - db.add_column("Orders".to_string(), ColumnSpec::new("CustName".to_string(), Type::Text), None) - .await - .unwrap(); let err = db .add_relationship( None, @@ -13489,7 +13631,8 @@ mod tests { ReferentialAction::NoAction, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap_err(); match err { @@ -13511,12 +13654,16 @@ mod tests { ReferentialAction::NoAction, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap(); - db.drop_relationship(RelationshipSelector::Named { - name: "cust_orders".to_string(), - }, None) + db.drop_relationship( + RelationshipSelector::Named { + name: "cust_orders".to_string(), + }, + None, + ) .await .unwrap(); let orders = db.describe_table("Orders".to_string()).await.unwrap(); @@ -13538,15 +13685,19 @@ mod tests { ReferentialAction::NoAction, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap(); - db.drop_relationship(RelationshipSelector::Endpoints { - parent_table: "Customers".to_string(), - parent_column: "id".to_string(), - child_table: "Orders".to_string(), - child_column: "CustId".to_string(), - }, None) + db.drop_relationship( + RelationshipSelector::Endpoints { + parent_table: "Customers".to_string(), + parent_column: "id".to_string(), + child_table: "Orders".to_string(), + child_column: "CustId".to_string(), + }, + None, + ) .await .unwrap(); let orders = db.describe_table("Orders".to_string()).await.unwrap(); @@ -13566,10 +13717,14 @@ mod tests { ReferentialAction::NoAction, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap(); - let err = db.drop_table("Customers".to_string(), None).await.unwrap_err(); + let err = db + .drop_table("Customers".to_string(), None) + .await + .unwrap_err(); match err { DbError::Unsupported(msg) => { assert!(msg.contains("referenced by"), "{msg}"); @@ -13591,7 +13746,8 @@ mod tests { ReferentialAction::NoAction, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap(); // Dropping the child is allowed (no inbound relationships @@ -13605,9 +13761,13 @@ mod tests { async fn add_relationship_with_duplicate_name_errors() { let db = db(); customers_orders_setup(&db).await; - db.add_column("Orders".to_string(), ColumnSpec::new("OtherCust".to_string(), Type::Int), None) - .await - .unwrap(); + db.add_column( + "Orders".to_string(), + ColumnSpec::new("OtherCust".to_string(), Type::Int), + None, + ) + .await + .unwrap(); db.add_relationship( Some("dup".to_string()), "Customers".to_string(), @@ -13617,7 +13777,8 @@ mod tests { ReferentialAction::NoAction, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap(); let err = db @@ -13630,7 +13791,8 @@ mod tests { ReferentialAction::NoAction, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap_err(); match err { @@ -13657,7 +13819,8 @@ mod tests { ReferentialAction::Cascade, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap(); // After the rebuild, the original columns are still @@ -13677,7 +13840,8 @@ mod tests { "Customers".to_string(), vec![col("id", Type::Serial), col("Name", Type::Text)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); } @@ -13691,14 +13855,18 @@ mod tests { "Customers".to_string(), None, vec![Value::Text("Alice".to_string())], - None) + None, + ) .await .unwrap(); assert_eq!(result.rows_affected, 1); // The InsertResult itself carries the just-inserted row. assert_eq!(result.data.rows.len(), 1); assert_eq!(result.data.rows[0][1], Some("Alice".to_string())); - let data = db.query_data("Customers".to_string(), None, None).await.unwrap(); + let data = db + .query_data("Customers".to_string(), None, None) + .await + .unwrap(); assert_eq!(data.columns, vec!["id".to_string(), "Name".to_string()]); assert_eq!(data.rows.len(), 1); assert_eq!(data.rows[0][1], Some("Alice".to_string())); @@ -13713,14 +13881,16 @@ mod tests { "Tags".to_string(), vec![col("id", Type::ShortId), col("Label", Type::Text)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); db.insert( "Tags".to_string(), None, vec![Value::Text("database".to_string())], - None) + None, + ) .await .unwrap(); let data = db.query_data("Tags".to_string(), None, None).await.unwrap(); @@ -13738,11 +13908,18 @@ mod tests { db.insert( "Customers".to_string(), Some(vec!["id".to_string(), "Name".to_string()]), - vec![Value::Number("99".to_string()), Value::Text("Bob".to_string())], - None) + vec![ + Value::Number("99".to_string()), + Value::Text("Bob".to_string()), + ], + None, + ) .await .unwrap(); - let data = db.query_data("Customers".to_string(), None, None).await.unwrap(); + let data = db + .query_data("Customers".to_string(), None, None) + .await + .unwrap(); assert_eq!(data.rows[0][0], Some("99".to_string())); assert_eq!(data.rows[0][1], Some("Bob".to_string())); } @@ -13757,7 +13934,8 @@ mod tests { "Customers".to_string(), Some(vec!["Name".to_string()]), vec![Value::Number("42".to_string())], - None) + None, + ) .await .unwrap_err(); assert!(matches!(err, DbError::InvalidValue(_)), "got {err:?}"); @@ -13772,7 +13950,8 @@ mod tests { "Customers".to_string(), None, vec![Value::Text(name.to_string())], - None) + None, + ) .await .unwrap(); } @@ -13781,14 +13960,18 @@ mod tests { "Customers".to_string(), vec![("Name".to_string(), Value::Text("Alicia".to_string()))], RowFilter::eq("id", Value::Number("1".to_string())), - None) + None, + ) .await .unwrap(); assert_eq!(result.rows_affected, 1); // The UpdateResult contains only the updated rows. assert_eq!(result.data.rows.len(), 1); assert_eq!(result.data.rows[0][1], Some("Alicia".to_string())); - let data = db.query_data("Customers".to_string(), None, None).await.unwrap(); + let data = db + .query_data("Customers".to_string(), None, None) + .await + .unwrap(); assert_eq!(data.rows[0][1], Some("Alicia".to_string())); assert_eq!(data.rows[1][1], Some("Bob".to_string())); } @@ -13852,9 +14035,7 @@ mod tests { /// `show data` command. fn parse_show(dsl: &str) -> (Option, Option) { match crate::dsl::parser::parse_command(dsl).expect("show parse") { - crate::dsl::command::Command::ShowData { filter, limit, .. } => { - (filter, limit) - } + crate::dsl::command::Command::ShowData { filter, limit, .. } => (filter, limit), other => panic!("expected show data, got {other:?}"), } } @@ -13874,9 +14055,7 @@ mod tests { let result = db .delete( "People".to_string(), - parse_filter( - "delete from People where Age >= 35 and Active = true", - ), + parse_filter("delete from People where Age >= 35 and Active = true"), None, ) .await @@ -13884,7 +14063,10 @@ mod tests { // Carol (45/true) and Dave (35/true) match; Bob (35) is // inactive, Alice (25) is too young. assert_eq!(result.rows_affected, 2); - let data = db.query_data("People".to_string(), None, None).await.unwrap(); + let data = db + .query_data("People".to_string(), None, None) + .await + .unwrap(); assert_eq!(names(&data), vec!["Alice", "Bob"]); } @@ -13901,7 +14083,10 @@ mod tests { .await .unwrap(); assert_eq!(result.rows_affected, 2); - let data = db.query_data("People".to_string(), None, None).await.unwrap(); + let data = db + .query_data("People".to_string(), None, None) + .await + .unwrap(); assert_eq!(names(&data), vec!["Bob", "Dave"]); } @@ -13919,7 +14104,10 @@ mod tests { .await .unwrap(); assert_eq!(result.rows_affected, 2); - let data = db.query_data("People".to_string(), None, None).await.unwrap(); + let data = db + .query_data("People".to_string(), None, None) + .await + .unwrap(); assert_eq!(names(&data), vec!["Bob", "Dave"]); } @@ -13937,7 +14125,10 @@ mod tests { .unwrap(); // Bob (35) and Dave (35) are in range. assert_eq!(result.rows_affected, 2); - let data = db.query_data("People".to_string(), None, None).await.unwrap(); + let data = db + .query_data("People".to_string(), None, None) + .await + .unwrap(); assert_eq!(names(&data), vec!["Alice", "Carol"]); } @@ -13948,15 +14139,16 @@ mod tests { let result = db .delete( "People".to_string(), - parse_filter( - "delete from People where Name in ('Alice', 'Carol')", - ), + parse_filter("delete from People where Name in ('Alice', 'Carol')"), None, ) .await .unwrap(); assert_eq!(result.rows_affected, 2); - let data = db.query_data("People".to_string(), None, None).await.unwrap(); + let data = db + .query_data("People".to_string(), None, None) + .await + .unwrap(); assert_eq!(names(&data), vec!["Bob", "Dave"]); } @@ -13973,7 +14165,10 @@ mod tests { .await .unwrap(); assert_eq!(result.rows_affected, 1, "only Alice matches `A%`"); - let data = db.query_data("People".to_string(), None, None).await.unwrap(); + let data = db + .query_data("People".to_string(), None, None) + .await + .unwrap(); assert_eq!(names(&data), vec!["Bob", "Carol", "Dave"]); } @@ -14028,8 +14223,7 @@ mod tests { async fn query_data_with_where_and_limit_combines_both() { let db = db(); people_table(&db).await; - let (filter, limit) = - parse_show("show data People where Age >= 35 limit 1"); + let (filter, limit) = parse_show("show data People where Age >= 35 limit 1"); let data = db .query_data("People".to_string(), filter, limit) .await @@ -14274,7 +14468,11 @@ mod tests { .await .unwrap(); assert!( - !data.rows.iter().flatten().any(|c| c.as_deref() == Some("Zed")), + !data + .rows + .iter() + .flatten() + .any(|c| c.as_deref() == Some("Zed")), "explain update must not modify rows", ); } @@ -14356,7 +14554,10 @@ mod tests { let result = db .explain_query_plan(parse_inner_adv("select * from NoSuchTable")) .await; - assert!(result.is_err(), "explaining a missing SQL table should fail"); + assert!( + result.is_err(), + "explaining a missing SQL table should fail" + ); } // --- column constraints at create-table (ADR-0029) ------ @@ -14448,7 +14649,13 @@ mod tests { vec![ col("id", Type::Serial), col("name", Type::Text), - col_c("tier", Type::Int, false, false, Some(Value::Number("3".to_string()))), + col_c( + "tier", + Type::Int, + false, + false, + Some(Value::Number("3".to_string())), + ), ], vec!["id".to_string()], None, @@ -14465,10 +14672,7 @@ mod tests { ) .await .unwrap(); - let data = db - .query_data("T".to_string(), None, None) - .await - .unwrap(); + let data = db.query_data("T".to_string(), None, None).await.unwrap(); let tier_idx = data.columns.iter().position(|c| c == "tier").unwrap(); assert_eq!( data.rows[0][tier_idx].as_deref(), @@ -14512,7 +14716,13 @@ mod tests { vec![ col("id", Type::Serial), col_c("email", Type::Text, true, true, None), - col_c("tier", Type::Int, false, false, Some(Value::Number("1".to_string()))), + col_c( + "tier", + Type::Int, + false, + false, + Some(Value::Number("1".to_string())), + ), ], vec!["id".to_string()], None, @@ -14531,7 +14741,10 @@ mod tests { .unwrap(); let desc = db.describe_table("T".to_string()).await.unwrap(); let email = desc.columns.iter().find(|c| c.name == "email").unwrap(); - assert!(email.notnull && email.unique, "email keeps NOT NULL + UNIQUE"); + assert!( + email.notnull && email.unique, + "email keeps NOT NULL + UNIQUE" + ); let tier = desc.columns.iter().find(|c| c.name == "tier").unwrap(); assert_eq!( tier.default.as_deref(), @@ -14548,7 +14761,13 @@ mod tests { people_table(&db).await; // 4 rows db.add_column( "People".to_string(), - col_c("tier", Type::Int, false, false, Some(Value::Number("1".to_string()))), + col_c( + "tier", + Type::Int, + false, + false, + Some(Value::Number("1".to_string())), + ), None, ) .await @@ -14609,7 +14828,13 @@ mod tests { people_table(&db).await; db.add_column( "People".to_string(), - col_c("tier", Type::Int, true, false, Some(Value::Number("0".to_string()))), + col_c( + "tier", + Type::Int, + true, + false, + Some(Value::Number("0".to_string())), + ), None, ) .await @@ -14632,7 +14857,13 @@ mod tests { .await .expect("a UNIQUE column with no default is fine — NULLs do not collide"); let desc = db.describe_table("People".to_string()).await.unwrap(); - assert!(desc.columns.iter().find(|c| c.name == "badge").unwrap().unique); + assert!( + desc.columns + .iter() + .find(|c| c.name == "badge") + .unwrap() + .unique + ); } #[tokio::test] @@ -14642,7 +14873,13 @@ mod tests { let result = db .add_column( "People".to_string(), - col_c("badge", Type::Text, false, true, Some(Value::Text("X".to_string()))), + col_c( + "badge", + Type::Text, + false, + true, + Some(Value::Text("X".to_string())), + ), None, ) .await; @@ -14659,7 +14896,13 @@ mod tests { let result = db .add_column( "People".to_string(), - col_c("seq", Type::Serial, false, false, Some(Value::Number("1".to_string()))), + col_c( + "seq", + Type::Serial, + false, + false, + Some(Value::Number("1".to_string())), + ), None, ) .await; @@ -14719,8 +14962,7 @@ mod tests { #[tokio::test] async fn describe_surfaces_the_check_constraint() { let db = db(); - let (n, c, pk) = - parse_create("create table T with pk age(int) check (age >= 0)"); + let (n, c, pk) = parse_create("create table T with pk age(int) check (age >= 0)"); db.create_table(n, c, pk, None).await.unwrap(); let desc = db.describe_table("T".to_string()).await.unwrap(); let age = desc.columns.iter().find(|c| c.name == "age").unwrap(); @@ -14743,7 +14985,14 @@ mod tests { .await .expect("a CHECK column adds via the rebuild path"); let desc = db.describe_table("People".to_string()).await.unwrap(); - assert!(desc.columns.iter().find(|c| c.name == "score").unwrap().check.is_some()); + assert!( + desc.columns + .iter() + .find(|c| c.name == "score") + .unwrap() + .check + .is_some() + ); // An update that violates the check is refused. let bad = db .update( @@ -14759,8 +15008,7 @@ mod tests { #[tokio::test] async fn rebuild_preserves_a_check_constraint() { let db = db(); - let (n, c, pk) = - parse_create("create table T with pk code(text) check (code like 'X%')"); + let (n, c, pk) = parse_create("create table T with pk code(text) check (code like 'X%')"); db.create_table(n, c, pk, None).await.unwrap(); db.add_column("T".to_string(), col("note", Type::Int), None) .await @@ -14778,7 +15026,12 @@ mod tests { .unwrap(); let desc = db.describe_table("T".to_string()).await.unwrap(); assert!( - desc.columns.iter().find(|c| c.name == "code").unwrap().check.is_some(), + desc.columns + .iter() + .find(|c| c.name == "code") + .unwrap() + .check + .is_some(), "code keeps its CHECK across the rebuild", ); } @@ -14809,7 +15062,8 @@ mod tests { "Customers".to_string(), None, vec![Value::Text(name.to_string())], - None) + None, + ) .await .unwrap(); } @@ -14818,7 +15072,8 @@ mod tests { "Customers".to_string(), vec![("Name".to_string(), Value::Text("X".to_string()))], RowFilter::AllRows, - None) + None, + ) .await .unwrap(); assert_eq!(result.rows_affected, 3); @@ -14834,7 +15089,8 @@ mod tests { "Customers".to_string(), None, vec![Value::Text(name.to_string())], - None) + None, + ) .await .unwrap(); } @@ -14842,12 +15098,16 @@ mod tests { .delete( "Customers".to_string(), RowFilter::eq("id", Value::Number("1".to_string())), - None) + None, + ) .await .unwrap(); assert_eq!(result.rows_affected, 1); assert!(result.cascade.is_empty(), "no children to cascade to"); - let data = db.query_data("Customers".to_string(), None, None).await.unwrap(); + let data = db + .query_data("Customers".to_string(), None, None) + .await + .unwrap(); assert_eq!(data.rows.len(), 1); assert_eq!(data.rows[0][1], Some("Bob".to_string())); } @@ -14875,7 +15135,8 @@ mod tests { "Orders".to_string(), vec![col("id", Type::Serial), col("CustId", Type::Int)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); db.add_relationship( @@ -14887,7 +15148,8 @@ mod tests { ReferentialAction::NoAction, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap(); @@ -14896,7 +15158,8 @@ mod tests { "Orders".to_string(), Some(vec!["CustId".to_string()]), vec![Value::Number("999".to_string())], - None) + None, + ) .await .unwrap_err(); match err { @@ -14918,7 +15181,8 @@ mod tests { "Orders".to_string(), vec![col("id", Type::Serial), col("CustId", Type::Int)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); db.add_relationship( @@ -14930,31 +15194,38 @@ mod tests { ReferentialAction::Cascade, ReferentialAction::NoAction, false, - None) + None, + ) .await .unwrap(); db.insert( "Customers".to_string(), None, vec![Value::Text("Alice".to_string())], - None) + None, + ) .await .unwrap(); db.insert( "Orders".to_string(), Some(vec!["CustId".to_string()]), vec![Value::Number("1".to_string())], - None) + None, + ) .await .unwrap(); // Delete Alice — cascades to Orders. db.delete( "Customers".to_string(), RowFilter::eq("id", Value::Number("1".to_string())), - None) + None, + ) .await .unwrap(); - let orders = db.query_data("Orders".to_string(), None, None).await.unwrap(); + let orders = db + .query_data("Orders".to_string(), None, None) + .await + .unwrap(); assert!(orders.rows.is_empty(), "child rows should be cascaded"); } @@ -15001,7 +15272,10 @@ mod tests { db.insert( "T".to_string(), Some(vec!["id".to_string(), "ParentId".to_string()]), - vec![Value::Number("2".to_string()), Value::Number("1".to_string())], + vec![ + Value::Number("2".to_string()), + Value::Number("1".to_string()), + ], None, ) .await @@ -15009,7 +15283,10 @@ mod tests { db.insert( "T".to_string(), Some(vec!["id".to_string(), "ParentId".to_string()]), - vec![Value::Number("3".to_string()), Value::Number("2".to_string())], + vec![ + Value::Number("3".to_string()), + Value::Number("2".to_string()), + ], None, ) .await @@ -15022,8 +15299,15 @@ mod tests { ) .await .unwrap(); - assert_eq!(result.rows_affected, 1, "one row matched the filter directly"); - assert_eq!(result.cascade.len(), 1, "self-ref relationship reported once"); + assert_eq!( + result.rows_affected, 1, + "one row matched the filter directly" + ); + assert_eq!( + result.cascade.len(), + 1, + "self-ref relationship reported once" + ); assert_eq!( result.cascade[0].rows_changed, 2, "only the 2 cascaded rows, not the directly-deleted root too" @@ -15037,24 +15321,20 @@ mod tests { "Flags".to_string(), vec![col("id", Type::Serial), col("Active", Type::Bool)], vec!["id".to_string()], - None) - .await - .unwrap(); - db.insert( - "Flags".to_string(), None, - vec![Value::Bool(true)], - None) + ) .await .unwrap(); - db.insert( - "Flags".to_string(), - None, - vec![Value::Bool(false)], - None) - .await - .unwrap(); - let data = db.query_data("Flags".to_string(), None, None).await.unwrap(); + db.insert("Flags".to_string(), None, vec![Value::Bool(true)], None) + .await + .unwrap(); + db.insert("Flags".to_string(), None, vec![Value::Bool(false)], None) + .await + .unwrap(); + let data = db + .query_data("Flags".to_string(), None, None) + .await + .unwrap(); assert_eq!(data.rows[0][1], Some("true".to_string())); assert_eq!(data.rows[1][1], Some("false".to_string())); } @@ -15066,16 +15346,13 @@ mod tests { "T".to_string(), vec![col("id", Type::Serial), col("note", Type::Text)], vec!["id".to_string()], - None) - .await - .unwrap(); - db.insert( - "T".to_string(), None, - vec![Value::Null], - None) + ) .await .unwrap(); + db.insert("T".to_string(), None, vec![Value::Null], None) + .await + .unwrap(); let data = db.query_data("T".to_string(), None, None).await.unwrap(); assert_eq!(data.rows[0][1], None); } @@ -15090,10 +15367,8 @@ mod tests { // constraint (C3-track). let conn = Connection::open_in_memory().unwrap(); configure_connection(&conn).unwrap(); - conn.execute_batch( - "CREATE TABLE T (id INTEGER PRIMARY KEY, code TEXT UNIQUE) STRICT;", - ) - .unwrap(); + conn.execute_batch("CREATE TABLE T (id INTEGER PRIMARY KEY, code TEXT UNIQUE) STRICT;") + .unwrap(); let schema = read_schema(&conn, "T").unwrap(); let id = schema.columns.iter().find(|c| c.name == "id").unwrap(); let code = schema.columns.iter().find(|c| c.name == "code").unwrap(); @@ -15204,9 +15479,13 @@ mod tests { ) .await .unwrap(); - db.add_column("T".to_string(), ColumnSpec::new("seq".to_string(), Type::Serial), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("seq".to_string(), Type::Serial), + None, + ) + .await + .unwrap(); // Read the persisted YAML straight from disk. let yaml = std::fs::read_to_string(dir.path().join(PROJECT_YAML)).unwrap(); assert!( @@ -15216,10 +15495,7 @@ mod tests { // PK column not separately marked unique — PK already // implies UNIQUE, double-marking would emit redundant // DDL on rebuild. - let lines_with_id: Vec<&str> = yaml - .lines() - .filter(|l| l.contains("name: id")) - .collect(); + let lines_with_id: Vec<&str> = yaml.lines().filter(|l| l.contains("name: id")).collect(); for line in &lines_with_id { assert!( !line.contains("unique: true"), @@ -15323,9 +15599,13 @@ mod tests { ) .await .unwrap(); - db.add_column("T".to_string(), ColumnSpec::new("seq".to_string(), Type::Serial), None) - .await - .unwrap(); + db.add_column( + "T".to_string(), + ColumnSpec::new("seq".to_string(), Type::Serial), + None, + ) + .await + .unwrap(); // Tear down the .db file and rebuild from yaml + csvs. drop(db); std::fs::remove_file(&db_path).unwrap(); @@ -15382,7 +15662,8 @@ mod tests { "Order Lines".to_string(), vec![col("id", Type::Serial)], vec!["id".to_string()], - None) + None, + ) .await .unwrap(); let tables = db.list_tables().await.unwrap(); @@ -15542,7 +15823,11 @@ mod tests { .await .expect("NOT NULL applies — no row holds a null"); assert!( - desc.columns.iter().find(|c| c.name == "Name").unwrap().notnull, + desc.columns + .iter() + .find(|c| c.name == "Name") + .unwrap() + .notnull, "the column is now NOT NULL", ); } @@ -15552,7 +15837,12 @@ mod tests { let db = db(); people_with_null_column(&db).await; // `x` is null in every row let result = db - .add_constraint("People".to_string(), "x".to_string(), Constraint::NotNull, None) + .add_constraint( + "People".to_string(), + "x".to_string(), + Constraint::NotNull, + None, + ) .await; let err = result.expect_err("NOT NULL is refused — rows hold null"); assert!( @@ -15574,7 +15864,13 @@ mod tests { ) .await .expect("UNIQUE applies — every Name is distinct"); - assert!(desc.columns.iter().find(|c| c.name == "Name").unwrap().unique); + assert!( + desc.columns + .iter() + .find(|c| c.name == "Name") + .unwrap() + .unique + ); } #[tokio::test] @@ -15582,7 +15878,12 @@ mod tests { let db = db(); people_table(&db).await; // Age 35 appears for Bob and Dave let result = db - .add_constraint("People".to_string(), "Age".to_string(), Constraint::Unique, None) + .add_constraint( + "People".to_string(), + "Age".to_string(), + Constraint::Unique, + None, + ) .await; assert!( result.is_err(), @@ -15603,7 +15904,14 @@ mod tests { ) .await .expect("the CHECK applies — all rows satisfy it"); - assert!(desc.columns.iter().find(|c| c.name == "Age").unwrap().check.is_some()); + assert!( + desc.columns + .iter() + .find(|c| c.name == "Age") + .unwrap() + .check + .is_some() + ); } #[tokio::test] @@ -15618,7 +15926,10 @@ mod tests { None, ) .await; - assert!(result.is_err(), "the CHECK is refused — three rows are under 40"); + assert!( + result.is_err(), + "the CHECK is refused — three rows are under 40" + ); } #[tokio::test] @@ -15635,7 +15946,12 @@ mod tests { .await .expect("a DEFAULT applies — it never touches existing rows"); assert_eq!( - desc.columns.iter().find(|c| c.name == "Age").unwrap().default.as_deref(), + desc.columns + .iter() + .find(|c| c.name == "Age") + .unwrap() + .default + .as_deref(), Some("0"), ); } @@ -15672,9 +15988,17 @@ mod tests { let db = db(); people_table(&db).await; let result = db - .add_constraint("People".to_string(), "id".to_string(), Constraint::NotNull, None) + .add_constraint( + "People".to_string(), + "id".to_string(), + Constraint::NotNull, + None, + ) .await; - assert!(result.is_err(), "a PK column is already NOT NULL (ADR-0029 §9)"); + assert!( + result.is_err(), + "a PK column is already NOT NULL (ADR-0029 §9)" + ); } #[tokio::test] @@ -15682,7 +16006,12 @@ mod tests { let db = db(); people_table(&db).await; let result = db - .add_constraint("People".to_string(), "id".to_string(), Constraint::Unique, None) + .add_constraint( + "People".to_string(), + "id".to_string(), + Constraint::Unique, + None, + ) .await; assert!( result.is_err(), @@ -15733,7 +16062,12 @@ mod tests { let db = db(); people_table(&db).await; let result = db - .add_constraint("People".to_string(), "ghost".to_string(), Constraint::Unique, None) + .add_constraint( + "People".to_string(), + "ghost".to_string(), + Constraint::Unique, + None, + ) .await; assert!(result.is_err(), "no such column"); } @@ -15760,7 +16094,12 @@ mod tests { .await .expect("the NOT NULL is dropped"); assert!( - !desc.columns.iter().find(|c| c.name == "Name").unwrap().notnull, + !desc + .columns + .iter() + .find(|c| c.name == "Name") + .unwrap() + .notnull, "the column is nullable again", ); } @@ -15787,7 +16126,12 @@ mod tests { .await .expect("the CHECK is dropped"); assert!( - desc.columns.iter().find(|c| c.name == "Age").unwrap().check.is_none(), + desc.columns + .iter() + .find(|c| c.name == "Age") + .unwrap() + .check + .is_none(), "the CHECK is gone from the structure view", ); // With the CHECK gone, a previously-forbidden value inserts. diff --git a/src/dsl/command.rs b/src/dsl/command.rs index db0e23e..c50670f 100644 --- a/src/dsl/command.rs +++ b/src/dsl/command.rs @@ -549,9 +549,7 @@ pub enum AppCommand { /// word like `insert` / `create` / `show`, or `types`), the /// focused detail for that command (or command group sharing /// the entry word). - Help { - topic: Option, - }, + Help { topic: Option }, /// Show a contextual tier-3 hint (H2 / ADR-0053). No argument: /// when submitted, it expands on the most recent runtime error /// (the buffer is empty post-submit). The live-input surface is @@ -580,7 +578,10 @@ pub enum AppCommand { /// Unpack a zip into a new project and switch to it. /// `target` overrides the project name (default: taken from /// the zip). - Import { path: String, target: Option }, + Import { + path: String, + target: Option, + }, /// Switch the persistent input mode. Mode { value: ModeValue }, /// Show or set the messages verbosity. @@ -791,9 +792,7 @@ impl PartialEq for Operand { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Column { name: a, .. }, Self::Column { name: b, .. }) => a == b, - (Self::Literal { value: a, .. }, Self::Literal { value: b, .. }) => { - a == b - } + (Self::Literal { value: a, .. }, Self::Literal { value: b, .. }) => a == b, _ => false, } } @@ -817,7 +816,9 @@ pub enum CompareOp { /// a single row in the metadata table. #[derive(Debug, Clone, PartialEq, Eq)] pub enum RelationshipSelector { - Named { name: String }, + Named { + name: String, + }, Endpoints { parent_table: String, parent_column: String, @@ -1156,9 +1157,7 @@ impl Command { parent_column, child_table, child_column, - } => format!( - "from {parent_table}.{parent_column} to {child_table}.{child_column}" - ), + } => format!("from {parent_table}.{parent_column} to {child_table}.{child_column}"), }, // A constraint command's subject is the dotted // `
.` it acts on (ADR-0029 §2.2). diff --git a/src/dsl/grammar/app.rs b/src/dsl/grammar/app.rs index a4f116f..b5653e0 100644 --- a/src/dsl/grammar/app.rs +++ b/src/dsl/grammar/app.rs @@ -9,8 +9,7 @@ use crate::dsl::command::{AppCommand, Command, CopyScope, MessagesValue, ModeValue}; use crate::dsl::grammar::{ - CommandNode, HintMode, IdentSource, IdentValidator, Node, ValidationError, - Word, + CommandNode, HintMode, IdentSource, IdentValidator, Node, ValidationError, Word, }; use crate::dsl::walker::outcome::{MatchedKind, MatchedPath}; @@ -60,19 +59,16 @@ const IMPORT_TARGET_IDENT: Node = Node::Ident { writes_table: false, writes_column: false, writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; const IMPORT_TARGET: Node = Node::Hinted { mode: HintMode::ForceProse("hint.ambient_typing_name"), inner: &IMPORT_TARGET_IDENT, }; -const IMPORT_AS_TARGET: Node = Node::Seq(&[ - Node::Word(Word::keyword("as")), - IMPORT_TARGET, -]); +const IMPORT_AS_TARGET: Node = Node::Seq(&[Node::Word(Word::keyword("as")), IMPORT_TARGET]); const IMPORT_AS_TARGET_OPT: Node = Node::Optional(&IMPORT_AS_TARGET); const IMPORT_PATH_AND_TARGET: Node = Node::Seq(&[Node::BarePath, IMPORT_AS_TARGET_OPT]); @@ -101,9 +97,9 @@ const MODE_CHOICES: &[Node] = &[ writes_table: false, writes_column: false, writes_user_listed_column: false, - writes_table_alias: false, - writes_cte_name: false, - writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, ]; const MODE_VALUE: Node = Node::Choice(MODE_CHOICES); @@ -119,9 +115,9 @@ const MESSAGES_CHOICES: &[Node] = &[ writes_table: false, writes_column: false, writes_user_listed_column: false, - writes_table_alias: false, - writes_cte_name: false, - writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, ]; const MESSAGES_VALUE: Node = Node::Choice(MESSAGES_CHOICES); @@ -271,7 +267,8 @@ pub static QUIT: CommandNode = CommandNode { ast_builder: build_quit, help_id: Some("app.quit"), hint_ids: &["quit"], - usage_ids: &["parse.usage.quit"],}; + usage_ids: &["parse.usage.quit"], +}; pub static HELP: CommandNode = CommandNode { entry: Word::keyword("help"), @@ -279,7 +276,8 @@ pub static HELP: CommandNode = CommandNode { ast_builder: build_help, help_id: Some("app.help"), hint_ids: &["help"], - usage_ids: &["parse.usage.help"],}; + usage_ids: &["parse.usage.help"], +}; pub static HINT: CommandNode = CommandNode { entry: Word::keyword("hint"), @@ -288,7 +286,8 @@ pub static HINT: CommandNode = CommandNode { help_id: Some("app.hint"), // hint_id assigned in Phase C with the tier-3 corpus (ADR-0053). hint_ids: &["hint"], - usage_ids: &["parse.usage.hint"],}; + usage_ids: &["parse.usage.hint"], +}; pub static REBUILD: CommandNode = CommandNode { entry: Word::keyword("rebuild"), @@ -296,7 +295,8 @@ pub static REBUILD: CommandNode = CommandNode { ast_builder: build_rebuild, help_id: Some("app.rebuild"), hint_ids: &["rebuild"], - usage_ids: &["parse.usage.rebuild"],}; + usage_ids: &["parse.usage.rebuild"], +}; pub static VERSION: CommandNode = CommandNode { entry: Word::keyword("version"), @@ -304,7 +304,8 @@ pub static VERSION: CommandNode = CommandNode { ast_builder: build_version, help_id: Some("app.version"), hint_ids: &["version"], - usage_ids: &["parse.usage.version"],}; + usage_ids: &["parse.usage.version"], +}; pub static SAVE: CommandNode = CommandNode { entry: Word::keyword("save"), @@ -312,7 +313,8 @@ pub static SAVE: CommandNode = CommandNode { ast_builder: build_save, help_id: Some("app.save"), hint_ids: &["save"], - usage_ids: &["parse.usage.save"],}; + usage_ids: &["parse.usage.save"], +}; pub static NEW: CommandNode = CommandNode { entry: Word::keyword("new"), @@ -320,7 +322,8 @@ pub static NEW: CommandNode = CommandNode { ast_builder: build_new, help_id: Some("app.new"), hint_ids: &["new"], - usage_ids: &["parse.usage.new"],}; + usage_ids: &["parse.usage.new"], +}; pub static LOAD: CommandNode = CommandNode { entry: Word::keyword("load"), @@ -328,7 +331,8 @@ pub static LOAD: CommandNode = CommandNode { ast_builder: build_load, help_id: Some("app.load"), hint_ids: &["load"], - usage_ids: &["parse.usage.load"],}; + usage_ids: &["parse.usage.load"], +}; pub static EXPORT: CommandNode = CommandNode { entry: Word::keyword("export"), @@ -336,7 +340,8 @@ pub static EXPORT: CommandNode = CommandNode { ast_builder: build_export, help_id: Some("app.export"), hint_ids: &["export"], - usage_ids: &["parse.usage.export"],}; + usage_ids: &["parse.usage.export"], +}; pub static IMPORT: CommandNode = CommandNode { entry: Word::keyword("import"), @@ -344,7 +349,8 @@ pub static IMPORT: CommandNode = CommandNode { ast_builder: build_import, help_id: Some("app.import"), hint_ids: &["import"], - usage_ids: &["parse.usage.import"],}; + usage_ids: &["parse.usage.import"], +}; pub static MODE: CommandNode = CommandNode { entry: Word::keyword("mode"), @@ -352,7 +358,8 @@ pub static MODE: CommandNode = CommandNode { ast_builder: build_mode, help_id: Some("app.mode"), hint_ids: &["mode"], - usage_ids: &["parse.usage.mode"],}; + usage_ids: &["parse.usage.mode"], +}; pub static MESSAGES: CommandNode = CommandNode { entry: Word::keyword("messages"), @@ -360,7 +367,8 @@ pub static MESSAGES: CommandNode = CommandNode { ast_builder: build_messages, help_id: Some("app.messages"), hint_ids: &["messages"], - usage_ids: &["parse.usage.messages"],}; + usage_ids: &["parse.usage.messages"], +}; pub static UNDO: CommandNode = CommandNode { entry: Word::keyword("undo"), @@ -368,7 +376,8 @@ pub static UNDO: CommandNode = CommandNode { ast_builder: build_undo, help_id: Some("app.undo"), hint_ids: &["undo"], - usage_ids: &["parse.usage.undo"],}; + usage_ids: &["parse.usage.undo"], +}; pub static REDO: CommandNode = CommandNode { entry: Word::keyword("redo"), @@ -376,7 +385,8 @@ pub static REDO: CommandNode = CommandNode { ast_builder: build_redo, help_id: Some("app.redo"), hint_ids: &["redo"], - usage_ids: &["parse.usage.redo"],}; + usage_ids: &["parse.usage.redo"], +}; pub static COPY: CommandNode = CommandNode { entry: Word::keyword("copy"), @@ -384,4 +394,5 @@ pub static COPY: CommandNode = CommandNode { ast_builder: build_copy, help_id: Some("app.copy"), hint_ids: &["copy"], - usage_ids: &["parse.usage.copy"],}; + usage_ids: &["parse.usage.copy"], +}; diff --git a/src/dsl/grammar/data.rs b/src/dsl/grammar/data.rs index 6441e24..7e17eca 100644 --- a/src/dsl/grammar/data.rs +++ b/src/dsl/grammar/data.rs @@ -24,19 +24,17 @@ //! later swap that capture for the same typed slots used here, adding //! live hints/highlighting. -use crate::dsl::command::{ - Command, Expr, RowFilter, SeedOverride, SeedOverrideKind, ShowListKind, -}; +use crate::dsl::command::{Command, Expr, RowFilter, SeedOverride, SeedOverrideKind, ShowListKind}; use crate::dsl::grammar::{ CommandNode, IdentSource, Node, NumberValidator, ValidationError, Word, expr, shared::{ - FALLBACK_VALUE_LIST, column_value_list, count_tuple_values, - current_column_value, insert_target_columns, + FALLBACK_VALUE_LIST, column_value_list, count_tuple_values, current_column_value, + insert_target_columns, }, sql_delete, sql_insert, sql_select, sql_update, }; -use crate::dsl::walker::context::WalkContext; use crate::dsl::value::Value; +use crate::dsl::walker::context::WalkContext; use crate::dsl::walker::outcome::{MatchedItem, MatchedKind, MatchedPath}; // ================================================================= @@ -56,10 +54,10 @@ const TABLE_NAME_EXISTING: Node = Node::Ident { highlight_override: None, writes_table: false, writes_column: false, - writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_user_listed_column: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; /// Table-name slot variant that populates @@ -75,10 +73,10 @@ const TABLE_NAME_INSERT: Node = Node::Ident { highlight_override: None, writes_table: true, writes_column: false, - writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_user_listed_column: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; // ================================================================= @@ -95,10 +93,7 @@ const SHOW_DATA_NODES: &[Node] = &[ ]; const SHOW_DATA: Node = Node::Seq(SHOW_DATA_NODES); -const SHOW_TABLE_NODES: &[Node] = &[ - Node::Word(Word::keyword("table")), - TABLE_NAME_EXISTING, -]; +const SHOW_TABLE_NODES: &[Node] = &[Node::Word(Word::keyword("table")), TABLE_NAME_EXISTING]; const SHOW_TABLE: Node = Node::Seq(SHOW_TABLE_NODES); // `show tables` / `show relationships` / `show indexes` — the @@ -144,8 +139,7 @@ const SHOW_INDEX_NAME: Node = Node::Ident { writes_cte_name: false, writes_projection_alias: false, }; -const SHOW_INDEX_NODES: &[Node] = - &[Node::Word(Word::keyword("index")), SHOW_INDEX_NAME]; +const SHOW_INDEX_NODES: &[Node] = &[Node::Word(Word::keyword("index")), SHOW_INDEX_NAME]; const SHOW_INDEX: Node = Node::Seq(SHOW_INDEX_NODES); const SHOW_CHOICES: &[Node] = &[ @@ -192,9 +186,9 @@ static FORM_A_COLUMN: Node = Node::Ident { writes_table: false, writes_column: false, writes_user_listed_column: true, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; static INSERT_COMMA: Node = Node::Punct(','); @@ -224,8 +218,7 @@ fn insert_first_paren(ctx: &WalkContext, source: &str, pos: usize) -> Node { /// or an identifier-shaped token (a column name) returns false. fn first_paren_item_is_value_literal(source: &str, pos: usize) -> bool { use crate::dsl::walker::lex_helpers::{ - consume_ident, consume_number_literal, consume_string_literal, - skip_whitespace, + consume_ident, consume_number_literal, consume_string_literal, skip_whitespace, }; let p = skip_whitespace(source, pos); if p >= source.len() { @@ -281,7 +274,11 @@ fn dsl_insert_value_list(ctx: &WalkContext, source: &str, pos: usize) -> Node { return FALLBACK_VALUE_LIST; }; let (count, closed) = count_tuple_values(source, pos); - let arity_ok = if closed { count == cols.len() } else { count <= cols.len() }; + let arity_ok = if closed { + count == cols.len() + } else { + count <= cols.len() + }; if arity_ok { Node::DynamicSubgrammar(column_value_list) } else { @@ -320,8 +317,7 @@ const INSERT_VALUES_KEYWORD_FIRST_NODES: &[Node] = &[ ]; const INSERT_VALUES_KEYWORD_FIRST: Node = Node::Seq(INSERT_VALUES_KEYWORD_FIRST_NODES); -const INSERT_AFTER_TABLE_CHOICES: &[Node] = - &[INSERT_VALUES_KEYWORD_FIRST, INSERT_PAREN_FIRST]; +const INSERT_AFTER_TABLE_CHOICES: &[Node] = &[INSERT_VALUES_KEYWORD_FIRST, INSERT_PAREN_FIRST]; const INSERT_AFTER_TABLE: Node = Node::Choice(INSERT_AFTER_TABLE_CHOICES); const INSERT_NODES: &[Node] = &[ @@ -349,10 +345,10 @@ const TABLE_NAME_WRITES: Node = Node::Ident { highlight_override: None, writes_table: true, writes_column: false, - writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_user_listed_column: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; /// Column-name slot in `set col = …` — resolves the column's @@ -366,9 +362,9 @@ const SET_COLUMN: Node = Node::Ident { writes_table: false, writes_column: true, writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; /// Value slot resolved at walk time from @@ -376,11 +372,7 @@ writes_projection_alias: false, /// value-literal choice when no current_column is bound. const PER_COLUMN_VALUE: Node = Node::DynamicSubgrammar(current_column_value); -const UPDATE_ASSIGNMENT_NODES: &[Node] = &[ - SET_COLUMN, - Node::Punct('='), - PER_COLUMN_VALUE, -]; +const UPDATE_ASSIGNMENT_NODES: &[Node] = &[SET_COLUMN, Node::Punct('='), PER_COLUMN_VALUE]; const UPDATE_ASSIGNMENT: Node = Node::Seq(UPDATE_ASSIGNMENT_NODES); const UPDATE_ASSIGNMENTS: Node = Node::Repeated { inner: &UPDATE_ASSIGNMENT, @@ -568,8 +560,7 @@ const SEED_OVERRIDES: Node = Node::Repeated { separator: Some(&Node::Punct(',')), min: 1, }; -const SEED_SET_CLAUSE_NODES: &[Node] = - &[Node::Word(Word::keyword("set")), SEED_OVERRIDES]; +const SEED_SET_CLAUSE_NODES: &[Node] = &[Node::Word(Word::keyword("set")), SEED_OVERRIDES]; const SEED_SET_CLAUSE: Node = Node::Seq(SEED_SET_CLAUSE_NODES); const SEED_NODES: &[Node] = &[ @@ -980,7 +971,10 @@ fn parse_seed_override_tail( MatchedKind::Word("in") => { *i += 1; // `in` // `(` - if matches!(region.get(*i).map(|t| &t.kind), Some(MatchedKind::Punct('('))) { + if matches!( + region.get(*i).map(|t| &t.kind), + Some(MatchedKind::Punct('(')) + ) { *i += 1; } let mut values = Vec::new(); @@ -1001,7 +995,10 @@ fn parse_seed_override_tail( MatchedKind::Word("between") => { *i += 1; // `between` let low = seed_take_value(region, i, column)?; - if matches!(region.get(*i).map(|t| &t.kind), Some(MatchedKind::Word("and"))) { + if matches!( + region.get(*i).map(|t| &t.kind), + Some(MatchedKind::Word("and")) + ) { *i += 1; } let high = seed_take_value(region, i, column)?; @@ -1011,7 +1008,15 @@ fn parse_seed_override_tail( *i += 1; // `as` let gen_item = region .get(*i) - .filter(|t| matches!(t.kind, MatchedKind::Ident { role: "seed_generator", .. })) + .filter(|t| { + matches!( + t.kind, + MatchedKind::Ident { + role: "seed_generator", + .. + } + ) + }) .ok_or_else(|| seed_set_error(column))?; *i += 1; Ok(SeedOverrideKind::Generator(gen_item.text.clone())) @@ -1085,7 +1090,15 @@ fn build_insert(path: &MatchedPath, _source: &str) -> Result Result Result Result, ValidationError> { +fn collect_assignments(path: &MatchedPath) -> Result, ValidationError> { let mut out = Vec::new(); let mut iter = path.items.iter(); while let Some(item) = iter.next() { @@ -1495,9 +1509,7 @@ fn build_sql_insert(path: &MatchedPath, source: &str) -> Result { + Command::ShowData { + name, + filter, + limit, + } => { assert_eq!(name, "Customers"); assert!(filter.is_some(), "where clause should survive"); assert_eq!(limit, Some(5)); @@ -2052,9 +2077,7 @@ mod explain_tests { /// Advanced-mode counterpart of `explain_inner`. fn explain_inner_adv(input: &str) -> Command { - match parse_command_in_mode(input, Mode::Advanced) - .expect("advanced explain should parse") - { + match parse_command_in_mode(input, Mode::Advanced).expect("advanced explain should parse") { Command::Explain { query } => *query, other => panic!("expected Command::Explain, got {other:?}"), } @@ -2085,7 +2108,9 @@ mod explain_tests { #[test] fn explain_sql_insert_wraps_a_sql_insert() { match explain_inner_adv("explain insert into Customers values (1, 'Bo')") { - Command::SqlInsert { sql, target_table, .. } => { + Command::SqlInsert { + sql, target_table, .. + } => { assert_eq!(target_table, "Customers"); assert_eq!(sql, "insert into Customers values (1, 'Bo')"); } @@ -2096,7 +2121,9 @@ mod explain_tests { #[test] fn explain_sql_update_wraps_a_sql_update_with_clean_sql() { match explain_inner_adv("explain update Customers set Name = 'Bo' where id = 1") { - Command::SqlUpdate { sql, target_table, .. } => { + Command::SqlUpdate { + sql, target_table, .. + } => { assert_eq!(target_table, "Customers"); assert_eq!(sql, "update Customers set Name = 'Bo' where id = 1"); } @@ -2107,7 +2134,9 @@ mod explain_tests { #[test] fn explain_sql_delete_wraps_a_sql_delete() { match explain_inner_adv("explain delete from Customers where id = 1") { - Command::SqlDelete { sql, target_table, .. } => { + Command::SqlDelete { + sql, target_table, .. + } => { assert_eq!(target_table, "Customers"); assert_eq!(sql, "delete from Customers where id = 1"); } @@ -2148,11 +2177,7 @@ mod explain_tests { fn explain_does_not_cover_ddl() { // EXPLAIN QUERY PLAN applies to DML/queries only (ADR-0039 // out of scope); there is no SQL DDL branch under explain. - assert!(parse_command_in_mode( - "explain create table T (id int)", - Mode::Advanced, - ) - .is_err()); + assert!(parse_command_in_mode("explain create table T (id int)", Mode::Advanced,).is_err()); } #[test] @@ -2165,9 +2190,8 @@ mod explain_tests { use crate::completion::candidates_at_cursor_in_mode; let schema = crate::completion::SchemaCache::default(); let input = "explain "; - let completion = - candidates_at_cursor_in_mode(input, input.len(), &schema, Mode::Advanced) - .expect("explain offers candidates"); + let completion = candidates_at_cursor_in_mode(input, input.len(), &schema, Mode::Advanced) + .expect("explain offers candidates"); let names: Vec<&str> = completion .candidates .iter() @@ -2178,4 +2202,3 @@ mod explain_tests { } } } - diff --git a/src/dsl/grammar/ddl.rs b/src/dsl/grammar/ddl.rs index 83839d8..cb4a680 100644 --- a/src/dsl/grammar/ddl.rs +++ b/src/dsl/grammar/ddl.rs @@ -16,11 +16,11 @@ use crate::dsl::command::{ AlterTableAction, ChangeColumnMode, ColumnSpec, Command, Constraint, ConstraintKind, Expr, IndexSelector, RelationshipSelector, SqlForeignKey, TableConstraint, }; -use crate::dsl::value::Value; use crate::dsl::grammar::{ CommandNode, HighlightClass, HintMode, IdentSource, Node, ValidationError, Word, shared::{REFERENTIAL_CLAUSES, TYPE_SLOT, TYPE_VALIDATOR}, }; +use crate::dsl::value::Value; /// `HintMode` annotation shared by every `NewName` ident slot: /// the user is inventing a name, so the hint panel forces the @@ -39,12 +39,12 @@ const TABLE_NAME_NEW_IDENT: Node = Node::Ident { role: "table_name", validator: None, highlight_override: None, - writes_table: false, - writes_column: false, - writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table: false, + writes_column: false, + writes_user_listed_column: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; const TABLE_NAME_NEW: Node = Node::Hinted { mode: NEW_NAME_HINT, @@ -63,12 +63,12 @@ const TABLE_NAME_EXISTING: Node = Node::Ident { role: "table_name", validator: None, highlight_override: None, - writes_table: true, - writes_column: false, - writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table: true, + writes_column: false, + writes_user_listed_column: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; const COLUMN_NAME: Node = Node::Ident { @@ -76,12 +76,12 @@ const COLUMN_NAME: Node = Node::Ident { role: "column_name", validator: None, highlight_override: None, - writes_table: false, - writes_column: false, - writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table: false, + writes_column: false, + writes_user_listed_column: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; const COLUMN_NAME_NEW_IDENT: Node = Node::Ident { @@ -89,12 +89,12 @@ const COLUMN_NAME_NEW_IDENT: Node = Node::Ident { role: "column_name", validator: None, highlight_override: None, - writes_table: false, - writes_column: false, - writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table: false, + writes_column: false, + writes_user_listed_column: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; const COLUMN_NAME_NEW: Node = Node::Hinted { mode: NEW_NAME_HINT, @@ -106,12 +106,12 @@ const RELATIONSHIP_NAME: Node = Node::Ident { role: "relationship_name", validator: None, highlight_override: None, - writes_table: false, - writes_column: false, - writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table: false, + writes_column: false, + writes_user_listed_column: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident { @@ -119,12 +119,12 @@ const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident { role: "relationship_name", validator: None, highlight_override: None, - writes_table: false, - writes_column: false, - writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table: false, + writes_column: false, + writes_user_listed_column: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; const RELATIONSHIP_NAME_NEW: Node = Node::Hinted { mode: NEW_NAME_HINT, @@ -139,9 +139,9 @@ const INDEX_NAME_EXISTING: Node = Node::Ident { writes_table: false, writes_column: false, writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; const INDEX_NAME_NEW_IDENT: Node = Node::Ident { @@ -152,9 +152,9 @@ const INDEX_NAME_NEW_IDENT: Node = Node::Ident { writes_table: false, writes_column: false, writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; const INDEX_NAME_NEW: Node = Node::Hinted { mode: NEW_NAME_HINT, @@ -181,10 +181,7 @@ const TABLE_OPT: Node = Node::Optional(&Node::Word(Word::keyword("table"))); // drop_table — `drop table ` // ================================================================= -const DROP_TABLE_NODES: &[Node] = &[ - Node::Word(Word::keyword("table")), - TABLE_NAME_EXISTING, -]; +const DROP_TABLE_NODES: &[Node] = &[Node::Word(Word::keyword("table")), TABLE_NAME_EXISTING]; const DROP_TABLE: Node = Node::Seq(DROP_TABLE_NODES); // Advanced-mode SQL `DROP TABLE [IF EXISTS] [;]` (ADR-0035 §4, @@ -192,8 +189,10 @@ const DROP_TABLE: Node = Node::Seq(DROP_TABLE_NODES); // plus the optional `IF EXISTS` no-op-with-note. The leading concrete // `table` keyword (not the Optional) keeps the element/dispatch // matching honest. -static SQL_DROP_IF_EXISTS_NODES: &[Node] = - &[Node::Word(Word::keyword("if")), Node::Word(Word::keyword("exists"))]; +static SQL_DROP_IF_EXISTS_NODES: &[Node] = &[ + Node::Word(Word::keyword("if")), + Node::Word(Word::keyword("exists")), +]; const SQL_DROP_IF_EXISTS_OPT: Node = Node::Optional(&Node::Seq(SQL_DROP_IF_EXISTS_NODES)); static SQL_DROP_TABLE_SHAPE_NODES: &[Node] = &[ Node::Word(Word::keyword("table")), @@ -257,9 +256,9 @@ const DR_PARENT_NODES: &[Node] = &[ writes_table: true, writes_column: false, writes_user_listed_column: false, - writes_table_alias: false, - writes_cte_name: false, - writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, Node::Punct('.'), Node::Ident { @@ -270,9 +269,9 @@ const DR_PARENT_NODES: &[Node] = &[ writes_table: false, writes_column: false, writes_user_listed_column: false, - writes_table_alias: false, - writes_cte_name: false, - writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, ]; const DR_PARENT: Node = Node::Seq(DR_PARENT_NODES); @@ -286,9 +285,9 @@ const DR_CHILD_NODES: &[Node] = &[ writes_table: true, writes_column: false, writes_user_listed_column: false, - writes_table_alias: false, - writes_cte_name: false, - writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, Node::Punct('.'), Node::Ident { @@ -299,9 +298,9 @@ const DR_CHILD_NODES: &[Node] = &[ writes_table: false, writes_column: false, writes_user_listed_column: false, - writes_table_alias: false, - writes_cte_name: false, - writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, ]; const DR_CHILD: Node = Node::Seq(DR_CHILD_NODES); @@ -317,10 +316,7 @@ const DR_ENDPOINTS: Node = Node::Seq(DR_ENDPOINTS_NODES); const DR_SELECTOR_CHOICES: &[Node] = &[DR_ENDPOINTS, RELATIONSHIP_NAME]; const DR_SELECTOR: Node = Node::Choice(DR_SELECTOR_CHOICES); -const DROP_RELATIONSHIP_NODES: &[Node] = &[ - Node::Word(Word::keyword("relationship")), - DR_SELECTOR, -]; +const DROP_RELATIONSHIP_NODES: &[Node] = &[Node::Word(Word::keyword("relationship")), DR_SELECTOR]; const DROP_RELATIONSHIP: Node = Node::Seq(DROP_RELATIONSHIP_NODES); // ================================================================= @@ -341,18 +337,20 @@ const DI_POSITIONAL: Node = Node::Seq(DI_POSITIONAL_NODES); const DI_SELECTOR_CHOICES: &[Node] = &[DI_POSITIONAL, INDEX_NAME_EXISTING]; const DI_SELECTOR: Node = Node::Choice(DI_SELECTOR_CHOICES); -const DROP_INDEX_NODES: &[Node] = &[ - Node::Word(Word::keyword("index")), - DI_SELECTOR, -]; +const DROP_INDEX_NODES: &[Node] = &[Node::Word(Word::keyword("index")), DI_SELECTOR]; const DROP_INDEX: Node = Node::Seq(DROP_INDEX_NODES); // ================================================================= // drop entry — `drop (table|column|relationship|index) ...` // ================================================================= -const DROP_CHOICES: &[Node] = - &[DROP_COLUMN, DROP_RELATIONSHIP, DROP_TABLE, DROP_INDEX, DROP_CONSTRAINT]; +const DROP_CHOICES: &[Node] = &[ + DROP_COLUMN, + DROP_RELATIONSHIP, + DROP_TABLE, + DROP_INDEX, + DROP_CONSTRAINT, +]; const DROP_SHAPE: Node = Node::Choice(DROP_CHOICES); // ================================================================= @@ -450,8 +448,7 @@ const AR_CHILD_COL_LIST: Node = Node::Repeated { separator: Some(&Node::Punct(',')), min: 1, }; -const AR_CHILD_COLS_PAREN_NODES: &[Node] = - &[Node::Punct('('), AR_CHILD_COL_LIST, Node::Punct(')')]; +const AR_CHILD_COLS_PAREN_NODES: &[Node] = &[Node::Punct('('), AR_CHILD_COL_LIST, Node::Punct(')')]; const AR_CHILD_COLS_PAREN: Node = Node::Seq(AR_CHILD_COLS_PAREN_NODES); const AR_CHILD_COLS_CHOICES: &[Node] = &[AR_CHILD_COLS_PAREN, AR_CHILD_COL]; const AR_CHILD_COLS: Node = Node::Choice(AR_CHILD_COLS_CHOICES); @@ -474,10 +471,7 @@ const AR_CHILD_NODES: &[Node] = &[ ]; const AR_CHILD: Node = Node::Seq(AR_CHILD_NODES); -const AR_AS_NAME_NODES: &[Node] = &[ - Node::Word(Word::keyword("as")), - RELATIONSHIP_NAME_NEW, -]; +const AR_AS_NAME_NODES: &[Node] = &[Node::Word(Word::keyword("as")), RELATIONSHIP_NAME_NEW]; const AR_AS_NAME_OPT: Node = Node::Optional(&Node::Seq(AR_AS_NAME_NODES)); const AR_CREATE_FK_OPT: Node = Node::Optional(&Node::Flag("create-fk")); @@ -501,10 +495,7 @@ const ADD_RELATIONSHIP: Node = Node::Seq(ADD_RELATIONSHIP_NODES); // add_index — `add index [as ] on (, …)` // ================================================================= -const AI_AS_NAME_NODES: &[Node] = &[ - Node::Word(Word::keyword("as")), - INDEX_NAME_NEW, -]; +const AI_AS_NAME_NODES: &[Node] = &[Node::Word(Word::keyword("as")), INDEX_NAME_NEW]; const AI_AS_NAME_OPT: Node = Node::Optional(&Node::Seq(AI_AS_NAME_NODES)); const ADD_INDEX_NODES: &[Node] = &[ @@ -537,9 +528,9 @@ const NEW_COLUMN_NAME_IDENT: Node = Node::Ident { writes_table: false, writes_column: false, writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; const NEW_COLUMN_NAME: Node = Node::Hinted { mode: NEW_NAME_HINT, @@ -563,10 +554,7 @@ const RENAME_COLUMN: Node = Node::Seq(RENAME_COLUMN_NODES); // ( ) [--force-conversion | --dont-convert]` // ================================================================= -const CHANGE_FLAG_CHOICES: &[Node] = &[ - Node::Flag("force-conversion"), - Node::Flag("dont-convert"), -]; +const CHANGE_FLAG_CHOICES: &[Node] = &[Node::Flag("force-conversion"), Node::Flag("dont-convert")]; const CHANGE_FLAG_OPT: Node = Node::Repeated { inner: &Node::Choice(CHANGE_FLAG_CHOICES), separator: None, @@ -732,8 +720,7 @@ fn build_add(path: &MatchedPath, _source: &str) -> Result Result [with pk [()[, ...]]]` @@ -1034,9 +1028,9 @@ const COL_NAME_IDENT: Node = Node::Ident { writes_table: false, writes_column: false, writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; const COL_NAME: Node = Node::Hinted { mode: NEW_NAME_HINT, @@ -1074,8 +1068,12 @@ const CHECK_CONSTRAINT_NODES: &[Node] = &[ ]; const CHECK_CONSTRAINT: Node = Node::Seq(CHECK_CONSTRAINT_NODES); -const COLUMN_CONSTRAINT_CHOICES: &[Node] = - &[NOT_NULL_CONSTRAINT, UNIQUE_CONSTRAINT, DEFAULT_CONSTRAINT, CHECK_CONSTRAINT]; +const COLUMN_CONSTRAINT_CHOICES: &[Node] = &[ + NOT_NULL_CONSTRAINT, + UNIQUE_CONSTRAINT, + DEFAULT_CONSTRAINT, + CHECK_CONSTRAINT, +]; const COLUMN_CONSTRAINT: Node = Node::Choice(COLUMN_CONSTRAINT_CHOICES); /// Zero-or-more constraints — the suffix after a column's @@ -1114,8 +1112,7 @@ const DROP_CONSTRAINT_KIND: Node = Node::Choice(DROP_CONSTRAINT_KIND_CHOICES); // `writes_table: true` on the table ident (via `TABLE_NAME_ // EXISTING`) narrows the `.` slot's completion // candidates to that table's columns. -const CONSTRAINT_TARGET_NODES: &[Node] = - &[TABLE_NAME_EXISTING, Node::Punct('.'), COLUMN_NAME]; +const CONSTRAINT_TARGET_NODES: &[Node] = &[TABLE_NAME_EXISTING, Node::Punct('.'), COLUMN_NAME]; const CONSTRAINT_TARGET: Node = Node::Seq(CONSTRAINT_TARGET_NODES); const ADD_CONSTRAINT_NODES: &[Node] = &[ @@ -1145,9 +1142,9 @@ const COL_SPEC_NODES: &[Node] = &[ writes_table: false, writes_column: false, writes_user_listed_column: false, - writes_table_alias: false, - writes_cte_name: false, - writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, Node::Punct(')'), COLUMN_CONSTRAINT_SUFFIX, @@ -1275,10 +1272,14 @@ fn build_create_table(path: &MatchedPath, _source: &str) -> Result { + MatchedKind::Ident { + role: "col_name", .. + } => { pending_name = Some(item.text.clone()); } - MatchedKind::Ident { role: "col_type", .. } => { + MatchedKind::Ident { + role: "col_type", .. + } => { let ty = item.text.parse::().map_err(|_| ValidationError { message_key: "parse.error_wrapper", args: vec![("detail", "unknown type".to_string())], @@ -1380,7 +1381,8 @@ pub static CREATE: CommandNode = CommandNode { ast_builder: build_create_table, help_id: Some("ddl.create"), hint_ids: &["create_table"], - usage_ids: &["parse.usage.create_table"],}; + usage_ids: &["parse.usage.create_table"], +}; // ================================================================= // create_m2n — `create m:n relationship from to [as ]` @@ -1506,11 +1508,15 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result { + MatchedKind::Ident { + role: "col_name", .. + } => { pending_name = Some(item.text.clone()); } // Single-word type — resolve through the SQL alias map. - MatchedKind::Ident { role: "col_type", .. } => { + MatchedKind::Ident { + role: "col_type", .. + } => { let ty = Type::from_sql_name(&item.text).ok_or_else(|| ValidationError { message_key: "parse.error_wrapper", args: vec![("detail", "unknown type".to_string())], @@ -1533,7 +1539,9 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result { + MatchedKind::Ident { + role: "pk_column", .. + } => { primary_key.push(item.text.clone()); } // `not null` column constraint (only once a column exists; @@ -1557,7 +1565,10 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result = Vec::new(); while let Some(it) = items.peek() { match &it.kind { - MatchedKind::Ident { role: "unique_column", .. } => { + MatchedKind::Ident { + role: "unique_column", + .. + } => { cols.push(it.text.clone()); items.next(); } @@ -1575,7 +1586,10 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result c.unique = true, None if !cols.is_empty() => unique_constraints.push(cols), None => {} @@ -1588,16 +1602,17 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result { - if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("key"))) { + if matches!( + items.peek().map(|i| &i.kind), + Some(MatchedKind::Word("key")) + ) { items.next(); // Table-level `PRIMARY KEY (…)` is followed by `(` // (then `pk_column` idents, collected above); // column-level `PRIMARY KEY` is not, and marks the // most-recent column. - let table_level = matches!( - items.peek().map(|i| &i.kind), - Some(MatchedKind::Punct('(')) - ); + let table_level = + matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Punct('('))); if !table_level && let Some(last) = columns.last() { primary_key.push(last.name.clone()); } @@ -1647,12 +1662,20 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result] foreign key () // references [()] [on …]` (ADR-0035 §5, 4b). MatchedKind::Word("foreign") => { - if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("key"))) { + if matches!( + items.peek().map(|i| &i.kind), + Some(MatchedKind::Word("key")) + ) { items.next(); // `key` } // `( [, ]* )` — a compound @@ -1674,7 +1697,10 @@ fn build_sql_create_table(path: &MatchedPath, source: &str) -> Result …` - if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("references"))) { + if matches!( + items.peek().map(|i| &i.kind), + Some(MatchedKind::Word("references")) + ) { items.next(); } let fk = @@ -1859,13 +1885,19 @@ where Some(MatchedKind::Word("cascade")) => ReferentialAction::Cascade, Some(MatchedKind::Word("restrict")) => ReferentialAction::Restrict, Some(MatchedKind::Word("set")) => { - if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("null"))) { + if matches!( + items.peek().map(|i| &i.kind), + Some(MatchedKind::Word("null")) + ) { items.next(); } ReferentialAction::SetNull } Some(MatchedKind::Word("no")) => { - if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("action"))) { + if matches!( + items.peek().map(|i| &i.kind), + Some(MatchedKind::Word("action")) + ) { items.next(); } ReferentialAction::NoAction @@ -1933,11 +1965,12 @@ pub static SQL_DROP_INDEX: CommandNode = CommandNode { // concrete keyword (`unique index` | `index`) — the trap-safe form (the // §3 rule forbids a leading *Optional*, not a leading `Choice`). The // builder reads `unique` presence via `contains_word("unique")`. -static SQL_CI_UNIQUE_INDEX_NODES: &[Node] = - &[Node::Word(Word::keyword("unique")), Node::Word(Word::keyword("index"))]; +static SQL_CI_UNIQUE_INDEX_NODES: &[Node] = &[ + Node::Word(Word::keyword("unique")), + Node::Word(Word::keyword("index")), +]; const SQL_CI_UNIQUE_INDEX: Node = Node::Seq(SQL_CI_UNIQUE_INDEX_NODES); -static SQL_CI_LEAD_CHOICES: &[Node] = - &[SQL_CI_UNIQUE_INDEX, Node::Word(Word::keyword("index"))]; +static SQL_CI_LEAD_CHOICES: &[Node] = &[SQL_CI_UNIQUE_INDEX, Node::Word(Word::keyword("index"))]; const SQL_CI_LEAD: Node = Node::Choice(SQL_CI_LEAD_CHOICES); static SQL_CI_IF_NOT_EXISTS_NODES: &[Node] = &[ @@ -2104,8 +2137,7 @@ static AT_RENAME_COLUMN_TAIL_NODES: &[Node] = &[ NEW_COLUMN_NAME, ]; const AT_RENAME_COLUMN_TAIL: Node = Node::Seq(AT_RENAME_COLUMN_TAIL_NODES); -static AT_RENAME_TABLE_TAIL_NODES: &[Node] = - &[Node::Word(Word::keyword("to")), NEW_TABLE_NAME]; +static AT_RENAME_TABLE_TAIL_NODES: &[Node] = &[Node::Word(Word::keyword("to")), NEW_TABLE_NAME]; const AT_RENAME_TABLE_TAIL: Node = Node::Seq(AT_RENAME_TABLE_TAIL_NODES); static AT_RENAME_TAIL_CHOICES: &[Node] = &[AT_RENAME_COLUMN_TAIL, AT_RENAME_TABLE_TAIL]; const AT_RENAME_TAIL: Node = Node::Choice(AT_RENAME_TAIL_CHOICES); @@ -2132,8 +2164,10 @@ static AT_AC_TYPE_NODES: &[Node] = &[ super::sql_create_table::SQL_TYPE, ]; const AT_AC_TYPE: Node = Node::Seq(AT_AC_TYPE_NODES); -static AT_AC_NOT_NULL_NODES: &[Node] = - &[Node::Word(Word::keyword("not")), Node::Word(Word::keyword("null"))]; +static AT_AC_NOT_NULL_NODES: &[Node] = &[ + Node::Word(Word::keyword("not")), + Node::Word(Word::keyword("null")), +]; const AT_AC_NOT_NULL: Node = Node::Seq(AT_AC_NOT_NULL_NODES); static AT_AC_SET_DATA_TYPE_NODES: &[Node] = &[ Node::Word(Word::keyword("data")), @@ -2149,8 +2183,7 @@ static AT_AC_SET_TAIL_CHOICES: &[Node] = &[ const AT_AC_SET_TAIL: Node = Node::Choice(AT_AC_SET_TAIL_CHOICES); static AT_AC_SET_NODES: &[Node] = &[Node::Word(Word::keyword("set")), AT_AC_SET_TAIL]; const AT_AC_SET: Node = Node::Seq(AT_AC_SET_NODES); -static AT_AC_DROP_TAIL_CHOICES: &[Node] = - &[AT_AC_NOT_NULL, Node::Word(Word::keyword("default"))]; +static AT_AC_DROP_TAIL_CHOICES: &[Node] = &[AT_AC_NOT_NULL, Node::Word(Word::keyword("default"))]; const AT_AC_DROP_TAIL: Node = Node::Choice(AT_AC_DROP_TAIL_CHOICES); static AT_AC_DROP_NODES: &[Node] = &[Node::Word(Word::keyword("drop")), AT_AC_DROP_TAIL]; const AT_AC_DROP: Node = Node::Seq(AT_AC_DROP_NODES); @@ -2258,10 +2291,14 @@ fn build_alter_add_column_spec( let mut items = path.items.iter().peekable(); while let Some(item) = items.next() { match &item.kind { - MatchedKind::Ident { role: "col_name", .. } => { + MatchedKind::Ident { + role: "col_name", .. + } => { pending_name = Some(item.text.clone()); } - MatchedKind::Ident { role: "col_type", .. } => { + MatchedKind::Ident { + role: "col_type", .. + } => { let ty = Type::from_sql_name(&item.text).ok_or_else(|| ValidationError { message_key: "parse.error_wrapper", args: vec![("detail", "unknown type".to_string())], @@ -2280,7 +2317,10 @@ fn build_alter_add_column_spec( spec = Some(ColumnSpec::new(name, Type::Real)); } MatchedKind::Word("not") => { - if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("null"))) { + if matches!( + items.peek().map(|i| &i.kind), + Some(MatchedKind::Word("null")) + ) { items.next(); if let Some(s) = spec.as_mut() { s.not_null = true; @@ -2326,11 +2366,15 @@ fn build_alter_column_type(path: &MatchedPath) -> Result { - ty = Some(Type::from_sql_name(&item.text).ok_or_else(|| ValidationError { - message_key: "parse.error_wrapper", - args: vec![("detail", "unknown type".to_string())], - })?); + MatchedKind::Ident { + role: "col_type", .. + } => { + ty = Some( + Type::from_sql_name(&item.text).ok_or_else(|| ValidationError { + message_key: "parse.error_wrapper", + args: vec![("detail", "unknown type".to_string())], + })?, + ); } MatchedKind::Word("double") => { if matches!( @@ -2379,7 +2423,10 @@ fn build_alter_column_attr( message_key: "parse.error_wrapper", args: vec![("detail", "set default needs a value".to_string())], })?; - AlterTableAction::SetColumnDefault { column, default_sql } + AlterTableAction::SetColumnDefault { + column, + default_sql, + } } (false, true) => AlterTableAction::DropColumnDefault { column }, (true, false) => AlterTableAction::SetColumnNotNull { column }, @@ -2495,10 +2542,7 @@ fn build_alter_add_table_constraint( /// Capture the raw SQL text of an `ADD … CHECK ()` (ADR-0035 §4g). /// `sql_expr` is validate-only, so the expression is captured by byte /// span — the 4a.2 / 4e mechanism. -fn capture_table_check_sql( - path: &MatchedPath, - source: &str, -) -> Result { +fn capture_table_check_sql(path: &MatchedPath, source: &str) -> Result { let mut items = path.items.iter().peekable(); while let Some(item) = items.next() { if matches!(item.kind, MatchedKind::Word("check")) @@ -2528,7 +2572,10 @@ fn build_alter_fk(path: &MatchedPath) -> SqlForeignKey { items.next(); } items.next(); // `foreign` - if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("key"))) { + if matches!( + items.peek().map(|i| &i.kind), + Some(MatchedKind::Word("key")) + ) { items.next(); } if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Punct('('))) { @@ -2548,7 +2595,10 @@ fn build_alter_fk(path: &MatchedPath) -> SqlForeignKey { if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Punct(')'))) { items.next(); } - if matches!(items.peek().map(|i| &i.kind), Some(MatchedKind::Word("references"))) { + if matches!( + items.peek().map(|i| &i.kind), + Some(MatchedKind::Word("references")) + ) { items.next(); } // `ALTER TABLE … ADD FOREIGN KEY (…)` is the table-level form. @@ -2626,7 +2676,10 @@ mod constraint_tests { fn an_unconstrained_create_table_still_parses() { let cols = create_columns("create table T with pk id(serial), name(text)"); assert_eq!(cols.len(), 2); - assert!(cols.iter().all(|c| !c.not_null && !c.unique && c.default.is_none())); + assert!( + cols.iter() + .all(|c| !c.not_null && !c.unique && c.default.is_none()) + ); } #[test] @@ -2651,7 +2704,9 @@ mod constraint_tests { #[test] fn add_column_parses_a_unique_constraint() { match parse_command("add column to T: email (text) unique").expect("parse") { - Command::AddColumn { unique, not_null, .. } => { + Command::AddColumn { + unique, not_null, .. + } => { assert!(unique); assert!(!not_null); } @@ -2682,9 +2737,7 @@ mod constraint_tests { fn check_with_a_parenthesised_sub_expression_parses() { // The check's own parens plus a nested group — the // builder's paren-depth scan must pair them correctly. - let cols = create_columns( - "create table T with pk n(int) check ((n > 0) or (n < -10))", - ); + let cols = create_columns("create table T with pk n(int) check ((n > 0) or (n < -10))"); assert!(cols[0].check.is_some()); } @@ -2731,8 +2784,7 @@ mod constraint_tests { #[test] fn add_constraint_check_parses() { - match parse_command("add constraint check (age >= 0) to Users.age").expect("parse") - { + match parse_command("add constraint check (age >= 0) to Users.age").expect("parse") { Command::AddConstraint { column, constraint, .. } => { @@ -2826,8 +2878,11 @@ mod sql_drop_table_tests { Command::DropColumn { .. } )); assert!(matches!( - parse_command_in_mode("drop relationship Customers_id_to_Orders_CustId", Mode::Advanced) - .expect("parses"), + parse_command_in_mode( + "drop relationship Customers_id_to_Orders_CustId", + Mode::Advanced + ) + .expect("parses"), Command::DropRelationship { .. } )); } @@ -2932,7 +2987,13 @@ mod sql_create_index_tests { columns, unique, if_not_exists, - } => Ci { name, table, columns, unique, if_not_exists }, + } => Ci { + name, + table, + columns, + unique, + if_not_exists, + }, other => panic!("expected SqlCreateIndex, got {other:?}"), } } @@ -3134,7 +3195,9 @@ mod sql_alter_table_tests { // The target slot carries the `reject_internal_table` validator // (mirroring CREATE TABLE), so an `__rdbms_*` target is refused // before submit — engine-neutral, not a raw engine error. - assert!(parse_command_in_mode("alter table T rename to __rdbms_evil", Mode::Advanced).is_err()); + assert!( + parse_command_in_mode("alter table T rename to __rdbms_evil", Mode::Advanced).is_err() + ); } #[test] @@ -3213,7 +3276,10 @@ mod sql_alter_table_tests { // alias map still applies through the synonym assert!(matches!( alter("alter table T alter column n set data type double precision").1, - AlterTableAction::AlterColumnType { ty: crate::dsl::types::Type::Real, .. } + AlterTableAction::AlterColumnType { + ty: crate::dsl::types::Type::Real, + .. + } )); } @@ -3238,7 +3304,10 @@ mod sql_alter_table_tests { #[test] fn alter_column_set_default_captures_raw_expr() { match alter("alter table T alter column qty set default 0").1 { - AlterTableAction::SetColumnDefault { column, default_sql } => { + AlterTableAction::SetColumnDefault { + column, + default_sql, + } => { assert_eq!(column, "qty"); assert_eq!(default_sql, "0"); } @@ -3317,7 +3386,9 @@ mod sql_alter_table_tests { match alter("alter table T add check (a < b)").1 { AlterTableAction::AddTableConstraint { name, constraint } => { assert_eq!(name, None); - assert!(matches!(*constraint, TableConstraint::Check { ref expr_sql } if expr_sql == "a < b")); + assert!( + matches!(*constraint, TableConstraint::Check { ref expr_sql } if expr_sql == "a < b") + ); } other => panic!("expected AddTableConstraint/Check, got {other:?}"), } @@ -3335,7 +3406,9 @@ mod sql_alter_table_tests { match alter("alter table T add unique (a, b)").1 { AlterTableAction::AddTableConstraint { name, constraint } => { assert_eq!(name, None); - assert!(matches!(*constraint, TableConstraint::Unique { ref columns } if columns == &["a".to_string(), "b".to_string()])); + assert!( + matches!(*constraint, TableConstraint::Unique { ref columns } if columns == &["a".to_string(), "b".to_string()]) + ); } other => panic!("expected AddTableConstraint/Unique, got {other:?}"), } @@ -3352,7 +3425,9 @@ mod sql_alter_table_tests { ) .expect_err("a named UNIQUE constraint is refused"); assert!( - err.to_string().to_lowercase().contains("unique constraint cannot be named"), + err.to_string() + .to_lowercase() + .contains("unique constraint cannot be named"), "expected the builder's named-UNIQUE refusal, got: {err}" ); } @@ -3364,7 +3439,9 @@ mod sql_alter_table_tests { let err = parse_command_in_mode("alter table T add primary key (id)", Mode::Advanced) .expect_err("ADD PRIMARY KEY is refused"); assert!( - err.to_string().to_lowercase().contains("primary key is fixed at creation"), + err.to_string() + .to_lowercase() + .contains("primary key is fixed at creation"), "expected the builder's ADD-PRIMARY-KEY refusal, got: {err}" ); } @@ -3392,7 +3469,10 @@ mod sql_alter_table_tests { assert_eq!(name.as_deref(), Some("fk_p")); match *constraint { TableConstraint::ForeignKey(fk) => { - assert_eq!(fk.parent_columns, None, "bare reference resolves at execution"); + assert_eq!( + fk.parent_columns, None, + "bare reference resolves at execution" + ); } other => panic!("expected ForeignKey, got {other:?}"), } diff --git a/src/dsl/grammar/expr.rs b/src/dsl/grammar/expr.rs index 8b7c324..4b7113e 100644 --- a/src/dsl/grammar/expr.rs +++ b/src/dsl/grammar/expr.rs @@ -79,9 +79,9 @@ const EXPR_COLUMN: Node = Node::Ident { writes_table: false, writes_column: true, writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; /// Operand alternatives. The literal keywords (`null` / `true` @@ -126,8 +126,7 @@ fn where_rhs_operand(ctx: &WalkContext) -> Node { // the leak is per distinct column (the walker // memoizes `DynamicSubgrammar` resolution on // `current_column`), not per keystroke. - let leaked: &'static str = - Box::leak(col.name.clone().into_boxed_str()); + let leaked: &'static str = Box::leak(col.name.clone().into_boxed_str()); Node::TypedValueSlot { ty: col.user_type, column_name: Some(leaked), @@ -260,10 +259,8 @@ static PAREN_GROUP_NODES: &[Node] = &[ Node::Subgrammar(&OR_EXPR), Node::Punct(')'), ]; -static BOOL_PRIMARY_CHOICES: &[Node] = &[ - Node::Seq(PAREN_GROUP_NODES), - Node::Subgrammar(&PREDICATE), -]; +static BOOL_PRIMARY_CHOICES: &[Node] = + &[Node::Seq(PAREN_GROUP_NODES), Node::Subgrammar(&PREDICATE)]; static BOOL_PRIMARY: Node = Node::Choice(BOOL_PRIMARY_CHOICES); /// `not_expr := NOT not_expr | bool_primary`. @@ -271,10 +268,7 @@ static NOT_FORM_NODES: &[Node] = &[ Node::Word(Word::keyword("not")), Node::Subgrammar(&NOT_EXPR), ]; -static NOT_EXPR_CHOICES: &[Node] = &[ - Node::Seq(NOT_FORM_NODES), - Node::Subgrammar(&BOOL_PRIMARY), -]; +static NOT_EXPR_CHOICES: &[Node] = &[Node::Seq(NOT_FORM_NODES), Node::Subgrammar(&BOOL_PRIMARY)]; static NOT_EXPR: Node = Node::Choice(NOT_EXPR_CHOICES); /// `and_expr := not_expr ( AND not_expr )*`. @@ -296,10 +290,7 @@ static AND_EXPR: Node = Node::Seq(AND_EXPR_NODES); /// `or_expr := and_expr ( OR and_expr )*` — the fragment entry /// point. `update` / `delete` / `show data` reference this /// through `Node::Subgrammar(&OR_EXPR)`. -static OR_TAIL_NODES: &[Node] = &[ - Node::Word(Word::keyword("or")), - Node::Subgrammar(&AND_EXPR), -]; +static OR_TAIL_NODES: &[Node] = &[Node::Word(Word::keyword("or")), Node::Subgrammar(&AND_EXPR)]; static OR_TAIL: Node = Node::Seq(OR_TAIL_NODES); static OR_EXPR_NODES: &[Node] = &[ Node::Subgrammar(&AND_EXPR), @@ -534,18 +525,18 @@ impl<'a> ExprParser<'a> { let span = item.span; let literal = |value: Value| Operand::Literal { value, span }; match &item.kind { - MatchedKind::Ident { role: "expr_column", .. } => { - Ok(Operand::Column { name: item.text.clone(), span }) - } + MatchedKind::Ident { + role: "expr_column", + .. + } => Ok(Operand::Column { + name: item.text.clone(), + span, + }), MatchedKind::Word("null") => Ok(literal(Value::Null)), MatchedKind::Word("true") => Ok(literal(Value::Bool(true))), MatchedKind::Word("false") => Ok(literal(Value::Bool(false))), - MatchedKind::NumberLit => { - Ok(literal(Value::Number(item.text.clone()))) - } - MatchedKind::StringLit => { - Ok(literal(Value::Text(item.text.clone()))) - } + MatchedKind::NumberLit => Ok(literal(Value::Number(item.text.clone()))), + MatchedKind::StringLit => Ok(literal(Value::Text(item.text.clone()))), _ => Err(drift_error("expected a column or literal operand")), } } @@ -591,8 +582,7 @@ mod tests { let mut ctx = WalkContext::new(); let mut path = MatchedPath::new(); let mut per_byte = Vec::new(); - let result = - walk_node(input, 0, &OR_EXPR, &mut ctx, &mut path, &mut per_byte); + let result = walk_node(input, 0, &OR_EXPR, &mut ctx, &mut path, &mut per_byte); match result { NodeWalkResult::Matched { end, .. } => { assert!( @@ -730,8 +720,7 @@ mod tests { negated: false, }), ); - let Expr::Predicate(Predicate::Like { negated, .. }) = - parse_expr("Name not like 'A%'") + let Expr::Predicate(Predicate::Like { negated, .. }) = parse_expr("Name not like 'A%'") else { panic!("expected a negated Like"); }; @@ -794,16 +783,16 @@ mod tests { fn nested_parentheses_round_trip() { // Exercises the Subgrammar recursion a few levels deep. let expr = parse_expr("((a = 1 and b = 2) or (c = 3))"); - assert!(matches!(expr, Expr::Or(_) | Expr::And(_) | Expr::Predicate(_))); + assert!(matches!( + expr, + Expr::Or(_) | Expr::And(_) | Expr::Predicate(_) + )); } #[test] fn case_insensitive_keywords() { // Keywords fold case; the built tree is identical. - assert_eq!( - parse_expr("a = 1 AND b = 2"), - parse_expr("a = 1 and b = 2"), - ); + assert_eq!(parse_expr("a = 1 AND b = 2"), parse_expr("a = 1 and b = 2"),); assert_eq!( parse_expr("Email IS NOT NULL"), parse_expr("Email is not null"), diff --git a/src/dsl/grammar/mod.rs b/src/dsl/grammar/mod.rs index ff2bc23..6122e09 100644 --- a/src/dsl/grammar/mod.rs +++ b/src/dsl/grammar/mod.rs @@ -27,9 +27,9 @@ pub mod data; pub mod ddl; pub mod expr; pub mod shared; -pub mod sql_expr; pub mod sql_create_table; pub mod sql_delete; +pub mod sql_expr; pub mod sql_insert; pub mod sql_select; pub mod sql_update; @@ -328,9 +328,7 @@ pub enum Node { /// A number literal. The optional `validator` runs against /// the matched text (used by Phase D value slots to enforce /// per-type integer/decimal rules). - NumberLit { - validator: Option, - }, + NumberLit { validator: Option }, /// A literal byte sequence at this position — matches /// bytes verbatim (whitespace-skipped) with a lookahead so /// `1` doesn't half-match `12` and `n` doesn't half-match @@ -701,7 +699,11 @@ fn selected_nodes_for_input_in_mode( .filter(|(_, _, c)| *c == CommandCategory::Simple) .collect() }; - if selected.is_empty() { candidates } else { selected } + if selected.is_empty() { + candidates + } else { + selected + } } /// The single usage template most relevant to `source`, when @@ -724,10 +726,7 @@ pub fn usage_key_for_input(source: &str) -> Option<&'static str> { /// disambiguates the single most-relevant usage key from the /// mode-selected key set. #[must_use] -pub fn usage_key_for_input_in_mode( - source: &str, - mode: crate::mode::Mode, -) -> Option<&'static str> { +pub fn usage_key_for_input_in_mode(source: &str, mode: crate::mode::Mode) -> Option<&'static str> { let (_entry, keys) = usage_keys_for_input_in_mode(source, mode)?; pick_form_key(source, &keys) } @@ -755,7 +754,10 @@ fn pick_form_key<'a>(source: &str, keys: &[&'a str]) -> Option<&'a str> { } // The `create m:n relationship` form (ADR-0045) opens with `m:n` // — a letter, so the digit branch misses it; its key ends `…m2n`. - if source[after..].get(..3).is_some_and(|s| s.eq_ignore_ascii_case("m:n")) { + if source[after..] + .get(..3) + .is_some_and(|s| s.eq_ignore_ascii_case("m:n")) + { return keys.iter().copied().find(|k| k.ends_with("m2n")); } // Otherwise the form word is an identifier — `column`, `index`, @@ -770,8 +772,7 @@ fn pick_form_key<'a>(source: &str, keys: &[&'a str]) -> Option<&'a str> { /// which read the same data through the legacy `usage::REGISTRY`. #[must_use] pub fn entry_words_alphabetised() -> Vec<&'static str> { - let mut words: Vec<&'static str> = - REGISTRY.iter().map(|(c, _)| c.entry.primary).collect(); + let mut words: Vec<&'static str> = REGISTRY.iter().map(|(c, _)| c.entry.primary).collect(); words.sort_unstable(); words.dedup(); words @@ -905,9 +906,7 @@ pub fn command_for_entry_word(word: &str) -> Option<(usize, &'static CommandNode /// returns its `Simple` DSL node and `Advanced` SQL node. The /// dispatcher picks among them by the active input mode. #[must_use] -pub fn commands_for_entry_word( - word: &str, -) -> Vec<(usize, &'static CommandNode, CommandCategory)> { +pub fn commands_for_entry_word(word: &str) -> Vec<(usize, &'static CommandNode, CommandCategory)> { REGISTRY .iter() .enumerate() @@ -1010,7 +1009,10 @@ mod hint_key_tests { ]; for c in classes { let key = format!("hint.err.{c}.what"); - assert!(cat.get(&key).is_some(), "missing tier-3 error block `{key}`"); + assert!( + cat.get(&key).is_some(), + "missing tier-3 error block `{key}`" + ); } } @@ -1098,10 +1100,7 @@ mod usage_key_tests { let cases = [ ("add column to T: c (int)", "parse.usage.add_column"), ("add index on T (c)", "parse.usage.add_index"), - ( - "add constraint unique to T.c", - "parse.usage.add_constraint", - ), + ("add constraint unique to T.c", "parse.usage.add_constraint"), ( "drop constraint check from T.c", "parse.usage.drop_constraint", @@ -1118,10 +1117,7 @@ mod usage_key_tests { ("drop table T", "parse.usage.drop_table"), ("drop column from table T: c", "parse.usage.drop_column"), ("drop index i", "parse.usage.drop_index"), - ( - "drop relationship r", - "parse.usage.drop_relationship", - ), + ("drop relationship r", "parse.usage.drop_relationship"), ("show data T", "parse.usage.show_data"), ("show table T", "parse.usage.show_table"), // `create` is multi-form (table vs m:n, ADR-0045): each typed diff --git a/src/dsl/grammar/shared.rs b/src/dsl/grammar/shared.rs index c62c2dc..10fc41f 100644 --- a/src/dsl/grammar/shared.rs +++ b/src/dsl/grammar/shared.rs @@ -7,8 +7,8 @@ use crate::completion::TableColumn; use crate::dsl::grammar::{ - HighlightClass, HintMode, IdentSource, IdentValidator, Node, - NumberValidator, ValidationError, Word, + HighlightClass, HintMode, IdentSource, IdentValidator, Node, NumberValidator, ValidationError, + Word, }; use crate::dsl::types::Type; use crate::dsl::walker::context::WalkContext; @@ -32,10 +32,7 @@ pub fn validate_type_name(value: &str) -> Result<(), ValidationError> { .join(", "); Err(ValidationError { message_key: "parse.custom.unknown_type", - args: vec![ - ("found", value.to_string()), - ("expected", expected), - ], + args: vec![("found", value.to_string()), ("expected", expected)], }) } } @@ -51,12 +48,12 @@ pub const TYPE_SLOT: Node = Node::Ident { role: "type", validator: Some(TYPE_VALIDATOR), highlight_override: Some(HighlightClass::Type), - writes_table: false, - writes_column: false, - writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table: false, + writes_column: false, + writes_user_listed_column: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; // --- Qualified column reference (`
.`) -------------- @@ -70,9 +67,9 @@ const QUALIFIED_COLUMN_NODES: &[Node] = &[ writes_table: false, writes_column: false, writes_user_listed_column: false, - writes_table_alias: false, - writes_cte_name: false, - writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, Node::Punct('.'), Node::Ident { @@ -83,9 +80,9 @@ const QUALIFIED_COLUMN_NODES: &[Node] = &[ writes_table: false, writes_column: false, writes_user_listed_column: false, - writes_table_alias: false, - writes_cte_name: false, - writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }, ]; pub const QUALIFIED_COLUMN: Node = Node::Seq(QUALIFIED_COLUMN_NODES); @@ -313,9 +310,7 @@ const fn slot_inner_for_type(ty: Type) -> &'static Node { Type::Real => &REAL_SLOT_INNER, Type::Decimal => &DECIMAL_SLOT_INNER, Type::Bool => &BOOL_SLOT_INNER, - Type::Text | Type::Date | Type::DateTime | Type::Blob | Type::ShortId => { - &TEXT_SLOT_INNER - } + Type::Text | Type::Date | Type::DateTime | Type::Blob | Type::ShortId => &TEXT_SLOT_INNER, } } @@ -397,9 +392,7 @@ pub(crate) const FALLBACK_VALUE_LIST: Node = Node::Repeated { /// This is the single source of truth shared by [`column_value_list`] /// (which builds the typed slots) and the `data.rs` arity gate (which /// counts them) so the two never disagree (issue #17). -pub fn insert_target_columns<'c>( - ctx: &'c WalkContext<'_>, -) -> Option> { +pub fn insert_target_columns<'c>(ctx: &'c WalkContext<'_>) -> Option> { let table_cols = ctx.current_table_columns.as_ref()?; if table_cols.is_empty() { return None; diff --git a/src/dsl/grammar/sql_create_table.rs b/src/dsl/grammar/sql_create_table.rs index 49415c3..df6b7e5 100644 --- a/src/dsl/grammar/sql_create_table.rs +++ b/src/dsl/grammar/sql_create_table.rs @@ -405,8 +405,14 @@ const TABLE_FK_NAMED: Node = Node::Seq(TABLE_FK_NAMED_NODES); // / `foreign`) that disambiguates it from a column name. (A column // literally named with one of those keywords is therefore unavailable, // the same trade real SQL makes with its reserved words.) -static ELEMENT_CHOICES: &[Node] = - &[TABLE_PK, TABLE_UNIQUE, TABLE_CHECK, TABLE_FK_NAMED, TABLE_FK, COLUMN_DEF]; +static ELEMENT_CHOICES: &[Node] = &[ + TABLE_PK, + TABLE_UNIQUE, + TABLE_CHECK, + TABLE_FK_NAMED, + TABLE_FK, + COLUMN_DEF, +]; const ELEMENT_INNER: Node = Node::Choice(ELEMENT_CHOICES); // Issue #4: wrap the element slot in `IntroProse` so a fresh element // position (`create table T (` and after every `,`) surfaces a prose @@ -495,18 +501,31 @@ mod tests { let mut ctx = WalkContext::new(); let mut path = MatchedPath::new(); let mut per_byte = Vec::new(); - match walk_node(input, 0, &SQL_CREATE_TABLE_SHAPE, &mut ctx, &mut path, &mut per_byte) { + match walk_node( + input, + 0, + &SQL_CREATE_TABLE_SHAPE, + &mut ctx, + &mut path, + &mut per_byte, + ) { NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(), _ => false, } } fn good(input: &str) { - assert!(walks(input), "{input:?} should be a valid CREATE TABLE tail"); + assert!( + walks(input), + "{input:?} should be a valid CREATE TABLE tail" + ); } fn bad(input: &str) { - assert!(!walks(input), "{input:?} should NOT walk as a complete CREATE TABLE tail"); + assert!( + !walks(input), + "{input:?} should NOT walk as a complete CREATE TABLE tail" + ); } #[test] @@ -638,7 +657,9 @@ mod tests { good("table t (id int, ref int references other(id))"); good("table t (id int, ref int references other)"); // bare ref good("table t (id int, ref int references other(id) on delete cascade)"); - good("table t (id int, ref int references other(id) on update set null on delete restrict)"); + good( + "table t (id int, ref int references other(id) on update set null on delete restrict)", + ); good("table t (id int, ref int, foreign key (ref) references other(id))"); good("table t (id int, ref int, constraint fk_x foreign key (ref) references other(id))"); good( @@ -691,7 +712,10 @@ mod builder_tests { assert_eq!(name, "t"); assert_eq!( cols, - vec![("id".to_string(), Type::Int), ("name".to_string(), Type::Text)] + vec![ + ("id".to_string(), Type::Int), + ("name".to_string(), Type::Text) + ] ); assert!(pk.is_empty(), "no PK declared"); assert!(!ine); @@ -740,7 +764,10 @@ mod builder_tests { let (_, cols, _, _) = sct("create table t (a varchar(255), b numeric(10, 2))"); assert_eq!( cols, - vec![("a".to_string(), Type::Text), ("b".to_string(), Type::Decimal)] + vec![ + ("a".to_string(), Type::Text), + ("b".to_string(), Type::Decimal) + ] ); } @@ -780,8 +807,7 @@ mod builder_tests { fn redundant_constraints_deduped_off_sole_pk_column() { // ADR-0035 §6.5: advanced mode accepts the redundant spelling // and silently drops the flags off the sole PK column. - match parse_command("create table t (id int primary key not null unique)") - .expect("parses") + match parse_command("create table t (id int primary key not null unique)").expect("parses") { Command::SqlCreateTable { columns, @@ -944,8 +970,7 @@ mod builder_tests { // depth 2, not an element boundary, so the following `check` // is still column-level. A naive "reset on any comma" would // misclassify it as table-level (the §4.2 probe). - let (cols, checks) = - parse_sct_checks("create table t (n numeric(10, 2) check (n > 0))"); + let (cols, checks) = parse_sct_checks("create table t (n numeric(10, 2) check (n > 0))"); assert_eq!(col(&cols, "n").check_sql.as_deref(), Some("n > 0")); assert!(checks.is_empty(), "no table-level CHECK was produced"); } @@ -977,8 +1002,7 @@ mod builder_tests { fn table_check_before_a_later_column_is_table_level() { // A CHECK element that appears between columns (not after a // column's type) is table-level even though more columns follow. - let (cols, checks) = - parse_sct_checks("create table t (a int, check (a > 0), b int)"); + let (cols, checks) = parse_sct_checks("create table t (a int, check (a > 0), b int)"); assert_eq!(checks, vec!["a > 0".to_string()]); assert!(col(&cols, "a").check_sql.is_none() && col(&cols, "b").check_sql.is_none()); } @@ -1004,7 +1028,10 @@ mod builder_tests { assert_eq!(fk.parent_columns, Some(vec!["id".to_string()])); assert_eq!(fk.on_delete, ReferentialAction::NoAction); assert_eq!(fk.on_update, ReferentialAction::NoAction); - assert!(fk.inline, "a column-level `references` is an inline FK (ADR-0043 D4)"); + assert!( + fk.inline, + "a column-level `references` is an inline FK (ADR-0043 D4)" + ); } #[test] @@ -1012,14 +1039,19 @@ mod builder_tests { // The table-level `FOREIGN KEY (...)` form is not inline, so it can // carry a multi-column reference and never triggers the inline // "use the table-level form" hint (ADR-0043 D4). - let fks = parse_sct_fks("create table t (id int, pid int, foreign key (pid) references parent(id))"); + let fks = parse_sct_fks( + "create table t (id int, pid int, foreign key (pid) references parent(id))", + ); assert!(!fks[0].inline, "table-level FOREIGN KEY is not inline"); } #[test] fn bare_inline_reference_has_no_parent_column() { let fks = parse_sct_fks("create table t (id int, pid int references parent)"); - assert_eq!(fks[0].parent_columns, None, "bare REFERENCES — resolved at execution"); + assert_eq!( + fks[0].parent_columns, None, + "bare REFERENCES — resolved at execution" + ); assert_eq!(fks[0].parent_table, "parent"); assert_eq!(fks[0].child_columns, vec!["pid".to_string()]); } @@ -1047,8 +1079,9 @@ mod builder_tests { #[test] fn table_level_foreign_key_captured() { - let fks = - parse_sct_fks("create table t (id int, pid int, foreign key (pid) references parent(id))"); + let fks = parse_sct_fks( + "create table t (id int, pid int, foreign key (pid) references parent(id))", + ); assert_eq!(fks.len(), 1); assert_eq!(fks[0].name, None); assert_eq!(fks[0].child_columns, vec!["pid".to_string()]); @@ -1073,8 +1106,20 @@ mod builder_tests { foreign key (a) references p(id), foreign key (b) references q(id))", ); assert_eq!(fks.len(), 2); - assert_eq!((fks[0].child_columns[0].as_str(), fks[0].parent_table.as_str()), ("a", "p")); - assert_eq!((fks[1].child_columns[0].as_str(), fks[1].parent_table.as_str()), ("b", "q")); + assert_eq!( + ( + fks[0].child_columns[0].as_str(), + fks[0].parent_table.as_str() + ), + ("a", "p") + ); + assert_eq!( + ( + fks[1].child_columns[0].as_str(), + fks[1].parent_table.as_str() + ), + ("b", "q") + ); } #[test] @@ -1108,7 +1153,12 @@ mod builder_tests { assert_eq!(foreign_keys[0].child_columns, vec!["pid".to_string()]); // the column-level CHECK still attaches to `pid` assert_eq!( - columns.iter().find(|c| c.name == "pid").unwrap().check_sql.as_deref(), + columns + .iter() + .find(|c| c.name == "pid") + .unwrap() + .check_sql + .as_deref(), Some("pid > 0") ); // the table-level CHECK is captured separately diff --git a/src/dsl/grammar/sql_delete.rs b/src/dsl/grammar/sql_delete.rs index b0ac701..f7b7dc1 100644 --- a/src/dsl/grammar/sql_delete.rs +++ b/src/dsl/grammar/sql_delete.rs @@ -82,7 +82,14 @@ mod tests { let mut ctx = WalkContext::new(); let mut path = MatchedPath::new(); let mut per_byte = Vec::new(); - match walk_node(input, 0, &SQL_DELETE_SHAPE, &mut ctx, &mut path, &mut per_byte) { + match walk_node( + input, + 0, + &SQL_DELETE_SHAPE, + &mut ctx, + &mut path, + &mut per_byte, + ) { NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(), _ => false, } @@ -93,7 +100,10 @@ mod tests { } fn bad(input: &str) { - assert!(!walks(input), "{input:?} should NOT walk as a complete DELETE tail"); + assert!( + !walks(input), + "{input:?} should NOT walk as a complete DELETE tail" + ); } #[test] diff --git a/src/dsl/grammar/sql_expr.rs b/src/dsl/grammar/sql_expr.rs index 8b33eb7..51c27bd 100644 --- a/src/dsl/grammar/sql_expr.rs +++ b/src/dsl/grammar/sql_expr.rs @@ -82,19 +82,16 @@ const EXPR_IDENT: Node = Node::Ident { writes_table: false, writes_column: false, writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; // ================================================================= // or_expr := and_expr ( OR and_expr )* — the fragment entry point // ================================================================= -static OR_TAIL_NODES: &[Node] = &[ - Node::Word(Word::keyword("or")), - Node::Subgrammar(&AND_EXPR), -]; +static OR_TAIL_NODES: &[Node] = &[Node::Word(Word::keyword("or")), Node::Subgrammar(&AND_EXPR)]; static OR_TAIL: Node = Node::Seq(OR_TAIL_NODES); static SQL_OR_EXPR_NODES: &[Node] = &[ Node::Subgrammar(&AND_EXPR), @@ -140,10 +137,7 @@ static NOT_FORM_NODES: &[Node] = &[ Node::Word(Word::keyword("not")), Node::Subgrammar(&NOT_EXPR), ]; -static NOT_EXPR_CHOICES: &[Node] = &[ - Node::Seq(NOT_FORM_NODES), - Node::Subgrammar(&PREDICATE), -]; +static NOT_EXPR_CHOICES: &[Node] = &[Node::Seq(NOT_FORM_NODES), Node::Subgrammar(&PREDICATE)]; static NOT_EXPR: Node = Node::Choice(NOT_EXPR_CHOICES); // ================================================================= @@ -156,10 +150,7 @@ static NOT_EXPR: Node = Node::Choice(NOT_EXPR_CHOICES); // needs. ADR-0026's DSL grammar made the tail mandatory because it // forbade a bare column as a boolean; SQL does not. -static PREDICATE_NODES: &[Node] = &[ - Node::Subgrammar(&ADDITIVE), - Node::Optional(&PREDICATE_TAIL), -]; +static PREDICATE_NODES: &[Node] = &[Node::Subgrammar(&ADDITIVE), Node::Optional(&PREDICATE_TAIL)]; static PREDICATE: Node = Node::Seq(PREDICATE_NODES); // ---- cmp_op := <= | <> | >= | != | < | > | = -------------------- @@ -181,10 +172,7 @@ static CMP_OP_CHOICES: &[Node] = &[ // ---- predicate_tail branches ------------------------------------ /// `cmp_op additive`. -static COMPARE_FORM_NODES: &[Node] = &[ - Node::Choice(CMP_OP_CHOICES), - Node::Subgrammar(&ADDITIVE), -]; +static COMPARE_FORM_NODES: &[Node] = &[Node::Choice(CMP_OP_CHOICES), Node::Subgrammar(&ADDITIVE)]; /// `IS [NOT] NULL`. static IS_NULL_NODES: &[Node] = &[ @@ -265,11 +253,7 @@ static PREDICATE_TAIL: Node = Node::Choice(PREDICATE_TAIL_CHOICES); // additive := multiplicative ( ( + | - | || ) multiplicative )* // ================================================================= -static ADD_OP_CHOICES: &[Node] = &[ - Node::Punct('+'), - Node::Punct('-'), - Node::Literal("||"), -]; +static ADD_OP_CHOICES: &[Node] = &[Node::Punct('+'), Node::Punct('-'), Node::Literal("||")]; static ADD_TAIL_NODES: &[Node] = &[ Node::Choice(ADD_OP_CHOICES), Node::Subgrammar(&MULTIPLICATIVE), @@ -289,15 +273,8 @@ static ADDITIVE: Node = Node::Seq(ADDITIVE_NODES); // multiplicative := unary ( ( * | / | % ) unary )* // ================================================================= -static MUL_OP_CHOICES: &[Node] = &[ - Node::Punct('*'), - Node::Punct('/'), - Node::Punct('%'), -]; -static MUL_TAIL_NODES: &[Node] = &[ - Node::Choice(MUL_OP_CHOICES), - Node::Subgrammar(&UNARY), -]; +static MUL_OP_CHOICES: &[Node] = &[Node::Punct('*'), Node::Punct('/'), Node::Punct('%')]; +static MUL_TAIL_NODES: &[Node] = &[Node::Choice(MUL_OP_CHOICES), Node::Subgrammar(&UNARY)]; static MUL_TAIL: Node = Node::Seq(MUL_TAIL_NODES); static MULTIPLICATIVE_NODES: &[Node] = &[ Node::Subgrammar(&UNARY), @@ -314,14 +291,8 @@ static MULTIPLICATIVE: Node = Node::Seq(MULTIPLICATIVE_NODES); // ================================================================= static SIGN_CHOICES: &[Node] = &[Node::Punct('-'), Node::Punct('+')]; -static UNARY_SIGN_NODES: &[Node] = &[ - Node::Choice(SIGN_CHOICES), - Node::Subgrammar(&UNARY), -]; -static UNARY_CHOICES: &[Node] = &[ - Node::Seq(UNARY_SIGN_NODES), - Node::Subgrammar(&PRIMARY), -]; +static UNARY_SIGN_NODES: &[Node] = &[Node::Choice(SIGN_CHOICES), Node::Subgrammar(&UNARY)]; +static UNARY_CHOICES: &[Node] = &[Node::Seq(UNARY_SIGN_NODES), Node::Subgrammar(&PRIMARY)]; static UNARY: Node = Node::Choice(UNARY_CHOICES); // ================================================================= @@ -402,10 +373,7 @@ static SIMPLE_CASE_NODES: &[Node] = &[ Node::Optional(&ELSE_CLAUSE), Node::Word(Word::keyword("end")), ]; -static CASE_BODY_CHOICES: &[Node] = &[ - Node::Seq(SEARCHED_CASE_NODES), - Node::Seq(SIMPLE_CASE_NODES), -]; +static CASE_BODY_CHOICES: &[Node] = &[Node::Seq(SEARCHED_CASE_NODES), Node::Seq(SIMPLE_CASE_NODES)]; static CASE_NODES: &[Node] = &[ Node::Word(Word::keyword("case")), Node::Choice(CASE_BODY_CHOICES), @@ -467,14 +435,11 @@ const QUALIFIED_REF_IDENT: Node = Node::Ident { writes_table: false, writes_column: false, writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; -static QUALIFIED_REF_TAIL_NODES: &[Node] = &[ - Node::Punct('.'), - QUALIFIED_REF_IDENT, -]; +static QUALIFIED_REF_TAIL_NODES: &[Node] = &[Node::Punct('.'), QUALIFIED_REF_IDENT]; static NAME_OR_CALL_TAIL_CHOICES: &[Node] = &[ Node::Seq(QUALIFIED_REF_TAIL_NODES), @@ -531,7 +496,10 @@ mod tests { /// Assert `input` is *not* a complete SQL expression. fn bad(input: &str) { - assert!(!walks(input), "{input:?} should NOT walk as a complete expression"); + assert!( + !walks(input), + "{input:?} should NOT walk as a complete expression" + ); } #[test] @@ -643,13 +611,13 @@ mod tests { #[test] fn malformed_expressions_do_not_walk() { - bad("a +"); // dangling operator - bad("a in b"); // IN requires a parenthesised list - bad("= 1"); // no left operand - bad("a = "); // no right operand - bad("case a end"); // CASE with no WHEN clause - bad("and b"); // leading connective - bad("upper("); // unclosed call + bad("a +"); // dangling operator + bad("a in b"); // IN requires a parenthesised list + bad("= 1"); // no left operand + bad("a = "); // no right operand + bad("case a end"); // CASE with no WHEN clause + bad("and b"); // leading connective + bad("upper("); // unclosed call } #[test] @@ -680,9 +648,9 @@ mod tests { // The optional tail dispatches `.identifier` (qualified // ref) vs `(args)` (function call) by first token — a // bare ident remains a column ref. - good("foo(x)"); // function call - good("foo.bar"); // qualified ref - good("foo"); // bare ref + good("foo(x)"); // function call + good("foo.bar"); // qualified ref + good("foo"); // bare ref } #[test] diff --git a/src/dsl/grammar/sql_insert.rs b/src/dsl/grammar/sql_insert.rs index ab3411e..409781f 100644 --- a/src/dsl/grammar/sql_insert.rs +++ b/src/dsl/grammar/sql_insert.rs @@ -120,7 +120,10 @@ fn target_value_columns(ctx: &WalkContext) -> Vec { listed .iter() .filter_map(|name| { - table_cols.iter().find(|c| c.name.eq_ignore_ascii_case(name)).cloned() + table_cols + .iter() + .find(|c| c.name.eq_ignore_ascii_case(name)) + .cloned() }) .collect() }, @@ -148,7 +151,11 @@ fn target_value_columns(ctx: &WalkContext) -> Vec { fn tuple_value_list(ctx: &WalkContext, source: &str, pos: usize) -> Node { let cols = target_value_columns(ctx); let (count, closed) = count_tuple_values(source, pos); - let arity_ok = if closed { count == cols.len() } else { count <= cols.len() }; + let arity_ok = if closed { + count == cols.len() + } else { + count <= cols.len() + }; if !cols.is_empty() && arity_ok { Node::DynamicSubgrammar(sql_value_list) } else { @@ -304,8 +311,10 @@ static DO_UPDATE_NODES: &[Node] = &[ /// the enclosing Seq, each branch's FIRST token (`nothing` vs /// `update`) disambiguates, so a non-match of branch 0 is a clean /// `NoMatch` that falls through to branch 1. -static DO_ACTION_CHOICES: &[Node] = - &[Node::Word(Word::keyword("nothing")), Node::Seq(DO_UPDATE_NODES)]; +static DO_ACTION_CHOICES: &[Node] = &[ + Node::Word(Word::keyword("nothing")), + Node::Seq(DO_UPDATE_NODES), +]; // `const` — used by value in `ON_CONFLICT_CLAUSE_NODES`. const DO_ACTION: Node = Node::Choice(DO_ACTION_CHOICES); @@ -361,7 +370,14 @@ mod tests { let mut ctx = WalkContext::new(); let mut path = MatchedPath::new(); let mut per_byte = Vec::new(); - match walk_node(input, 0, &SQL_INSERT_SHAPE, &mut ctx, &mut path, &mut per_byte) { + match walk_node( + input, + 0, + &SQL_INSERT_SHAPE, + &mut ctx, + &mut path, + &mut per_byte, + ) { NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(), _ => false, } @@ -372,7 +388,10 @@ mod tests { } fn bad(input: &str) { - assert!(!walks(input), "{input:?} should NOT walk as a complete INSERT tail"); + assert!( + !walks(input), + "{input:?} should NOT walk as a complete INSERT tail" + ); } #[test] @@ -418,8 +437,12 @@ mod tests { // 3h: ON CONFLICT … DO NOTHING / DO UPDATE (ADR-0033 §9). good("into t (id, name) values (1, 'x') on conflict (id) do nothing"); good("into t (id, name) values (1, 'x') on conflict do nothing"); - good("into t (id, name) values (1, 'x') on conflict (id) do update set name = excluded.name"); - good("into t (id, name) values (1, 'x') on conflict (id) do update set name = 'y' where id > 0"); + good( + "into t (id, name) values (1, 'x') on conflict (id) do update set name = excluded.name", + ); + good( + "into t (id, name) values (1, 'x') on conflict (id) do update set name = 'y' where id > 0", + ); // Multi-column conflict target + multi-assignment DO UPDATE. good("into t (a, b) values (1, 2) on conflict (a, b) do update set b = excluded.b, a = 9"); // ON CONFLICT composes with RETURNING (order: row source, diff --git a/src/dsl/grammar/sql_select.rs b/src/dsl/grammar/sql_select.rs index a8d40b6..0d488a5 100644 --- a/src/dsl/grammar/sql_select.rs +++ b/src/dsl/grammar/sql_select.rs @@ -141,8 +141,15 @@ static EMPTY_NOMATCH: Node = Node::Choice(&[]); /// suffix keywords. `as` is not listed — the AS-form alias is a /// separate `Choice` branch that fires before the lookahead. const PROJECTION_FOLLOW_SET: &[&str] = &[ - "from", "where", "group", "order", "having", "limit", - "union", "intersect", "except", + "from", + "where", + "group", + "order", + "having", + "limit", + "union", + "intersect", + "except", // `returning` belongs to an enclosing DML statement // (`INSERT … SELECT … RETURNING …`, ADR-0033 §5), never to a // projection item's bare alias — so a no-FROM SELECT row source @@ -158,9 +165,21 @@ const PROJECTION_FOLLOW_SET: &[&str] = &[ /// only when `b` has no alias — `on` is not a base-table name a /// learner would type as an alias. const TABLE_SOURCE_FOLLOW_SET: &[&str] = &[ - "where", "group", "order", "having", "limit", - "union", "intersect", "except", - "inner", "left", "right", "full", "cross", "join", "on", + "where", + "group", + "order", + "having", + "limit", + "union", + "intersect", + "except", + "inner", + "left", + "right", + "full", + "cross", + "join", + "on", // `returning` belongs to an enclosing DML statement // (`INSERT … SELECT … FROM t RETURNING …`, ADR-0033 §5), so the // SELECT row source must not read it as table `t`'s bare alias. @@ -172,15 +191,9 @@ fn peek_next_ident_lower(source: &str, pos: usize) -> Option { consume_ident(source, p).map(|(s, e)| source[s..e].to_ascii_lowercase()) } -fn projection_bare_alias_factory( - _: &WalkContext, - source: &str, - pos: usize, -) -> Node { +fn projection_bare_alias_factory(_: &WalkContext, source: &str, pos: usize) -> Node { match peek_next_ident_lower(source, pos) { - Some(word) - if PROJECTION_FOLLOW_SET.iter().any(|k| *k == word) => - { + Some(word) if PROJECTION_FOLLOW_SET.iter().any(|k| *k == word) => { Node::Subgrammar(&EMPTY_NOMATCH) } Some(_) => PROJECTION_BARE_ALIAS_IDENT, @@ -188,15 +201,9 @@ fn projection_bare_alias_factory( } } -fn table_source_bare_alias_factory( - _: &WalkContext, - source: &str, - pos: usize, -) -> Node { +fn table_source_bare_alias_factory(_: &WalkContext, source: &str, pos: usize) -> Node { match peek_next_ident_lower(source, pos) { - Some(word) - if TABLE_SOURCE_FOLLOW_SET.iter().any(|k| *k == word) => - { + Some(word) if TABLE_SOURCE_FOLLOW_SET.iter().any(|k| *k == word) => { Node::Subgrammar(&EMPTY_NOMATCH) } Some(_) => TABLE_SOURCE_BARE_ALIAS_IDENT, @@ -237,14 +244,12 @@ const TABLE_SOURCE_BARE_ALIAS_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: true, -writes_cte_name: false, -writes_projection_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; -static PROJECTION_AS_ALIAS_NODES: &[Node] = &[ - Node::Word(Word::keyword("as")), - PROJECTION_BARE_ALIAS_IDENT, -]; +static PROJECTION_AS_ALIAS_NODES: &[Node] = + &[Node::Word(Word::keyword("as")), PROJECTION_BARE_ALIAS_IDENT]; static PROJECTION_AS_ALIAS: Node = Node::Seq(PROJECTION_AS_ALIAS_NODES); static TABLE_SOURCE_AS_ALIAS_NODES: &[Node] = &[ @@ -258,17 +263,14 @@ static PROJECTION_ALIAS_CHOICES: &[Node] = &[ Node::Lookahead(projection_bare_alias_factory), ]; static PROJECTION_ALIAS_CHOICE: Node = Node::Choice(PROJECTION_ALIAS_CHOICES); -static PROJECTION_ALIAS_OPTIONAL: Node = - Node::Optional(&PROJECTION_ALIAS_CHOICE); +static PROJECTION_ALIAS_OPTIONAL: Node = Node::Optional(&PROJECTION_ALIAS_CHOICE); static TABLE_SOURCE_ALIAS_CHOICES: &[Node] = &[ Node::Subgrammar(&TABLE_SOURCE_AS_ALIAS), Node::Lookahead(table_source_bare_alias_factory), ]; -static TABLE_SOURCE_ALIAS_CHOICE: Node = - Node::Choice(TABLE_SOURCE_ALIAS_CHOICES); -static TABLE_SOURCE_ALIAS_OPTIONAL: Node = - Node::Optional(&TABLE_SOURCE_ALIAS_CHOICE); +static TABLE_SOURCE_ALIAS_CHOICE: Node = Node::Choice(TABLE_SOURCE_ALIAS_CHOICES); +static TABLE_SOURCE_ALIAS_OPTIONAL: Node = Node::Optional(&TABLE_SOURCE_ALIAS_CHOICE); // ================================================================= // Projection item @@ -282,16 +284,13 @@ const QUALIFIED_STAR_QUALIFIER: Node = Node::Ident { writes_table: false, writes_column: false, writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; -static QUALIFIED_STAR_NODES: &[Node] = &[ - QUALIFIED_STAR_QUALIFIER, - Node::Punct('.'), - Node::Punct('*'), -]; +static QUALIFIED_STAR_NODES: &[Node] = + &[QUALIFIED_STAR_QUALIFIER, Node::Punct('.'), Node::Punct('*')]; static QUALIFIED_STAR: Node = Node::Seq(QUALIFIED_STAR_NODES); static PROJECTION_EXPR_ITEM_NODES: &[Node] = &[ @@ -310,11 +309,7 @@ static PROJECTION_EXPR_ITEM: Node = Node::Seq(PROJECTION_EXPR_ITEM_NODES); /// ambiguity between `t.*` and `sql_expr` (which can match a /// bare `t`), since the walker's `Choice` doesn't backtrack on /// a committed match. -fn projection_item_factory( - _: &WalkContext, - source: &str, - pos: usize, -) -> Node { +fn projection_item_factory(_: &WalkContext, source: &str, pos: usize) -> Node { let p = skip_whitespace(source, pos); let bytes = source.as_bytes(); if bytes.get(p) == Some(&b'*') { @@ -363,8 +358,7 @@ static DISTINCT_OR_ALL_CHOICES: &[Node] = &[ Node::Word(Word::keyword("all")), ]; static DISTINCT_OR_ALL_CHOICE: Node = Node::Choice(DISTINCT_OR_ALL_CHOICES); -static DISTINCT_OR_ALL_OPTIONAL: Node = - Node::Optional(&DISTINCT_OR_ALL_CHOICE); +static DISTINCT_OR_ALL_OPTIONAL: Node = Node::Optional(&DISTINCT_OR_ALL_CHOICE); // ================================================================= // Table source (FROM / JOIN target) @@ -379,8 +373,8 @@ const TABLE_NAME_IDENT: Node = Node::Ident { writes_column: false, writes_user_listed_column: false, writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; static TABLE_SOURCE_NODES: &[Node] = &[ @@ -395,8 +389,7 @@ static TABLE_SOURCE: Node = Node::Seq(TABLE_SOURCE_NODES); const JOIN_WORD: Node = Node::Word(Word::keyword("join")); const ON_WORD: Node = Node::Word(Word::keyword("on")); -static OUTER_OPTIONAL: Node = - Node::Optional(&Node::Word(Word::keyword("outer"))); +static OUTER_OPTIONAL: Node = Node::Optional(&Node::Word(Word::keyword("outer"))); // `INNER JOIN` and bare `JOIN` are split into two Choice // branches so each branch has a distinct leading keyword @@ -585,8 +578,7 @@ static SET_OP_CHOICES: &[Node] = &[ ]; static SET_OP: Node = Node::Choice(SET_OP_CHOICES); -static SET_OP_TAIL_NODES: &[Node] = - &[Node::Subgrammar(&SET_OP), Node::Subgrammar(&SELECT_CORE)]; +static SET_OP_TAIL_NODES: &[Node] = &[Node::Subgrammar(&SET_OP), Node::Subgrammar(&SELECT_CORE)]; static SET_OP_TAIL: Node = Node::Seq(SET_OP_TAIL_NODES); static PLAIN_COMPOUND_NODES: &[Node] = &[ @@ -619,8 +611,7 @@ static WITH_PREFIXED_COMPOUND_NODES: &[Node] = &[ Node::Subgrammar(&WITH_CLAUSE), Node::Subgrammar(&PLAIN_COMPOUND), ]; -static WITH_PREFIXED_COMPOUND: Node = - Node::Seq(WITH_PREFIXED_COMPOUND_NODES); +static WITH_PREFIXED_COMPOUND: Node = Node::Seq(WITH_PREFIXED_COMPOUND_NODES); static COMPOUND_CHOICES: &[Node] = &[ Node::Subgrammar(&WITH_PREFIXED_COMPOUND), @@ -659,9 +650,9 @@ const CTE_COLUMN_IDENT: Node = Node::Ident { writes_table: false, writes_column: false, writes_user_listed_column: false, -writes_table_alias: false, -writes_cte_name: false, -writes_projection_alias: false, + writes_table_alias: false, + writes_cte_name: false, + writes_projection_alias: false, }; static CTE_COLUMN_LIST_NODES: &[Node] = &[ @@ -674,18 +665,13 @@ static CTE_COLUMN_LIST_NODES: &[Node] = &[ RPAREN, ]; static CTE_COLUMN_LIST_SEQ: Node = Node::Seq(CTE_COLUMN_LIST_NODES); -static CTE_COLUMN_LIST_OPTIONAL: Node = - Node::Optional(&CTE_COLUMN_LIST_SEQ); +static CTE_COLUMN_LIST_OPTIONAL: Node = Node::Optional(&CTE_COLUMN_LIST_SEQ); // CTE body recursion pushes a fresh lexical scope frame (ADR- // 0032 §4 / §10.2). Subqueries in `sql_expr.rs` do the same; // the top-level statement's own COMPOUND embedding does not // (it shares the implicit bottom frame). -static CTE_BODY_NODES: &[Node] = &[ - LPAREN, - Node::ScopedSubgrammar(&SQL_SELECT_COMPOUND), - RPAREN, -]; +static CTE_BODY_NODES: &[Node] = &[LPAREN, Node::ScopedSubgrammar(&SQL_SELECT_COMPOUND), RPAREN]; static CTE_BODY: Node = Node::Seq(CTE_BODY_NODES); static CTE_DEF_NODES: &[Node] = &[ @@ -807,9 +793,7 @@ mod tests { let mut path = MatchedPath::new(); let mut per_byte = Vec::new(); match walk_node(input, 0, fragment, &mut ctx, &mut path, &mut per_byte) { - NodeWalkResult::Matched { end, .. } => { - input[end..].trim().is_empty() - } + NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(), _ => false, } } @@ -819,10 +803,7 @@ mod tests { } fn good(input: &str) { - assert!( - walks(input), - "{input:?} should be a valid SELECT statement" - ); + assert!(walks(input), "{input:?} should be a valid SELECT statement"); } fn bad(input: &str) { @@ -1051,16 +1032,12 @@ mod tests { #[test] fn set_op_chain() { - good( - "select a from t union select b from u intersect select c from v", - ); + good("select a from t union select b from u intersect select c from v"); } #[test] fn set_op_with_outer_order_by_and_limit() { - good( - "select a from t union select b from u order by a limit 10", - ); + good("select a from t union select b from u order by a limit 10"); } // ----- ORDER BY / LIMIT / OFFSET ----- @@ -1126,16 +1103,12 @@ mod tests { #[test] fn recursive_cte() { - good( - "with recursive r as (select 1 union all select 2) select * from r", - ); + good("with recursive r as (select 1 union all select 2) select * from r"); } #[test] fn multiple_ctes() { - good( - "with a as (select 1), b as (select 2) select * from a union select * from b", - ); + good("with a as (select 1), b as (select 2) select * from a union select * from b"); } // ----- subquery shapes (recursion through SQL_SELECT_COMPOUND) ----- @@ -1147,9 +1120,7 @@ mod tests { #[test] fn nested_cte_body_with_union() { - good( - "with x as (select 1 union select 2) select * from x", - ); + good("with x as (select 1 union select 2) select * from x"); } // ----- case insensitivity / spacing ----- @@ -1363,9 +1334,7 @@ mod tests { #[test] fn in_subquery_in_where_clause() { good("select * from t where id in (select user_id from orders)"); - good( - "select * from customers where id not in (select customer_id from blocklist)", - ); + good("select * from customers where id not in (select customer_id from blocklist)"); } #[test] @@ -1378,9 +1347,7 @@ mod tests { #[test] fn nested_subqueries() { - good( - "select * from t where x in (select y from u where y in (select z from v))", - ); + good("select * from t where x in (select y from u where y in (select z from v))"); } #[test] @@ -1393,8 +1360,6 @@ mod tests { #[test] fn cte_body_references_qualified_columns() { - good( - "with x as (select t.name, t.age from t) select x.name from x", - ); + good("with x as (select t.name, t.age from t) select x.name from x"); } } diff --git a/src/dsl/grammar/sql_update.rs b/src/dsl/grammar/sql_update.rs index 0c5294c..0caf1b1 100644 --- a/src/dsl/grammar/sql_update.rs +++ b/src/dsl/grammar/sql_update.rs @@ -119,7 +119,14 @@ mod tests { let mut ctx = WalkContext::new(); let mut path = MatchedPath::new(); let mut per_byte = Vec::new(); - match walk_node(input, 0, &SQL_UPDATE_SHAPE, &mut ctx, &mut path, &mut per_byte) { + match walk_node( + input, + 0, + &SQL_UPDATE_SHAPE, + &mut ctx, + &mut path, + &mut per_byte, + ) { NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(), _ => false, } @@ -130,7 +137,10 @@ mod tests { } fn bad(input: &str) { - assert!(!walks(input), "{input:?} should NOT walk as a complete UPDATE tail"); + assert!( + !walks(input), + "{input:?} should NOT walk as a complete UPDATE tail" + ); } #[test] diff --git a/src/dsl/mod.rs b/src/dsl/mod.rs index 0c6b795..437fa73 100644 --- a/src/dsl/mod.rs +++ b/src/dsl/mod.rs @@ -21,9 +21,9 @@ pub mod walker; pub use action::ReferentialAction; pub use command::{ - AlterTableAction, AppCommand, ChangeColumnMode, ColumnSpec, Command, CompareOp, CopyScope, Expr, - IndexSelector, MessagesValue, ModeValue, Operand, Predicate, RelationshipSelector, RowFilter, - ShowListKind, SqlForeignKey, + AlterTableAction, AppCommand, ChangeColumnMode, ColumnSpec, Command, CompareOp, CopyScope, + Expr, IndexSelector, MessagesValue, ModeValue, Operand, Predicate, RelationshipSelector, + RowFilter, ShowListKind, SqlForeignKey, }; pub use parser::{ParseError, parse_command}; pub use types::Type; diff --git a/src/dsl/parser.rs b/src/dsl/parser.rs index 42ad61d..497f851 100644 --- a/src/dsl/parser.rs +++ b/src/dsl/parser.rs @@ -55,10 +55,9 @@ pub enum ParseError { impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Invalid { message, .. } => f.write_str(&crate::t!( - "parse.error_wrapper", - detail = message, - )), + Self::Invalid { message, .. } => { + f.write_str(&crate::t!("parse.error_wrapper", detail = message,)) + } Self::Empty => f.write_str(&crate::t!("parse.empty")), } } @@ -125,10 +124,7 @@ pub fn parse_command_with_schema( /// Schemaless, mode-aware parse (ADR-0030 §2). In `Mode::Simple` /// the walker gates SQL-only commands and produces the /// "this is SQL" hint instead of executing them. -pub fn parse_command_in_mode( - input: &str, - mode: Mode, -) -> Result { +pub fn parse_command_in_mode(input: &str, mode: Mode) -> Result { parse_command_inner(input, None, mode) } @@ -185,10 +181,8 @@ fn unknown_command_error(source: &str) -> ParseError { .collect(); let joined = oxford_join(&entries); let start = skip_whitespace(source, 0); - let (position, found_word) = consume_ident(source, start).map_or_else( - || (start, None), - |(s, e)| (s, Some(&source[s..e])), - ); + let (position, found_word) = consume_ident(source, start) + .map_or_else(|| (start, None), |(s, e)| (s, Some(&source[s..e]))); let message = found_word.map_or_else( || format!("expected one of {joined}"), |w| format!("expected one of {joined}, found `{w}`"), @@ -1034,19 +1028,22 @@ mod tests { false, ); assert_eq!( - ok("add 1:n relationship from Customers.Id to Orders.CustId on delete cascade on update set null"), + ok( + "add 1:n relationship from Customers.Id to Orders.CustId on delete cascade on update set null" + ), expected ); assert_eq!( - ok("add 1:n relationship from Customers.Id to Orders.CustId on update set null on delete cascade"), + ok( + "add 1:n relationship from Customers.Id to Orders.CustId on update set null on delete cascade" + ), expected ); } #[test] fn add_relationship_repeated_clause_errors() { - let e = - err("add 1:n relationship from C.id to O.cid on delete cascade on delete restrict"); + let e = err("add 1:n relationship from C.id to O.cid on delete cascade on delete restrict"); match e { ParseError::Invalid { message, .. } => { assert!(message.contains("specified twice"), "{message}"); @@ -1073,7 +1070,9 @@ mod tests { #[test] fn add_relationship_with_name_actions_and_flag() { assert_eq!( - ok("add 1:n relationship as cust_orders from Customers.Id to Orders.CustId on delete cascade on update no action --create-fk"), + ok( + "add 1:n relationship as cust_orders from Customers.Id to Orders.CustId on delete cascade on update no action --create-fk" + ), rel( Some("cust_orders"), ("Customers", "Id"), @@ -1300,10 +1299,7 @@ mod tests { #[test] fn advanced_ambiguous_update_routes_to_sql() { assert!(matches!( - parse_command_in_mode( - "update Orders set total = 0 where id = 1", - Mode::Advanced, - ), + parse_command_in_mode("update Orders set total = 0 where id = 1", Mode::Advanced,), Ok(Command::SqlUpdate { .. }) )); } @@ -1399,10 +1395,7 @@ mod tests { // in advanced mode)" pointer is added at the hint layer // (input_render), not in the parsed command/error here. assert!(matches!( - parse_command_in_mode( - "delete from Orders where id = 1 returning *", - Mode::Simple, - ), + parse_command_in_mode("delete from Orders where id = 1 returning *", Mode::Simple,), Err(ParseError::Invalid { .. }) )); } diff --git a/src/dsl/shortid.rs b/src/dsl/shortid.rs index b591bd7..6126115 100644 --- a/src/dsl/shortid.rs +++ b/src/dsl/shortid.rs @@ -9,8 +9,7 @@ use rand::RngExt; /// Base58 alphabet — Bitcoin-style. 0 / O / I / l are excluded /// because they are easily confused in print. -const ALPHABET: &[u8; 58] = - b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; +const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; const DEFAULT_LEN: usize = 10; diff --git a/src/dsl/sql_functions.rs b/src/dsl/sql_functions.rs index 7daa93d..e924c48 100644 --- a/src/dsl/sql_functions.rs +++ b/src/dsl/sql_functions.rs @@ -43,29 +43,9 @@ /// - **Broader scalars:** `date`, `datetime`, `hex`, `ifnull`, /// `instr`, `nullif`, `random`, `replace`, `strftime`, `typeof`. pub const KNOWN_SQL_FUNCTIONS: &[&str] = &[ - "abs", - "avg", - "coalesce", - "count", - "date", - "datetime", - "hex", - "ifnull", - "instr", - "length", - "lower", - "max", - "min", - "nullif", - "random", - "replace", - "round", - "strftime", - "substr", - "sum", - "trim", - "typeof", - "upper", + "abs", "avg", "coalesce", "count", "date", "datetime", "hex", "ifnull", "instr", "length", + "lower", "max", "min", "nullif", "random", "replace", "round", "strftime", "substr", "sum", + "trim", "typeof", "upper", ]; /// Whether `partial` is a case-insensitive prefix of at least one @@ -80,9 +60,7 @@ pub const KNOWN_SQL_FUNCTIONS: &[&str] = &[ #[must_use] pub fn is_known_function_prefix(partial: &str) -> bool { let lowered = partial.to_lowercase(); - KNOWN_SQL_FUNCTIONS - .iter() - .any(|f| f.starts_with(&lowered)) + KNOWN_SQL_FUNCTIONS.iter().any(|f| f.starts_with(&lowered)) } #[cfg(test)] diff --git a/src/dsl/types.rs b/src/dsl/types.rs index 9ed4ed2..3168af7 100644 --- a/src/dsl/types.rs +++ b/src/dsl/types.rs @@ -59,11 +59,7 @@ impl Type { #[must_use] pub const fn sqlite_strict_type(self) -> &'static str { match self { - Self::Text - | Self::ShortId - | Self::Decimal - | Self::Date - | Self::DateTime => "TEXT", + Self::Text | Self::ShortId | Self::Decimal | Self::Date | Self::DateTime => "TEXT", Self::Int | Self::Serial | Self::Bool => "INTEGER", Self::Real => "REAL", Self::Blob => "BLOB", @@ -107,10 +103,7 @@ impl Type { /// match against a numeric column (ADR-0027, Amendment 1). #[must_use] pub const fn is_numeric(self) -> bool { - matches!( - self, - Self::Int | Self::Real | Self::Decimal | Self::Serial - ) + matches!(self, Self::Int | Self::Real | Self::Decimal | Self::Serial) } /// The user-facing type that an FK column should use to diff --git a/src/dsl/value.rs b/src/dsl/value.rs index 83d356d..be0a9b7 100644 --- a/src/dsl/value.rs +++ b/src/dsl/value.rs @@ -129,13 +129,14 @@ impl Value { fn bind_int(&self, column: &str, ty: Type) -> Result { match self { - Self::Number(n) => n - .parse::() - .map(Bound::Integer) - .map_err(|_| ValueError::Format { - column: column.to_string(), - message: format!("`{n}` is not a valid {ty} (whole number expected)"), - }), + Self::Number(n) => { + n.parse::() + .map(Bound::Integer) + .map_err(|_| ValueError::Format { + column: column.to_string(), + message: format!("`{n}` is not a valid {ty} (whole number expected)"), + }) + } other => Err(ValueError::TypeMismatch { column: column.to_string(), expected_human: format!("a whole number for `{ty}`"), @@ -241,9 +242,7 @@ pub(crate) fn validate_date(s: &str) -> Result<(), String> { // Expect YYYY-MM-DD: 10 chars, two dashes at fixed positions. let bytes = s.as_bytes(); if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' { - return Err(format!( - "`{s}` is not a date in `YYYY-MM-DD` form" - )); + return Err(format!("`{s}` is not a date in `YYYY-MM-DD` form")); } let year = parse_digits(&s[0..4]).ok_or_else(|| format!("`{s}`: invalid year"))?; let month = parse_digits(&s[5..7]).ok_or_else(|| format!("`{s}`: invalid month"))?; @@ -272,7 +271,9 @@ pub(crate) fn validate_datetime(s: &str) -> Result<(), String> { validate_date(date_part)?; let bytes = s.as_bytes(); if bytes[10] != b'T' { - return Err(format!("`{s}`: missing `T` separator between date and time")); + return Err(format!( + "`{s}`: missing `T` separator between date and time" + )); } if bytes[13] != b':' || bytes[16] != b':' { return Err(format!("`{s}`: time portion must be `HH:MM:SS`")); @@ -326,8 +327,14 @@ mod tests { #[test] fn integer_for_int_column() { - assert_eq!(n("42").bind_for_column("c", Type::Int).unwrap(), Bound::Integer(42)); - assert_eq!(n("-7").bind_for_column("c", Type::Int).unwrap(), Bound::Integer(-7)); + assert_eq!( + n("42").bind_for_column("c", Type::Int).unwrap(), + Bound::Integer(42) + ); + assert_eq!( + n("-7").bind_for_column("c", Type::Int).unwrap(), + Bound::Integer(-7) + ); } #[test] @@ -355,7 +362,9 @@ mod tests { #[test] fn shortid_validation_runs_on_text_for_shortid_column() { - let err = t("toolong_xyz_more").bind_for_column("c", Type::ShortId).unwrap_err(); + let err = t("toolong_xyz_more") + .bind_for_column("c", Type::ShortId) + .unwrap_err(); assert!(matches!(err, ValueError::Format { .. })); // Well-formed shortid binds fine. @@ -367,8 +376,14 @@ mod tests { #[test] fn bool_for_bool_column_maps_to_zero_or_one() { - assert_eq!(Value::Bool(true).bind_for_column("c", Type::Bool).unwrap(), Bound::Integer(1)); - assert_eq!(Value::Bool(false).bind_for_column("c", Type::Bool).unwrap(), Bound::Integer(0)); + assert_eq!( + Value::Bool(true).bind_for_column("c", Type::Bool).unwrap(), + Bound::Integer(1) + ); + assert_eq!( + Value::Bool(false).bind_for_column("c", Type::Bool).unwrap(), + Bound::Integer(0) + ); } #[test] @@ -377,13 +392,17 @@ mod tests { t("2025-01-15").bind_for_column("c", Type::Date).unwrap(), Bound::Text("2025-01-15".to_string()) ); - let err = t("2025/01/15").bind_for_column("c", Type::Date).unwrap_err(); + let err = t("2025/01/15") + .bind_for_column("c", Type::Date) + .unwrap_err(); assert!(matches!(err, ValueError::Format { .. })); } #[test] fn date_range_check() { - let err = t("2025-13-01").bind_for_column("c", Type::Date).unwrap_err(); + let err = t("2025-13-01") + .bind_for_column("c", Type::Date) + .unwrap_err(); assert!(matches!(err, ValueError::Format { message, .. } if message.contains("month"))); } diff --git a/src/dsl/walker/driver.rs b/src/dsl/walker/driver.rs index 231bc36..d99f1f2 100644 --- a/src/dsl/walker/driver.rs +++ b/src/dsl/walker/driver.rs @@ -28,12 +28,10 @@ use crate::completion::TableColumn; use crate::dsl::grammar::{HighlightClass, Node, ValidationError}; use crate::dsl::walker::context::WalkContext; use crate::dsl::walker::lex_helpers::{ - consume_bare_path, consume_flag, consume_ident, consume_number_literal, - consume_string_literal, skip_whitespace, -}; -use crate::dsl::walker::outcome::{ - ByteClass, Expectation, MatchedItem, MatchedKind, MatchedPath, + consume_bare_path, consume_flag, consume_ident, consume_number_literal, consume_string_literal, + skip_whitespace, }; +use crate::dsl::walker::outcome::{ByteClass, Expectation, MatchedItem, MatchedKind, MatchedPath}; /// Maximum nesting of `Node::Subgrammar` frames (ADR-0026 §1). /// @@ -77,10 +75,7 @@ static DYNAMIC_CACHE: LazyLock>> = /// Resolve a `DynamicSubgrammar` factory to a `&'static Node`, /// reusing a previously-leaked Node when the factory's inputs /// match a cached entry. -fn resolve_dynamic( - factory: fn(&WalkContext) -> Node, - ctx: &WalkContext, -) -> &'static Node { +fn resolve_dynamic(factory: fn(&WalkContext) -> Node, ctx: &WalkContext) -> &'static Node { let key = DynamicKey { factory: factory as usize, current_table_columns: ctx.current_table_columns.clone(), @@ -123,10 +118,7 @@ pub enum NodeWalkResult { expected: Vec, }, /// Committed and hit a hard mismatch or validator failure. - Failed { - position: usize, - kind: FailureKind, - }, + Failed { position: usize, kind: FailureKind }, } const fn matched(end: usize) -> NodeWalkResult { @@ -218,9 +210,7 @@ fn walk_node_inner( kind: FailureKind::Mismatch { expected: vec![] }, } } - Node::Subgrammar(inner) => { - walk_subgrammar(source, pos, inner, ctx, path, per_byte) - } + Node::Subgrammar(inner) => walk_subgrammar(source, pos, inner, ctx, path, per_byte), Node::ScopedSubgrammar(inner) => { walk_scoped_subgrammar(source, pos, inner, ctx, path, per_byte) } @@ -247,8 +237,7 @@ fn walk_node_inner( // DynamicSubgrammar wrapper that delegates to the // memoized `column_value_list`), so the per-walk // leak is a few bytes, not a whole typed tree. - let resolved: &'static Node = - Box::leak(Box::new(factory(ctx, source, pos))); + let resolved: &'static Node = Box::leak(Box::new(factory(ctx, source, pos))); walk_node(source, pos, resolved, ctx, path, per_byte) } Node::SetColumn(col) => { @@ -262,7 +251,10 @@ fn walk_node_inner( let col: &crate::completion::TableColumn = col; ctx.current_column = Some(col.clone()); ctx.pending_value_column = Some(col.name.clone()); - NodeWalkResult::Matched { end: pos, skipped: Vec::new() } + NodeWalkResult::Matched { + end: pos, + skipped: Vec::new(), + } } Node::TypedValueSlot { ty, @@ -342,7 +334,10 @@ fn walk_word( // Amendment 4). Plain keywords leave it `None`. class: word.highlight_override.unwrap_or(HighlightClass::Keyword), }); - NodeWalkResult::Matched { end, skipped: Vec::new() } + NodeWalkResult::Matched { + end, + skipped: Vec::new(), + } } else { NodeWalkResult::NoMatch { position, @@ -477,9 +472,7 @@ fn walk_ident( // ScopedSubgrammar (which is structurally guaranteed to be // the CTE body — no intervening scoped subgrammar in CTE // syntax) runs the harvest at body-frame exit. - if writes_cte_name - && let Some(frame) = ctx.from_scope_stack.last_mut() - { + if writes_cte_name && let Some(frame) = ctx.from_scope_stack.last_mut() { frame .cte_bindings .push(crate::dsl::walker::context::CteBinding { @@ -487,13 +480,12 @@ fn walk_ident( columns: Vec::new(), }); let placeholder_index = frame.cte_bindings.len() - 1; - ctx.pending_cte_harvest = - Some(crate::dsl::walker::context::PendingCteHarvest { - placeholder_index, - col_list: Vec::new(), - cte_name: text.clone(), - cte_name_span: (start, end), - }); + ctx.pending_cte_harvest = Some(crate::dsl::walker::context::PendingCteHarvest { + placeholder_index, + col_list: Vec::new(), + cte_name: text.clone(), + cte_name_span: (start, end), + }); } // ADR-0032 §10.3: the optional `(c1, c2, …)` rename list // between the cte name and `AS`. Each `cte_column` ident @@ -507,9 +499,7 @@ fn walk_ident( } // ADR-0032 §10.4: projection-list alias accumulator for // ORDER BY completion candidates. - if writes_projection_alias - && let Some(frame) = ctx.from_scope_stack.last_mut() - { + if writes_projection_alias && let Some(frame) = ctx.from_scope_stack.last_mut() { frame.projection_aliases.push(text.clone()); } if writes_column && matches!(src, crate::dsl::grammar::IdentSource::Columns) { @@ -529,9 +519,7 @@ fn walk_ident( .map(|c| c.name.clone()) .or_else(|| Some(text.clone())); } - if writes_user_listed_column - && matches!(src, crate::dsl::grammar::IdentSource::Columns) - { + if writes_user_listed_column && matches!(src, crate::dsl::grammar::IdentSource::Columns) { // Form A: `insert into (col1, col2, …)`. Append the // matched column name to user_listed_columns so the // inner `values (…)` slot list mirrors the user's @@ -564,7 +552,10 @@ fn walk_ident( // (issue #8 / ADR-0022 Amendment 4). class: highlight_override.unwrap_or(HighlightClass::Identifier), }); - NodeWalkResult::Matched { end, skipped: Vec::new() } + NodeWalkResult::Matched { + end, + skipped: Vec::new(), + } } fn walk_string_lit( @@ -648,7 +639,10 @@ fn walk_literal( end, class, }); - NodeWalkResult::Matched { end, skipped: Vec::new() } + NodeWalkResult::Matched { + end, + skipped: Vec::new(), + } } fn walk_number_lit( @@ -683,7 +677,10 @@ fn walk_number_lit( end, class: HighlightClass::Number, }); - NodeWalkResult::Matched { end, skipped: Vec::new() } + NodeWalkResult::Matched { + end, + skipped: Vec::new(), + } } fn walk_flag( @@ -717,7 +714,10 @@ fn walk_flag( end, class: HighlightClass::Flag, }); - NodeWalkResult::Matched { end, skipped: Vec::new() } + NodeWalkResult::Matched { + end, + skipped: Vec::new(), + } } #[allow(clippy::too_many_arguments)] @@ -784,7 +784,10 @@ fn walk_repeated( count += 1; last_item_skipped = skipped; } - NodeWalkResult::NoMatch { expected, position: inner_pos } => { + NodeWalkResult::NoMatch { + expected, + position: inner_pos, + } => { // Mid-typing-the-next-item recovery: if the // separator just consumed and the inner failed // at EOF, the user is partway through typing the @@ -860,7 +863,10 @@ fn walk_bare_path( end, class: HighlightClass::String, }); - NodeWalkResult::Matched { end, skipped: Vec::new() } + NodeWalkResult::Matched { + end, + skipped: Vec::new(), + } } fn walk_choice( @@ -1031,7 +1037,10 @@ fn walk_optional( skipped: expected, } } - NodeWalkResult::Incomplete { position: p, expected } if !inner_committed => { + NodeWalkResult::Incomplete { + position: p, + expected, + } if !inner_committed => { // Inner reported Incomplete without consuming // anything — same as NoMatch from the user's // perspective. Roll back and skip. @@ -1156,9 +1165,7 @@ fn walk_scoped_subgrammar( // walks that NoMatch / Incomplete / Fail leave the placeholder // empty (the outer-frame state is also discarded in the // speculative path, so this is correct). - if let (Some(req), NodeWalkResult::Matched { end, .. }) = - (pending_cte, &result) - { + if let (Some(req), NodeWalkResult::Matched { end, .. }) = (pending_cte, &result) { run_cte_harvest(ctx, path, source, pos, *end, &req); } @@ -1240,9 +1247,8 @@ fn run_cte_harvest( select_idx = Some(i + 1); // start of projection list } MatchedKind::Word( - "from" | "where" | "group" | "having" | "order" - | "limit" | "offset" | "union" | "intersect" - | "except", + "from" | "where" | "group" | "having" | "order" | "limit" | "offset" | "union" + | "intersect" | "except", ) if select_idx.is_some() => { end_idx = i; break; @@ -1281,12 +1287,7 @@ fn run_cte_harvest( // Classify each projection item per ADR-0032 §10.3. let mut derived: Vec = Vec::new(); for slice in item_slices { - classify_projection_item( - slice, - body_frame, - &ctx.from_scope_stack, - &mut derived, - ); + classify_projection_item(slice, body_frame, &ctx.from_scope_stack, &mut derived); } // Apply (c1, c2, …) positional rename if provided. Types @@ -1339,8 +1340,7 @@ fn run_cte_harvest( let stack_len = ctx.from_scope_stack.len(); if stack_len >= 2 && let Some(outer) = ctx.from_scope_stack.get_mut(stack_len - 2) - && let Some(placeholder) = - outer.cte_bindings.get_mut(req.placeholder_index) + && let Some(placeholder) = outer.cte_bindings.get_mut(req.placeholder_index) { placeholder.columns = derived; } @@ -1368,9 +1368,7 @@ fn classify_projection_item( // empty because it wasn't a base-table lookup), resolve // through to the in-scope CteBinding so nested CTEs project // correctly. - if expr_slice.len() == 1 - && matches!(expr_slice[0].kind, MatchedKind::Punct('*')) - { + if expr_slice.len() == 1 && matches!(expr_slice[0].kind, MatchedKind::Punct('*')) { for binding in &body_frame.from_scope { for col in expand_binding(binding, scope_stack) { out.push(col); @@ -1383,7 +1381,10 @@ fn classify_projection_item( if expr_slice.len() == 3 && matches!( expr_slice[0].kind, - MatchedKind::Ident { role: "qualified_star_qualifier", .. } + MatchedKind::Ident { + role: "qualified_star_qualifier", + .. + } ) && matches!(expr_slice[1].kind, MatchedKind::Punct('.')) && matches!(expr_slice[2].kind, MatchedKind::Punct('*')) @@ -1413,11 +1414,7 @@ fn classify_projection_item( ) { let col_text = &expr_slice[0].text; - let resolved_type = resolve_bare_column_type_in_frame( - body_frame, - scope_stack, - col_text, - ); + let resolved_type = resolve_bare_column_type_in_frame(body_frame, scope_stack, col_text); let name = alias.unwrap_or_else(|| col_text.clone()); out.push(CteColumn { name: Some(name), @@ -1447,12 +1444,7 @@ fn classify_projection_item( { let qual = &expr_slice[0].text; let col_text = &expr_slice[2].text; - let resolved_type = resolve_qualified_column_type( - body_frame, - scope_stack, - qual, - col_text, - ); + let resolved_type = resolve_qualified_column_type(body_frame, scope_stack, qual, col_text); let name = alias.unwrap_or_else(|| col_text.clone()); out.push(CteColumn { name: Some(name), @@ -1493,16 +1485,8 @@ fn strip_trailing_alias<'a>( } ) { // Optional preceding `AS` keyword. - if slice.len() >= 2 - && matches!( - slice[slice.len() - 2].kind, - MatchedKind::Word("as") - ) - { - return ( - &slice[..slice.len() - 2], - Some(last.text.clone()), - ); + if slice.len() >= 2 && matches!(slice[slice.len() - 2].kind, MatchedKind::Word("as")) { + return (&slice[..slice.len() - 2], Some(last.text.clone())); } return (&slice[..slice.len() - 1], Some(last.text.clone())); } @@ -1613,8 +1597,8 @@ fn merge_expected(dst: &mut Vec, src: Vec) { #[cfg(test)] mod tests { use super::{ - DYNAMIC_CACHE, FailureKind, MAX_SUBGRAMMAR_DEPTH, NodeWalkResult, - resolve_dynamic, walk_node, + DYNAMIC_CACHE, FailureKind, MAX_SUBGRAMMAR_DEPTH, NodeWalkResult, resolve_dynamic, + walk_node, }; use crate::dsl::grammar::{Node, Word}; use crate::dsl::walker::context::WalkContext; @@ -1629,18 +1613,14 @@ mod tests { Node::Subgrammar(&NESTED), Node::Punct(')'), ]; - static NESTED_CHOICES: &[Node] = &[ - Node::Seq(NESTED_GROUP), - Node::Word(Word::keyword("x")), - ]; + static NESTED_CHOICES: &[Node] = &[Node::Seq(NESTED_GROUP), Node::Word(Word::keyword("x"))]; static NESTED: Node = Node::Choice(NESTED_CHOICES); fn walk_nested(input: &str) -> NodeWalkResult { let mut ctx = WalkContext::new(); let mut path = MatchedPath::new(); let mut per_byte = Vec::new(); - let result = - walk_node(input, 0, &NESTED, &mut ctx, &mut path, &mut per_byte); + let result = walk_node(input, 0, &NESTED, &mut ctx, &mut path, &mut per_byte); assert_eq!( ctx.subgrammar_depth, 0, "subgrammar_depth must be restored to 0 after the walk", @@ -1726,14 +1706,8 @@ mod tests { fn resolve_dynamic_cache_is_populated() { let ctx = WalkContext::new(); let _ = resolve_dynamic(const_factory, &ctx); - let populated = !DYNAMIC_CACHE - .lock() - .expect("cache lock") - .is_empty(); - assert!( - populated, - "resolve_dynamic should populate the memo cache", - ); + let populated = !DYNAMIC_CACHE.lock().expect("cache lock").is_empty(); + assert!(populated, "resolve_dynamic should populate the memo cache",); } // ---- ScopedSubgrammar (ADR-0032 §10.2) ----------------------- @@ -1758,14 +1732,7 @@ mod tests { let mut path = MatchedPath::new(); let mut per_byte = Vec::new(); let baseline_frames = ctx.from_scope_stack.len(); - let result = walk_node( - input, - 0, - &SCOPED_NESTED, - &mut ctx, - &mut path, - &mut per_byte, - ); + let result = walk_node(input, 0, &SCOPED_NESTED, &mut ctx, &mut path, &mut per_byte); assert_eq!( ctx.subgrammar_depth, 0, "subgrammar_depth must be restored to 0 after the walk", @@ -1801,9 +1768,9 @@ mod tests { kind: FailureKind::Validation(err), .. } => assert_eq!(err.message_key, "parse.custom.expression_too_deep"), - other => panic!( - "expected expression_too_deep on pathological scoped nesting, got {other:?}", - ), + other => { + panic!("expected expression_too_deep on pathological scoped nesting, got {other:?}",) + } } } @@ -1822,9 +1789,7 @@ mod tests { /// Walk a top-level SQL SELECT and return the bottom frame's /// `from_scope` after the walk completes. Used to verify that /// `writes_table` / `writes_table_alias` populate bindings. - fn from_scope_after_walk( - input: &str, - ) -> Vec { + fn from_scope_after_walk(input: &str) -> Vec { let mut ctx = WalkContext::new(); let mut path = MatchedPath::new(); let mut per_byte = Vec::new(); @@ -1871,9 +1836,7 @@ mod tests { #[test] fn join_pushes_a_second_binding() { - let bindings = from_scope_after_walk( - "select * from a join b on x = y", - ); + let bindings = from_scope_after_walk("select * from a join b on x = y"); assert_eq!(bindings.len(), 2); assert_eq!(bindings[0].table, "a"); assert_eq!(bindings[1].table, "b"); @@ -1881,9 +1844,7 @@ mod tests { #[test] fn join_with_aliases() { - let bindings = from_scope_after_walk( - "select * from a as x join b as y on x.id = y.id", - ); + let bindings = from_scope_after_walk("select * from a as x join b as y on x.id = y.id"); assert_eq!(bindings.len(), 2); assert_eq!(bindings[0].table, "a"); assert_eq!(bindings[0].alias, Some("x".to_string())); @@ -1893,9 +1854,8 @@ mod tests { #[test] fn three_way_join_pushes_three_bindings() { - let bindings = from_scope_after_walk( - "select * from a join b on x = y left join c on y = z", - ); + let bindings = + from_scope_after_walk("select * from a join b on x = y left join c on y = z"); assert_eq!(bindings.len(), 3); assert_eq!(bindings[0].table, "a"); assert_eq!(bindings[1].table, "b"); @@ -1908,9 +1868,8 @@ mod tests { // binding into the inner scope frame; on exit, the frame // pops and the inner binding is gone. The outer scope's // from_scope still contains only `outer_t`. - let bindings = from_scope_after_walk( - "select * from outer_t where id in (select id from inner_t)", - ); + let bindings = + from_scope_after_walk("select * from outer_t where id in (select id from inner_t)"); assert_eq!(bindings.len(), 1); assert_eq!(bindings[0].table, "outer_t"); } @@ -1921,9 +1880,8 @@ mod tests { // body's scope frame; on body-frame exit, the inner // binding goes away. The outer scope contains only // the CTE-name reference `cte_x`. - let bindings = from_scope_after_walk( - "with cte_x as (select * from base_table) select * from cte_x", - ); + let bindings = + from_scope_after_walk("with cte_x as (select * from base_table) select * from cte_x"); assert_eq!(bindings.len(), 1); assert_eq!(bindings[0].table, "cte_x"); } @@ -1940,10 +1898,7 @@ mod tests { /// `cte_bindings` and `projection_aliases` after the walk. fn frame_state_after_walk( input: &str, - ) -> ( - Vec, - Vec, - ) { + ) -> (Vec, Vec) { let mut ctx = WalkContext::new(); let mut path = MatchedPath::new(); let mut per_byte = Vec::new(); @@ -1968,9 +1923,7 @@ mod tests { #[test] fn cte_name_pushes_placeholder_binding() { - let (ctes, _) = frame_state_after_walk( - "with cte_x as (select 1) select * from cte_x", - ); + let (ctes, _) = frame_state_after_walk("with cte_x as (select 1) select * from cte_x"); assert_eq!(ctes.len(), 1); assert_eq!(ctes[0].name, "cte_x"); // §10.3 stage-2 harvest produces one CteColumn per @@ -1984,9 +1937,8 @@ mod tests { #[test] fn multiple_ctes_push_in_order() { - let (ctes, _) = frame_state_after_walk( - "with a as (select 1), b as (select 2) select * from b", - ); + let (ctes, _) = + frame_state_after_walk("with a as (select 1), b as (select 2) select * from b"); assert_eq!(ctes.len(), 2); assert_eq!(ctes[0].name, "a"); assert_eq!(ctes[1].name, "b"); @@ -2006,25 +1958,20 @@ mod tests { #[test] fn projection_aliases_captured_via_as_form() { - let (_, aliases) = frame_state_after_walk( - "select a as alpha, b as beta from t", - ); + let (_, aliases) = frame_state_after_walk("select a as alpha, b as beta from t"); assert_eq!(aliases, vec!["alpha".to_string(), "beta".to_string()]); } #[test] fn projection_aliases_captured_via_bare_form() { - let (_, aliases) = frame_state_after_walk( - "select a alpha, b beta from t", - ); + let (_, aliases) = frame_state_after_walk("select a alpha, b beta from t"); assert_eq!(aliases, vec!["alpha".to_string(), "beta".to_string()]); } #[test] fn projection_aliases_mixed_forms() { - let (_, aliases) = frame_state_after_walk( - "select a as alpha, b beta, c, d as delta from t", - ); + let (_, aliases) = + frame_state_after_walk("select a as alpha, b beta, c, d as delta from t"); assert_eq!( aliases, vec!["alpha".to_string(), "beta".to_string(), "delta".to_string()] @@ -2033,8 +1980,7 @@ mod tests { #[test] fn projection_aliases_empty_when_no_aliases() { - let (_, aliases) = - frame_state_after_walk("select a, b from t"); + let (_, aliases) = frame_state_after_walk("select a, b from t"); assert!(aliases.is_empty()); } @@ -2088,9 +2034,24 @@ mod tests { s.table_columns.insert( "users".to_string(), vec![ - TableColumn { name: "id".to_string(), user_type: Type::Int, not_null: false, has_default: false }, - TableColumn { name: "name".to_string(), user_type: Type::Text, not_null: false, has_default: false }, - TableColumn { name: "age".to_string(), user_type: Type::Int, not_null: false, has_default: false }, + TableColumn { + name: "id".to_string(), + user_type: Type::Int, + not_null: false, + has_default: false, + }, + TableColumn { + name: "name".to_string(), + user_type: Type::Text, + not_null: false, + has_default: false, + }, + TableColumn { + name: "age".to_string(), + user_type: Type::Int, + not_null: false, + has_default: false, + }, ], ); s @@ -2108,10 +2069,7 @@ mod tests { assert_eq!(ctes.len(), 1); assert_eq!(ctes[0].columns.len(), 3); assert_eq!(ctes[0].columns[0].name.as_deref(), Some("id")); - assert_eq!( - ctes[0].columns[0].type_, - Some(crate::dsl::types::Type::Int), - ); + assert_eq!(ctes[0].columns[0].type_, Some(crate::dsl::types::Type::Int),); assert_eq!(ctes[0].columns[1].name.as_deref(), Some("name")); assert_eq!( ctes[0].columns[1].type_, @@ -2161,10 +2119,7 @@ mod tests { ); assert_eq!(ctes[0].columns.len(), 1); assert_eq!(ctes[0].columns[0].name.as_deref(), Some("age")); - assert_eq!( - ctes[0].columns[0].type_, - Some(crate::dsl::types::Type::Int), - ); + assert_eq!(ctes[0].columns[0].type_, Some(crate::dsl::types::Type::Int),); } #[test] @@ -2259,15 +2214,9 @@ mod tests { .expect("outer_cte binding"); assert_eq!(outer.columns.len(), 2); assert_eq!(outer.columns[0].name.as_deref(), Some("id")); - assert_eq!( - outer.columns[0].type_, - Some(crate::dsl::types::Type::Int), - ); + assert_eq!(outer.columns[0].type_, Some(crate::dsl::types::Type::Int),); assert_eq!(outer.columns[1].name.as_deref(), Some("name")); - assert_eq!( - outer.columns[1].type_, - Some(crate::dsl::types::Type::Text), - ); + assert_eq!(outer.columns[1].type_, Some(crate::dsl::types::Type::Text),); } #[test] @@ -2287,15 +2236,9 @@ mod tests { let b = ctes.iter().find(|c| c.name == "b").expect("b binding"); assert_eq!(b.columns.len(), 2); assert_eq!(b.columns[0].name.as_deref(), Some("id")); - assert_eq!( - b.columns[0].type_, - Some(crate::dsl::types::Type::Int), - ); + assert_eq!(b.columns[0].type_, Some(crate::dsl::types::Type::Int),); assert_eq!(b.columns[1].name.as_deref(), Some("name")); - assert_eq!( - b.columns[1].type_, - Some(crate::dsl::types::Type::Text), - ); + assert_eq!(b.columns[1].type_, Some(crate::dsl::types::Type::Text),); } #[test] @@ -2310,10 +2253,7 @@ mod tests { ); assert_eq!(ctes[0].columns.len(), 3); assert_eq!(ctes[0].columns[0].name.as_deref(), Some("a")); - assert_eq!( - ctes[0].columns[0].type_, - Some(crate::dsl::types::Type::Int), - ); + assert_eq!(ctes[0].columns[0].type_, Some(crate::dsl::types::Type::Int),); assert_eq!(ctes[0].columns[1].name.as_deref(), Some("b")); assert_eq!( ctes[0].columns[1].type_, diff --git a/src/dsl/walker/highlight.rs b/src/dsl/walker/highlight.rs index 0a29ef4..40329a7 100644 --- a/src/dsl/walker/highlight.rs +++ b/src/dsl/walker/highlight.rs @@ -24,8 +24,8 @@ use crate::dsl::grammar::HighlightClass; use crate::dsl::walker::context::WalkContext; use crate::dsl::walker::lex_helpers::{ - consume_bare_path, consume_flag, consume_ident, consume_number_literal, - consume_string_literal, skip_whitespace, + consume_bare_path, consume_flag, consume_ident, consume_number_literal, consume_string_literal, + skip_whitespace, }; use crate::dsl::walker::outcome::{ByteClass, WalkBound}; @@ -47,16 +47,11 @@ pub fn highlight_runs(source: &str) -> Vec { /// token, producing the keyword classes the renderer needs to /// colour `select` / `from` / `where` / `union` / `case` / etc. #[must_use] -pub fn highlight_runs_in_mode( - source: &str, - mode: crate::mode::Mode, -) -> Vec { +pub fn highlight_runs_in_mode(source: &str, mode: crate::mode::Mode) -> Vec { let mut ctx = WalkContext::new(); ctx.mode = mode; let (result, _cmd) = super::walk(source, WalkBound::EndOfInput, &mut ctx); - let mut classes: Vec = result - .map(|r| r.per_byte_class) - .unwrap_or_default(); + let mut classes: Vec = result.map(|r| r.per_byte_class).unwrap_or_default(); let scan_start = classes.last().map_or(0, |c| c.end); scan_remainder(source, scan_start, &mut classes); @@ -133,9 +128,7 @@ fn scan_remainder(source: &str, start: usize, classes: &mut Vec) { .get(pos + 1) .copied() .is_some_and(|c| c.is_ascii_digit())); - if looks_like_number - && let Some((s, e)) = consume_number_literal(source, pos) - { + if looks_like_number && let Some((s, e)) = consume_number_literal(source, pos) { classes.push(ByteClass { start: s, end: e, @@ -222,8 +215,14 @@ mod tests { "no Error highlight on a valid m:n line: {runs:?}" ); let kinds: Vec = runs.iter().map(|(_, _, c)| *c).collect(); - assert!(kinds.contains(&HighlightClass::Keyword), "keywords highlighted: {runs:?}"); - assert!(kinds.contains(&HighlightClass::Identifier), "table names highlighted: {runs:?}"); + assert!( + kinds.contains(&HighlightClass::Keyword), + "keywords highlighted: {runs:?}" + ); + assert!( + kinds.contains(&HighlightClass::Identifier), + "table names highlighted: {runs:?}" + ); } #[test] @@ -276,10 +275,7 @@ mod tests { #[test] fn flag_classified_via_fallback() { // Walker doesn't engage for a bare `--all-rows`. - assert_eq!( - run("--all-rows"), - vec![(0, 10, HighlightClass::Flag)], - ); + assert_eq!(run("--all-rows"), vec![(0, 10, HighlightClass::Flag)],); } #[test] @@ -445,15 +441,13 @@ mod tests { // dispatcher, so only the entry word would highlight). let runs = run_advanced("select * from t"); assert!( - runs.iter().any(|(s, e, c)| { - *c == HighlightClass::Keyword && (*s, *e) == (0, 6) - }), + runs.iter() + .any(|(s, e, c)| { *c == HighlightClass::Keyword && (*s, *e) == (0, 6) }), "expected `select` keyword span 0..6; got {runs:?}", ); assert!( - runs.iter().any(|(s, e, c)| { - *c == HighlightClass::Keyword && (*s, *e) == (9, 13) - }), + runs.iter() + .any(|(s, e, c)| { *c == HighlightClass::Keyword && (*s, *e) == (9, 13) }), "expected `from` keyword span 9..13; got {runs:?}", ); } @@ -514,18 +508,37 @@ mod tests { let insert = keywords_of( "insert into t (a) values (1) on conflict (a) do update set a = excluded.a returning a", ); - for kw in ["insert", "into", "values", "on", "conflict", "do", "update", "set", "returning"] { - assert!(insert.contains(&kw), "INSERT/UPSERT: missing `{kw}`; got {insert:?}"); + for kw in [ + "insert", + "into", + "values", + "on", + "conflict", + "do", + "update", + "set", + "returning", + ] { + assert!( + insert.contains(&kw), + "INSERT/UPSERT: missing `{kw}`; got {insert:?}" + ); } let update = keywords_of("update t set a = 1 where id = 2 returning a"); for kw in ["update", "set", "where", "returning"] { - assert!(update.contains(&kw), "UPDATE: missing `{kw}`; got {update:?}"); + assert!( + update.contains(&kw), + "UPDATE: missing `{kw}`; got {update:?}" + ); } let delete = keywords_of("delete from t where id = 1 returning *"); for kw in ["delete", "from", "where", "returning"] { - assert!(delete.contains(&kw), "DELETE: missing `{kw}`; got {delete:?}"); + assert!( + delete.contains(&kw), + "DELETE: missing `{kw}`; got {delete:?}" + ); } } } diff --git a/src/dsl/walker/lex_helpers.rs b/src/dsl/walker/lex_helpers.rs index 9b24b61..0f6e80f 100644 --- a/src/dsl/walker/lex_helpers.rs +++ b/src/dsl/walker/lex_helpers.rs @@ -110,9 +110,7 @@ pub fn consume_number_literal(source: &str, start: usize) -> Option<(usize, usiz return None; } let mut i = start; - let leading_minus = bytes[i] == b'-' - && i + 1 < bytes.len() - && bytes[i + 1].is_ascii_digit(); + let leading_minus = bytes[i] == b'-' && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit(); if leading_minus { i += 1; } diff --git a/src/dsl/walker/mod.rs b/src/dsl/walker/mod.rs index 99a8e0b..d7b55c4 100644 --- a/src/dsl/walker/mod.rs +++ b/src/dsl/walker/mod.rs @@ -18,16 +18,12 @@ pub mod highlight; pub mod lex_helpers; pub mod outcome; -use crate::dsl::command::{ - Command, CompareOp, Expr, Operand, Predicate, RowFilter, -}; +use crate::dsl::command::{Command, CompareOp, Expr, Operand, Predicate, RowFilter}; use crate::dsl::grammar; use crate::dsl::walker::context::WalkContext; use crate::dsl::walker::driver::{FailureKind, NodeWalkResult, walk_node}; use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace}; -use crate::dsl::walker::outcome::{ - Expectation, MatchedPath, WalkBound, WalkOutcome, WalkResult, -}; +use crate::dsl::walker::outcome::{Expectation, MatchedPath, WalkBound, WalkOutcome, WalkResult}; pub use context::ColumnInfo; pub use highlight::{highlight_runs, highlight_runs_in_mode}; @@ -161,9 +157,7 @@ pub fn hint_resolution_at_input_in_mode( // ident slots ("Type a name"). match snap.pending_hint_mode { Some( - mode @ (HintMode::ProseOnly(_) - | HintMode::ForceProse(_) - | HintMode::IntroProse(_)), + mode @ (HintMode::ProseOnly(_) | HintMode::ForceProse(_) | HintMode::IntroProse(_)), ) => Some(HintResolution { mode, column: None, @@ -196,7 +190,11 @@ fn form_b_autogen_skipped( if user_listed.is_some() { return Vec::new(); } - if !source.trim_start().to_ascii_lowercase().starts_with("insert") { + if !source + .trim_start() + .to_ascii_lowercase() + .starts_with("insert") + { return Vec::new(); } let Some(cols) = table_columns else { @@ -292,10 +290,7 @@ pub struct CompletionProbe { /// Run a schema-aware walk and report the completion-engine's /// view (ADR-0024 §Phase D §column-narrowing). #[must_use] -pub fn completion_probe( - source: &str, - schema: &crate::completion::SchemaCache, -) -> CompletionProbe { +pub fn completion_probe(source: &str, schema: &crate::completion::SchemaCache) -> CompletionProbe { completion_probe_in_mode(source, schema, crate::mode::Mode::Advanced) } @@ -321,8 +316,7 @@ pub fn completion_probe_in_mode( REGISTRY .iter() .filter(|(c, _)| { - mode == crate::mode::Mode::Advanced - || !is_advanced_only(c.entry.primary) + mode == crate::mode::Mode::Advanced || !is_advanced_only(c.entry.primary) }) .filter(|(c, _)| seen.insert(c.entry.primary)) .map(|(c, _)| outcome::Expectation::Word(c.entry.primary)) @@ -452,9 +446,7 @@ pub fn completion_probe_in_mode( for (_, node, category) in candidates { // Simple mode never offers advanced SQL continuations // (ADR-0030 §2); only DSL forms contribute. - if mode == crate::mode::Mode::Simple - && category == CommandCategory::Advanced - { + if mode == crate::mode::Mode::Simple && category == CommandCategory::Advanced { continue; } let mut sctx = context::WalkContext::with_schema(schema); @@ -468,8 +460,7 @@ pub fn completion_probe_in_mode( let advanced = category == CommandCategory::Advanced; for e in &cont { match e { - outcome::Expectation::Word(w) - | outcome::Expectation::Literal(w) => { + outcome::Expectation::Word(w) | outcome::Expectation::Literal(w) => { match tally.iter_mut().find(|(kw, _, _)| kw == w) { Some(rec) => { if advanced { @@ -580,10 +571,7 @@ pub fn input_verdict_in_mode( if source.trim().is_empty() { return None; } - let mut ctx = schema.map_or_else( - context::WalkContext::new, - context::WalkContext::with_schema, - ); + let mut ctx = schema.map_or_else(context::WalkContext::new, context::WalkContext::with_schema); ctx.mode = mode; let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx); let Some(result) = result else { @@ -631,10 +619,7 @@ pub fn input_diagnostics_in_mode( if source.trim().is_empty() { return Vec::new(); } - let mut ctx = schema.map_or_else( - context::WalkContext::new, - context::WalkContext::with_schema, - ); + let mut ctx = schema.map_or_else(context::WalkContext::new, context::WalkContext::with_schema); ctx.mode = mode; let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx); result.map_or_else(Vec::new, |r| r.diagnostics) @@ -668,10 +653,7 @@ struct PassBinding { /// Resolve a qualifier identifier against the active bindings. /// Aliases shadow base-table names (ADR-0032 §10.5), so alias /// matches are tried first. -fn resolve_qualifier<'a>( - bindings: &'a [PassBinding], - qualifier: &str, -) -> Option<&'a PassBinding> { +fn resolve_qualifier<'a>(bindings: &'a [PassBinding], qualifier: &str) -> Option<&'a PassBinding> { bindings .iter() .find(|b| { @@ -1340,9 +1322,7 @@ fn cte_names_contains(names: &[String], candidate: &str) -> bool { /// compared against it on that leg's close. The op token's span /// is the diagnostic anchor — that's the join point the learner /// pointed the chain at. -fn compound_arity_diagnostics( - path: &MatchedPath, -) -> Vec { +fn compound_arity_diagnostics(path: &MatchedPath) -> Vec { use outcome::{Diagnostic, MatchedKind, Severity}; use std::collections::HashMap; @@ -1420,11 +1400,13 @@ fn compound_arity_diagnostics( // supersedes any stale state. legs.insert( depth, - LegState { arity: 1, in_projection: true }, + LegState { + arity: 1, + in_projection: true, + }, ); } - "from" | "where" | "group" | "having" | "order" - | "limit" | "offset" => { + "from" | "where" | "group" | "having" | "order" | "limit" | "offset" => { if let Some(leg) = legs.get_mut(&depth) { leg.in_projection = false; } @@ -1433,12 +1415,9 @@ fn compound_arity_diagnostics( // Close the just-finished leg, comparing // it against any pending set-op state at // this depth. - if let Some(arity) = close_leg( - depth, - &mut legs, - &mut pending, - &mut diagnostics, - ) { + if let Some(arity) = + close_leg(depth, &mut legs, &mut pending, &mut diagnostics) + { pending.insert( depth, Pending { @@ -1593,20 +1572,17 @@ fn dml_insert_arity_diagnostics( // (insert-specific); the DSL grammar reuses the generic `table_name` // role, so only trust that when `into` confirmed an insert (issue // #17). - let target_table: Option<&str> = path - .items - .iter() - .find_map(|it| match it.kind { - MatchedKind::Ident { - source: IdentSource::Tables, - role: "insert_target_table", - } => Some(it.text.as_str()), - MatchedKind::Ident { - source: IdentSource::Tables, - role: "table_name", - } if is_insert => Some(it.text.as_str()), - _ => None, - }); + let target_table: Option<&str> = path.items.iter().find_map(|it| match it.kind { + MatchedKind::Ident { + source: IdentSource::Tables, + role: "insert_target_table", + } => Some(it.text.as_str()), + MatchedKind::Ident { + source: IdentSource::Tables, + role: "table_name", + } if is_insert => Some(it.text.as_str()), + _ => None, + }); // Resolve the expected arity + a message builder. The builder // captures the per-case args because the message key (and its @@ -1815,8 +1791,8 @@ fn dml_insert_arity_diagnostics( MatchedKind::Punct(')') => depth -= 1, MatchedKind::Punct(',') if depth == 0 => proj_arity += 1, MatchedKind::Word( - "from" | "where" | "group" | "having" | "order" | "limit" | "offset" - | "union" | "intersect" | "except" | "on" | "returning", + "from" | "where" | "group" | "having" | "order" | "limit" | "offset" | "union" + | "intersect" | "except" | "on" | "returning", ) if depth == 0 => break, _ => { if depth == 0 && anchor.is_none() { @@ -2102,9 +2078,9 @@ fn sql_predicate_warnings( continue; } // Resolve column → which binding's column → what type. - let Some(col_type) = resolve_bare_column_type( - &bindings, &cte_names, schema, &items[i].text, - ) else { + let Some(col_type) = + resolve_bare_column_type(&bindings, &cte_names, schema, &items[i].text) + else { // Unknown column or in a CTE-binding (whose columns // are unknown until harvest lands). Either way, skip. continue; @@ -2125,10 +2101,7 @@ fn sql_predicate_warnings( diagnostics.push(Diagnostic { severity: Severity::Warning, span: third.span, - message: crate::friendly::translate( - "diagnostic.eq_null", - &[], - ), + message: crate::friendly::translate("diagnostic.eq_null", &[]), }); continue; } @@ -2136,9 +2109,7 @@ fn sql_predicate_warnings( // ` LIKE ` — pedagogical: LIKE is a // text-pattern match, so a numeric column rarely makes // sense as the target. - if matches!(next.kind, MatchedKind::Word("like")) - && col_type.is_numeric() - { + if matches!(next.kind, MatchedKind::Word("like")) && col_type.is_numeric() { diagnostics.push(Diagnostic { severity: Severity::Warning, span: items[i].span, @@ -2157,8 +2128,7 @@ fn sql_predicate_warnings( // the literal's type is structurally incompatible with // the column's type. Conservative: only flag clear-cut // numeric-vs-text mismatches. - if let MatchedKind::Word(op @ ("=" | "!=" | "<>" | "<" | "<=" | ">" | ">=")) - = next.kind + if let MatchedKind::Word(op @ ("=" | "!=" | "<>" | "<" | "<=" | ">" | ">=")) = next.kind && let Some(third) = items.get(i + 2) { let _ = op; @@ -2235,23 +2205,18 @@ fn resolve_bare_column_type( /// half of a `t.c` qualified reference. Used by /// `schema_existence_diagnostics` to skip the bare-column check /// on qualifiers. -fn is_followed_by_qualified_ref( - items: &[outcome::MatchedItem], - i: usize, -) -> bool { +fn is_followed_by_qualified_ref(items: &[outcome::MatchedItem], i: usize) -> bool { use outcome::MatchedKind; let dot = items.get(i + 1); let next_ident = items.get(i + 2); - matches!( - dot.map(|it| &it.kind), - Some(MatchedKind::Punct('.')) - ) && matches!( - next_ident.map(|it| &it.kind), - Some(MatchedKind::Ident { - role: "sql_expr_qualified_ref", - .. - }) - ) + matches!(dot.map(|it| &it.kind), Some(MatchedKind::Punct('.'))) + && matches!( + next_ident.map(|it| &it.kind), + Some(MatchedKind::Ident { + role: "sql_expr_qualified_ref", + .. + }) + ) } /// True when the ident at `items[i]` is the *name* of a function call — @@ -2262,10 +2227,7 @@ fn is_followed_by_qualified_ref( /// structurally (sql_expr.rs's CALL_ARGS comment: "it does not know /// which names are aggregates"), so this validator must match: an /// ident followed by `(` is a function-call name, not a column ref. -fn is_followed_by_call_args( - items: &[outcome::MatchedItem], - i: usize, -) -> bool { +fn is_followed_by_call_args(items: &[outcome::MatchedItem], i: usize) -> bool { use outcome::MatchedKind; matches!( items.get(i + 1).map(|it| &it.kind), @@ -2289,11 +2251,7 @@ fn schema_column_type( .map(|c| c.user_type) } -fn schema_has_column( - schema: &crate::completion::SchemaCache, - table: &str, - column: &str, -) -> bool { +fn schema_has_column(schema: &crate::completion::SchemaCache, table: &str, column: &str) -> bool { schema .columns_for_table(table) .is_some_and(|cols| cols.iter().any(|c| c.name.eq_ignore_ascii_case(column))) @@ -2376,9 +2334,7 @@ fn predicate_warnings( crate::friendly::translate("diagnostic.eq_null", &[]), operand.span(), )); - } else if let Some((message, span)) = - pair_type_mismatch(left, right, columns) - { + } else if let Some((message, span)) = pair_type_mismatch(left, right, columns) { out.push(warn(message, span)); } } @@ -2386,18 +2342,14 @@ fn predicate_warnings( target, low, high, .. } => { for bound in [low, high] { - if let Some((message, span)) = - pair_type_mismatch(target, bound, columns) - { + if let Some((message, span)) = pair_type_mismatch(target, bound, columns) { out.push(warn(message, span)); } } } Predicate::In { target, items, .. } => { for item in items { - if let Some((message, span)) = - pair_type_mismatch(target, item, columns) - { + if let Some((message, span)) = pair_type_mismatch(target, item, columns) { out.push(warn(message, span)); } } @@ -2407,9 +2359,7 @@ fn predicate_warnings( // (ADR-0027, Amendment 1). The negation is irrelevant — // `NOT LIKE` on a numeric column is just as dubious. Predicate::Like { target, .. } => { - if let Some((message, span)) = - like_numeric_warning(target, columns) - { + if let Some((message, span)) = like_numeric_warning(target, columns) { out.push(warn(message, span)); } } @@ -2471,14 +2421,8 @@ fn pair_type_mismatch( columns: &[crate::completion::TableColumn], ) -> Option<(String, (usize, usize))> { let (column, literal, span) = match (a, b) { - ( - Operand::Column { name, .. }, - Operand::Literal { value, span }, - ) - | ( - Operand::Literal { value, span }, - Operand::Column { name, .. }, - ) => (name, value, *span), + (Operand::Column { name, .. }, Operand::Literal { value, span }) + | (Operand::Literal { value, span }, Operand::Column { name, .. }) => (name, value, *span), _ => return None, }; // `null` fits any column; `= NULL` is flagged separately. @@ -2543,8 +2487,7 @@ pub fn expected_at_input_in_mode( REGISTRY .iter() .filter(|(c, _)| { - mode == crate::mode::Mode::Advanced - || !is_advanced_only(c.entry.primary) + mode == crate::mode::Mode::Advanced || !is_advanced_only(c.entry.primary) }) .filter(|(c, _)| seen.insert(c.entry.primary)) .map(|(c, _)| outcome::Expectation::Word(c.entry.primary)) @@ -2969,8 +2912,15 @@ fn scratch_outcome( ) -> WalkOutcome { let mut sctx = schema.map_or_else(context::WalkContext::new, context::WalkContext::with_schema); sctx.mode = mode; - let (result, _cmd) = - walk_one_command(effective_source, effective_source, kw_start, kw_end, 0, node, &mut sctx); + let (result, _cmd) = walk_one_command( + effective_source, + effective_source, + kw_start, + kw_end, + 0, + node, + &mut sctx, + ); result.outcome } @@ -3078,21 +3028,23 @@ fn walk_one_command<'a>( // the command really wanted something more.) let post = skip_whitespace(effective_source, position); if post >= effective_source.len() { - WalkOutcome::Incomplete { position: post, expected } + WalkOutcome::Incomplete { + position: post, + expected, + } } else { - WalkOutcome::Mismatch { position: post, expected } + WalkOutcome::Mismatch { + position: post, + expected, + } } } NodeWalkResult::Incomplete { position, expected } => { WalkOutcome::Incomplete { position, expected } } NodeWalkResult::Failed { position, kind } => match kind { - FailureKind::Mismatch { expected } => { - WalkOutcome::Mismatch { position, expected } - } - FailureKind::Validation(error) => { - WalkOutcome::ValidationFailed { position, error } - } + FailureKind::Mismatch { expected } => WalkOutcome::Mismatch { position, expected }, + FailureKind::Validation(error) => WalkOutcome::ValidationFailed { position, error }, }, }; @@ -3105,10 +3057,7 @@ fn walk_one_command<'a>( Ok(c) => (outcome, Some(c)), Err(error) => ( WalkOutcome::ValidationFailed { - position: path - .items - .last() - .map_or(kw_start, |i| i.span.0), + position: path.items.last().map_or(kw_start, |i| i.span.0), error, }, None, @@ -3228,7 +3177,10 @@ mod tests { #[test] fn walker_parses_help() { - assert_eq!(parse("help").unwrap(), Command::App(AppCommand::Help { topic: None })); + assert_eq!( + parse("help").unwrap(), + Command::App(AppCommand::Help { topic: None }) + ); } #[test] @@ -3539,7 +3491,10 @@ mod tests { }; assert_eq!(parse("drop column Customers: Email").unwrap(), want); assert_eq!(parse("drop column from Customers: Email").unwrap(), want); - assert_eq!(parse("drop column from table Customers: Email").unwrap(), want); + assert_eq!( + parse("drop column from table Customers: Email").unwrap(), + want + ); assert_eq!(parse("drop column table Customers: Email").unwrap(), want); } @@ -3799,8 +3754,8 @@ mod tests { // Phase D — data commands (show, insert, update, delete). // ========================================================= - use crate::dsl::value::Value; use crate::dsl::command::RowFilter; + use crate::dsl::value::Value; #[test] fn walker_parses_show_data() { @@ -3854,9 +3809,7 @@ mod tests { fn walker_parses_update_with_complex_where() { // The WHERE is a full boolean expression, not a single // equality (ADR-0026). - match parse("update T set Active=true where Age>30 and Name like 'A%'") - .unwrap() - { + match parse("update T set Active=true where Age>30 and Name like 'A%'").unwrap() { Command::Update { filter: RowFilter::Where(crate::dsl::Expr::And(terms)), .. @@ -3928,26 +3881,18 @@ mod tests { #[test] fn input_verdict_unknown_column_is_error() { - let schema = - schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); + let schema = schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); assert_eq!( - super::input_verdict( - "show data Customers where NoSuchCol = 1", - Some(&schema), - ), + super::input_verdict("show data Customers where NoSuchCol = 1", Some(&schema),), Some(super::Severity::Error), ); } #[test] fn input_verdict_known_table_and_column_is_clean() { - let schema = - schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); + let schema = schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); assert_eq!( - super::input_verdict( - "show data Customers where id = 1", - Some(&schema), - ), + super::input_verdict("show data Customers where id = 1", Some(&schema),), None, ); } @@ -3956,39 +3901,27 @@ mod tests { fn input_verdict_type_mismatch_is_warning() { // `Age` is int; comparing it with a text literal runs, // but is flagged (ADR-0026 §7). - let schema = - schema_with("Customers", &[("id", Type::Int), ("Age", Type::Int)]); + let schema = schema_with("Customers", &[("id", Type::Int), ("Age", Type::Int)]); assert_eq!( - super::input_verdict( - "delete from Customers where Age = 'hello'", - Some(&schema), - ), + super::input_verdict("delete from Customers where Age = 'hello'", Some(&schema),), Some(super::Severity::Warning), ); } #[test] fn input_verdict_eq_null_is_warning() { - let schema = - schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); + let schema = schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); assert_eq!( - super::input_verdict( - "delete from Customers where Name = null", - Some(&schema), - ), + super::input_verdict("delete from Customers where Name = null", Some(&schema),), Some(super::Severity::Warning), ); } #[test] fn input_verdict_compatible_comparison_is_clean() { - let schema = - schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); + let schema = schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); assert_eq!( - super::input_verdict( - "delete from Customers where id = 5", - Some(&schema), - ), + super::input_verdict("delete from Customers where id = 5", Some(&schema),), None, ); } @@ -4028,10 +3961,7 @@ mod tests { #[test] fn input_verdict_sweep_unknown_column_across_commands() { - let schema = schema_with( - "Customers", - &[("id", Type::Int), ("Name", Type::Text)], - ); + let schema = schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); for input in [ "drop column from table Customers: NoSuchCol", "update Customers set NoSuchCol = 1 where id = 1", @@ -4046,10 +3976,7 @@ mod tests { #[test] fn input_verdict_known_entities_across_commands_are_clean() { - let schema = schema_with( - "Customers", - &[("id", Type::Int), ("Name", Type::Text)], - ); + let schema = schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); for input in [ "show table Customers", "drop table Customers", @@ -4068,13 +3995,9 @@ mod tests { /// Walk `input` with `schema` and return the diagnostics the /// walk produced. - fn diagnostics( - input: &str, - schema: &SchemaCache, - ) -> Vec { + fn diagnostics(input: &str, schema: &SchemaCache) -> Vec { let mut ctx = super::context::WalkContext::with_schema(schema); - let (result, _cmd) = - super::walk(input, super::outcome::WalkBound::EndOfInput, &mut ctx); + let (result, _cmd) = super::walk(input, super::outcome::WalkBound::EndOfInput, &mut ctx); result.map_or_else(Vec::new, |r| r.diagnostics) } @@ -4113,8 +4036,7 @@ mod tests { let input = "delete from Customers where Age between 'a' and 'z'"; let diags = diagnostics(input, &schema); assert_eq!(diags.len(), 2); - let spans: Vec<&str> = - diags.iter().map(|d| &input[d.span.0..d.span.1]).collect(); + let spans: Vec<&str> = diags.iter().map(|d| &input[d.span.0..d.span.1]).collect(); assert_eq!(spans, vec!["'a'", "'z'"]); } @@ -4179,10 +4101,7 @@ mod tests { // `LIKE 'A%'` on a text column is its intended use. let schema = schema_with("Customers", &[("Name", Type::Text)]); assert_eq!( - super::input_verdict( - "delete from Customers where Name like 'A%'", - Some(&schema), - ), + super::input_verdict("delete from Customers where Name like 'A%'", Some(&schema),), None, ); } @@ -4194,7 +4113,10 @@ mod tests { Command::Insert { table: "Customers".to_string(), columns: Some(vec!["Email".to_string(), "Name".to_string()]), - values: vec![Value::Text("a@b.c".to_string()), Value::Text("Alice".to_string())], + values: vec![ + Value::Text("a@b.c".to_string()), + Value::Text("Alice".to_string()) + ], } ); } @@ -4392,8 +4314,8 @@ mod tests { // hint_mode_at_input (ADR-0024 §HintMode-per-node) // ========================================================= - use crate::dsl::grammar::HintMode; use super::hint_mode_at_input; + use crate::dsl::grammar::HintMode; #[test] fn hint_mode_value_literal_slot_after_insert_open_paren() { @@ -4507,14 +4429,16 @@ mod tests { // auto-fills it (ADR-0018 §3). let schema = schema_with( "Customers", - &[("id", Type::Serial), ("Name", Type::Text), ("Active", Type::Bool)], + &[ + ("id", Type::Serial), + ("Name", Type::Text), + ("Active", Type::Bool), + ], ); // 2 user-typed values: Name (text), Active (bool). - let cmd = parse_command_with_schema( - "insert into Customers values ('Alice', true)", - &schema, - ) - .expect("parse"); + let cmd = + parse_command_with_schema("insert into Customers values ('Alice', true)", &schema) + .expect("parse"); match cmd { Command::Insert { table, values, .. } => { assert_eq!(table, "Customers"); @@ -4534,19 +4458,15 @@ mod tests { // mismatch is now reported as an ERROR diagnostic rather than a // bare parse error, matching advanced mode. Dispatch is gated by // the submit pre-flight, not by a parse failure. - let schema = schema_with( - "Customers", - &[("id", Type::Serial), ("Name", Type::Text)], - ); + let schema = schema_with("Customers", &[("id", Type::Serial), ("Name", Type::Text)]); // Two values where Form B expects one (Name only): structurally // parses, but the simple-mode arity diagnostic flags it (Form B // expects 1 value for `Name`; `id` is auto-generated). - let diags = diag_keys_simple( - "insert into Customers values (1, 'Alice')", - &schema, - ); + let diags = diag_keys_simple("insert into Customers values (1, 'Alice')", &schema); assert!( - diags.iter().any(|d| d.contains("1 value(s)") && d.contains("2 given")), + diags + .iter() + .any(|d| d.contains("1 value(s)") && d.contains("2 given")), "Form B serial-skip count mismatch must fire the arity \ diagnostic (expected 1, got 2); got {diags:?}", ); @@ -4557,18 +4477,20 @@ mod tests { // Form A: user explicitly lists `id`. The dispatch path // accepts user-supplied serial values when they're in // the explicit column list; the grammar mirrors that. - let schema = schema_with( - "Customers", - &[("id", Type::Serial), ("Name", Type::Text)], - ); + let schema = schema_with("Customers", &[("id", Type::Serial), ("Name", Type::Text)]); let cmd = parse_command_with_schema( "insert into Customers (id, Name) values (1, 'Alice')", &schema, ) .expect("parse"); match cmd { - Command::Insert { columns, values, .. } => { - assert_eq!(columns.as_deref(), Some(&["id".to_string(), "Name".to_string()][..])); + Command::Insert { + columns, values, .. + } => { + assert_eq!( + columns.as_deref(), + Some(&["id".to_string(), "Name".to_string()][..]) + ); assert_eq!(values.len(), 2); } other => panic!("expected Insert, got {other:?}"), @@ -4582,15 +4504,19 @@ mod tests { // columns. let schema = schema_with( "Customers", - &[("id", Type::Serial), ("Name", Type::Text), ("Active", Type::Bool)], + &[ + ("id", Type::Serial), + ("Name", Type::Text), + ("Active", Type::Bool), + ], ); - let cmd = parse_command_with_schema( - "insert into Customers (Name) values ('Alice')", - &schema, - ) - .expect("parse"); + let cmd = + parse_command_with_schema("insert into Customers (Name) values ('Alice')", &schema) + .expect("parse"); match cmd { - Command::Insert { columns, values, .. } => { + Command::Insert { + columns, values, .. + } => { assert_eq!(columns.as_deref(), Some(&["Name".to_string()][..])); assert_eq!(values.len(), 1); } @@ -4624,11 +4550,8 @@ mod tests { "T", &[("a", Type::Int), ("b", Type::Text), ("c", Type::Bool)], ); - let cmd = parse_command_with_schema( - "insert into T values (null, null, null)", - &schema, - ) - .expect("parse"); + let cmd = parse_command_with_schema("insert into T values (null, null, null)", &schema) + .expect("parse"); match cmd { Command::Insert { values, .. } => { assert!(values.iter().all(|v| matches!(v, Value::Null))); @@ -4644,11 +4567,8 @@ mod tests { // back to the schemaless generic value-literal list and // accepts mixed-shape values as it did pre-Phase-D. let schema = SchemaCache::default(); - let cmd = parse_command_with_schema( - "insert into Customers values (1, 'Alice')", - &schema, - ) - .expect("parse — fallback path"); + let cmd = parse_command_with_schema("insert into Customers values (1, 'Alice')", &schema) + .expect("parse — fallback path"); match cmd { Command::Insert { values, .. } => assert_eq!(values.len(), 2), other => panic!("expected Insert, got {other:?}"), @@ -4670,8 +4590,8 @@ mod tests { #[test] fn phase_d_insert_accepts_bool_value_for_bool_column() { let schema = schema_with("T", &[("flag", Type::Bool)]); - let cmd = parse_command_with_schema("insert into T values (false)", &schema) - .expect("parse"); + let cmd = + parse_command_with_schema("insert into T values (false)", &schema).expect("parse"); match cmd { Command::Insert { values, .. } => { assert_eq!(values, vec![Value::Bool(false)]); @@ -4682,15 +4602,10 @@ mod tests { #[test] fn phase_d_update_accepts_text_value_for_text_column() { - let schema = schema_with( - "Customers", - &[("id", Type::Int), ("Email", Type::Text)], - ); - let cmd = parse_command_with_schema( - "update Customers set Email='new@b.c' where id=1", - &schema, - ) - .expect("parse"); + let schema = schema_with("Customers", &[("id", Type::Int), ("Email", Type::Text)]); + let cmd = + parse_command_with_schema("update Customers set Email='new@b.c' where id=1", &schema) + .expect("parse"); match cmd { Command::Update { assignments, .. } => { assert_eq!(assignments.len(), 1); @@ -4704,15 +4619,9 @@ mod tests { fn phase_d_update_rejects_decimal_in_int_set_column() { // Email is text; Score is int. Assigning `3.14` to Score // hits the int_slot validator. - let schema = schema_with( - "T", - &[("id", Type::Int), ("Score", Type::Int)], - ); - let err = parse_command_with_schema( - "update T set Score=3.14 where id=1", - &schema, - ) - .expect_err("should reject"); + let schema = schema_with("T", &[("id", Type::Int), ("Score", Type::Int)]); + let err = parse_command_with_schema("update T set Score=3.14 where id=1", &schema) + .expect_err("should reject"); match err { crate::dsl::ParseError::Invalid { message, .. } => { assert!( @@ -4728,8 +4637,7 @@ mod tests { fn phase_d_delete_where_uses_typed_column_value() { // `where id=1` — id is Int; `1` matches the int_slot. let schema = schema_with("T", &[("id", Type::Int), ("Name", Type::Text)]); - let cmd = parse_command_with_schema("delete from T where id=1", &schema) - .expect("parse"); + let cmd = parse_command_with_schema("delete from T where id=1", &schema).expect("parse"); match cmd { Command::Delete { .. } => {} other => panic!("expected Delete, got {other:?}"), @@ -4745,7 +4653,10 @@ mod tests { let schema = schema_with("T", &[("id", Type::Int)]); let cmd = parse_command_with_schema("delete from T where id=3.14", &schema) .expect("type-mismatched WHERE comparisons are permissive"); - assert!(matches!(cmd, crate::dsl::Command::Delete { .. }), "got {cmd:?}"); + assert!( + matches!(cmd, crate::dsl::Command::Delete { .. }), + "got {cmd:?}" + ); } // ---- Typed-slot HintMode (Phase D + HintMode dispatch) ---- @@ -4754,10 +4665,7 @@ mod tests { #[test] fn typed_hint_at_insert_first_value_position_for_int_column() { - let schema = schema_with( - "Customers", - &[("id", Type::Int), ("Name", Type::Text)], - ); + let schema = schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); match hint_mode_at_input_with_schema("insert into Customers values (", &schema) { Some(HintMode::ProseOnly("hint.value_slot_int")) => {} other => panic!("expected ProseOnly value_slot_int, got {other:?}"), @@ -4766,10 +4674,7 @@ mod tests { #[test] fn typed_hint_at_insert_second_value_position_for_text_column() { - let schema = schema_with( - "Customers", - &[("id", Type::Int), ("Name", Type::Text)], - ); + let schema = schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); match hint_mode_at_input_with_schema("insert into Customers values (1, ", &schema) { Some(HintMode::ProseOnly("hint.value_slot_text")) => {} other => panic!("expected ProseOnly value_slot_text, got {other:?}"), @@ -4778,10 +4683,7 @@ mod tests { #[test] fn typed_hint_at_update_set_value_uses_column_type() { - let schema = schema_with( - "Customers", - &[("id", Type::Int), ("Email", Type::Text)], - ); + let schema = schema_with("Customers", &[("id", Type::Int), ("Email", Type::Text)]); match hint_mode_at_input_with_schema("update Customers set Email=", &schema) { Some(HintMode::ProseOnly("hint.value_slot_text")) => {} other => panic!("expected ProseOnly value_slot_text, got {other:?}"), @@ -4790,10 +4692,7 @@ mod tests { #[test] fn typed_hint_at_update_set_value_for_int_column() { - let schema = schema_with( - "Customers", - &[("id", Type::Int), ("Score", Type::Int)], - ); + let schema = schema_with("Customers", &[("id", Type::Int), ("Score", Type::Int)]); match hint_mode_at_input_with_schema("update Customers set Score=", &schema) { Some(HintMode::ProseOnly("hint.value_slot_int")) => {} other => panic!("expected ProseOnly value_slot_int, got {other:?}"), @@ -4867,8 +4766,7 @@ mod tests { (Type::ShortId, "hint.value_slot_shortid"), ] { let schema = schema_with("T", &[("c", ty)]); - let mode = - hint_mode_at_input_with_schema("insert into T (c) values (", &schema); + let mode = hint_mode_at_input_with_schema("insert into T (c) values (", &schema); assert!( matches!(mode, Some(HintMode::ProseOnly(k)) if k == key), "expected ProseOnly({key}) for type {ty:?}, got {mode:?}", @@ -4972,11 +4870,7 @@ mod tests { // walk manually so we can set the right mode. let mut ctx = super::context::WalkContext::with_schema(schema); ctx.mode = crate::mode::Mode::Advanced; - let (result, _cmd) = super::walk( - source, - super::outcome::WalkBound::EndOfInput, - &mut ctx, - ); + let (result, _cmd) = super::walk(source, super::outcome::WalkBound::EndOfInput, &mut ctx); let diagnostics = result.map_or_else(Vec::new, |r| r.diagnostics); diagnostics .into_iter() @@ -4990,8 +4884,7 @@ mod tests { fn diag_keys_simple(source: &str, schema: &SchemaCache) -> Vec { let mut ctx = super::context::WalkContext::with_schema(schema); ctx.mode = crate::mode::Mode::Simple; - let (result, _cmd) = - super::walk(source, super::outcome::WalkBound::EndOfInput, &mut ctx); + let (result, _cmd) = super::walk(source, super::outcome::WalkBound::EndOfInput, &mut ctx); result.map_or_else(Vec::new, |r| { r.diagnostics.into_iter().map(|d| d.message).collect() }) @@ -5014,12 +4907,11 @@ mod tests { ("SerNo", Type::Serial), ], ); - let diags = diag_keys_simple( - "insert into Customers values ('Oli', 52, 3)", - &schema, - ); + let diags = diag_keys_simple("insert into Customers values ('Oli', 52, 3)", &schema); assert!( - diags.iter().any(|d| d.contains("2 value(s)") && d.contains("3 given")), + diags + .iter() + .any(|d| d.contains("2 value(s)") && d.contains("3 given")), "simple Form B must fire arity diagnostic with user-fillable \ count (2) and actual (3); got {diags:?}", ); @@ -5044,14 +4936,17 @@ mod tests { // `insert_column`). let schema = schema_with( "Customers", - &[("id", Type::Serial), ("Name", Type::Text), ("Age", Type::Int)], - ); - let diags = diag_keys_simple( - "insert into Customers (Name, Age) values ('Oli')", - &schema, + &[ + ("id", Type::Serial), + ("Name", Type::Text), + ("Age", Type::Int), + ], ); + let diags = diag_keys_simple("insert into Customers (Name, Age) values ('Oli')", &schema); assert!( - diags.iter().any(|d| d.contains("names 2 column(s)") && d.contains("1 value(s)")), + diags + .iter() + .any(|d| d.contains("names 2 column(s)") && d.contains("1 value(s)")), "simple Form A must fire the column-list arity diagnostic; got {diags:?}", ); } @@ -5063,14 +4958,17 @@ mod tests { // though there is no `values` keyword to anchor on (issue #17). let schema = schema_with( "Customers", - &[("id", Type::Serial), ("Name", Type::Text), ("Age", Type::Int)], - ); - let diags = diag_keys_simple( - "insert into Customers ('Oli', 52, 3)", - &schema, + &[ + ("id", Type::Serial), + ("Name", Type::Text), + ("Age", Type::Int), + ], ); + let diags = diag_keys_simple("insert into Customers ('Oli', 52, 3)", &schema); assert!( - diags.iter().any(|d| d.contains("2 value(s)") && d.contains("3 given")), + diags + .iter() + .any(|d| d.contains("2 value(s)") && d.contains("3 given")), "simple Form C must fire the arity diagnostic (expected 2, got 3); got {diags:?}", ); } @@ -5080,17 +4978,13 @@ mod tests { // Edge: every column auto-generated (all serial/shortid). The // user-fillable count is 0, so any supplied value is a mismatch — // the tailored "all auto-generated" message fires (issue #17). - let schema = schema_with( - "Counters", - &[("id", Type::Serial), ("seq", Type::Serial)], - ); - let diags = diag_keys_simple( - "insert into Counters values (1)", - &schema, - ); + let schema = schema_with("Counters", &[("id", Type::Serial), ("seq", Type::Serial)]); + let diags = diag_keys_simple("insert into Counters values (1)", &schema); assert!( - diags.iter().any(|d| d.contains("all auto-generated") - || d.contains("auto-generated, so no values")), + diags + .iter() + .any(|d| d.contains("all auto-generated") + || d.contains("auto-generated, so no values")), "all-auto table must fire the tailored message; got {diags:?}", ); } @@ -5112,7 +5006,9 @@ mod tests { ); let diags = diag_keys("insert into Customers values ('Oli', 52, 3)", &schema); assert!( - diags.iter().any(|d| d.contains("all 4 column(s)") && d.contains("3 value(s)")), + diags + .iter() + .any(|d| d.contains("all 4 column(s)") && d.contains("3 value(s)")), "advanced Form B must keep the full-column count (4); got {diags:?}", ); } @@ -5143,24 +5039,18 @@ mod tests { fn unambiguous_bare_column_no_error() { let schema = two_table_schema(); // `name` is only in `a`; `total` is only in `b` — no ambiguity. - let diags = diag_keys( - "select name, total from a join b on a.id = b.id", - &schema, - ); - assert!( - diags.is_empty(), - "expected no diagnostics; got {diags:?}", - ); + let diags = diag_keys("select name, total from a join b on a.id = b.id", &schema); + assert!(diags.is_empty(), "expected no diagnostics; got {diags:?}",); } #[test] fn qualified_refs_in_join_on_resolve_cleanly() { let schema = two_table_schema(); - let diags = diag_keys("select a.name, b.total from a join b on a.id = b.id", &schema); - assert!( - diags.is_empty(), - "expected no diagnostics; got {diags:?}", + let diags = diag_keys( + "select a.name, b.total from a join b on a.id = b.id", + &schema, ); + assert!(diags.is_empty(), "expected no diagnostics; got {diags:?}",); } #[test] @@ -5287,7 +5177,10 @@ mod tests { // target-only column must resolve to the target `a`, not be // flagged against the SELECT source `b`. let schema = two_table_schema(); // a(id,name), b(id,total) - let diags = diag_keys("insert into a (id) select id from b returning name", &schema); + let diags = diag_keys( + "insert into a (id) select id from b returning name", + &schema, + ); assert!( !diags.iter().any(|d| d.contains("no such column")), "RETURNING `name` (a's column) must not flag against `b`; got {diags:?}", @@ -5310,7 +5203,10 @@ mod tests { #[test] fn unrelated_qualified_star_in_returning_still_flagged() { let schema = two_table_schema(); - let diags = diag_keys("insert into a (id) select id from b returning zzz.*", &schema); + let diags = diag_keys( + "insert into a (id) select id from b returning zzz.*", + &schema, + ); assert!( diags.iter().any(|d| d.contains("no such table or alias")), "unrelated `zzz.*` qualifier should still flag; got {diags:?}", @@ -5324,7 +5220,10 @@ mod tests { // target `a`, not flagged as an unknown qualifier (a is the // target, b is the SELECT source). let schema = two_table_schema(); - let diags = diag_keys("insert into a (id) select id from b returning a.name", &schema); + let diags = diag_keys( + "insert into a (id) select id from b returning a.name", + &schema, + ); assert!( diags.is_empty(), "target-qualified RETURNING ref must resolve cleanly; got {diags:?}", @@ -5335,7 +5234,10 @@ mod tests { fn target_qualified_ref_unknown_column_still_flagged() { // `a.nope` — a is the target but nope isn't its column. let schema = two_table_schema(); - let diags = diag_keys("insert into a (id) select id from b returning a.nope", &schema); + let diags = diag_keys( + "insert into a (id) select id from b returning a.nope", + &schema, + ); assert!( diags.iter().any(|d| d.contains("no such column")), "unknown column under the target qualifier should flag; got {diags:?}", @@ -5347,7 +5249,10 @@ mod tests { // A qualifier that's neither `excluded` nor the target is // still an unknown qualifier (the leak guard holds). let schema = two_table_schema(); - let diags = diag_keys("insert into a (id) select id from b returning zzz.name", &schema); + let diags = diag_keys( + "insert into a (id) select id from b returning zzz.name", + &schema, + ); assert!( diags.iter().any(|d| d.contains("no such table or alias")), "unrelated qualifier should still flag; got {diags:?}", @@ -5359,7 +5264,10 @@ mod tests { // Flip side: a RETURNING ref to a column on neither table is // flagged (against the INSERT target). let schema = two_table_schema(); - let diags = diag_keys("insert into a (id) select id from b returning nope", &schema); + let diags = diag_keys( + "insert into a (id) select id from b returning nope", + &schema, + ); assert!( diags.iter().any(|d| d.contains("no such column")), "unknown RETURNING column should be flagged; got {diags:?}", @@ -5372,9 +5280,15 @@ mod tests { // a valid target column is silent, an unknown one flags. let schema = schema_with("t", &[("a", Type::Int), ("b", Type::Text)]); let ok = diag_keys("insert into t (a) values (1) returning b", &schema); - assert!(!ok.iter().any(|d| d.contains("no such column")), "got {ok:?}"); + assert!( + !ok.iter().any(|d| d.contains("no such column")), + "got {ok:?}" + ); let bad = diag_keys("insert into t (a) values (1) returning nope", &schema); - assert!(bad.iter().any(|d| d.contains("no such column")), "got {bad:?}"); + assert!( + bad.iter().any(|d| d.contains("no such column")), + "got {bad:?}" + ); } #[test] @@ -5473,7 +5387,10 @@ mod tests { // `b` is NOT NULL with no default and is omitted → WARNING. let schema = schema_required( "t", - &[("a", Type::Int, false, false), ("b", Type::Text, true, false)], + &[ + ("a", Type::Int, false, false), + ("b", Type::Text, true, false), + ], ); let diags = diag_keys("insert into t (a) values (1)", &schema); assert!( @@ -5486,7 +5403,10 @@ mod tests { fn not_null_missing_silent_when_included() { let schema = schema_required( "t", - &[("a", Type::Int, false, false), ("b", Type::Text, true, false)], + &[ + ("a", Type::Int, false, false), + ("b", Type::Text, true, false), + ], ); let diags = diag_keys("insert into t (a, b) values (1, 'x')", &schema); assert!( @@ -5500,7 +5420,10 @@ mod tests { // NOT NULL but DEFAULT present → omitting is fine. let schema = schema_required( "t", - &[("a", Type::Int, false, false), ("b", Type::Text, true, true)], + &[ + ("a", Type::Int, false, false), + ("b", Type::Text, true, true), + ], ); let diags = diag_keys("insert into t (a) values (1)", &schema); assert!( @@ -5515,7 +5438,10 @@ mod tests { // omitting it is correct — not a missing required value. let schema = schema_required( "t", - &[("id", Type::Serial, true, false), ("b", Type::Text, false, false)], + &[ + ("id", Type::Serial, true, false), + ("b", Type::Text, false, false), + ], ); let diags = diag_keys("insert into t (b) values ('x')", &schema); assert!( @@ -5551,10 +5477,7 @@ mod tests { ("SerNo", Type::Serial), ], ); - let diags = diag_keys( - "insert into Customers values ('Oli', 52, 3)", - &schema, - ); + let diags = diag_keys("insert into Customers values ('Oli', 52, 3)", &schema); assert!( diags.iter().any(|d| d.contains("value(s) are given")), "Form B under-supply must fire arity diagnostic; got {diags:?}", @@ -5597,10 +5520,7 @@ mod tests { ("SerNo", Type::Serial), ], ); - let diags = diag_keys( - "insert into Customers values (13, 'Oli', 42, 13)", - &schema, - ); + let diags = diag_keys("insert into Customers values (13, 'Oli', 42, 13)", &schema); assert!( !diags.iter().any(|d| d.contains("value(s) are given")), "matched Form B arity must not fire; got {diags:?}", @@ -5614,10 +5534,7 @@ mod tests { // (the schema-existence pass flags the unknown table, not the // arity pass). let schema = schema_with("Customers", &[("Name", Type::Text)]); - let diags = diag_keys( - "insert into Strangers values ('Oli', 52, 3)", - &schema, - ); + let diags = diag_keys("insert into Strangers values ('Oli', 52, 3)", &schema); assert!( !diags.iter().any(|d| d.contains("value(s) are given")), "unknown table must not trigger an arity diagnostic; got {diags:?}", @@ -5643,8 +5560,14 @@ mod tests { "insert into t (a, b) values (1, 2), (3, 4, 5), (6), (7, 8)", &schema, ); - let n = diags.iter().filter(|d| d.contains("value(s) are given")).count(); - assert_eq!(n, 2, "rows 2 and 3 mismatch, rows 1 and 4 match; got {diags:?}"); + let n = diags + .iter() + .filter(|d| d.contains("value(s) are given")) + .count(); + assert_eq!( + n, 2, + "rows 2 and 3 mismatch, rows 1 and 4 match; got {diags:?}" + ); } #[test] @@ -5657,8 +5580,14 @@ mod tests { "insert into t (a, b) values (1, 2, 3) on conflict (a) do nothing", &schema, ); - let n = diags.iter().filter(|d| d.contains("value(s) are given")).count(); - assert_eq!(n, 1, "only the 3-value tuple is flagged, not the conflict target; got {diags:?}"); + let n = diags + .iter() + .filter(|d| d.contains("value(s) are given")) + .count(); + assert_eq!( + n, 1, + "only the 3-value tuple is flagged, not the conflict target; got {diags:?}" + ); } #[test] @@ -5683,7 +5612,10 @@ mod tests { // RETURNING projection isn't mis-counted as a value tuple. let schema = schema_with("t", &[("a", Type::Int), ("b", Type::Int)]); let diags = diag_keys("insert into t (a, b) values (1, 2, 3) returning *", &schema); - let n = diags.iter().filter(|d| d.contains("value(s) are given")).count(); + let n = diags + .iter() + .filter(|d| d.contains("value(s) are given")) + .count(); assert_eq!(n, 1, "only the 3-value tuple is flagged; got {diags:?}"); } @@ -5777,7 +5709,10 @@ mod tests { "insert into a (id, name) values (1, 'x') on conflict (id) do update set name = excluded.name", &schema, ); - assert!(diags.is_empty(), "excluded.name should resolve in DO UPDATE; got {diags:?}"); + assert!( + diags.is_empty(), + "excluded.name should resolve in DO UPDATE; got {diags:?}" + ); } #[test] @@ -5789,7 +5724,10 @@ mod tests { "insert into a (id, name) values (1, 'x') on conflict (id) do update set name = 'y' where id = excluded.id", &schema, ); - assert!(diags.is_empty(), "excluded in DO UPDATE WHERE should resolve; got {diags:?}"); + assert!( + diags.is_empty(), + "excluded in DO UPDATE WHERE should resolve; got {diags:?}" + ); } #[test] @@ -5896,10 +5834,7 @@ mod tests { // the DISTINCT keyword doesn't shield the column-ref // validation downstream. let schema = schema_with("Customers", &[("Age", Type::Int)]); - let diags = diag_keys( - "select count(distinct nonexistent) from Customers", - &schema, - ); + let diags = diag_keys("select count(distinct nonexistent) from Customers", &schema); assert!( diags.iter().any(|d| d.contains("no such column")), "unknown column inside `count(distinct …)` must still fire; got {diags:?}", @@ -5913,10 +5848,7 @@ mod tests { // ordinary expressions and a genuinely unknown column inside // a call must still fire `no such column`. let schema = schema_with("Customers", &[("Age", Type::Int)]); - let diags = diag_keys( - "select sum(nonexistent) from Customers", - &schema, - ); + let diags = diag_keys("select sum(nonexistent) from Customers", &schema); assert!( diags.iter().any(|d| d.contains("no such column")), "unknown column inside the call args must still fire; got {diags:?}", @@ -5930,10 +5862,7 @@ mod tests { // embedded in an INSERT (no re-implementation needed). // `nonexistent_col` is not a column of `a`. let schema = two_table_schema(); - let diags = diag_keys( - "insert into b select nonexistent_col from a", - &schema, - ); + let diags = diag_keys("insert into b select nonexistent_col from a", &schema); assert!( diags.iter().any(|d| d.contains("no such column")), "expected unknown_column on the INSERT…SELECT projection; \ @@ -6027,7 +5956,10 @@ mod tests { // INSERT VALUES carries no row scope, so the realizable // claim is the INSERT … SELECT slot, not VALUES. let schema = two_table_schema(); // a(id,name), b(id,total real) - let diags = diag_keys("insert into a (id) select id from b where total like 5", &schema); + let diags = diag_keys( + "insert into a (id) select id from b where total like 5", + &schema, + ); assert!( diags.iter().any(|d| d.contains("LIKE")), "expected like_numeric on the INSERT…SELECT WHERE; got {diags:?}", @@ -6043,10 +5975,7 @@ mod tests { "with cte_x as (select * from base) select * from cte_x", &schema, ); - assert!( - diags.is_empty(), - "expected no diagnostics; got {diags:?}", - ); + assert!(diags.is_empty(), "expected no diagnostics; got {diags:?}",); } #[test] @@ -6060,8 +5989,7 @@ mod tests { let mut ctx = super::context::WalkContext::with_schema(&schema); let mut path = super::outcome::MatchedPath::new(); let mut per_byte: Vec = Vec::new(); - let input = - "with x as (select 1), x as (select 2) select * from x"; + let input = "with x as (select 1), x as (select 2) select * from x"; let result = crate::dsl::walker::driver::walk_node( input, 0, @@ -6078,8 +6006,7 @@ mod tests { "fragment should walk: {result:?}" ); let diags = super::schema_existence_diagnostics(&path, Some(&schema)); - let messages: Vec<&str> = - diags.iter().map(|d| d.message.as_str()).collect(); + let messages: Vec<&str> = diags.iter().map(|d| d.message.as_str()).collect(); assert!( messages.iter().any(|m| m.contains("duplicate")), "expected duplicate_cte; got {messages:?}", @@ -6135,10 +6062,7 @@ mod tests { let schema = two_table_schema(); // The alias `x` resolves to `a` — `x.name` finds `a.name`. let diags = diag_keys("select x.name from a x", &schema); - assert!( - diags.is_empty(), - "expected no diagnostics; got {diags:?}", - ); + assert!(diags.is_empty(), "expected no diagnostics; got {diags:?}",); } // ---- ADR-0032 §11.6 — Phase-1 carry-over gap closure ---- @@ -6166,8 +6090,7 @@ mod tests { // Phase 1's predicate-warning pass walked the DSL Expr // AST and never saw SQL WHERE. let schema = typed_schema(); - let diags = - diag_keys("select * from products where price like 5", &schema); + let diags = diag_keys("select * from products where price like 5", &schema); assert!( diags.iter().any(|d| d.contains("LIKE")), "expected like_numeric warning on SQL WHERE; got {diags:?}", @@ -6177,8 +6100,7 @@ mod tests { #[test] fn sql_where_eq_null_warns() { let schema = typed_schema(); - let diags = - diag_keys("select * from products where name = null", &schema); + let diags = diag_keys("select * from products where name = null", &schema); assert!( diags.iter().any(|d| d.contains("= NULL")), "expected eq_null warning on SQL WHERE; got {diags:?}", @@ -6188,8 +6110,7 @@ mod tests { #[test] fn sql_where_type_mismatch_text_vs_number_warns() { let schema = typed_schema(); - let diags = - diag_keys("select * from products where name = 5", &schema); + let diags = diag_keys("select * from products where name = 5", &schema); assert!( diags.iter().any(|d| d.contains("different type")), "expected type_mismatch warning on SQL WHERE; got {diags:?}", @@ -6199,10 +6120,7 @@ mod tests { #[test] fn sql_where_type_mismatch_number_vs_text_warns() { let schema = typed_schema(); - let diags = diag_keys( - "select * from products where price = 'high'", - &schema, - ); + let diags = diag_keys("select * from products where price = 'high'", &schema); assert!( diags.iter().any(|d| d.contains("different type")), "expected type_mismatch warning on SQL WHERE; got {diags:?}", @@ -6212,8 +6130,7 @@ mod tests { #[test] fn sql_where_type_compatible_does_not_warn() { let schema = typed_schema(); - let diags = - diag_keys("select * from products where price = 5", &schema); + let diags = diag_keys("select * from products where price = 5", &schema); // `price` is real; `5` is numeric — compatible (any // numeric-real comparison is fine). No warning. assert!( @@ -6258,10 +6175,7 @@ mod tests { // Predicate-shape inside ORDER BY's sql_expr — same // pass, same warning. let schema = typed_schema(); - let diags = diag_keys( - "select * from products order by price like 5", - &schema, - ); + let diags = diag_keys("select * from products order by price like 5", &schema); assert!( diags.iter().any(|d| d.contains("LIKE")), "expected like_numeric warning inside ORDER BY; got {diags:?}", @@ -6273,10 +6187,7 @@ mod tests { // Predicate shape used as a projection item (returns // 0/1). Same warning surface. let schema = typed_schema(); - let diags = diag_keys( - "select price like 5 from products", - &schema, - ); + let diags = diag_keys("select price like 5 from products", &schema); assert!( diags.iter().any(|d| d.contains("LIKE")), "expected like_numeric warning inside projection; got {diags:?}", @@ -6293,16 +6204,23 @@ mod tests { cache.columns.push("price".to_string()); cache.table_columns.insert( "a".to_string(), - vec![TableColumn { name: "id".to_string(), user_type: Type::Int, not_null: false, has_default: false }], + vec![TableColumn { + name: "id".to_string(), + user_type: Type::Int, + not_null: false, + has_default: false, + }], ); cache.table_columns.insert( "b".to_string(), - vec![TableColumn { name: "price".to_string(), user_type: Type::Real, not_null: false, has_default: false }], - ); - let diags = diag_keys( - "select * from a join b on price like 5", - &cache, + vec![TableColumn { + name: "price".to_string(), + user_type: Type::Real, + not_null: false, + has_default: false, + }], ); + let diags = diag_keys("select * from a join b on price like 5", &cache); assert!( diags.iter().any(|d| d.contains("LIKE")), "expected like_numeric warning on JOIN ON; got {diags:?}", @@ -6315,18 +6233,12 @@ mod tests { fn projection_alias_in_where_is_misplaced() { // ADR-0032 §11.2 plan test: `SELECT a + b AS x FROM t // WHERE x > 0` fires `projection_alias_misplaced`. - let schema = schema_with( - "t", - &[("a", Type::Int), ("b", Type::Int)], - ); - let diags = diag_keys( - "select a + b as x from t where x > 0", - &schema, - ); + let schema = schema_with("t", &[("a", Type::Int), ("b", Type::Int)]); + let diags = diag_keys("select a + b as x from t where x > 0", &schema); assert!( - diags.iter().any(|d| { - d.contains("alias `x`") && d.contains("WHERE") - }), + diags + .iter() + .any(|d| { d.contains("alias `x`") && d.contains("WHERE") }), "expected projection_alias_misplaced on WHERE; got {diags:?}", ); // The unknown_column diagnostic must NOT also fire on @@ -6339,36 +6251,24 @@ mod tests { #[test] fn projection_alias_in_having_is_misplaced() { - let schema = schema_with( - "t", - &[("a", Type::Int), ("b", Type::Int)], - ); - let diags = diag_keys( - "select a + b as x from t group by a having x > 0", - &schema, - ); + let schema = schema_with("t", &[("a", Type::Int), ("b", Type::Int)]); + let diags = diag_keys("select a + b as x from t group by a having x > 0", &schema); assert!( - diags.iter().any(|d| { - d.contains("alias `x`") && d.contains("HAVING") - }), + diags + .iter() + .any(|d| { d.contains("alias `x`") && d.contains("HAVING") }), "expected projection_alias_misplaced on HAVING; got {diags:?}", ); } #[test] fn projection_alias_in_group_by_is_misplaced() { - let schema = schema_with( - "t", - &[("a", Type::Int), ("b", Type::Int)], - ); - let diags = diag_keys( - "select a + b as x from t group by x", - &schema, - ); + let schema = schema_with("t", &[("a", Type::Int), ("b", Type::Int)]); + let diags = diag_keys("select a + b as x from t group by x", &schema); assert!( - diags.iter().any(|d| { - d.contains("alias `x`") && d.contains("GROUP BY") - }), + diags + .iter() + .any(|d| { d.contains("alias `x`") && d.contains("GROUP BY") }), "expected projection_alias_misplaced on GROUP BY; got {diags:?}", ); } @@ -6377,18 +6277,9 @@ mod tests { fn projection_alias_in_order_by_is_allowed() { // ADR-0032 §11.2 negative case: `… ORDER BY x` doesn't // fire — aliases are bound by ORDER BY evaluation time. - let schema = schema_with( - "t", - &[("a", Type::Int), ("b", Type::Int)], - ); - let diags = diag_keys( - "select a + b as x from t order by x", - &schema, - ); - assert!( - diags.is_empty(), - "ORDER BY alias is allowed; got {diags:?}", - ); + let schema = schema_with("t", &[("a", Type::Int), ("b", Type::Int)]); + let diags = diag_keys("select a + b as x from t order by x", &schema); + assert!(diags.is_empty(), "ORDER BY alias is allowed; got {diags:?}",); } #[test] @@ -6397,14 +6288,8 @@ mod tests { // `id` shadows nothing in the table, but a real column // `id` exists. WHERE id refers to the table column (per // SQL spec); the diagnostic must NOT fire. - let schema = schema_with( - "t", - &[("id", Type::Int), ("name", Type::Text)], - ); - let diags = diag_keys( - "select name as id from t where id > 0", - &schema, - ); + let schema = schema_with("t", &[("id", Type::Int), ("name", Type::Text)]); + let diags = diag_keys("select name as id from t where id > 0", &schema); assert!( diags.is_empty(), "real-column WHERE ref must not be flagged as misplaced; got {diags:?}", @@ -6440,8 +6325,7 @@ mod tests { // The same applies outside GROUP BY — a bare alias in the // projection (`SELECT o …`) is equally not a column. let schema = two_table_schema(); - let diags = - diag_keys("select o from a o join b on a.id = b.id", &schema); + let diags = diag_keys("select o from a o join b on a.id = b.id", &schema); assert!( diags.iter().any(|d| d.contains("`o` is a table alias")), "expected alias_used_as_column hint in projection; got {diags:?}", @@ -6472,7 +6356,9 @@ mod tests { &schema, ); assert!( - diags.iter().any(|d| d.contains("no such column") && d.contains("nope")), + diags + .iter() + .any(|d| d.contains("no such column") && d.contains("nope")), "a genuine unknown column must still report no such column; got {diags:?}", ); } @@ -6491,7 +6377,9 @@ mod tests { &schema, ); assert!( - diags.iter().any(|d| d.contains("no such column") && d.contains("`a`")), + diags + .iter() + .any(|d| d.contains("no such column") && d.contains("`a`")), "an aliased table referenced by its real name must fall through to \ unknown_column; got {diags:?}", ); @@ -6508,8 +6396,7 @@ mod tests { // has no `table.column` syntax, so advising the qualified form // would be wrong. A DSL bare table-name ref stays the generic // unknown_column it was before issue #31. - let schema = - schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); + let schema = schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]); for input in [ "show data Customers where Customers = 5", "update Customers set Name = 'x' where Customers = 5", @@ -6535,10 +6422,9 @@ mod tests { let schema = schema_with("t", &[("a", Type::Int)]); let diags = diag_keys("select 1, 2 union select 1", &schema); assert!( - diags.iter().any(|d| { - d.contains("union") - && d.contains("number of columns") - }), + diags + .iter() + .any(|d| { d.contains("union") && d.contains("number of columns") }), "expected compound_arity_mismatch on UNION; got {diags:?}", ); } @@ -6557,13 +6443,11 @@ mod tests { #[test] fn compound_intersect_arity_mismatch_fires() { let schema = schema_with("t", &[("a", Type::Int)]); - let diags = - diag_keys("select 1 intersect select 1, 2", &schema); + let diags = diag_keys("select 1 intersect select 1, 2", &schema); assert!( - diags.iter().any(|d| { - d.contains("intersect") - && d.contains("number of columns") - }), + diags + .iter() + .any(|d| { d.contains("intersect") && d.contains("number of columns") }), "expected compound_arity_mismatch on INTERSECT; got {diags:?}", ); } @@ -6571,13 +6455,11 @@ mod tests { #[test] fn compound_except_arity_mismatch_fires() { let schema = schema_with("t", &[("a", Type::Int)]); - let diags = - diag_keys("select 1, 2, 3 except select 1, 2", &schema); + let diags = diag_keys("select 1, 2, 3 except select 1, 2", &schema); assert!( - diags.iter().any(|d| { - d.contains("except") - && d.contains("number of columns") - }), + diags + .iter() + .any(|d| { d.contains("except") && d.contains("number of columns") }), "expected compound_arity_mismatch on EXCEPT; got {diags:?}", ); } @@ -6587,14 +6469,8 @@ mod tests { // Function-call commas are at deeper depth — they must // not be counted as projection items. // `count(a, b)` is ONE projection item. - let schema = schema_with( - "t", - &[("a", Type::Int), ("b", Type::Int)], - ); - let diags = diag_keys( - "select count(a, b) from t union select 1", - &schema, - ); + let schema = schema_with("t", &[("a", Type::Int), ("b", Type::Int)]); + let diags = diag_keys("select count(a, b) from t union select 1", &schema); assert!( !diags.iter().any(|d| d.contains("number of columns")), "function-call commas must not inflate arity; got {diags:?}", @@ -6607,10 +6483,9 @@ mod tests { let schema = schema_with("t", &[("a", Type::Int)]); let diags = diag_keys("select 1 union all select 1, 2", &schema); assert!( - diags.iter().any(|d| { - d.contains("union") - && d.contains("number of columns") - }), + diags + .iter() + .any(|d| { d.contains("union") && d.contains("number of columns") }), "expected compound_arity_mismatch on UNION ALL; got {diags:?}", ); } @@ -6620,10 +6495,7 @@ mod tests { // Chained legs at the same depth — each set-op compares // its preceding leg against its following leg. let schema = schema_with("t", &[("a", Type::Int)]); - let diags = diag_keys( - "select 1 union select 1, 2 union select 1", - &schema, - ); + let diags = diag_keys("select 1 union select 1, 2 union select 1", &schema); let mismatch_count = diags .iter() .filter(|d| d.contains("number of columns")) @@ -6644,10 +6516,9 @@ mod tests { &schema, ); assert!( - diags.iter().any(|d| { - d.contains("union") - && d.contains("number of columns") - }), + diags + .iter() + .any(|d| { d.contains("union") && d.contains("number of columns") }), "expected compound_arity_mismatch inside CTE body; got {diags:?}", ); } @@ -6659,10 +6530,7 @@ mod tests { // `WITH x(a, b) AS (SELECT 1, 2, 3)` — declared 2, // derived 3 → fires. let schema = schema_with("base", &[("id", Type::Int)]); - let diags = diag_keys( - "with x (a, b) as (select 1, 2, 3) select * from x", - &schema, - ); + let diags = diag_keys("with x (a, b) as (select 1, 2, 3) select * from x", &schema); assert!( diags.iter().any(|d| { d.contains("CTE `x`") @@ -6678,10 +6546,7 @@ mod tests { // `WITH x(a, b, c) AS (SELECT 1)` — declared 3, // derived 1 → fires. let schema = schema_with("base", &[("id", Type::Int)]); - let diags = diag_keys( - "with x (a, b, c) as (select 1) select * from x", - &schema, - ); + let diags = diag_keys("with x (a, b, c) as (select 1) select * from x", &schema); assert!( diags.iter().any(|d| { d.contains("CTE `x`") @@ -6697,10 +6562,7 @@ mod tests { // `WITH x(a, b) AS (SELECT 1, 2)` — matched arity, no // diagnostic. let schema = schema_with("base", &[("id", Type::Int)]); - let diags = diag_keys( - "with x (a, b) as (select 1, 2) select * from x", - &schema, - ); + let diags = diag_keys("with x (a, b) as (select 1, 2) select * from x", &schema); assert!( !diags.iter().any(|d| d.contains("declares")), "matched arity should not fire; got {diags:?}", @@ -6712,10 +6574,7 @@ mod tests { // No explicit col-list → no arity check (derived // columns are the canonical view). let schema = schema_with("base", &[("id", Type::Int)]); - let diags = diag_keys( - "with x as (select 1, 2, 3) select * from x", - &schema, - ); + let diags = diag_keys("with x as (select 1, 2, 3) select * from x", &schema); assert!( !diags.iter().any(|d| d.contains("declares")), "no col-list should suppress arity check; got {diags:?}", @@ -6730,14 +6589,8 @@ mod tests { // — here we test that the outer `WHERE y` is flagged // as unknown_column (not misplaced) because there is no // alias `y` in the OUTER leg's projection. - let schema = schema_with( - "t", - &[("a", Type::Int), ("b", Type::Int)], - ); - let diags = diag_keys( - "select (select a as y from t) from t where y > 0", - &schema, - ); + let schema = schema_with("t", &[("a", Type::Int), ("b", Type::Int)]); + let diags = diag_keys("select (select a as y from t) from t where y > 0", &schema); assert!( !diags.iter().any(|d| d.contains("misplaced")), "inner-subquery alias must not affect outer scope; got {diags:?}", @@ -6795,14 +6648,10 @@ mod projection_before_from_tests { s } - fn diagnostics_advanced( - source: &str, - schema: &SchemaCache, - ) -> Vec { + fn diagnostics_advanced(source: &str, schema: &SchemaCache) -> Vec { let mut ctx = context::WalkContext::with_schema(schema); ctx.mode = crate::mode::Mode::Advanced; - let (result, _) = - walk(source, outcome::WalkBound::EndOfInput, &mut ctx); + let (result, _) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx); result.map_or_else(Vec::new, |r| r.diagnostics) } @@ -6813,8 +6662,7 @@ mod projection_before_from_tests { // but the two-pass diagnostic resolves correctly // against the eventual scope. No diagnostic. let schema = schema_with_table_and_columns(); - let diags = - diagnostics_advanced("select real_col from mytable", &schema); + let diags = diagnostics_advanced("select real_col from mytable", &schema); assert!( diags.is_empty(), "projection-before-FROM legit column must not be flagged; got {diags:?}", @@ -6828,8 +6676,7 @@ mod projection_before_from_tests { // projection ident's span; the renderer overlays this // as Error in `input_render.rs`. let schema = schema_with_table_and_columns(); - let diags = - diagnostics_advanced("select bogus_col from mytable", &schema); + let diags = diagnostics_advanced("select bogus_col from mytable", &schema); assert_eq!(diags.len(), 1, "{diags:?}"); assert_eq!(diags[0].severity, outcome::Severity::Error); // Span should cover `bogus_col` (offset 7..16). @@ -6867,10 +6714,7 @@ mod projection_before_from_tests { // schema_existence pass. let schema = schema_with_table_and_columns(); let diags = diagnostics_advanced("select c1, c2", &schema); - assert!( - diags.is_empty(), - "no FROM → silent; got {diags:?}", - ); + assert!(diags.is_empty(), "no FROM → silent; got {diags:?}",); } } @@ -6889,9 +6733,7 @@ mod projection_before_from_tests { mod dispatch_3a_tests { use super::*; use crate::dsl::command::{AppCommand, Command}; - use crate::dsl::grammar::{ - CommandCategory, CommandNode, Node, ValidationError, Word, - }; + use crate::dsl::grammar::{CommandCategory, CommandNode, Node, ValidationError, Word}; use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace}; use crate::dsl::walker::outcome::MatchedPath; use crate::mode::Mode; @@ -6962,9 +6804,10 @@ mod dispatch_3a_tests { let (res, cmd) = walk_one_command(input, input, ks, ke, idx, node, &mut ctx); (res.outcome, cmd) } - Decision::ThisIsSql { primary } => { - (this_is_sql_result(entry_text, primary, ks, ke).outcome, None) - } + Decision::ThisIsSql { primary } => ( + this_is_sql_result(entry_text, primary, ks, ke).outcome, + None, + ), } } @@ -6981,11 +6824,17 @@ mod dispatch_3a_tests { fn simple_mode_dsl_input_matches_dsl() { let cands = shared(); assert!( - std::ptr::eq(committed_node("smk dsltail", Mode::Simple, &cands), &SMOKE_DSL), + std::ptr::eq( + committed_node("smk dsltail", Mode::Simple, &cands), + &SMOKE_DSL + ), "simple mode must commit the DSL node for DSL input", ); let (outcome, cmd) = dispatch("smk dsltail", Mode::Simple, &cands); - assert!(matches!(outcome, WalkOutcome::Match { .. }), "got {outcome:?}"); + assert!( + matches!(outcome, WalkOutcome::Match { .. }), + "got {outcome:?}" + ); assert_eq!(cmd, Some(Command::App(AppCommand::Help { topic: None }))); } @@ -6995,11 +6844,17 @@ mod dispatch_3a_tests { fn advanced_mode_sql_input_matches_sql() { let cands = shared(); assert!( - std::ptr::eq(committed_node("smk sqltail", Mode::Advanced, &cands), &SMOKE_SQL), + std::ptr::eq( + committed_node("smk sqltail", Mode::Advanced, &cands), + &SMOKE_SQL + ), "advanced mode must commit the SQL node for SQL input", ); let (outcome, cmd) = dispatch("smk sqltail", Mode::Advanced, &cands); - assert!(matches!(outcome, WalkOutcome::Match { .. }), "got {outcome:?}"); + assert!( + matches!(outcome, WalkOutcome::Match { .. }), + "got {outcome:?}" + ); assert_eq!(cmd, Some(Command::App(AppCommand::Quit))); } @@ -7020,7 +6875,10 @@ mod dispatch_3a_tests { // form (see `simple_mode_sql_only_entry_word_is_this_is_sql`). let cands = shared(); assert!( - std::ptr::eq(committed_node("smk sqltail", Mode::Simple, &cands), &SMOKE_DSL), + std::ptr::eq( + committed_node("smk sqltail", Mode::Simple, &cands), + &SMOKE_DSL + ), "simple mode must commit the DSL node for a shared word", ); let (outcome, cmd) = dispatch("smk sqltail", Mode::Simple, &cands); @@ -7057,11 +6915,17 @@ mod dispatch_3a_tests { // DSL node rather than surfacing the SQL node's failure. let cands = shared(); assert!( - std::ptr::eq(committed_node("smk dsltail", Mode::Advanced, &cands), &SMOKE_DSL), + std::ptr::eq( + committed_node("smk dsltail", Mode::Advanced, &cands), + &SMOKE_DSL + ), "advanced mode must fall back to DSL when SQL doesn't match", ); let (outcome, cmd) = dispatch("smk dsltail", Mode::Advanced, &cands); - assert!(matches!(outcome, WalkOutcome::Match { .. }), "got {outcome:?}"); + assert!( + matches!(outcome, WalkOutcome::Match { .. }), + "got {outcome:?}" + ); assert_eq!(cmd, Some(Command::App(AppCommand::Help { topic: None }))); } @@ -7075,7 +6939,10 @@ mod dispatch_3a_tests { &SMOKE_DSL, )); let (outcome, _) = dispatch("smk dsltail", Mode::Advanced, &cands); - assert!(matches!(outcome, WalkOutcome::Match { .. }), "got {outcome:?}"); + assert!( + matches!(outcome, WalkOutcome::Match { .. }), + "got {outcome:?}" + ); } } @@ -7106,8 +6973,19 @@ mod order_by_expected_set_tests { fn order_by_excludes_preceding_clause_keywords() { let words = expected_words("select Name from T order by "); let preceding_clause_kw = [ - "where", "group", "having", "join", "union", "intersect", - "except", "left", "right", "full", "cross", "inner", "as", + "where", + "group", + "having", + "join", + "union", + "intersect", + "except", + "left", + "right", + "full", + "cross", + "inner", + "as", ]; let leaked: Vec<&str> = preceding_clause_kw .iter() @@ -7131,10 +7009,7 @@ mod order_by_expected_set_tests { assert!(words.contains(&"asc"), "expected `asc`; got {words:?}"); assert!(words.contains(&"desc"), "expected `desc`; got {words:?}"); // The separator is deliberately not surfaced (user choice). - let full = expected_at_input_in_mode( - "select Name from T order by Name ", - Mode::Advanced, - ); + let full = expected_at_input_in_mode("select Name from T order by Name ", Mode::Advanced); assert!( !full.iter().any(|e| matches!(e, Expectation::Punct(','))), "`,` separator should not be surfaced; got {full:?}", @@ -7146,15 +7021,11 @@ mod order_by_expected_set_tests { // Guard against over-correction: the legitimate sort-item // continuation (a column identifier) must survive the // pending-skipped suppression. - let expected = expected_at_input_in_mode( - "select Name from T order by ", - Mode::Advanced, - ); + let expected = expected_at_input_in_mode("select Name from T order by ", Mode::Advanced); assert!( - expected.iter().any(|e| matches!( - e, - Expectation::Ident { .. } | Expectation::NumberLit - )), + expected + .iter() + .any(|e| matches!(e, Expectation::Ident { .. } | Expectation::NumberLit)), "ORDER BY must still offer a sort item; got {expected:?}", ); } diff --git a/src/echo.rs b/src/echo.rs index 04461ca..1eceaf7 100644 --- a/src/echo.rs +++ b/src/echo.rs @@ -14,12 +14,12 @@ //! advanced effective mode (ADR-0037). use crate::app::EffectiveMode; -use crate::dsl::ReferentialAction; -use crate::dsl::types::Type; use crate::dsl::Command; +use crate::dsl::ReferentialAction; use crate::dsl::command::{ ColumnSpec, CompareOp, Constraint, ConstraintKind, Expr, Operand, Predicate, RowFilter, }; +use crate::dsl::types::Type; use crate::dsl::value::Value; /// The dimmed `Executing SQL:` prefix on a teaching-echo line @@ -79,7 +79,12 @@ pub fn echo_for_query( name, filter, limit, - } => Some(vec![render_show_data(name, filter.as_ref(), *limit, primary_key)]), + } => Some(vec![render_show_data( + name, + filter.as_ref(), + *limit, + primary_key, + )]), _ => None, } } @@ -150,12 +155,12 @@ pub fn command_to_sql(command: &Command) -> Option { column, kind, } => match kind { - ConstraintKind::NotNull => { - Some(format!("ALTER TABLE {table} ALTER COLUMN {column} DROP NOT NULL")) - } - ConstraintKind::Default => { - Some(format!("ALTER TABLE {table} ALTER COLUMN {column} DROP DEFAULT")) - } + ConstraintKind::NotNull => Some(format!( + "ALTER TABLE {table} ALTER COLUMN {column} DROP NOT NULL" + )), + ConstraintKind::Default => Some(format!( + "ALTER TABLE {table} ALTER COLUMN {column} DROP DEFAULT" + )), // A column-level UNIQUE / CHECK is anonymous in our model — // no portable name to DROP CONSTRAINT by, so no echo (Bucket C, // ADR-0035 Amendment 2 residual gap / ADR-0038 §7). @@ -169,7 +174,10 @@ pub fn command_to_sql(command: &Command) -> Option { table, assignments, filter: RowFilter::AllRows, - } => Some(format!("UPDATE {table} SET {}", render_assignments(assignments))), + } => Some(format!( + "UPDATE {table} SET {}", + render_assignments(assignments) + )), Command::Delete { table, filter: RowFilter::AllRows, @@ -199,7 +207,13 @@ fn render_create_table(name: &str, columns: &[ColumnSpec], primary_key: &[String // The same column-constraint suffix `add column` emits (ADR-0029): // simple-mode `create table` can carry `default` / `check` too, so // the echo must render them or it is not equivalent (§1 contract). - append_constraints(&mut s, c.not_null, c.unique, c.default.as_ref(), c.check.as_ref()); + append_constraints( + &mut s, + c.not_null, + c.unique, + c.default.as_ref(), + c.check.as_ref(), + ); s }) .collect(); @@ -299,8 +313,10 @@ pub(crate) fn render_create_m2n( primary_key: &[String], foreign_keys: &[(Vec, String, Vec)], ) -> String { - let mut parts: Vec = - columns.iter().map(|(n, ty)| format!("{n} {}", ty.keyword())).collect(); + let mut parts: Vec = columns + .iter() + .map(|(n, ty)| format!("{n} {}", ty.keyword())) + .collect(); parts.push(format!("PRIMARY KEY ({})", primary_key.join(", "))); for (child_columns, parent_table, parent_columns) in foreign_keys { parts.push(format!( @@ -368,7 +384,12 @@ pub(crate) fn render_add_relationship_create_fk( ) -> Vec { let mut lines: Vec = new_columns .iter() - .map(|(col, ty)| format!("ALTER TABLE {child_table} ADD COLUMN {col} {}", ty.keyword())) + .map(|(col, ty)| { + format!( + "ALTER TABLE {child_table} ADD COLUMN {col} {}", + ty.keyword() + ) + }) .collect(); lines.push(render_add_relationship( name, @@ -461,7 +482,11 @@ fn predicate_to_sql(predicate: &Predicate) -> String { negated, } => { let not = if *negated { "NOT " } else { "" }; - format!("{} {not}LIKE {}", operand_to_sql(target), operand_to_sql(pattern)) + format!( + "{} {not}LIKE {}", + operand_to_sql(target), + operand_to_sql(pattern) + ) } Predicate::Between { target, @@ -484,7 +509,11 @@ fn predicate_to_sql(predicate: &Predicate) -> String { } => { let not = if *negated { "NOT " } else { "" }; let rendered: Vec = items.iter().map(operand_to_sql).collect(); - format!("{} {not}IN ({})", operand_to_sql(target), rendered.join(", ")) + format!( + "{} {not}IN ({})", + operand_to_sql(target), + rendered.join(", ") + ) } Predicate::IsNull { target, negated } => { let not = if *negated { "NOT " } else { "" }; @@ -562,7 +591,10 @@ mod tests { fn create_table_compound_pk_renders_table_level() { let cmd = create_table( "T", - vec![ColumnSpec::new("a", Type::Int), ColumnSpec::new("b", Type::Int)], + vec![ + ColumnSpec::new("a", Type::Int), + ColumnSpec::new("b", Type::Int), + ], &["a", "b"], ); assert_eq!( @@ -594,7 +626,11 @@ mod tests { default: Some(Value::Text("A".to_string())), ..ColumnSpec::new("grade", Type::Text) }; - let cmd = create_table("T", vec![ColumnSpec::new("id", Type::Serial), age, grade], &["id"]); + let cmd = create_table( + "T", + vec![ColumnSpec::new("id", Type::Serial), age, grade], + &["id"], + ); let sql = command_to_sql(&cmd).expect("echo"); assert_eq!( sql, @@ -625,11 +661,11 @@ mod tests { check: None, }; let sql = command_to_sql(&cmd).expect("echo"); - assert_eq!(sql, "ALTER TABLE T ADD COLUMN note text NOT NULL DEFAULT 'n/a'"); - assert!(matches!( - reparse(&sql), - Ok(Command::SqlAlterTable { .. }) - )); + assert_eq!( + sql, + "ALTER TABLE T ADD COLUMN note text NOT NULL DEFAULT 'n/a'" + ); + assert!(matches!(reparse(&sql), Ok(Command::SqlAlterTable { .. }))); } #[test] @@ -657,7 +693,10 @@ mod tests { })), }; let sql = command_to_sql(&cmd).expect("echo"); - assert_eq!(sql, "ALTER TABLE T ADD COLUMN score int UNIQUE CHECK (score >= 0)"); + assert_eq!( + sql, + "ALTER TABLE T ADD COLUMN score int UNIQUE CHECK (score >= 0)" + ); assert!(matches!(reparse(&sql), Ok(Command::SqlAlterTable { .. }))); } @@ -1031,7 +1070,10 @@ mod tests { let lines = render_drop_column_cascade( "Orders", "CustId", - &["Orders_CustId_idx".to_string(), "Orders_CustId_Day_idx".to_string()], + &[ + "Orders_CustId_idx".to_string(), + "Orders_CustId_Day_idx".to_string(), + ], ); assert_eq!( lines.as_slice(), @@ -1043,9 +1085,18 @@ mod tests { ); // Each line is itself runnable advanced-mode SQL (the §1 contract // holds per line for category 2). - assert!(matches!(reparse(&lines[0]), Ok(Command::SqlDropIndex { .. }))); - assert!(matches!(reparse(&lines[1]), Ok(Command::SqlDropIndex { .. }))); - assert!(matches!(reparse(&lines[2]), Ok(Command::SqlAlterTable { .. }))); + assert!(matches!( + reparse(&lines[0]), + Ok(Command::SqlDropIndex { .. }) + )); + assert!(matches!( + reparse(&lines[1]), + Ok(Command::SqlDropIndex { .. }) + )); + assert!(matches!( + reparse(&lines[2]), + Ok(Command::SqlAlterTable { .. }) + )); } #[test] @@ -1054,7 +1105,10 @@ mod tests { // plain `DROP COLUMN` — still semantically equivalent. let lines = render_drop_column_cascade("T", "c", &[]); assert_eq!(lines.as_slice(), &["ALTER TABLE T DROP COLUMN c"]); - assert!(matches!(reparse(&lines[0]), Ok(Command::SqlAlterTable { .. }))); + assert!(matches!( + reparse(&lines[0]), + Ok(Command::SqlAlterTable { .. }) + )); } #[test] @@ -1078,8 +1132,14 @@ mod tests { "ALTER TABLE Orders ADD CONSTRAINT Customers_id_to_Orders_CustId FOREIGN KEY (CustId) REFERENCES Customers (id) ON DELETE CASCADE", ] ); - assert!(matches!(reparse(&lines[0]), Ok(Command::SqlAlterTable { .. }))); - assert!(matches!(reparse(&lines[1]), Ok(Command::SqlAlterTable { .. }))); + assert!(matches!( + reparse(&lines[0]), + Ok(Command::SqlAlterTable { .. }) + )); + assert!(matches!( + reparse(&lines[1]), + Ok(Command::SqlAlterTable { .. }) + )); } #[test] @@ -1116,8 +1176,16 @@ mod tests { ], &["Students_id".to_string(), "Courses_id".to_string()], &[ - (vec!["Students_id".to_string()], "Students".to_string(), vec!["id".to_string()]), - (vec!["Courses_id".to_string()], "Courses".to_string(), vec!["id".to_string()]), + ( + vec!["Students_id".to_string()], + "Students".to_string(), + vec!["id".to_string()], + ), + ( + vec!["Courses_id".to_string()], + "Courses".to_string(), + vec!["id".to_string()], + ), ], ); assert_eq!( @@ -1172,8 +1240,14 @@ mod tests { #[test] fn value_literal_renders_null_uppercase_and_quotes_text() { assert_eq!(value_to_sql_literal(&Value::Null), "NULL"); - assert_eq!(value_to_sql_literal(&Value::Text("O'Hara".to_string())), "'O''Hara'"); - assert_eq!(value_to_sql_literal(&Value::Number("3.14".to_string())), "3.14"); + assert_eq!( + value_to_sql_literal(&Value::Text("O'Hara".to_string())), + "'O''Hara'" + ); + assert_eq!( + value_to_sql_literal(&Value::Number("3.14".to_string())), + "3.14" + ); assert_eq!(value_to_sql_literal(&Value::Bool(false)), "false"); } @@ -1258,9 +1332,7 @@ mod tests { "Command::App({app:?}) is Bucket C — no echo" ); // Also confirm echo_for gates the same in advanced mode. - assert!( - echo_for(&Command::App(app), EffectiveMode::AdvancedPersistent).is_none(), - ); + assert!(echo_for(&Command::App(app), EffectiveMode::AdvancedPersistent).is_none(),); } } diff --git a/src/event.rs b/src/event.rs index b71b850..d30b360 100644 --- a/src/event.rs +++ b/src/event.rs @@ -8,9 +8,8 @@ use crossterm::event::KeyEvent; use crate::db::{ - AddColumnResult, ChangeColumnTypeResult, DataResult, DbError, DeleteResult, - DropColumnResult, InsertResult, QueryPlan, RelationshipDiagramData, TableDescription, - UpdateResult, + AddColumnResult, ChangeColumnTypeResult, DataResult, DbError, DeleteResult, DropColumnResult, + InsertResult, QueryPlan, RelationshipDiagramData, TableDescription, UpdateResult, }; use crate::dsl::Command; @@ -73,10 +72,16 @@ pub enum AppEvent { }, /// An `explain …` command succeeded (ADR-0028). `plan` /// carries the captured query plan; nothing was executed. - DslExplainSucceeded { command: Command, plan: QueryPlan }, + DslExplainSucceeded { + command: Command, + plan: QueryPlan, + }, /// A `show ` list command (V5) — carries pre-formatted /// display lines (tables / relationships / indexes). - DslShowListSucceeded { command: Command, lines: Vec }, + DslShowListSucceeded { + command: Command, + lines: Vec, + }, /// `show relationship ` (ADR-0044) — structured data for the /// diagram, rendered App-side; `None` when no such relationship. DslShowRelationshipSucceeded { diff --git a/src/friendly/format.rs b/src/friendly/format.rs index 7e7192b..404166d 100644 --- a/src/friendly/format.rs +++ b/src/friendly/format.rs @@ -43,17 +43,11 @@ impl Catalog { } } -fn flatten( - value: &serde_norway::Value, - prefix: String, - out: &mut HashMap, -) { +fn flatten(value: &serde_norway::Value, prefix: String, out: &mut HashMap) { match value { serde_norway::Value::Mapping(map) => { for (k, v) in map { - let k_str = k - .as_str() - .expect("catalog keys must be strings"); + let k_str = k.as_str().expect("catalog keys must be strings"); let next = if prefix.is_empty() { k_str.to_string() } else { @@ -85,9 +79,7 @@ pub fn catalog() -> &'static Catalog { /// See module docs for failure modes. pub fn translate(key: &str, args: &[(&str, &dyn Display)]) -> String { let template = catalog().get(key).unwrap_or_else(|| { - panic!( - "missing catalog key: `{key}` (the validator should have caught this)" - ); + panic!("missing catalog key: `{key}` (the validator should have caught this)"); }); substitute(template, args, key) } diff --git a/src/friendly/keys.rs b/src/friendly/keys.rs index bd40341..21af9f3 100644 --- a/src/friendly/keys.rs +++ b/src/friendly/keys.rs @@ -41,8 +41,14 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("diagnostic.alias_used_as_column", &["name"]), ("diagnostic.ambiguous_column", &["column", "qualifiers"]), ("diagnostic.auto_column_overridden", &["column", "type"]), - ("diagnostic.compound_arity_mismatch", &["op", "left_n", "right_n"]), - ("diagnostic.cte_arity_mismatch", &["cte", "declared", "actual"]), + ( + "diagnostic.compound_arity_mismatch", + &["op", "left_n", "right_n"], + ), + ( + "diagnostic.cte_arity_mismatch", + &["cte", "declared", "actual"], + ), ("diagnostic.duplicate_cte", &["name"]), ("diagnostic.eq_null", &[]), ("diagnostic.insert_arity_mismatch", &["expected", "actual"]), @@ -63,7 +69,10 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ), ("diagnostic.not_null_missing", &["column"]), ("diagnostic.like_numeric", &["column", "type"]), - ("diagnostic.projection_alias_misplaced", &["alias", "clause"]), + ( + "diagnostic.projection_alias_misplaced", + &["alias", "clause"], + ), ("diagnostic.table_used_as_column", &["name"]), ("diagnostic.type_mismatch", &["column", "type"]), ("diagnostic.unknown_column", &["name", "table"]), @@ -149,10 +158,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ "error.type_mismatch.change_column.headline", &["table", "column", "src_type", "target_type"], ), - ( - "error.type_mismatch.change_column.hint", - &["target_type"], - ), + ("error.type_mismatch.change_column.hint", &["target_type"]), ( "error.type_mismatch.insert.headline", &["value", "expected_type"], @@ -219,10 +225,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("help.data.explain", &[]), // ---- Hint panel ambient typing assistance (ADR-0022 §6) ---- ("hint.ambient_complete", &[]), - ( - "hint.ambient_error_with_usage", - &["message", "usage"], - ), + ("hint.ambient_error_with_usage", &["message", "usage"]), ("hint.ambient_expected", &["expected"]), ("hint.getting_started", &[]), ("hint.block.heading", &[]), @@ -404,10 +407,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("hint.cmd.explain_sql.what", &[]), ("hint.cmd.explain_sql.example", &[]), ("hint.cmd.explain_sql.concept", &[]), - ( - "hint.ambient_invalid_ident", - &["kind", "found"], - ), + ("hint.ambient_invalid_ident", &["kind", "found"]), ("hint.ambient_typing_name", &[]), // Issue #4: introduce the advanced-mode CREATE TABLE element // slot (`create table T (`) so the otherwise-invisible @@ -415,10 +415,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("hint.create_table_element", &[]), ("hint.seed_count", &[]), ("hint.value_literal_slot", &[]), - ( - "hint.ambient_typing_name_then", - &["next"], - ), + ("hint.ambient_typing_name_then", &["next"]), // Per-column-type value-slot hints (ADR-0024 §Phase D). ("hint.value_slot_blob", &[]), ("hint.value_slot_bool", &[]), @@ -441,7 +438,10 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("parse.custom.alter_named_unique", &[]), ("parse.custom.bind_type_mismatch", &["found", "expected"]), ("parse.custom.change_column_flags_exclusive", &[]), - ("parse.custom.constraint_redundant_on_pk", &["column", "constraint"]), + ( + "parse.custom.constraint_redundant_on_pk", + &["column", "constraint"], + ), ("parse.custom.create_table_needs_pk", &[]), ("parse.custom.expression_too_deep", &[]), ("parse.custom.insert_form_a_missing_values", &["columns"]), @@ -576,10 +576,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ &["table", "col_count", "col_list", "supplied", "non_auto_csv"], ), ("select.internal_table", &["table"]), - ( - "cli.invalid_value", - &["flag", "value", "expected"], - ), + ("cli.invalid_value", &["flag", "value", "expected"]), ("cli.missing_value", &["flag"]), ("cli.multiple_paths", &["first", "second"]), ("cli.resume_with_path", &[]), @@ -867,8 +864,7 @@ mod tests { } } - let declared: HashSet<&str> = - KEYS_AND_PLACEHOLDERS.iter().map(|(k, _)| *k).collect(); + let declared: HashSet<&str> = KEYS_AND_PLACEHOLDERS.iter().map(|(k, _)| *k).collect(); for key in cat.keys() { if key.starts_with("_test.") { continue; @@ -890,9 +886,8 @@ mod tests { /// Mirror of `tests/engine_vocabulary_audit.rs::FORBIDDEN`, /// duplicated here so the catalog validator is self-contained /// (no dependency on the integration-test binary). - const FORBIDDEN_ENGINE_VOCABULARY: &[&str] = &[ - "SQLite", "sqlite", "rusqlite", "STRICT", "PRAGMA", - ]; + const FORBIDDEN_ENGINE_VOCABULARY: &[&str] = + &["SQLite", "sqlite", "rusqlite", "STRICT", "PRAGMA"]; /// Detect a `{name:...}` format-specifier placeholder. /// Doubled braces `{{` / `}}` are escapes — must skip them. diff --git a/src/friendly/mod.rs b/src/friendly/mod.rs index 4c571f6..5cfa58f 100644 --- a/src/friendly/mod.rs +++ b/src/friendly/mod.rs @@ -34,8 +34,8 @@ pub mod keys; pub mod translate; pub use error::{DiagnosticTable, FriendlyError}; -pub use format::{catalog, Catalog}; -pub use translate::{error_hint_class, FailureContext, Operation, TranslateContext, Verbosity}; +pub use format::{Catalog, catalog}; +pub use translate::{FailureContext, Operation, TranslateContext, Verbosity, error_hint_class}; // `translate::translate` and `format::translate` are different // callables — the former is the structured DbError → FriendlyError diff --git a/src/friendly/translate.rs b/src/friendly/translate.rs index aecf487..b52dc7c 100644 --- a/src/friendly/translate.rs +++ b/src/friendly/translate.rs @@ -201,11 +201,7 @@ impl TranslateContext { /// Combine schema-resolved facts with operation and /// verbosity to build the full translator input. #[must_use] - pub fn from_facts( - operation: Operation, - verbosity: Verbosity, - facts: FailureContext, - ) -> Self { + pub fn from_facts(operation: Operation, verbosity: Verbosity, facts: FailureContext) -> Self { Self { operation: Some(operation), table: facts.table, @@ -234,15 +230,15 @@ pub fn translate(error: &DbError, ctx: &TranslateContext) -> FriendlyError { // refusal sites). Catalog entries exist for the typed // invalid-value cases but the migration sweep // (ADR-0019 §9) is what wires them. For now, passthrough. - DbError::Unsupported(message) | DbError::InvalidValue(message) => { - passthrough(message) - } + DbError::Unsupported(message) | DbError::InvalidValue(message) => passthrough(message), DbError::PersistenceFatal { message, .. } - | DbError::RebuildRowFailed { detail: message, .. } + | DbError::RebuildRowFailed { + detail: message, .. + } | DbError::Io(message) => passthrough(message), - DbError::WorkerGone => passthrough( - "the database worker is no longer available — the application must restart", - ), + DbError::WorkerGone => { + passthrough("the database worker is no longer available — the application must restart") + } }; // Attach the row pinpoint when the runtime resolved one. // The translator never builds the table itself — it only @@ -320,11 +316,7 @@ const fn fk_hint_class(ctx: &TranslateContext) -> &'static str { } } -fn translate_sqlite( - message: &str, - kind: SqliteErrorKind, - ctx: &TranslateContext, -) -> FriendlyError { +fn translate_sqlite(message: &str, kind: SqliteErrorKind, ctx: &TranslateContext) -> FriendlyError { // `change column ... --dont-convert` lets the engine // accept or refuse each cell. Whatever the engine returns // (constraint, datatype mismatch, …) means "the new type @@ -392,8 +384,8 @@ fn translate_constraint(message: &str, ctx: &TranslateContext) -> FriendlyError // ---- UNIQUE ----------------------------------------------------- fn translate_unique(message: &str, ctx: &TranslateContext) -> FriendlyError { - let (table, column) = parse_qualified_target(message) - .unwrap_or_else(|| (ctx_table(ctx), ctx_column(ctx))); + let (table, column) = + parse_qualified_target(message).unwrap_or_else(|| (ctx_table(ctx), ctx_column(ctx))); let value = ctx_value(ctx); match ctx.operation { Some(Operation::Update) => fe( @@ -405,11 +397,7 @@ fn translate_unique(message: &str, ctx: &TranslateContext) -> FriendlyError { ), verbose_hint( ctx, - t!( - "error.unique.update.hint", - table = table, - column = column - ), + t!("error.unique.update.hint", table = table, column = column), ), ), // Default to the INSERT variant — it's the most common @@ -425,11 +413,7 @@ fn translate_unique(message: &str, ctx: &TranslateContext) -> FriendlyError { ), verbose_hint( ctx, - t!( - "error.unique.insert.hint", - table = table, - column = column - ), + t!("error.unique.insert.hint", table = table, column = column), ), ), } @@ -542,8 +526,8 @@ fn fk_parent_side_update(ctx: &TranslateContext) -> FriendlyError { // ---- NOT NULL -------------------------------------------------- fn translate_not_null(message: &str, ctx: &TranslateContext) -> FriendlyError { - let (table, column) = parse_qualified_target(message) - .unwrap_or_else(|| (ctx_table(ctx), ctx_column(ctx))); + let (table, column) = + parse_qualified_target(message).unwrap_or_else(|| (ctx_table(ctx), ctx_column(ctx))); match ctx.operation { Some(Operation::Update) => fe( t!( @@ -576,9 +560,17 @@ fn translate_check(_message: &str, ctx: &TranslateContext) -> FriendlyError { let column = ctx_column(ctx); let is_update = matches!(ctx.operation, Some(Operation::Update)); let headline = if is_update { - t!("error.check.update.headline", table = table, column = column) + t!( + "error.check.update.headline", + table = table, + column = column + ) } else { - t!("error.check.insert.headline", table = table, column = column) + t!( + "error.check.insert.headline", + table = table, + column = column + ) }; let hint = ctx.check_rule.as_ref().map_or_else( || { @@ -613,8 +605,7 @@ fn translate_check(_message: &str, ctx: &TranslateContext) -> FriendlyError { // ---- not_found / already_exists -------------------------------- fn translate_not_found_table(message: &str, ctx: &TranslateContext) -> FriendlyError { - let name = parse_after_colon(message) - .map_or_else(|| ctx_table(ctx), str::to_string); + let name = parse_after_colon(message).map_or_else(|| ctx_table(ctx), str::to_string); headline_only(t!("error.not_found.table.headline", name = name)) } @@ -656,17 +647,11 @@ fn translate_already_exists(message: &str, ctx: &TranslateContext) -> FriendlyEr column = column )); } - return headline_only(t!( - "error.already_exists.table.headline", - name = name - )); + return headline_only(t!("error.already_exists.table.headline", name = name)); } // No backticks — engine-style "table T already exists". if let Some(name) = parse_after_word(message, "table") { - return headline_only(t!( - "error.already_exists.table.headline", - name = name - )); + return headline_only(t!("error.already_exists.table.headline", name = name)); } if let Some(name) = parse_after_word(message, "relationship") { return headline_only(t!( @@ -696,36 +681,25 @@ fn translate_generic(message: &str, ctx: &TranslateContext) -> FriendlyError { if lower.contains("misuse of aggregate") { return headline_only(t!("engine.aggregate_misuse", name = "?")); } - if lower.contains("group by") - || lower.contains("must appear in") - { + if lower.contains("group by") || lower.contains("must appear in") { return headline_only(t!("engine.group_by_required")); } - if (lower.contains("union") - || lower.contains("intersect") - || lower.contains("except")) + if (lower.contains("union") || lower.contains("intersect") || lower.contains("except")) && lower.contains("result columns") { // Last-resort safety net — the pre-flight pass in 2d.1 // catches this in most cases; if the engine surfaces it // anyway, route it through the engine-neutral key. - return headline_only(t!( - "engine.compound_arity_mismatch", - op = "set operator" - )); + return headline_only(t!("engine.compound_arity_mismatch", op = "set operator")); } if lower.contains("scalar subquery") || lower.contains("more than one row") { return headline_only(t!("engine.scalar_subquery_too_many_rows")); } - if lower.contains("recursive") - && (lower.contains("cte") || lower.contains("union")) - { + if lower.contains("recursive") && (lower.contains("cte") || lower.contains("union")) { return headline_only(t!("engine.recursive_cte_malformed")); } - let operation = ctx - .operation - .map_or("operation", Operation::keyword); + let operation = ctx.operation.map_or("operation", Operation::keyword); // F2 (ADR-0035 Amendment 1): when no table is in context, use the // table-less hint so a contextless `friendly_message()` (replay, undo, // rebuild, export) never renders a literal `{table}` placeholder. @@ -789,23 +763,33 @@ fn ctx_table(ctx: &TranslateContext) -> String { } fn ctx_column(ctx: &TranslateContext) -> String { - ctx.column.clone().unwrap_or_else(|| "the column".to_string()) + ctx.column + .clone() + .unwrap_or_else(|| "the column".to_string()) } fn ctx_value(ctx: &TranslateContext) -> String { - ctx.value.clone().unwrap_or_else(|| "that value".to_string()) + ctx.value + .clone() + .unwrap_or_else(|| "that value".to_string()) } fn ctx_parent_table(ctx: &TranslateContext) -> String { - ctx.parent_table.clone().unwrap_or_else(|| "the referenced table".to_string()) + ctx.parent_table + .clone() + .unwrap_or_else(|| "the referenced table".to_string()) } fn ctx_parent_column(ctx: &TranslateContext) -> String { - ctx.parent_column.clone().unwrap_or_else(|| "the referenced column".to_string()) + ctx.parent_column + .clone() + .unwrap_or_else(|| "the referenced column".to_string()) } fn ctx_child_table(ctx: &TranslateContext) -> String { - ctx.child_table.clone().unwrap_or_else(|| "the referencing table".to_string()) + ctx.child_table + .clone() + .unwrap_or_else(|| "the referencing table".to_string()) } /// Extract `T.col` from a message like @@ -847,11 +831,7 @@ fn parse_after_word<'a>(message: &'a str, keyword: &str) -> Option<&'a str> { let rest = message[pos..].trim_start(); let token_end = rest.find(|c: char| c.is_whitespace()).unwrap_or(rest.len()); let token = rest[..token_end].trim_matches(|c: char| c == '`' || c == '\''); - if token.is_empty() { - None - } else { - Some(token) - } + if token.is_empty() { None } else { Some(token) } } #[cfg(test)] @@ -876,15 +856,24 @@ mod tests { }; let d = TranslateContext::default; assert_eq!( - error_hint_class(&sqlite(SqliteErrorKind::NoSuchTable, "no such table: X"), &d()), + error_hint_class( + &sqlite(SqliteErrorKind::NoSuchTable, "no such table: X"), + &d() + ), Some("not_found") ); assert_eq!( - error_hint_class(&sqlite(SqliteErrorKind::NoSuchColumn, "no such column: X"), &d()), + error_hint_class( + &sqlite(SqliteErrorKind::NoSuchColumn, "no such column: X"), + &d() + ), Some("not_found") ); assert_eq!( - error_hint_class(&sqlite(SqliteErrorKind::AlreadyExists, "already exists"), &d()), + error_hint_class( + &sqlite(SqliteErrorKind::AlreadyExists, "already exists"), + &d() + ), Some("already_exists") ); assert_eq!( @@ -933,13 +922,19 @@ mod tests { parent_table: Some("Parent".to_string()), ..TranslateContext::default() }; - assert_eq!(error_hint_class(&fk(), &ctx), Some("foreign_key.child_side")); + assert_eq!( + error_hint_class(&fk(), &ctx), + Some("foreign_key.child_side") + ); // child_table populated → parent-side. let ctx = TranslateContext { child_table: Some("Child".to_string()), ..TranslateContext::default() }; - assert_eq!(error_hint_class(&fk(), &ctx), Some("foreign_key.parent_side")); + assert_eq!( + error_hint_class(&fk(), &ctx), + Some("foreign_key.parent_side") + ); // No enrichment: operation is the tiebreaker. assert_eq!( error_hint_class(&fk(), &ctx_with(Operation::Delete)), @@ -1049,14 +1044,22 @@ mod tests { ctx.parent_column = Some("country, code".to_string()); ctx.value = Some("7, 8".to_string()); let f = translate(&err, &ctx); - assert!(f.headline.contains("no parent row"), "child-side: {}", f.headline); + assert!( + f.headline.contains("no parent row"), + "child-side: {}", + f.headline + ); assert!(f.headline.contains("Region")); assert!( f.headline.contains("country, code"), "both parent columns must appear: {}", f.headline ); - assert!(f.headline.contains("`7, 8`"), "joined value: {}", f.headline); + assert!( + f.headline.contains("`7, 8`"), + "joined value: {}", + f.headline + ); } #[test] diff --git a/src/input_render.rs b/src/input_render.rs index 01af66b..abd8ee3 100644 --- a/src/input_render.rs +++ b/src/input_render.rs @@ -25,9 +25,9 @@ use ratatui::style::{Color, Modifier, Style}; use crate::dsl::parser::{parse_command_with_schema, parse_command_with_schema_in_mode}; -use crate::mode::Mode; use crate::dsl::walker; use crate::dsl::{ParseError, parse_command}; +use crate::mode::Mode; use crate::theme::Theme; /// A run of text with its byte range in the source and the @@ -85,7 +85,16 @@ pub fn render_input_runs_in_mode( mode: Mode, ) -> Vec { // Identity feedback view — highlight/overlay the whole input. - render_input_runs_feedback(input, cursor_byte, theme, cache, mode, input, cursor_byte, 0) + render_input_runs_feedback( + input, + cursor_byte, + theme, + cache, + mode, + input, + cursor_byte, + 0, + ) } /// [`render_input_runs_in_mode`] with a separate **feedback view** for @@ -121,12 +130,14 @@ pub fn render_input_runs_feedback( byte_range: (0, offset), style: ratatui::style::Style::default().fg(theme.fg), }]; - r.extend(lex_to_runs_in_mode(view, theme, mode).into_iter().map(|run| { - StyledRun { - byte_range: (run.byte_range.0 + offset, run.byte_range.1 + offset), - ..run - } - })); + r.extend( + lex_to_runs_in_mode(view, theme, mode) + .into_iter() + .map(|run| StyledRun { + byte_range: (run.byte_range.0 + offset, run.byte_range.1 + offset), + ..run + }), + ); r }; if let InputState::DefiniteErrorAt(pos) = @@ -150,7 +161,11 @@ pub fn render_input_runs_feedback( walker::Severity::Error => theme.tok_error, walker::Severity::Warning => theme.warning, }; - overlay_span(&mut runs, (diag.span.0 + offset, diag.span.1 + offset), colour); + overlay_span( + &mut runs, + (diag.span.0 + offset, diag.span.1 + offset), + colour, + ); } inject_cursor(&mut runs, input, cursor_byte, theme); runs @@ -234,9 +249,7 @@ pub fn classify_input_with_schema_in_mode( )) } -fn classify_parse_result( - result: Result, -) -> InputState { +fn classify_parse_result(result: Result) -> InputState { match result { Ok(_) => InputState::Valid, Err(ParseError::Empty) => InputState::Empty, @@ -372,8 +385,7 @@ pub fn advanced_alternative_note( // carries a blocking ERROR diagnostic such as a value-count // mismatch. Incomplete input (still being typed) and empty input are // excluded so the pointer doesn't flicker mid-keystroke. - let definite_dsl_error = match classify_input_with_schema_in_mode(input, cache, Mode::Simple) - { + let definite_dsl_error = match classify_input_with_schema_in_mode(input, cache, Mode::Simple) { InputState::DefiniteErrorAt(_) => true, InputState::Valid => { crate::dsl::walker::input_verdict_in_mode(input, Some(cache), Mode::Simple) @@ -386,8 +398,7 @@ pub fn advanced_alternative_note( } // The validity-verdict-driven gate (ADR-0033 Amendment 5): the // line must be fully valid (verdict `None`) in advanced mode. - if crate::dsl::walker::input_verdict_in_mode(input, Some(cache), Mode::Advanced).is_some() - { + if crate::dsl::walker::input_verdict_in_mode(input, Some(cache), Mode::Advanced).is_some() { return None; } Some(crate::t!("advanced_mode.also_valid_sql")) @@ -714,8 +725,7 @@ fn ambient_hint_core_in_mode( // narrows column candidates to the active table and runs the // §10.6 look-ahead, so it is the authoritative "what can go // here" set. - let completion = - crate::completion::candidates_at_cursor_in_mode(input, cursor, cache, mode); + let completion = crate::completion::candidates_at_cursor_in_mode(input, cursor, cache, mode); // Schema-aware diagnostics (ADR-0027 §2). `input_diagnostics` // is non-empty only for a command that *structurally parses* @@ -834,7 +844,9 @@ fn ambient_hint_core_in_mode( // keyword set. return Some(AmbientHint::Prose(crate::friendly::translate(key, &[]))); } - Some(crate::dsl::grammar::HintMode::SuppressProse | crate::dsl::grammar::HintMode::Default) + Some( + crate::dsl::grammar::HintMode::SuppressProse | crate::dsl::grammar::HintMode::Default, + ) | None => {} } @@ -855,7 +867,8 @@ fn ambient_hint_core_in_mode( // Invalid identifier: cursor sits in a known-set slot but // the typed prefix matches nothing in the schema. (Stage // 8e / the user's #5.) - if let Some(inv) = crate::completion::invalid_ident_at_cursor_in_mode(input, cursor, cache, mode) + if let Some(inv) = + crate::completion::invalid_ident_at_cursor_in_mode(input, cursor, cache, mode) { let kind = match inv.source { crate::dsl::grammar::IdentSource::Tables => "table", @@ -1036,11 +1049,7 @@ pub fn lex_to_runs(input: &str, theme: &Theme) -> Vec { /// with `Mode::Advanced` so SQL keywords past the entry word /// match and get highlighted (ADR-0030 §8). #[must_use] -pub fn lex_to_runs_in_mode( - input: &str, - theme: &Theme, - mode: Mode, -) -> Vec { +pub fn lex_to_runs_in_mode(input: &str, theme: &Theme, mode: Mode) -> Vec { base_runs(input, theme, mode) } @@ -1076,12 +1085,7 @@ fn base_runs(input: &str, theme: &Theme, mode: Mode) -> Vec { runs } -fn inject_cursor( - runs: &mut Vec, - input: &str, - cursor_byte: usize, - theme: &Theme, -) { +fn inject_cursor(runs: &mut Vec, input: &str, cursor_byte: usize, theme: &Theme) { let cursor_byte = cursor_byte.min(input.len()); // End-of-input cursor: append the empty-range sentinel. @@ -1164,9 +1168,10 @@ mod tests { let mut cache = SchemaCache::default(); cache.tables.push("Customers".into()); cache.columns.push("name".into()); - cache - .table_columns - .insert("Customers".into(), vec![TableColumn::new("name", Type::Text)]); + cache.table_columns.insert( + "Customers".into(), + vec![TableColumn::new("name", Type::Text)], + ); let input = ": select name from Customers"; let view = "select name from Customers"; let offset = 2; // ": " @@ -1362,9 +1367,10 @@ mod tests { let mut cache = crate::completion::SchemaCache::default(); cache.tables.push("users".to_string()); cache.columns.push("email".to_string()); - cache - .table_columns - .insert("users".to_string(), vec![TableColumn::new("email", Type::Text)]); + cache.table_columns.insert( + "users".to_string(), + vec![TableColumn::new("email", Type::Text)], + ); cache } @@ -1392,7 +1398,10 @@ mod tests { } // Tab candidates remain available (completion is independent). let comp = crate::completion::candidates_at_cursor_in_mode( - input, input.len(), &cache, Mode::Simple, + input, + input.len(), + &cache, + Mode::Simple, ) .expect("completion remains available"); let texts: Vec<&str> = comp.candidates.iter().map(|c| c.text.as_str()).collect(); @@ -1424,7 +1433,10 @@ mod tests { &hint, Some(AmbientHint::Prose(p)) if p.contains("row count") ); - assert!(!is_count_prose, "count hint must not show for {input:?}; got {hint:?}"); + assert!( + !is_count_prose, + "count hint must not show for {input:?}; got {hint:?}" + ); } } @@ -1502,14 +1514,12 @@ mod tests { use crate::dsl::types::Type; let mut cache = SchemaCache::default(); cache.tables.push("Customers".to_string()); - let tc = vec![ - TableColumn { - name: "Age".to_string(), - user_type: Type::Int, - not_null: false, - has_default: false, - }, - ]; + let tc = vec![TableColumn { + name: "Age".to_string(), + user_type: Type::Int, + not_null: false, + has_default: false, + }]; for c in &tc { cache.columns.push(c.name.clone()); } @@ -1551,9 +1561,7 @@ mod tests { p.contains("No such") && p.contains("Agx"), "a genuine column typo before FROM must warn at typing time; got: {p:?}", ), - other => panic!( - "`select Agx` must surface a typing-time typo hint; got: {other:?}", - ), + other => panic!("`select Agx` must surface a typing-time typo hint; got: {other:?}",), } } @@ -1652,8 +1660,7 @@ mod tests { // ADR-0022 Amendment 1: advanced-mode ambient assistance // surfaces SQL completion candidates (here the FROM-slot // table) instead of the simple-mode "this is SQL" gate. - let cache = - schema_with_columns("Customers", &[("id", crate::dsl::types::Type::Int)]); + let cache = schema_with_columns("Customers", &[("id", crate::dsl::types::Type::Int)]); let input = "select * from "; match ambient_hint_in_mode( input, @@ -1678,10 +1685,7 @@ mod tests { // `INSERT … (` column list. (The simple-mode DSL value-slot // prose is a separate surface; this pins the §8 advanced claim.) use crate::dsl::types::Type; - let cache = schema_with_columns( - "Customers", - &[("id", Type::Int), ("Name", Type::Text)], - ); + let cache = schema_with_columns("Customers", &[("id", Type::Int), ("Name", Type::Text)]); let set_slot = "update Customers set "; match ambient_hint_in_mode(set_slot, set_slot.len(), None, &cache, Mode::Advanced) { @@ -1706,16 +1710,10 @@ mod tests { fn simple_mode_ambient_does_not_surface_sql_candidates() { // The simple-mode entry point keeps gating SQL — advanced // assistance is opt-in via mode, never leaked into simple. - let cache = - schema_with_columns("Customers", &[("id", crate::dsl::types::Type::Int)]); + let cache = schema_with_columns("Customers", &[("id", crate::dsl::types::Type::Int)]); let input = "select * from "; - let hint = ambient_hint_in_mode( - input, - input.len(), - None, - &cache, - crate::mode::Mode::Simple, - ); + let hint = + ambient_hint_in_mode(input, input.len(), None, &cache, crate::mode::Mode::Simple); let offers_table = matches!( &hint, Some(AmbientHint::Candidates { items, .. }) @@ -1733,8 +1731,7 @@ mod tests { fn f1_mid_typed_table_prefix_shows_completion_not_error() { // "select * from c" — `c` prefix-matches `Customers`. The // hint must offer the completion, not "no such table c". - let cache = - schema_with_columns("Customers", &[("id", crate::dsl::types::Type::Int)]); + let cache = schema_with_columns("Customers", &[("id", crate::dsl::types::Type::Int)]); match ambient_hint_in_mode( "select * from c", "select * from c".len(), @@ -1753,8 +1750,7 @@ mod tests { #[test] fn f1_genuinely_unknown_table_still_shows_error() { // "zzz" matches no table prefix — the error must still show. - let cache = - schema_with_columns("Customers", &[("id", crate::dsl::types::Type::Int)]); + let cache = schema_with_columns("Customers", &[("id", crate::dsl::types::Type::Int)]); match ambient_hint_in_mode( "select * from zzz", "select * from zzz".len(), @@ -1773,8 +1769,7 @@ mod tests { fn f1_simple_mode_dsl_mid_typed_table_completes() { // The same shadowing affects DSL commands in simple mode: // "show data c" must offer Customers, not "no such table c". - let cache = - schema_with_columns("Customers", &[("id", crate::dsl::types::Type::Int)]); + let cache = schema_with_columns("Customers", &[("id", crate::dsl::types::Type::Int)]); match ambient_hint_in_mode( "show data c", "show data c".len(), @@ -1804,7 +1799,12 @@ mod tests { cache.columns.push("order_col".to_string()); cache.table_columns.insert( "Orders".to_string(), - vec![TableColumn { name: "order_col".to_string(), user_type: Type::Int, not_null: false, has_default: false }], + vec![TableColumn { + name: "order_col".to_string(), + user_type: Type::Int, + not_null: false, + has_default: false, + }], ); let comp = crate::completion::candidates_at_cursor_in_mode( @@ -1846,9 +1846,7 @@ mod tests { for c in &columns { cache.columns.push(c.name.clone()); } - cache - .table_columns - .insert(table.to_string(), columns); + cache.table_columns.insert(table.to_string(), columns); cache } @@ -1860,7 +1858,11 @@ mod tests { ("Customers", &[("id", Type::Serial), ("name", Type::Text)]), ( "Products", - &[("id", Type::Serial), ("name", Type::Text), ("price", Type::Decimal)], + &[ + ("id", Type::Serial), + ("name", Type::Text), + ("price", Type::Decimal), + ], ), ( "OrderLines", @@ -1873,13 +1875,19 @@ mod tests { ), ( "Orders", - &[("id", Type::Serial), ("customer_id", Type::Int), ("date", Type::Date)], + &[ + ("id", Type::Serial), + ("customer_id", Type::Int), + ("date", Type::Date), + ], ), ]; for (t, cols) in tables { cache.tables.push((*t).to_string()); - let tc: Vec = - cols.iter().map(|(n, ty)| TableColumn::new(*n, *ty)).collect(); + let tc: Vec = cols + .iter() + .map(|(n, ty)| TableColumn::new(*n, *ty)) + .collect(); for c in &tc { cache.columns.push(c.name.clone()); } @@ -1914,17 +1922,11 @@ mod tests { #[test] fn ambient_hint_at_insert_first_value_shows_int_prose() { use crate::dsl::types::Type; - let cache = schema_with_columns( - "Customers", - &[("id", Type::Int), ("Name", Type::Text)], - ); + let cache = schema_with_columns("Customers", &[("id", Type::Int), ("Name", Type::Text)]); let input = "insert into Customers values ("; match ambient_hint(input, input.len(), None, &cache) { Some(AmbientHint::Prose(p)) => { - assert!( - p.contains("integer"), - "expected int-slot prose, got: {p:?}", - ); + assert!(p.contains("integer"), "expected int-slot prose, got: {p:?}",); } other => panic!("expected Prose, got {other:?}"), } @@ -1942,7 +1944,11 @@ mod tests { use crate::dsl::types::Type; let cache = schema_with_columns( "Customers", - &[("id", Type::Serial), ("Name", Type::Text), ("Email", Type::Text)], + &[ + ("id", Type::Serial), + ("Name", Type::Text), + ("Email", Type::Text), + ], ); let input = "insert into Customers values (1, 'Alice', 'a@b.c')"; match ambient_hint(input, input.len(), None, &cache) { @@ -1966,10 +1972,7 @@ mod tests { // A valid simple-mode DSL command gets no advanced pointer — // it isn't an error, and there is nothing to switch modes for. use crate::dsl::types::Type; - let cache = schema_with_columns( - "Customers", - &[("id", Type::Serial), ("Name", Type::Text)], - ); + let cache = schema_with_columns("Customers", &[("id", Type::Serial), ("Name", Type::Text)]); let input = "insert into Customers values ('Alice')"; if let Some(AmbientHint::Prose(p)) = ambient_hint(input, input.len(), None, &cache) { assert!( @@ -2010,10 +2013,7 @@ mod tests { #[test] fn ambient_hint_at_insert_second_value_shows_text_prose() { use crate::dsl::types::Type; - let cache = schema_with_columns( - "Customers", - &[("id", Type::Int), ("Name", Type::Text)], - ); + let cache = schema_with_columns("Customers", &[("id", Type::Int), ("Name", Type::Text)]); let input = "insert into Customers values (1, "; match ambient_hint(input, input.len(), None, &cache) { Some(AmbientHint::Prose(p)) => { @@ -2029,10 +2029,8 @@ mod tests { #[test] fn ambient_hint_at_update_set_shows_per_column_prose() { use crate::dsl::types::Type; - let cache = schema_with_columns( - "Customers", - &[("id", Type::Int), ("Birthday", Type::Date)], - ); + let cache = + schema_with_columns("Customers", &[("id", Type::Int), ("Birthday", Type::Date)]); let input = "update Customers set Birthday="; match ambient_hint(input, input.len(), None, &cache) { Some(AmbientHint::Prose(p)) => { @@ -2057,9 +2055,7 @@ mod tests { // hasn't committed), the Optional propagates Incomplete // and the user sees no error overlay until they submit. assert_eq!( - classify_input( - "insert into Orders (id, CustId, Total) values (42, 89, 17.59" - ), + classify_input("insert into Orders (id, CustId, Total) values (42, 89, 17.59"), InputState::IncompleteAtEof, ); assert_eq!( @@ -2071,18 +2067,12 @@ mod tests { #[test] fn ambient_hint_at_insert_first_value_mentions_column_name() { use crate::dsl::types::Type; - let cache = schema_with_columns( - "Customers", - &[("id", Type::Int), ("Name", Type::Text)], - ); + let cache = schema_with_columns("Customers", &[("id", Type::Int), ("Name", Type::Text)]); let input = "insert into Customers values ("; match ambient_hint(input, input.len(), None, &cache) { Some(AmbientHint::Prose(p)) => { assert!(p.contains("id"), "expected column name `id`, got {p:?}"); - assert!( - p.contains("integer"), - "expected int prose, got {p:?}", - ); + assert!(p.contains("integer"), "expected int prose, got {p:?}",); } other => panic!("expected Prose, got {other:?}"), } @@ -2091,10 +2081,7 @@ mod tests { #[test] fn ambient_hint_at_update_set_mentions_column_name() { use crate::dsl::types::Type; - let cache = schema_with_columns( - "Customers", - &[("id", Type::Int), ("Email", Type::Text)], - ); + let cache = schema_with_columns("Customers", &[("id", Type::Int), ("Email", Type::Text)]); let input = "update Customers set Email="; match ambient_hint(input, input.len(), None, &cache) { Some(AmbientHint::Prose(p)) => { @@ -2131,10 +2118,7 @@ mod tests { #[test] fn ambient_hint_at_second_insert_value_mentions_second_column() { use crate::dsl::types::Type; - let cache = schema_with_columns( - "Customers", - &[("id", Type::Int), ("Name", Type::Text)], - ); + let cache = schema_with_columns("Customers", &[("id", Type::Int), ("Name", Type::Text)]); let input = "insert into Customers values (1, "; match ambient_hint(input, input.len(), None, &cache) { Some(AmbientHint::Prose(p)) => { @@ -2165,14 +2149,20 @@ mod tests { use crate::dsl::types::Type; let cases: &[(&[(&str, Type)], &str)] = &[ // string first value (the report's case): first col text. - (&[("Name", Type::Text), ("Age", Type::Int)], - "insert into Customers values ('Oli'"), + ( + &[("Name", Type::Text), ("Age", Type::Int)], + "insert into Customers values ('Oli'", + ), // integer first value: first col int. - (&[("Age", Type::Int), ("Name", Type::Text)], - "insert into Customers values (42"), + ( + &[("Age", Type::Int), ("Name", Type::Text)], + "insert into Customers values (42", + ), // real first value: first col real. - (&[("Score", Type::Real), ("Name", Type::Text)], - "insert into Customers values (3.5"), + ( + &[("Score", Type::Real), ("Name", Type::Text)], + "insert into Customers values (3.5", + ), ]; for (cols, input) in cases { let cache = schema_with_columns("Customers", cols); @@ -2232,10 +2222,7 @@ mod tests { // is nothing left to fill. Guards against over-correcting the // fix into never suggesting the close paren. use crate::dsl::types::Type; - let cache = schema_with_columns( - "Customers", - &[("Name", Type::Text), ("Age", Type::Int)], - ); + let cache = schema_with_columns("Customers", &[("Name", Type::Text), ("Age", Type::Int)]); let input = "insert into Customers values ('Oli', 52"; match ambient_hint(input, input.len(), None, &cache) { Some(AmbientHint::Prose(p)) => { @@ -2384,10 +2371,7 @@ mod tests { match ambient_hint("show data Missing", 17, None, &cache) { Some(AmbientHint::Prose(p)) => { assert!(p.contains("Missing"), "got {p:?}"); - assert!( - p.to_lowercase().contains("no such table"), - "got {p:?}", - ); + assert!(p.to_lowercase().contains("no such table"), "got {p:?}",); } other => panic!("expected Prose, got {other:?}"), } @@ -2440,8 +2424,7 @@ mod tests { use crate::dsl::types::Type; // Two type-mismatch WARNINGs; the hint names the column // whose offending literal the cursor sits in. - let cache = - schema_with_columns("Events", &[("a", Type::Int), ("b", Type::Int)]); + let cache = schema_with_columns("Events", &[("a", Type::Int), ("b", Type::Int)]); let input = "delete from Events where a = 'x' or b = 'y'"; let on_x = input.find("'x'").expect("'x' literal") + 1; let on_y = input.find("'y'").expect("'y' literal") + 1; @@ -2460,8 +2443,16 @@ mod tests { inserted_range: (5, 5), original_text: String::new(), candidates: vec![ - Candidate { text: "data".to_string(), kind: CandidateKind::Keyword, mode: crate::completion::ModeClass::Both }, - Candidate { text: "table".to_string(), kind: CandidateKind::Keyword, mode: crate::completion::ModeClass::Both }, + Candidate { + text: "data".to_string(), + kind: CandidateKind::Keyword, + mode: crate::completion::ModeClass::Both, + }, + Candidate { + text: "table".to_string(), + kind: CandidateKind::Keyword, + mode: crate::completion::ModeClass::Both, + }, ], selection_idx: 1, }; @@ -2494,8 +2485,16 @@ mod tests { // produce — proves the memo's list is being used, // not a recomputed one. candidates: vec![ - Candidate { text: "data".to_string(), kind: CandidateKind::Keyword, mode: crate::completion::ModeClass::Both }, - Candidate { text: "table".to_string(), kind: CandidateKind::Keyword, mode: crate::completion::ModeClass::Both }, + Candidate { + text: "data".to_string(), + kind: CandidateKind::Keyword, + mode: crate::completion::ModeClass::Both, + }, + Candidate { + text: "table".to_string(), + kind: CandidateKind::Keyword, + mode: crate::completion::ModeClass::Both, + }, ], selection_idx: 1, }; @@ -2564,10 +2563,7 @@ mod tests { fn classify_trailing_whitespace_does_not_create_definite_error() { // Trailing whitespace alone shouldn't promote an // incomplete-at-EOF state into a definite error. - assert_eq!( - classify_input("create "), - InputState::IncompleteAtEof, - ); + assert_eq!(classify_input("create "), InputState::IncompleteAtEof,); } #[test] diff --git a/src/logging.rs b/src/logging.rs index 6183e49..5e6e9c9 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -60,8 +60,8 @@ pub fn init(path: Option<&Path>) -> Result { .with_context(|| format!("create log directory {}", parent.display()))?; } let file = open_log_file(&chosen)?; - let filter = EnvFilter::try_from_env("RDBMS_PLAYGROUND_LOG") - .unwrap_or_else(|_| EnvFilter::new("info")); + let filter = + EnvFilter::try_from_env("RDBMS_PLAYGROUND_LOG").unwrap_or_else(|_| EnvFilter::new("info")); let layer = fmt::layer() .with_writer(file) .with_ansi(false) @@ -95,8 +95,7 @@ fn home_dir() -> Option { if let Some(p) = std::env::var_os("HOME") { return Some(PathBuf::from(p)); } - if let (Some(drive), Some(path)) = - (std::env::var_os("HOMEDRIVE"), std::env::var_os("HOMEPATH")) + if let (Some(drive), Some(path)) = (std::env::var_os("HOMEDRIVE"), std::env::var_os("HOMEPATH")) { let mut combined = PathBuf::from(drive); combined.push(path); diff --git a/src/main.rs b/src/main.rs index af54091..452970e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use std::process::ExitCode; -use rdbms_playground::cli::{help_text, version_text, Args}; +use rdbms_playground::cli::{Args, help_text, version_text}; use rdbms_playground::{logging, runtime}; fn main() -> ExitCode { diff --git a/src/output_render.rs b/src/output_render.rs index ed00a6d..ac2654a 100644 --- a/src/output_render.rs +++ b/src/output_render.rs @@ -172,7 +172,10 @@ fn constraint_lines(desc: &TableDescription) -> Vec { /// A `detail` matching no marker renders neutral — the engine's /// plan vocabulary may grow (ADR-0028 §4). const PLAN_TAXONOMY: &[(&str, OutputStyleClass)] = &[ - ("USING AUTOMATIC COVERING INDEX", OutputStyleClass::AutomaticIndex), + ( + "USING AUTOMATIC COVERING INDEX", + OutputStyleClass::AutomaticIndex, + ), ("USING AUTOMATIC INDEX", OutputStyleClass::AutomaticIndex), ("USING COVERING INDEX", OutputStyleClass::Efficient), ("USING INTEGER PRIMARY KEY", OutputStyleClass::Efficient), @@ -225,8 +228,7 @@ fn render_plan_subtree( emitted: &mut HashSet, mode: Mode, ) { - let children: Vec<&ExplainRow> = - rows.iter().filter(|r| r.parent == parent).collect(); + let children: Vec<&ExplainRow> = rows.iter().filter(|r| r.parent == parent).collect(); let last_idx = children.len().saturating_sub(1); for (idx, row) in children.iter().enumerate() { if !emitted.insert(row.id) { @@ -235,8 +237,7 @@ fn render_plan_subtree( let is_last = idx == last_idx; let connector = if is_last { "└─ " } else { "├─ " }; out.push(plan_node_line(prefix, connector, &row.detail, mode)); - let child_prefix = - format!("{prefix}{}", if is_last { " " } else { "│ " }); + let child_prefix = format!("{prefix}{}", if is_last { " " } else { "│ " }); render_plan_subtree(rows, row.id, &child_prefix, out, emitted, mode); } } @@ -343,13 +344,8 @@ pub fn render_diagnostic_table( const fn alignment_for(ty: Option) -> Alignment { match ty { Some(Type::Int | Type::Real | Type::Decimal | Type::Serial) => Alignment::Right, - Some(Type::Text) - | Some(Type::Bool) - | Some(Type::Date) - | Some(Type::DateTime) - | Some(Type::Blob) - | Some(Type::ShortId) - | None => Alignment::Left, + Some(Type::Text) | Some(Type::Bool) | Some(Type::Date) | Some(Type::DateTime) + | Some(Type::Blob) | Some(Type::ShortId) | None => Alignment::Left, } } @@ -406,11 +402,7 @@ fn cell_width(s: &str) -> usize { /// Render a single bordered table given header cells, body /// rows, and per-column alignment. Outer frame + /// header-underline only. -fn render_table( - headers: &[String], - body: &[Vec], - alignments: &[Alignment], -) -> Vec { +fn render_table(headers: &[String], body: &[Vec], alignments: &[Alignment]) -> Vec { debug_assert_eq!(headers.len(), alignments.len()); // Compute column widths: max(header, all body cells). @@ -792,13 +784,12 @@ fn gutter_seg(i: usize, child_rows: &[usize], parent_rows: &[usize], w: usize) - } // The vertical bus spans the full range of endpoint rows. - let bounds = child_rows - .iter() - .chain(parent_rows) - .copied() - .fold(None, |acc: Option<(usize, usize)>, r| { + let bounds = child_rows.iter().chain(parent_rows).copied().fold( + None, + |acc: Option<(usize, usize)>, r| { Some(acc.map_or((r, r), |(lo, hi)| (lo.min(r), hi.max(r)))) - }); + }, + ); if let Some((top, bot)) = bounds && i >= top && i <= bot @@ -1138,7 +1129,10 @@ mod tests { assert!(out.contains("customer_id ●"), "FK marker:\n{out}"); assert!(out.contains("id (PK) ●"), "parent endpoint marker:\n{out}"); assert!(out.contains('▶'), "arrowhead:\n{out}"); - assert!(out.contains('n') && out.contains('1'), "cardinality:\n{out}"); + assert!( + out.contains('n') && out.contains('1'), + "cardinality:\n{out}" + ); assert!( out.contains("on delete cascade · on update no action"), "actions:\n{out}" @@ -1237,7 +1231,10 @@ mod tests { let (r_out, r_in) = blank_rels(); let region = TableDescription { name: "Region".to_string(), - columns: vec![col("country", Type::Int, true, false), col("code", Type::Int, true, false)], + columns: vec![ + col("country", Type::Int, true, false), + col("code", Type::Int, true, false), + ], outbound_relationships: r_out, inbound_relationships: r_in, indexes: Vec::new(), @@ -1277,7 +1274,10 @@ mod tests { .collect::>() .join("\n"); assert!(text.contains("region_code ●"), "child endpoint 2:\n{text}"); - assert!(text.contains("(PK) ●"), "parent endpoint is PK + marked:\n{text}"); + assert!( + text.contains("(PK) ●"), + "parent endpoint is PK + marked:\n{text}" + ); assert!( text.contains("(country, region_code) ▶ Region.(country, code)"), "pairing line:\n{text}", @@ -1412,11 +1412,7 @@ mod tests { let data = DataResult { table_name: "Customers".to_string(), columns: vec!["id".to_string(), "Name".to_string(), "Email".to_string()], - column_types: vec![ - Some(Type::Serial), - Some(Type::Text), - Some(Type::Text), - ], + column_types: vec![Some(Type::Serial), Some(Type::Text), Some(Type::Text)], rows: vec![ vec![ Some("1".to_string()), @@ -1634,7 +1630,10 @@ mod tests { assert!(out.contains("Indexes:"), "got:\n{out}"); assert!(out.contains("idx_email (Email)"), "got:\n{out}"); // A plain index carries no uniqueness marker. - assert!(!out.contains("[unique]"), "plain index unmarked; got:\n{out}"); + assert!( + !out.contains("[unique]"), + "plain index unmarked; got:\n{out}" + ); } #[test] @@ -1677,7 +1676,10 @@ mod tests { indexes: Vec::new(), unique_constraints: vec![vec!["a".to_string(), "b".to_string()]], check_constraints: vec![ - crate::persistence::TableCheck { name: None, expr: "a < b".to_string() }, + crate::persistence::TableCheck { + name: None, + expr: "a < b".to_string(), + }, crate::persistence::TableCheck { name: Some("a_lt_b".to_string()), expr: "a <> b".to_string(), @@ -1691,7 +1693,10 @@ mod tests { // (ADR-0035 Amendment 1) so the user can `drop constraint `. assert!(out.contains("unique_a_b: unique (a, b)"), "got:\n{out}"); assert!(out.contains("check (a < b)"), "unnamed check; got:\n{out}"); - assert!(out.contains("check a_lt_b (a <> b)"), "named check shows its name; got:\n{out}"); + assert!( + out.contains("check a_lt_b (a <> b)"), + "named check shows its name; got:\n{out}" + ); } #[test] @@ -1732,17 +1737,37 @@ mod tests { let plan = QueryPlan { display_sql: "SELECT 1".to_string(), rows: vec![ - ExplainRow { id: 1, parent: 0, detail: "root".to_string() }, - ExplainRow { id: 2, parent: 1, detail: "child-a".to_string() }, - ExplainRow { id: 3, parent: 1, detail: "child-b".to_string() }, + ExplainRow { + id: 1, + parent: 0, + detail: "root".to_string(), + }, + ExplainRow { + id: 2, + parent: 1, + detail: "child-a".to_string(), + }, + ExplainRow { + id: 3, + parent: 1, + detail: "child-b".to_string(), + }, ], }; let lines = render_explain_plan(&plan, Mode::Simple); // display SQL + 3 plan nodes. assert_eq!(lines.len(), 4); assert!(lines[1].text.contains("root")); - assert!(lines[2].text.contains("├─ child-a"), "got {:?}", lines[2].text); - assert!(lines[3].text.contains("└─ child-b"), "got {:?}", lines[3].text); + assert!( + lines[2].text.contains("├─ child-a"), + "got {:?}", + lines[2].text + ); + assert!( + lines[3].text.contains("└─ child-b"), + "got {:?}", + lines[3].text + ); // The single root uses `└─`; its children are indented // by three spaces (no `│` spine, the root being last). assert!(lines[1].text.starts_with("└─ root")); @@ -1775,7 +1800,10 @@ mod tests { fn render_explain_plan_colours_a_full_scan_expensive() { let plan = one_node_plan("SCAN Customers"); let lines = render_explain_plan(&plan, Mode::Simple); - assert_eq!(span_class_for(&lines[1], "SCAN"), OutputStyleClass::Expensive); + assert_eq!( + span_class_for(&lines[1], "SCAN"), + OutputStyleClass::Expensive + ); // The table name stays neutral (ADR-0028 §6). assert_eq!( span_class_for(&lines[1], "Customers"), @@ -1801,8 +1829,7 @@ mod tests { #[test] fn render_explain_plan_flags_an_automatic_index() { - let plan = - one_node_plan("SEARCH Orders USING AUTOMATIC COVERING INDEX (CustId=?)"); + let plan = one_node_plan("SEARCH Orders USING AUTOMATIC COVERING INDEX (CustId=?)"); let lines = render_explain_plan(&plan, Mode::Simple); assert_eq!( span_class_for(&lines[1], "USING AUTOMATIC COVERING INDEX"), diff --git a/src/persistence/csv_io.rs b/src/persistence/csv_io.rs index e116da5..51ba8a3 100644 --- a/src/persistence/csv_io.rs +++ b/src/persistence/csv_io.rs @@ -150,7 +150,9 @@ fn encode_cell(ty: Type, value: &CellValue) -> Result { other => Err(format!("expected date/datetime (text), got {other:?}")), }, Type::Blob => match value { - CellValue::Blob(bytes) => Ok(Cell::Plain(base64::engine::general_purpose::STANDARD.encode(bytes))), + CellValue::Blob(bytes) => Ok(Cell::Plain( + base64::engine::general_purpose::STANDARD.encode(bytes), + )), other => Err(format!("expected blob, got {other:?}")), }, Type::Serial => match value { @@ -169,7 +171,11 @@ fn format_real(f: f64) -> String { if f.is_nan() { "nan".to_string() } else if f.is_infinite() { - if f > 0.0 { "inf".to_string() } else { "-inf".to_string() } + if f > 0.0 { + "inf".to_string() + } else { + "-inf".to_string() + } } else { // Default `{}` formatting on f64 emits a shortest // round-tripping decimal — exactly what the ADR asks @@ -318,8 +324,7 @@ fn parse_field(bytes: &[u8]) -> Result<(RawCell, usize), CsvError> { _ => i += 1, } } - let content = - String::from_utf8(bytes[..i].to_vec()).map_err(|_| CsvError::InvalidUtf8)?; + let content = String::from_utf8(bytes[..i].to_vec()).map_err(|_| CsvError::InvalidUtf8)?; Ok(( RawCell { content, @@ -435,7 +440,10 @@ mod tests { name: "T".to_string(), columns: vec![col("n", Type::Int), col("r", Type::Real)], rows: vec![ - vec![CellValue::Integer(42), CellValue::Real(std::f64::consts::PI)], + vec![ + CellValue::Integer(42), + CellValue::Real(std::f64::consts::PI), + ], vec![CellValue::Integer(-7), CellValue::Real(0.0)], ], }) @@ -452,10 +460,7 @@ mod tests { let body = serialize_table(&TableSnapshot { name: "T".to_string(), columns: vec![col("b", Type::Bool)], - rows: vec![ - vec![CellValue::Integer(1)], - vec![CellValue::Integer(0)], - ], + rows: vec![vec![CellValue::Integer(1)], vec![CellValue::Integer(0)]], }) .unwrap(); let s = String::from_utf8(body).unwrap(); @@ -555,13 +560,21 @@ mod tests { let body = serialize_table(&table).unwrap(); let parsed = parse_csv(std::str::from_utf8(&body).unwrap()).unwrap(); let row = &parsed.rows[0]; - assert!(matches!(decode_cell(Type::Int, &row[0]).unwrap(), CellValue::Integer(42))); + assert!(matches!( + decode_cell(Type::Int, &row[0]).unwrap(), + CellValue::Integer(42) + )); match decode_cell(Type::Real, &row[1]).unwrap() { CellValue::Real(f) => assert!((f - std::f64::consts::PI).abs() < 1e-12), other => panic!("got {other:?}"), } - assert!(matches!(decode_cell(Type::Bool, &row[2]).unwrap(), CellValue::Integer(1))); - assert!(matches!(decode_cell(Type::Blob, &row[3]).unwrap(), CellValue::Blob(b) if b == b"hi")); + assert!(matches!( + decode_cell(Type::Bool, &row[2]).unwrap(), + CellValue::Integer(1) + )); + assert!( + matches!(decode_cell(Type::Blob, &row[3]).unwrap(), CellValue::Blob(b) if b == b"hi") + ); } #[test] @@ -572,7 +585,10 @@ mod tests { #[test] fn decode_cell_reports_friendly_error_for_bad_int() { - let cell = RawCell { content: "abc".to_string(), was_quoted: false }; + let cell = RawCell { + content: "abc".to_string(), + was_quoted: false, + }; let err = decode_cell(Type::Int, &cell).expect_err("must error"); assert!(err.contains("integer")); assert!(err.contains("abc")); diff --git a/src/persistence/history.rs b/src/persistence/history.rs index 9fff901..b2296a2 100644 --- a/src/persistence/history.rs +++ b/src/persistence/history.rs @@ -108,10 +108,7 @@ pub(super) fn read_recent_sources( }); } }; - let mut sources: Vec = body - .lines() - .filter_map(parse_record_source) - .collect(); + let mut sources: Vec = body.lines().filter_map(parse_record_source).collect(); if sources.len() > max_n { let skip = sources.len() - max_n; sources.drain(0..skip); @@ -187,12 +184,26 @@ fn looks_like_iso8601(s: &str) -> bool { return false; } let digit = |i: usize| b[i].is_ascii_digit(); - digit(0) && digit(1) && digit(2) && digit(3) && b[4] == b'-' - && digit(5) && digit(6) && b[7] == b'-' - && digit(8) && digit(9) && b[10] == b'T' - && digit(11) && digit(12) && b[13] == b':' - && digit(14) && digit(15) && b[16] == b':' - && digit(17) && digit(18) && b[19] == b'Z' + digit(0) + && digit(1) + && digit(2) + && digit(3) + && b[4] == b'-' + && digit(5) + && digit(6) + && b[7] == b'-' + && digit(8) + && digit(9) + && b[10] == b'T' + && digit(11) + && digit(12) + && b[13] == b':' + && digit(14) + && digit(15) + && b[16] == b':' + && digit(17) + && digit(18) + && b[19] == b'Z' } fn unescape_command(s: &str) -> String { @@ -321,10 +332,8 @@ mod tests { #[test] fn parse_journal_record_ok_extracts_unescaped_source() { - let rec = parse_journal_record( - "2026-05-24T10:00:00Z|ok|create table T with pk id(int)", - ) - .expect("valid ok journal record"); + let rec = parse_journal_record("2026-05-24T10:00:00Z|ok|create table T with pk id(int)") + .expect("valid ok journal record"); assert!(rec.status_is_ok); assert_eq!(rec.source, "create table T with pk id(int)"); } @@ -370,8 +379,8 @@ mod tests { fn parse_journal_record_preserves_pipe_in_source() { // `|` is not escaped by the writer (it's a valid SQL char); // `splitn(3, '|')` keeps everything after the second `|`. - let rec = parse_journal_record("2026-05-24T10:00:00Z|ok|select 'a|b' from t") - .expect("ok record"); + let rec = + parse_journal_record("2026-05-24T10:00:00Z|ok|select 'a|b' from t").expect("ok record"); assert_eq!(rec.source, "select 'a|b' from t"); } @@ -406,7 +415,10 @@ mod tests { #[test] fn iso8601_known_seconds() { assert_eq!(iso8601_from_unix_secs(0), "1970-01-01T00:00:00Z"); - assert_eq!(iso8601_from_unix_secs(1_778_112_000), "2026-05-07T00:00:00Z"); + assert_eq!( + iso8601_from_unix_secs(1_778_112_000), + "2026-05-07T00:00:00Z" + ); } #[test] @@ -437,7 +449,10 @@ mod tests { .collect(); std::fs::write(&path, body).unwrap(); let got = read_recent_sources(&path, 3).unwrap(); - assert_eq!(got, vec!["cmd7".to_string(), "cmd8".to_string(), "cmd9".to_string()]); + assert_eq!( + got, + vec!["cmd7".to_string(), "cmd8".to_string(), "cmd9".to_string()] + ); } #[test] diff --git a/src/persistence/migrations.rs b/src/persistence/migrations.rs index 9ea3810..cce9b59 100644 --- a/src/persistence/migrations.rs +++ b/src/persistence/migrations.rs @@ -82,7 +82,10 @@ impl Default for MigratorRegistry { #[derive(Debug)] pub enum MigrateError { VersionParse(String), - NewerThanSupported { file: u32, latest: u32 }, + NewerThanSupported { + file: u32, + latest: u32, + }, NoMigratorForVersion(u32), StepFailed { from: u32, @@ -108,10 +111,9 @@ impl std::fmt::Display for MigrateError { file = file, latest = latest, )), - Self::NoMigratorForVersion(v) => f.write_str(&crate::t!( - "persistence.migrate.no_migrator", - version = v, - )), + Self::NoMigratorForVersion(v) => { + f.write_str(&crate::t!("persistence.migrate.no_migrator", version = v,)) + } Self::StepFailed { from, to, source } => f.write_str(&crate::t!( "persistence.migrate.step_failed", from = from, @@ -192,8 +194,11 @@ pub fn migrate_to_latest( // Write the .bak before any transformation runs so a // mid-migration crash leaves the original recoverable. - let bak_path = - project_path.join(format!("{}.v{}.bak", crate::project::PROJECT_YAML, file_version)); + let bak_path = project_path.join(format!( + "{}.v{}.bak", + crate::project::PROJECT_YAML, + file_version + )); std::fs::write(&bak_path, body).map_err(|source| MigrateError::Io { path: bak_path.clone(), source, @@ -214,8 +219,8 @@ pub fn migrate_to_latest( // Sanity: the new body must declare the next version. // If a migrator forgets to bump, we'd loop endlessly // through the chain — catch it here. - let advertised = read_version(&next_body) - .map_err(|e| MigrateError::BadOutput(e.to_string()))?; + let advertised = + read_version(&next_body).map_err(|e| MigrateError::BadOutput(e.to_string()))?; if advertised != v + 1 { return Err(MigrateError::BadOutput(format!( "v{v}→v{} migrator left version field at {advertised}", @@ -281,9 +286,8 @@ fn read_version(body: &str) -> Result { struct VersionOnly { version: u32, } - let v: VersionOnly = serde_norway::from_str(body).map_err(|e| { - MigrateError::VersionParse(e.to_string()) - })?; + let v: VersionOnly = + serde_norway::from_str(body).map_err(|e| MigrateError::VersionParse(e.to_string()))?; Ok(v.version) } @@ -309,12 +313,8 @@ mod tests { #[test] fn no_migration_runs_when_body_already_latest() { let tmp = tempdir(); - let outcome = migrate_to_latest( - &v1_body(), - &MigratorRegistry::production(), - tmp.path(), - ) - .unwrap(); + let outcome = + migrate_to_latest(&v1_body(), &MigratorRegistry::production(), tmp.path()).unwrap(); assert_eq!(outcome.body, v1_body()); assert_eq!(outcome.migrated_from, None); // No .bak written when nothing migrated. @@ -328,7 +328,13 @@ mod tests { let err = migrate_to_latest(body, &MigratorRegistry::production(), Path::new("/tmp")) .expect_err("must reject"); assert!( - matches!(err, MigrateError::NewerThanSupported { file: 99, latest: 1 }), + matches!( + err, + MigrateError::NewerThanSupported { + file: 99, + latest: 1 + } + ), "got: {err:?}", ); } @@ -366,12 +372,7 @@ mod tests { #[test] fn migrate_runs_chain_and_writes_bak() { let tmp = tempdir(); - let outcome = migrate_to_latest( - &v1_body(), - ®istry_with_v1_to_v2(), - tmp.path(), - ) - .unwrap(); + let outcome = migrate_to_latest(&v1_body(), ®istry_with_v1_to_v2(), tmp.path()).unwrap(); assert_eq!(outcome.migrated_from, Some(1)); assert!(outcome.body.contains("version: 2")); let bak = tmp.path().join("project.yaml.v1.bak"); @@ -396,11 +397,8 @@ mod tests { let tmp = tempdir(); let yaml_path = tmp.path().join("project.yaml"); std::fs::write(&yaml_path, v1_body()).unwrap(); - let outcome = ensure_project_yaml_migrated( - tmp.path(), - &MigratorRegistry::production(), - ) - .unwrap(); + let outcome = + ensure_project_yaml_migrated(tmp.path(), &MigratorRegistry::production()).unwrap(); assert_eq!(outcome.migrated_from, None); // File unchanged. let on_disk = std::fs::read_to_string(&yaml_path).unwrap(); @@ -413,36 +411,32 @@ mod tests { let tmp = tempdir(); let yaml_path = tmp.path().join("project.yaml"); std::fs::write(&yaml_path, v1_body()).unwrap(); - let outcome = ensure_project_yaml_migrated( - tmp.path(), - ®istry_with_v1_to_v2(), - ) - .unwrap(); + let outcome = ensure_project_yaml_migrated(tmp.path(), ®istry_with_v1_to_v2()).unwrap(); assert_eq!(outcome.migrated_from, Some(1)); let on_disk = std::fs::read_to_string(&yaml_path).unwrap(); assert!(on_disk.contains("version: 2"), "got: {on_disk}"); let bak = tmp.path().join("project.yaml.v1.bak"); assert!(bak.exists()); - assert!(std::fs::read_to_string(&bak).unwrap().contains("version: 1")); + assert!( + std::fs::read_to_string(&bak) + .unwrap() + .contains("version: 1") + ); } #[test] fn ensure_yaml_migrated_handles_missing_yaml() { let tmp = tempdir(); // No project.yaml exists. - let outcome = ensure_project_yaml_migrated( - tmp.path(), - &MigratorRegistry::production(), - ) - .unwrap(); + let outcome = + ensure_project_yaml_migrated(tmp.path(), &MigratorRegistry::production()).unwrap(); assert_eq!(outcome.migrated_from, None); assert!(outcome.body.is_empty()); } #[test] fn migrator_that_returns_internal_error_propagates() { - let bad: MigrateFn = - |_| Err(MigrateError::VersionParse("simulated".to_string())); + let bad: MigrateFn = |_| Err(MigrateError::VersionParse("simulated".to_string())); let registry = MigratorRegistry { migrators: vec![bad], }; diff --git a/src/persistence/mod.rs b/src/persistence/mod.rs index 745d3c4..3d5234e 100644 --- a/src/persistence/mod.rs +++ b/src/persistence/mod.rs @@ -368,12 +368,11 @@ impl Persistence { path: data_dir.clone(), source, })?; - let body = - csv_io::serialize_table(table).map_err(|message| PersistenceError::Encode { - kind: "CSV", - path: data_dir.join(format!("{}.csv", table.name)), - message, - })?; + let body = csv_io::serialize_table(table).map_err(|message| PersistenceError::Encode { + kind: "CSV", + path: data_dir.join(format!("{}.csv", table.name)), + message, + })?; atomic_write(&data_dir.join(format!("{}.csv", table.name)), &body) } @@ -406,11 +405,8 @@ impl Persistence { ) -> Result<(), PersistenceError> { let path = self.project_path.join(HISTORY_LOG); let status = history::status_token(history::STATUS_OK, advanced); - let line = history::format_record_with_status( - command_text, - history::utc_iso8601_now(), - &status, - ); + let line = + history::format_record_with_status(command_text, history::utc_iso8601_now(), &status); debug!( len = command_text.len(), advanced, "persist: append ok record to history.log" @@ -432,11 +428,8 @@ impl Persistence { ) -> Result<(), PersistenceError> { let path = self.project_path.join(HISTORY_LOG); let status = history::status_token(history::STATUS_ERR, advanced); - let line = history::format_record_with_status( - command_text, - history::utc_iso8601_now(), - &status, - ); + let line = + history::format_record_with_status(command_text, history::utc_iso8601_now(), &status); debug!( len = command_text.len(), advanced, "persist: append err record to history.log" @@ -531,8 +524,14 @@ mod tests { #[test] fn extension_with_tmp_appends_to_existing_extension() { - assert_eq!(extension_with_tmp(Path::new("a/b/project.yaml")), "yaml.tmp"); - assert_eq!(extension_with_tmp(Path::new("a/b/Customers.csv")), "csv.tmp"); + assert_eq!( + extension_with_tmp(Path::new("a/b/project.yaml")), + "yaml.tmp" + ); + assert_eq!( + extension_with_tmp(Path::new("a/b/Customers.csv")), + "csv.tmp" + ); assert_eq!(extension_with_tmp(Path::new("a/b/lockfile")), "tmp"); } @@ -600,7 +599,8 @@ mod tests { fn append_history_creates_and_appends() { let dir = tempdir(); let p = Persistence::new(dir.path().to_path_buf()); - p.append_history("create table Foo with pk id(serial)", false).unwrap(); + p.append_history("create table Foo with pk id(serial)", false) + .unwrap(); p.append_history("insert into Foo (1)", false).unwrap(); let body = fs::read_to_string(dir.path().join(HISTORY_LOG)).unwrap(); let lines: Vec<&str> = body.trim_end().lines().collect(); diff --git a/src/persistence/yaml.rs b/src/persistence/yaml.rs index 518a288..3a4fcd6 100644 --- a/src/persistence/yaml.rs +++ b/src/persistence/yaml.rs @@ -261,7 +261,10 @@ fn needs_quoting(s: &str) -> bool { } // Scalar text that looks like a YAML keyword needs quoting // even if every character is safe. - if matches!(s, "true" | "false" | "null" | "~" | "yes" | "no" | "on" | "off") { + if matches!( + s, + "true" | "false" | "null" | "~" | "yes" | "no" | "on" | "off" + ) { return true; } s.chars().any(|c| !is_safe_yaml_char(c)) @@ -287,13 +290,14 @@ pub(crate) fn parse_schema(body: &str) -> Result { for t in raw.tables { let mut columns: Vec = Vec::with_capacity(t.columns.len()); for c in t.columns { - let user_type = c.user_type.parse::().map_err(|_| { - YamlError::UnknownType { + let user_type = c + .user_type + .parse::() + .map_err(|_| YamlError::UnknownType { table: t.name.clone(), column: c.name.clone(), raw: c.user_type.clone(), - } - })?; + })?; columns.push(ColumnSchema { name: c.name, user_type, @@ -308,7 +312,11 @@ pub(crate) fn parse_schema(body: &str) -> Result { primary_key: t.primary_key, columns, unique_constraints: t.unique_constraints, - check_constraints: t.check_constraints.into_iter().map(TableCheck::from).collect(), + check_constraints: t + .check_constraints + .into_iter() + .map(TableCheck::from) + .collect(), }); } let mut relationships: Vec = Vec::with_capacity(raw.relationships.len()); @@ -381,10 +389,7 @@ pub(crate) enum YamlError { impl std::fmt::Display for YamlError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Syntax(msg) => f.write_str(&crate::t!( - "persistence.yaml.syntax", - detail = msg, - )), + Self::Syntax(msg) => f.write_str(&crate::t!("persistence.yaml.syntax", detail = msg,)), Self::UnsupportedVersion(v) => f.write_str(&crate::t!( "persistence.yaml.unsupported_version", version = v, @@ -395,10 +400,9 @@ impl std::fmt::Display for YamlError { column = column, raw = raw, )), - Self::UnknownAction(raw) => f.write_str(&crate::t!( - "persistence.yaml.unknown_action", - raw = raw, - )), + Self::UnknownAction(raw) => { + f.write_str(&crate::t!("persistence.yaml.unknown_action", raw = raw,)) + } } } } @@ -545,8 +549,22 @@ mod tests { name: "Customers".to_string(), primary_key: vec!["id".to_string()], columns: vec![ - ColumnSchema { name: "id".to_string(), user_type: Type::Serial, unique: false, not_null: false, default: None, check: None }, - ColumnSchema { name: "Name".to_string(), user_type: Type::Text, unique: false, not_null: false, default: None, check: None }, + ColumnSchema { + name: "id".to_string(), + user_type: Type::Serial, + unique: false, + not_null: false, + default: None, + check: None, + }, + ColumnSchema { + name: "Name".to_string(), + user_type: Type::Text, + unique: false, + not_null: false, + default: None, + check: None, + }, ], unique_constraints: Vec::new(), check_constraints: Vec::new(), @@ -555,8 +573,22 @@ mod tests { name: "Orders".to_string(), primary_key: vec!["id".to_string()], columns: vec![ - ColumnSchema { name: "id".to_string(), user_type: Type::Serial, unique: false, not_null: false, default: None, check: None }, - ColumnSchema { name: "CustId".to_string(), user_type: Type::Int, unique: false, not_null: false, default: None, check: None }, + ColumnSchema { + name: "id".to_string(), + user_type: Type::Serial, + unique: false, + not_null: false, + default: None, + check: None, + }, + ColumnSchema { + name: "CustId".to_string(), + user_type: Type::Int, + unique: false, + not_null: false, + default: None, + check: None, + }, ], unique_constraints: Vec::new(), check_constraints: Vec::new(), @@ -798,15 +830,33 @@ indexes: name: "T".to_string(), primary_key: vec![], columns: vec![ - ColumnSchema { name: "a".to_string(), user_type: Type::Int, unique: false, not_null: false, default: None, check: None }, - ColumnSchema { name: "b".to_string(), user_type: Type::Int, unique: false, not_null: false, default: None, check: None }, - ColumnSchema { name: "c".to_string(), user_type: Type::Int, unique: false, not_null: false, default: None, check: None }, + ColumnSchema { + name: "a".to_string(), + user_type: Type::Int, + unique: false, + not_null: false, + default: None, + check: None, + }, + ColumnSchema { + name: "b".to_string(), + user_type: Type::Int, + unique: false, + not_null: false, + default: None, + check: None, + }, + ColumnSchema { + name: "c".to_string(), + user_type: Type::Int, + unique: false, + not_null: false, + default: None, + check: None, + }, ], unique_constraints: vec![vec!["a".to_string(), "b".to_string()]], - check_constraints: vec![ - TableCheck::unnamed("a < b"), - TableCheck::unnamed("b < c"), - ], + check_constraints: vec![TableCheck::unnamed("a < b"), TableCheck::unnamed("b < c")], }], relationships: vec![], indexes: vec![], @@ -830,12 +880,29 @@ indexes: name: "T".to_string(), primary_key: vec!["id".to_string()], columns: vec![ - ColumnSchema { name: "id".to_string(), user_type: Type::Int, unique: false, not_null: false, default: None, check: None }, - ColumnSchema { name: "qty".to_string(), user_type: Type::Int, unique: false, not_null: false, default: None, check: None }, + ColumnSchema { + name: "id".to_string(), + user_type: Type::Int, + unique: false, + not_null: false, + default: None, + check: None, + }, + ColumnSchema { + name: "qty".to_string(), + user_type: Type::Int, + unique: false, + not_null: false, + default: None, + check: None, + }, ], unique_constraints: vec![], check_constraints: vec![ - TableCheck { name: Some("qty_positive".to_string()), expr: "qty >= 0".to_string() }, + TableCheck { + name: Some("qty_positive".to_string()), + expr: "qty >= 0".to_string(), + }, TableCheck::unnamed("qty < 1000"), ], }], @@ -844,7 +911,10 @@ indexes: }; let body = serialize_schema(&snap); let parsed = parse_schema(&body).expect("parse schema"); - assert_eq!(parsed, snap, "named + unnamed table-CHECKs survive the yaml round-trip"); + assert_eq!( + parsed, snap, + "named + unnamed table-CHECKs survive the yaml round-trip" + ); } #[test] @@ -968,8 +1038,22 @@ relationships: name: "Items".to_string(), primary_key: vec!["a".to_string(), "b".to_string()], columns: vec![ - ColumnSchema { name: "a".to_string(), user_type: Type::Int, unique: false, not_null: false, default: None, check: None }, - ColumnSchema { name: "b".to_string(), user_type: Type::Int, unique: false, not_null: false, default: None, check: None }, + ColumnSchema { + name: "a".to_string(), + user_type: Type::Int, + unique: false, + not_null: false, + default: None, + check: None, + }, + ColumnSchema { + name: "b".to_string(), + user_type: Type::Int, + unique: false, + not_null: false, + default: None, + check: None, + }, ], unique_constraints: Vec::new(), check_constraints: Vec::new(), @@ -1019,12 +1103,10 @@ relationships: let absent = "version: 1\nproject:\n created_at: x\ntables: []\n"; assert_eq!(parse_stored_mode(absent), None); - let explicit_simple = - "version: 1\nproject:\n created_at: x\n mode: simple\ntables: []\n"; + let explicit_simple = "version: 1\nproject:\n created_at: x\n mode: simple\ntables: []\n"; assert_eq!(parse_stored_mode(explicit_simple), Some(Mode::Simple)); - let advanced = - "version: 1\nproject:\n created_at: x\n mode: advanced\ntables: []\n"; + let advanced = "version: 1\nproject:\n created_at: x\n mode: advanced\ntables: []\n"; assert_eq!(parse_stored_mode(advanced), Some(Mode::Advanced)); } diff --git a/src/project/lock.rs b/src/project/lock.rs index 958972c..138a084 100644 --- a/src/project/lock.rs +++ b/src/project/lock.rs @@ -170,7 +170,10 @@ fn local_hostname() -> String { /// Uses `sysinfo` to query the OS process table. fn pid_is_alive(pid: u32) -> bool { let mut sys = System::new(); - sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(pid)]), true); + sys.refresh_processes( + sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(pid)]), + true, + ); sys.process(Pid::from_u32(pid)).is_some() } @@ -211,7 +214,10 @@ mod tests { // The first lock writes our own PID; a second attempt // should refuse because the PID is alive on this host. let err = Lock::acquire(dir.path()).expect_err("should refuse second acquisition"); - assert!(matches!(err, LockError::AlreadyHeld { .. }), "unexpected: {err:?}"); + assert!( + matches!(err, LockError::AlreadyHeld { .. }), + "unexpected: {err:?}" + ); } #[test] diff --git a/src/project/mod.rs b/src/project/mod.rs index 98d5436..d007f6e 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -78,10 +78,7 @@ pub fn read_last_project(data_root: &Path) -> std::io::Result> { /// a moved/deleted directory is the kind of error `--resume` /// is supposed to surface clearly, not paper over by /// resolving symlinks at write time. -pub fn write_last_project( - data_root: &Path, - project_path: &Path, -) -> std::io::Result<()> { +pub fn write_last_project(data_root: &Path, project_path: &Path) -> std::io::Result<()> { fs::create_dir_all(data_root)?; let final_path = data_root.join(LAST_PROJECT_FILE); let tmp_path = data_root.join(format!("{LAST_PROJECT_FILE}.tmp")); @@ -108,9 +105,8 @@ pub fn resolve_data_root(override_dir: Option<&Path>) -> Result) -> std::fmt::Result { match self { - Self::DataRootUnavailable => { - f.write_str(&crate::t!("project.data_root_unavailable")) + Self::DataRootUnavailable => f.write_str(&crate::t!("project.data_root_unavailable")), + Self::PathNotFound(p) => { + f.write_str(&crate::t!("project.path_not_found", path = p.display(),)) + } + Self::NotAProject(p) => { + f.write_str(&crate::t!("project.not_a_project", path = p.display(),)) + } + Self::AlreadyExists(p) => { + f.write_str(&crate::t!("project.already_exists", path = p.display(),)) } - Self::PathNotFound(p) => f.write_str(&crate::t!( - "project.path_not_found", - path = p.display(), - )), - Self::NotAProject(p) => f.write_str(&crate::t!( - "project.not_a_project", - path = p.display(), - )), - Self::AlreadyExists(p) => f.write_str(&crate::t!( - "project.already_exists", - path = p.display(), - )), Self::Io { path, source } => f.write_str(&crate::t!( "project.io", path = path.display(), @@ -609,11 +600,10 @@ pub fn safely_delete_temp_project( // 2. Canonicalize for the containment check. We do this // only after the symlink-at-top check so we can't be // tricked by a top-level symlink. - let project_canon = - fs::canonicalize(project_path).map_err(|source| SafeDeleteError::Io { - path: project_path.to_path_buf(), - source, - })?; + let project_canon = fs::canonicalize(project_path).map_err(|source| SafeDeleteError::Io { + path: project_path.to_path_buf(), + source, + })?; // 3. Containment: canonical path must be inside the // canonical data-root projects dir. @@ -848,7 +838,10 @@ mod tests { assert!(gi.contains("/playground.db")); assert!(gi.contains("/.rdbms-playground.lock")); assert!(gi.contains("/.snapshots/"), "undo ring should be ignored"); - assert!(!gi.contains("history.log"), "history.log should NOT be ignored"); + assert!( + !gi.contains("history.log"), + "history.log should NOT be ignored" + ); } #[test] @@ -890,7 +883,10 @@ mod tests { let target = tmp.path().join("MyProject"); fs::create_dir(&target).unwrap(); let err = Project::create_named(&target).expect_err("must refuse"); - assert!(matches!(err, ProjectError::AlreadyExists(_)), "got: {err:?}"); + assert!( + matches!(err, ProjectError::AlreadyExists(_)), + "got: {err:?}" + ); } #[test] @@ -962,7 +958,10 @@ mod tests { ) .unwrap(); let read_back = read_last_project(tmp.path()).unwrap(); - assert_eq!(read_back, Some(std::path::PathBuf::from("/tmp/some/project"))); + assert_eq!( + read_back, + Some(std::path::PathBuf::from("/tmp/some/project")) + ); } #[test] diff --git a/src/project/naming.rs b/src/project/naming.rs index 1f4e195..53b2eb9 100644 --- a/src/project/naming.rs +++ b/src/project/naming.rs @@ -17,8 +17,8 @@ use std::path::Path; -use rand::seq::IndexedRandom; use rand::Rng; +use rand::seq::IndexedRandom; const WORDLIST: &str = include_str!("wordlist.txt"); const MAX_COLLISION_RETRIES: usize = 100; @@ -41,10 +41,9 @@ pub enum NamingError { impl std::fmt::Display for NamingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::WordlistTooSmall(n) => f.write_str(&crate::t!( - "project.naming.wordlist_too_small", - count = n, - )), + Self::WordlistTooSmall(n) => { + f.write_str(&crate::t!("project.naming.wordlist_too_small", count = n,)) + } Self::TooManyCollisions(n) => f.write_str(&crate::t!( "project.naming.too_many_collisions", attempts = n, @@ -189,10 +188,9 @@ impl std::fmt::Display for UserNameError { match self { Self::Empty => f.write_str(&crate::t!("project.user_name.empty")), Self::LeadingDot => f.write_str(&crate::t!("project.user_name.leading_dot")), - Self::InvalidChar(c) => f.write_str(&crate::t!( - "project.user_name.invalid_char", - ch = c, - )), + Self::InvalidChar(c) => { + f.write_str(&crate::t!("project.user_name.invalid_char", ch = c,)) + } } } } @@ -209,14 +207,22 @@ mod tests { #[test] fn wordlist_has_enough_entries() { let pool = words(); - assert!(pool.len() >= 100, "wordlist suspiciously small: {} entries", pool.len()); + assert!( + pool.len() >= 100, + "wordlist suspiciously small: {} entries", + pool.len() + ); } #[test] fn wordlist_has_no_duplicates() { let pool = words(); let unique: std::collections::HashSet<_> = pool.iter().collect(); - assert_eq!(unique.len(), pool.len(), "wordlist contains duplicate entries"); + assert_eq!( + unique.len(), + pool.len(), + "wordlist contains duplicate entries" + ); } #[test] @@ -290,7 +296,7 @@ mod tests { assert!(!is_temp_dirname("MyOrders")); assert!(!is_temp_dirname("term_planner")); assert!(!is_temp_dirname("20260507-water-buffalo-skating")); // no marker - assert!(!is_temp_dirname("temp-project")); // no brackets + assert!(!is_temp_dirname("temp-project")); // no brackets } #[test] @@ -301,9 +307,18 @@ mod tests { assert!(validate_user_name("project.v2").is_ok()); assert_eq!(validate_user_name(""), Err(UserNameError::Empty)); - assert_eq!(validate_user_name(".hidden"), Err(UserNameError::LeadingDot)); - assert!(matches!(validate_user_name("a/b"), Err(UserNameError::InvalidChar('/')))); - assert!(matches!(validate_user_name("a b"), Err(UserNameError::InvalidChar(' ')))); + assert_eq!( + validate_user_name(".hidden"), + Err(UserNameError::LeadingDot) + ); + assert!(matches!( + validate_user_name("a/b"), + Err(UserNameError::InvalidChar('/')) + )); + assert!(matches!( + validate_user_name("a b"), + Err(UserNameError::InvalidChar(' ')) + )); } fn tempdir() -> tempfile::TempDir { diff --git a/src/project/prettifier.rs b/src/project/prettifier.rs index 31854b5..6f92d23 100644 --- a/src/project/prettifier.rs +++ b/src/project/prettifier.rs @@ -129,7 +129,10 @@ mod tests { #[test] fn strips_date_prefix_from_temp_project_names() { - assert_eq!(prettify("20260507-water-buffalo-skating"), "Water Buffalo Skating"); + assert_eq!( + prettify("20260507-water-buffalo-skating"), + "Water Buffalo Skating" + ); } #[test] @@ -205,6 +208,9 @@ mod tests { #[test] fn handles_mixed_separators_and_case() { - assert_eq!(prettify("MyTeam_lessonPlan-2026"), "My Team Lesson Plan 2026"); + assert_eq!( + prettify("MyTeam_lessonPlan-2026"), + "My Team Lesson Plan 2026" + ); } } diff --git a/src/runtime.rs b/src/runtime.rs index ca17a3f..41d887f 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -36,8 +36,8 @@ use crate::db::{ use crate::dsl::command::{ Constraint, ConstraintKind, IndexSelector, RelationshipSelector, TableConstraint, }; -use crate::dsl::{AlterTableAction, ChangeColumnMode, Command, ColumnSpec}; use crate::dsl::walker::Severity; +use crate::dsl::{AlterTableAction, ChangeColumnMode, ColumnSpec, Command}; use crate::event::AppEvent; use crate::project::{ Project, ProjectKind, copy_project, list_projects, open_or_create, projects_dir, @@ -130,8 +130,7 @@ pub async fn run(args: Args) -> Result<()> { // to it for `new` (creates a temp) and `load` (lists // projects). We can't easily recover this from the // Project alone, so we keep it ourselves. - let data_root = resolve_data_root(args.data_dir.as_deref()) - .context("resolve data root")?; + let data_root = resolve_data_root(args.data_dir.as_deref()).context("resolve data root")?; // Resolve the initial project path: --resume reads it from // /last_project; otherwise an explicit positional @@ -143,17 +142,12 @@ pub async fn run(args: Args) -> Result<()> { // terminal so the message lands directly in the user's // shell. let initial_path: Option = if args.resume { - match read_last_project(&data_root) - .context("read last_project")? - { + match read_last_project(&data_root).context("read last_project")? { Some(p) if p.exists() => Some(p), Some(p) => { eprintln!( "rdbms-playground: {}", - crate::t!( - "project.resume_recorded_missing", - path = p.display(), - ), + crate::t!("project.resume_recorded_missing", path = p.display(),), ); return Ok(()); } @@ -488,19 +482,15 @@ async fn run_loop( // Best-effort — a failure to record a failure must // never escalate a user error into a fatal, so the // result is logged and ignored. - if let Err(e) = crate::persistence::Persistence::new( - session.project().path().to_path_buf(), - ) - .append_history_failure(&source, advanced) + if let Err(e) = + crate::persistence::Persistence::new(session.project().path().to_path_buf()) + .append_history_failure(&source, advanced) { tracing::warn!(error = %e, "failed to journal err record (ignored)"); } } Action::PrepareRebuild => { - spawn_prepare_rebuild( - session.project().path().to_path_buf(), - event_tx.clone(), - ); + spawn_prepare_rebuild(session.project().path().to_path_buf(), event_tx.clone()); } Action::Rebuild { source } => { spawn_rebuild( @@ -671,8 +661,8 @@ async fn run_loop( // mutually exclusive (one needs an unmodified temp, the // other anything else). let project_at_quit = session.project.as_ref(); - let cleanup_on_quit: Option = project_at_quit - .and_then(|p| p.is_unmodified_temp().then(|| p.path().to_path_buf())); + let cleanup_on_quit: Option = + project_at_quit.and_then(|p| p.is_unmodified_temp().then(|| p.path().to_path_buf())); let resume_target_on_quit: Option = project_at_quit .filter(|p| !p.is_unmodified_temp()) .map(|p| p.path().to_path_buf()); @@ -831,7 +821,10 @@ async fn perform_switch( Some(p) } SwitchRequest::NewTemp => None, - SwitchRequest::Import { zip_path, as_target } => { + SwitchRequest::Import { + zip_path, + as_target, + } => { if !zip_path.exists() { return Err(crate::t!( "project.import_zip_missing", @@ -840,8 +833,7 @@ async fn perform_switch( } // Validate the zip up front so we don't drop the // current project for an unimportable file. - let inspection = crate::archive::inspect_zip(zip_path) - .map_err(|e| e.to_string())?; + let inspection = crate::archive::inspect_zip(zip_path).map_err(|e| e.to_string())?; let resolved = resolve_import_destination( as_target.as_deref(), &inspection.top_folder, @@ -856,16 +848,19 @@ async fn perform_switch( // state matches the in-memory db). if let SwitchRequest::SaveAs { .. } = &req { let src = session.project().path().to_path_buf(); - let dst = resolved_target.as_ref().expect("SaveAs has resolved target"); + let dst = resolved_target + .as_ref() + .expect("SaveAs has resolved target"); copy_project(&src, dst).map_err(|e| e.to_string())?; } // For Import: extract the zip into the resolved target. // We do this *before* dropping the current project so // a failure here leaves the user where they were. if let SwitchRequest::Import { zip_path, .. } = &req { - let dst = resolved_target.as_ref().expect("Import has resolved target"); - let inspection = crate::archive::inspect_zip(zip_path) - .map_err(|e| e.to_string())?; + let dst = resolved_target + .as_ref() + .expect("Import has resolved target"); + let inspection = crate::archive::inspect_zip(zip_path).map_err(|e| e.to_string())?; crate::archive::extract_into(zip_path, dst, &inspection.top_folder) .map_err(|e| e.to_string())?; } @@ -874,10 +869,10 @@ async fn perform_switch( // we drop it: if it was an unmodified empty temp, we // delete its directory after the switch so the data dir // doesn't accumulate empty scratch projects. - let outgoing_cleanup_path: Option = - session.project.as_ref().and_then(|p| { - p.is_unmodified_temp().then(|| p.path().to_path_buf()) - }); + let outgoing_cleanup_path: Option = session + .project + .as_ref() + .and_then(|p| p.is_unmodified_temp().then(|| p.path().to_path_buf())); // Drop current project + database BEFORE opening the new // ones, releasing the old lock and stopping the old @@ -954,9 +949,7 @@ async fn perform_switch( let new_database = Database::open_with_persistence_and_undo(&db_path, persistence, undo_enabled) .map_err(|e| e.to_string())?; - if !db_existed - && let Err(e) = new_database.rebuild_from_text(new_path.clone(), None).await - { + if !db_existed && let Err(e) = new_database.rebuild_from_text(new_path.clone(), None).await { return Err(e.friendly_message()); } @@ -982,9 +975,7 @@ async fn perform_switch( // fresh empty temp (a `new` command), which must not be // recorded (see the gate in `run()`). Write failures are // non-fatal. - if new_worth_recording - && let Err(e) = write_last_project(&session.data_root, &new_path) - { + if new_worth_recording && let Err(e) = write_last_project(&session.data_root, &new_path) { tracing::warn!(error = %e, "could not update last_project after switch"); } @@ -1045,8 +1036,8 @@ fn spawn_export( event_tx: mpsc::Sender, ) { // `export` app command: journalled simple (ADR-0052). - let _ = crate::persistence::Persistence::new(project_path.clone()) - .append_history(&source, false); + let _ = + crate::persistence::Persistence::new(project_path.clone()).append_history(&source, false); tokio::spawn(async move { let outcome = tokio::task::spawn_blocking(move || { do_export(&project_path, &project_name, &data_root, target.as_deref()) @@ -1081,9 +1072,8 @@ fn do_export( } None => { std::fs::create_dir_all(data_root).map_err(|e| e.to_string())?; - let (filename, _) = - crate::archive::next_export_sequence(data_root, project_name) - .map_err(|e| e.to_string())?; + let (filename, _) = crate::archive::next_export_sequence(data_root, project_name) + .map_err(|e| e.to_string())?; data_root.join(filename) } }; @@ -1143,10 +1133,7 @@ async fn seed_initial_tables(database: &Database, event_tx: &mpsc::Sender, -) { +async fn refresh_schema_cache(database: &Database, event_tx: &mpsc::Sender) { let cache = build_schema_cache(database).await; let _ = event_tx.send(AppEvent::SchemaCacheRefreshed(cache)).await; // ADR-0046 DB2: full relationship records for the sidebar panel. @@ -1234,10 +1221,7 @@ async fn build_schema_cache(database: &Database) -> crate::completion::SchemaCac /// summary that the confirmation modal shows. Runs off the /// event loop so the brief I/O doesn't stall input handling /// even on slow filesystems. -fn spawn_prepare_rebuild( - project_path: std::path::PathBuf, - event_tx: mpsc::Sender, -) { +fn spawn_prepare_rebuild(project_path: std::path::PathBuf, event_tx: mpsc::Sender) { tokio::spawn(async move { let summary = match summarize_project(&project_path) { Ok(s) => s, @@ -1317,9 +1301,7 @@ fn spawn_rebuild( } let summary = summarize_project(&project_path) .unwrap_or_else(|_| "rebuild complete".to_string()); - let _ = event_tx - .send(AppEvent::RebuildSucceeded { summary }) - .await; + let _ = event_tx.send(AppEvent::RebuildSucceeded { summary }).await; // Refresh the table list so the items panel // reflects whatever the rebuild produced. if let Ok(tables) = database.list_tables().await { @@ -1462,12 +1444,8 @@ fn spawn_dsl_dispatch( } let event = match outcome { Ok(CommandOutcome::Schema(description)) => { - let schema_echo = build_schema_echo( - &command, - submission_mode, - description.as_ref(), - &lookups, - ); + let schema_echo = + build_schema_echo(&command, submission_mode, description.as_ref(), &lookups); AppEvent::DslSucceeded { command: command.clone(), description, @@ -1484,12 +1462,10 @@ fn spawn_dsl_dispatch( Ok(CommandOutcome::SchemaDropIndexSkipped) => AppEvent::DslDropIndexSkipped { command: command.clone(), }, - Ok(CommandOutcome::SchemaCreateIndexSkipped(name)) => { - AppEvent::DslCreateIndexSkipped { - command: command.clone(), - name, - } - } + Ok(CommandOutcome::SchemaCreateIndexSkipped(name)) => AppEvent::DslCreateIndexSkipped { + command: command.clone(), + name, + }, 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 @@ -1507,12 +1483,10 @@ fn spawn_dsl_dispatch( command: command.clone(), lines, }, - Ok(CommandOutcome::ShowRelationship(data)) => { - AppEvent::DslShowRelationshipSucceeded { - command: command.clone(), - data: data.map(|b| *b), - } - } + Ok(CommandOutcome::ShowRelationship(data)) => AppEvent::DslShowRelationshipSucceeded { + command: command.clone(), + data: data.map(|b| *b), + }, Ok(CommandOutcome::QueryPlan(plan)) => AppEvent::DslExplainSucceeded { command: command.clone(), plan, @@ -1568,11 +1542,8 @@ fn spawn_dsl_dispatch( // the covering indexes the rebuild removed — Bucket B // category 2, ADR-0038 §7 Slice 2b). Non-cascade falls // through to the pre-execution `echo` from `echo_for`. - let cascade_echo = build_drop_column_cascade_echo( - &command, - submission_mode, - &result, - ); + let cascade_echo = + build_drop_column_cascade_echo(&command, submission_mode, &result); AppEvent::DslDropColumnSucceeded { command: command.clone(), result, @@ -1931,12 +1902,14 @@ fn build_schema_echo( )]) } } - Command::DropRelationship { .. } => lookups - .drop_relationship - .as_ref() - .map(|(name, child_table)| { - vec![crate::echo::render_drop_relationship(name, child_table)] - }), + Command::DropRelationship { .. } => { + lookups + .drop_relationship + .as_ref() + .map(|(name, child_table)| { + vec![crate::echo::render_drop_relationship(name, child_table)] + }) + } // `create m:n relationship` (ADR-0045): the resolved junction // columns/FKs only exist on the post-exec description, so the // teaching echo is rendered from it (not `command_to_sql`). @@ -1946,14 +1919,29 @@ fn build_schema_echo( .iter() .filter_map(|c| c.user_type.map(|ty| (c.name.clone(), ty))) .collect(); - let primary_key: Vec = - desc.columns.iter().filter(|c| c.primary_key).map(|c| c.name.clone()).collect(); + let primary_key: Vec = desc + .columns + .iter() + .filter(|c| c.primary_key) + .map(|c| c.name.clone()) + .collect(); let foreign_keys: Vec<(Vec, String, Vec)> = desc .outbound_relationships .iter() - .map(|r| (r.local_columns.clone(), r.other_table.clone(), r.other_columns.clone())) + .map(|r| { + ( + r.local_columns.clone(), + r.other_table.clone(), + r.other_columns.clone(), + ) + }) .collect(); - vec![crate::echo::render_create_m2n(&desc.name, &columns, &primary_key, &foreign_keys)] + vec![crate::echo::render_create_m2n( + &desc.name, + &columns, + &primary_key, + &foreign_keys, + )] }), // Everything else (Bucket A pure-Command, plus the no-echo Bucket C // variants like `Sql*` / `ShowTable`) routes through the existing @@ -2103,10 +2091,7 @@ async fn enrich_unique_violation( facts } -fn enrich_not_null_violation( - command: &Command, - message: &str, -) -> crate::friendly::FailureContext { +fn enrich_not_null_violation(command: &Command, message: &str) -> crate::friendly::FailureContext { let mut facts = crate::friendly::FailureContext::default(); let Some((table, column)) = parse_qualified_target(message) else { return facts; @@ -2133,9 +2118,7 @@ async fn enrich_fk_violation( // schema-aware lookup so natural-order multi-value // INSERT (which `user_value_for_column` alone can't // resolve) gets handled too. - let Ok((outbound, _)) = - database.read_relationships(table.clone()).await - else { + let Ok((outbound, _)) = database.read_relationships(table.clone()).await else { return facts; }; facts.table = Some(table.clone()); @@ -2173,8 +2156,7 @@ async fn enrich_fk_violation( // children reference). Check inbound as a fallback. if facts.parent_table.is_none() && matches!(command, Command::Update { .. }) - && let Ok((_, inbound)) = - database.read_relationships(table.clone()).await + && let Ok((_, inbound)) = database.read_relationships(table.clone()).await && let Some(rel) = inbound.first() { facts.child_table = Some(rel.other_table.clone()); @@ -2184,9 +2166,7 @@ async fn enrich_fk_violation( // Parent-side: inbound FK lookup. Surface a child // table that still references the row(s) being // deleted. - let Ok((_, inbound)) = - database.read_relationships(table.clone()).await - else { + let Ok((_, inbound)) = database.read_relationships(table.clone()).await else { return facts; }; facts.table = Some(table.clone()); @@ -2271,10 +2251,7 @@ async fn user_value_for_column_with_schema( .. } = command { - let desc = database - .describe_table(table.to_string()) - .await - .ok()?; + let desc = database.describe_table(table.to_string()).await.ok()?; // Build the natural-order column list the same way // `do_insert` does: filter out serial / shortid columns // because the engine auto-fills them and the user's @@ -2285,8 +2262,7 @@ async fn user_value_for_column_with_schema( .filter(|c| { !matches!( c.user_type, - Some(crate::dsl::Type::Serial) - | Some(crate::dsl::Type::ShortId) + Some(crate::dsl::Type::Serial) | Some(crate::dsl::Type::ShortId) ) }) .map(|c| c.name.as_str()) @@ -2310,10 +2286,7 @@ async fn user_value_for_column_with_schema( && listed_columns.is_empty() && literal_rows.len() == 1 { - let desc = database - .describe_table(table.to_string()) - .await - .ok()?; + let desc = database.describe_table(table.to_string()).await.ok()?; let idx = desc.columns.iter().position(|c| c.name == column)?; return literal_rows[0].get(idx).cloned().flatten(); } @@ -2323,16 +2296,12 @@ async fn user_value_for_column_with_schema( /// Render a `DataResult` as a `DiagnosticTable` for the /// friendly-error layer's bordered renderer (ADR-0019 §7, /// reusing ADR-0017 §7's renderer). -fn diagnostic_from_data_result( - data: &DataResult, -) -> crate::friendly::DiagnosticTable { - use crate::output_render::{numeric_alignment_for, Alignment}; +fn diagnostic_from_data_result(data: &DataResult) -> crate::friendly::DiagnosticTable { + use crate::output_render::{Alignment, numeric_alignment_for}; let alignments: Vec = data .column_types .iter() - .map(|t| { - t.map_or(Alignment::Left, numeric_alignment_for) - }) + .map(|t| t.map_or(Alignment::Left, numeric_alignment_for)) .collect(); let rows: Vec> = data .rows @@ -2543,9 +2512,7 @@ pub async fn run_replay( // command, which was skipped above) — report it with the line // number and stop. let schema = build_schema_cache(database).await; - let command = match crate::dsl::parser::parse_command_with_schema( - &command_text, &schema, - ) { + let command = match crate::dsl::parser::parse_command_with_schema(&command_text, &schema) { Ok(c) => c, Err(e) => { events.push(AppEvent::ReplayFailed { @@ -2566,8 +2533,7 @@ pub async fn run_replay( // Retain a clone for failure enrichment (the command is moved into // dispatch). ADR-0035 Amendment 1, F2 follow-up. let command_for_ctx = command.clone(); - let outcome = - execute_command_typed(database, command, command_text.clone()).await; + let outcome = execute_command_typed(database, command, command_text.clone()).await; match outcome { Ok(_) => { // ADR-0052: journal the replayed line at the dispatch @@ -2873,7 +2839,10 @@ async fn execute_command_typed( .drop_constraint(table, column, ConstraintKind::NotNull, src) .await .map(|d| CommandOutcome::Schema(Some(d))), - AlterTableAction::SetColumnDefault { column, default_sql } => database + AlterTableAction::SetColumnDefault { + column, + default_sql, + } => database .set_column_default(table, column, default_sql, src) .await .map(|d| CommandOutcome::Schema(Some(d))), @@ -2989,10 +2958,7 @@ async fn execute_command_typed( // A SQL `SELECT` (advanced mode; ADR-0030 §6, ADR-0031). // The grammar walker has already validated `sql` is in // the supported subset; the worker runs it as text. - Command::Select { sql } => database - .run_select(sql) - .await - .map(CommandOutcome::Query), + Command::Select { sql } => database.run_select(sql).await.map(CommandOutcome::Query), // A SQL `INSERT` (advanced mode; ADR-0033 §1). Grammar-as- // text: the worker runs the validated `sql` and re-persists // the parsed `target_table`'s CSV. Reuses the DSL insert @@ -3112,12 +3078,9 @@ fn setup_terminal() -> Result>> { Ok(terminal) } -fn teardown_terminal( - terminal: &mut Terminal>, -) -> Result<()> { +fn teardown_terminal(terminal: &mut Terminal>) -> Result<()> { disable_raw_mode().context("disable raw mode")?; - execute!(terminal.backend_mut(), LeaveAlternateScreen) - .context("leave alternate screen")?; + execute!(terminal.backend_mut(), LeaveAlternateScreen).context("leave alternate screen")?; terminal.show_cursor().context("show cursor")?; Ok(()) } @@ -3257,7 +3220,9 @@ mod tests { // Limited → ORDER BY the resolved primary key. assert_eq!( super::build_show_data_echo(&db, &limited, EffectiveMode::AdvancedPersistent).await, - Some(vec!["SELECT * FROM Customers ORDER BY id LIMIT 5".to_string()]), + Some(vec![ + "SELECT * FROM Customers ORDER BY id LIMIT 5".to_string() + ]), ); // Simple mode → silent, gated before any lookup. assert_eq!( @@ -3288,10 +3253,10 @@ mod tests { async fn bucket_b_resolved_name_echoes_against_real_worker() { use crate::app::EffectiveMode; use crate::db::Database; + use crate::dsl::Command; use crate::dsl::ReferentialAction; use crate::dsl::command::{ColumnSpec, IndexSelector, RelationshipSelector}; use crate::dsl::types::Type; - use crate::dsl::Command; let db = Database::open(":memory:").expect("open in-memory"); db.create_table( @@ -3319,7 +3284,12 @@ mod tests { // --- add index (auto-named) ---------------------------------- let desc_after_add_index = db - .add_index(None, "Customers".to_string(), vec!["Email".to_string()], None) + .add_index( + None, + "Customers".to_string(), + vec!["Email".to_string()], + None, + ) .await .expect("add index"); let add_idx_cmd = Command::AddIndex { @@ -3439,7 +3409,10 @@ mod tests { .await; assert_eq!( endpoints_lookups.drop_relationship, - Some(("Customers_id_to_Orders_CustId".to_string(), "Orders".to_string())), + Some(( + "Customers_id_to_Orders_CustId".to_string(), + "Orders".to_string() + )), "endpoints selector resolves name via child describe", ); @@ -3454,7 +3427,10 @@ mod tests { .await; assert_eq!( named_lookups.drop_relationship, - Some(("Customers_id_to_Orders_CustId".to_string(), "Orders".to_string())), + Some(( + "Customers_id_to_Orders_CustId".to_string(), + "Orders".to_string() + )), "named selector scans user tables to find the child", ); @@ -3487,10 +3463,10 @@ mod tests { async fn bucket_b_multi_statement_echoes_against_real_worker() { use crate::app::EffectiveMode; use crate::db::Database; + use crate::dsl::Command; use crate::dsl::ReferentialAction; use crate::dsl::command::ColumnSpec; use crate::dsl::types::Type; - use crate::dsl::Command; // --- drop column --cascade ----------------------------------- let db = Database::open(":memory:").expect("open in-memory"); @@ -3505,9 +3481,14 @@ mod tests { ) .await .expect("create Customers"); - db.add_index(None, "Customers".to_string(), vec!["Email".to_string()], None) - .await - .expect("index Email"); + db.add_index( + None, + "Customers".to_string(), + vec!["Email".to_string()], + None, + ) + .await + .expect("index Email"); let drop_cmd = Command::DropColumn { table: "Customers".to_string(), @@ -3531,12 +3512,8 @@ mod tests { ); // Simple mode → silent. assert!( - super::build_drop_column_cascade_echo( - &drop_cmd, - EffectiveMode::Simple, - &drop_result, - ) - .is_none(), + super::build_drop_column_cascade_echo(&drop_cmd, EffectiveMode::Simple, &drop_result,) + .is_none(), ); // --- add relationship --create-fk (column newly created) ---- @@ -3673,11 +3650,11 @@ mod tests { // switch (an unmodified temp would be cleaned up, taking its // project.yaml with it). Without the unload persist the // outgoing skeleton carries no `mode:` → `None`. - use super::{handle_project_switch, Session, SwitchRequest}; + use super::{Session, SwitchRequest, handle_project_switch}; use crate::db::Database; use crate::mode::Mode; use crate::persistence::Persistence; - use crate::project::{projects_dir, Project}; + use crate::project::{Project, projects_dir}; use tokio::sync::mpsc; let data_root = tempfile::tempdir().unwrap(); @@ -3686,8 +3663,7 @@ mod tests { let outgoing_path = projects.join("Outgoing"); let outgoing = Project::create_named(&outgoing_path).unwrap(); let db_path = outgoing.db_path(); - let persistence = - Persistence::new(outgoing.path().to_path_buf()).with_mode(Mode::Advanced); + let persistence = Persistence::new(outgoing.path().to_path_buf()).with_mode(Mode::Advanced); let database = Database::open_with_persistence_and_undo(&db_path, persistence, true).unwrap(); let mut session = Session { diff --git a/src/seed/check.rs b/src/seed/check.rs index 1294eb1..ad27f29 100644 --- a/src/seed/check.rs +++ b/src/seed/check.rs @@ -18,7 +18,11 @@ pub fn parse_in_check_values(check: &str, column: &str) -> Option> { return None; } let values = extract_quoted_list(&check[paren_open..])?; - if values.is_empty() { None } else { Some(values) } + if values.is_empty() { + None + } else { + Some(values) + } } const fn is_ident_byte(b: u8) -> bool { @@ -45,8 +49,8 @@ fn find_in_paren(check: &str) -> Option<(usize, usize)> { i += 1; continue; } - let is_in = (b == b'i' || b == b'I') - && bytes.get(i + 1).is_some_and(|n| *n == b'n' || *n == b'N'); + let is_in = + (b == b'i' || b == b'I') && bytes.get(i + 1).is_some_and(|n| *n == b'n' || *n == b'N'); if is_in { let before_ok = i == 0 || !is_ident_byte(bytes[i - 1]); let after = i + 2; diff --git a/src/seed/generators.rs b/src/seed/generators.rs index 7e81f86..9d09be1 100644 --- a/src/seed/generators.rs +++ b/src/seed/generators.rs @@ -81,17 +81,22 @@ pub fn generate_value(generator: &Generator, ty: Type, rng: &mut SeedRng) -> Val Generator::CurrencyAmount => currency_amount(ty, rng), Generator::Age => Value::Number(rng.random_range(18..=80).to_string()), Generator::SmallInt => Value::Number(rng.random_range(1..=100).to_string()), - Generator::YearRecent => { - Value::Number(rng.random_range((REF_YEAR - YEAR_RECENT_SPAN)..=REF_YEAR).to_string()) - } + Generator::YearRecent => Value::Number( + rng.random_range((REF_YEAR - YEAR_RECENT_SPAN)..=REF_YEAR) + .to_string(), + ), Generator::YearBirth => Value::Number( rng.random_range((REF_YEAR - YEAR_BIRTH_MAX_AGE)..=(REF_YEAR - YEAR_BIRTH_MIN_AGE)) .to_string(), ), - Generator::DateRecent => Value::Text(format_date(random_past_date(rng, 0, RECENT_WINDOW_DAYS))), - Generator::DateAdult => { - Value::Text(format_date(random_past_date(rng, ADULT_MIN_DAYS, ADULT_MAX_DAYS))) + Generator::DateRecent => { + Value::Text(format_date(random_past_date(rng, 0, RECENT_WINDOW_DAYS))) } + Generator::DateAdult => Value::Text(format_date(random_past_date( + rng, + ADULT_MIN_DAYS, + ADULT_MAX_DAYS, + ))), Generator::DateTimeRecent => Value::Text(random_recent_datetime(rng)), Generator::Boolean => Value::Bool(rng.random_range(0..2) == 1), Generator::PickFrom(values) if !values.is_empty() => { @@ -232,8 +237,7 @@ fn random_datetime_between( } else { rng.random_range(hi_s..=lo_s) }; - let dt = chrono::DateTime::from_timestamp(secs, 0) - .map_or(lo, |d| d.naive_utc()); + let dt = chrono::DateTime::from_timestamp(secs, 0).map_or(lo, |d| d.naive_utc()); dt.format("%Y-%m-%dT%H:%M:%S").to_string() } @@ -294,20 +298,35 @@ fn currency_amount(ty: Type, rng: &mut SeedRng) -> Value { // — the hand-rolled `product` generator (D9) — const PRODUCT_ADJECTIVES: &[&str] = &[ - "Sleek", "Rustic", "Ergonomic", "Handcrafted", "Refined", "Modern", - "Vintage", "Compact", "Premium", "Lightweight", "Durable", "Elegant", - "Sturdy", "Smooth", "Gorgeous", "Intelligent", "Practical", "Awesome", - "Incredible", "Recycled", + "Sleek", + "Rustic", + "Ergonomic", + "Handcrafted", + "Refined", + "Modern", + "Vintage", + "Compact", + "Premium", + "Lightweight", + "Durable", + "Elegant", + "Sturdy", + "Smooth", + "Gorgeous", + "Intelligent", + "Practical", + "Awesome", + "Incredible", + "Recycled", ]; const PRODUCT_MATERIALS: &[&str] = &[ - "Wooden", "Copper", "Granite", "Cotton", "Steel", "Leather", "Bamboo", - "Plastic", "Ceramic", "Glass", "Concrete", "Rubber", "Bronze", "Marble", - "Linen", "Silk", "Aluminum", "Wool", "Gold", "Carbon", + "Wooden", "Copper", "Granite", "Cotton", "Steel", "Leather", "Bamboo", "Plastic", "Ceramic", + "Glass", "Concrete", "Rubber", "Bronze", "Marble", "Linen", "Silk", "Aluminum", "Wool", "Gold", + "Carbon", ]; const PRODUCT_NOUNS: &[&str] = &[ - "Chair", "Lamp", "Table", "Bottle", "Backpack", "Keyboard", "Mug", - "Shoes", "Jacket", "Watch", "Wallet", "Bench", "Hat", "Gloves", - "Towel", "Ball", "Bike", "Knife", "Pillow", "Blanket", + "Chair", "Lamp", "Table", "Bottle", "Backpack", "Keyboard", "Mug", "Shoes", "Jacket", "Watch", + "Wallet", "Bench", "Hat", "Gloves", "Towel", "Ball", "Bike", "Knife", "Pillow", "Blanket", ]; fn product_name(rng: &mut SeedRng) -> String { @@ -396,7 +415,9 @@ mod tests { ] { let v = gen_once(&generator, Type::Text, 3); match v { - Value::Text(s) => assert!(!s.trim().is_empty(), "{generator:?} produced empty text"), + Value::Text(s) => { + assert!(!s.trim().is_empty(), "{generator:?} produced empty text") + } other => panic!("{generator:?} produced non-text {other:?}"), } } @@ -405,18 +426,25 @@ mod tests { #[test] fn email_looks_like_an_email() { let v = gen_once(&Generator::Email, Type::Text, 11); - let Value::Text(s) = v else { panic!("not text") }; + let Value::Text(s) = v else { + panic!("not text") + }; assert!(s.contains('@'), "email should contain @: {s}"); } #[test] fn product_name_is_three_capitalised_words() { let v = gen_once(&Generator::ProductName, Type::Text, 99); - let Value::Text(s) = v else { panic!("not text") }; + let Value::Text(s) = v else { + panic!("not text") + }; let words: Vec<&str> = s.split(' ').collect(); assert_eq!(words.len(), 3, "product name should be 3 words: {s}"); for w in words { - assert!(w.chars().next().unwrap().is_ascii_uppercase(), "word `{w}` not capitalised"); + assert!( + w.chars().next().unwrap().is_ascii_uppercase(), + "word `{w}` not capitalised" + ); } } @@ -429,9 +457,14 @@ mod tests { let latest = reference_date(); for _ in 0..200 { let v = generate_value(&Generator::DateRecent, Type::Date, &mut rng); - let Value::Text(s) = v else { panic!("date not text") }; + let Value::Text(s) = v else { + panic!("date not text") + }; let d = NaiveDate::parse_from_str(&s, "%Y-%m-%d").expect("valid ISO date"); - assert!(d >= earliest && d <= latest, "date {d} outside recent window"); + assert!( + d >= earliest && d <= latest, + "date {d} outside recent window" + ); } } @@ -446,7 +479,9 @@ mod tests { .unwrap(); for _ in 0..200 { let v = generate_value(&Generator::DateAdult, Type::Date, &mut rng); - let Value::Text(s) = v else { panic!("date not text") }; + let Value::Text(s) = v else { + panic!("date not text") + }; let d = NaiveDate::parse_from_str(&s, "%Y-%m-%d").expect("valid ISO date"); assert!(d >= earliest && d <= latest, "dob {d} outside adult window"); } @@ -455,7 +490,9 @@ mod tests { #[test] fn datetime_is_iso_shaped() { let v = gen_once(&Generator::DateTimeRecent, Type::DateTime, 5); - let Value::Text(s) = v else { panic!("not text") }; + let Value::Text(s) = v else { + panic!("not text") + }; assert!(s.contains('T'), "datetime needs a T separator: {s}"); // Parses as a naive datetime. chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S") @@ -467,11 +504,17 @@ mod tests { let Value::Number(int_amt) = gen_once(&Generator::CurrencyAmount, Type::Int, 4) else { panic!("not a number") }; - assert!(!int_amt.contains('.'), "int currency should be whole: {int_amt}"); + assert!( + !int_amt.contains('.'), + "int currency should be whole: {int_amt}" + ); let Value::Number(dec_amt) = gen_once(&Generator::CurrencyAmount, Type::Decimal, 4) else { panic!("not a number") }; - assert!(dec_amt.contains('.'), "decimal currency should have cents: {dec_amt}"); + assert!( + dec_amt.contains('.'), + "decimal currency should have cents: {dec_amt}" + ); } #[test] @@ -494,7 +537,10 @@ mod tests { let Value::Text(s) = generate_value(&generator, Type::Text, &mut rng) else { panic!("not text") }; - assert!(matches!(s.as_str(), "active" | "closed"), "unexpected pick {s}"); + assert!( + matches!(s.as_str(), "active" | "closed"), + "unexpected pick {s}" + ); } } @@ -503,7 +549,10 @@ mod tests { let generator = Generator::PickFrom(vec!["1".into(), "2".into(), "3".into()]); let mut rng = make_rng(Some(6)); let v = generate_value(&generator, Type::Int, &mut rng); - assert!(matches!(v, Value::Number(_)), "numeric pick should be a Number: {v:?}"); + assert!( + matches!(v, Value::Number(_)), + "numeric pick should be a Number: {v:?}" + ); } #[test] @@ -517,7 +566,10 @@ mod tests { panic!("YearRecent must be a Number") }; let n: i32 = s.parse().unwrap(); - assert!((1950..=2025).contains(&n), "YearRecent {n} out of [1950,2025]"); + assert!( + (1950..=2025).contains(&n), + "YearRecent {n} out of [1950,2025]" + ); } for _ in 0..300 { let Value::Number(s) = generate_value(&Generator::YearBirth, Type::Int, &mut rng) @@ -525,7 +577,10 @@ mod tests { panic!("YearBirth must be a Number") }; let n: i32 = s.parse().unwrap(); - assert!((1945..=2007).contains(&n), "YearBirth {n} out of [1945,2007]"); + assert!( + (1945..=2007).contains(&n), + "YearBirth {n} out of [1945,2007]" + ); } } @@ -543,7 +598,10 @@ mod tests { #[test] fn int_range_stays_within_inclusive_bounds() { - let g = Generator::Range { low: "10".into(), high: "20".into() }; + let g = Generator::Range { + low: "10".into(), + high: "20".into(), + }; let mut rng = make_rng(Some(5)); for _ in 0..200 { let Value::Number(s) = generate_value(&g, Type::Int, &mut rng) else { @@ -556,7 +614,10 @@ mod tests { #[test] fn real_range_stays_within_bounds_and_has_cents() { - let g = Generator::Range { low: "1.0".into(), high: "9.0".into() }; + let g = Generator::Range { + low: "1.0".into(), + high: "9.0".into(), + }; let mut rng = make_rng(Some(5)); for _ in 0..200 { let Value::Number(s) = generate_value(&g, Type::Real, &mut rng) else { @@ -588,13 +649,19 @@ mod tests { #[test] fn reversed_bounds_are_tolerated() { - let g = Generator::Range { low: "20".into(), high: "10".into() }; + let g = Generator::Range { + low: "20".into(), + high: "10".into(), + }; let mut rng = make_rng(Some(1)); let Value::Number(s) = generate_value(&g, Type::Int, &mut rng) else { panic!("number") }; let n: i64 = s.parse().unwrap(); - assert!((10..=20).contains(&n), "reversed bounds still produce in-range: {n}"); + assert!( + (10..=20).contains(&n), + "reversed bounds still produce in-range: {n}" + ); } #[test] @@ -603,7 +670,10 @@ mod tests { assert!(range_bounds_reason(Type::Int, "1", "10").is_none()); assert!(range_bounds_reason(Type::Real, "1.5", "9.9").is_none()); assert!(range_bounds_reason(Type::Date, "2023-01-01", "2024-01-01").is_none()); - assert!(range_bounds_reason(Type::DateTime, "2023-01-01T00:00:00", "2024-01-01T00:00:00").is_none()); + assert!( + range_bounds_reason(Type::DateTime, "2023-01-01T00:00:00", "2024-01-01T00:00:00") + .is_none() + ); // Non-numeric bound on a numeric column. assert!(range_bounds_reason(Type::Int, "abc", "10").is_some()); // A range on a text column is meaningless. @@ -623,14 +693,29 @@ mod tests { #[test] fn generic_fallback_matches_each_type() { let mut rng = make_rng(Some(0)); - assert!(matches!(generate_value(&Generator::Generic, Type::Text, &mut rng), Value::Text(_))); - assert!(matches!(generate_value(&Generator::Generic, Type::Int, &mut rng), Value::Number(_))); - assert!(matches!(generate_value(&Generator::Generic, Type::Bool, &mut rng), Value::Bool(_))); - assert!(matches!(generate_value(&Generator::Generic, Type::Blob, &mut rng), Value::Null)); + assert!(matches!( + generate_value(&Generator::Generic, Type::Text, &mut rng), + Value::Text(_) + )); + assert!(matches!( + generate_value(&Generator::Generic, Type::Int, &mut rng), + Value::Number(_) + )); + assert!(matches!( + generate_value(&Generator::Generic, Type::Bool, &mut rng), + Value::Bool(_) + )); + assert!(matches!( + generate_value(&Generator::Generic, Type::Blob, &mut rng), + Value::Null + )); // shortid fallback is a valid base58 id. let Value::Text(sid) = generate_value(&Generator::Generic, Type::ShortId, &mut rng) else { panic!("shortid not text") }; - assert!(crate::dsl::shortid::validate(&sid).is_ok(), "invalid shortid {sid}"); + assert!( + crate::dsl::shortid::validate(&sid).is_ok(), + "invalid shortid {sid}" + ); } } diff --git a/src/seed/heuristics.rs b/src/seed/heuristics.rs index 3162dd2..1701386 100644 --- a/src/seed/heuristics.rs +++ b/src/seed/heuristics.rs @@ -63,8 +63,7 @@ pub fn is_enum_ish(name: &str) -> bool { // `rating` / `stars` were never here. `status` stays — it is // deliberately left to the advisory (no built-in set). const ENUM_TOKENS: &[&str] = &[ - "role", "status", "state", "type", "kind", "category", "level", - "tier", "stage", "gender", + "role", "status", "state", "type", "kind", "category", "level", "tier", "stage", "gender", ]; let toks = tokens(name); toks.iter().any(|t| ENUM_TOKENS.contains(&t.as_str())) @@ -81,9 +80,7 @@ fn match_name_generator(table: &str, toks: &[String], ty: Type) -> Option Option Option Option Option Option Generator { let toks = tokens(table); const PRODUCTY: &[&str] = &[ - "product", "products", "item", "items", "good", "goods", - "merchandise", "catalog", "catalogue", "inventory", "sku", "skus", + "product", + "products", + "item", + "items", + "good", + "goods", + "merchandise", + "catalog", + "catalogue", + "inventory", + "sku", + "skus", ]; const COMPANYISH: &[&str] = &[ - "company", "companies", "vendor", "vendors", "supplier", - "suppliers", "manufacturer", "manufacturers", "brand", "brands", - "organization", "organisation", + "company", + "companies", + "vendor", + "vendors", + "supplier", + "suppliers", + "manufacturer", + "manufacturers", + "brand", + "brands", + "organization", + "organisation", ]; const PERSONISH: &[&str] = &[ - "user", "users", "customer", "customers", "person", "people", - "employee", "employees", "member", "members", "contact", - "contacts", "author", "authors", "student", "students", + "user", + "users", + "customer", + "customers", + "person", + "people", + "employee", + "employees", + "member", + "members", + "contact", + "contacts", + "author", + "authors", + "student", + "students", ]; if has_any(&toks, PRODUCTY) { Generator::ProductName @@ -264,9 +322,8 @@ fn name_by_table_context(table: &str) -> Generator { /// before this guard; this catches structural names. fn is_name_false_positive(toks: &[String]) -> bool { const NON_PERSON: &[&str] = &[ - "file", "table", "host", "domain", "field", "class", "tag", - "event", "path", "col", "column", "db", "schema", "index", "key", - "page", "node", "type", + "file", "table", "host", "domain", "field", "class", "tag", "event", "path", "col", + "column", "db", "schema", "index", "key", "page", "node", "type", ]; has_any(toks, NON_PERSON) && has_any(toks, &["name", "title"]) } @@ -357,9 +414,18 @@ mod tests { #[test] fn person_name_fields_map_to_name_generators() { - assert_eq!(choose("users", "first_name", Type::Text), Generator::FirstName); - assert_eq!(choose("users", "firstName", Type::Text), Generator::FirstName); - assert_eq!(choose("users", "last_name", Type::Text), Generator::LastName); + assert_eq!( + choose("users", "first_name", Type::Text), + Generator::FirstName + ); + assert_eq!( + choose("users", "firstName", Type::Text), + Generator::FirstName + ); + assert_eq!( + choose("users", "last_name", Type::Text), + Generator::LastName + ); assert_eq!(choose("users", "surname", Type::Text), Generator::LastName); } @@ -368,9 +434,15 @@ mod tests { assert_eq!(choose("users", "email", Type::Text), Generator::Email); assert_eq!(choose("users", "work_email", Type::Text), Generator::Email); assert_eq!(choose("users", "username", Type::Text), Generator::Username); - assert_eq!(choose("users", "user_name", Type::Text), Generator::Username); + assert_eq!( + choose("users", "user_name", Type::Text), + Generator::Username + ); assert_eq!(choose("users", "phone", Type::Text), Generator::Phone); - assert_eq!(choose("accounts", "password", Type::Text), Generator::Password); + assert_eq!( + choose("accounts", "password", Type::Text), + Generator::Password + ); } #[test] @@ -386,7 +458,10 @@ mod tests { #[test] fn bare_name_uses_table_context() { // D11 — the same column name resolves differently by table. - assert_eq!(choose("products", "name", Type::Text), Generator::ProductName); + assert_eq!( + choose("products", "name", Type::Text), + Generator::ProductName + ); assert_eq!(choose("items", "title", Type::Text), Generator::ProductName); assert_eq!(choose("users", "name", Type::Text), Generator::FullName); assert_eq!(choose("customers", "name", Type::Text), Generator::FullName); @@ -399,7 +474,10 @@ mod tests { fn name_false_positives_do_not_become_person_names() { // These must NOT resolve to a person/product name. assert_ne!(choose("files", "filename", Type::Text), Generator::FullName); - assert_ne!(choose("meta", "table_name", Type::Text), Generator::FullName); + assert_ne!( + choose("meta", "table_name", Type::Text), + Generator::FullName + ); // They fall through to a generic / non-person generator. assert_eq!(choose("files", "filename", Type::Text), Generator::Generic); } @@ -408,7 +486,10 @@ mod tests { fn numeric_name_heuristics_are_type_gated() { // `price` on a numeric column → currency; on text → falls through. assert_eq!(choose("p", "price", Type::Int), Generator::CurrencyAmount); - assert_eq!(choose("p", "price", Type::Decimal), Generator::CurrencyAmount); + assert_eq!( + choose("p", "price", Type::Decimal), + Generator::CurrencyAmount + ); assert_eq!(choose("p", "price", Type::Text), Generator::Generic); assert_eq!(choose("u", "age", Type::Int), Generator::Age); assert_eq!(choose("o", "quantity", Type::Int), Generator::SmallInt); @@ -425,8 +506,14 @@ mod tests { fn temporal_fields_are_bounded_and_type_gated() { assert_eq!(choose("u", "dob", Type::Date), Generator::DateAdult); assert_eq!(choose("o", "order_date", Type::Date), Generator::DateRecent); - assert_eq!(choose("o", "created_at", Type::DateTime), Generator::DateTimeRecent); - assert_eq!(choose("o", "timestamp", Type::DateTime), Generator::DateTimeRecent); + assert_eq!( + choose("o", "created_at", Type::DateTime), + Generator::DateTimeRecent + ); + assert_eq!( + choose("o", "timestamp", Type::DateTime), + Generator::DateTimeRecent + ); // Wrong type → not a date generator. assert_eq!(choose("o", "order_date", Type::Int), Generator::Generic); } @@ -440,17 +527,32 @@ mod tests { #[test] fn identifier_family_is_unique_sequential() { - assert_eq!(choose("t", "code", Type::Text), Generator::IdentitySequential); - assert_eq!(choose("t", "sku", Type::Text), Generator::IdentitySequential); - assert_eq!(choose("t", "order_number", Type::Int), Generator::IdentitySequential); - assert_eq!(choose("t", "external_id", Type::Int), Generator::IdentitySequential); + assert_eq!( + choose("t", "code", Type::Text), + Generator::IdentitySequential + ); + assert_eq!( + choose("t", "sku", Type::Text), + Generator::IdentitySequential + ); + assert_eq!( + choose("t", "order_number", Type::Int), + Generator::IdentitySequential + ); + assert_eq!( + choose("t", "external_id", Type::Int), + Generator::IdentitySequential + ); } #[test] fn foreign_key_columns_defer_to_executor() { let mut spec = ColumnSpec::plain("user_id", Type::Int); spec.is_foreign_key = true; - assert_eq!(choose_generator("orders", &spec), Generator::ForeignKeySample); + assert_eq!( + choose_generator("orders", &spec), + Generator::ForeignKeySample + ); } #[test] @@ -481,13 +583,28 @@ mod tests { fn year_like_int_columns_map_to_bounded_years() { // Issue #33: `int`-gated year heuristics. `birth`/`born`/`dob` // years pick the birth window; the rest a recent window. - assert_eq!(choose("authors", "birth_year", Type::Int), Generator::YearBirth); - assert_eq!(choose("authors", "birthYear", Type::Int), Generator::YearBirth); + assert_eq!( + choose("authors", "birth_year", Type::Int), + Generator::YearBirth + ); + assert_eq!( + choose("authors", "birthYear", Type::Int), + Generator::YearBirth + ); assert_eq!(choose("u", "year_born", Type::Int), Generator::YearBirth); assert_eq!(choose("books", "year", Type::Int), Generator::YearRecent); - assert_eq!(choose("films", "release_year", Type::Int), Generator::YearRecent); - assert_eq!(choose("books", "published", Type::Int), Generator::YearRecent); - assert_eq!(choose("companies", "founded", Type::Int), Generator::YearRecent); + assert_eq!( + choose("films", "release_year", Type::Int), + Generator::YearRecent + ); + assert_eq!( + choose("books", "published", Type::Int), + Generator::YearRecent + ); + assert_eq!( + choose("companies", "founded", Type::Int), + Generator::YearRecent + ); // Type-gated: a text `year` is not a bounded-year int. assert_eq!(choose("books", "year", Type::Text), Generator::Generic); // `year_count` is a count, not a year — the quantity rule wins. @@ -507,7 +624,12 @@ mod tests { ); assert_eq!( choose("bugs", "severity", Type::Text), - Generator::PickFrom(vec!["low".into(), "medium".into(), "high".into(), "critical".into()]), + Generator::PickFrom(vec![ + "low".into(), + "medium".into(), + "high".into(), + "critical".into() + ]), ); assert_eq!( choose("bugs", "severity", Type::Int), @@ -515,11 +637,23 @@ mod tests { ); assert_eq!( choose("reviews", "rating", Type::Int), - Generator::PickFrom(vec!["1".into(), "2".into(), "3".into(), "4".into(), "5".into()]), + Generator::PickFrom(vec![ + "1".into(), + "2".into(), + "3".into(), + "4".into(), + "5".into() + ]), ); assert_eq!( choose("reviews", "stars", Type::Int), - Generator::PickFrom(vec!["1".into(), "2".into(), "3".into(), "4".into(), "5".into()]), + Generator::PickFrom(vec![ + "1".into(), + "2".into(), + "3".into(), + "4".into(), + "5".into() + ]), ); } @@ -552,7 +686,10 @@ mod tests { #[test] fn unmatched_columns_use_type_based_fallback() { - assert_eq!(choose("t", "some_freeform_field", Type::Text), Generator::Generic); + assert_eq!( + choose("t", "some_freeform_field", Type::Text), + Generator::Generic + ); } #[test] diff --git a/src/seed/mod.rs b/src/seed/mod.rs index 452097b..385efcd 100644 --- a/src/seed/mod.rs +++ b/src/seed/mod.rs @@ -32,7 +32,7 @@ mod vocabulary; pub use check::parse_in_check_values; pub use generators::{generate_value, range_bounds_reason}; pub use heuristics::{choose_generator, is_enum_ish}; -pub use vocabulary::{generator_for_name, is_known_generator_prefix, KNOWN_GENERATORS}; +pub use vocabulary::{KNOWN_GENERATORS, generator_for_name, is_known_generator_prefix}; use rand::rngs::StdRng; use rand::{RngExt, SeedableRng}; @@ -183,7 +183,10 @@ pub enum Generator { /// does not parse for the column type is a friendly error), so /// [`generate_value`] only ever sees parseable bounds; a defensive /// parse failure falls back to type-based generation. - Range { low: String, high: String }, + Range { + low: String, + high: String, + }, /// Type-based fallback (D8) when no name heuristic matches. Generic, } diff --git a/src/theme.rs b/src/theme.rs index 8ffeabb..ef43d8e 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -104,15 +104,15 @@ impl Theme { // remains restful; literals and flags get warm // accent tones; keyword takes a cool accent tone // distinct from the mode-banner blue. - tok_keyword: Color::Rgb(0xC7, 0x92, 0xEA), // muted purple + tok_keyword: Color::Rgb(0xC7, 0x92, 0xEA), // muted purple tok_identifier: Color::Rgb(0x56, 0xB6, 0xC2), // cyan-teal — identifiers are the user's content, deserve a vivid distinct colour - tok_type: Color::Rgb(0xF0, 0x8F, 0xC0), // pink — types sit in the red-purple range, clearly apart from the lavender keyword and teal identifier - tok_number: Color::Rgb(0xF7, 0x8C, 0x6C), // warm orange - tok_string: Color::Rgb(0xC3, 0xE8, 0x8D), // soft green - tok_punct: Color::Rgb(0x8B, 0x90, 0x9A), // == muted - tok_flag: Color::Rgb(0xFF, 0xCB, 0x6B), // amber - tok_error: Color::Rgb(0xFF, 0x6B, 0x6B), // == error - tok_function: Color::Rgb(0x82, 0xCF, 0xFD), // sky blue — cool like keyword but bluer, clearly apart from purple keyword + teal identifier + pink type + tok_type: Color::Rgb(0xF0, 0x8F, 0xC0), // pink — types sit in the red-purple range, clearly apart from the lavender keyword and teal identifier + tok_number: Color::Rgb(0xF7, 0x8C, 0x6C), // warm orange + tok_string: Color::Rgb(0xC3, 0xE8, 0x8D), // soft green + tok_punct: Color::Rgb(0x8B, 0x90, 0x9A), // == muted + tok_flag: Color::Rgb(0xFF, 0xCB, 0x6B), // amber + tok_error: Color::Rgb(0xFF, 0x6B, 0x6B), // == error + tok_function: Color::Rgb(0x82, 0xCF, 0xFD), // sky blue — cool like keyword but bluer, clearly apart from purple keyword + teal identifier + pink type } } @@ -135,15 +135,15 @@ impl Theme { // Light-theme token palette: same intent as dark — // identifier/punct close to fg/muted; warm tones for // literals + flags; cool accent for keyword. - tok_keyword: Color::Rgb(0x6F, 0x42, 0xC1), // royal purple + tok_keyword: Color::Rgb(0x6F, 0x42, 0xC1), // royal purple tok_identifier: Color::Rgb(0x0F, 0x6B, 0x76), // deep teal — same role as dark variant: identifiers stand out - tok_type: Color::Rgb(0xA8, 0x2D, 0x73), // deep magenta — red-purple, distinct from royal-purple keyword + teal identifier - tok_number: Color::Rgb(0xBC, 0x4F, 0x1F), // burnt orange - tok_string: Color::Rgb(0x22, 0x86, 0x3A), // forest green - tok_punct: Color::Rgb(0x60, 0x66, 0x73), // == muted - tok_flag: Color::Rgb(0xB0, 0x88, 0x00), // mustard - tok_error: Color::Rgb(0xC0, 0x39, 0x2B), // == error - tok_function: Color::Rgb(0x1A, 0x5F, 0xB0), // strong blue — cool like keyword but bluer, apart from royal-purple keyword + teal identifier + magenta type + tok_type: Color::Rgb(0xA8, 0x2D, 0x73), // deep magenta — red-purple, distinct from royal-purple keyword + teal identifier + tok_number: Color::Rgb(0xBC, 0x4F, 0x1F), // burnt orange + tok_string: Color::Rgb(0x22, 0x86, 0x3A), // forest green + tok_punct: Color::Rgb(0x60, 0x66, 0x73), // == muted + tok_flag: Color::Rgb(0xB0, 0x88, 0x00), // mustard + tok_error: Color::Rgb(0xC0, 0x39, 0x2B), // == error + tok_function: Color::Rgb(0x1A, 0x5F, 0xB0), // strong blue — cool like keyword but bluer, apart from royal-purple keyword + teal identifier + magenta type } } @@ -192,10 +192,7 @@ mod tests { ("tok_function", t.tok_function), ("warning", t.warning), ] { - assert_ne!( - c, t.bg, - "{name} must contrast against bg in dark theme", - ); + assert_ne!(c, t.bg, "{name} must contrast against bg in dark theme",); } } @@ -212,24 +209,36 @@ mod tests { ("tok_function", t.tok_function), ("warning", t.warning), ] { - assert_ne!( - c, t.bg, - "{name} must contrast against bg in light theme", - ); + assert_ne!(c, t.bg, "{name} must contrast against bg in light theme",); } } #[test] fn highlight_class_color_maps_each_variant() { let t = Theme::dark(); - assert_eq!(t.highlight_class_color(HighlightClass::Keyword), t.tok_keyword); - assert_eq!(t.highlight_class_color(HighlightClass::Identifier), t.tok_identifier); + assert_eq!( + t.highlight_class_color(HighlightClass::Keyword), + t.tok_keyword + ); + assert_eq!( + t.highlight_class_color(HighlightClass::Identifier), + t.tok_identifier + ); assert_eq!(t.highlight_class_color(HighlightClass::Type), t.tok_type); - assert_eq!(t.highlight_class_color(HighlightClass::Number), t.tok_number); - assert_eq!(t.highlight_class_color(HighlightClass::String), t.tok_string); + assert_eq!( + t.highlight_class_color(HighlightClass::Number), + t.tok_number + ); + assert_eq!( + t.highlight_class_color(HighlightClass::String), + t.tok_string + ); assert_eq!(t.highlight_class_color(HighlightClass::Punct), t.tok_punct); assert_eq!(t.highlight_class_color(HighlightClass::Flag), t.tok_flag); - assert_eq!(t.highlight_class_color(HighlightClass::Function), t.tok_function); + assert_eq!( + t.highlight_class_color(HighlightClass::Function), + t.tok_function + ); assert_eq!(t.highlight_class_color(HighlightClass::Error), t.tok_error); } diff --git a/src/type_change.rs b/src/type_change.rs index e4360ac..fa3a2d9 100644 --- a/src/type_change.rs +++ b/src/type_change.rs @@ -87,9 +87,7 @@ pub fn static_refusal(src: Type, target: Type) -> Option { } const fn is_in_matrix(src: Type, target: Type) -> bool { - use Type::{ - Bool, Date, DateTime, Decimal, Int, Real, Serial, ShortId, Text, - }; + use Type::{Bool, Date, DateTime, Decimal, Int, Real, Serial, ShortId, Text}; matches!( (src, target), // Always-clean transformers @@ -130,9 +128,7 @@ pub fn transform_cell(src: Type, target: Type, value: &Value) -> CellOutcome { if matches!(value, Value::Null) { return CellOutcome::Clean(Value::Null); } - use Type::{ - Bool, Date, DateTime, Decimal, Int, Real, Serial, ShortId, Text, - }; + use Type::{Bool, Date, DateTime, Decimal, Int, Real, Serial, ShortId, Text}; match (src, target) { // ---- Always-clean: int / serial source ---- (Int | Serial, Real) => match value { @@ -179,9 +175,11 @@ pub fn transform_cell(src: Type, target: Type, value: &Value) -> CellOutcome { (Bool, Text) => match value { // "true" / "false" matches the DSL boolean grammar // (ADR-0014 §5), not raw 0/1 stringification. - Value::Integer(i) => CellOutcome::Clean(Value::Text( - if *i == 0 { "false".into() } else { "true".into() }, - )), + Value::Integer(i) => CellOutcome::Clean(Value::Text(if *i == 0 { + "false".into() + } else { + "true".into() + })), other => unexpected_storage("bool", other), }, @@ -369,9 +367,7 @@ pub fn transform_cell(src: Type, target: Type, value: &Value) -> CellOutcome { } } else { CellOutcome::Incompatible { - reason: format!( - "`{s}` is not a datetime in `YYYY-MM-DDTHH:MM:SS` form" - ), + reason: format!("`{s}` is not a datetime in `YYYY-MM-DDTHH:MM:SS` form"), } } } @@ -450,10 +446,7 @@ fn real_to_int(r: f64) -> CellOutcome { let discarded = r - r.trunc(); CellOutcome::Lossy { new: Value::Integer(truncated), - reason: format!( - "truncated; would discard {}", - format_real(discarded) - ), + reason: format!("truncated; would discard {}", format_real(discarded)), } } } @@ -555,9 +548,7 @@ fn format_real(r: f64) -> String { fn unexpected_storage(label: &str, value: &Value) -> CellOutcome { CellOutcome::Incompatible { - reason: format!( - "internal: cell stored unexpectedly for `{label}` source ({value:?})" - ), + reason: format!("internal: cell stored unexpectedly for `{label}` source ({value:?})"), } } @@ -638,7 +629,10 @@ mod tests { (Type::Date, Type::Int), (Type::ShortId, Type::Int), ] { - assert!(static_refusal(src, target).is_some(), "{src:?} -> {target:?}"); + assert!( + static_refusal(src, target).is_some(), + "{src:?} -> {target:?}" + ); } } @@ -672,7 +666,10 @@ mod tests { (Type::Bool, Type::Real), ]; for (s, t) in pairs { - assert_eq!(transform_cell(s, t, &Value::Null), CellOutcome::Clean(Value::Null)); + assert_eq!( + transform_cell(s, t, &Value::Null), + CellOutcome::Clean(Value::Null) + ); } } diff --git a/src/ui.rs b/src/ui.rs index d2f6f25..cdefc3f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -196,7 +196,16 @@ fn render_badge_box(label: &str, area: Rect, above: Option, frame: &mut Fr area.y + area.height - box_h - 1 } }; - fill_overlay_rect(Rect { x, y, width: box_w, height: box_h }, label.to_string(), frame); + fill_overlay_rect( + Rect { + x, + y, + width: box_w, + height: box_h, + }, + label.to_string(), + frame, + ); } /// A step-caption box inset one cell from the bottom-right of `area` @@ -309,7 +318,9 @@ fn render_path_entry( let inner_w = dialog_w.saturating_sub(4) as usize; let prompt_lines = wrap_lines(&m.prompt, inner_w); // Title + blank + prompt + blank + input box (1 row + borders) + blank + key hints. - let dialog_h = (prompt_lines.len() as u16).saturating_add(8).min(area.height); + let dialog_h = (prompt_lines.len() as u16) + .saturating_add(8) + .min(area.height); let x = area.x + (area.width.saturating_sub(dialog_w)) / 2; let y = area.y + (area.height.saturating_sub(dialog_h)) / 2; let dialog_area = Rect { @@ -320,9 +331,7 @@ fn render_path_entry( }; frame.render_widget(ratatui::widgets::Clear, dialog_area); - let title_style = Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD); + let title_style = Style::default().fg(theme.fg).add_modifier(Modifier::BOLD); let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) @@ -386,9 +395,7 @@ fn render_load_picker( }; frame.render_widget(ratatui::widgets::Clear, dialog_area); - let title_style = Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD); + let title_style = Style::default().fg(theme.fg).add_modifier(Modifier::BOLD); let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) @@ -411,9 +418,7 @@ fn render_load_picker( let marker = if i == m.selected { "›" } else { " " }; let temp_tag = if entry.is_temp { "[TEMP] " } else { "" }; let style = if i == m.selected { - Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD) + Style::default().fg(theme.fg).add_modifier(Modifier::BOLD) } else { Style::default().fg(theme.fg) }; @@ -447,11 +452,7 @@ fn render_load_picker( let display_input = if *cursor == input.len() { format!("{input}{cursor_marker}") } else { - format!( - "{}{cursor_marker}{}", - &input[..*cursor], - &input[*cursor..] - ) + format!("{}{cursor_marker}{}", &input[..*cursor], &input[*cursor..]) }; text_lines.push(Line::from(format!("> {display_input}"))); text_lines.push(Line::from("")); @@ -500,9 +501,7 @@ fn render_rebuild_confirm(summary: &str, theme: &Theme, frame: &mut Frame<'_>, a let bg = ratatui::widgets::Clear; frame.render_widget(bg, dialog_area); - let title_style = Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD); + let title_style = Style::default().fg(theme.fg).add_modifier(Modifier::BOLD); let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) @@ -524,16 +523,12 @@ fn render_rebuild_confirm(summary: &str, theme: &Theme, frame: &mut Frame<'_>, a text_lines.push(Line::from(vec![ Span::styled( "[Y]", - Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD), + Style::default().fg(theme.fg).add_modifier(Modifier::BOLD), ), Span::raw(format!(" {} ", crate::t!("shortcut.yes"))), Span::styled( "[N]", - Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD), + Style::default().fg(theme.fg).add_modifier(Modifier::BOLD), ), Span::raw(format!(" {} ", crate::t!("shortcut.no"))), Span::styled("Esc", Style::default().fg(theme.muted)), @@ -578,18 +573,14 @@ where /// dialog (issue #13): wide enough to hold the longest content /// line on a single row, clamped to sane bounds and the available /// area so a short insert no longer wraps on roomy terminals. -fn undo_dialog_width( - content_widths: impl IntoIterator, - area_width: u16, -) -> u16 { +fn undo_dialog_width(content_widths: impl IntoIterator, area_width: u16) -> u16 { /// Floor — comfortably fits the button row plus borders. const MIN: u16 = 34; /// Ceiling for outlier (ultra-wide) terminals. const MAX: u16 = 100; let widest = content_widths.into_iter().max().unwrap_or(0); // +4: left/right border (2) + one padding column each side (2). - let preferred = - u16::try_from(widest).unwrap_or(u16::MAX).saturating_add(4); + let preferred = u16::try_from(widest).unwrap_or(u16::MAX).saturating_add(4); let upper = area_width.min(MAX); let lower = MIN.min(upper); preferred.clamp(lower, upper) @@ -617,8 +608,7 @@ fn render_undo_confirm( let intro_line = format!("{intro} {}", m.command); // Local-time, human-formatted snapshot stamp (issue #13). let when_display = format_snapshot_timestamp(&m.timestamp); - let when_line = - crate::t!("modal.undo_confirm_when", timestamp = when_display); + let when_line = crate::t!("modal.undo_confirm_when", timestamp = when_display); let prompt = crate::t!("modal.undo_confirm_prompt"); // Reconstruct the button row purely to measure its width — the // styled spans are built below. Keep this in sync with them. @@ -681,9 +671,15 @@ fn render_undo_confirm( text_lines.push(Line::from(prompt)); text_lines.push(Line::from("")); text_lines.push(Line::from(vec![ - Span::styled("[Y]", Style::default().fg(theme.fg).add_modifier(Modifier::BOLD)), + Span::styled( + "[Y]", + Style::default().fg(theme.fg).add_modifier(Modifier::BOLD), + ), Span::raw(format!(" {} ", crate::t!("shortcut.yes"))), - Span::styled("[N]", Style::default().fg(theme.fg).add_modifier(Modifier::BOLD)), + Span::styled( + "[N]", + Style::default().fg(theme.fg).add_modifier(Modifier::BOLD), + ), Span::raw(format!(" {} ", crate::t!("shortcut.no"))), Span::styled("Esc", Style::default().fg(theme.muted)), Span::styled( @@ -811,17 +807,13 @@ fn clamp_wrapped(text: &str, width: usize, max_rows: usize) -> Vec { fn render_project_label(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) { let label_style = Style::default().fg(theme.muted); - let value_style = Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD); + let value_style = Style::default().fg(theme.fg).add_modifier(Modifier::BOLD); let bar_style = Style::default().bg(theme.bg).fg(theme.muted); let no_project = crate::t!("status.no_project"); let display = app.project_name.as_deref().unwrap_or(no_project.as_str()); - let mut spans: Vec> = vec![Span::styled( - crate::t!("status.project_label"), - label_style, - )]; + let mut spans: Vec> = + vec![Span::styled(crate::t!("status.project_label"), label_style)]; if app.project_is_temp { spans.push(Span::styled( "[TEMP] ", @@ -875,9 +867,7 @@ fn render_items_panel(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area: )) .title(Span::styled( format!(" {} ", crate::t!("panel.tables_title")), - Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD), + Style::default().fg(theme.fg).add_modifier(Modifier::BOLD), )) .style(Style::default().bg(theme.bg).fg(theme.fg)); @@ -918,9 +908,7 @@ fn render_items_panel(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area: let mut lines: Vec> = Vec::new(); for name in &app.tables { let style = if name == highlight { - Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD) + Style::default().fg(theme.fg).add_modifier(Modifier::BOLD) } else { Style::default().fg(theme.fg) }; @@ -937,7 +925,9 @@ fn render_items_panel(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area: } } } - let paragraph = Paragraph::new(lines).block(block).scroll((offset as u16, 0)); + let paragraph = Paragraph::new(lines) + .block(block) + .scroll((offset as u16, 0)); frame.render_widget(paragraph, area); } @@ -956,9 +946,7 @@ fn render_relationships_panel(app: &mut App, theme: &Theme, frame: &mut Frame<'_ )) .title(Span::styled( format!(" {} ", crate::t!("panel.relationships_title")), - Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD), + Style::default().fg(theme.fg).add_modifier(Modifier::BOLD), )) .style(Style::default().bg(theme.bg).fg(theme.fg)); @@ -992,12 +980,24 @@ fn render_relationships_panel(app: &mut App, theme: &Theme, frame: &mut Frame<'_ ellipsize(&rel.name, inner_w), name_style, ))); - let parent = format!(" {}.{} ->", rel.parent_table, rel.parent_columns.join(", ")); - lines.push(Line::from(Span::styled(ellipsize(&parent, inner_w), detail_style))); + let parent = format!( + " {}.{} ->", + rel.parent_table, + rel.parent_columns.join(", ") + ); + lines.push(Line::from(Span::styled( + ellipsize(&parent, inner_w), + detail_style, + ))); let child = format!(" {}.{}", rel.child_table, rel.child_columns.join(", ")); - lines.push(Line::from(Span::styled(ellipsize(&child, inner_w), detail_style))); + lines.push(Line::from(Span::styled( + ellipsize(&child, inner_w), + detail_style, + ))); } - let paragraph = Paragraph::new(lines).block(block).scroll((offset as u16, 0)); + let paragraph = Paragraph::new(lines) + .block(block) + .scroll((offset as u16, 0)); frame.render_widget(paragraph, area); } @@ -1022,9 +1022,7 @@ fn render_output_panel(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area .border_style(Style::default().fg(theme.border)) .title(Span::styled( format!(" {} ", crate::t!("panel.output_title")), - Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD), + Style::default().fg(theme.fg).add_modifier(Modifier::BOLD), )) .style(Style::default().bg(theme.bg).fg(theme.fg)); @@ -1132,9 +1130,9 @@ const fn output_span_style(class: OutputStyleClass, theme: &Theme) -> Style { OutputStyleClass::Neutral => Style::new().fg(theme.fg), OutputStyleClass::Efficient => Style::new().fg(theme.plan_efficient), OutputStyleClass::Expensive => Style::new().fg(theme.warning), - OutputStyleClass::AutomaticIndex => Style::new() - .fg(theme.warning) - .add_modifier(Modifier::BOLD), + OutputStyleClass::AutomaticIndex => { + Style::new().fg(theme.warning).add_modifier(Modifier::BOLD) + } // ADR-0038 §4 / §6: de-emphasised text — the `Executing SQL:` // prefix and every category-3 prose line (caveat + the // existing `client_side.*` notes). `theme.muted` is the @@ -1239,9 +1237,7 @@ fn render_output_line<'a>(line: &'a OutputLine, theme: &Theme) -> Line<'a> { &line.text[..prefix_len], Style::default().fg(theme.muted), )); - for run in - crate::input_render::lex_to_runs_in_mode(rest, theme, Mode::Advanced) - { + for run in crate::input_render::lex_to_runs_in_mode(rest, theme, Mode::Advanced) { spans.push(Span::styled( &rest[run.byte_range.0..run.byte_range.1], run.style, @@ -1350,9 +1346,7 @@ fn render_input_panel(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area: Span::raw(" "), Span::styled( label, - Style::default() - .fg(mode_color) - .add_modifier(Modifier::BOLD), + Style::default().fg(mode_color).add_modifier(Modifier::BOLD), ), Span::raw(" "), ]); @@ -1474,7 +1468,10 @@ fn render_input_one_row( if offset > 0 { frame.render_widget( Paragraph::new(Span::styled("<", marker)), - Rect { width: 1, ..text_area }, + Rect { + width: 1, + ..text_area + }, ); } if offset + eff < line_cols { @@ -1536,8 +1533,16 @@ fn render_input_two_rows( // Overflowing both rows reserves a marker column on each row's // outer edge; otherwise both rows use their full text width. let overflow = line_cols >= capacity; - let row0_text_w = if overflow { row0_w.saturating_sub(1) } else { row0_w }; - let row1_text_w = if overflow { row1_w.saturating_sub(1) } else { row1_w }; + let row0_text_w = if overflow { + row0_w.saturating_sub(1) + } else { + row0_w + }; + let row1_text_w = if overflow { + row1_w.saturating_sub(1) + } else { + row1_w + }; let eff_cap = row0_text_w + row1_text_w; let start = offset.min(len); @@ -1552,7 +1557,11 @@ fn render_input_two_rows( ) }; - let row0_x = if overflow { text_area.x + 1 } else { text_area.x }; + let row0_x = if overflow { + text_area.x + 1 + } else { + text_area.x + }; frame.render_widget( Paragraph::new(to_line(&window[..split])), Rect { @@ -1622,10 +1631,7 @@ fn expand_runs_to_cells( /// Convert `StyledRun`s into ratatui `Span`s borrowed from /// `input`. The end-of-input cursor sentinel (empty range) is /// rendered as an inverted space. -fn runs_to_spans<'a>( - input: &'a str, - runs: &[crate::input_render::StyledRun], -) -> Vec> { +fn runs_to_spans<'a>(input: &'a str, runs: &[crate::input_render::StyledRun]) -> Vec> { runs.iter() .map(|r| { if r.byte_range.0 == r.byte_range.1 { @@ -1710,21 +1716,14 @@ fn resolve_hint_lines( } } -fn render_hint_panel( - theme: &Theme, - frame: &mut Frame<'_>, - area: Rect, - lines: Vec>, -) { +fn render_hint_panel(theme: &Theme, frame: &mut Frame<'_>, area: Rect, lines: Vec>) { let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(theme.border)) .title(Span::styled( format!(" {} ", crate::t!("panel.hint_title")), - Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD), + Style::default().fg(theme.fg).add_modifier(Modifier::BOLD), )) .style(Style::default().bg(theme.bg).fg(theme.fg)); @@ -1918,9 +1917,7 @@ fn status_bar_bindings(app: &App) -> Vec<(&'static str, String)> { } fn render_status_bar(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) { - let key_style = Style::default() - .fg(theme.fg) - .add_modifier(Modifier::BOLD); + let key_style = Style::default().fg(theme.fg).add_modifier(Modifier::BOLD); let sep_style = Style::default().fg(theme.muted); let label_style = Style::default().fg(theme.muted); let bar_style = Style::default().bg(theme.bg).fg(theme.muted); @@ -2016,9 +2013,16 @@ mod tests { }; let rendered = render_output_line(&line, &theme); // [system] tag, then the dim prefix, then ≥1 SQL spans. - assert!(rendered.spans.len() >= 3, "tag + prefix + sql: {:?}", rendered.spans); + assert!( + rendered.spans.len() >= 3, + "tag + prefix + sql: {:?}", + rendered.spans + ); assert_eq!(rendered.spans[0].content.as_ref(), "[system] "); - assert_eq!(rendered.spans[1].content.as_ref(), crate::echo::TEACHING_ECHO_LABEL); + assert_eq!( + rendered.spans[1].content.as_ref(), + crate::echo::TEACHING_ECHO_LABEL + ); assert_eq!( rendered.spans[1].style.fg, Some(theme.muted), @@ -2152,17 +2156,41 @@ mod tests { use crate::completion::{Candidate, CandidateKind, ModeClass}; let theme = Theme::dark(); let items = vec![ - Candidate { text: "table".into(), kind: CandidateKind::Keyword, mode: ModeClass::Both }, - Candidate { text: "index".into(), kind: CandidateKind::Keyword, mode: ModeClass::Advanced }, - Candidate { text: "relationship".into(), kind: CandidateKind::Keyword, mode: ModeClass::Simple }, + Candidate { + text: "table".into(), + kind: CandidateKind::Keyword, + mode: ModeClass::Both, + }, + Candidate { + text: "index".into(), + kind: CandidateKind::Keyword, + mode: ModeClass::Advanced, + }, + Candidate { + text: "relationship".into(), + kind: CandidateKind::Keyword, + mode: ModeClass::Simple, + }, ]; let line = render_candidate_line(&items, None, 100, &theme); assert_eq!(line.spans[0].content.as_ref(), "table"); - assert_eq!(line.spans[0].style.fg, Some(theme.tok_keyword), "Both keeps the kind colour"); + assert_eq!( + line.spans[0].style.fg, + Some(theme.tok_keyword), + "Both keeps the kind colour" + ); assert_eq!(line.spans[2].content.as_ref(), "index"); - assert_eq!(line.spans[2].style.fg, Some(theme.mode_advanced), "Advanced → advanced colour"); + assert_eq!( + line.spans[2].style.fg, + Some(theme.mode_advanced), + "Advanced → advanced colour" + ); assert_eq!(line.spans[4].content.as_ref(), "relationship"); - assert_eq!(line.spans[4].style.fg, Some(theme.mode_simple), "Simple → simple colour"); + assert_eq!( + line.spans[4].style.fg, + Some(theme.mode_simple), + "Simple → simple colour" + ); } #[test] @@ -2173,8 +2201,16 @@ mod tests { use crate::completion::{Candidate, CandidateKind, ModeClass}; let theme = Theme::dark(); let items = vec![ - Candidate { text: "values".into(), kind: CandidateKind::Keyword, mode: ModeClass::Advanced }, - Candidate { text: "select".into(), kind: CandidateKind::Keyword, mode: ModeClass::Advanced }, + Candidate { + text: "values".into(), + kind: CandidateKind::Keyword, + mode: ModeClass::Advanced, + }, + Candidate { + text: "select".into(), + kind: CandidateKind::Keyword, + mode: ModeClass::Advanced, + }, ]; let line = render_candidate_line(&items, None, 100, &theme); assert_eq!( @@ -2248,7 +2284,10 @@ mod tests { "the error body is neutral fg, not flooded red", ); assert!( - rendered.spans[1].style.add_modifier.contains(Modifier::BOLD), + rendered.spans[1] + .style + .add_modifier + .contains(Modifier::BOLD), "the error body is bold for weight without the red-wall readability cost", ); } @@ -2509,10 +2548,14 @@ mod tests { "the tail around the cursor must be visible:\n{out}" ); assert!( - !out.lines().any(|l| l.contains("select * from Customers where")), + !out.lines() + .any(|l| l.contains("select * from Customers where")), "the head must be scrolled off:\n{out}" ); - assert!(out.contains('<'), "a left scroll marker signals the hidden head:\n{out}"); + assert!( + out.contains('<'), + "a left scroll marker signals the hidden head:\n{out}" + ); } #[test] @@ -2525,9 +2568,18 @@ mod tests { let theme = Theme::dark(); // Narrow (sidebar hidden, DB1) so the line overflows the field. let out = render_to_string(&mut app, &theme, 60, 24); - assert!(out.contains("select * from"), "head visible at Home:\n{out}"); - assert!(out.contains('>'), "a right scroll marker signals the hidden tail:\n{out}"); - assert!(!out.contains("Wonderland"), "the tail must be scrolled off:\n{out}"); + assert!( + out.contains("select * from"), + "head visible at Home:\n{out}" + ); + assert!( + out.contains('>'), + "a right scroll marker signals the hidden tail:\n{out}" + ); + assert!( + !out.contains("Wonderland"), + "the tail must be scrolled off:\n{out}" + ); } // ---- ADR-0046 DA4: two-row input on tall terminals ----------- @@ -2569,8 +2621,14 @@ mod tests { let theme = Theme::dark(); // Very narrow + tall: two rows, but the line exceeds both. let out = render_to_string(&mut app, &theme, 38, 44); - assert!(out.contains("Wonderland"), "the tail/cursor stays visible:\n{out}"); - assert!(out.contains('<'), "a left marker signals the hidden head:\n{out}"); + assert!( + out.contains("Wonderland"), + "the tail/cursor stays visible:\n{out}" + ); + assert!( + out.contains('<'), + "a left marker signals the hidden head:\n{out}" + ); } #[test] @@ -2644,7 +2702,10 @@ mod tests { /// The `key` column of the strip's bindings, in order. fn strip_keys(app: &App) -> Vec<&'static str> { - status_bar_bindings(app).into_iter().map(|(k, _)| k).collect() + status_bar_bindings(app) + .into_iter() + .map(|(k, _)| k) + .collect() } /// The full rendered strip text (keys + labels + separators). @@ -2659,7 +2720,12 @@ mod tests { fn hint_text(lines: &[Line<'_>]) -> String { lines .iter() - .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::() + }) .collect::>() .join("\n") } @@ -2686,10 +2752,7 @@ mod tests { fn strip_sidebar_focus_state_is_pane_scroll_input() { let mut app = App::new(); app.nav_focus = NavFocus::SidebarTables; - assert_eq!( - strip_keys(&app), - vec!["Ctrl-O", "↑↓/PgUp/PgDn", "Esc"], - ); + assert_eq!(strip_keys(&app), vec!["Ctrl-O", "↑↓/PgUp/PgDn", "Esc"],); // ...and the relationships sidebar is the same state. app.nav_focus = NavFocus::SidebarRelationships; assert_eq!(strip_keys(&app), vec!["Ctrl-O", "↑↓/PgUp/PgDn", "Esc"]); @@ -2802,11 +2865,7 @@ mod tests { assert_eq!(two, vec!["alpha beta", "gamma delta"]); // > max rows: clamp to max, last row ends with an ellipsis, // and every row stays within the width. - let many = clamp_wrapped( - "alpha beta gamma delta epsilon zeta eta theta iota", - 11, - 3, - ); + let many = clamp_wrapped("alpha beta gamma delta epsilon zeta eta theta iota", 11, 3); assert_eq!(many.len(), 3); assert!(many[2].ends_with('…'), "last row ellipsized: {many:?}"); for row in &many { @@ -2931,9 +2990,18 @@ mod tests { app.output.push_back(err); let out = render_to_string(&mut app, &Theme::dark(), 100, 20); - assert!(out.contains("running: drop table Orders"), "pending keeps running::\n{out}"); - assert!(out.contains("create table T with pk ✓"), "ok shows ✓:\n{out}"); - assert!(out.contains("insert into T values (1) ✗"), "err shows ✗:\n{out}"); + assert!( + out.contains("running: drop table Orders"), + "pending keeps running::\n{out}" + ); + assert!( + out.contains("create table T with pk ✓"), + "ok shows ✓:\n{out}" + ); + assert!( + out.contains("insert into T values (1) ✗"), + "err shows ✗:\n{out}" + ); assert!( !out.contains("running: create table"), "a completed echo drops the running: prefix:\n{out}" @@ -2970,7 +3038,10 @@ mod tests { #[test] fn format_snapshot_timestamp_falls_back_on_garbage() { - assert_eq!(format_snapshot_timestamp("not a timestamp"), "not a timestamp"); + assert_eq!( + format_snapshot_timestamp("not a timestamp"), + "not a timestamp" + ); } #[test] @@ -2999,9 +3070,9 @@ mod tests { let theme = Theme::dark(); let out = render_to_string(&mut app, &theme, 120, 30); assert!( - out.lines().any(|l| l.contains( - "This will undo: insert into Customers values (1, 'Oliver Sturm')" - )), + out.lines() + .any(|l| l + .contains("This will undo: insert into Customers values (1, 'Oliver Sturm')")), "command must sit on one row on a wide terminal:\n{out}" ); } @@ -3017,7 +3088,10 @@ mod tests { })); let theme = Theme::dark(); let out = render_to_string(&mut app, &theme, 120, 30); - assert!(out.contains("Snapshot taken"), "capitalized Snapshot:\n{out}"); + assert!( + out.contains("Snapshot taken"), + "capitalized Snapshot:\n{out}" + ); assert!(out.contains("[Y] Yes"), "capitalized Yes:\n{out}"); assert!(out.contains("[N] No"), "capitalized No:\n{out}"); assert!( @@ -3113,8 +3187,14 @@ mod tests { app.schema_cache.table_indexes.insert( "Customers".to_string(), vec![ - IndexEntry { name: "idx_email".to_string(), unique: false }, - IndexEntry { name: "uidx_login".to_string(), unique: true }, + IndexEntry { + name: "idx_email".to_string(), + unique: false, + }, + IndexEntry { + name: "uidx_login".to_string(), + unique: true, + }, ], ); let theme = Theme::dark(); @@ -3123,7 +3203,10 @@ mod tests { assert!(out.contains("Customers"), "table listed:\n{out}"); assert!(out.contains("Orders"), "table listed:\n{out}"); assert!(out.contains("idx_email"), "index nested in panel:\n{out}"); - assert!(out.contains("uidx_login [unique]"), "unique index marked:\n{out}"); + assert!( + out.contains("uidx_login [unique]"), + "unique index marked:\n{out}" + ); } #[test] @@ -3143,10 +3226,19 @@ mod tests { app.tables = vec!["Customers".to_string()]; let theme = Theme::dark(); let narrow = render_to_string(&mut app, &theme, 80, 24); - assert!(!narrow.contains("Tables"), "sidebar hidden at 80 wide:\n{narrow}"); + assert!( + !narrow.contains("Tables"), + "sidebar hidden at 80 wide:\n{narrow}" + ); let wide = render_to_string(&mut app, &theme, 110, 24); - assert!(wide.contains("Tables"), "sidebar shown at 110 wide:\n{wide}"); - assert!(wide.contains("Customers"), "tables listed when shown:\n{wide}"); + assert!( + wide.contains("Tables"), + "sidebar shown at 110 wide:\n{wide}" + ); + assert!( + wide.contains("Customers"), + "tables listed when shown:\n{wide}" + ); } #[test] @@ -3181,7 +3273,10 @@ mod tests { let theme = Theme::dark(); let out = render_to_string(&mut app, &theme, 110, 24); assert!(out.contains("Relationships"), "panel title present:\n{out}"); - assert!(out.contains("Customers_Orders"), "relationship name:\n{out}"); + assert!( + out.contains("Customers_Orders"), + "relationship name:\n{out}" + ); assert!( out.lines().any(|l| l.contains("Customers.id ->")), "parent endpoint, broken at the arrow:\n{out}" @@ -3228,8 +3323,14 @@ mod tests { app.nav_focus = NavFocus::SidebarTables; let focused = render_to_string(&mut app, &theme, 80, 24); - assert!(focused.contains("Tables"), "sidebar revealed in nav mode:\n{focused}"); - assert!(focused.contains("Customers"), "tables in the overlay:\n{focused}"); + assert!( + focused.contains("Tables"), + "sidebar revealed in nav mode:\n{focused}" + ); + assert!( + focused.contains("Customers"), + "tables in the overlay:\n{focused}" + ); assert!( focused.contains("Relationships"), "relationships panel in the overlay:\n{focused}" @@ -3365,7 +3466,9 @@ mod tests { } let backend = TestBackend::new(width, height); let mut terminal = Terminal::new(backend).expect("create terminal"); - terminal.draw(|f| render(app, theme, f)).expect("draw frame"); + terminal + .draw(|f| render(app, theme, f)) + .expect("draw frame"); terminal.backend().buffer().clone() } diff --git a/src/undo.rs b/src/undo.rs index a2df9fa..9dd2158 100644 --- a/src/undo.rs +++ b/src/undo.rs @@ -737,7 +737,11 @@ mod tests { let payload_dirs = fs::read_dir(&store.root) .unwrap() .filter_map(std::result::Result::ok) - .filter(|e| e.file_name().to_str().is_some_and(|n| n.parse::().is_ok())) + .filter(|e| { + e.file_name() + .to_str() + .is_some_and(|n| n.parse::().is_ok()) + }) .count(); assert_eq!(payload_dirs, 2, "ring capped at 2 payloads"); } @@ -766,7 +770,11 @@ mod tests { let payload_dirs = fs::read_dir(&store.root) .unwrap() .filter_map(std::result::Result::ok) - .filter(|e| e.file_name().to_str().is_some_and(|n| n.parse::().is_ok())) + .filter(|e| { + e.file_name() + .to_str() + .is_some_and(|n| n.parse::().is_ok()) + }) .count(); assert_eq!(payload_dirs, 1, "only the surviving undo payload remains"); } @@ -820,6 +828,10 @@ mod tests { fs::create_dir_all(store.payload_dir(41)).unwrap(); stage_finalize(&store, &fx.conn, "cmd"); let meta = store.peek_undo().unwrap().unwrap(); - assert!(meta.id >= 42, "id allocated above the orphan, got {}", meta.id); + assert!( + meta.id >= 42, + "id allocated above the orphan, got {}", + meta.id + ); } } diff --git a/tests/it/case_insensitive_names.rs b/tests/it/case_insensitive_names.rs index fe28129..b751a5f 100644 --- a/tests/it/case_insensitive_names.rs +++ b/tests/it/case_insensitive_names.rs @@ -24,8 +24,7 @@ fn rt() -> tokio::runtime::Runtime { fn open() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let db = Database::open_with_persistence( project.db_path(), Persistence::new(project.path().to_path_buf()), @@ -78,7 +77,10 @@ fn rename_column_with_case_variant_table_keeps_metadata_in_step() { let r = rt(); r.block_on(db.create_table( "Items".to_string(), - vec![ColumnSpec::new("id", Type::Int), ColumnSpec::new("qty", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Int), + ColumnSpec::new("qty", Type::Int), + ], vec!["id".to_string()], Some("create".to_string()), )) @@ -129,7 +131,11 @@ fn insert_with_case_variant_table_persists_and_survives_rebuild() { .block_on(db.query_data("Items".to_string(), None, None)) .expect("query") .rows; - assert_eq!(rows.len(), 1, "the wrong-case insert survived the rebuild (no data loss)"); + assert_eq!( + rows.len(), + 1, + "the wrong-case insert survived the rebuild (no data loss)" + ); assert_eq!(rows[0][1].as_deref(), Some("kept")); } @@ -146,9 +152,19 @@ fn add_column_with_case_variant_table_survives_rebuild() { ); let db = fresh_rebuild(db, &project, &r); - let desc = r.block_on(db.describe_table("Items".to_string())).expect("describe"); - let qty = desc.columns.iter().find(|c| c.name == "qty").expect("qty added"); - assert_eq!(qty.user_type, Some(Type::Int), "qty's user-type survived the rebuild"); + let desc = r + .block_on(db.describe_table("Items".to_string())) + .expect("describe"); + let qty = desc + .columns + .iter() + .find(|c| c.name == "qty") + .expect("qty added"); + assert_eq!( + qty.user_type, + Some(Type::Int), + "qty's user-type survived the rebuild" + ); // The CHECK is intact too (a negative qty is refused under the real table). assert!( r.block_on(db.insert( @@ -175,9 +191,15 @@ fn drop_table_with_case_variant_name_clears_table_and_csv() { insert into Items (id, note) values (1, 'x')\n\ drop table items\n", ); - assert!(!tables(&db, &r).contains(&"Items".to_string()), "the table was dropped"); + assert!( + !tables(&db, &r).contains(&"Items".to_string()), + "the table was dropped" + ); let csv = project.path().join(project::DATA_DIR).join("Items.csv"); - assert!(!csv.exists(), "the CSV was removed despite the case-variant drop"); + assert!( + !csv.exists(), + "the CSV was removed despite the case-variant drop" + ); // A fresh rebuild yields no Items (the metadata/yaml has no orphan). let db = fresh_rebuild(db, &project, &r); @@ -224,12 +246,24 @@ fn add_relationship_with_case_variant_tables_survives_rebuild() { add 1:n relationship from parent.id to child.parent_id\n", ); // The parent's inbound relationship is visible under the stored case. - let p = r.block_on(db.describe_table("Parent".to_string())).expect("describe Parent"); - assert_eq!(p.inbound_relationships.len(), 1, "relationship recorded under the stored case"); + let p = r + .block_on(db.describe_table("Parent".to_string())) + .expect("describe Parent"); + assert_eq!( + p.inbound_relationships.len(), + 1, + "relationship recorded under the stored case" + ); assert_eq!(p.inbound_relationships[0].other_table, "Child"); let db = fresh_rebuild(db, &project, &r); - let p = r.block_on(db.describe_table("Parent".to_string())).expect("describe Parent"); - assert_eq!(p.inbound_relationships.len(), 1, "relationship survived the rebuild"); + let p = r + .block_on(db.describe_table("Parent".to_string())) + .expect("describe Parent"); + assert_eq!( + p.inbound_relationships.len(), + 1, + "relationship survived the rebuild" + ); assert_eq!(p.inbound_relationships[0].other_table, "Child"); } diff --git a/tests/it/column_op_guards.rs b/tests/it/column_op_guards.rs index bc9f43e..4c6cafb 100644 --- a/tests/it/column_op_guards.rs +++ b/tests/it/column_op_guards.rs @@ -24,8 +24,7 @@ fn rt() -> tokio::runtime::Runtime { fn open() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence_and_undo(project.db_path(), persistence, true) .expect("open db with persistence"); @@ -48,7 +47,9 @@ fn make_t_with_check(db: &Database, r: &tokio::runtime::Runtime) { vec!["a < b".to_string()], vec![], false, - Some("create table T (id int primary key, a int, b int, c text, check (a < b))".to_string()), + Some( + "create table T (id int primary key, a int, b int, c text, check (a < b))".to_string(), + ), )) .expect("create T with table CHECK"); } @@ -285,7 +286,10 @@ fn drop_column_covered_by_a_composite_unique_is_refused_with_the_derived_name() .block_on(db.drop_column("T".to_string(), "a".to_string(), false, None)) .expect_err("dropping a composite-UNIQUE column is refused"); let msg = err.friendly_message(); - assert!(msg.contains("unique_a_b"), "names the derived constraint; got: {msg}"); + assert!( + msg.contains("unique_a_b"), + "names the derived constraint; got: {msg}" + ); assert!( msg.contains("drop constraint unique_a_b"), "points at the actionable drop command; got: {msg}" @@ -351,14 +355,24 @@ fn rename_column_with_a_column_level_check_is_refused() { make_t_with_column_checks(&db, &r); // `qty`'s own check `qty >= 0` references qty → refused. assert!( - r.block_on(db.rename_column("T".to_string(), "qty".to_string(), "amount".to_string(), None)) - .is_err(), + r.block_on(db.rename_column( + "T".to_string(), + "qty".to_string(), + "amount".to_string(), + None + )) + .is_err(), "renaming a column with its own column-level CHECK is refused" ); // `price` is referenced by `discount`'s check `discount < price`. assert!( - r.block_on(db.rename_column("T".to_string(), "price".to_string(), "cost".to_string(), None)) - .is_err(), + r.block_on(db.rename_column( + "T".to_string(), + "price".to_string(), + "cost".to_string(), + None + )) + .is_err(), "renaming a column referenced by another column's CHECK is refused" ); // `id` is referenced by no CHECK → rename succeeds. diff --git a/tests/it/compound_fk.rs b/tests/it/compound_fk.rs index 7ab6000..b6f2f94 100644 --- a/tests/it/compound_fk.rs +++ b/tests/it/compound_fk.rs @@ -9,7 +9,7 @@ use rdbms_playground::db::Database; use rdbms_playground::dsl::{ - parse_command, ColumnSpec, Command, ReferentialAction, SqlForeignKey, Type, Value, + ColumnSpec, Command, ReferentialAction, SqlForeignKey, Type, Value, parse_command, }; use rdbms_playground::persistence::Persistence; use rdbms_playground::project; @@ -18,10 +18,8 @@ use rdbms_playground::project; #[test] fn parenthesized_compound_endpoint_parses_to_column_lists() { - let cmd = parse_command( - "add 1:n relationship from Parent.(a, b) to Child.(x, y)", - ) - .expect("parses"); + let cmd = + parse_command("add 1:n relationship from Parent.(a, b) to Child.(x, y)").expect("parses"); match cmd { Command::AddRelationship { parent_table, @@ -41,8 +39,7 @@ fn parenthesized_compound_endpoint_parses_to_column_lists() { #[test] fn single_column_endpoint_still_parses_unparenthesized() { - let cmd = parse_command("add 1:n relationship from Parent.id to Child.pid") - .expect("parses"); + let cmd = parse_command("add 1:n relationship from Parent.id to Child.pid").expect("parses"); match cmd { Command::AddRelationship { parent_columns, @@ -148,7 +145,10 @@ fn sql_create_table_compound_fk_executes_and_enforces() { db.insert( "Region".to_string(), Some(vec!["country".to_string(), "code".to_string()]), - vec![Value::Number("1".to_string()), Value::Number("10".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Number("10".to_string()), + ], None, ) .await @@ -157,7 +157,10 @@ fn sql_create_table_compound_fk_executes_and_enforces() { .insert( "City".to_string(), Some(vec!["country".to_string(), "region_code".to_string()]), - vec![Value::Number("9".to_string()), Value::Number("9".to_string())], + vec![ + Value::Number("9".to_string()), + Value::Number("9".to_string()), + ], None, ) .await; @@ -176,8 +179,7 @@ fn rt() -> tokio::runtime::Runtime { fn open_project_db() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence(project.db_path(), persistence) .expect("open db with persistence"); @@ -241,7 +243,10 @@ fn compound_fk_declares_enforces_and_round_trips() { db.insert( "Region".to_string(), Some(vec!["country".to_string(), "code".to_string()]), - vec![Value::Number("1".to_string()), Value::Number("10".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Number("10".to_string()), + ], None, ) .await @@ -253,7 +258,11 @@ fn compound_fk_declares_enforces_and_round_trips() { "region_code".to_string(), "name".to_string(), ]), - vec![Value::Number("1".to_string()), Value::Number("10".to_string()), Value::Text("Metropolis".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Number("10".to_string()), + Value::Text("Metropolis".to_string()), + ], None, ) .await @@ -266,7 +275,11 @@ fn compound_fk_declares_enforces_and_round_trips() { "region_code".to_string(), "name".to_string(), ]), - vec![Value::Number("9".to_string()), Value::Number("9".to_string()), Value::Text("Nowhere".to_string())], + vec![ + Value::Number("9".to_string()), + Value::Number("9".to_string()), + Value::Text("Nowhere".to_string()), + ], None, ) .await; @@ -360,7 +373,10 @@ fn compound_fk_arity_mismatch_is_refused() { None, ) .await; - assert!(err.is_err(), "mismatched child/parent arity must be refused"); + assert!( + err.is_err(), + "mismatched child/parent arity must be refused" + ); }); } @@ -386,10 +402,8 @@ fn inline_fk_referencing_compound_pk_points_at_table_level_form() { .expect("create Region"); // Parse the inline form so the `inline` flag is set by the grammar. - let cmd = parse_command( - "create table City (country int references Region(country, code))", - ) - .expect("parses"); + let cmd = parse_command("create table City (country int references Region(country, code))") + .expect("parses"); let Command::SqlCreateTable { name, columns, @@ -465,7 +479,10 @@ fn compound_fk_type_mismatch_per_pair_is_refused() { None, ) .await; - assert!(err.is_err(), "a type-incompatible column pair must be refused"); + assert!( + err.is_err(), + "a type-incompatible column pair must be refused" + ); }); } @@ -478,11 +495,8 @@ fn compound_fk_survives_rebuild_from_text() { let path = project.path().to_path_buf(); let rt = rt(); { - let db = Database::open_with_persistence( - project.db_path(), - Persistence::new(path.clone()), - ) - .expect("open db"); + let db = Database::open_with_persistence(project.db_path(), Persistence::new(path.clone())) + .expect("open db"); rt.block_on(async { seed_compound(&db).await; db.add_relationship( @@ -512,7 +526,10 @@ fn compound_fk_survives_rebuild_from_text() { db.insert( "Region".to_string(), Some(vec!["country".to_string(), "code".to_string()]), - vec![Value::Number("1".to_string()), Value::Number("10".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Number("10".to_string()), + ], None, ) .await @@ -521,11 +538,17 @@ fn compound_fk_survives_rebuild_from_text() { .insert( "City".to_string(), Some(vec!["country".to_string(), "region_code".to_string()]), - vec![Value::Number("9".to_string()), Value::Number("9".to_string())], + vec![ + Value::Number("9".to_string()), + Value::Number("9".to_string()), + ], None, ) .await; - assert!(bad.is_err(), "compound FK still enforced after rebuild from text"); + assert!( + bad.is_err(), + "compound FK still enforced after rebuild from text" + ); // Endpoints survived the round-trip intact. let city = db.describe_table("City".to_string()).await.unwrap(); assert_eq!( diff --git a/tests/it/engine_vocabulary_audit.rs b/tests/it/engine_vocabulary_audit.rs index b579135..ebe6f94 100644 --- a/tests/it/engine_vocabulary_audit.rs +++ b/tests/it/engine_vocabulary_audit.rs @@ -32,10 +32,8 @@ use rdbms_playground::event::AppEvent; const FORBIDDEN: &[&str] = &[ // Product names. - "SQLite", "sqlite", - // Crate name. - "rusqlite", - // Engine-specific keywords / idioms. + "SQLite", "sqlite", // Crate name. + "rusqlite", // Engine-specific keywords / idioms. "STRICT", "PRAGMA", ]; @@ -52,9 +50,7 @@ fn engine_vocab_leak(s: &str) -> Option<(&'static str, usize)> { fn assert_clean(label: &str, s: &str) { if let Some((needle, pos)) = engine_vocab_leak(s) { - panic!( - "ADR-0002 leak in {label}: found `{needle}` at byte {pos} in:\n{s}" - ); + panic!("ADR-0002 leak in {label}: found `{needle}` at byte {pos} in:\n{s}"); } } @@ -118,8 +114,7 @@ fn parse_errors_use_no_engine_vocabulary() { "this is not a command", ]; for input in inputs { - let err = parse_command(input) - .expect_err(&format!("expected parse failure for `{input}`")); + let err = parse_command(input).expect_err(&format!("expected parse failure for `{input}`")); let rendered = format!("{err:?}"); assert_clean(&format!("parse error for `{input}`"), &rendered); } diff --git a/tests/it/friendly_enrichment.rs b/tests/it/friendly_enrichment.rs index 30c46b0..78ad04e 100644 --- a/tests/it/friendly_enrichment.rs +++ b/tests/it/friendly_enrichment.rs @@ -18,10 +18,10 @@ use tokio::runtime::Runtime; use rdbms_playground::db::{Database, DbError, SqliteErrorKind}; -use rdbms_playground::dsl::{ - action::ReferentialAction, ColumnSpec, Command, RowFilter, Type, Value, -}; use rdbms_playground::dsl::parser::parse_command; +use rdbms_playground::dsl::{ + ColumnSpec, Command, RowFilter, Type, Value, action::ReferentialAction, +}; use rdbms_playground::runtime::enrich_dsl_failure; fn rt() -> Runtime { @@ -57,7 +57,10 @@ fn enrich_unique_insert_resolves_table_column_value_and_pinpoint() { db.insert( "Customers".to_string(), None, - vec![Value::Number("5".to_string()), Value::Text("Alice".to_string())], + vec![ + Value::Number("5".to_string()), + Value::Text("Alice".to_string()), + ], None, ) .await @@ -86,7 +89,10 @@ fn enrich_unique_insert_resolves_table_column_value_and_pinpoint() { .unwrap_err(); assert!(matches!( err, - DbError::Sqlite { kind: SqliteErrorKind::UniqueViolation, .. } + DbError::Sqlite { + kind: SqliteErrorKind::UniqueViolation, + .. + } )); let facts = enrich_dsl_failure(&db, &cmd, &err).await; @@ -169,7 +175,10 @@ fn enrich_unique_sql_insert_natural_order_resolves_value_via_schema() { db.insert( "Customers".to_string(), None, - vec![Value::Number("5".to_string()), Value::Text("Alice".to_string())], + vec![ + Value::Number("5".to_string()), + Value::Text("Alice".to_string()), + ], None, ) .await @@ -189,7 +198,10 @@ fn enrich_unique_sql_insert_natural_order_resolves_value_via_schema() { else { panic!("expected Command::SqlInsert, got {cmd:?}"); }; - assert!(listed_columns.is_empty(), "natural-order form has no column list"); + assert!( + listed_columns.is_empty(), + "natural-order form has no column list" + ); let err = db .run_sql_insert_with_literals( sql, @@ -204,7 +216,10 @@ fn enrich_unique_sql_insert_natural_order_resolves_value_via_schema() { .unwrap_err(); assert!(matches!( err, - DbError::Sqlite { kind: SqliteErrorKind::UniqueViolation, .. } + DbError::Sqlite { + kind: SqliteErrorKind::UniqueViolation, + .. + } )); let facts = enrich_dsl_failure(&db, &cmd, &err).await; @@ -235,7 +250,10 @@ fn enrich_unique_update_resolves_value_from_assignments() { db.insert( "Customers".to_string(), None, - vec![Value::Number("1".to_string()), Value::Text("Alice".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Text("Alice".to_string()), + ], None, ) .await @@ -243,7 +261,10 @@ fn enrich_unique_update_resolves_value_from_assignments() { db.insert( "Customers".to_string(), None, - vec![Value::Number("2".to_string()), Value::Text("Bob".to_string())], + vec![ + Value::Number("2".to_string()), + Value::Text("Bob".to_string()), + ], None, ) .await @@ -294,7 +315,10 @@ fn enrich_unique_sql_update_resolves_value_from_set_literals() { db.insert( "Customers".to_string(), None, - vec![Value::Number("1".to_string()), Value::Text("Alice".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Text("Alice".to_string()), + ], None, ) .await @@ -302,7 +326,10 @@ fn enrich_unique_sql_update_resolves_value_from_set_literals() { db.insert( "Customers".to_string(), None, - vec![Value::Number("2".to_string()), Value::Text("Bob".to_string())], + vec![ + Value::Number("2".to_string()), + Value::Text("Bob".to_string()), + ], None, ) .await @@ -328,7 +355,10 @@ fn enrich_unique_sql_update_resolves_value_from_set_literals() { .unwrap_err(); assert!(matches!( err, - DbError::Sqlite { kind: SqliteErrorKind::UniqueViolation, .. } + DbError::Sqlite { + kind: SqliteErrorKind::UniqueViolation, + .. + } )); let facts = enrich_dsl_failure(&db, &cmd, &err).await; @@ -666,7 +696,10 @@ fn enrich_fk_delete_resolves_child_table() { db.insert( "Orders".to_string(), None, - vec![Value::Number("1".to_string()), Value::Number("1".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Number("1".to_string()), + ], None, ) .await @@ -708,16 +741,15 @@ fn enrich_check_insert_resolves_table_column_value_and_rule() { ) .await .unwrap(); - let score_spec = match parse_command( - "create table __probe with pk score(int) check (score >= 0)", - ) - .expect("probe create parses") - { - Command::CreateTable { columns, .. } => { - columns.into_iter().next().expect("one column") - } - other => panic!("expected CreateTable, got {other:?}"), - }; + let score_spec = + match parse_command("create table __probe with pk score(int) check (score >= 0)") + .expect("probe create parses") + { + Command::CreateTable { columns, .. } => { + columns.into_iter().next().expect("one column") + } + other => panic!("expected CreateTable, got {other:?}"), + }; db.add_column("Scores".to_string(), score_spec, None) .await .unwrap(); @@ -757,7 +789,9 @@ fn enrich_unsupported_returns_default_facts() { let db = db(); rt().block_on(async { let err = DbError::Unsupported("nope".to_string()); - let cmd = Command::DropTable { name: "X".to_string() }; + let cmd = Command::DropTable { + name: "X".to_string(), + }; let facts = enrich_dsl_failure(&db, &cmd, &err).await; assert!(facts.table.is_none()); assert!(facts.column.is_none()); diff --git a/tests/it/help_command.rs b/tests/it/help_command.rs index b455f60..261611e 100644 --- a/tests/it/help_command.rs +++ b/tests/it/help_command.rs @@ -11,7 +11,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use rdbms_playground::app::App; -use rdbms_playground::dsl::{parse_command, AppCommand, Command}; +use rdbms_playground::dsl::{AppCommand, Command, parse_command}; use rdbms_playground::event::AppEvent; const fn key(code: KeyCode) -> AppEvent { diff --git a/tests/it/iteration2_persistence.rs b/tests/it/iteration2_persistence.rs index 0260113..026a98a 100644 --- a/tests/it/iteration2_persistence.rs +++ b/tests/it/iteration2_persistence.rs @@ -14,9 +14,7 @@ use std::path::Path; use rdbms_playground::db::Database; use rdbms_playground::dsl::{ColumnSpec, ReferentialAction, RowFilter, Type, Value}; use rdbms_playground::persistence::Persistence; -use rdbms_playground::project::{ - self, DATA_DIR, PROJECT_YAML, -}; +use rdbms_playground::project::{self, DATA_DIR, PROJECT_YAML}; fn tempdir() -> tempfile::TempDir { tempfile::tempdir().expect("create tempdir") @@ -33,9 +31,7 @@ fn rt() -> tokio::runtime::Runtime { /// `Database` (with persistence wired) plus the path so the /// test can inspect on-disk state. The project is held alive /// implicitly via the leaked `TempDir` returned alongside. -fn open_project( - data: &tempfile::TempDir, -) -> (project::Project, Database, std::path::PathBuf) { +fn open_project(data: &tempfile::TempDir) -> (project::Project, Database, std::path::PathBuf) { let project = project::open_or_create(None, Some(data.path())).expect("open project"); let path = project.path().to_path_buf(); let persistence = Persistence::new(path.clone()); @@ -72,7 +68,10 @@ fn create_table_writes_yaml_and_history() { }); let yaml = read_yaml(&path); - assert!(yaml.contains("- name: Customers"), "yaml missing table:\n{yaml}"); + assert!( + yaml.contains("- name: Customers"), + "yaml missing table:\n{yaml}" + ); assert!(yaml.contains("primary_key: [id]"), "yaml: {yaml}"); assert!(yaml.contains("type: serial"), "yaml: {yaml}"); assert!(yaml.contains("type: text"), "yaml: {yaml}"); @@ -151,9 +150,15 @@ fn drop_table_removes_its_csv() { .unwrap(); }); - assert!(read_csv(&path, "Customers").is_none(), "CSV should be deleted"); + assert!( + read_csv(&path, "Customers").is_none(), + "CSV should be deleted" + ); let yaml = read_yaml(&path); - assert!(!yaml.contains("- name: Customers"), "table should be gone from yaml:\n{yaml}"); + assert!( + !yaml.contains("- name: Customers"), + "table should be gone from yaml:\n{yaml}" + ); } #[test] @@ -263,7 +268,10 @@ fn create_table_does_not_write_csv_for_empty_table() { // Schema landed in YAML. let yaml = read_yaml(&path); - assert!(yaml.contains("- name: Customers"), "yaml missing table:\n{yaml}"); + assert!( + yaml.contains("- name: Customers"), + "yaml missing table:\n{yaml}" + ); // ...but no CSV until there's data. assert!( read_csv(&path, "Customers").is_none(), @@ -394,7 +402,10 @@ fn project_yaml_carries_relationship_after_add() { }); let yaml = read_yaml(&path); - assert!(yaml.contains("- name: Customers_id_to_Orders_CustId"), "yaml: {yaml}"); + assert!( + yaml.contains("- name: Customers_id_to_Orders_CustId"), + "yaml: {yaml}" + ); assert!(yaml.contains("on_delete: cascade"), "yaml: {yaml}"); assert!(yaml.contains("on_update: no_action"), "yaml: {yaml}"); } diff --git a/tests/it/iteration3_rebuild.rs b/tests/it/iteration3_rebuild.rs index 922811d..4f3511b 100644 --- a/tests/it/iteration3_rebuild.rs +++ b/tests/it/iteration3_rebuild.rs @@ -35,11 +35,8 @@ fn rebuild_restores_schema_only_project() { let project_path = { let project = project::open_or_create(None, Some(data.path())).unwrap(); let path = project.path().to_path_buf(); - let db = Database::open_with_persistence( - project.db_path(), - Persistence::new(path.clone()), - ) - .unwrap(); + let db = Database::open_with_persistence(project.db_path(), Persistence::new(path.clone())) + .unwrap(); rt().block_on(async { db.create_table( "Customers".to_string(), @@ -89,11 +86,8 @@ fn rebuild_restores_rows_from_csv() { let project_path = { let project = project::open_or_create(None, Some(data.path())).unwrap(); let path = project.path().to_path_buf(); - let db = Database::open_with_persistence( - project.db_path(), - Persistence::new(path.clone()), - ) - .unwrap(); + let db = Database::open_with_persistence(project.db_path(), Persistence::new(path.clone())) + .unwrap(); rt().block_on(async { db.create_table( "Customers".to_string(), @@ -157,11 +151,8 @@ fn rebuild_restores_relationships_and_cascade_behaviour() { let project_path = { let project = project::open_or_create(None, Some(data.path())).unwrap(); let path = project.path().to_path_buf(); - let db = Database::open_with_persistence( - project.db_path(), - Persistence::new(path.clone()), - ) - .unwrap(); + let db = Database::open_with_persistence(project.db_path(), Persistence::new(path.clone())) + .unwrap(); rt().block_on(async { db.create_table( "Customers".to_string(), @@ -244,7 +235,11 @@ fn rebuild_restores_relationships_and_cascade_behaviour() { }) .expect("delete"); assert_eq!(result.rows_affected, 1); - assert_eq!(result.cascade.len(), 1, "expected one cascade entry: {result:?}"); + assert_eq!( + result.cascade.len(), + 1, + "expected one cascade entry: {result:?}" + ); assert_eq!(result.cascade[0].child_table, "Orders"); } @@ -256,11 +251,8 @@ fn rebuild_reports_fatal_error_on_bad_csv_row() { let project_path = { let project = project::open_or_create(None, Some(data.path())).unwrap(); let path = project.path().to_path_buf(); - let db = Database::open_with_persistence( - project.db_path(), - Persistence::new(path.clone()), - ) - .unwrap(); + let db = Database::open_with_persistence(project.db_path(), Persistence::new(path.clone())) + .unwrap(); rt().block_on(async { db.create_table( "Numbers".to_string(), @@ -303,13 +295,17 @@ fn rebuild_reports_fatal_error_on_bad_csv_row() { .unwrap(); let err = rt() .block_on(async { - db.rebuild_from_text(project.path().to_path_buf(), None).await + db.rebuild_from_text(project.path().to_path_buf(), None) + .await }) .expect_err("must fail with row-level error"); let msg = format!("{err}"); assert!(msg.contains("row 2"), "msg should name the row: {msg}"); assert!(msg.contains("Numbers"), "msg should name the table: {msg}"); - assert!(msg.contains("integer"), "msg should explain the type mismatch: {msg}"); + assert!( + msg.contains("integer"), + "msg should explain the type mismatch: {msg}" + ); } #[test] @@ -318,11 +314,8 @@ fn rebuild_preserves_created_at_from_yaml() { let project_path = { let project = project::open_or_create(None, Some(data.path())).unwrap(); let path = project.path().to_path_buf(); - let db = Database::open_with_persistence( - project.db_path(), - Persistence::new(path.clone()), - ) - .unwrap(); + let db = Database::open_with_persistence(project.db_path(), Persistence::new(path.clone())) + .unwrap(); rt().block_on(async { db.create_table( "T".to_string(), @@ -371,9 +364,7 @@ fn rebuild_preserves_created_at_from_yaml() { // Trigger any successful command so project.yaml is // rewritten from the now-rebuilt db state. rt().block_on(async { - db.describe_table("T".to_string()) - .await - .unwrap(); + db.describe_table("T".to_string()).await.unwrap(); // describe is read-only; force a rewrite by adding a column. db.add_column( "T".to_string(), @@ -400,11 +391,8 @@ fn rebuild_restores_indexes() { let project_path = { let project = project::open_or_create(None, Some(data.path())).unwrap(); let path = project.path().to_path_buf(); - let db = Database::open_with_persistence( - project.db_path(), - Persistence::new(path.clone()), - ) - .unwrap(); + let db = Database::open_with_persistence(project.db_path(), Persistence::new(path.clone())) + .unwrap(); rt().block_on(async { db.create_table( "Customers".to_string(), @@ -434,7 +422,10 @@ fn rebuild_restores_indexes() { // The index must be recorded in project.yaml — the `.db` is // a derived artifact and gets discarded next. let yaml = fs::read_to_string(project_path.join(project::PROJECT_YAML)).unwrap(); - assert!(yaml.contains("idx_email"), "yaml should record the index:\n{yaml}"); + assert!( + yaml.contains("idx_email"), + "yaml should record the index:\n{yaml}" + ); fs::remove_file(project_path.join(PLAYGROUND_DB)).unwrap(); diff --git a/tests/it/iteration4a_rebuild_command.rs b/tests/it/iteration4a_rebuild_command.rs index ed64a62..381c9fb 100644 --- a/tests/it/iteration4a_rebuild_command.rs +++ b/tests/it/iteration4a_rebuild_command.rs @@ -113,7 +113,10 @@ fn modal_swallows_unrelated_keys() { // field while the modal is up. app.update(key(KeyCode::Char('x'))); assert!(app.input.is_empty(), "modal should swallow key input"); - assert!(app.modal.is_some(), "modal still active after unrelated key"); + assert!( + app.modal.is_some(), + "modal still active after unrelated key" + ); } #[test] @@ -122,11 +125,8 @@ fn rebuild_against_populated_db_wipes_and_reloads() { let project_path = { let project = project::open_or_create(None, Some(data.path())).unwrap(); let path = project.path().to_path_buf(); - let db = Database::open_with_persistence( - project.db_path(), - Persistence::new(path.clone()), - ) - .unwrap(); + let db = Database::open_with_persistence(project.db_path(), Persistence::new(path.clone())) + .unwrap(); rt().block_on(async { db.create_table( "Customers".to_string(), @@ -156,7 +156,9 @@ fn rebuild_against_populated_db_wipes_and_reloads() { // Hand-edit the CSV to introduce a different row content. // Rebuild should pick up the edited content. let csv_path = project_path.join("data").join("Customers.csv"); - let edited = fs::read_to_string(&csv_path).unwrap().replace("Alice", "Edna"); + let edited = fs::read_to_string(&csv_path) + .unwrap() + .replace("Alice", "Edna"); fs::write(&csv_path, edited).unwrap(); // Reopen with persistence (the .db still exists but has diff --git a/tests/it/iteration4b_lifecycle_commands.rs b/tests/it/iteration4b_lifecycle_commands.rs index bcd15dd..669fc77 100644 --- a/tests/it/iteration4b_lifecycle_commands.rs +++ b/tests/it/iteration4b_lifecycle_commands.rs @@ -16,9 +16,9 @@ use rdbms_playground::app::{ App, LoadPickerEntry, LoadPickerModal, LoadPickerSubMode, Modal, PathEntryModal, PathEntryPurpose, }; -use rdbms_playground::event::AppEvent; use rdbms_playground::db::Database; use rdbms_playground::dsl::{ColumnSpec, Type}; +use rdbms_playground::event::AppEvent; use rdbms_playground::persistence::Persistence; use rdbms_playground::project::{ self, Project, ProjectKind, copy_project, safely_delete_temp_project, @@ -462,11 +462,8 @@ fn temp_with_a_table_is_no_longer_unmodified() { let data = tempdir(); let project = project::open_or_create(None, Some(data.path())).unwrap(); let path = project.path().to_path_buf(); - let db = Database::open_with_persistence( - project.db_path(), - Persistence::new(path.clone()), - ) - .unwrap(); + let db = + Database::open_with_persistence(project.db_path(), Persistence::new(path.clone())).unwrap(); let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() diff --git a/tests/it/iteration5_export_import.rs b/tests/it/iteration5_export_import.rs index 62ccee1..a273980 100644 --- a/tests/it/iteration5_export_import.rs +++ b/tests/it/iteration5_export_import.rs @@ -14,8 +14,8 @@ use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use rdbms_playground::action::Action; use rdbms_playground::app::App; use rdbms_playground::archive::{ - default_export_filename, export_project, extract_into, inspect_zip, - next_export_sequence, resolve_import_target, + default_export_filename, export_project, extract_into, inspect_zip, next_export_sequence, + resolve_import_target, }; use rdbms_playground::event::AppEvent; use rdbms_playground::project::{HISTORY_LOG, PLAYGROUND_DB, PROJECT_YAML}; @@ -295,11 +295,9 @@ fn end_to_end_export_then_import_real_project() { // Build a populated source project. let src_path = { let p = project::Project::create_named(&data.path().join("Source")).unwrap(); - let db = Database::open_with_persistence( - p.db_path(), - Persistence::new(p.path().to_path_buf()), - ) - .unwrap(); + let db = + Database::open_with_persistence(p.db_path(), Persistence::new(p.path().to_path_buf())) + .unwrap(); rt().block_on(async { db.create_table( "Customers".to_string(), @@ -362,7 +360,11 @@ fn end_to_end_export_then_import_real_project() { // Round-trip: the inserted row is back. let data_view = rt() - .block_on(async { imported_db.query_data("Customers".to_string(), None, None).await }) + .block_on(async { + imported_db + .query_data("Customers".to_string(), None, None) + .await + }) .expect("query data"); assert_eq!(data_view.rows.len(), 1); // Serial id auto-filled to 1; Name was the inserted value. diff --git a/tests/it/iteration6_resume_history.rs b/tests/it/iteration6_resume_history.rs index d4fe1d4..d4dba27 100644 --- a/tests/it/iteration6_resume_history.rs +++ b/tests/it/iteration6_resume_history.rs @@ -166,7 +166,8 @@ fn hydration_reads_both_ok_and_err_records() { let project = Project::create_temp(tmp.path()).unwrap(); let p = Persistence::new(project.path().to_path_buf()); p.append_history("create table A with pk", false).unwrap(); - p.append_history_failure("insert into A (1, 2, 3)", false).unwrap(); + p.append_history_failure("insert into A (1, 2, 3)", false) + .unwrap(); p.append_history("show data A", false).unwrap(); let entries = p.read_recent_history(10).unwrap(); assert_eq!( diff --git a/tests/it/m2n.rs b/tests/it/m2n.rs index 189c37d..b611aa2 100644 --- a/tests/it/m2n.rs +++ b/tests/it/m2n.rs @@ -8,7 +8,7 @@ use rdbms_playground::db::Database; use rdbms_playground::dsl::command::RowFilter; -use rdbms_playground::dsl::{parse_command, ColumnSpec, Command, Type, Value}; +use rdbms_playground::dsl::{ColumnSpec, Command, Type, Value, parse_command}; use rdbms_playground::persistence::Persistence; use rdbms_playground::project::{self, PLAYGROUND_DB}; @@ -22,8 +22,11 @@ fn rt() -> tokio::runtime::Runtime { fn open() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("tempdir"); let project = project::open_or_create(None, Some(dir.path())).expect("project"); - let db = Database::open_with_persistence(project.db_path(), Persistence::new(project.path().to_path_buf())) - .expect("db"); + let db = Database::open_with_persistence( + project.db_path(), + Persistence::new(project.path().to_path_buf()), + ) + .expect("db"); (project, db, dir) } @@ -45,7 +48,10 @@ fn open_with_undo() -> (project::Project, Database, tempfile::TempDir) { async fn serial_pk_table(db: &Database, name: &str) { db.create_table( name.to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("label", Type::Text)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("label", Type::Text), + ], vec!["id".to_string()], None, ) @@ -84,7 +90,9 @@ fn parses_with_as_name() { match parse_command("create m:n relationship from Students to Courses as Enrollments") .expect("parses") { - Command::CreateM2nRelationship { name, .. } => assert_eq!(name.as_deref(), Some("Enrollments")), + Command::CreateM2nRelationship { name, .. } => { + assert_eq!(name.as_deref(), Some("Enrollments")) + } other => panic!("expected CreateM2nRelationship, got {other:?}"), } } @@ -104,12 +112,21 @@ fn generates_junction_with_compound_pk_and_two_enforced_fks() { // Auto-named `Students_Courses` exists. let tables = db.list_tables().await.unwrap(); - assert!(tables.contains(&"Students_Courses".to_string()), "tables: {tables:?}"); + assert!( + tables.contains(&"Students_Courses".to_string()), + "tables: {tables:?}" + ); // Two FK columns, both part of the compound PK. - let desc = db.describe_table("Students_Courses".to_string()).await.unwrap(); - let cols: Vec<(&str, bool)> = - desc.columns.iter().map(|c| (c.name.as_str(), c.primary_key)).collect(); + let desc = db + .describe_table("Students_Courses".to_string()) + .await + .unwrap(); + let cols: Vec<(&str, bool)> = desc + .columns + .iter() + .map(|c| (c.name.as_str(), c.primary_key)) + .collect(); assert_eq!( cols, vec![("Students_id", true), ("Courses_id", true)], @@ -124,7 +141,10 @@ fn generates_junction_with_compound_pk_and_two_enforced_fks() { db.insert( "Students_Courses".to_string(), Some(vec!["Students_id".to_string(), "Courses_id".to_string()]), - vec![Value::Number("1".to_string()), Value::Number("1".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Number("1".to_string()), + ], None, ) .await @@ -134,21 +154,33 @@ fn generates_junction_with_compound_pk_and_two_enforced_fks() { .insert( "Students_Courses".to_string(), Some(vec!["Students_id".to_string(), "Courses_id".to_string()]), - vec![Value::Number("1".to_string()), Value::Number("1".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Number("1".to_string()), + ], None, ) .await; - assert!(dup.is_err(), "duplicate (Students_id, Courses_id) must be refused"); + assert!( + dup.is_err(), + "duplicate (Students_id, Courses_id) must be refused" + ); // A link to a non-existent parent is refused by the FK. let orphan = db .insert( "Students_Courses".to_string(), Some(vec!["Students_id".to_string(), "Courses_id".to_string()]), - vec![Value::Number("1".to_string()), Value::Number("99".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Number("99".to_string()), + ], None, ) .await; - assert!(orphan.is_err(), "link to a non-existent Course must be refused"); + assert!( + orphan.is_err(), + "link to a non-existent Course must be refused" + ); }); } @@ -167,7 +199,10 @@ fn as_name_overrides_the_junction_table_name() { .await .expect("create m:n as Enrollments"); let tables = db.list_tables().await.unwrap(); - assert!(tables.contains(&"Enrollments".to_string()), "tables: {tables:?}"); + assert!( + tables.contains(&"Enrollments".to_string()), + "tables: {tables:?}" + ); assert!(!tables.contains(&"Students_Courses".to_string())); }); } @@ -179,7 +214,10 @@ fn compound_parent_pk_contributes_one_fk_column_each() { // Sections has a 2-column PK (course_id, term). db.create_table( "Sections".to_string(), - vec![ColumnSpec::new("course_id", Type::Int), ColumnSpec::new("term", Type::Int)], + vec![ + ColumnSpec::new("course_id", Type::Int), + ColumnSpec::new("term", Type::Int), + ], vec!["course_id".to_string(), "term".to_string()], None, ) @@ -191,11 +229,20 @@ fn compound_parent_pk_contributes_one_fk_column_each() { .await .expect("create m:n"); - let desc = db.describe_table("Students_Sections".to_string()).await.unwrap(); + let desc = db + .describe_table("Students_Sections".to_string()) + .await + .unwrap(); let names: Vec<&str> = desc.columns.iter().map(|c| c.name.as_str()).collect(); - assert_eq!(names, vec!["Students_id", "Sections_course_id", "Sections_term"]); + assert_eq!( + names, + vec!["Students_id", "Sections_course_id", "Sections_term"] + ); // All three form the compound PK. - assert!(desc.columns.iter().all(|c| c.primary_key), "all columns are PK: {names:?}"); + assert!( + desc.columns.iter().all(|c| c.primary_key), + "all columns are PK: {names:?}" + ); }); } @@ -213,16 +260,28 @@ fn deleting_a_parent_cascades_to_the_junction() { db.insert( "Students_Courses".to_string(), Some(vec!["Students_id".to_string(), "Courses_id".to_string()]), - vec![Value::Number("1".to_string()), Value::Number("1".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Number("1".to_string()), + ], None, ) .await .unwrap(); // Deleting the student cascades to the junction (ON DELETE CASCADE). - db.delete("Students".to_string(), RowFilter::AllRows, None).await.unwrap(); - let rows = db.query_data("Students_Courses".to_string(), None, None).await.unwrap(); - assert!(rows.rows.is_empty(), "junction rows should cascade-delete, got {:?}", rows.rows); + db.delete("Students".to_string(), RowFilter::AllRows, None) + .await + .unwrap(); + let rows = db + .query_data("Students_Courses".to_string(), None, None) + .await + .unwrap(); + assert!( + rows.rows.is_empty(), + "junction rows should cascade-delete, got {:?}", + rows.rows + ); }); } @@ -242,15 +301,26 @@ fn create_m2n_is_one_undo_step() { ) .await .unwrap(); - assert!(db.list_tables().await.unwrap().contains(&"Students_Courses".to_string())); + assert!( + db.list_tables() + .await + .unwrap() + .contains(&"Students_Courses".to_string()) + ); // One undo removes the junction table AND both relationships. db.undo().await.unwrap(); let tables = db.list_tables().await.unwrap(); - assert!(!tables.contains(&"Students_Courses".to_string()), "undo should remove the junction: {tables:?}"); + assert!( + !tables.contains(&"Students_Courses".to_string()), + "undo should remove the junction: {tables:?}" + ); // The parents' relationships are gone too (the junction held them). let students = db.describe_table("Students".to_string()).await.unwrap(); - assert!(students.inbound_relationships.is_empty(), "no leftover relationship after undo"); + assert!( + students.inbound_relationships.is_empty(), + "no leftover relationship after undo" + ); }); } @@ -265,7 +335,10 @@ fn self_referential_m2n_is_refused() { .create_m2n_relationship("Users".to_string(), "Users".to_string(), None, None) .await .expect_err("self m:n must be refused"); - assert!(format!("{err}").contains("two different tables"), "got: {err}"); + assert!( + format!("{err}").contains("two different tables"), + "got: {err}" + ); }); } @@ -275,11 +348,19 @@ fn missing_parent_table_is_refused() { rt().block_on(async { serial_pk_table(&db, "Students").await; let err = db - .create_m2n_relationship("Students".to_string(), "Nonexistent".to_string(), None, None) + .create_m2n_relationship( + "Students".to_string(), + "Nonexistent".to_string(), + None, + None, + ) .await .expect_err("a missing parent table must be refused"); // The standard "no such table" guard (require_canonical_table). - assert!(format!("{err}").to_lowercase().contains("no such table"), "got: {err}"); + assert!( + format!("{err}").to_lowercase().contains("no such table"), + "got: {err}" + ); }); } @@ -297,7 +378,10 @@ fn junction_name_collision_is_refused() { .create_m2n_relationship("Students".to_string(), "Courses".to_string(), None, None) .await .expect_err("a junction-name collision must be refused"); - assert!(format!("{err}").to_lowercase().contains("exist"), "got: {err}"); + assert!( + format!("{err}").to_lowercase().contains("exist"), + "got: {err}" + ); }); } @@ -314,15 +398,26 @@ fn the_junction_can_be_renamed() { db.create_m2n_relationship("Students".to_string(), "Courses".to_string(), None, None) .await .unwrap(); - db.rename_table("Students_Courses".to_string(), "Enrollments".to_string(), None) - .await - .expect("rename the junction"); + db.rename_table( + "Students_Courses".to_string(), + "Enrollments".to_string(), + None, + ) + .await + .expect("rename the junction"); let tables = db.list_tables().await.unwrap(); - assert!(tables.contains(&"Enrollments".to_string()), "tables: {tables:?}"); + assert!( + tables.contains(&"Enrollments".to_string()), + "tables: {tables:?}" + ); assert!(!tables.contains(&"Students_Courses".to_string())); // Both relationships survive the rename (rebuild-preserving). let desc = db.describe_table("Enrollments".to_string()).await.unwrap(); - assert_eq!(desc.outbound_relationships.len(), 2, "FKs preserved across rename"); + assert_eq!( + desc.outbound_relationships.len(), + 2, + "FKs preserved across rename" + ); }); } @@ -355,16 +450,33 @@ fn junction_survives_save_and_rebuild() { // Discard the derived .db so the next open rebuilds from text. std::fs::remove_file(project_path.join(PLAYGROUND_DB)).unwrap(); let project = project::Project::open(&project_path).unwrap(); - let db = - Database::open_with_persistence(project.db_path(), Persistence::new(project.path().to_path_buf())) - .unwrap(); + let db = Database::open_with_persistence( + project.db_path(), + Persistence::new(project.path().to_path_buf()), + ) + .unwrap(); rt().block_on(async { - db.rebuild_from_text(project.path().to_path_buf(), None).await.expect("rebuild"); + db.rebuild_from_text(project.path().to_path_buf(), None) + .await + .expect("rebuild"); let tables = db.list_tables().await.unwrap(); - assert!(tables.contains(&"Students_Courses".to_string()), "junction survived: {tables:?}"); - let desc = db.describe_table("Students_Courses".to_string()).await.unwrap(); - assert_eq!(desc.outbound_relationships.len(), 2, "both FKs reconstructed"); - assert!(desc.columns.iter().all(|c| c.primary_key), "compound PK reconstructed"); + assert!( + tables.contains(&"Students_Courses".to_string()), + "junction survived: {tables:?}" + ); + let desc = db + .describe_table("Students_Courses".to_string()) + .await + .unwrap(); + assert_eq!( + desc.outbound_relationships.len(), + 2, + "both FKs reconstructed" + ); + assert!( + desc.columns.iter().all(|c| c.primary_key), + "compound PK reconstructed" + ); }); } @@ -387,7 +499,12 @@ fn as_an_internal_name_is_refused() { .await .expect_err("an internal junction name must be refused"); assert!(format!("{err}").contains("no such table"), "got: {err}"); - assert!(!db.list_tables().await.unwrap().contains(&"__rdbms_evil".to_string())); + assert!( + !db.list_tables() + .await + .unwrap() + .contains(&"__rdbms_evil".to_string()) + ); }); } @@ -442,7 +559,10 @@ fn read_all_relationships_returns_the_junction_relationships() { ); // Both have the junction (Students_Courses) as their child. for r in &rels { - assert_eq!(r.child_table, "Students_Courses", "child is the junction: {r:?}"); + assert_eq!( + r.child_table, "Students_Courses", + "child is the junction: {r:?}" + ); } // One points back to each parent. let parents: std::collections::BTreeSet<&str> = diff --git a/tests/it/main.rs b/tests/it/main.rs index cbc6d4b..8383378 100644 --- a/tests/it/main.rs +++ b/tests/it/main.rs @@ -24,6 +24,7 @@ mod parse_error_pedagogy; mod project_lifecycle; mod replay_command; mod seed; +mod show_list; mod sql_alter_table; mod sql_create_index; mod sql_create_table; @@ -34,6 +35,5 @@ mod sql_drop_table; mod sql_insert; mod sql_select; mod sql_update; -mod show_list; mod undo_snapshots; mod walking_skeleton; diff --git a/tests/it/parse_error_pedagogy.rs b/tests/it/parse_error_pedagogy.rs index 919c07a..47a9588 100644 --- a/tests/it/parse_error_pedagogy.rs +++ b/tests/it/parse_error_pedagogy.rs @@ -54,10 +54,7 @@ fn error_lines_for(input: &str) -> Vec { } fn dump(input: &str, lines: &[String]) -> String { - format!( - "INPUT: {input:?}\nERROR LINES:\n{}", - lines.join("\n"), - ) + format!("INPUT: {input:?}\nERROR LINES:\n{}", lines.join("\n"),) } /// The simple-mode near-miss matrix (ADR-0042 §1). Each row is a @@ -71,57 +68,228 @@ fn near_miss_matrix_simple_mode() { // app-lifecycle arg errors. The arg-less commands all reject // trailing junk with "expected end of input" + their usage // (audited 2026-06-05); locked here as regression insurance. - ("quit now", &["after `quit`, expected end of input", " quit"]), + ( + "quit now", + &["after `quit`, expected end of input", " quit"], + ), // `help` now takes an optional single-word topic (H3), so // `help foo` parses (topic lookup); only a *multi-word* // topic is the near-miss that rejects trailing junk. - ("help foo bar", &["after `help foo`, expected end of input", "help []"]), - ("rebuild now", &["after `rebuild`, expected end of input", " rebuild"]), + ( + "help foo bar", + &[ + "after `help foo`, expected end of input", + "help []", + ], + ), + ( + "rebuild now", + &["after `rebuild`, expected end of input", " rebuild"], + ), ("new foo", &["after `new`, expected end of input", " new"]), - ("load foo", &["after `load`, expected end of input", " load"]), - ("undo foo", &["after `undo`, expected end of input", " undo"]), - ("redo foo", &["after `redo`, expected end of input", " redo"]), - ("export foo bar", &["after `export foo`, expected end of input", "export []"]), - ("import a b c", &["after `import a`, expected end of input", "import "]), - ("save sideways", &["after `save`, expected end of input", "save | save as"]), - ("mode sideways", &["unknown mode 'sideways'", "mode simple | mode advanced"]), - ("messages louder", &["unknown messages mode 'louder'", "messages short"]), - ("copy everything", &["unknown copy target 'everything'", "copy all"]), + ( + "load foo", + &["after `load`, expected end of input", " load"], + ), + ( + "undo foo", + &["after `undo`, expected end of input", " undo"], + ), + ( + "redo foo", + &["after `redo`, expected end of input", " redo"], + ), + ( + "export foo bar", + &[ + "after `export foo`, expected end of input", + "export []", + ], + ), + ( + "import a b c", + &[ + "after `import a`, expected end of input", + "import ", + ], + ), + ( + "save sideways", + &["after `save`, expected end of input", "save | save as"], + ), + ( + "mode sideways", + &["unknown mode 'sideways'", "mode simple | mode advanced"], + ), + ( + "messages louder", + &["unknown messages mode 'louder'", "messages short"], + ), + ( + "copy everything", + &["unknown copy target 'everything'", "copy all"], + ), // DDL bare + missing-slot - ("create", &["after `create`, expected `table`", "create table with pk"]), - ("create table", &["after `create table`, expected identifier", "create table with pk"]), - ("create table T", &["with pk", "create table with pk"]), + ( + "create", + &[ + "after `create`, expected `table`", + "create table with pk", + ], + ), + ( + "create table", + &[ + "after `create table`, expected identifier", + "create table with pk", + ], + ), + ( + "create table T", + &["with pk", "create table with pk"], + ), // G1: relationship cardinality reads as the named construct. - ("add", &["after `add`, expected `column`, `1:n relationship`", "add 1:n relationship"]), - ("drop table", &["after `drop table`, expected table name", "drop table "]), - ("add column", &["after `add column`, expected table name", "add column [to] [table]"]), - ("rename", &["after `rename`, expected `column`", "rename column [in] [table]"]), - ("rename column", &["after `rename column`, expected table name", "rename column [in] [table]"]), - ("change", &["after `change`, expected `column`", "change column [in] [table]"]), - ("change column", &["after `change column`, expected table name", "change column [in] [table]"]), + ( + "add", + &[ + "after `add`, expected `column`, `1:n relationship`", + "add 1:n relationship", + ], + ), + ( + "drop table", + &[ + "after `drop table`, expected table name", + "drop table ", + ], + ), + ( + "add column", + &[ + "after `add column`, expected table name", + "add column [to] [table]", + ], + ), + ( + "rename", + &[ + "after `rename`, expected `column`", + "rename column [in] [table]", + ], + ), + ( + "rename column", + &[ + "after `rename column`, expected table name", + "rename column [in] [table]", + ], + ), + ( + "change", + &[ + "after `change`, expected `column`", + "change column [in] [table]", + ], + ), + ( + "change column", + &[ + "after `change column`, expected table name", + "change column [in] [table]", + ], + ), // data bare + missing-clause - ("insert", &["after `insert`, expected `into`", "insert into
"]), - ("insert into", &["after `insert into`, expected table name", "insert into
"]), - ("insert into T", &["after `insert into T`, expected `values` or `(`", "insert into
"]), - ("update", &["after `update`, expected table name", "update
set"]), - ("update T", &["after `update T`, expected `set`", "update
set"]), - ("update T set x=1", &["expected `where` or `--all-rows`", "update
set"]), - ("delete", &["after `delete`, expected `from`", "delete from
"]), - ("delete from", &["after `delete from`, expected table name", "delete from
"]), - ("delete from T", &["expected `where` or `--all-rows`", "delete from
"]), - ("seed", &["after `seed`, expected table name", "seed
[count]"]), + ( + "insert", + &["after `insert`, expected `into`", "insert into
"], + ), + ( + "insert into", + &[ + "after `insert into`, expected table name", + "insert into
", + ], + ), + ( + "insert into T", + &[ + "after `insert into T`, expected `values` or `(`", + "insert into
", + ], + ), + ( + "update", + &["after `update`, expected table name", "update
set"], + ), + ( + "update T", + &["after `update T`, expected `set`", "update
set"], + ), + ( + "update T set x=1", + &["expected `where` or `--all-rows`", "update
set"], + ), + ( + "delete", + &["after `delete`, expected `from`", "delete from
"], + ), + ( + "delete from", + &[ + "after `delete from`, expected table name", + "delete from
", + ], + ), + ( + "delete from T", + &["expected `where` or `--all-rows`", "delete from
"], + ), + ( + "seed", + &["after `seed`, expected table name", "seed
[count]"], + ), // Phase 2 (ADR-0048 D2/D1): malformed `set` clause + column-fill. - ("seed T set", &["after `seed T set`, expected column name", "seed
."]), + ( + "seed T set", + &[ + "after `seed T set`, expected column name", + "seed
.", + ], + ), ( "seed T set role", - &["after `seed T set role`, expected `=`, `in`, `between`, or `as`", "seed
."], + &[ + "after `seed T set role`, expected `=`, `in`, `between`, or `as`", + "seed
.", + ], + ), + ( + "seed T.", + &[ + "after `seed T.`, expected column name", + "seed
.", + ], + ), + ( + "replay", + &[ + "after `replay`, expected string literal or path", + "replay ", + ], + ), + ( + "explain", + &[ + "after `explain`, expected `show`, `update`, or `delete`", + "explain show data", + ], ), - ("seed T.", &["after `seed T.`, expected column name", "seed
."]), - ("replay", &["after `replay`, expected string literal or path", "replay "]), - ("explain", &["after `explain`, expected `show`, `update`, or `delete`", "explain show data"]), // advanced-only entry word typed in simple mode → "this is SQL" rail ("select * from T", &["`select` is SQL", "mode advanced"]), - ("alter table T add column c int", &["`alter` is SQL", "mode advanced"]), + ( + "alter table T add column c int", + &["`alter` is SQL", "mode advanced"], + ), ]; for (input, needles) in matrix { let lines = error_lines_for(input); @@ -164,26 +332,160 @@ fn near_miss_matrix_committed_multiforms() { // (input, advanced?, required-substrings) let matrix: &[(&str, bool, &[&str])] = &[ // add / drop multi-forms (simple) - ("add index", false, &["after `add index`, expected `on` or `as`", "add index [as ] on"]), - ("add index on T", false, &["after `add index on T`, expected `(`", "add index [as ] on"]), - ("add constraint", false, &["after `add constraint`, expected `not`, `unique`, `default`, or `check`", "add constraint not null to"]), - ("add constraint not null", false, &["after `add constraint not null`, expected `to`", "add constraint not null to"]), - ("add 1:n relationship", false, &["after `add 1:n relationship`, expected `from` or `as`", "add 1:n relationship"]), - ("add 1:n relationship from", false, &["after `add 1:n relationship from`, expected table name", "from ."]), - ("drop constraint", false, &["after `drop constraint`, expected `not`, `unique`, `default`, or `check`", "drop constraint (not null"]), - ("drop constraint not null", false, &["after `drop constraint not null`, expected `from`", "drop constraint (not null"]), - ("drop index", false, &["after `drop index`, expected `on` or index name", "drop index ", "drop index on
"]), - ("drop index on T", false, &["after `drop index on T`, expected `(`", "drop index on
"]), - ("drop relationship", false, &["after `drop relationship`, expected `from` or relationship name", "drop relationship "]), - ("show table", false, &["after `show table`, expected table name", "show table
"]), - ("show relationship", false, &["after `show relationship`, expected relationship name", "show relationship "]), - ("show index", false, &["after `show index`, expected index name", "show index "]), - ("change column in table T: c", false, &["after `change column in table T: c`, expected `(`", "change column [in] [table]"]), + ( + "add index", + false, + &[ + "after `add index`, expected `on` or `as`", + "add index [as ] on", + ], + ), + ( + "add index on T", + false, + &[ + "after `add index on T`, expected `(`", + "add index [as ] on", + ], + ), + ( + "add constraint", + false, + &[ + "after `add constraint`, expected `not`, `unique`, `default`, or `check`", + "add constraint not null to", + ], + ), + ( + "add constraint not null", + false, + &[ + "after `add constraint not null`, expected `to`", + "add constraint not null to", + ], + ), + ( + "add 1:n relationship", + false, + &[ + "after `add 1:n relationship`, expected `from` or `as`", + "add 1:n relationship", + ], + ), + ( + "add 1:n relationship from", + false, + &[ + "after `add 1:n relationship from`, expected table name", + "from .", + ], + ), + ( + "drop constraint", + false, + &[ + "after `drop constraint`, expected `not`, `unique`, `default`, or `check`", + "drop constraint (not null", + ], + ), + ( + "drop constraint not null", + false, + &[ + "after `drop constraint not null`, expected `from`", + "drop constraint (not null", + ], + ), + ( + "drop index", + false, + &[ + "after `drop index`, expected `on` or index name", + "drop index ", + "drop index on
", + ], + ), + ( + "drop index on T", + false, + &[ + "after `drop index on T`, expected `(`", + "drop index on
", + ], + ), + ( + "drop relationship", + false, + &[ + "after `drop relationship`, expected `from` or relationship name", + "drop relationship ", + ], + ), + ( + "show table", + false, + &[ + "after `show table`, expected table name", + "show table
", + ], + ), + ( + "show relationship", + false, + &[ + "after `show relationship`, expected relationship name", + "show relationship ", + ], + ), + ( + "show index", + false, + &[ + "after `show index`, expected index name", + "show index ", + ], + ), + ( + "change column in table T: c", + false, + &[ + "after `change column in table T: c`, expected `(`", + "change column [in] [table]", + ], + ), // advanced committed multi-forms - ("create index on", true, &["after `create index on`, expected table name", "create [unique] index"]), - ("create unique index", true, &["after `create unique index`, expected `on`, identifier, or `if`", "create [unique] index"]), - ("alter table T add", true, &["after `alter table T add`, expected `column`, `constraint`, `check`, `unique`, `foreign`, or `primary`", "alter table
add column"]), - ("alter table T drop", true, &["after `alter table T drop`, expected `column` or `constraint`", "alter table
drop column"]), + ( + "create index on", + true, + &[ + "after `create index on`, expected table name", + "create [unique] index", + ], + ), + ( + "create unique index", + true, + &[ + "after `create unique index`, expected `on`, identifier, or `if`", + "create [unique] index", + ], + ), + ( + "alter table T add", + true, + &[ + "after `alter table T add`, expected `column`, `constraint`, `check`, `unique`, `foreign`, or `primary`", + "alter table
add column", + ], + ), + ( + "alter table T drop", + true, + &[ + "after `alter table T drop`, expected `column` or `constraint`", + "alter table
drop column", + ], + ), ]; for (input, advanced, needles) in matrix { let lines = if *advanced { @@ -265,7 +567,10 @@ fn advanced_mode_usage_block_shows_sql_and_dsl_forms() { // (mode-primary first). let sql_at = joined.find("create table [if not exists]").unwrap(); let dsl_at = joined.find("create table with pk").unwrap(); - assert!(sql_at < dsl_at, "SQL form should precede the DSL form\n{dump_msg}"); + assert!( + sql_at < dsl_at, + "SQL form should precede the DSL form\n{dump_msg}" + ); } #[test] @@ -307,23 +612,94 @@ fn advanced_cross_join_with_on_teaches_no_on_clause() { fn near_miss_matrix_advanced_mode() { let matrix: &[(&str, &[&str])] = &[ // SQL select / with (G2, G4) - ("select", &["expected a projection: `*`, a column, or an expression", "select (* |"]), - ("select * from", &["after `select * from`, expected table name", "select (* |"]), - ("with", &["after `with`, expected identifier or `recursive`", "with [recursive]", "as ("]), + ( + "select", + &[ + "expected a projection: `*`, a column, or an expression", + "select (* |", + ], + ), + ( + "select * from", + &["after `select * from`, expected table name", "select (* |"], + ), + ( + "with", + &[ + "after `with`, expected identifier or `recursive`", + "with [recursive]", + "as (", + ], + ), // create / drop / alter — SQL forms AND the still-valid DSL // fallback forms, SQL-primary first (G3). - ("create", &["after `create`, expected `table`", "create table [if not exists]", "create [unique] index", "create table with pk"]), - ("create table", &["after `create table`, expected identifier or `if`", "create table [if not exists]"]), - ("create index", &["after `create index`, expected `on`", "create [unique] index"]), - ("drop", &["after `drop`, expected `table`", "drop table [if exists]", "drop column [from]", "drop relationship"]), - ("alter", &["after `alter`, expected `table`", "alter table
add column"]), - ("alter table T", &["expected `add`, `drop`, `rename`, or `alter`", "alter table
"]), + ( + "create", + &[ + "after `create`, expected `table`", + "create table [if not exists]", + "create [unique] index", + "create table with pk", + ], + ), + ( + "create table", + &[ + "after `create table`, expected identifier or `if`", + "create table [if not exists]", + ], + ), + ( + "create index", + &[ + "after `create index`, expected `on`", + "create [unique] index", + ], + ), + ( + "drop", + &[ + "after `drop`, expected `table`", + "drop table [if exists]", + "drop column [from]", + "drop relationship", + ], + ), + ( + "alter", + &[ + "after `alter`, expected `table`", + "alter table
add column", + ], + ), + ( + "alter table T", + &[ + "expected `add`, `drop`, `rename`, or `alter`", + "alter table
", + ], + ), // shared insert/update/delete — must show usage, not the // available-commands fallback (regression guard for the // empty-usage_ids SQL nodes). - ("insert into T", &["after `insert into T`, expected `values`, `with`, `select`, or `(`", "insert into
"]), - ("update T", &["after `update T`, expected `set`", "update
set"]), - ("delete from", &["after `delete from`, expected table name", "delete from
"]), + ( + "insert into T", + &[ + "after `insert into T`, expected `values`, `with`, `select`, or `(`", + "insert into
", + ], + ), + ( + "update T", + &["after `update T`, expected `set`", "update
set"], + ), + ( + "delete from", + &[ + "after `delete from`, expected table name", + "delete from
", + ], + ), ]; for (input, needles) in matrix { let lines = advanced_error_lines_for(input); @@ -365,7 +741,9 @@ fn with_alone_renders_cte_usage_not_select() { .collect(); let dump_msg = dump("with", &lines); assert!( - lines.iter().any(|l| l.trim_start().starts_with("with ") && l.contains("as (")), + lines + .iter() + .any(|l| l.trim_start().starts_with("with ") && l.contains("as (")), "missing CTE-specific `with … as (…)` usage template\n{dump_msg}", ); } @@ -383,7 +761,9 @@ fn create_alone_renders_create_table_usage() { "missing usage: header\n{dump_msg}", ); assert!( - lines.iter().any(|l| l.contains("create table") && l.contains("with pk")), + lines + .iter() + .any(|l| l.contains("create table") && l.contains("with pk")), "missing create_table usage template\n{dump_msg}", ); } @@ -464,8 +844,7 @@ fn unknown_command_falls_back_to_available_commands_list() { .unwrap_or_else(|| panic!("missing available commands line\n{dump_msg}")); // The list must include all ten command-entry keywords. for cmd in [ - "add", "change", "create", "delete", "drop", "insert", - "rename", "replay", "show", "update", + "add", "change", "create", "delete", "drop", "insert", "rename", "replay", "show", "update", ] { assert!( available.contains(&format!("`{cmd}`")), @@ -543,8 +922,3 @@ fn caret_aligns_under_offending_token() { "caret should sit at column 9 (under `f` of `frobulate` after the `running: ` prefix); got {leading_spaces} spaces in {caret:?}", ); } - - - - - diff --git a/tests/it/project_lifecycle.rs b/tests/it/project_lifecycle.rs index a8bed7b..ad5bd71 100644 --- a/tests/it/project_lifecycle.rs +++ b/tests/it/project_lifecycle.rs @@ -22,14 +22,16 @@ fn tempdir() -> tempfile::TempDir { #[test] fn no_args_creates_temp_project_under_data_root() { let data = tempdir(); - let project = project::open_or_create(None, Some(data.path())) - .expect("open_or_create with empty CLI"); + let project = + project::open_or_create(None, Some(data.path())).expect("open_or_create with empty CLI"); let path = project.path(); assert!(path.exists(), "project dir should exist"); assert!(path.starts_with(data.path())); assert_eq!( - path.parent().and_then(|p| p.file_name()).map(|s| s.to_string_lossy().into_owned()), + path.parent() + .and_then(|p| p.file_name()) + .map(|s| s.to_string_lossy().into_owned()), Some(PROJECTS_SUBDIR.to_string()), ); @@ -96,8 +98,7 @@ fn positional_path_opens_existing_project() { // Now drive open_or_create with the path as if it were a // CLI positional argument. - let project = project::open_or_create(Some(&path), None) - .expect("open via positional path"); + let project = project::open_or_create(Some(&path), None).expect("open via positional path"); assert_eq!(project.path(), path); } @@ -142,7 +143,10 @@ fn data_dir_override_does_not_touch_default_os_dir() { assert!(p1_path.starts_with(data.path())); assert!(p2_path.starts_with(data.path())); - assert_ne!(p1_path, p2_path, "two temp projects must have distinct names"); + assert_ne!( + p1_path, p2_path, + "two temp projects must have distinct names" + ); } #[test] @@ -167,11 +171,18 @@ fn db_persists_across_open_close_cycles() { db.create_table( "Customers".to_string(), vec![ - rdbms_playground::dsl::ColumnSpec::new("id".to_string(), rdbms_playground::dsl::Type::Serial), - rdbms_playground::dsl::ColumnSpec::new("Name".to_string(), rdbms_playground::dsl::Type::Text), + rdbms_playground::dsl::ColumnSpec::new( + "id".to_string(), + rdbms_playground::dsl::Type::Serial, + ), + rdbms_playground::dsl::ColumnSpec::new( + "Name".to_string(), + rdbms_playground::dsl::Type::Text, + ), ], vec!["id".to_string()], - None) + None, + ) .await .expect("create_table"); }); @@ -187,7 +198,9 @@ fn db_persists_across_open_close_cycles() { .enable_all() .build() .unwrap(); - let tables = rt.block_on(async { db.list_tables().await }).expect("list_tables"); + let tables = rt + .block_on(async { db.list_tables().await }) + .expect("list_tables"); assert!(tables.iter().any(|t| t == "Customers"), "got: {tables:?}"); // Sanity: the project.yaml and history.log are still empty diff --git a/tests/it/replay_command.rs b/tests/it/replay_command.rs index d5ccb33..755edfb 100644 --- a/tests/it/replay_command.rs +++ b/tests/it/replay_command.rs @@ -47,8 +47,7 @@ fn tempdir() -> tempfile::TempDir { /// harness — most tests only need to write a script file and /// call `run_replay`. fn open_project_db(data_root: &Path) -> (project::Project, Database) { - let project = project::open_or_create(None, Some(data_root)) - .expect("open_or_create"); + let project = project::open_or_create(None, Some(data_root)).expect("open_or_create"); let db = Database::open_with_persistence( project.db_path(), Persistence::new(project.path().to_path_buf()), @@ -132,9 +131,7 @@ fn replay_three_lines_dispatches_three_commands() { insert into T (1, 'Alice')\n", ); - let events = rt().block_on(async { - run_replay(&db, project.path(), "seed.commands").await - }); + let events = rt().block_on(async { run_replay(&db, project.path(), "seed.commands").await }); assert_completed(&events, 3); // The dispatched commands actually mutated state. @@ -167,8 +164,7 @@ fn replay_of_actual_history_log_runs_ok_commands_and_skips_err() { 2026-05-24T10:00:03Z|ok|insert into T (id, v) values (1, 'alpha')\n", ); - let events = - rt().block_on(async { run_replay(&db, project.path(), "history.log").await }); + let events = rt().block_on(async { run_replay(&db, project.path(), "history.log").await }); // Three `ok` records replayed; the `err` record is skipped (not // counted, not a failure). assert_completed(&events, 3); @@ -215,14 +211,21 @@ fn replay_skips_app_lifecycle_commands_silently() { 2026-05-24T10:00:13Z|ok|add column T: v (text)\n\ 2026-05-24T10:00:14Z|ok|insert into T (id, v) values (1, 'alpha')\n", ); - let events = - rt().block_on(async { run_replay(&db, project.path(), "history.log").await }); + let events = rt().block_on(async { run_replay(&db, project.path(), "history.log").await }); // Three data/schema commands ran; every app-lifecycle line was // skipped silently (no panic, no abort, no warnings, no quit). match events.last().expect("event") { - AppEvent::ReplayCompleted { count, warnings, .. } => { - assert_eq!(*count, 3, "only the 3 write commands ran; events: {events:?}"); - assert!(warnings.is_empty(), "these skips are silent; got {warnings:?}"); + AppEvent::ReplayCompleted { + count, warnings, .. + } => { + assert_eq!( + *count, 3, + "only the 3 write commands ran; events: {events:?}" + ); + assert!( + warnings.is_empty(), + "these skips are silent; got {warnings:?}" + ); } other => panic!("expected ReplayCompleted, got {other:?}"), } @@ -251,10 +254,11 @@ fn replay_skips_import_with_a_warning() { "2026-05-24T10:00:00Z|ok|create table T with pk id(int)\n\ 2026-05-24T10:00:01Z|ok|import shared.zip as Imported\n", ); - let events = - rt().block_on(async { run_replay(&db, project.path(), "history.log").await }); + let events = rt().block_on(async { run_replay(&db, project.path(), "history.log").await }); match events.last().expect("event") { - AppEvent::ReplayCompleted { count, warnings, .. } => { + AppEvent::ReplayCompleted { + count, warnings, .. + } => { assert_eq!(*count, 1, "only the create ran; events: {events:?}"); assert!( warnings.iter().any(|w| w.contains("import shared.zip")), @@ -282,9 +286,7 @@ fn replay_skips_blank_lines_and_comments() { \n", ); - let events = rt().block_on(async { - run_replay(&db, project.path(), "seed.commands").await - }); + let events = rt().block_on(async { run_replay(&db, project.path(), "seed.commands").await }); // Only two non-blank, non-comment lines. assert_completed(&events, 2); } @@ -295,9 +297,7 @@ fn replay_empty_file_completes_with_zero_commands() { let (project, db) = open_project_db(data.path()); write_script(project.path(), "empty.commands", ""); - let events = rt().block_on(async { - run_replay(&db, project.path(), "empty.commands").await - }); + let events = rt().block_on(async { run_replay(&db, project.path(), "empty.commands").await }); assert_completed(&events, 0); } @@ -311,9 +311,8 @@ fn replay_only_comments_completes_with_zero_commands() { "# just\n# comments\n\n", ); - let events = rt().block_on(async { - run_replay(&db, project.path(), "comments.commands").await - }); + let events = + rt().block_on(async { run_replay(&db, project.path(), "comments.commands").await }); assert_completed(&events, 0); } @@ -350,8 +349,14 @@ fn replay_constraint_failure_shows_real_names_not_placeholders() { // INSERT command — the **real offending value** is shown too (it used // to degrade to the neutral "that value" because `SqlInsert` discarded // its literals). - assert!(error.contains("T.email"), "names the real table.column; got: {error}"); - assert!(error.contains("a@b.com"), "shows the real offending value; got: {error}"); + assert!( + error.contains("T.email"), + "names the real table.column; got: {error}" + ); + assert!( + error.contains("a@b.com"), + "shows the real offending value; got: {error}" + ); } #[test] @@ -359,9 +364,8 @@ fn replay_missing_file_fails_with_line_number_zero() { let data = tempdir(); let (project, db) = open_project_db(data.path()); - let events = rt().block_on(async { - run_replay(&db, project.path(), "no-such-file.commands").await - }); + let events = + rt().block_on(async { run_replay(&db, project.path(), "no-such-file.commands").await }); let failed = assert_failed_at(&events, 0); let AppEvent::ReplayFailed { error, .. } = failed else { unreachable!() @@ -387,9 +391,7 @@ fn replay_aborts_on_first_parse_failure_and_reports_line() { insert into T (1, 'should not happen')\n", ); - let events = rt().block_on(async { - run_replay(&db, project.path(), "bad.commands").await - }); + let events = rt().block_on(async { run_replay(&db, project.path(), "bad.commands").await }); let failed = assert_failed_at(&events, 3); let AppEvent::ReplayFailed { error, command, .. } = failed else { unreachable!() @@ -452,9 +454,7 @@ fn replay_rejects_wrong_type_value_in_a_hand_built_script() { insert into T values (1, 'not a number')\n", ); - let events = rt().block_on(async { - run_replay(&db, project.path(), "typed.commands").await - }); + let events = rt().block_on(async { run_replay(&db, project.path(), "typed.commands").await }); let failed = assert_failed_at(&events, 3); let AppEvent::ReplayFailed { error, .. } = failed else { unreachable!() @@ -489,9 +489,7 @@ fn replay_aborts_on_first_runtime_failure_and_reports_line() { insert into T (1)\n", ); - let events = rt().block_on(async { - run_replay(&db, project.path(), "bad.commands").await - }); + let events = rt().block_on(async { run_replay(&db, project.path(), "bad.commands").await }); let _ = assert_failed_at(&events, 2); } @@ -504,23 +502,29 @@ fn replay_skips_nested_replay_with_a_warning() { // because the nested file's commands are not reconstructed. let data = tempdir(); let (project, db) = open_project_db(data.path()); - write_script(project.path(), "inner.commands", "create table T with pk id(int)\n"); + write_script( + project.path(), + "inner.commands", + "create table T with pk id(int)\n", + ); write_script( project.path(), "outer.commands", "create table U with pk id(int)\nreplay inner.commands\n", ); - let events = rt().block_on(async { - run_replay(&db, project.path(), "outer.commands").await - }); + let events = rt().block_on(async { run_replay(&db, project.path(), "outer.commands").await }); // The outer `create table U` ran; the nested `replay` was // skipped (count 1), with a warning. match events.last().expect("event") { - AppEvent::ReplayCompleted { count, warnings, .. } => { + AppEvent::ReplayCompleted { + count, warnings, .. + } => { assert_eq!(*count, 1, "only the outer create ran; events: {events:?}"); assert!( - warnings.iter().any(|w| w.contains("nested") && w.contains("replay inner.commands")), + warnings + .iter() + .any(|w| w.contains("nested") && w.contains("replay inner.commands")), "expected a nested-replay skip warning; got {warnings:?}", ); } @@ -528,7 +532,10 @@ fn replay_skips_nested_replay_with_a_warning() { } // The nested file's table was NOT created (the replay was skipped). let cols = rt().block_on(async { db.query_data("T".to_string(), None, None).await }); - assert!(cols.is_err(), "inner.commands' table T must not exist (nested replay skipped)"); + assert!( + cols.is_err(), + "inner.commands' table T must not exist (nested replay skipped)" + ); } #[test] @@ -546,20 +553,22 @@ fn replay_history_log_records_subcommands_only() { "create table T with pk id(int)\nadd column T: name (text)\n", ); - let events = rt().block_on(async { - run_replay(&db, project.path(), "seed.commands").await - }); + let events = rt().block_on(async { run_replay(&db, project.path(), "seed.commands").await }); assert_completed(&events, 2); - let history = fs::read_to_string(project.path().join("history.log")) - .expect("history.log exists"); + let history = + fs::read_to_string(project.path().join("history.log")).expect("history.log exists"); // Per-command entries landed. assert!( - history.lines().any(|l| l.contains("create table T with pk id(int)")), + history + .lines() + .any(|l| l.contains("create table T with pk id(int)")), "history.log missing create line:\n{history}" ); assert!( - history.lines().any(|l| l.contains("add column T: name (text)")), + history + .lines() + .any(|l| l.contains("add column T: name (text)")), "history.log missing add column line:\n{history}" ); // The replay invocation itself did NOT land — that's diff --git a/tests/it/seed.rs b/tests/it/seed.rs index ed389b1..7e2e41c 100644 --- a/tests/it/seed.rs +++ b/tests/it/seed.rs @@ -18,8 +18,7 @@ fn rt() -> tokio::runtime::Runtime { fn open_project_db() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence(project.db_path(), persistence) .expect("open db with persistence"); @@ -76,7 +75,10 @@ fn seed_parses_with_and_without_count() { match parse_command("seed People").expect("`seed People` parses") { Command::Seed { table, count, .. } => { assert_eq!(table, "People"); - assert_eq!(count, None, "omitted count is None (executor defaults to 20)"); + assert_eq!( + count, None, + "omitted count is None (executor defaults to 20)" + ); } other => panic!("expected Command::Seed, got {other:?}"), } @@ -134,7 +136,10 @@ fn seed_set_fixed_value_override_parses() { let (_t, ov) = seed_overrides("seed users 5 set status = 'active'"); assert_eq!(ov.len(), 1); assert_eq!(ov[0].column, "status"); - assert_eq!(ov[0].kind, SeedOverrideKind::Fixed(Value::Text("active".into()))); + assert_eq!( + ov[0].kind, + SeedOverrideKind::Fixed(Value::Text("active".into())) + ); } #[test] @@ -177,8 +182,7 @@ fn seed_set_numeric_range_override_parses() { #[test] fn seed_set_date_range_override_parses_with_quoted_dates() { // ADR-0048 D2 amendment: dates in the range form are quoted strings. - let (_t, ov) = - seed_overrides("seed users set signup between '2023-01-01' and '2024-12-31'"); + let (_t, ov) = seed_overrides("seed users set signup between '2023-01-01' and '2024-12-31'"); assert_eq!( ov[0].kind, SeedOverrideKind::Range { @@ -207,7 +211,9 @@ fn seed_count_is_not_confused_by_a_range_value() { // No positional count, but `between 18 and 80` carries NumberLits — // they must not be read as the count (bounded to before `set`). match parse_command("seed users set age between 18 and 80").expect("parses") { - Command::Seed { count, overrides, .. } => { + Command::Seed { + count, overrides, .. + } => { assert_eq!(count, None, "the count is None, not 18"); assert_eq!(overrides.len(), 1); } @@ -267,7 +273,14 @@ fn seed_populates_a_table_and_persists_rows() { create_people(&db, &rt); let result = rt - .block_on(db.seed("People".into(), None, Some(7), Vec::new(), Some(42), Some("seed People 7".into()))) + .block_on(db.seed( + "People".into(), + None, + Some(7), + Vec::new(), + Some(42), + Some("seed People 7".into()), + )) .expect("seed succeeds"); assert_eq!(result.produced, 7); @@ -278,22 +291,34 @@ fn seed_populates_a_table_and_persists_rows() { "CSV should hold 7 generated rows:\n{csv}" ); // The generated `email` column produces address-shaped values. - assert!(csv.contains('@'), "seeded emails should appear in the CSV:\n{csv}"); + assert!( + csv.contains('@'), + "seeded emails should appear in the CSV:\n{csv}" + ); } /// Parse a seeded table's CSV into per-column value lists (simple /// comma-split — the values under test carry no commas/quotes). fn csv_columns(csv: &str) -> (Vec, Vec>) { let mut lines = csv.lines().filter(|l| !l.trim().is_empty()); - let header: Vec = lines.next().unwrap().split(',').map(str::to_string).collect(); - let rows: Vec> = - lines.map(|l| l.split(',').map(str::to_string).collect()).collect(); + let header: Vec = lines + .next() + .unwrap() + .split(',') + .map(str::to_string) + .collect(); + let rows: Vec> = lines + .map(|l| l.split(',').map(str::to_string).collect()) + .collect(); (header, rows) } fn column_values(csv: &str, col: &str) -> Vec { let (header, rows) = csv_columns(csv); - let idx = header.iter().position(|h| h == col).expect("column present"); + let idx = header + .iter() + .position(|h| h == col) + .expect("column present"); rows.iter().map(|r| r[idx].clone()).collect() } @@ -321,20 +346,36 @@ fn seed_year_and_choice_set_heuristics() { )) .expect("create Records"); - rt.block_on(db.seed("Records".into(), None, Some(30), Vec::new(), Some(99), Some("seed Records 30".into()))) - .expect("seed succeeds"); + rt.block_on(db.seed( + "Records".into(), + None, + Some(30), + Vec::new(), + Some(99), + Some("seed Records 30".into()), + )) + .expect("seed succeeds"); let csv = read_csv(&project, "Records").expect("Records CSV exists"); for y in column_values(&csv, "birth_year") { let n: i32 = y.parse().expect("birth_year is an int"); - assert!((1945..=2007).contains(&n), "birth_year {n} must be a plausible birth year"); + assert!( + (1945..=2007).contains(&n), + "birth_year {n} must be a plausible birth year" + ); } for y in column_values(&csv, "published") { let n: i32 = y.parse().expect("published is an int"); - assert!((1950..=2025).contains(&n), "published {n} must be a plausible recent year"); + assert!( + (1950..=2025).contains(&n), + "published {n} must be a plausible recent year" + ); } for p in column_values(&csv, "priority") { - assert!(["low", "medium", "high"].contains(&p.as_str()), "priority `{p}` must be low/medium/high"); + assert!( + ["low", "medium", "high"].contains(&p.as_str()), + "priority `{p}` must be low/medium/high" + ); } for s in column_values(&csv, "severity") { assert!( @@ -405,7 +446,14 @@ fn seed_count_defaults_to_twenty() { create_people(&db, &rt); let result = rt - .block_on(db.seed("People".into(), None, None, Vec::new(), Some(1), Some("seed People".into()))) + .block_on(db.seed( + "People".into(), + None, + None, + Vec::new(), + Some(1), + Some("seed People".into()), + )) .expect("seed succeeds"); assert_eq!(result.produced, 20, "omitted count defaults to 20"); let csv = read_csv(&project, "People").expect("People CSV exists"); @@ -420,10 +468,24 @@ fn seed_is_reproducible_with_a_fixed_seed() { create_people(&db1, &rt); create_people(&db2, &rt); - rt.block_on(db1.seed("People".into(), None, Some(4), Vec::new(), Some(123), Some("seed People 4".into()))) - .expect("seed run 1"); - rt.block_on(db2.seed("People".into(), None, Some(4), Vec::new(), Some(123), Some("seed People 4".into()))) - .expect("seed run 2"); + rt.block_on(db1.seed( + "People".into(), + None, + Some(4), + Vec::new(), + Some(123), + Some("seed People 4".into()), + )) + .expect("seed run 1"); + rt.block_on(db2.seed( + "People".into(), + None, + Some(4), + Vec::new(), + Some(123), + Some("seed People 4".into()), + )) + .expect("seed run 2"); let csv1 = read_csv(&p1, "People").expect("csv 1"); let csv2 = read_csv(&p2, "People").expect("csv 2"); @@ -493,10 +555,24 @@ fn seed_fills_foreign_keys_from_existing_parents() { create_users_and_orders(&db, &rt, true); // 5 parents → serial ids 1..=5. - rt.block_on(db.seed("Users".into(), None, Some(5), Vec::new(), Some(1), Some("seed Users 5".into()))) - .expect("seed Users"); + rt.block_on(db.seed( + "Users".into(), + None, + Some(5), + Vec::new(), + Some(1), + Some("seed Users 5".into()), + )) + .expect("seed Users"); let res = rt - .block_on(db.seed("Orders".into(), None, Some(10), Vec::new(), Some(2), Some("seed Orders 10".into()))) + .block_on(db.seed( + "Orders".into(), + None, + Some(10), + Vec::new(), + Some(2), + Some("seed Orders 10".into()), + )) .expect("seed Orders"); assert_eq!(res.produced, 10, "every child row must insert (valid FK)"); @@ -520,10 +596,20 @@ fn seed_refuses_when_a_parent_table_is_empty() { // Users is empty — no valid FK can be fabricated. let err = rt - .block_on(db.seed("Orders".into(), None, Some(3), Vec::new(), Some(1), Some("seed Orders 3".into()))) + .block_on(db.seed( + "Orders".into(), + None, + Some(3), + Vec::new(), + Some(1), + Some("seed Orders 3".into()), + )) .expect_err("seed must refuse an empty parent"); let msg = err.to_string(); - assert!(msg.contains("Users"), "error should name the empty parent: {msg}"); + assert!( + msg.contains("Users"), + "error should name the empty parent: {msg}" + ); let lower = msg.to_lowercase(); assert!( lower.contains("no rows") || lower.contains("first"), @@ -546,7 +632,14 @@ fn seed_refuses_a_not_null_blob_column() { .expect("create Files"); let err = rt - .block_on(db.seed("Files".into(), None, Some(2), Vec::new(), Some(1), Some("seed Files 2".into()))) + .block_on(db.seed( + "Files".into(), + None, + Some(2), + Vec::new(), + Some(1), + Some("seed Files 2".into()), + )) .expect_err("seed must refuse a NOT NULL blob"); let msg = err.to_string(); assert!( @@ -573,7 +666,14 @@ fn seed_omits_a_nullable_blob_column() { .expect("create Files"); let res = rt - .block_on(db.seed("Files".into(), None, Some(3), Vec::new(), Some(1), Some("seed Files 3".into()))) + .block_on(db.seed( + "Files".into(), + None, + Some(3), + Vec::new(), + Some(1), + Some("seed Files 3".into()), + )) .expect("seed succeeds despite the nullable blob"); assert_eq!(res.produced, 3); let csv = read_csv(&project, "Files").expect("Files CSV"); @@ -607,14 +707,25 @@ fn seed_keeps_unique_columns_distinct() { .expect("create Tags"); let res = rt - .block_on(db.seed("Tags".into(), None, Some(8), Vec::new(), Some(3), Some("seed Tags 8".into()))) + .block_on(db.seed( + "Tags".into(), + None, + Some(8), + Vec::new(), + Some(3), + Some("seed Tags 8".into()), + )) .expect("seed"); assert_eq!(res.produced, 8); let csv = read_csv(&project, "Tags").expect("Tags CSV"); let labels = nth_column_values(&csv, 1); let distinct: std::collections::HashSet<&String> = labels.iter().collect(); - assert_eq!(distinct.len(), labels.len(), "UNIQUE column has duplicates:\n{csv}"); + assert_eq!( + distinct.len(), + labels.len(), + "UNIQUE column has duplicates:\n{csv}" + ); } #[test] @@ -636,7 +747,14 @@ fn seed_sequences_identifier_int_columns() { .expect("create Items"); let res = rt - .block_on(db.seed("Items".into(), None, Some(5), Vec::new(), Some(1), Some("seed Items 5".into()))) + .block_on(db.seed( + "Items".into(), + None, + Some(5), + Vec::new(), + Some(1), + Some("seed Items 5".into()), + )) .expect("seed"); assert_eq!(res.produced, 5); @@ -646,7 +764,11 @@ fn seed_sequences_identifier_int_columns() { .map(|s| s.parse().expect("code is an int")) .collect(); let distinct: std::collections::HashSet = codes.iter().copied().collect(); - assert_eq!(distinct.len(), 5, "identifier ints must be unique: {codes:?}"); + assert_eq!( + distinct.len(), + 5, + "identifier ints must be unique: {codes:?}" + ); } #[test] @@ -667,14 +789,24 @@ fn seed_junction_produces_distinct_combinations_and_caps() { ) .await .expect("create parent"); - db.seed(t.into(), None, Some(2), Vec::new(), Some(1), Some(format!("seed {t} 2"))) - .await - .expect("seed parent"); + db.seed( + t.into(), + None, + Some(2), + Vec::new(), + Some(1), + Some(format!("seed {t} 2")), + ) + .await + .expect("seed parent"); } // Junction with a compound PK over its two FK columns. db.create_table( "J".to_string(), - vec![ColumnSpec::new("a", Type::Int), ColumnSpec::new("b", Type::Int)], + vec![ + ColumnSpec::new("a", Type::Int), + ColumnSpec::new("b", Type::Int), + ], vec!["a".to_string(), "b".to_string()], None, ) @@ -709,11 +841,21 @@ fn seed_junction_produces_distinct_combinations_and_caps() { // Requesting 10 caps at the 4 available distinct combinations. let res = db - .seed("J".into(), None, Some(10), Vec::new(), Some(7), Some("seed J 10".into())) + .seed( + "J".into(), + None, + Some(10), + Vec::new(), + Some(7), + Some("seed J 10".into()), + ) .await .expect("seed J"); assert_eq!(res.produced, 4, "junction caps at available combos"); - assert_eq!(res.requested, 10, "the requested count is reported for the cap note"); + assert_eq!( + res.requested, 10, + "the requested count is reported for the cap note" + ); }); let csv = read_csv(&project, "J").expect("J CSV"); @@ -724,7 +866,11 @@ fn seed_junction_produces_distinct_combinations_and_caps() { .map(str::to_string) .collect(); let distinct: std::collections::HashSet<&String> = pairs.iter().collect(); - assert_eq!(distinct.len(), pairs.len(), "junction rows must be distinct:\n{csv}"); + assert_eq!( + distinct.len(), + pairs.len(), + "junction rows must be distinct:\n{csv}" + ); } #[test] @@ -743,9 +889,19 @@ fn seed_draws_enum_values_from_an_in_check() { // Every generated status must satisfy the CHECK, so all rows insert. let res = rt - .block_on(db.seed("Tickets".into(), None, Some(12), Vec::new(), Some(2), Some("seed Tickets 12".into()))) + .block_on(db.seed( + "Tickets".into(), + None, + Some(12), + Vec::new(), + Some(2), + Some("seed Tickets 12".into()), + )) .expect("seed"); - assert_eq!(res.produced, 12, "all rows insert — values satisfy the CHECK"); + assert_eq!( + res.produced, 12, + "all rows insert — values satisfy the CHECK" + ); let csv = read_csv(&project, "Tickets").expect("Tickets CSV"); for v in nth_column_values(&csv, 1) { @@ -780,7 +936,14 @@ fn seed_advises_on_enum_ish_columns() { .expect("create Tasks"); let res = rt - .block_on(db.seed("Tasks".into(), None, Some(3), Vec::new(), Some(1), Some("seed Tasks 3".into()))) + .block_on(db.seed( + "Tasks".into(), + None, + Some(3), + Vec::new(), + Some(1), + Some("seed Tasks 3".into()), + )) .expect("seed"); assert!( res.advisory_columns.contains(&"status".to_string()), @@ -795,7 +958,14 @@ fn seed_refuses_an_excessive_count() { let rt = rt(); create_people(&db, &rt); let err = rt - .block_on(db.seed("People".into(), None, Some(1_000_000), Vec::new(), Some(1), Some("seed People 1000000".into()))) + .block_on(db.seed( + "People".into(), + None, + Some(1_000_000), + Vec::new(), + Some(1), + Some("seed People 1000000".into()), + )) .expect_err("an excessive count must be refused"); assert!( err.to_string().to_lowercase().contains("maximum"), @@ -810,7 +980,14 @@ fn seed_preview_is_capped_but_count_is_full() { create_people(&db, &rt); let res = rt - .block_on(db.seed("People".into(), None, Some(25), Vec::new(), Some(1), Some("seed People 25".into()))) + .block_on(db.seed( + "People".into(), + None, + Some(25), + Vec::new(), + Some(1), + Some("seed People 25".into()), + )) .expect("seed"); assert_eq!(res.produced, 25, "the full count is produced"); assert_eq!(res.data.rows.len(), 20, "the preview is capped at 20 rows"); @@ -860,14 +1037,24 @@ fn seed_is_one_undo_step() { .expect("open db with undo"); let rt = rt(); create_people(&db, &rt); - rt.block_on(db.seed("People".into(), None, Some(6), Vec::new(), Some(1), Some("seed People 6".into()))) - .expect("seed"); + rt.block_on(db.seed( + "People".into(), + None, + Some(6), + Vec::new(), + Some(1), + Some("seed People 6".into()), + )) + .expect("seed"); assert_eq!(data_row_count(&read_csv(&project, "People").unwrap()), 6); // One undo removes the whole seed batch (ADR-0048 D15). rt.block_on(db.undo()).unwrap().expect("undo applied"); let rows = read_csv(&project, "People").map_or(0, |c| data_row_count(&c)); - assert_eq!(rows, 0, "one undo must remove every seeded row in a single step"); + assert_eq!( + rows, 0, + "one undo must remove every seeded row in a single step" + ); } #[test] @@ -882,10 +1069,17 @@ fn seed_column_fill_is_one_undo_step() { create_members(&db, &rt); run_seed(&db, &rt, "seed Members 5 --seed 1").expect("seed"); // Fill `status` across all 5 rows with a constant, then undo once. - run_seed(&db, &rt, "seed Members.status set status = 'flagged' --seed 2") - .expect("column-fill"); + run_seed( + &db, + &rt, + "seed Members.status set status = 'flagged' --seed 2", + ) + .expect("column-fill"); let before = named_column_values(&read_csv(&project, "Members").unwrap(), "status"); - assert!(before.iter().all(|s| s == "flagged"), "all rows filled: {before:?}"); + assert!( + before.iter().all(|s| s == "flagged"), + "all rows filled: {before:?}" + ); rt.block_on(db.undo()).unwrap().expect("undo applied"); let after = named_column_values(&read_csv(&project, "Members").unwrap(), "status"); @@ -893,7 +1087,11 @@ fn seed_column_fill_is_one_undo_step() { after.iter().all(|s| s != "flagged"), "one undo reverts the whole column-fill in a single step: {after:?}" ); - assert_eq!(after.len(), 5, "undo restores the original rows, not removes them"); + assert_eq!( + after.len(), + 5, + "undo restores the original rows, not removes them" + ); } #[test] @@ -930,10 +1128,23 @@ fn seed_rolls_back_atomically_on_a_constraint_failure() { )) .expect("create Bad"); - let res = rt.block_on(db.seed("Bad".into(), None, Some(5), Vec::new(), Some(1), Some("seed Bad 5".into()))); - assert!(res.is_err(), "seed must fail when generated rows violate the CHECK"); + let res = rt.block_on(db.seed( + "Bad".into(), + None, + Some(5), + Vec::new(), + Some(1), + Some("seed Bad 5".into()), + )); + assert!( + res.is_err(), + "seed must fail when generated rows violate the CHECK" + ); let rows = read_csv(&project, "Bad").map_or(0, |c| data_row_count(&c)); - assert_eq!(rows, 0, "a failed seed must leave the table unchanged (atomic)"); + assert_eq!( + rows, 0, + "a failed seed must leave the table unchanged (atomic)" + ); } #[test] @@ -942,7 +1153,14 @@ fn seed_zero_is_a_no_op() { let rt = rt(); create_people(&db, &rt); let res = rt - .block_on(db.seed("People".into(), None, Some(0), Vec::new(), Some(1), Some("seed People 0".into()))) + .block_on(db.seed( + "People".into(), + None, + Some(0), + Vec::new(), + Some(1), + Some("seed People 0".into()), + )) .expect("seed 0 succeeds"); assert_eq!(res.produced, 0); let rows = read_csv(&project, "People").map_or(0, |c| data_row_count(&c)); @@ -967,7 +1185,14 @@ fn seed_advises_on_a_complex_check_column() { .expect("create Widgets"); let res = rt - .block_on(db.seed("Widgets".into(), None, Some(3), Vec::new(), Some(1), Some("seed Widgets 3".into()))) + .block_on(db.seed( + "Widgets".into(), + None, + Some(3), + Vec::new(), + Some(1), + Some("seed Widgets 3".into()), + )) .expect("seed"); assert!( res.advisory_columns.contains(&"label".to_string()), @@ -981,10 +1206,24 @@ fn seed_foreign_keys_are_reproducible_with_a_fixed_seed() { let rt = rt(); let seed_one = |db: &Database| { create_users_and_orders(db, &rt, true); - rt.block_on(db.seed("Users".into(), None, Some(4), Vec::new(), Some(1), Some("seed Users 4".into()))) - .expect("seed users"); - rt.block_on(db.seed("Orders".into(), None, Some(8), Vec::new(), Some(99), Some("seed Orders 8".into()))) - .expect("seed orders"); + rt.block_on(db.seed( + "Users".into(), + None, + Some(4), + Vec::new(), + Some(1), + Some("seed Users 4".into()), + )) + .expect("seed users"); + rt.block_on(db.seed( + "Orders".into(), + None, + Some(8), + Vec::new(), + Some(99), + Some("seed Orders 8".into()), + )) + .expect("seed orders"); }; let (p1, db1, _d1) = open_project_db(); let (p2, db2, _d2) = open_project_db(); @@ -1013,8 +1252,15 @@ fn seed_shortid_columns_are_reproducible_with_a_fixed_seed() { None, )) .expect("create Contacts"); - rt.block_on(db.seed("Contacts".into(), None, Some(5), Vec::new(), Some(42), Some("seed Contacts 5".into()))) - .expect("seed"); + rt.block_on(db.seed( + "Contacts".into(), + None, + Some(5), + Vec::new(), + Some(42), + Some("seed Contacts 5".into()), + )) + .expect("seed"); }; let (p1, db1, _d1) = open_project_db(); let (p2, db2, _d2) = open_project_db(); @@ -1023,13 +1269,20 @@ fn seed_shortid_columns_are_reproducible_with_a_fixed_seed() { let csv1 = read_csv(&p1, "Contacts").unwrap(); let csv2 = read_csv(&p2, "Contacts").unwrap(); - assert_eq!(csv1, csv2, "shortid values must reproduce under a fixed --seed"); + assert_eq!( + csv1, csv2, + "shortid values must reproduce under a fixed --seed" + ); // The shortid PK is populated with distinct 10-char base58 ids. let codes = nth_column_values(&csv1, 0); assert_eq!(codes.len(), 5); let distinct: std::collections::HashSet<&String> = codes.iter().collect(); - assert_eq!(distinct.len(), 5, "shortid PK values must be distinct: {codes:?}"); + assert_eq!( + distinct.len(), + 5, + "shortid PK values must be distinct: {codes:?}" + ); for code in &codes { assert_eq!(code.len(), 10, "shortid should be 10 chars: {code}"); } @@ -1105,7 +1358,10 @@ fn seed_set_fixed_value_fills_every_row() { let csv = read_csv(&project, "Members").unwrap(); let statuses = named_column_values(&csv, "status"); assert_eq!(statuses.len(), 6); - assert!(statuses.iter().all(|s| s == "active"), "every status pinned: {statuses:?}"); + assert!( + statuses.iter().all(|s| s == "active"), + "every status pinned: {statuses:?}" + ); } #[test] @@ -1113,7 +1369,12 @@ fn seed_set_pick_list_draws_only_from_the_list() { let (project, db, _d) = open_project_db(); let rt = rt(); create_members(&db, &rt); - run_seed(&db, &rt, "seed Members 20 set role in ('admin', 'user') --seed 2").expect("seed"); + run_seed( + &db, + &rt, + "seed Members 20 set role in ('admin', 'user') --seed 2", + ) + .expect("seed"); let csv = read_csv(&project, "Members").unwrap(); let roles = named_column_values(&csv, "role"); assert!( @@ -1131,7 +1392,10 @@ fn seed_set_as_generator_forces_the_shape() { run_seed(&db, &rt, "seed Members 5 set name as email --seed 3").expect("seed"); let csv = read_csv(&project, "Members").unwrap(); let names = named_column_values(&csv, "name"); - assert!(names.iter().all(|n| n.contains('@')), "name forced to email shape: {names:?}"); + assert!( + names.iter().all(|n| n.contains('@')), + "name forced to email shape: {names:?}" + ); } #[test] @@ -1139,7 +1403,12 @@ fn seed_set_numeric_range_stays_within_bounds() { let (project, db, _d) = open_project_db(); let rt = rt(); create_members(&db, &rt); - run_seed(&db, &rt, "seed Members 30 set age between 30 and 40 --seed 4").expect("seed"); + run_seed( + &db, + &rt, + "seed Members 30 set age between 30 and 40 --seed 4", + ) + .expect("seed"); let csv = read_csv(&project, "Members").unwrap(); for a in named_column_values(&csv, "age") { let n: i64 = a.parse().unwrap_or_else(|_| panic!("age `{a}` not an int")); @@ -1190,7 +1459,10 @@ fn seed_incompatible_range_is_a_friendly_error() { // A numeric range on a text column (`name`) is rejected. let err = run_seed(&db, &rt, "seed Members 3 set name between 1 and 10").unwrap_err(); let msg = format!("{err}"); - assert!(msg.contains("between"), "range error should mention `between`: {msg}"); + assert!( + msg.contains("between"), + "range error should mention `between`: {msg}" + ); } #[test] @@ -1221,8 +1493,12 @@ fn seed_column_fill_updates_existing_rows_without_adding() { let before = data_row_count(&read_csv(&project, "Members").unwrap()); assert_eq!(before, 5); - let res = run_seed(&db, &rt, "seed Members.status set status in ('x', 'y') --seed 2") - .expect("column-fill"); + let res = run_seed( + &db, + &rt, + "seed Members.status set status in ('x', 'y') --seed 2", + ) + .expect("column-fill"); assert_eq!(res.produced, 5, "column-fill touches the 5 existing rows"); let csv = read_csv(&project, "Members").unwrap(); assert_eq!(data_row_count(&csv), 5, "no new rows added"); @@ -1240,7 +1516,10 @@ fn seed_column_fill_refuses_a_pk_target() { create_members(&db, &rt); run_seed(&db, &rt, "seed Members 3 --seed 1").expect("seed"); let err = run_seed(&db, &rt, "seed Members.id").unwrap_err(); - assert!(format!("{err}").contains("primary key"), "PK target refused: {err}"); + assert!( + format!("{err}").contains("primary key"), + "PK target refused: {err}" + ); } #[test] @@ -1282,7 +1561,10 @@ fn seed_column_fill_rejects_a_row_count() { Some("seed Members.status 5".into()), )) .unwrap_err(); - assert!(format!("{err}").contains("no row count"), "count refused: {err}"); + assert!( + format!("{err}").contains("no row count"), + "count refused: {err}" + ); } #[test] @@ -1298,7 +1580,11 @@ fn seed_column_fill_fk_target_samples_the_parent() { assert_eq!(res.produced, 8); let csv = read_csv(&project, "Orders").unwrap(); let user_ids = named_column_values(&csv, "user_id"); - assert!(user_ids.iter().all(|v| (1..=4).contains(&v.parse::().unwrap()))); + assert!( + user_ids + .iter() + .all(|v| (1..=4).contains(&v.parse::().unwrap())) + ); } #[test] @@ -1310,14 +1596,11 @@ fn seed_fixed_override_on_unique_column_is_a_friendly_error() { let rt = rt(); rt.block_on(db.create_table( "U".to_string(), - vec![ - ColumnSpec::new("id", Type::Serial), - { - let mut c = ColumnSpec::new("email", Type::Text); - c.unique = true; - c - }, - ], + vec![ColumnSpec::new("id", Type::Serial), { + let mut c = ColumnSpec::new("email", Type::Text); + c.unique = true; + c + }], vec!["id".to_string()], None, )) @@ -1330,7 +1613,10 @@ fn seed_fixed_override_on_unique_column_is_a_friendly_error() { ); // A short pick-list (< count) is likewise refused... let err2 = run_seed(&db, &rt, "seed U 5 set email in ('a@b.c', 'd@e.f')").unwrap_err(); - assert!(format!("{err2}").contains("distinct"), "short list refused: {err2}"); + assert!( + format!("{err2}").contains("distinct"), + "short list refused: {err2}" + ); // ...but a pick-list with enough distinct values succeeds. let ok = run_seed( &db, @@ -1354,14 +1640,11 @@ fn seed_column_fill_fixed_on_unique_column_is_a_friendly_error() { let rt = rt(); rt.block_on(db.create_table( "U".to_string(), - vec![ - ColumnSpec::new("id", Type::Serial), - { - let mut c = ColumnSpec::new("email", Type::Text); - c.unique = true; - c - }, - ], + vec![ColumnSpec::new("id", Type::Serial), { + let mut c = ColumnSpec::new("email", Type::Text); + c.unique = true; + c + }], vec!["id".to_string()], None, )) diff --git a/tests/it/show_list.rs b/tests/it/show_list.rs index a5f9c69..82c8980 100644 --- a/tests/it/show_list.rs +++ b/tests/it/show_list.rs @@ -17,7 +17,7 @@ use rdbms_playground::action::Action; use rdbms_playground::app::App; use rdbms_playground::db::Database; use rdbms_playground::dsl::{ - parse_command, ColumnSpec, Command, ReferentialAction, ShowListKind, Type, + ColumnSpec, Command, ReferentialAction, ShowListKind, Type, parse_command, }; use rdbms_playground::event::AppEvent; use rdbms_playground::mode::Mode; @@ -108,8 +108,7 @@ fn rt() -> tokio::runtime::Runtime { fn open_project_db() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence(project.db_path(), persistence) .expect("open db with persistence"); @@ -195,8 +194,7 @@ fn show_relationships_lists_name_endpoints_and_nondefault_action() { // Name, both endpoints, and the non-default ON DELETE CASCADE // (ON UPDATE NO ACTION is the default and is omitted). assert_eq!( - lines[1], - " orders_customer: Customers.id → Orders.customer_id on delete cascade", + lines[1], " orders_customer: Customers.id → Orders.customer_id on delete cascade", "relationship summary line: {lines:?}", ); } @@ -222,7 +220,8 @@ fn show_lists_report_empty_collections_with_friendly_lines() { let rt = rt(); // No schema seeded — every kind is empty. assert_eq!( - rt.block_on(db.show_list(ShowListKind::Tables, None)).unwrap(), + rt.block_on(db.show_list(ShowListKind::Tables, None)) + .unwrap(), vec!["No tables in this project yet.".to_string()], ); assert_eq!( @@ -231,7 +230,8 @@ fn show_lists_report_empty_collections_with_friendly_lines() { vec!["No relationships in this project yet.".to_string()], ); assert_eq!( - rt.block_on(db.show_list(ShowListKind::Indexes, None)).unwrap(), + rt.block_on(db.show_list(ShowListKind::Indexes, None)) + .unwrap(), vec!["No indexes in this project yet.".to_string()], ); } @@ -246,7 +246,10 @@ fn show_one_relationship_renders_detail_block() { let rt = rt(); rt.block_on(seed_schema(&db)); let lines = rt - .block_on(db.show_list(ShowListKind::Relationships, Some("orders_customer".to_string()))) + .block_on(db.show_list( + ShowListKind::Relationships, + Some("orders_customer".to_string()), + )) .expect("show relationship"); assert_eq!(lines[0], "Relationship `orders_customer`:"); assert_eq!(lines[1], " Customers.id → Orders.customer_id"); @@ -262,7 +265,10 @@ fn show_one_index_renders_detail_block() { let rt = rt(); rt.block_on(seed_schema(&db)); let lines = rt - .block_on(db.show_list(ShowListKind::Indexes, Some("idx_orders_customer".to_string()))) + .block_on(db.show_list( + ShowListKind::Indexes, + Some("idx_orders_customer".to_string()), + )) .expect("show index"); assert_eq!(lines[0], "Index `idx_orders_customer` on Orders:"); assert!( @@ -329,7 +335,10 @@ fn app_show_tables_dispatches_show_list_command() { } ) }); - assert!(dispatched, "submit dispatches ShowList(Tables): {actions:?}"); + assert!( + dispatched, + "submit dispatches ShowList(Tables): {actions:?}" + ); } #[test] @@ -337,10 +346,11 @@ fn app_renders_show_list_lines_as_system_output() { // Feed the success event directly so the test stays // self-contained (the worker round-trip is covered above). let mut app = App::new(); - app.output.push_back(rdbms_playground::app::OutputLine::echo( - "show tables", - Mode::Simple, - )); + app.output + .push_back(rdbms_playground::app::OutputLine::echo( + "show tables", + Mode::Simple, + )); app.update(AppEvent::DslShowListSucceeded { command: Command::ShowList { kind: ShowListKind::Tables, @@ -403,10 +413,11 @@ fn app_renders_show_relationship_as_a_styled_diagram() { .expect("found"); let mut app = App::new(); - app.output.push_back(rdbms_playground::app::OutputLine::echo( - "show relationship orders_customer", - Mode::Simple, - )); + app.output + .push_back(rdbms_playground::app::OutputLine::echo( + "show relationship orders_customer", + Mode::Simple, + )); app.update(AppEvent::DslShowRelationshipSucceeded { command: Command::ShowList { kind: ShowListKind::Relationships, @@ -423,7 +434,10 @@ fn app_renders_show_relationship_as_a_styled_diagram() { // Both tables, box-drawing, the connector arrow, the actions line. assert!(text.contains("Orders"), "child box: {text}"); assert!(text.contains("Customers"), "parent box: {text}"); - assert!(text.contains('┌') && text.contains('│'), "box drawing: {text}"); + assert!( + text.contains('┌') && text.contains('│'), + "box drawing: {text}" + ); assert!(text.contains('▶'), "connector arrow: {text}"); assert!(text.contains("on delete cascade"), "actions: {text}"); // The diagram lines are styled (per-span runs), not plain system. @@ -436,10 +450,11 @@ fn app_renders_show_relationship_as_a_styled_diagram() { #[test] fn app_show_relationship_not_found_shows_friendly_line() { let mut app = App::new(); - app.output.push_back(rdbms_playground::app::OutputLine::echo( - "show relationship nope", - Mode::Simple, - )); + app.output + .push_back(rdbms_playground::app::OutputLine::echo( + "show relationship nope", + Mode::Simple, + )); app.update(AppEvent::DslShowRelationshipSucceeded { command: Command::ShowList { kind: ShowListKind::Relationships, @@ -466,10 +481,11 @@ fn app_show_table_renders_relationships_as_compact_diagrams() { .expect("describe Orders"); let mut app = App::new(); - app.output.push_back(rdbms_playground::app::OutputLine::echo( - "show table Orders", - Mode::Simple, - )); + app.output + .push_back(rdbms_playground::app::OutputLine::echo( + "show table Orders", + Mode::Simple, + )); app.update(AppEvent::DslSucceeded { command: Command::ShowTable { name: "Orders".to_string(), diff --git a/tests/it/sql_alter_table.rs b/tests/it/sql_alter_table.rs index 401b097..c262a34 100644 --- a/tests/it/sql_alter_table.rs +++ b/tests/it/sql_alter_table.rs @@ -30,8 +30,7 @@ fn rt() -> tokio::runtime::Runtime { fn open() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let db = Database::open_with_persistence( project.db_path(), Persistence::new(project.path().to_path_buf()), @@ -42,8 +41,7 @@ fn open() -> (project::Project, Database, tempfile::TempDir) { fn open_with_undo() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let db = Database::open_with_persistence_and_undo( project.db_path(), Persistence::new(project.path().to_path_buf()), @@ -159,7 +157,10 @@ fn e2e_alter_table_add_rename_drop_and_raw_default_check() { // Final schema: id, label (renamed from v), qty; `note` added then // dropped. let cols = column_names(&db, &r); - assert_eq!(cols, vec!["id".to_string(), "label".to_string(), "qty".to_string()]); + assert_eq!( + cols, + vec!["id".to_string(), "label".to_string(), "qty".to_string()] + ); // The DEFAULT backfilled the pre-existing row to qty = 0. let rows = r @@ -168,14 +169,21 @@ fn e2e_alter_table_add_rename_drop_and_raw_default_check() { .rows; assert_eq!(rows.len(), 1); // qty is the third column; the rebuild backfilled the default. - assert_eq!(rows[0][2].as_deref(), Some("0"), "DEFAULT 0 backfilled the existing row"); + assert_eq!( + rows[0][2].as_deref(), + Some("0"), + "DEFAULT 0 backfilled the existing row" + ); // The CHECK (qty >= 0) is enforced: a negative qty is refused. assert!( r.block_on(db.insert( "T".to_string(), Some(vec!["id".to_string(), "qty".to_string()]), - vec![Value::Number("2".to_string()), Value::Number("-1".to_string())], + vec![ + Value::Number("2".to_string()), + Value::Number("-1".to_string()) + ], Some("insert".to_string()), )) .is_err(), @@ -185,7 +193,10 @@ fn e2e_alter_table_add_rename_drop_and_raw_default_check() { r.block_on(db.insert( "T".to_string(), Some(vec!["id".to_string(), "qty".to_string()]), - vec![Value::Number("3".to_string()), Value::Number("7".to_string())], + vec![ + Value::Number("3".to_string()), + Value::Number("7".to_string()), + ], Some("insert".to_string()), )) .expect("qty = 7 satisfies the CHECK"); @@ -214,7 +225,10 @@ fn e2e_alter_add_column_survives_rebuild() { r.block_on(db.insert( "T".to_string(), Some(vec!["id".to_string(), "qty".to_string()]), - vec![Value::Number("1".to_string()), Value::Number("-5".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Number("-5".to_string()) + ], Some("insert".to_string()), )) .is_err(), @@ -257,9 +271,17 @@ fn e2e_alter_column_type_clean_and_lossy_convert() { .rows; assert_eq!(rows.len(), 1); // v (col 1): lossy real→int performed → 3.7 stored as 3. - assert_eq!(rows[0][1].as_deref(), Some("3"), "lossy real→int performed (3.7→3)"); + assert_eq!( + rows[0][1].as_deref(), + Some("3"), + "lossy real→int performed (3.7→3)" + ); // w (col 2): clean int→text stringified → "42". - assert_eq!(rows[0][2].as_deref(), Some("42"), "clean int→text stringified"); + assert_eq!( + rows[0][2].as_deref(), + Some("42"), + "clean int→text stringified" + ); // The columns now carry the new user-facing types (round-tripped // through the metadata). @@ -290,12 +312,20 @@ fn e2e_alter_column_type_int_to_serial_is_allowed() { } other => panic!("expected ReplayCompleted, got {other:?} (events: {events:?})"), } - assert_eq!(col_type(&db, &r, "n"), Some(Type::Serial), "int→serial converted the column"); + assert_eq!( + col_type(&db, &r, "n"), + Some(Type::Serial), + "int→serial converted the column" + ); let rows = r .block_on(db.query_data("T".to_string(), None, None)) .expect("query") .rows; - assert_eq!(rows[0][1].as_deref(), Some("100"), "the existing value is preserved"); + assert_eq!( + rows[0][1].as_deref(), + Some("100"), + "the existing value is preserved" + ); } #[test] @@ -368,11 +398,19 @@ fn e2e_alter_column_type_survives_rebuild() { ) .expect("write script"); r.block_on(run_replay(&db, project.path(), "conv.commands")); - assert_eq!(col_type(&db, &r, "v"), Some(Type::Int), "converted before rebuild"); + assert_eq!( + col_type(&db, &r, "v"), + Some(Type::Int), + "converted before rebuild" + ); r.block_on(db.rebuild_from_text(project.path().to_path_buf(), Some("rebuild".to_string()))) .expect("rebuild"); - assert_eq!(col_type(&db, &r, "v"), Some(Type::Int), "the converted type survives rebuild"); + assert_eq!( + col_type(&db, &r, "v"), + Some(Type::Int), + "the converted type survives rebuild" + ); } #[test] @@ -393,14 +431,22 @@ fn e2e_alter_column_type_is_one_undo_step() { ) .expect("write script"); r.block_on(run_replay(&db, project.path(), "conv.commands")); - assert_eq!(col_type(&db, &r, "v"), Some(Type::Int), "the SQL ALTER COLUMN TYPE converted v"); + assert_eq!( + col_type(&db, &r, "v"), + Some(Type::Int), + "the SQL ALTER COLUMN TYPE converted v" + ); // A single undo reverts the whole conversion. assert!( r.block_on(db.undo()).expect("undo").is_some(), "the conversion was one undo step" ); - assert_eq!(col_type(&db, &r, "v"), Some(Type::Real), "one undo restored the pre-conversion type"); + assert_eq!( + col_type(&db, &r, "v"), + Some(Type::Real), + "one undo restored the pre-conversion type" + ); } // --- 4g: ADD/DROP constraint + ADD foreign key (ADR-0035 §4g) ----------- @@ -410,7 +456,10 @@ fn insert_t_qty_ok(db: &Database, r: &tokio::runtime::Runtime, id: i64, qty: i64 r.block_on(db.insert( "T".to_string(), Some(vec!["id".to_string(), "qty".to_string()]), - vec![Value::Number(id.to_string()), Value::Number(qty.to_string())], + vec![ + Value::Number(id.to_string()), + Value::Number(qty.to_string()), + ], Some("insert".to_string()), )) .is_ok() @@ -436,14 +485,23 @@ fn e2e_add_named_check_enforced_and_survives_rebuild_with_its_name() { "events: {events:?}" ); // Enforced: qty = -1 refused, qty = 5 accepted. - assert!(!insert_t_qty_ok(&db, &r, 1, -1), "the CHECK rejects qty = -1"); - assert!(insert_t_qty_ok(&db, &r, 2, 5), "qty = 5 satisfies the CHECK"); + assert!( + !insert_t_qty_ok(&db, &r, 1, -1), + "the CHECK rejects qty = -1" + ); + assert!( + insert_t_qty_ok(&db, &r, 2, 5), + "qty = 5 satisfies the CHECK" + ); // Rebuild from text, then DROP CONSTRAINT by name must still work → // the name survived the round-trip. r.block_on(db.rebuild_from_text(project.path().to_path_buf(), Some("rebuild".to_string()))) .expect("rebuild"); - assert!(!insert_t_qty_ok(&db, &r, 3, -2), "the CHECK is intact after rebuild"); + assert!( + !insert_t_qty_ok(&db, &r, 3, -2), + "the CHECK is intact after rebuild" + ); std::fs::write( project.path().join("drop.commands"), "alter table T drop constraint qty_positive\n", @@ -502,13 +560,19 @@ fn e2e_add_composite_unique_enforced_and_survives_rebuild() { )) .is_ok() }; - assert!(!dup_ok(2, 1, 2), "the composite UNIQUE rejects the duplicate (1, 2)"); + assert!( + !dup_ok(2, 1, 2), + "the composite UNIQUE rejects the duplicate (1, 2)" + ); assert!(dup_ok(3, 1, 3), "(1, 3) is distinct and accepted"); // Survives rebuild (the unique_constraints yaml path). r.block_on(db.rebuild_from_text(project.path().to_path_buf(), Some("rebuild".to_string()))) .expect("rebuild"); - assert!(!dup_ok(4, 1, 2), "the composite UNIQUE is intact after rebuild"); + assert!( + !dup_ok(4, 1, 2), + "the composite UNIQUE is intact after rebuild" + ); } #[test] @@ -559,7 +623,10 @@ fn e2e_drop_composite_unique_by_derived_name() { .is_ok() }; assert!(dup_ok(1, 1, 2), "first (1, 2) accepted"); - assert!(!dup_ok(2, 1, 2), "duplicate (1, 2) rejected while the UNIQUE stands"); + assert!( + !dup_ok(2, 1, 2), + "duplicate (1, 2) rejected while the UNIQUE stands" + ); // Drop the UNIQUE by its derived name through the existing DROP // CONSTRAINT grammar. @@ -572,7 +639,10 @@ fn e2e_drop_composite_unique_by_derived_name() { // The UNIQUE no longer enforces: the previously-rejected duplicate is // now accepted. - assert!(dup_ok(3, 1, 2), "duplicate (1, 2) accepted after the UNIQUE was dropped"); + assert!( + dup_ok(3, 1, 2), + "duplicate (1, 2) accepted after the UNIQUE was dropped" + ); // And it stays gone across a rebuild from text. r.block_on(db.rebuild_from_text(project.path().to_path_buf(), Some("rebuild".to_string()))) @@ -676,8 +746,14 @@ fn e2e_add_foreign_key_missing_child_column_refuses_without_dsl_flag() { let AppEvent::ReplayFailed { error, .. } = events.last().expect("an event") else { panic!("expected ReplayFailed; events: {events:?}"); }; - assert!(!error.contains("--create-fk"), "no DSL flag in the SQL refusal; got: {error}"); - assert!(error.contains("pid"), "names the missing column; got: {error}"); + assert!( + !error.contains("--create-fk"), + "no DSL flag in the SQL refusal; got: {error}" + ); + assert!( + error.contains("pid"), + "names the missing column; got: {error}" + ); assert!( error.to_lowercase().contains("add it first") || error.to_lowercase().contains("does not exist"), @@ -709,12 +785,21 @@ fn e2e_add_foreign_key_creates_an_enforced_relationship() { r.block_on(db.insert( "C".to_string(), Some(vec!["cid".to_string(), "pid".to_string()]), - vec![Value::Number(cid.to_string()), Value::Number(pid.to_string())], + vec![ + Value::Number(cid.to_string()), + Value::Number(pid.to_string()), + ], Some("insert".to_string()), )) }; - assert!(insert_c(10, 1).is_ok(), "a child referencing parent id=1 is accepted"); - assert!(insert_c(11, 999).is_err(), "a child referencing a missing parent is rejected"); + assert!( + insert_c(10, 1).is_ok(), + "a child referencing parent id=1 is accepted" + ); + assert!( + insert_c(11, 999).is_err(), + "a child referencing a missing parent is rejected" + ); } #[test] @@ -740,7 +825,10 @@ fn e2e_drop_constraint_removes_a_named_foreign_key() { r.block_on(db.insert( "C".to_string(), Some(vec!["cid".to_string(), "pid".to_string()]), - vec![Value::Number("1".to_string()), Value::Number("999".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Number("999".to_string()) + ], Some("insert".to_string()), )) .is_ok(), @@ -798,7 +886,10 @@ fn e2e_add_constraint_is_one_undo_step() { "the ADD CONSTRAINT was one undo step" ); // After undo the CHECK is gone: qty = -1 is accepted. - assert!(insert_t_qty_ok(&db, &r, 3, -1), "one undo removed the CHECK"); + assert!( + insert_t_qty_ok(&db, &r, 3, -1), + "one undo removed the CHECK" + ); } #[test] @@ -819,7 +910,10 @@ fn e2e_named_check_metadata_survives_a_fresh_rebuild() { .expect("db"); r.block_on(db.sql_create_table( "T".to_string(), - vec![ColumnSpec::new("id", Type::Int), ColumnSpec::new("qty", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Int), + ColumnSpec::new("qty", Type::Int), + ], vec!["id".to_string()], vec![], vec![], @@ -846,7 +940,8 @@ fn e2e_named_check_metadata_survives_a_fresh_rebuild() { Persistence::new(project.path().to_path_buf()), ) .unwrap(); - r.block_on(db.rebuild_from_text(project.path().to_path_buf(), None)).expect("rebuild"); + r.block_on(db.rebuild_from_text(project.path().to_path_buf(), None)) + .expect("rebuild"); // The named CHECK metadata survived: DROP CONSTRAINT by name resolves. r.block_on(db.alter_drop_constraint( @@ -878,7 +973,9 @@ fn e2e_describe_shows_table_level_constraints() { "events: {events:?}" ); - let desc = r.block_on(db.describe_table("T".to_string())).expect("describe"); + let desc = r + .block_on(db.describe_table("T".to_string())) + .expect("describe"); assert_eq!( desc.unique_constraints, vec![vec!["a".to_string(), "b".to_string()]], @@ -890,7 +987,9 @@ fn e2e_describe_shows_table_level_constraints() { .map(|c| (c.name.clone(), c.expr.clone())) .collect(); assert!( - checks.iter().any(|(n, e)| n.is_none() && e.contains("a < b")), + checks + .iter() + .any(|(n, e)| n.is_none() && e.contains("a < b")), "unnamed table CHECK surfaced: {checks:?}" ); assert!( @@ -972,8 +1071,14 @@ fn e2e_rename_table_with_rows_csv_follows_and_survives_rebuild() { tables.contains(&"Purchases".to_string()) && !tables.contains(&"Orders".to_string()), "the table is now Purchases, not Orders: {tables:?}" ); - assert!(csv_path(&project, "Purchases").exists(), "data/Purchases.csv written"); - assert!(!csv_path(&project, "Orders").exists(), "data/Orders.csv removed"); + assert!( + csv_path(&project, "Purchases").exists(), + "data/Purchases.csv written" + ); + assert!( + !csv_path(&project, "Orders").exists(), + "data/Orders.csv removed" + ); let rows = r .block_on(db.query_data("Purchases".to_string(), None, None)) @@ -1052,7 +1157,10 @@ fn e2e_rename_table_with_table_qualified_check_survives_fresh_rebuild() { ], Some("i".into()), )); - assert!(bad_after.is_err(), "the rewritten CHECK enforces after a fresh rebuild"); + assert!( + bad_after.is_err(), + "the rewritten CHECK enforces after a fresh rebuild" + ); } #[test] @@ -1077,7 +1185,9 @@ fn e2e_rename_fk_parent_updates_metadata_and_still_enforces() { ); // The child's outbound relationship now points at the new parent name. - let c = r.block_on(db.describe_table("C".to_string())).expect("describe C"); + let c = r + .block_on(db.describe_table("C".to_string())) + .expect("describe C"); assert_eq!(c.outbound_relationships.len(), 1); assert_eq!(c.outbound_relationships[0].other_table, "Parent"); @@ -1129,7 +1239,9 @@ fn e2e_rename_fk_child_updates_metadata_and_still_enforces() { ); // The parent's inbound relationship now names the renamed child. - let p = r.block_on(db.describe_table("P".to_string())).expect("describe P"); + let p = r + .block_on(db.describe_table("P".to_string())) + .expect("describe P"); assert_eq!(p.inbound_relationships.len(), 1); assert_eq!(p.inbound_relationships[0].other_table, "Child"); @@ -1168,7 +1280,9 @@ fn e2e_rename_self_referential_table_updates_both_ends() { ); // Both ends of the self-reference now name `Tree`. - let t = r.block_on(db.describe_table("Tree".to_string())).expect("describe Tree"); + let t = r + .block_on(db.describe_table("Tree".to_string())) + .expect("describe Tree"); assert_eq!(t.outbound_relationships[0].other_table, "Tree"); assert_eq!(t.inbound_relationships[0].other_table, "Tree"); @@ -1216,7 +1330,9 @@ fn e2e_rename_table_keeps_its_index_with_a_stale_name() { "events: {events:?}" ); - let u = r.block_on(db.describe_table("Users".to_string())).expect("describe Users"); + let u = r + .block_on(db.describe_table("Users".to_string())) + .expect("describe Users"); assert_eq!(u.indexes.len(), 1, "the index followed the rename"); assert_eq!( u.indexes[0].name, "T_email_idx", @@ -1226,7 +1342,9 @@ fn e2e_rename_table_keeps_its_index_with_a_stale_name() { // Survives a fresh rebuild (recreated from IndexSchema on table Users). let db = fresh_rebuild(db, &project, &r); - let u = r.block_on(db.describe_table("Users".to_string())).expect("describe Users"); + let u = r + .block_on(db.describe_table("Users".to_string())) + .expect("describe Users"); assert_eq!(u.indexes.len(), 1); assert_eq!(u.indexes[0].name, "T_email_idx"); } @@ -1248,14 +1366,20 @@ fn e2e_rename_table_is_one_undo_step() { assert!(table_names(&db, &r).contains(&"Purchases".to_string())); // One undo reverts the rename. - assert!(r.block_on(db.undo()).expect("undo").is_some(), "rename was one undo step"); + assert!( + r.block_on(db.undo()).expect("undo").is_some(), + "rename was one undo step" + ); let tables = table_names(&db, &r); assert!( tables.contains(&"Orders".to_string()) && !tables.contains(&"Purchases".to_string()), "undo restored the old table name: {tables:?}" ); assert_eq!( - r.block_on(db.query_data("Orders".to_string(), None, None)).expect("query").rows.len(), + r.block_on(db.query_data("Orders".to_string(), None, None)) + .expect("query") + .rows + .len(), 1, "the row is back under the old name" ); @@ -1286,19 +1410,23 @@ fn e2e_rename_table_refusals() { r.block_on(run_replay(&db, project.path(), "setup.commands")); assert!( - r.block_on(db.rename_table("T".into(), "X".into(), Some("rn".into()))).is_err(), + r.block_on(db.rename_table("T".into(), "X".into(), Some("rn".into()))) + .is_err(), "rename to an existing other table is refused" ); assert!( - r.block_on(db.rename_table("T".into(), "T".into(), Some("rn".into()))).is_err(), + r.block_on(db.rename_table("T".into(), "T".into(), Some("rn".into()))) + .is_err(), "rename to the same name is refused" ); assert!( - r.block_on(db.rename_table("Ghost".into(), "G".into(), Some("rn".into()))).is_err(), + r.block_on(db.rename_table("Ghost".into(), "G".into(), Some("rn".into()))) + .is_err(), "rename of a non-existent table is refused" ); assert!( - r.block_on(db.rename_table("T".into(), "__rdbms_evil".into(), Some("rn".into()))).is_err(), + r.block_on(db.rename_table("T".into(), "__rdbms_evil".into(), Some("rn".into()))) + .is_err(), "rename to an internal table name is refused at the executor" ); @@ -1315,7 +1443,8 @@ fn e2e_rename_table_refusals() { ); } assert!( - r.block_on(db.rename_table("T".into(), "x".into(), Some("rn".into()))).is_err(), + r.block_on(db.rename_table("T".into(), "x".into(), Some("rn".into()))) + .is_err(), "rename to a name colliding case-insensitively with another table (X) is refused" ); @@ -1348,7 +1477,10 @@ fn e2e_alter_column_set_not_null_enforced() { .expect("write"); let events = r.block_on(run_replay(&db, project.path(), "a.commands")); assert!( - matches!(events.last(), Some(AppEvent::ReplayCompleted { count: 4, .. })), + matches!( + events.last(), + Some(AppEvent::ReplayCompleted { count: 4, .. }) + ), "set not null on a clean column succeeds; events: {events:?}" ); assert!( @@ -1390,7 +1522,10 @@ fn e2e_alter_column_drop_not_null_allows_nulls() { .expect("write"); let events = r.block_on(run_replay(&db, project.path(), "a.commands")); assert!( - matches!(events.last(), Some(AppEvent::ReplayCompleted { count: 4, .. })), + matches!( + events.last(), + Some(AppEvent::ReplayCompleted { count: 4, .. }) + ), "events: {events:?}" ); r.block_on(db.insert( @@ -1416,7 +1551,10 @@ fn e2e_alter_column_set_default_applies() { .expect("write"); let events = r.block_on(run_replay(&db, project.path(), "a.commands")); assert!( - matches!(events.last(), Some(AppEvent::ReplayCompleted { count: 3, .. })), + matches!( + events.last(), + Some(AppEvent::ReplayCompleted { count: 3, .. }) + ), "events: {events:?}" ); r.block_on(db.insert( @@ -1462,7 +1600,10 @@ fn e2e_alter_column_drop_default_removes_it() { .expect("write"); let events = r.block_on(run_replay(&db, project.path(), "a.commands")); assert!( - matches!(events.last(), Some(AppEvent::ReplayCompleted { count: 4, .. })), + matches!( + events.last(), + Some(AppEvent::ReplayCompleted { count: 4, .. }) + ), "events: {events:?}" ); r.block_on(db.insert( @@ -1499,7 +1640,10 @@ fn e2e_alter_column_set_data_type_converts() { .expect("write"); let events = r.block_on(run_replay(&db, project.path(), "a.commands")); assert!( - matches!(events.last(), Some(AppEvent::ReplayCompleted { count: 4, .. })), + matches!( + events.last(), + Some(AppEvent::ReplayCompleted { count: 4, .. }) + ), "events: {events:?}" ); assert_eq!( diff --git a/tests/it/sql_create_index.rs b/tests/it/sql_create_index.rs index 442022d..ec3ccb9 100644 --- a/tests/it/sql_create_index.rs +++ b/tests/it/sql_create_index.rs @@ -21,8 +21,7 @@ fn rt() -> tokio::runtime::Runtime { fn open(undo: bool) -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence_and_undo(project.db_path(), persistence, undo) .expect("open db with persistence"); @@ -33,7 +32,10 @@ fn open(undo: bool) -> (project::Project, Database, tempfile::TempDir) { fn make_t(db: &Database, r: &tokio::runtime::Runtime) { r.block_on(db.sql_create_table( "T".to_string(), - vec![ColumnSpec::new("id", Type::Int), ColumnSpec::new("email", Type::Text)], + vec![ + ColumnSpec::new("id", Type::Int), + ColumnSpec::new("email", Type::Text), + ], vec!["id".to_string()], vec![], vec![], @@ -48,8 +50,13 @@ fn insert_row(db: &Database, r: &tokio::runtime::Runtime, id: i64, email: &str) r.block_on(db.insert( "T".to_string(), Some(vec!["id".to_string(), "email".to_string()]), - vec![Value::Number(id.to_string()), Value::Text(email.to_string())], - Some(format!("insert into T (id, email) values ({id}, '{email}')")), + vec![ + Value::Number(id.to_string()), + Value::Text(email.to_string()), + ], + Some(format!( + "insert into T (id, email) values ({id}, '{email}')" + )), )) .is_ok() } @@ -79,7 +86,10 @@ fn create_plain_index() { )) .expect("create index"); assert!(matches!(out, CreateIndexOutcome::Created(_))); - assert_eq!(index(&db, &r, "ix"), Some((vec!["email".to_string()], false))); + assert_eq!( + index(&db, &r, "ix"), + Some((vec!["email".to_string()], false)) + ); } #[test] @@ -97,14 +107,20 @@ fn create_unique_index_round_trips_and_survives_rebuild_and_enforces() { )) .expect("create unique index"); // Reported as unique. - assert_eq!(index(&db, &r, "ux"), Some((vec!["email".to_string()], true))); + assert_eq!( + index(&db, &r, "ux"), + Some((vec!["email".to_string()], true)) + ); // Persisted to project.yaml as a unique index. let yaml = std::fs::read_to_string(p.path().join("project.yaml")).expect("read project.yaml"); assert!(yaml.contains("unique: true"), "project.yaml:\n{yaml}"); // Uniqueness is enforced by the engine. assert!(insert_row(&db, &r, 1, "a@x")); - assert!(!insert_row(&db, &r, 2, "a@x"), "duplicate email refused by the unique index"); + assert!( + !insert_row(&db, &r, 2, "a@x"), + "duplicate email refused by the unique index" + ); // Rebuild from the text artifacts: the index comes back UNIQUE // (the rebuild re-emits CREATE UNIQUE INDEX), not demoted to plain. @@ -116,7 +132,10 @@ fn create_unique_index_round_trips_and_survives_rebuild_and_enforces() { "the unique flag survived rebuild" ); // Still enforced after rebuild. - assert!(!insert_row(&db, &r, 3, "a@x"), "uniqueness enforced after rebuild too"); + assert!( + !insert_row(&db, &r, 3, "a@x"), + "uniqueness enforced after rebuild too" + ); } #[test] @@ -264,7 +283,10 @@ fn plain_duplicate_name_errors() { false, Some("create index ix on T (id)".to_string()), )); - assert!(res.is_err(), "duplicate index name without IF NOT EXISTS errors"); + assert!( + res.is_err(), + "duplicate index name without IF NOT EXISTS errors" + ); } #[test] @@ -307,7 +329,10 @@ fn plain_and_unique_over_the_same_columns_are_not_duplicates() { false, Some("create index ix_plain2 on T (email)".to_string()), )); - assert!(res.is_err(), "a second plain index over the same columns is redundant"); + assert!( + res.is_err(), + "a second plain index over the same columns is redundant" + ); } #[test] @@ -328,7 +353,10 @@ fn create_index_on_an_internal_table_is_refused_on_both_surfaces() { false, Some("create index bad on __rdbms_playground_columns (table_name)".to_string()), )); - assert!(sql.is_err(), "SQL CREATE INDEX on an internal table is refused"); + assert!( + sql.is_err(), + "SQL CREATE INDEX on an internal table is refused" + ); // Simple `add index` on an internal table → error (same guard). let dsl = r.block_on(db.add_index( Some("bad2".to_string()), @@ -336,7 +364,10 @@ fn create_index_on_an_internal_table_is_refused_on_both_surfaces() { vec!["table_name".to_string()], Some("add index as bad2 on __rdbms_playground_columns (table_name)".to_string()), )); - assert!(dsl.is_err(), "simple add index on an internal table is refused"); + assert!( + dsl.is_err(), + "simple add index on an internal table is refused" + ); } #[test] @@ -355,6 +386,9 @@ fn create_index_is_one_undo_step() { .expect("create index"); assert!(index(&db, &r, "ix").is_some()); // One undo removes the index. - assert!(r.block_on(db.undo()).expect("undo").is_some(), "the create was one undo step"); + assert!( + r.block_on(db.undo()).expect("undo").is_some(), + "the create was one undo step" + ); assert!(index(&db, &r, "ix").is_none(), "undo removed the index"); } diff --git a/tests/it/sql_create_table.rs b/tests/it/sql_create_table.rs index e2e6474..d3a4b7d 100644 --- a/tests/it/sql_create_table.rs +++ b/tests/it/sql_create_table.rs @@ -31,8 +31,7 @@ fn rt() -> tokio::runtime::Runtime { fn open(undo: bool) -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence_and_undo(project.db_path(), persistence, undo) .expect("open db with persistence"); @@ -178,7 +177,7 @@ fn if_not_exists_is_a_noop_when_table_exists() { vec![], // no composite UNIQUE vec![], // no table CHECK vec![], // no FK - true, // IF NOT EXISTS + true, // IF NOT EXISTS Some("create table if not exists T (id int)".to_string()), )) .expect("second create should succeed as a no-op"); @@ -245,7 +244,10 @@ fn check_constraint_is_enforced() { let r = rt(); r.block_on(db.sql_create_table( "T".to_string(), - vec![ColumnSpec::new("id", Type::Serial), col_check("price", Type::Real, "price >= 0")], + vec![ + ColumnSpec::new("id", Type::Serial), + col_check("price", Type::Real, "price >= 0"), + ], vec!["id".to_string()], vec![], vec![], // no table CHECK @@ -301,8 +303,16 @@ fn default_is_applied_when_column_omitted() { let data = r .block_on(db.query_data("T".to_string(), None, None)) .expect("query"); - let n_idx = data.columns.iter().position(|c| c == "n").expect("n column"); - assert_eq!(data.rows[0][n_idx].as_deref(), Some("7"), "DEFAULT 7 applied"); + let n_idx = data + .columns + .iter() + .position(|c| c == "n") + .expect("n column"); + assert_eq!( + data.rows[0][n_idx].as_deref(), + Some("7"), + "DEFAULT 7 applied" + ); } #[test] @@ -311,7 +321,10 @@ fn composite_unique_is_enforced() { let r = rt(); r.block_on(db.sql_create_table( "T".to_string(), - vec![ColumnSpec::new("a", Type::Int), ColumnSpec::new("b", Type::Int)], + vec![ + ColumnSpec::new("a", Type::Int), + ColumnSpec::new("b", Type::Int), + ], vec![], vec![vec!["a".to_string(), "b".to_string()]], vec![], // no table CHECK @@ -329,8 +342,12 @@ fn composite_unique_is_enforced() { ) }; r.block_on(ins("1", "2")).expect("first (1,2)"); - assert!(r.block_on(ins("1", "2")).is_err(), "UNIQUE(a,b) rejects duplicate (1,2)"); - r.block_on(ins("1", "3")).expect("distinct (1,3) is allowed"); + assert!( + r.block_on(ins("1", "2")).is_err(), + "UNIQUE(a,b) rejects duplicate (1,2)" + ); + r.block_on(ins("1", "3")) + .expect("distinct (1,3) is allowed"); } #[test] @@ -340,25 +357,27 @@ fn check_default_and_composite_unique_survive_rebuild() { // must all be reconstructed from project.yaml on rebuild. let (p, db, _d) = open(false); let r = rt(); - r.block_on(db.sql_create_table( - "T".to_string(), - vec![ - ColumnSpec::new("a", Type::Int), - ColumnSpec::new("b", Type::Int), - col_check("price", Type::Real, "price >= 0"), - col_default("n", Type::Int, "7"), - ], - vec![], - vec![vec!["a".to_string(), "b".to_string()]], - vec![], // no table CHECK - vec![], // no FK - false, - Some( - "create table T (a int, b int, price real check (price >= 0), \ + r.block_on( + db.sql_create_table( + "T".to_string(), + vec![ + ColumnSpec::new("a", Type::Int), + ColumnSpec::new("b", Type::Int), + col_check("price", Type::Real, "price >= 0"), + col_default("n", Type::Int, "7"), + ], + vec![], + vec![vec!["a".to_string(), "b".to_string()]], + vec![], // no table CHECK + vec![], // no FK + false, + Some( + "create table T (a int, b int, price real check (price >= 0), \ n int default 7, unique (a, b))" - .to_string(), + .to_string(), + ), ), - )) + ) .expect("create"); r.block_on(db.rebuild_from_text(p.path().to_path_buf(), None)) @@ -377,16 +396,30 @@ fn check_default_and_composite_unique_survive_rebuild() { ) }; // CHECK survived: a negative price is rejected. - assert!(r.block_on(ins("1", "1", "-1")).is_err(), "CHECK survived rebuild"); + assert!( + r.block_on(ins("1", "1", "-1")).is_err(), + "CHECK survived rebuild" + ); // A valid row inserts; DEFAULT n=7 survived. r.block_on(ins("1", "1", "5")).expect("valid row"); let data = r .block_on(db.query_data("T".to_string(), None, None)) .expect("query"); - let n_idx = data.columns.iter().position(|c| c == "n").expect("n column"); - assert_eq!(data.rows[0][n_idx].as_deref(), Some("7"), "DEFAULT survived rebuild"); + let n_idx = data + .columns + .iter() + .position(|c| c == "n") + .expect("n column"); + assert_eq!( + data.rows[0][n_idx].as_deref(), + Some("7"), + "DEFAULT survived rebuild" + ); // Composite UNIQUE survived: (1,1) again is rejected. - assert!(r.block_on(ins("1", "1", "5")).is_err(), "composite UNIQUE survived rebuild"); + assert!( + r.block_on(ins("1", "1", "5")).is_err(), + "composite UNIQUE survived rebuild" + ); } #[test] @@ -398,11 +431,14 @@ fn table_level_check_is_enforced() { let r = rt(); r.block_on(db.sql_create_table( "T".to_string(), - vec![ColumnSpec::new("a", Type::Int), ColumnSpec::new("b", Type::Int)], + vec![ + ColumnSpec::new("a", Type::Int), + ColumnSpec::new("b", Type::Int), + ], vec![], vec![], // no composite UNIQUE vec!["a < b".to_string()], // table-level CHECK - vec![], // no FK + vec![], // no FK false, Some("create table T (a int, b int, check (a < b))".to_string()), )) @@ -416,8 +452,14 @@ fn table_level_check_is_enforced() { ) }; r.block_on(ins("1", "2")).expect("(1,2) satisfies a < b"); - assert!(r.block_on(ins("2", "1")).is_err(), "CHECK (a < b) rejects (2,1)"); - assert!(r.block_on(ins("3", "3")).is_err(), "CHECK (a < b) rejects (3,3)"); + assert!( + r.block_on(ins("2", "1")).is_err(), + "CHECK (a < b) rejects (2,1)" + ); + assert!( + r.block_on(ins("3", "3")).is_err(), + "CHECK (a < b) rejects (3,3)" + ); } #[test] @@ -451,9 +493,16 @@ fn multiple_table_level_checks_all_enforced() { Some("insert".to_string()), ) }; - r.block_on(ins("1", "2", "3")).expect("(1,2,3) satisfies both checks"); - assert!(r.block_on(ins("2", "1", "3")).is_err(), "first CHECK (a < b) enforced"); - assert!(r.block_on(ins("1", "3", "2")).is_err(), "second CHECK (b < c) enforced"); + r.block_on(ins("1", "2", "3")) + .expect("(1,2,3) satisfies both checks"); + assert!( + r.block_on(ins("2", "1", "3")).is_err(), + "first CHECK (a < b) enforced" + ); + assert!( + r.block_on(ins("1", "3", "2")).is_err(), + "second CHECK (b < c) enforced" + ); } #[test] @@ -467,7 +516,10 @@ fn dropping_a_table_clears_its_table_check_metadata() { let make = || { db.sql_create_table( "T".to_string(), - vec![ColumnSpec::new("a", Type::Int), ColumnSpec::new("b", Type::Int)], + vec![ + ColumnSpec::new("a", Type::Int), + ColumnSpec::new("b", Type::Int), + ], vec![], vec![], // no composite UNIQUE vec!["a < b".to_string()], @@ -479,7 +531,8 @@ fn dropping_a_table_clears_its_table_check_metadata() { r.block_on(make()).expect("first create"); r.block_on(db.drop_table("T".to_string(), Some("drop table T".to_string()))) .expect("drop"); - r.block_on(make()).expect("re-create must not collide on orphaned CHECK metadata"); + r.block_on(make()) + .expect("re-create must not collide on orphaned CHECK metadata"); // The re-created CHECK is enforced (and there is exactly one of it). let ins = |a: &str, b: &str| { db.insert( @@ -490,7 +543,10 @@ fn dropping_a_table_clears_its_table_check_metadata() { ) }; r.block_on(ins("1", "2")).expect("(1,2) valid"); - assert!(r.block_on(ins("2", "1")).is_err(), "CHECK enforced after re-create"); + assert!( + r.block_on(ins("2", "1")).is_err(), + "CHECK enforced after re-create" + ); } #[test] @@ -505,7 +561,10 @@ fn table_level_check_survives_a_rebuild_triggering_column_add() { let r = rt(); r.block_on(db.sql_create_table( "T".to_string(), - vec![ColumnSpec::new("a", Type::Int), ColumnSpec::new("b", Type::Int)], + vec![ + ColumnSpec::new("a", Type::Int), + ColumnSpec::new("b", Type::Int), + ], vec![], vec![], // no composite UNIQUE vec!["a < b".to_string()], @@ -518,8 +577,12 @@ fn table_level_check_survives_a_rebuild_triggering_column_add() { // A UNIQUE column forces the rebuild path (ADR-0029 §6). let mut c = ColumnSpec::new("c", Type::Int); c.unique = true; - r.block_on(db.add_column("T".to_string(), c, Some("add column T: c(int) unique".to_string()))) - .expect("add column via rebuild"); + r.block_on(db.add_column( + "T".to_string(), + c, + Some("add column T: c(int) unique".to_string()), + )) + .expect("add column via rebuild"); let ins = |a: &str, b: &str, c: &str| { db.insert( @@ -534,8 +597,12 @@ fn table_level_check_survives_a_rebuild_triggering_column_add() { ) }; // Engine still enforces the CHECK right after the rebuild. - r.block_on(ins("1", "2", "10")).expect("(1,2) valid after column add"); - assert!(r.block_on(ins("2", "1", "20")).is_err(), "CHECK survived the column-add rebuild"); + r.block_on(ins("1", "2", "10")) + .expect("(1,2) valid after column add"); + assert!( + r.block_on(ins("2", "1", "20")).is_err(), + "CHECK survived the column-add rebuild" + ); // And the metadata survived too: a fresh rebuild from project.yaml // re-emits the CHECK (it would be lost if the rebuild primitive had @@ -557,7 +624,10 @@ fn table_level_check_survives_rebuild() { let r = rt(); r.block_on(db.sql_create_table( "T".to_string(), - vec![ColumnSpec::new("a", Type::Int), ColumnSpec::new("b", Type::Int)], + vec![ + ColumnSpec::new("a", Type::Int), + ColumnSpec::new("b", Type::Int), + ], vec![], vec![], // no composite UNIQUE vec!["a < b".to_string()], @@ -578,7 +648,8 @@ fn table_level_check_survives_rebuild() { Some("insert".to_string()), ) }; - r.block_on(ins("1", "2")).expect("(1,2) still valid after rebuild"); + r.block_on(ins("1", "2")) + .expect("(1,2) still valid after rebuild"); assert!( r.block_on(ins("5", "4")).is_err(), "table-level CHECK survived rebuild via the metadata table" @@ -645,10 +716,13 @@ fn plain_create_errors_when_table_exists() { vec![], // no composite UNIQUE vec![], // no table CHECK vec![], // no FK - false, // no IF NOT EXISTS + false, // no IF NOT EXISTS Some("create table T (id int)".to_string()), )); - assert!(err.is_err(), "re-creating an existing table without IF NOT EXISTS errors"); + assert!( + err.is_err(), + "re-creating an existing table without IF NOT EXISTS errors" + ); } #[test] @@ -666,12 +740,18 @@ fn sql_create_table_is_one_undo_step() { Some("create table T (id int)".to_string()), )) .expect("create"); - assert!(r.block_on(db.list_tables()).unwrap().contains(&"T".to_string())); + assert!( + r.block_on(db.list_tables()) + .unwrap() + .contains(&"T".to_string()) + ); let undone = r.block_on(db.undo()).expect("undo call"); assert!(undone.is_some(), "the CREATE TABLE recorded one undo step"); assert!( - !r.block_on(db.list_tables()).unwrap().contains(&"T".to_string()), + !r.block_on(db.list_tables()) + .unwrap() + .contains(&"T".to_string()), "table is gone after a single undo" ); } @@ -726,7 +806,11 @@ fn serial_pk_first_column_autoincrements_after_rebuild() { insert_row(&db, &r, "c"); assert_eq!( ids(&db, &r), - vec![Some("1".to_string()), Some("2".to_string()), Some("3".to_string())] + vec![ + Some("1".to_string()), + Some("2".to_string()), + Some("3".to_string()) + ] ); } @@ -755,13 +839,20 @@ fn serial_pk_non_first_column_autoincrements_after_rebuild() { .expect("create"); insert_row(&db, &r, "a"); insert_row(&db, &r, "b"); - assert_eq!(ids(&db, &r), vec![Some("1".to_string()), Some("2".to_string())]); + assert_eq!( + ids(&db, &r), + vec![Some("1".to_string()), Some("2".to_string())] + ); r.block_on(db.rebuild_from_text(p.path().to_path_buf(), None)) .expect("rebuild"); insert_row(&db, &r, "c"); assert_eq!( ids(&db, &r), - vec![Some("1".to_string()), Some("2".to_string()), Some("3".to_string())], + vec![ + Some("1".to_string()), + Some("2".to_string()), + Some("3".to_string()) + ], "serial keeps autoincrement after a rebuild even as a non-first column" ); } @@ -781,7 +872,10 @@ fn dropping_a_column_a_table_check_references_fails_cleanly() { let r = rt(); r.block_on(db.sql_create_table( "T".to_string(), - vec![ColumnSpec::new("a", Type::Int), ColumnSpec::new("b", Type::Int)], + vec![ + ColumnSpec::new("a", Type::Int), + ColumnSpec::new("b", Type::Int), + ], vec![], vec![], // no composite UNIQUE vec!["a < b".to_string()], @@ -797,14 +891,20 @@ fn dropping_a_column_a_table_check_references_fails_cleanly() { false, Some("drop column T: a".to_string()), )); - assert!(dropped.is_err(), "dropping a column a CHECK references is rejected"); + assert!( + dropped.is_err(), + "dropping a column a CHECK references is rejected" + ); // The table is intact: both columns survive (rollback) ... let desc = r .block_on(db.describe_table("T".to_string())) .expect("describe still works"); assert_eq!( - desc.columns.iter().map(|c| c.name.clone()).collect::>(), + desc.columns + .iter() + .map(|c| c.name.clone()) + .collect::>(), vec!["a".to_string(), "b".to_string()], "the failed drop rolled back — no half-migrated table" ); @@ -817,8 +917,12 @@ fn dropping_a_column_a_table_check_references_fails_cleanly() { Some("insert".to_string()), ) }; - r.block_on(ins("1", "2")).expect("(1,2) valid — table survived intact"); - assert!(r.block_on(ins("2", "1")).is_err(), "CHECK still enforced after the failed drop"); + r.block_on(ins("1", "2")) + .expect("(1,2) valid — table survived intact"); + assert!( + r.block_on(ins("2", "1")).is_err(), + "CHECK still enforced after the failed drop" + ); } // ================================================================= @@ -849,7 +953,10 @@ fn fk(child_column: &str, parent_table: &str, parent_column: Option<&str>) -> Sq fn make_parent(db: &Database, r: &tokio::runtime::Runtime) { r.block_on(db.sql_create_table( "parent".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("label", Type::Text)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("label", Type::Text), + ], vec!["id".to_string()], vec![], vec![], @@ -878,13 +985,18 @@ fn foreign_key_is_enforced() { make_parent(&db, &r); r.block_on(db.sql_create_table( "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Int), + ], vec!["id".to_string()], vec![], vec![], vec![fk("pid", "parent", Some("id"))], false, - Some("create table child (id serial primary key, pid int references parent(id))".to_string()), + Some( + "create table child (id serial primary key, pid int references parent(id))".to_string(), + ), )) .expect("create child with FK"); @@ -914,26 +1026,46 @@ fn foreign_key_creates_named_relationship_visible_in_describe() { make_parent(&db, &r); r.block_on(db.sql_create_table( "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Int), + ], vec!["id".to_string()], vec![], vec![], vec![fk("pid", "parent", Some("id"))], false, - Some("create table child (id serial primary key, pid int references parent(id))".to_string()), + Some( + "create table child (id serial primary key, pid int references parent(id))".to_string(), + ), )) .expect("create child with FK"); // The child has an outbound relationship; the parent an inbound one. - let child = r.block_on(db.describe_table("child".to_string())).expect("describe child"); - assert_eq!(child.outbound_relationships.len(), 1, "child references parent"); + let child = r + .block_on(db.describe_table("child".to_string())) + .expect("describe child"); + assert_eq!( + child.outbound_relationships.len(), + 1, + "child references parent" + ); let rel = &child.outbound_relationships[0]; - assert_eq!(rel.name, "parent_id_to_child_pid", "auto-named per ADR-0013"); + assert_eq!( + rel.name, "parent_id_to_child_pid", + "auto-named per ADR-0013" + ); assert_eq!(rel.other_table, "parent"); assert_eq!(rel.local_columns, vec!["pid".to_string()]); - let parent = r.block_on(db.describe_table("parent".to_string())).expect("describe parent"); - assert_eq!(parent.inbound_relationships.len(), 1, "parent is referenced by child"); + let parent = r + .block_on(db.describe_table("parent".to_string())) + .expect("describe parent"); + assert_eq!( + parent.inbound_relationships.len(), + 1, + "parent is referenced by child" + ); } #[test] @@ -954,7 +1086,9 @@ fn explicit_constraint_name_is_used() { Some("create table child (id serial primary key, pid int, constraint child_to_parent foreign key (pid) references parent(id))".to_string()), )) .expect("create child with named FK"); - let child = r.block_on(db.describe_table("child".to_string())).expect("describe"); + let child = r + .block_on(db.describe_table("child".to_string())) + .expect("describe"); assert_eq!(child.outbound_relationships[0].name, "child_to_parent"); } @@ -965,7 +1099,10 @@ fn bare_references_resolves_to_parent_single_column_pk() { make_parent(&db, &r); r.block_on(db.sql_create_table( "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Int), + ], vec!["id".to_string()], vec![], vec![], @@ -974,8 +1111,14 @@ fn bare_references_resolves_to_parent_single_column_pk() { Some("create table child (id serial primary key, pid int references parent)".to_string()), )) .expect("create child with bare REFERENCES"); - let child = r.block_on(db.describe_table("child".to_string())).expect("describe"); - assert_eq!(child.outbound_relationships[0].other_columns, vec!["id".to_string()], "resolved to parent PK"); + let child = r + .block_on(db.describe_table("child".to_string())) + .expect("describe"); + assert_eq!( + child.outbound_relationships[0].other_columns, + vec!["id".to_string()], + "resolved to parent PK" + ); } #[test] @@ -984,7 +1127,10 @@ fn self_referencing_foreign_key_is_enforced() { let r = rt(); r.block_on(db.sql_create_table( "emp".to_string(), - vec![ColumnSpec::new("id", Type::Int), ColumnSpec::new("mgr", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Int), + ColumnSpec::new("mgr", Type::Int), + ], vec!["id".to_string()], vec![], vec![], @@ -1003,11 +1149,19 @@ fn self_referencing_foreign_key_is_enforced() { ) }, ); - db.insert("emp".to_string(), Some(cols), vals, Some("insert".to_string())) + db.insert( + "emp".to_string(), + Some(cols), + vals, + Some("insert".to_string()), + ) }; r.block_on(ins("1", None)).expect("root (mgr NULL)"); r.block_on(ins("2", Some("1"))).expect("emp 2 reports to 1"); - assert!(r.block_on(ins("3", Some("99"))).is_err(), "self-FK rejects mgr=99"); + assert!( + r.block_on(ins("3", Some("99"))).is_err(), + "self-FK rejects mgr=99" + ); } #[test] @@ -1016,17 +1170,28 @@ fn foreign_key_type_mismatch_is_rejected() { let r = rt(); make_parent(&db, &r); // parent.id is serial -> fk_target_type int // child.pid declared text -> incompatible with int. - let res = r.block_on(db.sql_create_table( - "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Text)], - vec!["id".to_string()], - vec![], - vec![], - vec![fk("pid", "parent", Some("id"))], - false, - Some("create table child (id serial primary key, pid text references parent(id))".to_string()), - )); - assert!(res.is_err(), "FK column type must match the parent's fk_target_type"); + let res = r.block_on( + db.sql_create_table( + "child".to_string(), + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Text), + ], + vec!["id".to_string()], + vec![], + vec![], + vec![fk("pid", "parent", Some("id"))], + false, + Some( + "create table child (id serial primary key, pid text references parent(id))" + .to_string(), + ), + ), + ); + assert!( + res.is_err(), + "FK column type must match the parent's fk_target_type" + ); } #[test] @@ -1035,7 +1200,10 @@ fn foreign_key_to_non_pk_column_is_rejected() { let r = rt(); r.block_on(db.sql_create_table( "parent".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("label", Type::Text)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("label", Type::Text), + ], vec!["id".to_string()], vec![], vec![], @@ -1044,16 +1212,24 @@ fn foreign_key_to_non_pk_column_is_rejected() { Some("create table parent (id serial primary key, label text)".to_string()), )) .expect("create parent"); - let res = r.block_on(db.sql_create_table( - "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("plabel", Type::Text)], - vec!["id".to_string()], - vec![], - vec![], - vec![fk("plabel", "parent", Some("label"))], // label is not a PK - false, - Some("create table child (id serial primary key, plabel text references parent(label))".to_string()), - )); + let res = r.block_on( + db.sql_create_table( + "child".to_string(), + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("plabel", Type::Text), + ], + vec!["id".to_string()], + vec![], + vec![], + vec![fk("plabel", "parent", Some("label"))], // label is not a PK + false, + Some( + "create table child (id serial primary key, plabel text references parent(label))" + .to_string(), + ), + ), + ); assert!(res.is_err(), "FK must target a primary key"); } @@ -1064,16 +1240,22 @@ fn foreign_key_survives_rebuild() { make_parent(&db, &r); r.block_on(db.sql_create_table( "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Int), + ], vec!["id".to_string()], vec![], vec![], vec![fk("pid", "parent", Some("id"))], false, - Some("create table child (id serial primary key, pid int references parent(id))".to_string()), + Some( + "create table child (id serial primary key, pid int references parent(id))".to_string(), + ), )) .expect("create child with FK"); - r.block_on(db.rebuild_from_text(p.path().to_path_buf(), None)).expect("rebuild"); + r.block_on(db.rebuild_from_text(p.path().to_path_buf(), None)) + .expect("rebuild"); insert_parent_row(&db, &r); assert!( @@ -1095,21 +1277,37 @@ fn create_table_with_fk_is_one_undo_step() { make_parent(&db, &r); r.block_on(db.sql_create_table( "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Int), + ], vec!["id".to_string()], vec![], vec![], vec![fk("pid", "parent", Some("id"))], false, - Some("create table child (id serial primary key, pid int references parent(id))".to_string()), + Some( + "create table child (id serial primary key, pid int references parent(id))".to_string(), + ), )) .expect("create child with FK"); // One undo removes the child table AND its relationship row, so the // parent (now un-referenced) can be described without a dangling rel. - r.block_on(db.undo()).expect("undo").expect("a step was undone"); - assert!(!r.block_on(db.list_tables()).unwrap().contains(&"child".to_string())); - let parent = r.block_on(db.describe_table("parent".to_string())).expect("describe parent"); - assert!(parent.inbound_relationships.is_empty(), "the relationship was undone with the table"); + r.block_on(db.undo()) + .expect("undo") + .expect("a step was undone"); + assert!( + !r.block_on(db.list_tables()) + .unwrap() + .contains(&"child".to_string()) + ); + let parent = r + .block_on(db.describe_table("parent".to_string())) + .expect("describe parent"); + assert!( + parent.inbound_relationships.is_empty(), + "the relationship was undone with the table" + ); } #[test] @@ -1121,20 +1319,25 @@ fn foreign_key_on_delete_cascade_takes_effect() { make_parent(&db, &r); let mut spec = fk("pid", "parent", Some("id")); spec.on_delete = ReferentialAction::Cascade; - r.block_on(db.sql_create_table( - "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], - vec!["id".to_string()], - vec![], - vec![], - vec![spec], - false, - Some( - "create table child (id serial primary key, pid int references parent(id) \ + r.block_on( + db.sql_create_table( + "child".to_string(), + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Int), + ], + vec!["id".to_string()], + vec![], + vec![], + vec![spec], + false, + Some( + "create table child (id serial primary key, pid int references parent(id) \ on delete cascade)" - .to_string(), + .to_string(), + ), ), - )) + ) .expect("create child with ON DELETE CASCADE"); insert_parent_row(&db, &r); // id=1 r.block_on(db.insert( @@ -1154,7 +1357,10 @@ fn foreign_key_on_delete_cascade_takes_effect() { let child_rows = r .block_on(db.query_data("child".to_string(), None, None)) .expect("query child"); - assert!(child_rows.rows.is_empty(), "ON DELETE CASCADE removed the child row"); + assert!( + child_rows.rows.is_empty(), + "ON DELETE CASCADE removed the child row" + ); } #[test] @@ -1163,17 +1369,29 @@ fn foreign_key_to_unknown_parent_is_rejected() { let r = rt(); let res = r.block_on(db.sql_create_table( "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Int), + ], vec!["id".to_string()], vec![], vec![], vec![fk("pid", "ghost", Some("id"))], // no such table false, - Some("create table child (id serial primary key, pid int references ghost(id))".to_string()), + Some( + "create table child (id serial primary key, pid int references ghost(id))".to_string(), + ), )); - assert!(res.is_err(), "a FK to a non-existent parent table is rejected"); + assert!( + res.is_err(), + "a FK to a non-existent parent table is rejected" + ); // And the failed create left nothing behind. - assert!(!r.block_on(db.list_tables()).unwrap().contains(&"child".to_string())); + assert!( + !r.block_on(db.list_tables()) + .unwrap() + .contains(&"child".to_string()) + ); } #[test] @@ -1182,7 +1400,10 @@ fn composite_pk_bare_reference_is_rejected() { let r = rt(); r.block_on(db.sql_create_table( "cp".to_string(), - vec![ColumnSpec::new("a", Type::Int), ColumnSpec::new("b", Type::Int)], + vec![ + ColumnSpec::new("a", Type::Int), + ColumnSpec::new("b", Type::Int), + ], vec!["a".to_string(), "b".to_string()], // composite PK vec![], vec![], @@ -1194,7 +1415,10 @@ fn composite_pk_bare_reference_is_rejected() { // A bare `REFERENCES cp` cannot disambiguate which PK column. let res = r.block_on(db.sql_create_table( "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("ref", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("ref", Type::Int), + ], vec!["id".to_string()], vec![], vec![], @@ -1202,7 +1426,10 @@ fn composite_pk_bare_reference_is_rejected() { false, Some("create table child (id serial primary key, ref int references cp)".to_string()), )); - assert!(res.is_err(), "bare REFERENCES to a composite-PK parent must be rejected"); + assert!( + res.is_err(), + "bare REFERENCES to a composite-PK parent must be rejected" + ); } #[test] @@ -1216,27 +1443,43 @@ fn fk_survives_a_rebuild_triggering_column_add() { make_parent(&db, &r); r.block_on(db.sql_create_table( "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Int), + ], vec!["id".to_string()], vec![], vec![], vec![fk("pid", "parent", Some("id"))], false, - Some("create table child (id serial primary key, pid int references parent(id))".to_string()), + Some( + "create table child (id serial primary key, pid int references parent(id))".to_string(), + ), )) .expect("create child with FK"); // A UNIQUE column forces the rebuild path. let mut c = ColumnSpec::new("code", Type::Int); c.unique = true; - r.block_on(db.add_column("child".to_string(), c, Some("add column child: code(int) unique".to_string()))) - .expect("add column via rebuild"); + r.block_on(db.add_column( + "child".to_string(), + c, + Some("add column child: code(int) unique".to_string()), + )) + .expect("add column via rebuild"); // The relationship still exists after the rebuild. - let child = r.block_on(db.describe_table("child".to_string())).expect("describe"); - assert_eq!(child.outbound_relationships.len(), 1, "FK survived the column-add rebuild"); + let child = r + .block_on(db.describe_table("child".to_string())) + .expect("describe"); + assert_eq!( + child.outbound_relationships.len(), + 1, + "FK survived the column-add rebuild" + ); // And the engine still enforces it (now and after a fresh rebuild). insert_parent_row(&db, &r); - r.block_on(db.rebuild_from_text(p.path().to_path_buf(), None)).expect("rebuild"); + r.block_on(db.rebuild_from_text(p.path().to_path_buf(), None)) + .expect("rebuild"); assert!( r.block_on(db.insert( "child".to_string(), @@ -1259,26 +1502,42 @@ fn fk_referential_actions_survive_rebuild() { let mut spec = fk("pid", "parent", Some("id")); spec.on_delete = ReferentialAction::Cascade; spec.on_update = ReferentialAction::SetNull; - r.block_on(db.sql_create_table( - "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], - vec!["id".to_string()], - vec![], - vec![], - vec![spec], - false, - Some( - "create table child (id serial primary key, pid int references parent(id) \ + r.block_on( + db.sql_create_table( + "child".to_string(), + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Int), + ], + vec!["id".to_string()], + vec![], + vec![], + vec![spec], + false, + Some( + "create table child (id serial primary key, pid int references parent(id) \ on delete cascade on update set null)" - .to_string(), + .to_string(), + ), ), - )) + ) .expect("create"); - r.block_on(db.rebuild_from_text(p.path().to_path_buf(), None)).expect("rebuild"); - let child = r.block_on(db.describe_table("child".to_string())).expect("describe"); + r.block_on(db.rebuild_from_text(p.path().to_path_buf(), None)) + .expect("rebuild"); + let child = r + .block_on(db.describe_table("child".to_string())) + .expect("describe"); let rel = &child.outbound_relationships[0]; - assert_eq!(rel.on_delete, ReferentialAction::Cascade, "ON DELETE survived rebuild"); - assert_eq!(rel.on_update, ReferentialAction::SetNull, "ON UPDATE survived rebuild"); + assert_eq!( + rel.on_delete, + ReferentialAction::Cascade, + "ON DELETE survived rebuild" + ); + assert_eq!( + rel.on_update, + ReferentialAction::SetNull, + "ON UPDATE survived rebuild" + ); } #[test] @@ -1288,19 +1547,29 @@ fn dropping_the_child_clears_the_fk_relationship() { make_parent(&db, &r); r.block_on(db.sql_create_table( "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Int), + ], vec!["id".to_string()], vec![], vec![], vec![fk("pid", "parent", Some("id"))], false, - Some("create table child (id serial primary key, pid int references parent(id))".to_string()), + Some( + "create table child (id serial primary key, pid int references parent(id))".to_string(), + ), )) .expect("create"); r.block_on(db.drop_table("child".to_string(), Some("drop table child".to_string()))) .expect("drop child"); - let parent = r.block_on(db.describe_table("parent".to_string())).expect("describe parent"); - assert!(parent.inbound_relationships.is_empty(), "dropping the child cleared the relationship"); + let parent = r + .block_on(db.describe_table("parent".to_string())) + .expect("describe parent"); + assert!( + parent.inbound_relationships.is_empty(), + "dropping the child cleared the relationship" + ); } #[test] @@ -1311,17 +1580,23 @@ fn dropping_a_referenced_parent_is_refused() { make_parent(&db, &r); r.block_on(db.sql_create_table( "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Int), + ], vec!["id".to_string()], vec![], vec![], vec![fk("pid", "parent", Some("id"))], false, - Some("create table child (id serial primary key, pid int references parent(id))".to_string()), + Some( + "create table child (id serial primary key, pid int references parent(id))".to_string(), + ), )) .expect("create"); assert!( - r.block_on(db.drop_table("parent".to_string(), Some("drop table parent".to_string()))).is_err(), + r.block_on(db.drop_table("parent".to_string(), Some("drop table parent".to_string()))) + .is_err(), "a referenced parent can't be dropped while the child's FK exists" ); } @@ -1332,7 +1607,10 @@ fn bare_self_reference_resolves_to_own_pk() { let r = rt(); r.block_on(db.sql_create_table( "emp".to_string(), - vec![ColumnSpec::new("id", Type::Int), ColumnSpec::new("mgr", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Int), + ColumnSpec::new("mgr", Type::Int), + ], vec!["id".to_string()], vec![], vec![], @@ -1341,8 +1619,14 @@ fn bare_self_reference_resolves_to_own_pk() { Some("create table emp (id int primary key, mgr int references emp)".to_string()), )) .expect("create self-referential emp with a bare reference"); - let emp = r.block_on(db.describe_table("emp".to_string())).expect("describe"); - assert_eq!(emp.outbound_relationships[0].other_columns, vec!["id".to_string()], "bare self-ref resolved to own PK"); + let emp = r + .block_on(db.describe_table("emp".to_string())) + .expect("describe"); + assert_eq!( + emp.outbound_relationships[0].other_columns, + vec!["id".to_string()], + "bare self-ref resolved to own PK" + ); // Enforced: a non-existent manager is rejected. r.block_on(db.insert( "emp".to_string(), @@ -1355,7 +1639,10 @@ fn bare_self_reference_resolves_to_own_pk() { r.block_on(db.insert( "emp".to_string(), Some(vec!["id".to_string(), "mgr".to_string()]), - vec![Value::Number("2".to_string()), Value::Number("99".to_string())], + vec![ + Value::Number("2".to_string()), + Value::Number("99".to_string()) + ], Some("insert".to_string()), )) .is_err(), diff --git a/tests/it/sql_delete.rs b/tests/it/sql_delete.rs index 8f95463..7fe4cc9 100644 --- a/tests/it/sql_delete.rs +++ b/tests/it/sql_delete.rs @@ -34,8 +34,7 @@ fn rt() -> tokio::runtime::Runtime { fn open_project_db() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence(project.db_path(), persistence) .expect("open db with persistence"); @@ -83,9 +82,11 @@ fn run_delete( input: &str, ) -> Result { match parse_command(input).expect("parse delete") { - Command::SqlDelete { sql, target_table, returning } => { - rt.block_on(db.run_sql_delete(sql, Some(input.to_string()), target_table, returning)) - } + Command::SqlDelete { + sql, + target_table, + returning, + } => rt.block_on(db.run_sql_delete(sql, Some(input.to_string()), target_table, returning)), other => panic!("expected Command::SqlDelete, got {other:?}"), } } @@ -95,8 +96,20 @@ fn run_delete( /// `ON DELETE CASCADE`. Seeds Alice (1) with two orders (10, 11) /// and Bob (2) with one order (12). fn cascade_fixture(db: &Database, rt: &tokio::runtime::Runtime) { - create_cols(db, rt, "Customers", &[("id", Type::Int), ("Name", Type::Text)], &["id"]); - create_cols(db, rt, "Orders", &[("id", Type::Int), ("CustId", Type::Int)], &["id"]); + create_cols( + db, + rt, + "Customers", + &[("id", Type::Int), ("Name", Type::Text)], + &["id"], + ); + create_cols( + db, + rt, + "Orders", + &[("id", Type::Int), ("CustId", Type::Int)], + &["id"], + ); rt.block_on(db.add_relationship( Some("places".to_string()), "Customers".to_string(), @@ -109,16 +122,28 @@ fn cascade_fixture(db: &Database, rt: &tokio::runtime::Runtime) { None, )) .expect("add cascade relationship"); - seed(db, rt, "insert into Customers (id, Name) values (1, 'Alice'), (2, 'Bob')", "Customers"); - seed(db, rt, "insert into Orders (id, CustId) values (10, 1), (11, 1), (12, 2)", "Orders"); + seed( + db, + rt, + "insert into Customers (id, Name) values (1, 'Alice'), (2, 'Bob')", + "Customers", + ); + seed( + db, + rt, + "insert into Orders (id, CustId) values (10, 1), (11, 1), (12, 2)", + "Orders", + ); } #[test] fn parse_path_lowers_sql_delete_to_command() { - let command = parse_command("delete from Orders where id = 1") - .expect("delete parses in advanced mode"); + let command = + parse_command("delete from Orders where id = 1").expect("delete parses in advanced mode"); match command { - Command::SqlDelete { sql, target_table, .. } => { + Command::SqlDelete { + sql, target_table, .. + } => { assert_eq!(sql, "delete from Orders where id = 1"); assert_eq!(target_table, "Orders"); } @@ -130,14 +155,28 @@ fn parse_path_lowers_sql_delete_to_command() { fn delete_with_where_persists() { let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]); - seed(&db, &rt, "insert into t (id, v) values (1, 'gone'), (2, 'keep')", "t"); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("v", Type::Text)], + &["id"], + ); + seed( + &db, + &rt, + "insert into t (id, v) values (1, 'gone'), (2, 'keep')", + "t", + ); let result = run_delete(&db, &rt, "delete from t where id = 1").expect("delete runs"); assert_eq!(result.rows_affected, 1, "one row deleted"); assert!(result.cascade.is_empty(), "no children, no cascade"); let csv = read_csv(&project, "t").expect("t.csv"); assert!(csv.contains("keep"), "untouched row preserved: {csv:?}"); - assert!(!csv.contains("gone"), "deleted row removed from CSV: {csv:?}"); + assert!( + !csv.contains("gone"), + "deleted row removed from CSV: {csv:?}" + ); } #[test] @@ -145,18 +184,35 @@ fn delete_without_where_runs_across_all_rows() { // ADR-0030 §12: no `--all-rows` rail. let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]); - seed(&db, &rt, "insert into t (id, v) values (1, 'a'), (2, 'b'), (3, 'c')", "t"); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("v", Type::Text)], + &["id"], + ); + seed( + &db, + &rt, + "insert into t (id, v) values (1, 'a'), (2, 'b'), (3, 'c')", + "t", + ); let result = run_delete(&db, &rt, "delete from t").expect("unfiltered delete runs"); assert_eq!(result.rows_affected, 3, "all rows deleted"); // Empty tables produce no CSV (CLAUDE.md persistence note), so the // file is either absent or has only a header — either way, no data. let csv = read_csv(&project, "t").unwrap_or_default(); - assert!(!csv.contains('a') && !csv.contains('b') && !csv.contains('c'), "no rows left: {csv:?}"); + assert!( + !csv.contains('a') && !csv.contains('b') && !csv.contains('c'), + "no rows left: {csv:?}" + ); let remaining = rt .block_on(db.query_data("t".to_string(), None, None)) .expect("query t"); - assert!(remaining.rows.is_empty(), "table empty after unfiltered delete"); + assert!( + remaining.rows.is_empty(), + "table empty after unfiltered delete" + ); } #[test] @@ -165,8 +221,8 @@ fn cascade_delete_reports_summary_and_repersists_child() { let rt = rt(); cascade_fixture(&db, &rt); // Delete Alice (customer 1) — cascades to her two orders (10, 11). - let result = run_delete(&db, &rt, "delete from Customers where id = 1") - .expect("cascading delete runs"); + let result = + run_delete(&db, &rt, "delete from Customers where id = 1").expect("cascading delete runs"); assert_eq!(result.rows_affected, 1, "one parent row deleted"); assert_eq!(result.cascade.len(), 1, "one cascade relationship reported"); let effect = &result.cascade[0]; @@ -177,9 +233,14 @@ fn cascade_delete_reports_summary_and_repersists_child() { assert_eq!(effect.rows_changed, 2, "both of Alice's orders cascaded"); // The child CSV must be re-persisted to reflect the cascade. let orders_csv = read_csv(&project, "Orders").expect("Orders.csv"); - assert!(orders_csv.contains("12"), "Bob's order (12) preserved: {orders_csv:?}"); - assert!(!orders_csv.contains("10") && !orders_csv.contains("11"), - "Alice's cascaded orders gone from CSV: {orders_csv:?}"); + assert!( + orders_csv.contains("12"), + "Bob's order (12) preserved: {orders_csv:?}" + ); + assert!( + !orders_csv.contains("10") && !orders_csv.contains("11"), + "Alice's cascaded orders gone from CSV: {orders_csv:?}" + ); } #[test] @@ -193,8 +254,8 @@ fn cascade_parity_with_dsl() { let (_p_sql, db_sql, _d_sql) = open_project_db(); cascade_fixture(&db_sql, &rt); - let sql_result = run_delete(&db_sql, &rt, "delete from Customers where id = 1") - .expect("SQL delete runs"); + let sql_result = + run_delete(&db_sql, &rt, "delete from Customers where id = 1").expect("SQL delete runs"); let (_p_dsl, db_dsl, _d_dsl) = open_project_db(); cascade_fixture(&db_dsl, &rt); @@ -206,8 +267,14 @@ fn cascade_parity_with_dsl() { )) .expect("DSL delete runs"); - assert_eq!(sql_result.rows_affected, dsl_result.rows_affected, "row counts match"); - assert_eq!(sql_result.cascade, dsl_result.cascade, "cascade summaries identical"); + assert_eq!( + sql_result.rows_affected, dsl_result.rows_affected, + "row counts match" + ); + assert_eq!( + sql_result.cascade, dsl_result.cascade, + "cascade summaries identical" + ); } #[test] @@ -229,9 +296,14 @@ fn r2_where_with_subquery() { assert_eq!(result.rows_affected, 2, "Alice's two orders deleted"); assert!(result.cascade.is_empty(), "Orders has no cascade children"); let orders_csv = read_csv(&project, "Orders").expect("Orders.csv"); - assert!(orders_csv.contains("12"), "Bob's order preserved: {orders_csv:?}"); - assert!(!orders_csv.contains("10") && !orders_csv.contains("11"), - "Alice's orders deleted: {orders_csv:?}"); + assert!( + orders_csv.contains("12"), + "Bob's order preserved: {orders_csv:?}" + ); + assert!( + !orders_csv.contains("10") && !orders_csv.contains("11"), + "Alice's orders deleted: {orders_csv:?}" + ); } #[test] @@ -255,10 +327,15 @@ fn r2_cascade_with_subquery_where() { .expect("cascade + subquery-WHERE delete runs"); assert_eq!(result.rows_affected, 1, "Alice deleted"); assert_eq!(result.cascade.len(), 1, "one cascade relationship"); - assert_eq!(result.cascade[0].rows_changed, 2, "both Alice orders cascaded"); + assert_eq!( + result.cascade[0].rows_changed, 2, + "both Alice orders cascaded" + ); let orders_csv = read_csv(&project, "Orders").expect("Orders.csv"); - assert!(orders_csv.contains("12") && !orders_csv.contains("10") && !orders_csv.contains("11"), - "only Bob's order remains: {orders_csv:?}"); + assert!( + orders_csv.contains("12") && !orders_csv.contains("10") && !orders_csv.contains("11"), + "only Bob's order remains: {orders_csv:?}" + ); } #[test] @@ -269,9 +346,27 @@ fn cascade_to_two_children_reports_both() { // emitting more than one effect. let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "Customers", &[("id", Type::Int), ("Name", Type::Text)], &["id"]); - create_cols(&db, &rt, "Orders", &[("id", Type::Int), ("CustId", Type::Int)], &["id"]); - create_cols(&db, &rt, "Reviews", &[("id", Type::Int), ("CustId", Type::Int)], &["id"]); + create_cols( + &db, + &rt, + "Customers", + &[("id", Type::Int), ("Name", Type::Text)], + &["id"], + ); + create_cols( + &db, + &rt, + "Orders", + &[("id", Type::Int), ("CustId", Type::Int)], + &["id"], + ); + create_cols( + &db, + &rt, + "Reviews", + &[("id", Type::Int), ("CustId", Type::Int)], + &["id"], + ); for (child, name) in [("Orders", "places"), ("Reviews", "writes")] { rt.block_on(db.add_relationship( Some(name.to_string()), @@ -286,14 +381,33 @@ fn cascade_to_two_children_reports_both() { )) .unwrap_or_else(|e| panic!("add rel {name}: {e:?}")); } - seed(&db, &rt, "insert into Customers (id, Name) values (1, 'Alice')", "Customers"); - seed(&db, &rt, "insert into Orders (id, CustId) values (10, 1), (11, 1)", "Orders"); - seed(&db, &rt, "insert into Reviews (id, CustId) values (20, 1)", "Reviews"); + seed( + &db, + &rt, + "insert into Customers (id, Name) values (1, 'Alice')", + "Customers", + ); + seed( + &db, + &rt, + "insert into Orders (id, CustId) values (10, 1), (11, 1)", + "Orders", + ); + seed( + &db, + &rt, + "insert into Reviews (id, CustId) values (20, 1)", + "Reviews", + ); let result = run_delete(&db, &rt, "delete from Customers where id = 1") .expect("cascade-to-two delete runs"); assert_eq!(result.rows_affected, 1); - assert_eq!(result.cascade.len(), 2, "both cascade relationships reported"); + assert_eq!( + result.cascade.len(), + 2, + "both cascade relationships reported" + ); let by_child: std::collections::HashMap<&str, i64> = result .cascade .iter() @@ -302,9 +416,16 @@ fn cascade_to_two_children_reports_both() { assert_eq!(by_child.get("Orders"), Some(&2), "two orders cascaded"); assert_eq!(by_child.get("Reviews"), Some(&1), "one review cascaded"); // Both child CSVs re-persisted to the post-cascade (empty) state. - let orders = rt.block_on(db.query_data("Orders".to_string(), None, None)).unwrap(); - let reviews = rt.block_on(db.query_data("Reviews".to_string(), None, None)).unwrap(); - assert!(orders.rows.is_empty() && reviews.rows.is_empty(), "both children emptied"); + let orders = rt + .block_on(db.query_data("Orders".to_string(), None, None)) + .unwrap(); + let reviews = rt + .block_on(db.query_data("Reviews".to_string(), None, None)) + .unwrap(); + assert!( + orders.rows.is_empty() && reviews.rows.is_empty(), + "both children emptied" + ); let _ = &project; } @@ -318,11 +439,19 @@ fn delete_childless_parent_reports_no_cascade() { let rt = rt(); cascade_fixture(&db, &rt); // Carol (3) exists with no orders; deleting her cascades nothing. - seed(&db, &rt, "insert into Customers (id, Name) values (3, 'Carol')", "Customers"); + seed( + &db, + &rt, + "insert into Customers (id, Name) values (3, 'Carol')", + "Customers", + ); let result = run_delete(&db, &rt, "delete from Customers where id = 3") .expect("childless-parent delete runs"); assert_eq!(result.rows_affected, 1, "Carol deleted"); - assert!(result.cascade.is_empty(), "no children → no cascade effect reported"); + assert!( + result.cascade.is_empty(), + "no children → no cascade effect reported" + ); // All three orders untouched. let orders_csv = read_csv(&project, "Orders").expect("Orders.csv"); assert!( @@ -340,8 +469,20 @@ fn delete_violating_fk_fails_and_persists_nothing() { // parent row survives and history records no line. let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "Customers", &[("id", Type::Int), ("Name", Type::Text)], &["id"]); - create_cols(&db, &rt, "Orders", &[("id", Type::Int), ("CustId", Type::Int)], &["id"]); + create_cols( + &db, + &rt, + "Customers", + &[("id", Type::Int), ("Name", Type::Text)], + &["id"], + ); + create_cols( + &db, + &rt, + "Orders", + &[("id", Type::Int), ("CustId", Type::Int)], + &["id"], + ); rt.block_on(db.add_relationship( Some("places".to_string()), "Customers".to_string(), @@ -354,18 +495,40 @@ fn delete_violating_fk_fails_and_persists_nothing() { None, )) .expect("add NO ACTION relationship"); - seed(&db, &rt, "insert into Customers (id, Name) values (1, 'Alice')", "Customers"); - seed(&db, &rt, "insert into Orders (id, CustId) values (10, 1)", "Orders"); + seed( + &db, + &rt, + "insert into Customers (id, Name) values (1, 'Alice')", + "Customers", + ); + seed( + &db, + &rt, + "insert into Orders (id, CustId) values (10, 1)", + "Orders", + ); let input = "delete from Customers where id = 1"; let result = run_delete(&db, &rt, input); - assert!(result.is_err(), "delete of a referenced parent must be rejected"); + assert!( + result.is_err(), + "delete of a referenced parent must be rejected" + ); // Rolled back: Alice survives. - let customers = rt.block_on(db.query_data("Customers".to_string(), None, None)).unwrap(); - assert_eq!(customers.rows.len(), 1, "parent row preserved after rejected delete"); + let customers = rt + .block_on(db.query_data("Customers".to_string(), None, None)) + .unwrap(); + assert_eq!( + customers.rows.len(), + 1, + "parent row preserved after rejected delete" + ); // No history line for the failed statement (written only on success). let history = std::fs::read_to_string(project.path().join("history.log")).unwrap_or_default(); - assert!(!history.contains(input), "failed delete not logged: {history:?}"); + assert!( + !history.contains(input), + "failed delete not logged: {history:?}" + ); } #[test] @@ -378,7 +541,13 @@ fn self_referential_cascade_counts_only_cascaded_rows() { // direct delete). Without the self-ref correction this reports 3. let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "T", &[("id", Type::Int), ("ParentId", Type::Int)], &["id"]); + create_cols( + &db, + &rt, + "T", + &[("id", Type::Int), ("ParentId", Type::Int)], + &["id"], + ); rt.block_on(db.add_relationship( Some("parent_of".to_string()), "T".to_string(), @@ -391,11 +560,22 @@ fn self_referential_cascade_counts_only_cascaded_rows() { None, )) .expect("add self-referential relationship"); - seed(&db, &rt, "insert into T (id, ParentId) values (1, null), (2, 1), (3, 2)", "T"); - let result = - run_delete(&db, &rt, "delete from T where id = 1").expect("self-ref delete runs"); - assert_eq!(result.rows_affected, 1, "one row matched the WHERE directly"); - assert_eq!(result.cascade.len(), 1, "self-ref relationship reported once"); + seed( + &db, + &rt, + "insert into T (id, ParentId) values (1, null), (2, 1), (3, 2)", + "T", + ); + let result = run_delete(&db, &rt, "delete from T where id = 1").expect("self-ref delete runs"); + assert_eq!( + result.rows_affected, 1, + "one row matched the WHERE directly" + ); + assert_eq!( + result.cascade.len(), + 1, + "self-ref relationship reported once" + ); assert_eq!( result.cascade[0].rows_changed, 2, "only the 2 cascaded rows, not the directly-deleted root too" @@ -421,8 +601,19 @@ fn internal_target_table_rejected_at_parse() { fn delete_returning_yields_predelete_row() { let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]); - seed(&db, &rt, "insert into t (id, v) values (1, 'gone'), (2, 'keep')", "t"); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("v", Type::Text)], + &["id"], + ); + seed( + &db, + &rt, + "insert into t (id, v) values (1, 'gone'), (2, 'keep')", + "t", + ); let result = run_delete(&db, &rt, "delete from t where id = 1 returning *") .expect("DELETE … RETURNING * runs"); assert_eq!(result.rows_affected, 1, "one row deleted"); @@ -431,7 +622,10 @@ fn delete_returning_yields_predelete_row() { assert_eq!(result.data.rows[0][1], Some("gone".to_string())); // And it really is gone from the table. let csv = read_csv(&project, "t").expect("t.csv"); - assert!(!csv.contains("gone") && csv.contains("keep"), "row actually deleted: {csv:?}"); + assert!( + !csv.contains("gone") && csv.contains("keep"), + "row actually deleted: {csv:?}" + ); } #[test] @@ -449,5 +643,8 @@ fn delete_returning_with_cascade_surfaces_both() { // Cascade summary still computed alongside the result set. assert_eq!(result.cascade.len(), 1, "cascade reported"); assert_eq!(result.cascade[0].child_table, "Orders"); - assert_eq!(result.cascade[0].rows_changed, 2, "both Alice's orders cascaded"); + assert_eq!( + result.cascade[0].rows_changed, 2, + "both Alice's orders cascaded" + ); } diff --git a/tests/it/sql_dml_e2e.rs b/tests/it/sql_dml_e2e.rs index d316c08..9cf74fd 100644 --- a/tests/it/sql_dml_e2e.rs +++ b/tests/it/sql_dml_e2e.rs @@ -55,8 +55,7 @@ fn rt() -> tokio::runtime::Runtime { fn open_project_db() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence(project.db_path(), persistence) .expect("open db with persistence"); @@ -116,15 +115,18 @@ fn run_update( input: &str, ) -> Result { match parse_command(input).unwrap_or_else(|e| panic!("parse {input:?}: {e:?}")) { - Command::SqlUpdate { sql, target_table, returning, set_literals } => rt.block_on( - db.run_sql_update_with_literals( - sql, - Some(input.to_string()), - target_table, - returning, - set_literals, - ), - ), + Command::SqlUpdate { + sql, + target_table, + returning, + set_literals, + } => rt.block_on(db.run_sql_update_with_literals( + sql, + Some(input.to_string()), + target_table, + returning, + set_literals, + )), other => panic!("expected Command::SqlUpdate from {input:?}, got {other:?}"), } } @@ -135,9 +137,11 @@ fn run_delete( input: &str, ) -> Result { match parse_command(input).unwrap_or_else(|e| panic!("parse {input:?}: {e:?}")) { - Command::SqlDelete { sql, target_table, returning } => rt.block_on( - db.run_sql_delete(sql, Some(input.to_string()), target_table, returning), - ), + Command::SqlDelete { + sql, + target_table, + returning, + } => rt.block_on(db.run_sql_delete(sql, Some(input.to_string()), target_table, returning)), other => panic!("expected Command::SqlDelete from {input:?}, got {other:?}"), } } @@ -162,9 +166,25 @@ fn query(db: &Database, rt: &tokio::runtime::Runtime, table: &str) -> Vec = rows.iter().filter_map(|r| r[9].as_deref()).collect(); assert_eq!(sids.len(), 2, "both shortids present"); - assert!(sids.iter().all(|s| !s.is_empty()), "shortids non-empty: {sids:?}"); - assert_ne!(sids[0], sids[1], "auto-filled shortids are distinct: {sids:?}"); + assert!( + sids.iter().all(|s| !s.is_empty()), + "shortids non-empty: {sids:?}" + ); + assert_ne!( + sids[0], sids[1], + "auto-filled shortids are distinct: {sids:?}" + ); let sers: Vec<&str> = rows.iter().filter_map(|r| r[0].as_deref()).collect(); - assert!(sers.contains(&"1") && sers.contains(&"2"), "serial auto-incremented: {sers:?}"); + assert!( + sers.contains(&"1") && sers.contains(&"2"), + "serial auto-incremented: {sers:?}" + ); } // =============================================================== @@ -275,18 +310,34 @@ fn e2e_update_with_subquery_in_set() { &db, &rt, "customers", - &[("id", Type::Int), ("name", Type::Text), ("last_order", Type::Int)], + &[ + ("id", Type::Int), + ("name", Type::Text), + ("last_order", Type::Int), + ], &["id"], ); create_cols( &db, &rt, "orders", - &[("id", Type::Int), ("cust", Type::Int), ("amount", Type::Int)], + &[ + ("id", Type::Int), + ("cust", Type::Int), + ("amount", Type::Int), + ], &["id"], ); - seed(&db, &rt, "insert into customers (id, name, last_order) values (1, 'A', 0), (2, 'B', 0)"); - seed(&db, &rt, "insert into orders (id, cust, amount) values (10, 1, 50), (11, 1, 30), (12, 2, 99)"); + seed( + &db, + &rt, + "insert into customers (id, name, last_order) values (1, 'A', 0), (2, 'B', 0)", + ); + seed( + &db, + &rt, + "insert into orders (id, cust, amount) values (10, 1, 50), (11, 1, 30), (12, 2, 99)", + ); let result = run_update( &db, @@ -298,13 +349,26 @@ fn e2e_update_with_subquery_in_set() { assert_eq!(result.rows_affected, 2, "both customers updated"); let rows = query(&db, &rt, "customers"); - let c1 = rows.iter().find(|r| r[0].as_deref() == Some("1")).expect("customer 1"); - let c2 = rows.iter().find(|r| r[0].as_deref() == Some("2")).expect("customer 2"); - assert_eq!(c1[2].as_deref(), Some("50"), "customer 1 → max(50, 30) = 50"); + let c1 = rows + .iter() + .find(|r| r[0].as_deref() == Some("1")) + .expect("customer 1"); + let c2 = rows + .iter() + .find(|r| r[0].as_deref() == Some("2")) + .expect("customer 2"); + assert_eq!( + c1[2].as_deref(), + Some("50"), + "customer 1 → max(50, 30) = 50" + ); assert_eq!(c2[2].as_deref(), Some("99"), "customer 2 → max(99) = 99"); let csv = read_csv(&project, "customers").expect("customers.csv"); - assert!(csv.contains("50") && csv.contains("99"), "CSV reflects the update: {csv:?}"); + assert!( + csv.contains("50") && csv.contains("99"), + "CSV reflects the update: {csv:?}" + ); } // =============================================================== @@ -313,8 +377,20 @@ fn e2e_update_with_subquery_in_set() { // =============================================================== fn cascade_fixture(db: &Database, rt: &tokio::runtime::Runtime) { - create_cols(db, rt, "Customers", &[("id", Type::Int), ("Name", Type::Text)], &["id"]); - create_cols(db, rt, "Orders", &[("id", Type::Int), ("CustId", Type::Int)], &["id"]); + create_cols( + db, + rt, + "Customers", + &[("id", Type::Int), ("Name", Type::Text)], + &["id"], + ); + create_cols( + db, + rt, + "Orders", + &[("id", Type::Int), ("CustId", Type::Int)], + &["id"], + ); rt.block_on(db.add_relationship( Some("places".to_string()), "Customers".to_string(), @@ -327,8 +403,16 @@ fn cascade_fixture(db: &Database, rt: &tokio::runtime::Runtime) { None, )) .expect("add cascade relationship"); - seed(db, rt, "insert into Customers (id, Name) values (1, 'Alice'), (2, 'Bob')"); - seed(db, rt, "insert into Orders (id, CustId) values (10, 1), (11, 1), (12, 2)"); + seed( + db, + rt, + "insert into Customers (id, Name) values (1, 'Alice'), (2, 'Bob')", + ); + seed( + db, + rt, + "insert into Orders (id, CustId) values (10, 1), (11, 1), (12, 2)", + ); } #[test] @@ -337,8 +421,8 @@ fn e2e_delete_with_cascade_reports_summary_and_repersists_children() { let rt = rt(); cascade_fixture(&db, &rt); - let result = run_delete(&db, &rt, "delete from Customers where id = 1") - .expect("cascading DELETE runs"); + let result = + run_delete(&db, &rt, "delete from Customers where id = 1").expect("cascading DELETE runs"); assert_eq!(result.rows_affected, 1, "one parent row deleted"); assert_eq!(result.cascade.len(), 1, "one cascade relationship affected"); let effect = &result.cascade[0]; @@ -346,9 +430,18 @@ fn e2e_delete_with_cascade_reports_summary_and_repersists_children() { assert_eq!(effect.rows_changed, 2, "Alice's two orders cascaded"); let orders_csv = read_csv(&project, "Orders").expect("Orders.csv re-persisted"); - assert!(orders_csv.contains("12"), "Bob's order (12) preserved: {orders_csv:?}"); - assert!(!orders_csv.contains("10"), "Alice's order 10 cascaded away: {orders_csv:?}"); - assert!(!orders_csv.contains("11"), "Alice's order 11 cascaded away: {orders_csv:?}"); + assert!( + orders_csv.contains("12"), + "Bob's order (12) preserved: {orders_csv:?}" + ); + assert!( + !orders_csv.contains("10"), + "Alice's order 10 cascaded away: {orders_csv:?}" + ); + assert!( + !orders_csv.contains("11"), + "Alice's order 11 cascaded away: {orders_csv:?}" + ); } // =============================================================== @@ -359,7 +452,13 @@ fn e2e_delete_with_cascade_reports_summary_and_repersists_children() { fn e2e_upsert_round_trip_do_update_then_do_nothing() { let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "kv", &[("id", Type::Int), ("name", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "kv", + &[("id", Type::Int), ("name", Type::Text)], + &["id"], + ); seed(&db, &rt, "insert into kv (id, name) values (1, 'old')"); // DO UPDATE on a conflict mutates the existing row. @@ -369,9 +468,15 @@ fn e2e_upsert_round_trip_do_update_then_do_nothing() { "insert into kv (id, name) values (1, 'new') on conflict (id) do update set name = excluded.name", ) .expect("UPSERT DO UPDATE runs"); - assert_eq!(upd.rows_affected, 1, "DO UPDATE touches the conflicting row"); + assert_eq!( + upd.rows_affected, 1, + "DO UPDATE touches the conflicting row" + ); let csv = read_csv(&project, "kv").expect("kv.csv"); - assert!(csv.contains("new") && !csv.contains("old"), "row updated to 'new': {csv:?}"); + assert!( + csv.contains("new") && !csv.contains("old"), + "row updated to 'new': {csv:?}" + ); // DO NOTHING on a conflict is a no-op. let nothing = run_insert( @@ -382,7 +487,10 @@ fn e2e_upsert_round_trip_do_update_then_do_nothing() { .expect("UPSERT DO NOTHING runs"); assert_eq!(nothing.rows_affected, 0, "DO NOTHING changes no rows"); let csv = read_csv(&project, "kv").expect("kv.csv"); - assert!(csv.contains("new") && !csv.contains("ignored"), "row unchanged by DO NOTHING: {csv:?}"); + assert!( + csv.contains("new") && !csv.contains("ignored"), + "row unchanged by DO NOTHING: {csv:?}" + ); } // =============================================================== @@ -393,23 +501,52 @@ fn e2e_upsert_round_trip_do_update_then_do_nothing() { fn e2e_returning_on_insert_update_delete() { let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("v", Type::Text)], + &["id"], + ); - let ins = run_insert(&db, &rt, "insert into t (id, v) values (1, 'a') returning id, v") - .expect("INSERT … RETURNING runs"); - assert_eq!(ins.data.rows.len(), 1, "INSERT RETURNING yields the inserted row"); + let ins = run_insert( + &db, + &rt, + "insert into t (id, v) values (1, 'a') returning id, v", + ) + .expect("INSERT … RETURNING runs"); + assert_eq!( + ins.data.rows.len(), + 1, + "INSERT RETURNING yields the inserted row" + ); assert_eq!(ins.data.rows[0][1].as_deref(), Some("a")); let upd = run_update(&db, &rt, "update t set v = 'b' where id = 1 returning v") .expect("UPDATE … RETURNING runs"); - assert_eq!(upd.data.rows.len(), 1, "UPDATE RETURNING yields the modified row"); + assert_eq!( + upd.data.rows.len(), + 1, + "UPDATE RETURNING yields the modified row" + ); assert_eq!(upd.data.rows[0][0].as_deref(), Some("b")); let del = run_delete(&db, &rt, "delete from t where id = 1 returning *") .expect("DELETE … RETURNING runs"); - assert_eq!(del.data.rows.len(), 1, "DELETE RETURNING yields the pre-delete row"); - assert_eq!(del.data.rows[0][1].as_deref(), Some("b"), "pre-delete value surfaced"); - assert!(query(&db, &rt, "t").is_empty(), "row is gone after the DELETE"); + assert_eq!( + del.data.rows.len(), + 1, + "DELETE RETURNING yields the pre-delete row" + ); + assert_eq!( + del.data.rows[0][1].as_deref(), + Some("b"), + "pre-delete value surfaced" + ); + assert!( + query(&db, &rt, "t").is_empty(), + "row is gone after the DELETE" + ); } // =============================================================== @@ -463,7 +600,7 @@ fn e2e_replay_phase3_dml_forms_from_a_script() { ("1".to_string(), Some("a".to_string())), ("11".to_string(), Some("a".to_string())), // INSERT…SELECT id+10 ("2".to_string(), Some("z".to_string())), // UPDATE - // id 3 was DELETEd + // id 3 was DELETEd ] .into_iter() .collect::>() @@ -481,15 +618,42 @@ fn e2e_replay_phase3_dml_forms_from_a_script() { fn e2e_out_of_scope_dml_forms_parse_reject() { let cases = [ ("OOS-1 DEFAULT VALUES", "insert into t default values"), - ("OOS-2 INSERT OR REPLACE", "insert or replace into t values (1)"), - ("OOS-2 INSERT OR IGNORE", "insert or ignore into t values (1)"), - ("OOS-3 UPDATE … FROM", "update t set a = b.x from other b where t.id = b.id"), - ("OOS-4 WITH … UPDATE", "with x as (select 1) update t set a = 1 where id = 1"), - ("OOS-4 WITH … DELETE", "with x as (select 1) delete from t where id = 1"), - ("OOS-5 INDEXED BY", "delete from t indexed by idx where id = 1"), - ("OOS-5 NOT INDEXED", "update t not indexed set a = 1 where id = 1"), - ("OOS-6 multi-statement (DELETE; DELETE)", "delete from t where id = 1; delete from t where id = 2"), - ("OOS-6 multi-statement (INSERT; INSERT)", "insert into t values (1); insert into t values (2)"), + ( + "OOS-2 INSERT OR REPLACE", + "insert or replace into t values (1)", + ), + ( + "OOS-2 INSERT OR IGNORE", + "insert or ignore into t values (1)", + ), + ( + "OOS-3 UPDATE … FROM", + "update t set a = b.x from other b where t.id = b.id", + ), + ( + "OOS-4 WITH … UPDATE", + "with x as (select 1) update t set a = 1 where id = 1", + ), + ( + "OOS-4 WITH … DELETE", + "with x as (select 1) delete from t where id = 1", + ), + ( + "OOS-5 INDEXED BY", + "delete from t indexed by idx where id = 1", + ), + ( + "OOS-5 NOT INDEXED", + "update t not indexed set a = 1 where id = 1", + ), + ( + "OOS-6 multi-statement (DELETE; DELETE)", + "delete from t where id = 1; delete from t where id = 2", + ), + ( + "OOS-6 multi-statement (INSERT; INSERT)", + "insert into t values (1); insert into t values (2)", + ), ]; for (label, src) in cases { assert!( @@ -525,7 +689,10 @@ fn e2e_update_all_rows_in_advanced_falls_back_to_dsl() { assert!( matches!( parse_command_in_mode("update Orders set total = 42 --all-rows", Mode::Advanced), - Ok(Command::Update { filter: RowFilter::AllRows, .. }) + Ok(Command::Update { + filter: RowFilter::AllRows, + .. + }) ), "advanced `update … --all-rows` falls back to the DSL Update", ); @@ -599,5 +766,8 @@ fn e2e_validity_indicator_fires_for_sql_dml_diagnostic() { // And the indicator renders the `[WRN]` label. app.input_indicator = app.input_validity_verdict(); let text = rendered_text(&mut app, &Theme::dark(), 80, 24); - assert!(text.contains("[WRN]"), "the SQL DML warning surfaces as [WRN]:\n{text}"); + assert!( + text.contains("[WRN]"), + "the SQL DML warning surfaces as [WRN]:\n{text}" + ); } diff --git a/tests/it/sql_drop_index.rs b/tests/it/sql_drop_index.rs index 6ae5127..dcfb74e 100644 --- a/tests/it/sql_drop_index.rs +++ b/tests/it/sql_drop_index.rs @@ -21,8 +21,7 @@ fn rt() -> tokio::runtime::Runtime { fn open(undo: bool) -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence_and_undo(project.db_path(), persistence, undo) .expect("open db with persistence"); @@ -33,7 +32,10 @@ fn open(undo: bool) -> (project::Project, Database, tempfile::TempDir) { fn make_t_with_index(db: &Database, r: &tokio::runtime::Runtime) -> String { r.block_on(db.sql_create_table( "T".to_string(), - vec![ColumnSpec::new("id", Type::Int), ColumnSpec::new("email", Type::Text)], + vec![ + ColumnSpec::new("id", Type::Int), + ColumnSpec::new("email", Type::Text), + ], vec!["id".to_string()], vec![], vec![], @@ -75,7 +77,10 @@ fn drop_index_removes_an_existing_index_and_shows_the_table() { match out { DropIndexOutcome::Dropped(desc) => { assert_eq!(desc.name, "T"); - assert!(desc.indexes.is_empty(), "the index is gone from the structure"); + assert!( + desc.indexes.is_empty(), + "the index is gone from the structure" + ); } DropIndexOutcome::Skipped => panic!("expected Dropped, got Skipped"), } @@ -105,7 +110,10 @@ fn plain_drop_of_an_absent_index_errors() { false, Some("drop index ghost_idx".to_string()), )); - assert!(res.is_err(), "plain DROP INDEX on an absent index errors (no IF EXISTS)"); + assert!( + res.is_err(), + "plain DROP INDEX on an absent index errors (no IF EXISTS)" + ); } #[test] @@ -113,11 +121,18 @@ fn drop_index_is_one_undo_step_and_restores_the_index() { let (_p, db, _d) = open(true); // undo enabled let r = rt(); let name = make_t_with_index(&db, &r); - r.block_on(db.sql_drop_index(name.clone(), false, Some("drop index T_email_idx".to_string()))) - .expect("drop index"); + r.block_on(db.sql_drop_index( + name.clone(), + false, + Some("drop index T_email_idx".to_string()), + )) + .expect("drop index"); assert!(index_names(&db, &r).is_empty()); // One undo brings the index back. - assert!(r.block_on(db.undo()).expect("undo").is_some(), "the drop was one undo step"); + assert!( + r.block_on(db.undo()).expect("undo").is_some(), + "the drop was one undo step" + ); assert_eq!(index_names(&db, &r), vec![name], "undo restored the index"); } diff --git a/tests/it/sql_drop_table.rs b/tests/it/sql_drop_table.rs index 5ea6550..69c323d 100644 --- a/tests/it/sql_drop_table.rs +++ b/tests/it/sql_drop_table.rs @@ -22,8 +22,7 @@ fn rt() -> tokio::runtime::Runtime { fn open(undo: bool) -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence_and_undo(project.db_path(), persistence, undo) .expect("open db with persistence"); @@ -34,7 +33,10 @@ fn open(undo: bool) -> (project::Project, Database, tempfile::TempDir) { fn make_t(db: &Database, r: &tokio::runtime::Runtime) { r.block_on(db.sql_create_table( "T".to_string(), - vec![ColumnSpec::new("id", Type::Int), ColumnSpec::new("body", Type::Text)], + vec![ + ColumnSpec::new("id", Type::Int), + ColumnSpec::new("body", Type::Text), + ], vec!["id".to_string()], vec![], vec![], @@ -54,7 +56,11 @@ fn drop_table_removes_an_existing_table() { .block_on(db.sql_drop_table("T".to_string(), false, Some("drop table T".to_string()))) .expect("drop"); assert!(matches!(out, DropOutcome::Dropped)); - assert!(!r.block_on(db.list_tables()).unwrap().contains(&"T".to_string())); + assert!( + !r.block_on(db.list_tables()) + .unwrap() + .contains(&"T".to_string()) + ); } #[test] @@ -75,8 +81,15 @@ fn if_exists_on_an_absent_table_is_a_noop_and_journalled() { fn plain_drop_of_an_absent_table_errors() { let (_p, db, _d) = open(false); let r = rt(); - let res = r.block_on(db.sql_drop_table("Ghost".to_string(), false, Some("drop table Ghost".to_string()))); - assert!(res.is_err(), "plain DROP TABLE on an absent table errors (no IF EXISTS)"); + let res = r.block_on(db.sql_drop_table( + "Ghost".to_string(), + false, + Some("drop table Ghost".to_string()), + )); + assert!( + res.is_err(), + "plain DROP TABLE on an absent table errors (no IF EXISTS)" + ); } #[test] @@ -87,7 +100,10 @@ fn dropping_a_referenced_parent_is_refused() { let r = rt(); r.block_on(db.sql_create_table( "parent".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("label", Type::Text)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("label", Type::Text), + ], vec!["id".to_string()], vec![], vec![], @@ -98,7 +114,10 @@ fn dropping_a_referenced_parent_is_refused() { .expect("create parent"); r.block_on(db.sql_create_table( "child".to_string(), - vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], + vec![ + ColumnSpec::new("id", Type::Serial), + ColumnSpec::new("pid", Type::Int), + ], vec!["id".to_string()], vec![], vec![], @@ -112,22 +131,36 @@ fn dropping_a_referenced_parent_is_refused() { inline: true, }], false, - Some("create table child (id serial primary key, pid int references parent(id))".to_string()), + Some( + "create table child (id serial primary key, pid int references parent(id))".to_string(), + ), )) .expect("create child with FK"); // The parent is referenced — refused (even with IF EXISTS, since the // table *does* exist; the refusal is about the relationship). assert!( - r.block_on(db.sql_drop_table("parent".to_string(), false, Some("drop table parent".to_string()))) - .is_err(), + r.block_on(db.sql_drop_table( + "parent".to_string(), + false, + Some("drop table parent".to_string()) + )) + .is_err(), "a referenced parent can't be dropped" ); // Dropping the child first succeeds, then the parent. - r.block_on(db.sql_drop_table("child".to_string(), false, Some("drop table child".to_string()))) - .expect("drop child"); - r.block_on(db.sql_drop_table("parent".to_string(), false, Some("drop table parent".to_string()))) - .expect("now the parent drops"); + r.block_on(db.sql_drop_table( + "child".to_string(), + false, + Some("drop table child".to_string()), + )) + .expect("drop child"); + r.block_on(db.sql_drop_table( + "parent".to_string(), + false, + Some("drop table parent".to_string()), + )) + .expect("now the parent drops"); } #[test] @@ -138,17 +171,31 @@ fn drop_table_is_one_undo_step_and_restores_data() { r.block_on(db.insert( "T".to_string(), Some(vec!["id".to_string(), "body".to_string()]), - vec![Value::Number("1".to_string()), Value::Text("hi".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Text("hi".to_string()), + ], Some("insert".to_string()), )) .expect("row"); r.block_on(db.sql_drop_table("T".to_string(), false, Some("drop table T".to_string()))) .expect("drop"); - assert!(!r.block_on(db.list_tables()).unwrap().contains(&"T".to_string())); + assert!( + !r.block_on(db.list_tables()) + .unwrap() + .contains(&"T".to_string()) + ); // One undo brings the table — and its row — back. - assert!(r.block_on(db.undo()).expect("undo").is_some(), "the drop was one undo step"); - assert!(r.block_on(db.list_tables()).unwrap().contains(&"T".to_string())); + assert!( + r.block_on(db.undo()).expect("undo").is_some(), + "the drop was one undo step" + ); + assert!( + r.block_on(db.list_tables()) + .unwrap() + .contains(&"T".to_string()) + ); let data = r .block_on(db.query_data("T".to_string(), None, None)) .expect("query"); diff --git a/tests/it/sql_insert.rs b/tests/it/sql_insert.rs index fdd71ec..5d75ed8 100644 --- a/tests/it/sql_insert.rs +++ b/tests/it/sql_insert.rs @@ -41,8 +41,7 @@ fn rt() -> tokio::runtime::Runtime { fn open_project_db() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence(project.db_path(), persistence) .expect("open db with persistence"); @@ -86,7 +85,10 @@ fn single_row_insert_persists_and_counts() { .expect("insert runs"); assert_eq!(result.rows_affected, 1, "one row inserted"); let csv = read_csv(&project, "T").expect("T.csv written after insert"); - assert!(csv.contains("Ada"), "CSV reflects the inserted row: {csv:?}"); + assert!( + csv.contains("Ada"), + "CSV reflects the inserted row: {csv:?}" + ); } #[test] @@ -193,7 +195,10 @@ fn failed_multi_row_insert_is_atomic() { String::new(), false, )); - assert!(outcome.is_err(), "multi-row PK conflict must fail: {outcome:?}"); + assert!( + outcome.is_err(), + "multi-row PK conflict must fail: {outcome:?}" + ); let csv = read_csv(&project, "T").expect("T.csv still present"); assert!( csv.contains("existing") && !csv.contains("fresh") && !csv.contains("collides"), @@ -208,7 +213,9 @@ fn parse_path_lowers_sqlinsert_scaffold_to_command() { let command = parse_command("insert into Orders (id, total) values (1, 99.5)") .expect("insert parses in advanced mode"); match command { - Command::SqlInsert { sql, target_table, .. } => { + Command::SqlInsert { + sql, target_table, .. + } => { assert_eq!(sql, "insert into Orders (id, total) values (1, 99.5)"); assert_eq!(target_table, "Orders"); } @@ -248,7 +255,9 @@ fn parse_path_lowers_insert_select_to_command() { let command = parse_command("insert into archive select * from source") .expect("INSERT … SELECT parses in advanced mode"); match command { - Command::SqlInsert { sql, target_table, .. } => { + Command::SqlInsert { + sql, target_table, .. + } => { assert_eq!(sql, "insert into archive select * from source"); assert_eq!(target_table, "archive"); } @@ -259,12 +268,13 @@ fn parse_path_lowers_insert_select_to_command() { #[test] fn parse_path_lowers_with_prefixed_insert_select() { // R4: a WITH-prefixed SELECT row source lowers verbatim. - let command = parse_command( - "insert into archive with t as (select * from orders) select * from t", - ) - .expect("WITH-prefixed INSERT … SELECT parses"); + let command = + parse_command("insert into archive with t as (select * from orders) select * from t") + .expect("WITH-prefixed INSERT … SELECT parses"); match command { - Command::SqlInsert { sql, target_table, .. } => { + Command::SqlInsert { + sql, target_table, .. + } => { assert_eq!( sql, "insert into archive with t as (select * from orders) select * from t", @@ -472,7 +482,13 @@ fn csv_rows(project: &project::Project, table: &str) -> Vec> { fn values_autofills_omitted_shortid_pk() { let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::ShortId), ("label", Type::Text)], + &["id"], + ); let result = run_sqlinsert(&db, &rt, "insert into t (label) values ('x')") .expect("auto-fill insert runs"); assert_eq!(result.rows_affected, 1); @@ -486,25 +502,36 @@ fn values_autofills_omitted_shortid_pk() { fn values_multirow_autofills_distinct_shortids() { let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]); - let result = run_sqlinsert( + create_cols( &db, &rt, - "insert into t (label) values ('a'), ('b'), ('c')", - ) - .expect("multi-row auto-fill runs"); + "t", + &[("id", Type::ShortId), ("label", Type::Text)], + &["id"], + ); + let result = run_sqlinsert(&db, &rt, "insert into t (label) values ('a'), ('b'), ('c')") + .expect("multi-row auto-fill runs"); assert_eq!(result.rows_affected, 3); let rows = csv_rows(&project, "t"); let ids: std::collections::HashSet<&String> = rows.iter().map(|r| &r[0]).collect(); assert_eq!(ids.len(), 3, "three DISTINCT non-empty shortids: {rows:?}"); - assert!(rows.iter().all(|r| !r[0].is_empty()), "no empty id: {rows:?}"); + assert!( + rows.iter().all(|r| !r[0].is_empty()), + "no empty id: {rows:?}" + ); } #[test] fn explicit_shortid_value_is_respected() { let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::ShortId), ("label", Type::Text)], + &["id"], + ); // The user provided `id` explicitly — it must be honoured // verbatim (the override WARNING is sub-phase 3i). run_sqlinsert( @@ -521,10 +548,21 @@ fn explicit_shortid_value_is_respected() { fn insert_select_autofills_distinct_shortids() { let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "source", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]); - create_cols(&db, &rt, "target", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]); - run_sqlinsert(&db, &rt, "insert into source (label) values ('a'), ('b')") - .expect("seed source"); + create_cols( + &db, + &rt, + "source", + &[("id", Type::ShortId), ("label", Type::Text)], + &["id"], + ); + create_cols( + &db, + &rt, + "target", + &[("id", Type::ShortId), ("label", Type::Text)], + &["id"], + ); + run_sqlinsert(&db, &rt, "insert into source (label) values ('a'), ('b')").expect("seed source"); let result = run_sqlinsert( &db, &rt, @@ -535,7 +573,10 @@ fn insert_select_autofills_distinct_shortids() { let rows = csv_rows(&project, "target"); let ids: std::collections::HashSet<&String> = rows.iter().map(|r| &r[0]).collect(); assert_eq!(ids.len(), 2, "two DISTINCT fresh shortids: {rows:?}"); - assert!(rows.iter().all(|r| !r[0].is_empty()), "no empty id: {rows:?}"); + assert!( + rows.iter().all(|r| !r[0].is_empty()), + "no empty id: {rows:?}" + ); } #[test] @@ -547,11 +588,14 @@ fn combined_serial_and_shortid_autofill() { &db, &rt, "t", - &[("id", Type::Serial), ("code", Type::ShortId), ("name", Type::Text)], + &[ + ("id", Type::Serial), + ("code", Type::ShortId), + ("name", Type::Text), + ], &["id"], ); - run_sqlinsert(&db, &rt, "insert into t (name) values ('x')") - .expect("combined auto-fill runs"); + run_sqlinsert(&db, &rt, "insert into t (name) values ('x')").expect("combined auto-fill runs"); let rows = csv_rows(&project, "t"); assert_eq!(rows.len(), 1, "{rows:?}"); assert_eq!(rows[0][0], "1", "serial PK engine-filled: {rows:?}"); @@ -566,7 +610,13 @@ fn shortid_autofill_respects_mixed_case_column_name() { // schema name `MyId`, not a lowercased form. let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("MyId", Type::ShortId), ("label", Type::Text)], &["MyId"]); + create_cols( + &db, + &rt, + "t", + &[("MyId", Type::ShortId), ("label", Type::Text)], + &["MyId"], + ); run_sqlinsert(&db, &rt, "insert into t (label) values ('x')") .expect("mixed-case shortid auto-fill runs"); let rows = csv_rows(&project, "t"); @@ -584,7 +634,11 @@ fn two_shortids_pk_and_nonpk_both_autofill_distinctly() { &db, &rt, "t", - &[("id", Type::ShortId), ("code", Type::ShortId), ("label", Type::Text)], + &[ + ("id", Type::ShortId), + ("code", Type::ShortId), + ("label", Type::Text), + ], &["id"], ); let result = run_sqlinsert(&db, &rt, "insert into t (label) values ('x'), ('y')") @@ -611,7 +665,11 @@ fn two_shortids_one_provided_one_autofilled() { &db, &rt, "t", - &[("id", Type::ShortId), ("code", Type::ShortId), ("label", Type::Text)], + &[ + ("id", Type::ShortId), + ("code", Type::ShortId), + ("label", Type::Text), + ], &["id"], ); run_sqlinsert(&db, &rt, "insert into t (id, label) values ('myid', 'x')") @@ -631,7 +689,11 @@ fn compound_pk_with_shortid_member_autofills() { &db, &rt, "t", - &[("id", Type::ShortId), ("region", Type::Int), ("label", Type::Text)], + &[ + ("id", Type::ShortId), + ("region", Type::Int), + ("label", Type::Text), + ], &["id", "region"], ); run_sqlinsert(&db, &rt, "insert into t (region, label) values (1, 'x')") @@ -653,14 +715,23 @@ fn autofill_does_not_mask_arity_mismatch() { // engine rather than mask the error.) let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::ShortId), ("label", Type::Text)], + &["id"], + ); let outcome = run_sqlinsert(&db, &rt, "insert into t (label) values ('a', 'b')"); assert!( outcome.is_err(), "arity mismatch must be rejected, not masked: {outcome:?}", ); let rows = csv_rows(&project, "t"); - assert!(rows.is_empty(), "no row should land on a rejected insert: {rows:?}"); + assert!( + rows.is_empty(), + "no row should land on a rejected insert: {rows:?}" + ); } #[test] @@ -672,14 +743,19 @@ fn sql_insert_autofills_omitted_nonpk_serial() { // shortid auto-fill, which already runs on this path. let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("seq", Type::Serial)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("seq", Type::Serial)], + &["id"], + ); // Single row, omitting the non-PK serial `seq`. run_sqlinsert(&db, &rt, "insert into t (id) values (10)").expect("single-row insert runs"); // Multi-row, omitting `seq` — each row gets a distinct, increasing // serial continuing from the current MAX. - run_sqlinsert(&db, &rt, "insert into t (id) values (20), (30)") - .expect("multi-row insert runs"); + run_sqlinsert(&db, &rt, "insert into t (id) values (20), (30)").expect("multi-row insert runs"); let rows = csv_rows(&project, "t"); // No NULL serials, and the sequence is 1, 2, 3 across the three rows. @@ -700,11 +776,26 @@ fn autofill_insert_select_wider_projection_is_rejected() { // to the engine instead of dropping the extra projection. let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "src", &[("a", Type::Text), ("b", Type::Text)], &["a"]); - create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "src", + &[("a", Type::Text), ("b", Type::Text)], + &["a"], + ); + create_cols( + &db, + &rt, + "t", + &[("id", Type::ShortId), ("label", Type::Text)], + &["id"], + ); run_sqlinsert(&db, &rt, "insert into src (a, b) values ('p', 'q')").expect("seed"); let outcome = run_sqlinsert(&db, &rt, "insert into t (label) select a, b from src"); - assert!(outcome.is_err(), "wider projection must be rejected: {outcome:?}"); + assert!( + outcome.is_err(), + "wider projection must be rejected: {outcome:?}" + ); assert!(csv_rows(&project, "t").is_empty(), "nothing should land"); } @@ -724,7 +815,10 @@ fn autofill_insert_select_narrower_projection_is_rejected() { ); run_sqlinsert(&db, &rt, "insert into src (a) values ('p')").expect("seed"); let outcome = run_sqlinsert(&db, &rt, "insert into t (x, y) select a from src"); - assert!(outcome.is_err(), "narrower projection must be rejected: {outcome:?}"); + assert!( + outcome.is_err(), + "narrower projection must be rejected: {outcome:?}" + ); assert!(csv_rows(&project, "t").is_empty(), "nothing should land"); } @@ -736,11 +830,25 @@ fn autofill_insert_select_narrower_projection_is_rejected() { fn insert_returning_star_returns_inserted_row() { let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("b", Type::Text)], &["id"]); - let result = run_sqlinsert(&db, &rt, "insert into t (id, b) values (1, 'Ada') returning *") - .expect("INSERT … RETURNING * runs"); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("b", Type::Text)], + &["id"], + ); + let result = run_sqlinsert( + &db, + &rt, + "insert into t (id, b) values (1, 'Ada') returning *", + ) + .expect("INSERT … RETURNING * runs"); assert_eq!(result.rows_affected, 1, "one row inserted"); - assert_eq!(result.data.rows.len(), 1, "RETURNING yielded the inserted row"); + assert_eq!( + result.data.rows.len(), + 1, + "RETURNING yielded the inserted row" + ); assert_eq!(result.data.columns, vec!["id".to_string(), "b".to_string()]); assert_eq!(result.data.rows[0][1], Some("Ada".to_string())); } @@ -749,7 +857,13 @@ fn insert_returning_star_returns_inserted_row() { fn insert_multirow_returning_id_yields_distinct_rows() { let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("b", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("b", Type::Text)], + &["id"], + ); let result = run_sqlinsert( &db, &rt, @@ -760,7 +874,12 @@ fn insert_multirow_returning_id_yields_distinct_rows() { assert_eq!(result.data.columns, vec!["id".to_string()]); let ids: std::collections::BTreeSet<_> = result.data.rows.iter().map(|r| r[0].clone()).collect(); - assert_eq!(ids.len(), 3, "three distinct ids returned: {:?}", result.data.rows); + assert_eq!( + ids.len(), + 3, + "three distinct ids returned: {:?}", + result.data.rows + ); } #[test] @@ -771,16 +890,33 @@ fn insert_returning_autofills_shortid_and_returns_it() { // surfaces in the returned row. let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::ShortId), ("label", Type::Text)], + &["id"], + ); let result = run_sqlinsert(&db, &rt, "insert into t (label) values ('x') returning *") .expect("auto-fill INSERT … RETURNING * runs"); - assert_eq!(result.rows_affected, 1, "one row inserted (RETURNING-counted)"); + assert_eq!( + result.rows_affected, 1, + "one row inserted (RETURNING-counted)" + ); assert_eq!(result.data.rows.len(), 1, "RETURNING yielded the row"); // `id` is the auto-filled shortid column; it must be non-empty in // the returned row (proving the rewrite kept RETURNING). - let id_idx = result.data.columns.iter().position(|c| c == "id").expect("id column"); + let id_idx = result + .data + .columns + .iter() + .position(|c| c == "id") + .expect("id column"); let id_val = result.data.rows[0][id_idx].clone(); - assert!(id_val.is_some_and(|s| !s.is_empty()), "generated shortid surfaced via RETURNING"); + assert!( + id_val.is_some_and(|s| !s.is_empty()), + "generated shortid surfaced via RETURNING" + ); } #[test] @@ -790,11 +926,29 @@ fn insert_returning_recovers_bare_column_type() { // renders as the word, not 0/1). let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("active", Type::Bool)], &["id"]); - let result = run_sqlinsert(&db, &rt, "insert into t (id, active) values (1, true) returning active") - .expect("INSERT … RETURNING active runs"); - assert_eq!(result.data.column_types, vec![Some(Type::Bool)], "bool type recovered"); - assert_eq!(result.data.rows[0][0], Some("true".to_string()), "rendered as the bool word"); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("active", Type::Bool)], + &["id"], + ); + let result = run_sqlinsert( + &db, + &rt, + "insert into t (id, active) values (1, true) returning active", + ) + .expect("INSERT … RETURNING active runs"); + assert_eq!( + result.data.column_types, + vec![Some(Type::Bool)], + "bool type recovered" + ); + assert_eq!( + result.data.rows[0][0], + Some("true".to_string()), + "rendered as the bool word" + ); } #[test] @@ -803,11 +957,29 @@ fn insert_returning_computed_expression_is_typeless() { // so its recovered type is None (renders with neutral alignment). let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("n", Type::Int)], &["id"]); - let result = run_sqlinsert(&db, &rt, "insert into t (id, n) values (1, 5) returning n + 1") - .expect("INSERT … RETURNING runs"); - assert_eq!(result.data.column_types, vec![None], "computed projection is typeless"); - assert_eq!(result.data.rows[0][0], Some("6".to_string()), "engine evaluated n + 1"); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("n", Type::Int)], + &["id"], + ); + let result = run_sqlinsert( + &db, + &rt, + "insert into t (id, n) values (1, 5) returning n + 1", + ) + .expect("INSERT … RETURNING runs"); + assert_eq!( + result.data.column_types, + vec![None], + "computed projection is typeless" + ); + assert_eq!( + result.data.rows[0][0], + Some("6".to_string()), + "engine evaluated n + 1" + ); } #[test] @@ -858,7 +1030,13 @@ fn multirow_autofill_returning_yields_distinct_generated_ids() { // rows each carrying its own generated id. let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::ShortId), ("label", Type::Text)], + &["id"], + ); let result = run_sqlinsert( &db, &rt, @@ -867,11 +1045,25 @@ fn multirow_autofill_returning_yields_distinct_generated_ids() { .expect("multi-row auto-fill INSERT … RETURNING * runs"); assert_eq!(result.rows_affected, 3, "three rows inserted"); assert_eq!(result.data.rows.len(), 3, "three rows returned"); - let id_idx = result.data.columns.iter().position(|c| c == "id").expect("id column"); + let id_idx = result + .data + .columns + .iter() + .position(|c| c == "id") + .expect("id column"); let ids: std::collections::BTreeSet<_> = result.data.rows.iter().map(|r| r[id_idx].clone()).collect(); - assert_eq!(ids.len(), 3, "three DISTINCT generated ids via RETURNING: {:?}", result.data.rows); - assert!(ids.iter().all(|v| v.as_ref().is_some_and(|s| !s.is_empty())), "all ids non-empty"); + assert_eq!( + ids.len(), + 3, + "three DISTINCT generated ids via RETURNING: {:?}", + result.data.rows + ); + assert!( + ids.iter() + .all(|v| v.as_ref().is_some_and(|s| !s.is_empty())), + "all ids non-empty" + ); } #[test] @@ -881,15 +1073,39 @@ fn insert_select_returning_executes_and_returns_rows() { // source feeds the insert, and RETURNING yields the inserted rows). let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "src", &[("id", Type::Int), ("b", Type::Text)], &["id"]); - create_cols(&db, &rt, "dst", &[("id", Type::Int), ("b", Type::Text)], &["id"]); - run_sqlinsert(&db, &rt, "insert into src (id, b) values (1, 'x'), (2, 'y')").expect("seed src"); - let result = run_sqlinsert(&db, &rt, "insert into dst select * from src returning id, b") - .expect("INSERT … SELECT … RETURNING runs"); + create_cols( + &db, + &rt, + "src", + &[("id", Type::Int), ("b", Type::Text)], + &["id"], + ); + create_cols( + &db, + &rt, + "dst", + &[("id", Type::Int), ("b", Type::Text)], + &["id"], + ); + run_sqlinsert( + &db, + &rt, + "insert into src (id, b) values (1, 'x'), (2, 'y')", + ) + .expect("seed src"); + let result = run_sqlinsert( + &db, + &rt, + "insert into dst select * from src returning id, b", + ) + .expect("INSERT … SELECT … RETURNING runs"); assert_eq!(result.rows_affected, 2, "two rows copied"); - assert_eq!(result.data.rows.len(), 2, "RETURNING yielded both inserted rows"); - let bs: std::collections::BTreeSet<_> = - result.data.rows.iter().map(|r| r[1].clone()).collect(); + assert_eq!( + result.data.rows.len(), + 2, + "RETURNING yielded both inserted rows" + ); + let bs: std::collections::BTreeSet<_> = result.data.rows.iter().map(|r| r[1].clone()).collect(); assert!(bs.contains(&Some("x".to_string())) && bs.contains(&Some("y".to_string()))); } @@ -934,32 +1150,59 @@ fn autofill_upsert_real_conflict_preserves_clause_and_excluded() { "t".to_string(), vec![ ColumnSpec::new("id", Type::ShortId), - ColumnSpec { unique: true, ..ColumnSpec::new("code", Type::Text) }, + ColumnSpec { + unique: true, + ..ColumnSpec::new("code", Type::Text) + }, ColumnSpec::new("label", Type::Text), ], vec!["id".to_string()], None, )) .expect("create table with shortid pk + unique code"); - run_sqlinsert(&db, &rt, "insert into t (code, label) values ('A', 'first')").expect("seed"); + run_sqlinsert( + &db, + &rt, + "insert into t (code, label) values ('A', 'first')", + ) + .expect("seed"); let result = run_sqlinsert( &db, &rt, "insert into t (code, label) values ('A', 'second') on conflict (code) do update set label = excluded.label", ) .expect("auto-filled UPSERT with a real conflict (clause preserved)"); - assert_eq!(result.rows_affected, 1, "the conflicting row was updated, not inserted"); + assert_eq!( + result.rows_affected, 1, + "the conflicting row was updated, not inserted" + ); let rows = csv_rows(&project, "t"); - assert_eq!(rows.len(), 1, "still one row (DO UPDATE, not a second insert)"); - assert!(rows[0].iter().any(|c| c == "second"), "label updated via excluded: {rows:?}"); - assert!(!rows[0].iter().any(|c| c == "first"), "old label replaced: {rows:?}"); + assert_eq!( + rows.len(), + 1, + "still one row (DO UPDATE, not a second insert)" + ); + assert!( + rows[0].iter().any(|c| c == "second"), + "label updated via excluded: {rows:?}" + ); + assert!( + !rows[0].iter().any(|c| c == "first"), + "old label replaced: {rows:?}" + ); } #[test] fn on_conflict_do_nothing_keeps_existing_row() { let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("name", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("name", Type::Text)], + &["id"], + ); run_sqlinsert(&db, &rt, "insert into t (id, name) values (1, 'orig')").expect("seed"); let result = run_sqlinsert( &db, @@ -970,14 +1213,23 @@ fn on_conflict_do_nothing_keeps_existing_row() { assert_eq!(result.rows_affected, 0, "conflicting row left untouched"); let rows = csv_rows(&project, "t"); assert_eq!(rows.len(), 1, "still one row"); - assert!(rows[0].iter().any(|c| c == "orig"), "original value kept: {rows:?}"); + assert!( + rows[0].iter().any(|c| c == "orig"), + "original value kept: {rows:?}" + ); } #[test] fn on_conflict_do_update_applies_excluded() { let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("name", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("name", Type::Text)], + &["id"], + ); run_sqlinsert(&db, &rt, "insert into t (id, name) values (1, 'orig')").expect("seed"); let result = run_sqlinsert( &db, @@ -988,14 +1240,23 @@ fn on_conflict_do_update_applies_excluded() { assert_eq!(result.rows_affected, 1, "the conflicting row was updated"); let rows = csv_rows(&project, "t"); assert_eq!(rows.len(), 1, "still one row (updated, not inserted)"); - assert!(rows[0].iter().any(|c| c == "new"), "row updated to excluded.name: {rows:?}"); + assert!( + rows[0].iter().any(|c| c == "new"), + "row updated to excluded.name: {rows:?}" + ); } #[test] fn on_conflict_do_nothing_without_target() { let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("name", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("name", Type::Text)], + &["id"], + ); run_sqlinsert(&db, &rt, "insert into t (id, name) values (1, 'orig')").expect("seed"); let result = run_sqlinsert( &db, @@ -1003,7 +1264,10 @@ fn on_conflict_do_nothing_without_target() { "insert into t (id, name) values (1, 'x') on conflict do nothing", ) .expect("ON CONFLICT (no target) DO NOTHING runs"); - assert_eq!(result.rows_affected, 0, "any-conflict do-nothing absorbed the duplicate"); + assert_eq!( + result.rows_affected, 0, + "any-conflict do-nothing absorbed the duplicate" + ); } #[test] @@ -1017,7 +1281,13 @@ fn autofill_preserves_on_conflict_clause() { // the rewrite doesn't prepare-fail and the clause survives. let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::ShortId), ("label", Type::Text)], + &["id"], + ); let result = run_sqlinsert( &db, &rt, @@ -1054,7 +1324,10 @@ fn sql_dml_validates_literal_values_like_the_dsl() { let dsl = r.block_on(db.insert( "T".to_string(), Some(vec!["id".to_string(), "d".to_string()]), - vec![Value::Number("1".to_string()), Value::Text("2025/01/15".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Text("2025/01/15".to_string()), + ], Some("insert".to_string()), )); assert!( @@ -1187,20 +1460,34 @@ fn advanced_upsert_do_update_set_offers_typed_slot_hint() { // column-typed slot hint (boundary-aware lookahead → typed slot). let mut cache = SchemaCache::default(); let cols = vec![ - TableColumn { name: "id".to_string(), user_type: Type::Int, not_null: false, has_default: false }, - TableColumn { name: "Name".to_string(), user_type: Type::Text, not_null: false, has_default: false }, + TableColumn { + name: "id".to_string(), + user_type: Type::Int, + not_null: false, + has_default: false, + }, + TableColumn { + name: "Name".to_string(), + user_type: Type::Text, + not_null: false, + has_default: false, + }, ]; cache.tables.push("Customers".to_string()); cache.columns.push("id".to_string()); cache.columns.push("Name".to_string()); cache.table_columns.insert("Customers".to_string(), cols); - let input = "insert into Customers (id, Name) values (1, 'x') on conflict (id) do update set Name="; + let input = + "insert into Customers (id, Name) values (1, 'x') on conflict (id) do update set Name="; let hint = ambient_hint_in_mode(input, input.len(), None, &cache, Mode::Advanced); let Some(AmbientHint::Prose(prose)) = hint else { panic!("expected a Prose hint at the UPSERT SET value slot, got {hint:?}"); }; - assert!(prose.contains("Name"), "hint names the column `Name`: {prose:?}"); + assert!( + prose.contains("Name"), + "hint names the column `Name`: {prose:?}" + ); assert!( prose.contains("quoted string"), "text-column hint says `quoted string`: {prose:?}" @@ -1251,8 +1538,14 @@ fn advanced_insert_form_a_value_offers_typed_slot_hint() { // user-listed column, so the hint is that column's typed prose. let schema = vschema(&[("Things", &[("k", Type::Int), ("note", Type::Text)])]); let prose = prose_at("insert into Things (note) values (", &schema); - assert!(prose.contains("note"), "names listed column `note`: {prose:?}"); - assert!(prose.contains("quoted string"), "text-column prose: {prose:?}"); + assert!( + prose.contains("note"), + "names listed column `note`: {prose:?}" + ); + assert!( + prose.contains("quoted string"), + "text-column prose: {prose:?}" + ); } #[test] @@ -1271,8 +1564,14 @@ fn advanced_insert_second_position_hints_second_column() { // hint is the SECOND column's typed prose. let schema = vschema(&[("Things", &[("k", Type::Int), ("note", Type::Text)])]); let prose = prose_at("insert into Things (k, note) values (5, ", &schema); - assert!(prose.contains("note"), "second position names `note`: {prose:?}"); - assert!(prose.contains("quoted string"), "text-column prose: {prose:?}"); + assert!( + prose.contains("note"), + "second position names `note`: {prose:?}" + ); + assert!( + prose.contains("quoted string"), + "text-column prose: {prose:?}" + ); } #[test] @@ -1283,13 +1582,19 @@ fn advanced_insert_value_int_mismatch_is_caught_live() { &schema, Mode::Advanced, ); - assert!(!matches!(bad, InputState::Valid), "decimal into int rejected live: {bad:?}"); + assert!( + !matches!(bad, InputState::Valid), + "decimal into int rejected live: {bad:?}" + ); let ok = classify_input_with_schema_in_mode( "insert into Things (k) values (5)", &schema, Mode::Advanced, ); - assert!(matches!(ok, InputState::Valid), "valid int literal parses: {ok:?}"); + assert!( + matches!(ok, InputState::Valid), + "valid int literal parses: {ok:?}" + ); } #[test] @@ -1303,7 +1608,10 @@ fn advanced_insert_string_into_int_is_caught_live() { &schema, Mode::Advanced, ); - assert!(!matches!(bad, InputState::Valid), "string into int rejected live: {bad:?}"); + assert!( + !matches!(bad, InputState::Valid), + "string into int rejected live: {bad:?}" + ); } #[test] @@ -1314,7 +1622,10 @@ fn advanced_insert_multi_row_typed_and_mismatch_caught() { &schema, Mode::Advanced, ); - assert!(matches!(ok, InputState::Valid), "well-formed multi-row parses: {ok:?}"); + assert!( + matches!(ok, InputState::Valid), + "well-formed multi-row parses: {ok:?}" + ); let bad = classify_input_with_schema_in_mode( "insert into Things (k, note) values (1, 'a'), (3.14, 'b')", &schema, @@ -1333,14 +1644,21 @@ fn advanced_insert_form_b_maps_all_columns_including_serial() { // takes an int literal (unlike the DSL, which omits auto-gen cols). let schema = vschema(&[( "Customers", - &[("id", Type::Serial), ("Name", Type::Text), ("Email", Type::Text)], + &[ + ("id", Type::Serial), + ("Name", Type::Text), + ("Email", Type::Text), + ], )]); let state = classify_input_with_schema_in_mode( "insert into Customers values (1, 'Bob', 'b@c')", &schema, Mode::Advanced, ); - assert!(matches!(state, InputState::Valid), "Form B maps all 3 columns: {state:?}"); + assert!( + matches!(state, InputState::Valid), + "Form B maps all 3 columns: {state:?}" + ); } #[test] @@ -1356,6 +1674,9 @@ fn advanced_insert_value_expressions_still_parse_via_sql_expr() { "insert into Things (k) values ((select 1))", ] { let state = classify_input_with_schema_in_mode(input, &schema, Mode::Advanced); - assert!(matches!(state, InputState::Valid), "{input:?} must parse: {state:?}"); + assert!( + matches!(state, InputState::Valid), + "{input:?} must parse: {state:?}" + ); } } diff --git a/tests/it/sql_select.rs b/tests/it/sql_select.rs index c3c31d4..6d6aa35 100644 --- a/tests/it/sql_select.rs +++ b/tests/it/sql_select.rs @@ -57,11 +57,13 @@ fn advanced_mode_select_dispatches_as_command_select() { type_str(&mut app, "select 1"); let actions = submit(&mut app); match actions.as_slice() { - [Action::ExecuteDsl { - command: Command::Select { sql }, - source, - .. - }] => { + [ + Action::ExecuteDsl { + command: Command::Select { sql }, + source, + .. + }, + ] => { assert!( sql.contains("select 1"), "Command::Select carries the validated SQL text: {sql:?}", @@ -118,10 +120,12 @@ fn colon_one_shot_from_simple_mode_dispatches_select() { // Persistent mode is unchanged. assert_eq!(app.mode, Mode::Simple); match actions.as_slice() { - [Action::ExecuteDsl { - command: Command::Select { sql }, - .. - }] => { + [ + Action::ExecuteDsl { + command: Command::Select { sql }, + .. + }, + ] => { assert!( sql.contains("select 1") && !sql.starts_with(':'), "the `:` is stripped before the SQL is queued: {sql:?}", @@ -167,8 +171,7 @@ fn rt() -> tokio::runtime::Runtime { fn open_project_db() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence(project.db_path(), persistence) .expect("open db with persistence"); @@ -315,22 +318,12 @@ fn database_run_select_recovers_bool_column_type() { ) .await .expect("create table"); - db.insert( - "Products".to_string(), - None, - vec![Value::Bool(true)], - None, - ) - .await - .expect("insert row"); - db.insert( - "Products".to_string(), - None, - vec![Value::Bool(false)], - None, - ) - .await - .expect("insert row"); + db.insert("Products".to_string(), None, vec![Value::Bool(true)], None) + .await + .expect("insert row"); + db.insert("Products".to_string(), None, vec![Value::Bool(false)], None) + .await + .expect("insert row"); }); let data = rt .block_on(db.run_select("select Active from Products".to_string())) @@ -370,9 +363,7 @@ fn database_run_select_recovers_text_type_through_alias() { // origin metadata still points at `Users.Name`, so the // playground type is recovered. let data = rt - .block_on( - db.run_select("select Name as n from Users".to_string()), - ) + .block_on(db.run_select("select Name as n from Users".to_string())) .expect("SELECT runs"); assert_eq!(data.columns, vec!["n".to_string()]); assert_eq!(data.column_types, vec![Some(Type::Text)]); @@ -394,9 +385,14 @@ fn database_run_select_computed_expression_stays_typeless() { ) .await .expect("create table"); - db.insert("T".to_string(), None, vec![Value::Number("5".to_string())], None) - .await - .expect("insert"); + db.insert( + "T".to_string(), + None, + vec![Value::Number("5".to_string())], + None, + ) + .await + .expect("insert"); }); let data = rt .block_on(db.run_select("select Score + 1 from T".to_string())) @@ -434,17 +430,12 @@ fn engine_aggregate_in_where_routes_through_catalog() { // ADR-0032 §11.4. Run the bad query and confirm the // friendly layer routes the message through engine.aggregate_misuse. let err = rt - .block_on(db.run_select( - "select id from T where count(score) > 0".to_string(), - )) + .block_on(db.run_select("select id from T where count(score) > 0".to_string())) .expect_err("engine should reject aggregate in WHERE"); let DbError::Sqlite { .. } = &err else { panic!("expected Sqlite engine error; got {err:?}"); }; - let friendly = friendly::translate_error( - &err, - &friendly::TranslateContext::default(), - ); + let friendly = friendly::translate_error(&err, &friendly::TranslateContext::default()); let rendered = friendly.render(); assert!( rendered.contains("aggregate"), @@ -506,23 +497,17 @@ fn engine_group_by_missing_routes_through_catalog() { // false-positive on phrasings that happen to contain // "group by" elsewhere. Any successful query is fine. let _ = rt - .block_on(db.run_select( - "select category, count(*) from T group by category".to_string(), - )) + .block_on(db.run_select("select category, count(*) from T group by category".to_string())) .expect("benign GROUP BY query runs"); // Direct unit test on the matcher: ensure a message that // mentions GROUP BY routes through the catalog. let synthetic = DbError::Sqlite { - message: - "column must appear in the GROUP BY clause or be used in an aggregate function" - .to_string(), + message: "column must appear in the GROUP BY clause or be used in an aggregate function" + .to_string(), kind: rdbms_playground::db::SqliteErrorKind::Other, }; - let rendered = friendly::translate_error( - &synthetic, - &friendly::TranslateContext::default(), - ) - .render(); + let rendered = + friendly::translate_error(&synthetic, &friendly::TranslateContext::default()).render(); assert!( rendered.contains("GROUP BY"), "engine.group_by_required wording missing; got {rendered:?}", @@ -567,19 +552,14 @@ fn engine_scalar_subquery_too_many_rows_routes_through_catalog() { // catalog (the matcher would fire if SQLite ever // surfaced this verbatim). let _ = rt - .block_on(db.run_select( - "select (select v from T) from T".to_string(), - )) + .block_on(db.run_select("select (select v from T) from T".to_string())) .expect("benign scalar subquery query runs"); let synthetic = DbError::Sqlite { message: "scalar subquery returned more than one row".to_string(), kind: rdbms_playground::db::SqliteErrorKind::Other, }; - let rendered = friendly::translate_error( - &synthetic, - &friendly::TranslateContext::default(), - ) - .render(); + let rendered = + friendly::translate_error(&synthetic, &friendly::TranslateContext::default()).render(); assert!( rendered.contains("more than one row"), "engine.scalar_subquery_too_many_rows wording missing; got {rendered:?}", diff --git a/tests/it/sql_update.rs b/tests/it/sql_update.rs index 88ef502..5c3d204 100644 --- a/tests/it/sql_update.rs +++ b/tests/it/sql_update.rs @@ -28,8 +28,7 @@ fn rt() -> tokio::runtime::Runtime { fn open_project_db() -> (project::Project, Database, tempfile::TempDir) { let dir = tempfile::tempdir().expect("create tempdir"); - let project = - project::open_or_create(None, Some(dir.path())).expect("open or create project"); + let project = project::open_or_create(None, Some(dir.path())).expect("open or create project"); let persistence = Persistence::new(project.path().to_path_buf()); let db = Database::open_with_persistence(project.db_path(), persistence) .expect("open db with persistence"); @@ -77,15 +76,18 @@ fn run_update( input: &str, ) -> Result { match parse_command(input).expect("parse update") { - Command::SqlUpdate { sql, target_table, returning, set_literals } => rt.block_on( - db.run_sql_update_with_literals( - sql, - Some(input.to_string()), - target_table, - returning, - set_literals, - ), - ), + Command::SqlUpdate { + sql, + target_table, + returning, + set_literals, + } => rt.block_on(db.run_sql_update_with_literals( + sql, + Some(input.to_string()), + target_table, + returning, + set_literals, + )), other => panic!("expected Command::SqlUpdate, got {other:?}"), } } @@ -95,7 +97,9 @@ fn parse_path_lowers_sql_update_to_command() { let command = parse_command("update Orders set total = 0 where id = 1") .expect("update parses in advanced mode"); match command { - Command::SqlUpdate { sql, target_table, .. } => { + Command::SqlUpdate { + sql, target_table, .. + } => { assert_eq!(sql, "update Orders set total = 0 where id = 1"); assert_eq!(target_table, "Orders"); } @@ -107,10 +111,20 @@ fn parse_path_lowers_sql_update_to_command() { fn single_column_update_with_where_persists() { let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]); - seed(&db, &rt, "insert into t (id, v) values (1, 'old'), (2, 'keep')", "t"); - let result = run_update(&db, &rt, "update t set v = 'new' where id = 1") - .expect("update runs"); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("v", Type::Text)], + &["id"], + ); + seed( + &db, + &rt, + "insert into t (id, v) values (1, 'old'), (2, 'keep')", + "t", + ); + let result = run_update(&db, &rt, "update t set v = 'new' where id = 1").expect("update runs"); assert_eq!(result.rows_affected, 1, "one row updated"); let csv = read_csv(&project, "t").expect("t.csv"); assert!(csv.contains("new"), "updated value present: {csv:?}"); @@ -134,7 +148,10 @@ fn multi_column_update_persists() { .expect("multi-col update runs"); assert_eq!(result.rows_affected, 1); let csv = read_csv(&project, "t").expect("t.csv"); - assert!(csv.contains('9') && csv.contains('y'), "both columns updated: {csv:?}"); + assert!( + csv.contains('9') && csv.contains('y'), + "both columns updated: {csv:?}" + ); } #[test] @@ -142,10 +159,21 @@ fn update_without_where_runs_across_all_rows() { // ADR-0030 §12: no `--all-rows` rail. let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("active", Type::Bool)], &["id"]); - seed(&db, &rt, "insert into t (id, active) values (1, true), (2, true)", "t"); - let result = run_update(&db, &rt, "update t set active = false") - .expect("unfiltered update runs"); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("active", Type::Bool)], + &["id"], + ); + seed( + &db, + &rt, + "insert into t (id, active) values (1, true), (2, true)", + "t", + ); + let result = + run_update(&db, &rt, "update t set active = false").expect("unfiltered update runs"); assert_eq!(result.rows_affected, 2, "all rows updated"); let csv = read_csv(&project, "t").expect("t.csv"); assert!(!csv.contains("true"), "no row left active: {csv:?}"); @@ -159,10 +187,20 @@ fn update_with_sql_expr_in_set() { &db, &rt, "t", - &[("id", Type::Int), ("price", Type::Int), ("qty", Type::Int), ("total", Type::Int)], + &[ + ("id", Type::Int), + ("price", Type::Int), + ("qty", Type::Int), + ("total", Type::Int), + ], &["id"], ); - seed(&db, &rt, "insert into t (id, price, qty, total) values (1, 6, 7, 0)", "t"); + seed( + &db, + &rt, + "insert into t (id, price, qty, total) values (1, 6, 7, 0)", + "t", + ); let result = run_update(&db, &rt, "update t set total = price * qty where id = 1") .expect("expression update runs"); assert_eq!(result.rows_affected, 1); @@ -176,8 +214,19 @@ fn update_with_subquery_in_set() { let (project, db, _dir) = open_project_db(); let rt = rt(); create_cols(&db, &rt, "other", &[("n", Type::Int)], &["n"]); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Int)], &["id"]); - seed(&db, &rt, "insert into other (n) values (3), (8), (5)", "other"); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("v", Type::Int)], + &["id"], + ); + seed( + &db, + &rt, + "insert into other (n) values (3), (8), (5)", + "other", + ); seed(&db, &rt, "insert into t (id, v) values (1, 0)", "t"); let result = run_update( &db, @@ -196,13 +245,22 @@ fn update_matching_no_rows_is_ok() { // the path doesn't crash, and the CSV is unchanged. let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("v", Type::Text)], + &["id"], + ); seed(&db, &rt, "insert into t (id, v) values (1, 'keep')", "t"); let result = run_update(&db, &rt, "update t set v = 'x' where id = 999") .expect("no-match update is a success"); assert_eq!(result.rows_affected, 0, "no rows matched"); let csv = read_csv(&project, "t").expect("t.csv"); - assert!(csv.contains("keep") && !csv.contains('x'), "unchanged: {csv:?}"); + assert!( + csv.contains("keep") && !csv.contains('x'), + "unchanged: {csv:?}" + ); } // ================================================================= @@ -220,8 +278,19 @@ fn sql_update_validates_set_literals_like_the_dsl() { // STRICT TEXT column accept anything). let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("d", Type::Date)], &["id"]); - seed(&db, &rt, "insert into t (id, d) values (1, '2025-01-15')", "t"); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("d", Type::Date)], + &["id"], + ); + seed( + &db, + &rt, + "insert into t (id, d) values (1, '2025-01-15')", + "t", + ); // SQL path (advanced mode, full replay pipeline) — REJECTS the bad date. std::fs::write( @@ -300,7 +369,12 @@ fn sql_update_validates_every_assignment_not_just_the_first() { &[("id", Type::Int), ("v", Type::Text), ("d", Type::Date)], &["id"], ); - seed(&db, &rt, "insert into t (id, v, d) values (1, 'a', '2025-01-01')", "t"); + seed( + &db, + &rt, + "insert into t (id, v, d) values (1, 'a', '2025-01-01')", + "t", + ); std::fs::write( project.path().join("multi.commands"), "update t set v = 'ok', d = '2025/01/15' where id = 1\n", @@ -321,26 +395,64 @@ fn sql_update_validates_every_assignment_not_just_the_first() { fn update_returning_yields_modified_columns() { let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]); - seed(&db, &rt, "insert into t (id, v) values (1, 'old'), (2, 'keep')", "t"); - let result = run_update(&db, &rt, "update t set v = 'new' where id = 1 returning id, v") - .expect("UPDATE … RETURNING runs"); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("v", Type::Text)], + &["id"], + ); + seed( + &db, + &rt, + "insert into t (id, v) values (1, 'old'), (2, 'keep')", + "t", + ); + let result = run_update( + &db, + &rt, + "update t set v = 'new' where id = 1 returning id, v", + ) + .expect("UPDATE … RETURNING runs"); assert_eq!(result.rows_affected, 1, "one row updated"); assert_eq!(result.data.columns, vec!["id".to_string(), "v".to_string()]); assert_eq!(result.data.rows.len(), 1); // RETURNING reflects the POST-update value. - assert_eq!(result.data.rows[0][1], Some("new".to_string()), "modified value returned"); + assert_eq!( + result.data.rows[0][1], + Some("new".to_string()), + "modified value returned" + ); } #[test] fn update_returning_recovers_bare_column_type() { let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("active", Type::Bool)], &["id"]); - seed(&db, &rt, "insert into t (id, active) values (1, false)", "t"); - let result = run_update(&db, &rt, "update t set active = true where id = 1 returning active") - .expect("UPDATE … RETURNING active runs"); - assert_eq!(result.data.column_types, vec![Some(Type::Bool)], "bool type recovered"); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("active", Type::Bool)], + &["id"], + ); + seed( + &db, + &rt, + "insert into t (id, active) values (1, false)", + "t", + ); + let result = run_update( + &db, + &rt, + "update t set active = true where id = 1 returning active", + ) + .expect("UPDATE … RETURNING active runs"); + assert_eq!( + result.data.column_types, + vec![Some(Type::Bool)], + "bool type recovered" + ); assert_eq!(result.data.rows[0][0], Some("true".to_string())); } @@ -352,13 +464,27 @@ fn update_returning_matching_no_rows_is_ok_and_empty() { // phantom row. let (_project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("v", Type::Text)], + &["id"], + ); seed(&db, &rt, "insert into t (id, v) values (1, 'keep')", "t"); - let result = run_update(&db, &rt, "update t set v = 'x' where id = 999 returning id, v") - .expect("no-match UPDATE … RETURNING is a success"); + let result = run_update( + &db, + &rt, + "update t set v = 'x' where id = 999 returning id, v", + ) + .expect("no-match UPDATE … RETURNING is a success"); assert_eq!(result.rows_affected, 0, "no rows matched"); assert!(result.data.rows.is_empty(), "no rows returned"); - assert_eq!(result.data.columns, vec!["id".to_string(), "v".to_string()], "columns still present"); + assert_eq!( + result.data.columns, + vec!["id".to_string(), "v".to_string()], + "columns still present" + ); } // ================================================================= @@ -399,14 +525,21 @@ fn advanced_update_set_value_offers_typed_slot_hint_for_column() { // instead of the type-blind sql_expr surface. let schema = schema_cache(&[( "Customers", - &[("id", Type::Serial), ("Name", Type::Text), ("Email", Type::Text)], + &[ + ("id", Type::Serial), + ("Name", Type::Text), + ("Email", Type::Text), + ], )]); let input = "update Customers set Email="; let hint = ambient_hint_in_mode(input, input.len(), None, &schema, Mode::Advanced); let Some(AmbientHint::Prose(prose)) = hint else { panic!("expected a Prose hint at the typed value slot, got {hint:?}"); }; - assert!(prose.contains("Email"), "hint names the column `Email`: {prose:?}"); + assert!( + prose.contains("Email"), + "hint names the column `Email`: {prose:?}" + ); assert!( prose.contains("quoted string"), "text-column hint says `quoted string`: {prose:?}" @@ -449,7 +582,10 @@ fn advanced_update_set_int_value_type_mismatch_is_caught_live() { &schema, Mode::Advanced, ); - assert!(matches!(ok, InputState::Valid), "a valid int literal parses: {ok:?}"); + assert!( + matches!(ok, InputState::Valid), + "a valid int literal parses: {ok:?}" + ); } #[test] @@ -463,10 +599,10 @@ fn advanced_update_set_expression_still_parses_via_sql_expr() { ("other", &[("n", Type::Int)]), ]); for input in [ - "update Things set k = 3 + 2 where k = 0", // literal-prefixed expression + "update Things set k = 3 + 2 where k = 0", // literal-prefixed expression "update Things set k = (select max(n) from other) where k = 0", // scalar subquery "update Things set note = upper(note) where k = 0", // function call - "update Things set k = -5 where k = 0", // signed number → sql_expr + "update Things set k = -5 where k = 0", // signed number → sql_expr ] { let state = classify_input_with_schema_in_mode(input, &schema, Mode::Advanced); assert!( @@ -488,7 +624,13 @@ fn update_all_rows_flag_in_advanced_updates_every_row() { // parse-level dispatch (covered in tests/sql_dml_e2e.rs). let (project, db, _dir) = open_project_db(); let rt = rt(); - create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Int)], &["id"]); + create_cols( + &db, + &rt, + "t", + &[("id", Type::Int), ("v", Type::Int)], + &["id"], + ); seed(&db, &rt, "insert into t (id, v) values (1, 1), (2, 2)", "t"); std::fs::write( project.path().join("allrows.commands"), @@ -497,7 +639,10 @@ fn update_all_rows_flag_in_advanced_updates_every_row() { .expect("write script"); let events = rt.block_on(run_replay(&db, project.path(), "allrows.commands")); assert!( - matches!(events.last(), Some(AppEvent::ReplayCompleted { count: 1, .. })), + matches!( + events.last(), + Some(AppEvent::ReplayCompleted { count: 1, .. }) + ), "the --all-rows update replays through the DSL fall-back; events: {events:?}" ); let rows = rt diff --git a/tests/it/undo_snapshots.rs b/tests/it/undo_snapshots.rs index c0ce5f2..17f4189 100644 --- a/tests/it/undo_snapshots.rs +++ b/tests/it/undo_snapshots.rs @@ -179,7 +179,10 @@ fn undo_disabled_takes_no_snapshots() { .unwrap(); // Nothing to undo, and no snapshot machinery on disk. - assert!(db.undo().await.unwrap().is_none(), "undo is a no-op when disabled"); + assert!( + db.undo().await.unwrap().is_none(), + "undo is a no-op when disabled" + ); assert!(db.peek_undo().await.unwrap().is_none()); }); @@ -360,7 +363,10 @@ fn undo_restores_db_and_csv_consistently() { db.insert( "T".to_string(), Some(vec!["id".to_string(), "name".to_string()]), - vec![Value::Number("1".to_string()), Value::Text("Alice".to_string())], + vec![ + Value::Number("1".to_string()), + Value::Text("Alice".to_string()), + ], Some("insert Alice".to_string()), ) .await @@ -444,8 +450,8 @@ fn undo_ring_persists_across_reopen() { // held by `project`). The persisted ring must survive. drop(db); let persistence = Persistence::new(path); - let db2 = Database::open_with_persistence_and_undo(&db_path, persistence, true) - .expect("reopen db"); + let db2 = + Database::open_with_persistence_and_undo(&db_path, persistence, true).expect("reopen db"); rt().block_on(async { let peek = db2 diff --git a/tests/it/walking_skeleton.rs b/tests/it/walking_skeleton.rs index 40cea3c..c2dec15 100644 --- a/tests/it/walking_skeleton.rs +++ b/tests/it/walking_skeleton.rs @@ -49,7 +49,11 @@ fn submit(app: &mut App) -> Vec { /// and don't care about the verbatim user input. #[track_caller] fn assert_one_execute_dsl(actions: &[Action], expected: &Command) { - assert_eq!(actions.len(), 1, "expected exactly one action; got {actions:?}"); + assert_eq!( + actions.len(), + 1, + "expected exactly one action; got {actions:?}" + ); match &actions[0] { Action::ExecuteDsl { command, .. } => assert_eq!(command, expected), other => panic!("expected ExecuteDsl, got {other:?}"), @@ -234,9 +238,18 @@ fn status_bar_is_keystroke_only_and_state_aware() { // Default (empty input): nav / complete / history / run keystrokes. let default_view = rendered_text(&mut app, &theme, 80, 24); - assert!(default_view.contains("Ctrl-O sidebar"), "strip lists sidebar:\n{default_view}"); - assert!(default_view.contains("Enter run"), "strip lists run:\n{default_view}"); - assert!(!default_view.contains("Ctrl-C"), "quit dropped from the strip:\n{default_view}"); + assert!( + default_view.contains("Ctrl-O sidebar"), + "strip lists sidebar:\n{default_view}" + ); + assert!( + default_view.contains("Enter run"), + "strip lists run:\n{default_view}" + ); + assert!( + !default_view.contains("Ctrl-C"), + "quit dropped from the strip:\n{default_view}" + ); assert!( !default_view.contains("advanced once"), "`:` command word dropped from the strip:\n{default_view}", @@ -245,8 +258,14 @@ fn status_bar_is_keystroke_only_and_state_aware() { // Editing (input has text): the #29 readline edit keys appear. type_str(&mut app, "create"); let editing = rendered_text(&mut app, &theme, 80, 24); - assert!(editing.contains("Esc clear"), "editing strip lists clear:\n{editing}"); - assert!(editing.contains("Ctrl-W del word"), "editing strip lists del word:\n{editing}"); + assert!( + editing.contains("Esc clear"), + "editing strip lists clear:\n{editing}" + ); + assert!( + editing.contains("Ctrl-W del word"), + "editing strip lists del word:\n{editing}" + ); } // --------------------------------------------------------------- @@ -326,7 +345,9 @@ fn create_table_flow_updates_tables_list_and_structure_view() { // `id` row shows both the name and its `serial` type // separated by box-drawing characters. assert!( - rendered.lines().any(|l| l.contains("id") && l.contains("serial")), + rendered + .lines() + .any(|l| l.contains("id") && l.contains("serial")), "output should show the id/serial column row:\n{rendered}" ); } @@ -336,10 +357,7 @@ fn add_column_flow_updates_structure_view() { let mut app = App::new(); // Simulate the prior create_table state. app.tables = vec!["Customers".to_string()]; - app.current_table = Some(fake_table( - "Customers", - &[("id", Type::Serial, true)], - )); + app.current_table = Some(fake_table("Customers", &[("id", Type::Serial, true)])); type_str(&mut app, "add column to table Customers: Name (text)"); let actions = submit(&mut app); @@ -376,7 +394,9 @@ fn add_column_flow_updates_structure_view() { assert_eq!(app.current_table, Some(updated)); let rendered = rendered_text(&mut app, &Theme::dark(), 80, 24); assert!( - rendered.lines().any(|l| l.contains("Name") && l.contains("text")), + rendered + .lines() + .any(|l| l.contains("Name") && l.contains("text")), "expected the Name/text column row:\n{rendered}", ); } @@ -496,10 +516,7 @@ fn add_relationship_flow_shows_parent_side_with_inbound_section() { assert!(rendered.contains("on delete cascade"), "{rendered}"); // The [ok] subject lists the endpoints. Long lines wrap in // the panel, so we check the first half of the phrase only. - assert!( - rendered.contains("from Customers.Id"), - "{rendered}" - ); + assert!(rendered.contains("from Customers.Id"), "{rendered}"); } #[test] @@ -551,11 +568,23 @@ fn add_column_confirmation_omits_relationship_prose() { let rendered = rendered_text(&mut app, &Theme::dark(), 80, 24); // The structure box still renders (table name + the column box from // the returned description). - assert!(rendered.contains("Customers"), "structure header:\n{rendered}"); - assert!(rendered.contains("Constraints"), "structure box:\n{rendered}"); + assert!( + rendered.contains("Customers"), + "structure header:\n{rendered}" + ); + assert!( + rendered.contains("Constraints"), + "structure box:\n{rendered}" + ); // The relationship block is gone — neither prose heading nor line. - assert!(!rendered.contains("Referenced by:"), "no prose heading:\n{rendered}"); - assert!(!rendered.contains("References:"), "no prose heading:\n{rendered}"); + assert!( + !rendered.contains("Referenced by:"), + "no prose heading:\n{rendered}" + ); + assert!( + !rendered.contains("References:"), + "no prose heading:\n{rendered}" + ); assert!( !rendered.contains("Orders.CustId → Id"), "no prose line:\n{rendered}", @@ -682,8 +711,14 @@ fn validity_indicator_renders_err_and_wrn_labels() { let mut app = App::new(); let clean = rendered_text(&mut app, &Theme::dark(), 80, 24); - assert!(!clean.contains("[ERR]"), "clean input shows no label:\n{clean}"); - assert!(!clean.contains("[WRN]"), "clean input shows no label:\n{clean}"); + assert!( + !clean.contains("[ERR]"), + "clean input shows no label:\n{clean}" + ); + assert!( + !clean.contains("[WRN]"), + "clean input shows no label:\n{clean}" + ); app.input_indicator = Some(Severity::Error); let err = rendered_text(&mut app, &Theme::dark(), 80, 24); diff --git a/tests/typing_surface/add_relationship.rs b/tests/typing_surface/add_relationship.rs index 88ed820..512d84e 100644 --- a/tests/typing_surface/add_relationship.rs +++ b/tests/typing_surface/add_relationship.rs @@ -28,10 +28,7 @@ fn one_n_relationship_keyword_sequence_is_incomplete() { #[test] fn after_from_offers_table_names() { let schema = schema_multi_table(); - let a = assess_at_end( - "add 1:n relationship from ", - &schema, - ); + let a = assess_at_end("add 1:n relationship from ", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); assert_candidate_present(&a, &["Customers", "Orders"]); crate::snap!("after_from", a); @@ -41,10 +38,7 @@ fn after_from_offers_table_names() { fn after_parent_table_dot_narrows_to_parent_columns() { // §2.2 follow-up — `from Customers.` narrows to Customers. let schema = schema_multi_table(); - let a = assess_at_end( - "add 1:n relationship from Customers.", - &schema, - ); + let a = assess_at_end("add 1:n relationship from Customers.", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); assert_candidate_present(&a, &["id", "Name"]); assert_no_candidate_named(&a, &["OrderId", "CustId", "Total"]); @@ -54,10 +48,7 @@ fn after_parent_table_dot_narrows_to_parent_columns() { #[test] fn after_child_table_dot_narrows_to_child_columns() { let schema = schema_multi_table(); - let a = assess_at_end( - "add 1:n relationship from Customers.id to Orders.", - &schema, - ); + let a = assess_at_end("add 1:n relationship from Customers.id to Orders.", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); assert_candidate_present(&a, &["OrderId", "CustId", "Total"]); assert_no_candidate_named(&a, &["Name"]); @@ -101,10 +92,7 @@ fn add_relationship_with_on_delete_clause_parses() { #[test] fn add_relationship_in_progress_after_dot_is_incomplete() { let schema = schema_multi_table(); - let a = assess_at_end( - "add 1:n relationship from Customers.id to ", - &schema, - ); + let a = assess_at_end("add 1:n relationship from Customers.id to ", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); crate::snap!("in_progress_after_to", a); } diff --git a/tests/typing_surface/create_m2n.rs b/tests/typing_surface/create_m2n.rs index 10abc06..e143a8f 100644 --- a/tests/typing_surface/create_m2n.rs +++ b/tests/typing_surface/create_m2n.rs @@ -67,7 +67,10 @@ fn create_m2n_with_as_name_parses() { #[test] fn after_as_keyword_is_incomplete() { let schema = schema_multi_table(); - let a = assess_at_end("create m:n relationship from Customers to Orders as ", &schema); + let a = assess_at_end( + "create m:n relationship from Customers to Orders as ", + &schema, + ); assert!(matches!(a.state, InputState::IncompleteAtEof)); crate::snap!("after_as", a); } diff --git a/tests/typing_surface/create_table.rs b/tests/typing_surface/create_table.rs index c6fb1c4..2c80e98 100644 --- a/tests/typing_surface/create_table.rs +++ b/tests/typing_surface/create_table.rs @@ -83,25 +83,16 @@ fn after_pk_space_with_col_name_typed_expects_paren() { #[test] fn after_paren_expects_type_candidates() { let schema = schema_empty(); - let a = assess_at_end( - "create table Customers with pk Code(", - &schema, - ); + let a = assess_at_end("create table Customers with pk Code(", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); - assert_candidate_present( - &a, - &["text", "int", "serial", "shortid", "bool"], - ); + assert_candidate_present(&a, &["text", "int", "serial", "shortid", "bool"]); crate::snap!("after_paren", a); } #[test] fn create_table_with_explicit_pk_parses() { let schema = schema_empty(); - let a = assess_at_end( - "create table Customers with pk Code(text)", - &schema, - ); + let a = assess_at_end("create table Customers with pk Code(text)", &schema); assert!(matches!(a.state, InputState::Valid)); crate::snap!("with_explicit_pk", a); } diff --git a/tests/typing_surface/delete_with_where.rs b/tests/typing_surface/delete_with_where.rs index 000f623..90a0fdc 100644 --- a/tests/typing_surface/delete_with_where.rs +++ b/tests/typing_surface/delete_with_where.rs @@ -42,13 +42,8 @@ fn after_where_offers_active_table_columns_no_leakage() { #[test] fn after_where_column_equals_offers_typed_prose() { let schema = schema_serial_pk(); - let a = assess_at_end( - "delete from Customers where Email=", - &schema, - ); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose, got {:?}", a.hint) - }); + let a = assess_at_end("delete from Customers where Email=", &schema); + let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose, got {:?}", a.hint)); assert!( prose.contains("Email"), "should name `Email`, got prose: {prose:?}", @@ -63,10 +58,7 @@ fn after_where_column_equals_offers_typed_prose() { #[test] fn complete_delete_with_where_parses() { let schema = schema_serial_pk(); - let a = assess_at_end( - "delete from Customers where id=1", - &schema, - ); + let a = assess_at_end("delete from Customers where id=1", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("Delete")); crate::snap!("complete_delete", a); @@ -76,9 +68,7 @@ fn complete_delete_with_where_parses() { fn delete_with_datetime_column_says_yyyy_mm_dd_t() { let schema = schema_every_type(); let a = assess_at_end("delete from Things where ts=", &schema); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose, got {:?}", a.hint) - }); + let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose, got {:?}", a.hint)); assert!( prose.contains("ts"), "should name `ts`, got prose: {prose:?}", diff --git a/tests/typing_surface/drop_column.rs b/tests/typing_surface/drop_column.rs index 68b5cc7..a3ce8ad 100644 --- a/tests/typing_surface/drop_column.rs +++ b/tests/typing_surface/drop_column.rs @@ -63,10 +63,7 @@ fn complete_drop_column_parses() { #[test] fn drop_column_with_table_keyword_parses() { let schema = schema_serial_pk(); - let a = assess_at_end( - "drop column from table Customers: Email", - &schema, - ); + let a = assess_at_end("drop column from table Customers: Email", &schema); assert!(matches!(a.state, InputState::Valid)); crate::snap!("with_table_keyword", a); } diff --git a/tests/typing_surface/drop_relationship.rs b/tests/typing_surface/drop_relationship.rs index 5f59a22..ffd8d30 100644 --- a/tests/typing_surface/drop_relationship.rs +++ b/tests/typing_surface/drop_relationship.rs @@ -30,10 +30,7 @@ fn after_relationship_keyword_offers_from_and_names() { assert!(matches!(a.state, InputState::IncompleteAtEof)); // Both selector forms discoverable: `from` for endpoints, // the relationship name for the by-name form. - assert_candidate_present( - &a, - &["from", "Orders_CustId_to_Customers"], - ); + assert_candidate_present(&a, &["from", "Orders_CustId_to_Customers"]); crate::snap!("after_relationship_keyword", a); } @@ -61,10 +58,7 @@ fn after_parent_table_dot_narrows_to_parent_columns() { #[test] fn after_to_offers_table_names() { let schema = schema_with_relationship(); - let a = assess_at_end( - "drop relationship from Orders.CustId to ", - &schema, - ); + let a = assess_at_end("drop relationship from Orders.CustId to ", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); assert_candidate_present(&a, &["Customers", "Orders"]); crate::snap!("after_to", a); @@ -98,10 +92,7 @@ fn complete_drop_relationship_by_endpoints_parses() { #[test] fn complete_drop_relationship_by_name_parses() { let schema = schema_with_relationship(); - let a = assess_at_end( - "drop relationship Orders_CustId_to_Customers", - &schema, - ); + let a = assess_at_end("drop relationship Orders_CustId_to_Customers", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("DropRelationship")); crate::snap!("complete_by_name", a); diff --git a/tests/typing_surface/explain.rs b/tests/typing_surface/explain.rs index dab465e..696fcf7 100644 --- a/tests/typing_surface/explain.rs +++ b/tests/typing_surface/explain.rs @@ -66,10 +66,7 @@ fn complete_explain_show_data_parses_as_explain() { #[test] fn complete_explain_show_data_with_where_and_limit_parses() { let schema = schema_serial_pk(); - let a = assess_at_end( - "explain show data Customers where id = 1 limit 5", - &schema, - ); + let a = assess_at_end("explain show data Customers where id = 1 limit 5", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("Explain")); crate::snap!("complete_explain_show_data_where_limit", a); @@ -95,10 +92,7 @@ fn after_explain_update_table_expects_set() { #[test] fn complete_explain_update_parses_as_explain() { let schema = schema_serial_pk(); - let a = assess_at_end( - "explain update Customers set Name='Bo' where id=1", - &schema, - ); + let a = assess_at_end("explain update Customers set Name='Bo' where id=1", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("Explain")); crate::snap!("complete_explain_update", a); diff --git a/tests/typing_surface/insert_form_a.rs b/tests/typing_surface/insert_form_a.rs index ce00ef2..2873966 100644 --- a/tests/typing_surface/insert_form_a.rs +++ b/tests/typing_surface/insert_form_a.rs @@ -143,10 +143,7 @@ fn after_values_keyword_expects_open_paren() { // Trailing space so we're past the `values` word boundary // — without it the partial-prefix logic re-offers `values` // itself as the candidate that matches the typed prefix. - let a = assess_at_end( - "insert into Customers (Name) values ", - &schema, - ); + let a = assess_at_end("insert into Customers (Name) values ", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); assert_candidate_present(&a, &["("]); crate::snap!("after_values_keyword", a); @@ -159,10 +156,7 @@ fn after_values_keyword_expects_open_paren() { #[test] fn after_values_open_paren_form_a_text_column_prose_names_column() { let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers (Name) values (", - &schema, - ); + let a = assess_at_end("insert into Customers (Name) values (", &schema); assert!( hint_prose_contains(&a, "Name"), "expected column name in prose, got {:?}", @@ -179,13 +173,12 @@ fn after_values_open_paren_form_a_text_column_prose_names_column() { #[test] fn after_values_open_paren_form_a_serial_column_offers_null_to_auto_generate() { let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers (id, Name) values (", - &schema, + let a = assess_at_end("insert into Customers (id, Name) values (", &schema); + let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose hint, got {:?}", a.hint)); + assert!( + prose.contains("id"), + "prose should name `id`, got {prose:?}" ); - let prose = hint_prose(&a) - .unwrap_or_else(|| panic!("expected Prose hint, got {:?}", a.hint)); - assert!(prose.contains("id"), "prose should name `id`, got {prose:?}"); assert!( prose.contains("null") && prose.contains("auto-generate"), "prose should mention `null` to auto-generate, got {prose:?}", @@ -200,8 +193,7 @@ fn mid_value_list_after_comma_advances_to_next_column_prose() { "insert into Customers (Name, Email) values ('Alice', ", &schema, ); - let prose = hint_prose(&a) - .unwrap_or_else(|| panic!("expected Prose hint, got {:?}", a.hint)); + let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose hint, got {:?}", a.hint)); assert!( prose.contains("Email"), "prose should name `Email`, got {prose:?}", @@ -281,9 +273,8 @@ fn form_a_complete_with_serial_in_list_parses() { fn form_a_int_slot_prose_says_integer() { let schema = schema_every_type(); let a = assess_at_end("insert into Things (k) values (", &schema); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose for int slot, got {:?}", a.hint) - }); + let prose = + hint_prose(&a).unwrap_or_else(|| panic!("expected Prose for int slot, got {:?}", a.hint)); assert!( prose.contains("integer"), "int-slot prose should say `integer`, got {prose:?}", @@ -295,9 +286,8 @@ fn form_a_int_slot_prose_says_integer() { fn form_a_date_slot_prose_says_yyyy_mm_dd() { let schema = schema_every_type(); let a = assess_at_end("insert into Things (dt) values (", &schema); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose for date slot, got {:?}", a.hint) - }); + let prose = + hint_prose(&a).unwrap_or_else(|| panic!("expected Prose for date slot, got {:?}", a.hint)); assert!( prose.contains("YYYY-MM-DD"), "date-slot prose should reference YYYY-MM-DD format, got {prose:?}", @@ -309,9 +299,8 @@ fn form_a_date_slot_prose_says_yyyy_mm_dd() { fn form_a_bool_slot_prose_mentions_true_false() { let schema = schema_every_type(); let a = assess_at_end("insert into Things (b) values (", &schema); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose for bool slot, got {:?}", a.hint) - }); + let prose = + hint_prose(&a).unwrap_or_else(|| panic!("expected Prose for bool slot, got {:?}", a.hint)); assert!( prose.contains("true") && prose.contains("false"), "bool-slot prose should mention `true`/`false`, got {prose:?}", @@ -323,9 +312,8 @@ fn form_a_bool_slot_prose_mentions_true_false() { fn form_a_shortid_slot_prose_mentions_null_to_auto_generate() { let schema = schema_every_type(); let a = assess_at_end("insert into Things (sid) values (", &schema); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose for shortid slot, got {:?}", a.hint) - }); + let prose = hint_prose(&a) + .unwrap_or_else(|| panic!("expected Prose for shortid slot, got {:?}", a.hint)); assert!( prose.contains("null") && prose.contains("auto-generate"), "shortid-slot prose should mention `null` to auto-generate, got {prose:?}", diff --git a/tests/typing_surface/insert_form_b.rs b/tests/typing_surface/insert_form_b.rs index b994f6f..c453bd8 100644 --- a/tests/typing_surface/insert_form_b.rs +++ b/tests/typing_surface/insert_form_b.rs @@ -38,7 +38,10 @@ fn form_b_first_value_skips_serial_column() { let schema = schema_serial_pk(); let a = assess_at_end("insert into Customers values (", &schema); let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose at first Form B value slot, got {:?}", a.hint) + panic!( + "expected Prose at first Form B value slot, got {:?}", + a.hint + ) }); // The value slot itself must be keyed on `Name` — the first // non-auto column — not on the skipped `id`. @@ -60,9 +63,7 @@ fn form_b_first_value_skips_serial_column() { fn form_b_first_value_text_pk_names_first_column() { let schema = schema_text_pk(); let a = assess_at_end("insert into Items values (", &schema); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose, got {:?}", a.hint) - }); + let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose, got {:?}", a.hint)); assert!( prose.contains("Code"), "Form B should name the PK column `Code`, got prose: {prose:?}", @@ -76,9 +77,7 @@ fn form_b_first_value_every_type_first_column_is_int() { // must say `integer` and name `k`. let schema = schema_every_type(); let a = assess_at_end("insert into Things values (", &schema); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose, got {:?}", a.hint) - }); + let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose, got {:?}", a.hint)); assert!( prose.contains("k"), "should name column `k`, got prose: {prose:?}", @@ -98,13 +97,9 @@ fn form_b_first_value_every_type_first_column_is_int() { #[test] fn form_b_after_first_value_advances_to_next_column() { let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers values ('Alice', ", - &schema, - ); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose at second slot, got {:?}", a.hint) - }); + let a = assess_at_end("insert into Customers values ('Alice', ", &schema); + let prose = + hint_prose(&a).unwrap_or_else(|| panic!("expected Prose at second slot, got {:?}", a.hint)); assert!( prose.contains("Email"), "second slot should name `Email`, got prose: {prose:?}", @@ -121,10 +116,7 @@ fn form_b_after_first_value_advances_to_next_column() { #[test] fn form_b_in_progress_after_comma_is_incomplete() { let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers values ('Alice', ", - &schema, - ); + let a = assess_at_end("insert into Customers values ('Alice', ", &schema); assert!( matches!(a.state, InputState::IncompleteAtEof), "in-progress Form B should be Incomplete, got {:?}", @@ -136,10 +128,7 @@ fn form_b_in_progress_after_comma_is_incomplete() { #[test] fn form_b_in_progress_without_closing_paren_is_incomplete() { let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers values ('Alice', 'a@b.c'", - &schema, - ); + let a = assess_at_end("insert into Customers values ('Alice', 'a@b.c'", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); crate::snap!("form_b_in_progress_no_close_paren", a); } @@ -201,10 +190,7 @@ fn form_b_with_extra_value_for_serial_column_is_invalid() { #[test] fn form_b_with_correct_values_parses() { let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers values ('Alice', 'a@b.c')", - &schema, - ); + let a = assess_at_end("insert into Customers values ('Alice', 'a@b.c')", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("Insert")); crate::snap!("form_b_valid", a); @@ -213,10 +199,7 @@ fn form_b_with_correct_values_parses() { #[test] fn form_b_text_pk_with_correct_values_parses() { let schema = schema_text_pk(); - let a = assess_at_end( - "insert into Items values ('SKU-1', 'Widget')", - &schema, - ); + let a = assess_at_end("insert into Items values ('SKU-1', 'Widget')", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("Insert")); crate::snap!("form_b_text_pk_valid", a); @@ -240,9 +223,8 @@ fn form_b_text_pk_with_correct_values_parses() { fn form_b_first_slot_mentions_skipped_serial_column() { let schema = schema_serial_pk(); let a = assess_at_end("insert into Customers values (", &schema); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose at first Form B slot, got {:?}", a.hint) - }); + let prose = hint_prose(&a) + .unwrap_or_else(|| panic!("expected Prose at first Form B slot, got {:?}", a.hint)); // Names the skipped auto-gen column. assert!( prose.contains("`id`"), @@ -261,13 +243,9 @@ fn form_b_second_slot_omits_skip_note() { // The note fires once, at the first slot only — not at // every comma. let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers values ('Alice', ", - &schema, - ); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose at second slot, got {:?}", a.hint) - }); + let a = assess_at_end("insert into Customers values ('Alice', ", &schema); + let prose = + hint_prose(&a).unwrap_or_else(|| panic!("expected Prose at second slot, got {:?}", a.hint)); assert!( !prose.contains("auto-generated"), "second-slot hint must NOT repeat the skip note, got: {prose:?}", @@ -280,9 +258,7 @@ fn form_b_text_pk_has_no_skip_note() { // No auto-gen columns → no skip note. let schema = schema_text_pk(); let a = assess_at_end("insert into Items values (", &schema); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose, got {:?}", a.hint) - }); + let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose, got {:?}", a.hint)); assert!( !prose.contains("auto-generated"), "text-PK table has no auto-gen column — no skip note expected, got: {prose:?}", @@ -295,13 +271,8 @@ fn form_a_first_slot_has_no_skip_note() { // Form A lists columns explicitly — the user is in control, // no pedagogical pointer needed. let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers (Name) values (", - &schema, - ); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose, got {:?}", a.hint) - }); + let a = assess_at_end("insert into Customers (Name) values (", &schema); + let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose, got {:?}", a.hint)); assert!( !prose.contains("auto-generated"), "Form A must not show the Form-B skip note, got: {prose:?}", @@ -315,9 +286,8 @@ fn form_b_advances_through_every_type_first_to_real() { // first value, prose must name `r` and say `number`. let schema = schema_every_type(); let a = assess_at_end("insert into Things values (1, ", &schema); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose at 2nd slot, got {:?}", a.hint) - }); + let prose = + hint_prose(&a).unwrap_or_else(|| panic!("expected Prose at 2nd slot, got {:?}", a.hint)); assert!(prose.contains("r"), "should name `r`, got prose: {prose:?}"); assert!( prose.contains("number"), diff --git a/tests/typing_surface/insert_form_c.rs b/tests/typing_surface/insert_form_c.rs index 1499b65..eb6e778 100644 --- a/tests/typing_surface/insert_form_c.rs +++ b/tests/typing_surface/insert_form_c.rs @@ -26,10 +26,7 @@ fn form_c_text_pk_correct_values_parses() { // Items(Code:text, Title:text) — Form C expects two text // values (no auto-gen columns to skip). let schema = schema_text_pk(); - let a = assess_at_end( - "insert into Items ('SKU-1', 'Widget')", - &schema, - ); + let a = assess_at_end("insert into Items ('SKU-1', 'Widget')", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("Insert")); crate::snap!("form_c_text_pk_valid", a); @@ -40,10 +37,7 @@ fn form_c_serial_pk_correct_values_parses() { // Customers(id:serial, Name:text, Email:text) — Form C // skips the serial `id`, expects two text values. let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers ('Alice', 'a@b.c')", - &schema, - ); + let a = assess_at_end("insert into Customers ('Alice', 'a@b.c')", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("Insert")); crate::snap!("form_c_serial_pk_valid", a); @@ -53,10 +47,7 @@ fn form_c_serial_pk_correct_values_parses() { fn form_c_with_null_value_parses() { // null is type-compatible with any slot. let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers (null, 'a@b.c')", - &schema, - ); + let a = assess_at_end("insert into Customers (null, 'a@b.c')", &schema); assert!(matches!(a.state, InputState::Valid)); crate::snap!("form_c_null_value", a); } @@ -71,10 +62,7 @@ fn form_c_rejects_number_for_text_column() { // rejects it at parse time. Before Form-C type-awareness // this parsed Valid and only failed at bind time. let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers (3.14, 'a@b.c')", - &schema, - ); + let a = assess_at_end("insert into Customers (3.14, 'a@b.c')", &schema); assert!( !matches!(a.state, InputState::Valid), "Form C should now type-check `3.14` against Name(text), got {:?}", @@ -114,13 +102,9 @@ fn form_c_second_slot_shows_typed_prose_for_column() { // First token `'Alice'` is a string literal → Form C. At // the second slot the hint names the Email column. let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers ('Alice', ", - &schema, - ); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose at Form C second slot, got {:?}", a.hint) - }); + let a = assess_at_end("insert into Customers ('Alice', ", &schema); + let prose = hint_prose(&a) + .unwrap_or_else(|| panic!("expected Prose at Form C second slot, got {:?}", a.hint)); assert!( prose.contains("Email"), "Form C second slot should name `Email`, got prose: {prose:?}", @@ -143,10 +127,7 @@ fn form_c_in_progress_after_comma_is_incomplete() { #[test] fn form_c_in_progress_without_close_paren_is_incomplete() { let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers ('Alice', 'a@b.c'", - &schema, - ); + let a = assess_at_end("insert into Customers ('Alice', 'a@b.c'", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); crate::snap!("form_c_in_progress_no_close", a); } diff --git a/tests/typing_surface/mod.rs b/tests/typing_surface/mod.rs index bf4d88a..dae318e 100644 --- a/tests/typing_surface/mod.rs +++ b/tests/typing_surface/mod.rs @@ -22,25 +22,25 @@ use rdbms_playground::input_render::{ }; use rdbms_playground::mode::Mode; +pub mod add_relationship; +pub mod app_commands; +pub mod candidate_ordering; +pub mod constraints; +pub mod create_m2n; +pub mod create_table; +pub mod delete_all_rows; +pub mod delete_with_where; +pub mod drop_column; +pub mod drop_relationship; +pub mod explain; +pub mod index_ops; pub mod insert_form_a; pub mod insert_form_b; pub mod insert_form_c; -pub mod update_with_where; -pub mod update_all_rows; -pub mod delete_with_where; -pub mod where_expression; -pub mod delete_all_rows; -pub mod explain; -pub mod create_table; -pub mod drop_column; -pub mod drop_relationship; -pub mod add_relationship; -pub mod create_m2n; -pub mod index_ops; -pub mod constraints; pub mod rename_change_column; -pub mod app_commands; -pub mod candidate_ordering; +pub mod update_all_rows; +pub mod update_with_where; +pub mod where_expression; // ========================================================= // Canonical schema shapes (handoff §1 — CANONICAL_SCHEMA_SHAPES) @@ -81,10 +81,7 @@ pub fn schema_serial_pk() -> SchemaCache { /// Exercises the no-auto-gen path: Form A and Form B both /// require values for every column. pub fn schema_text_pk() -> SchemaCache { - build_schema(&[( - "Items", - &[("Code", Type::Text), ("Title", Type::Text)], - )]) + build_schema(&[("Items", &[("Code", Type::Text), ("Title", Type::Text)])]) } /// Two tables sharing no column names. @@ -96,10 +93,7 @@ pub fn schema_text_pk() -> SchemaCache { /// the other table. pub fn schema_multi_table() -> SchemaCache { build_schema(&[ - ( - "Customers", - &[("id", Type::Serial), ("Name", Type::Text)], - ), + ("Customers", &[("id", Type::Serial), ("Name", Type::Text)]), ( "Orders", &[ @@ -436,10 +430,7 @@ fn smoke_assess_at_end_returns_each_field() { #[test] fn smoke_assess_parse_label_round_trips() { let schema = schema_serial_pk(); - let a = assess_at_end( - "insert into Customers values ('Alice', 'a@b.c')", - &schema, - ); + let a = assess_at_end("insert into Customers values ('Alice', 'a@b.c')", &schema); assert_eq!(a.parse_result.as_deref(), Ok("Insert")); assert!(matches!(a.state, InputState::Valid)); } @@ -462,7 +453,11 @@ fn seed_completion_and_validity() { // Validity (ADR-0027): a known table seeds clean; an unknown one is // flagged (same table slot as update/delete/show data). let ok = assess_at_end("seed Customers 5", &schema); - assert!(matches!(ok.state, InputState::Valid), "known table: {:?}", ok.state); + assert!( + matches!(ok.state, InputState::Valid), + "known table: {:?}", + ok.state + ); // seed's unknown-table behaviour must match its closest sibling // `show data` (same table-only slot), whatever that is. let seed_ghost = assess_at_end("seed Ghost 5", &schema).state; diff --git a/tests/typing_surface/rename_change_column.rs b/tests/typing_surface/rename_change_column.rs index 4b8a809..699627f 100644 --- a/tests/typing_surface/rename_change_column.rs +++ b/tests/typing_surface/rename_change_column.rs @@ -34,10 +34,7 @@ fn rename_after_old_column_expects_to() { #[test] fn rename_after_to_is_new_name_slot() { let schema = schema_serial_pk(); - let a = assess_at_end( - "rename column in Customers: Email to ", - &schema, - ); + let a = assess_at_end("rename column in Customers: Email to ", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); crate::snap!("rename_after_to", a); } @@ -45,10 +42,7 @@ fn rename_after_to_is_new_name_slot() { #[test] fn complete_rename_column_parses() { let schema = schema_serial_pk(); - let a = assess_at_end( - "rename column in Customers: Email to ContactEmail", - &schema, - ); + let a = assess_at_end("rename column in Customers: Email to ContactEmail", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("RenameColumn")); crate::snap!("complete_rename", a); @@ -78,10 +72,7 @@ fn change_after_open_paren_offers_type_candidates() { #[test] fn complete_change_column_parses() { let schema = schema_serial_pk(); - let a = assess_at_end( - "change column in Customers: Email (text)", - &schema, - ); + let a = assess_at_end("change column in Customers: Email (text)", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("ChangeColumnType")); crate::snap!("complete_change", a); diff --git a/tests/typing_surface/update_all_rows.rs b/tests/typing_surface/update_all_rows.rs index ffab249..deac580 100644 --- a/tests/typing_surface/update_all_rows.rs +++ b/tests/typing_surface/update_all_rows.rs @@ -7,10 +7,7 @@ use rdbms_playground::input_render::InputState; #[test] fn before_all_rows_flag_is_incomplete() { let schema = schema_serial_pk(); - let a = assess_at_end( - "update Customers set Email='x' ", - &schema, - ); + let a = assess_at_end("update Customers set Email='x' ", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); // Both `where` and `--all-rows` are valid continuations. assert_candidate_present(&a, &["--all-rows"]); @@ -20,10 +17,7 @@ fn before_all_rows_flag_is_incomplete() { #[test] fn complete_update_all_rows_parses() { let schema = schema_serial_pk(); - let a = assess_at_end( - "update Customers set Email='new@b.c' --all-rows", - &schema, - ); + let a = assess_at_end("update Customers set Email='new@b.c' --all-rows", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("Update")); crate::snap!("complete_all_rows", a); @@ -33,10 +27,7 @@ fn complete_update_all_rows_parses() { fn update_without_filter_clause_is_incomplete() { // Per ADR-0014, update requires WHERE or --all-rows. let schema = schema_serial_pk(); - let a = assess_at_end( - "update Customers set Email='new@b.c'", - &schema, - ); + let a = assess_at_end("update Customers set Email='new@b.c'", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); crate::snap!("no_filter_clause", a); } @@ -44,10 +35,7 @@ fn update_without_filter_clause_is_incomplete() { #[test] fn update_partial_flag_name_is_incomplete() { let schema = schema_serial_pk(); - let a = assess_at_end( - "update Customers set Email='x' --all", - &schema, - ); + let a = assess_at_end("update Customers set Email='x' --all", &schema); // Partial flag still in progress. assert!(!matches!(a.state, InputState::Valid)); crate::snap!("partial_flag", a); diff --git a/tests/typing_surface/update_with_where.rs b/tests/typing_surface/update_with_where.rs index efeb8b9..727ba85 100644 --- a/tests/typing_surface/update_with_where.rs +++ b/tests/typing_surface/update_with_where.rs @@ -64,9 +64,7 @@ fn after_set_column_expects_equals() { fn after_equals_offers_typed_slot_prose_for_column() { let schema = schema_serial_pk(); let a = assess_at_end("update Customers set Email=", &schema); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose, got {:?}", a.hint) - }); + let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose, got {:?}", a.hint)); assert!( prose.contains("Email"), "should name `Email`, got prose: {prose:?}", @@ -82,9 +80,7 @@ fn after_equals_offers_typed_slot_prose_for_column() { fn after_equals_for_date_column_says_yyyy_mm_dd() { let schema = schema_every_type(); let a = assess_at_end("update Things set dt=", &schema); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose, got {:?}", a.hint) - }); + let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose, got {:?}", a.hint)); assert!( prose.contains("YYYY-MM-DD"), "date-slot prose should reference YYYY-MM-DD, got {prose:?}", @@ -95,10 +91,7 @@ fn after_equals_for_date_column_says_yyyy_mm_dd() { #[test] fn mid_assignment_list_after_comma_offers_remaining_columns() { let schema = schema_multi_table(); - let a = assess_at_end( - "update Customers set Name='x', ", - &schema, - ); + let a = assess_at_end("update Customers set Name='x', ", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); // Customers columns offered, no leakage. assert_candidate_present(&a, &["id"]); @@ -109,10 +102,7 @@ fn mid_assignment_list_after_comma_offers_remaining_columns() { #[test] fn after_assignments_expects_where_or_all_rows() { let schema = schema_serial_pk(); - let a = assess_at_end( - "update Customers set Email='new@b.c' ", - &schema, - ); + let a = assess_at_end("update Customers set Email='new@b.c' ", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); assert_candidate_present(&a, &["where", "--all-rows"]); crate::snap!("after_assignments", a); @@ -125,10 +115,7 @@ fn after_assignments_expects_where_or_all_rows() { #[test] fn after_where_keyword_offers_active_table_columns() { let schema = schema_multi_table(); - let a = assess_at_end( - "update Customers set Name='x' where ", - &schema, - ); + let a = assess_at_end("update Customers set Name='x' where ", &schema); assert!(matches!(a.state, InputState::IncompleteAtEof)); assert_candidate_present(&a, &["id", "Name"]); assert_no_candidate_named(&a, &["OrderId", "CustId", "Total"]); @@ -138,13 +125,8 @@ fn after_where_keyword_offers_active_table_columns() { #[test] fn after_where_column_equals_offers_typed_prose() { let schema = schema_serial_pk(); - let a = assess_at_end( - "update Customers set Email='x' where id=", - &schema, - ); - let prose = hint_prose(&a).unwrap_or_else(|| { - panic!("expected Prose, got {:?}", a.hint) - }); + let a = assess_at_end("update Customers set Email='x' where id=", &schema); + let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose, got {:?}", a.hint)); assert!( prose.contains("id"), "should name where column `id`, got prose: {prose:?}", @@ -159,10 +141,7 @@ fn after_where_column_equals_offers_typed_prose() { #[test] fn complete_update_with_where_parses() { let schema = schema_serial_pk(); - let a = assess_at_end( - "update Customers set Email='new@b.c' where id=1", - &schema, - ); + let a = assess_at_end("update Customers set Email='new@b.c' where id=1", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("Update")); crate::snap!("complete_update", a); diff --git a/tests/typing_surface/where_expression.rs b/tests/typing_surface/where_expression.rs index 4725c92..627b5c5 100644 --- a/tests/typing_surface/where_expression.rs +++ b/tests/typing_surface/where_expression.rs @@ -95,10 +95,7 @@ fn show_data_after_where_predicate_offers_limit() { #[test] fn complete_show_data_with_where_and_limit_parses() { let schema = schema_serial_pk(); - let a = assess_at_end( - "show data Customers where id=1 limit 10", - &schema, - ); + let a = assess_at_end("show data Customers where id=1 limit 10", &schema); assert!(matches!(a.state, InputState::Valid)); assert_eq!(a.parse_result.as_deref(), Ok("ShowData")); crate::snap!("complete_show_where_limit", a);