Foreign-key relationships, rebuild-table, polish round
DSL:
- add 1:n relationship [as <name>] from <P>.<col> to <C>.<col>
[on delete <action>] [on update <action>] [--create-fk]
- drop relationship <name> | from <P>.<col> to <C>.<col>
- show table <name> for re-displaying a structure on demand
Database (ADR-0013):
- Rebuild-table primitive following SQLite's
ALTER-via-rebuild recipe (foreign_keys=OFF outside tx,
copy-by-name, foreign_key_check before commit). Reusable for
B2 (column drops/renames/type changes).
- ReferentialAction enum (no action / restrict / set null /
cascade); SET DEFAULT awaits column DEFAULTs.
- __rdbms_playground_relationships metadata table -- names,
auto-generated as <Parent>_<pcol>_to_<Child>_<ccol>.
- Type::fk_target_type() validation at declaration; friendly
errors for type mismatch, non-PK target, missing column,
duplicate name.
- describe_table populates symmetric outbound + inbound
relationship lists. drop_table refuses while inbound
references exist; outbound metadata cleaned up alongside drop.
App / UI:
- In-line cursor editing in the input field: Left, Right,
Home, End, Delete, Backspace honoring UTF-8 boundaries.
- PageUp / PageDown scrolls the output buffer; viewport row
count fed back from the renderer via App::note_output_viewport
so scroll is capped against the actual visible area
(regression-tested) and snaps to the bottom on new output.
- Failure messages quote the command portion ("verb target"
failed: ...) for visual clarity; RelationshipSelector has a
proper Display impl so "no such relationship" reads cleanly.
- Structure rendering shows References / Referenced by sections.
Docs:
- ADR-0013 covers naming, metadata table, symmetric view, and
the rebuild-table strategy.
- requirements.md updates: C3 (FK done), B2 (primitive in),
T3 (compound-PK FK still pending). New entries: I1a (cursor
editing -- landed), I1b (Ctrl-A/E and readline shortcuts --
pending), V4 partial scroll, V5 (show family), C3a (modify
relationship -- deferred).
Tests: 154 passing (140 lib + 14 integration), 0 skipped.
Clippy clean with nursery enabled.
This commit is contained in:
@@ -17,7 +17,13 @@ use crate::mode::Mode;
|
||||
use crate::theme::Theme;
|
||||
|
||||
/// Render the entire application frame.
|
||||
pub fn render(app: &App, theme: &Theme, frame: &mut Frame<'_>) {
|
||||
///
|
||||
/// Takes `&mut App` because the renderer reports the current
|
||||
/// output-panel row count back to the App for scroll-cap
|
||||
/// computation — without that feedback, scrolling past the top
|
||||
/// of the buffer would slide the visible window off and
|
||||
/// "eat" lines from the bottom on subsequent renders.
|
||||
pub fn render(app: &mut App, theme: &Theme, frame: &mut Frame<'_>) {
|
||||
let area = frame.area();
|
||||
paint_background(theme, frame, area);
|
||||
|
||||
@@ -37,7 +43,7 @@ pub fn render(app: &App, theme: &Theme, frame: &mut Frame<'_>) {
|
||||
render_status_bar(app, theme, frame, outer[1]);
|
||||
}
|
||||
|
||||
fn render_right_column(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
fn render_right_column(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
let rows = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
@@ -105,7 +111,7 @@ fn render_items_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rec
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn render_output_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
fn render_output_panel(app: &mut App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
@@ -123,15 +129,25 @@ fn render_output_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Re
|
||||
vertical: 1,
|
||||
});
|
||||
|
||||
// Show the most recent lines that fit. The output buffer is
|
||||
// append-only, so taking from the back gives "most recent".
|
||||
// Show a window of the buffer ending `output_scroll` lines
|
||||
// above the most recent entry. With scroll == 0 the last
|
||||
// line shown is the most recent; PageUp increases the
|
||||
// scroll, revealing older lines. We report the visible row
|
||||
// count back to App so input handling can cap scroll
|
||||
// correctly between renders (otherwise scroll could drift
|
||||
// past the top and slide the window off).
|
||||
let visible = inner.height as usize;
|
||||
app.note_output_viewport(visible);
|
||||
let total = app.output.len();
|
||||
let max_scroll = total.saturating_sub(visible);
|
||||
let effective_scroll = app.output_scroll.min(max_scroll);
|
||||
let end = total - effective_scroll;
|
||||
let start = end.saturating_sub(visible);
|
||||
let lines: Vec<Line<'_>> = app
|
||||
.output
|
||||
.iter()
|
||||
.rev()
|
||||
.take(visible)
|
||||
.rev()
|
||||
.skip(start)
|
||||
.take(end - start)
|
||||
.map(|line| render_output_line(line, theme))
|
||||
.collect();
|
||||
|
||||
@@ -193,11 +209,29 @@ fn render_input_panel(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rec
|
||||
.title(title)
|
||||
.style(Style::default().bg(theme.bg).fg(theme.fg));
|
||||
|
||||
// Cursor block: the character at the cursor position is rendered
|
||||
// inverted so it is visible without enabling a real terminal cursor.
|
||||
// Cursor block: render the character at the cursor position
|
||||
// inverted so the cursor is visible without enabling a real
|
||||
// terminal cursor. When the cursor is at end-of-input we
|
||||
// append an inverted space.
|
||||
let cursor = app.input_cursor.min(app.input.len());
|
||||
let before = &app.input[..cursor];
|
||||
let (under, after) = if cursor < app.input.len() {
|
||||
// Find the end of the character under the cursor.
|
||||
let mut end = cursor + 1;
|
||||
while end < app.input.len() && !app.input.is_char_boundary(end) {
|
||||
end += 1;
|
||||
}
|
||||
(&app.input[cursor..end], &app.input[end..])
|
||||
} else {
|
||||
(" ", "")
|
||||
};
|
||||
let spans = vec![
|
||||
Span::styled(app.input.as_str(), Style::default().fg(theme.fg)),
|
||||
Span::styled(" ", Style::default().add_modifier(Modifier::REVERSED)),
|
||||
Span::styled(before, Style::default().fg(theme.fg)),
|
||||
Span::styled(
|
||||
under,
|
||||
Style::default().fg(theme.fg).add_modifier(Modifier::REVERSED),
|
||||
),
|
||||
Span::styled(after, Style::default().fg(theme.fg)),
|
||||
];
|
||||
let paragraph = Paragraph::new(Line::from(spans)).block(block);
|
||||
frame.render_widget(paragraph, area);
|
||||
@@ -273,7 +307,7 @@ mod tests {
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
fn render_to_string(app: &App, theme: &Theme, width: u16, height: u16) -> String {
|
||||
fn render_to_string(app: &mut App, theme: &Theme, width: u16, height: u16) -> String {
|
||||
let backend = TestBackend::new(width, height);
|
||||
let mut terminal = Terminal::new(backend).expect("create terminal");
|
||||
terminal
|
||||
@@ -292,17 +326,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn dark_theme_default_view_snapshot() {
|
||||
let app = App::new();
|
||||
let mut app = App::new();
|
||||
let theme = Theme::dark();
|
||||
let snapshot = render_to_string(&app, &theme, 80, 24);
|
||||
let snapshot = render_to_string(&mut app, &theme, 80, 24);
|
||||
insta::assert_snapshot!("default_simple_dark", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_theme_default_view_snapshot() {
|
||||
let app = App::new();
|
||||
let mut app = App::new();
|
||||
let theme = Theme::light();
|
||||
let snapshot = render_to_string(&app, &theme, 80, 24);
|
||||
let snapshot = render_to_string(&mut app, &theme, 80, 24);
|
||||
insta::assert_snapshot!("default_simple_light", snapshot);
|
||||
}
|
||||
|
||||
@@ -311,7 +345,7 @@ mod tests {
|
||||
let mut app = App::new();
|
||||
app.mode = Mode::Advanced;
|
||||
let theme = Theme::dark();
|
||||
let snapshot = render_to_string(&app, &theme, 80, 24);
|
||||
let snapshot = render_to_string(&mut app, &theme, 80, 24);
|
||||
insta::assert_snapshot!("default_advanced_dark", snapshot);
|
||||
}
|
||||
|
||||
@@ -323,7 +357,7 @@ mod tests {
|
||||
let mut app = App::new();
|
||||
app.input.push_str(": sel");
|
||||
let theme = Theme::dark();
|
||||
let snapshot = render_to_string(&app, &theme, 80, 24);
|
||||
let snapshot = render_to_string(&mut app, &theme, 80, 24);
|
||||
insta::assert_snapshot!("one_shot_advanced_dark", snapshot);
|
||||
}
|
||||
|
||||
@@ -355,6 +389,8 @@ mod tests {
|
||||
primary_key: false,
|
||||
},
|
||||
],
|
||||
outbound_relationships: Vec::new(),
|
||||
inbound_relationships: Vec::new(),
|
||||
};
|
||||
app.current_table = Some(desc);
|
||||
// Mirror what the App writes when a DSL command succeeds.
|
||||
@@ -380,7 +416,7 @@ mod tests {
|
||||
});
|
||||
|
||||
let theme = Theme::dark();
|
||||
let snapshot = render_to_string(&app, &theme, 80, 24);
|
||||
let snapshot = render_to_string(&mut app, &theme, 80, 24);
|
||||
insta::assert_snapshot!("populated_with_table_dark", snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user