Commits
Open from anywhere with Ctrl+P or Ctrl+K (Ctrl+K is a fallback for
IDEs like Zed that capture Ctrl+P before it reaches the terminal).
Features:
- Lists all tables (with [T] prefix) and saved queries ([Q] prefix)
- Real-time fuzzy filtering using the Skim matching algorithm
- Navigate with Up/Down arrows, select with Enter
- Esc to close, Backspace to edit query
- All letter keys type into the search box (j/k/q no longer intercept)
- Works from any state including help/details modals
Previously, the sidebar only showed tables from the 'public' schema.
Now all non-system schemas (e.g. 'public', 'drizzle') are listed and
grouped. Each group header shows a ▶/▼ indicator and can be toggled
with Left/Right. The 'public' schema starts expanded by default.
SQLite remains unchanged — tables are shown flat since it has no
schema concept.
Breaking changes:
- DbDriver::list_tables() now returns Vec<TableInfo> instead of Vec<String>
- table_name parameters in driver methods now use 'schema.name' format for PostgreSQL
Relative paths (e.g. `squeal my.db`) are now absolutized before saving to
the recent list via `canonicalize()` (if the file exists) or by prepending
the current working directory as a fallback. This ensures recent entries
remain valid regardless of which directory the user launches squeal from.
The should_auto_refresh guard required table_focused, which is only true
after pressing Enter/Tab to enter the table. Removed the condition so
auto-refresh also works in preview mode (table selected in sidebar).
Add polling mechanism that re-fetches table rows every 5 seconds when a
table view is active and not in an edit mode. The event loop uses
crossterm's event::poll() with a timeout instead of blocking
event::read(), enabling periodic refresh without background threads.
Supports manual refresh via 'r' key in table view.
Filters now adapt to the column type and expose more operators:
- Number columns: =, ≠, ~, >, <, ≥, ≤
- String columns: ~, =, ≠ (default is ~ for easier UX)
- All columns: new ≠ operator
Also adds bidirectional navigation in the operator selector (h/j/left/down
goes back, k/l/right/up goes forward), and uses unicode symbols in the UI.
The column type is detected from SQLite/Postgres schema metadata. Numeric
comparisons cast both sides to REAL for consistent behaviour.
When squeal is run without arguments, it now displays a startup screen
with ASCII art and a list of recently opened databases. Users can
navigate with j/k or arrow keys, press Enter to open, or Delete to
remove entries.
- Adds ~/.config/squeal/recent.toml to persist recent databases
- Tracks up to 10 recent entries with connection type and timestamp
- Saves new databases to recents when opened via CLI
- Shows example commands when no history exists
- Adds credit footer with author links
Dependencies added: serde, toml, artbox
Extract app.rs tests to app_tests.rs following the driver test pattern.
Reduces app.rs from ~1900 to ~1080 lines (43% reduction).
Add ui/table_tests.rs with unit tests for pure functions:
- compute_col_widths (basic, empty, max cap, partial rows)
- visible_column_range (all fit, partial, scroll, edge cases)
- table_title (all visible, partial, scroll)
Add driver/mod.rs tests for extracted helpers:
- sqlite_value_to_string (all 5 Value variants + invalid UTF8)
- collect_active_filters (empty, single, multiple, all none)
Net: +226 lines of focused test coverage, better file organization.
Remove 5 tests that duplicate driver-level coverage now in sqlite_tests.rs:
- test_app_new_loads_tables (list_tables, table_columns, fetch_rows)
- test_app_in_memory_demo (same assertions as new_loads_tables)
- test_app_empty_db (empty table edge case)
- test_app_large_db (fetch_rows limit, covered by fetch_more_rows)
- test_app_wide_db (wide table columns, covered by table_columns)
The remaining 28 app tests focus on App-level behavior: navigation,
focus/unfocus, scrolling, pagination, filter UI, modal state, and
FK record handling.
Create sqlite_tests.rs with 15 tests covering all SQLite driver methods:
list_tables, table_columns, fetch_rows, fetch_rows_with_filter,
fetch_rows_with_contains_filter, fetch_rows_with_sort,
fetch_rows_with_limit_offset, run_query, run_query_error,
get_foreign_keys, get_foreign_keys_no_fks, fetch_related_record,
fetch_related_record_not_found, null_value_roundtrip,
real_value_roundtrip.
Brings SQLite test coverage to parity with Postgres.
Extract duplicated row-value conversion logic from sqlite.rs into a shared
sqlite_value_to_string() helper in driver/mod.rs. This eliminates three
copies of the same 5-line match block.
Extract duplicated active-filter collection from sqlite.rs, postgres.rs,
and app.rs into a shared collect_active_filters() helper in driver/mod.rs.
Net: -46 lines across 4 files.
Replace the fixed 3-second sleep in start_postgres() with a retry loop
that polls Postgres until it's actually accepting connections. This
evaluates the container readiness rather than relying on a static timeout,
which was causing intermittent 'Connection reset by peer' and 'database
system is starting up' errors on parallel test runs.
Add testcontainers-based PostgreSQL integration tests that run against
a real PostgreSQL instance in Docker. These tests exercise all
DbDriver trait methods:
- list_tables, table_columns
- fetch_rows (with and without filters/sorting)
- run_query (including error handling)
- get_foreign_keys (with and without FKs)
- fetch_related_record (including UUID types)
Tests run sequentially with --test-threads=1 to avoid Docker port
conflicts between concurrent test containers.
This would have caught the u32 oid type mismatch bug that caused
pressing Enter to panic on PostgreSQL tables with foreign keys.
Add a note that commit message titles should explain the bug from a
user perspective (what failed). Technical details and root cause go
in the body. Include good/bad examples for clarity.
The user story was: opening row details on a PostgreSQL database
by pressing Enter on a table row caused a panic instead of showing
the details modal.
Root cause: the get_foreign_keys() query selected con.oid as a
PostgreSQL oid type. The postgres crate deserializes oid as u32,
but the code used row.get::<_, i32>(0), which panics at runtime
when the type mismatch is detected.
Fix: read con.oid as u32 and cast safely to i32 for the
ForeignKeyInfo.id field.
- Replace fragile generate_series+array_length query with UNNEST+WITH
ORDINALITY for robust foreign key detection across PostgreSQL versions
- Add identifier escaping to prevent SQL injection from quoted names
- Use CAST(column AS TEXT) = in fetch_related_record for type-safe
parameter matching against any column type
- Expand row_value_to_string to handle uuid, chrono timestamps, dates,
serde_json values, and bytea types to prevent empty values
Previously pressing Enter on a table with no FKs silently did nothing.
Now it opens a details modal showing the current row data, matching
the behavior for query result views.
Update test_open_modal_no_fks to assert the new expected behavior.
- Introduce driver abstraction via DbDriver trait in src/driver/
- Extract all SQLite-specific logic into src/driver/sqlite.rs
- Add PostgreSQL driver implementation in src/driver/postgres.rs
- Auto-detect connection type: postgres:// -> PostgresDriver, otherwise SQLiteDriver
- Update App to use Box<dyn DbDriver> instead of direct rusqlite::Connection
- Update CLI help text and usage examples for both SQLite and Postgres
- All 48 existing tests pass with the new driver architecture
- Add tui-syntax dependency for syntax highlighting
- Update crossterm to 0.29 and ratatui to 0.29
- Use Highlighter to highlight SQL in query view
- Fix deprecated ratatui API calls (frame.area(), set_cursor_position(), row_highlight_style())
- Unified sidebar now shows tables and queries together with a separator
- Queries are saved to .squeal/queries/ in the working directory
- Query view splits between top SQL textarea and bottom results table
- Tab toggles between SQL edit mode and results view
- Enter opens row detail modal in query results
- Results support sorting, filtering, and horizontal scrolling
- Rename queries with 'r' keybind
- Demo mode (--demo) keeps queries in-memory without disk writes
- Foreign key records modal title adapts to query view context
Add a new filter mode (triggered with /) that allows users to:
- Navigate column headers with h/l and cycle sort order with j/k
- Press Enter to add/edit a filter for the selected column
- Choose between Equals (=) and Contains (~) operators
- Type filter values and press Enter to apply
- Delete existing filters with the Delete key
- See active filters always visible above the table header
Filters use case-insensitive LIKE for Contains and exact match for Equals.
Sort arrows (↑/↓) are reserved 1 char so column widths never jump.
All existing features (FK modal, help screen, pagination) remain intact.
- Enter now works alongside Tab to focus table view from table list
- Add help modal (?) showing all keybinds with context groups
- FK modal now supports j/k cycling, h/l horizontal scrolling, and Enter to switch tables
- Modal tables render with alternating column colors and selection highlight
- Help modal footer pinned to bottom: Press <esc> to close this modal
Pressing Enter on a focused row opens a modal that queries PRAGMA foreign_key_list and fetches the referenced records from each related table. Each record is rendered as a single-row table (headers + data). Pressing Esc closes the modal. The modal auto-closes when switching tables or unfocusing.
- Add scroll_offset to track viewport position independently of cursor
- PageDown/PageUp scroll by page_size while preserving cursor's visual position
- Cursor clamps to last row when target page is smaller than visual position
- Add ensure_cursor_visible for j/k scrolling
- Render only visible window of rows for performance
- Update keybind bar to show PgUp/PgDn controls
- Highlight table list border (yellow+bold) when it is the active view
- Replace l/Right with Tab to toggle between table list and table views
- Hide Esc from keybind legend while keeping it functional
Displays context-sensitive keybinds that change based on whether
the table is focused. Controls that don't change between screens
are left-aligned before those that do. Key names use default text
color with control labels in DarkGray.
Also fixes stale COL_BG_EVEN/COL_BG_ODD references that were
renamed to COL_FG_EVEN/COL_FG_ODD.
- Add App struct for managing database connection and table state
- Implement two-column TUI layout: table list (left) and data (right)
- Add keyboard navigation with arrow keys and j/k
- Add clap-based CLI with --demo flag for in-memory demo database
- Add unit tests for database loading, navigation and demo mode
Open from anywhere with Ctrl+P or Ctrl+K (Ctrl+K is a fallback for
IDEs like Zed that capture Ctrl+P before it reaches the terminal).
Features:
- Lists all tables (with [T] prefix) and saved queries ([Q] prefix)
- Real-time fuzzy filtering using the Skim matching algorithm
- Navigate with Up/Down arrows, select with Enter
- Esc to close, Backspace to edit query
- All letter keys type into the search box (j/k/q no longer intercept)
- Works from any state including help/details modals
Previously, the sidebar only showed tables from the 'public' schema.
Now all non-system schemas (e.g. 'public', 'drizzle') are listed and
grouped. Each group header shows a ▶/▼ indicator and can be toggled
with Left/Right. The 'public' schema starts expanded by default.
SQLite remains unchanged — tables are shown flat since it has no
schema concept.
Breaking changes:
- DbDriver::list_tables() now returns Vec<TableInfo> instead of Vec<String>
- table_name parameters in driver methods now use 'schema.name' format for PostgreSQL
Add polling mechanism that re-fetches table rows every 5 seconds when a
table view is active and not in an edit mode. The event loop uses
crossterm's event::poll() with a timeout instead of blocking
event::read(), enabling periodic refresh without background threads.
Supports manual refresh via 'r' key in table view.
Filters now adapt to the column type and expose more operators:
- Number columns: =, ≠, ~, >, <, ≥, ≤
- String columns: ~, =, ≠ (default is ~ for easier UX)
- All columns: new ≠ operator
Also adds bidirectional navigation in the operator selector (h/j/left/down
goes back, k/l/right/up goes forward), and uses unicode symbols in the UI.
The column type is detected from SQLite/Postgres schema metadata. Numeric
comparisons cast both sides to REAL for consistent behaviour.
When squeal is run without arguments, it now displays a startup screen
with ASCII art and a list of recently opened databases. Users can
navigate with j/k or arrow keys, press Enter to open, or Delete to
remove entries.
- Adds ~/.config/squeal/recent.toml to persist recent databases
- Tracks up to 10 recent entries with connection type and timestamp
- Saves new databases to recents when opened via CLI
- Shows example commands when no history exists
- Adds credit footer with author links
Dependencies added: serde, toml, artbox
Extract app.rs tests to app_tests.rs following the driver test pattern.
Reduces app.rs from ~1900 to ~1080 lines (43% reduction).
Add ui/table_tests.rs with unit tests for pure functions:
- compute_col_widths (basic, empty, max cap, partial rows)
- visible_column_range (all fit, partial, scroll, edge cases)
- table_title (all visible, partial, scroll)
Add driver/mod.rs tests for extracted helpers:
- sqlite_value_to_string (all 5 Value variants + invalid UTF8)
- collect_active_filters (empty, single, multiple, all none)
Net: +226 lines of focused test coverage, better file organization.
Remove 5 tests that duplicate driver-level coverage now in sqlite_tests.rs:
- test_app_new_loads_tables (list_tables, table_columns, fetch_rows)
- test_app_in_memory_demo (same assertions as new_loads_tables)
- test_app_empty_db (empty table edge case)
- test_app_large_db (fetch_rows limit, covered by fetch_more_rows)
- test_app_wide_db (wide table columns, covered by table_columns)
The remaining 28 app tests focus on App-level behavior: navigation,
focus/unfocus, scrolling, pagination, filter UI, modal state, and
FK record handling.
Create sqlite_tests.rs with 15 tests covering all SQLite driver methods:
list_tables, table_columns, fetch_rows, fetch_rows_with_filter,
fetch_rows_with_contains_filter, fetch_rows_with_sort,
fetch_rows_with_limit_offset, run_query, run_query_error,
get_foreign_keys, get_foreign_keys_no_fks, fetch_related_record,
fetch_related_record_not_found, null_value_roundtrip,
real_value_roundtrip.
Brings SQLite test coverage to parity with Postgres.
Extract duplicated row-value conversion logic from sqlite.rs into a shared
sqlite_value_to_string() helper in driver/mod.rs. This eliminates three
copies of the same 5-line match block.
Extract duplicated active-filter collection from sqlite.rs, postgres.rs,
and app.rs into a shared collect_active_filters() helper in driver/mod.rs.
Net: -46 lines across 4 files.
Replace the fixed 3-second sleep in start_postgres() with a retry loop
that polls Postgres until it's actually accepting connections. This
evaluates the container readiness rather than relying on a static timeout,
which was causing intermittent 'Connection reset by peer' and 'database
system is starting up' errors on parallel test runs.
Add testcontainers-based PostgreSQL integration tests that run against
a real PostgreSQL instance in Docker. These tests exercise all
DbDriver trait methods:
- list_tables, table_columns
- fetch_rows (with and without filters/sorting)
- run_query (including error handling)
- get_foreign_keys (with and without FKs)
- fetch_related_record (including UUID types)
Tests run sequentially with --test-threads=1 to avoid Docker port
conflicts between concurrent test containers.
This would have caught the u32 oid type mismatch bug that caused
pressing Enter to panic on PostgreSQL tables with foreign keys.
The user story was: opening row details on a PostgreSQL database
by pressing Enter on a table row caused a panic instead of showing
the details modal.
Root cause: the get_foreign_keys() query selected con.oid as a
PostgreSQL oid type. The postgres crate deserializes oid as u32,
but the code used row.get::<_, i32>(0), which panics at runtime
when the type mismatch is detected.
Fix: read con.oid as u32 and cast safely to i32 for the
ForeignKeyInfo.id field.
- Replace fragile generate_series+array_length query with UNNEST+WITH
ORDINALITY for robust foreign key detection across PostgreSQL versions
- Add identifier escaping to prevent SQL injection from quoted names
- Use CAST(column AS TEXT) = in fetch_related_record for type-safe
parameter matching against any column type
- Expand row_value_to_string to handle uuid, chrono timestamps, dates,
serde_json values, and bytea types to prevent empty values
- Introduce driver abstraction via DbDriver trait in src/driver/
- Extract all SQLite-specific logic into src/driver/sqlite.rs
- Add PostgreSQL driver implementation in src/driver/postgres.rs
- Auto-detect connection type: postgres:// -> PostgresDriver, otherwise SQLiteDriver
- Update App to use Box<dyn DbDriver> instead of direct rusqlite::Connection
- Update CLI help text and usage examples for both SQLite and Postgres
- All 48 existing tests pass with the new driver architecture
- Unified sidebar now shows tables and queries together with a separator
- Queries are saved to .squeal/queries/ in the working directory
- Query view splits between top SQL textarea and bottom results table
- Tab toggles between SQL edit mode and results view
- Enter opens row detail modal in query results
- Results support sorting, filtering, and horizontal scrolling
- Rename queries with 'r' keybind
- Demo mode (--demo) keeps queries in-memory without disk writes
- Foreign key records modal title adapts to query view context
Add a new filter mode (triggered with /) that allows users to:
- Navigate column headers with h/l and cycle sort order with j/k
- Press Enter to add/edit a filter for the selected column
- Choose between Equals (=) and Contains (~) operators
- Type filter values and press Enter to apply
- Delete existing filters with the Delete key
- See active filters always visible above the table header
Filters use case-insensitive LIKE for Contains and exact match for Equals.
Sort arrows (↑/↓) are reserved 1 char so column widths never jump.
All existing features (FK modal, help screen, pagination) remain intact.
- Enter now works alongside Tab to focus table view from table list
- Add help modal (?) showing all keybinds with context groups
- FK modal now supports j/k cycling, h/l horizontal scrolling, and Enter to switch tables
- Modal tables render with alternating column colors and selection highlight
- Help modal footer pinned to bottom: Press <esc> to close this modal
- Add scroll_offset to track viewport position independently of cursor
- PageDown/PageUp scroll by page_size while preserving cursor's visual position
- Cursor clamps to last row when target page is smaller than visual position
- Add ensure_cursor_visible for j/k scrolling
- Render only visible window of rows for performance
- Update keybind bar to show PgUp/PgDn controls
Displays context-sensitive keybinds that change based on whether
the table is focused. Controls that don't change between screens
are left-aligned before those that do. Key names use default text
color with control labels in DarkGray.
Also fixes stale COL_BG_EVEN/COL_BG_ODD references that were
renamed to COL_FG_EVEN/COL_FG_ODD.
- Add App struct for managing database connection and table state
- Implement two-column TUI layout: table list (left) and data (right)
- Add keyboard navigation with arrow keys and j/k
- Add clap-based CLI with --demo flag for in-memory demo database
- Add unit tests for database loading, navigation and demo mode