Skip to content

Rewrite interactive mode: ratatui TUI with completion, syntax highlighting, and more#11

Open
tobias-fire wants to merge 144 commits intomainfrom
rewrite
Open

Rewrite interactive mode: ratatui TUI with completion, syntax highlighting, and more#11
tobias-fire wants to merge 144 commits intomainfrom
rewrite

Conversation

@tobias-fire
Copy link

@tobias-fire tobias-fire commented Feb 25, 2026

Summary

This PR replaces the rustyline-based REPL with a full-featured terminal UI built on ratatui + tui-textarea + crossterm.

Core TUI

  • Three-pane layout: scrollable output pane, multi-line input textarea, status bar footer
  • SQL syntax highlighting in the input area and history search popup
  • Streaming progress bar during query execution
  • Mouse support: click to position cursor, click to accept completion items

Editing

  • Multi-line input with Shift/Alt+Enter to insert newlines
  • Ctrl+Z / Ctrl+Y undo/redo
  • Ctrl+E — open current query in $EDITOR; content loads back on save
  • Alt+F — format SQL in-place using sqlformat
  • Bracket matching highlight

Completion & Search

  • Tab — context-aware completion: SQL keywords/tables/columns, slash command names, @-prefixed file paths
  • Ctrl+R — reverse history search with cursor navigation (Left/Right/Ctrl+A)
  • Ctrl+Space — fuzzy schema search overlay
  • Function signature hint popup
  • Common-prefix completion when multiple candidates share a prefix

Slash commands

Command Description
/run @<file>|<sql> Execute SQL from a file or inline
/benchmark [N] @<file>|<sql> N timed runs + 1 warmup, live stats, Ctrl+C to stop
/watch [N] @<file>|<sql> Re-run every N seconds, Ctrl+C to stop
/qh [limit] [minutes] Query history from information_schema
/refresh Refresh schema completion cache
/view Open last result in csvlens viewer

Dot commands

.format = client:auto|vertical|horizontal|<server-format>, .setting = value, etc.

Transactions

Full Firebolt transaction support (protocol 2.4): BEGIN/COMMIT/ROLLBACK with a TXN badge in the status bar. Server-driven parameter updates (firebolt-update-parameters, firebolt-remove-parameters) are propagated back to the TUI context.

Connection monitoring

  • Startup and background schema cache refresh; retries until information_schema queries succeed (handles "Cluster not yet healthy")
  • Red footer with "✗ No server connection" when server is unreachable
  • Auto-reconnect: schema cache is refreshed and "Reconnected." is shown when the server becomes available

Output & rendering

  • Client-side table renderer (client:auto, client:vertical, client:horizontal) replacing comfy-table
  • client:auto is now the default in interactive mode
  • Headless (non-interactive) mode uses the same renderer; cleaner output without URL/stats noise
  • Structured exit codes: 0 = success, 1 = query error, 2 = system error

Parameterized queries (-p / --param)

Positional query parameters for non-interactive and pipe mode. Values are auto-typed (int, float, bool, NULL, or string) and sent as Firebolt's query_parameters URL parameter.

fb --core -p 42 -p 'Alice' "SELECT * FROM users WHERE id = \$1 AND name = \$2"
echo "SELECT \$1 + \$2;" | fb --core -p 10 -p 32

Ctrl+H help overlay

Floating help window listing all keybindings and slash commands.

Test plan

  • cargo test passes
  • Interactive TUI starts and displays output/input/status panes
  • Tab completion suggests tables, columns, functions, and file paths
  • Ctrl+R opens history search; Left/Right move cursor within search
  • Ctrl+E opens $EDITOR; edited content loads back
  • /benchmark 3 SELECT 1; runs warmup + 3 timed runs with live stats
  • /watch 2 SELECT now(); re-runs every 2s; Ctrl+C stops it
  • BEGIN; ... COMMIT; shows TXN badge; server param updates are reflected
  • Disconnecting server shows red footer; reconnecting shows "Reconnected." and restores completion
  • fb --core -p 42 -p hello "SELECT \$1, \$2" returns 42 hello
  • cargo run -- --core SELECT 1; works headlessly with correct exit code

🤖 Generated with Claude Code

tobias-fire and others added 30 commits February 6, 2026 16:52
Implements --format=auto option that renders JSONLines_Compact output
as formatted tables with automatic content wrapping.

Features:
- New table_renderer module for parsing JSONLines_Compact format
- Dynamic terminal-width-aware table rendering using comfy-table
- Automatic cell content wrapping to fit terminal width
- Support for multiple DATA messages (accumulates before rendering)
- Graceful error handling with fallback to raw output
- Works in both single-query and REPL modes
- Compatible with existing flags (--verbose, --concise)
- Can be saved as default with --update-defaults

Dependencies added:
- terminal_size 0.3 for terminal width detection
- comfy-table 6.2 for table rendering
- home version constraint to avoid edition2024 issues

All tests passing (41/41).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add display_width() function to correctly calculate line width ignoring ANSI escape codes
- Enable ContentArrangement::Dynamic for proper content wrapping in expanded mode
- Add special handling for single-column chunks with wide content using UpperBoundary constraint
- Make should_use_expanded_mode() consistent with rendering by applying same truncation logic
- Add max_value_length parameter to render functions for context-specific truncation (1000 chars for expanded, 10000 for horizontal)
- Add comprehensive tests for expanded mode, truncation, and ANSI width calculation

Fixes alignment issues where very long truncated values (like settings_names with 1000+ chars) caused table borders to extend beyond terminal width. All chunks now align properly at the right edge.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Use LowerBoundary constraints instead of fixed boundaries for multi-column chunks to prevent unnecessary content wrapping
- Skip bottom border of non-last chunks to eliminate double borders between chunks within the same row
- Improves readability by ensuring column names display on single lines and reducing repetitive border lines

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Use lighter '--' borders for header separators (between column names and values)
- Use heavier '==' borders for chunk separators (between different column groups)
- This emphasizes the separation between chunks while keeping headers lighter

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Use '==' borders at the top of each chunk (except first) to emphasize new section
- Use '--' borders for bottom, header separators, and internal structure
- Creates clearer visual separation where each chunk begins

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Remove bottom border of non-last chunks since the next chunk's top border provides separation
- Reduces visual clutter by having only one separator line (==) between chunks
- Last chunk still has bottom border for proper closure

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Integrate csvlens library to provide an interactive viewer for query
results in REPL mode. Users can type \view or press Ctrl+V (then Enter)
to open the last query result in a full-screen viewer with vim-like
navigation, search, and scrolling capabilities.

Features:
- Store last query result in Context for viewing
- Convert query results to CSV format with proper escaping
- Launch csvlens viewer with temporary CSV file
- Support both \view text command and Ctrl+V keybind
- Add \help command to show available commands
- Handle error cases gracefully (no results, query errors, empty data)

Implementation:
- New viewer module (src/viewer.rs) for csvlens integration
- CSV conversion functions in table_renderer with RFC 4180 escaping
- Temporary file creation using process ID for uniqueness
- Command detection in REPL loop for \view and \help
- Ctrl+V keybind inserts \view command (user presses Enter to execute)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replaces the complex chunked expanded mode with a cleaner vertical format
that displays each row as a two-column table (column name | value). This
eliminates ~145 lines of chunking logic while improving readability.

Changes:
- Replace render_table_expanded() with render_table_vertical()
- Rename all "expanded" terminology to "vertical" throughout codebase
- Update format option from --format=expanded to --format=vertical
- Simplify row display: "Row N:" header with simple two-column table
- Column names in cyan bold, values with natural wrapping
- Update all tests to match new format

Benefits: Simpler code, cleaner output, easier to scan, no chunk boundaries

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replaces content-aware vertical mode detection with simple math based on
available space per column. Adds two configurable parameters for control.

Changes:
- Add --min-col-width (default: 15) to control vertical mode threshold
- Add --max-cell-length (default: 1000) for content truncation
- Replace should_use_vertical_mode with simple calculation:
  terminal_width / num_columns < min_col_width
- Update horizontal table renderer to use equal column widths
- Set explicit ColumnConstraint for predictable layout
- Remove all content inspection from decision logic

Benefits:
- Predictable behavior independent of content
- User-configurable thresholds via command-line options
- Simpler code with no content-aware logic
- Equal column widths for consistent visual alignment

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace eprintln!("") and println!("") with eprintln!() and println!()
to fix clippy warnings about unnecessary empty string arguments.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add #[allow(dead_code)] attributes with explanatory comments to:
- JsonLineMessage::Start fields (query_id, request_id, query_label)
- ResultColumn.column_type field

These fields are part of the Firebolt JSONLines_Compact protocol and
required for deserialization but not currently used by the renderer.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add 11 new test functions validating Firebolt's JSONLines_Compact
serialization format for all data types:
- BIGINT (JSON string for precision)
- NUMERIC/DECIMAL (JSON string for exact decimals)
- INT (JSON number)
- DOUBLE/REAL (JSON number)
- DATE/TIMESTAMP (ISO format strings)
- ARRAY (JSON array)
- TEXT (JSON string with unicode)
- BYTEA (hex-encoded string)
- GEOGRAPHY (WKB hex string)
- BOOLEAN/NULL
- CSV null handling differences

These tests verify that format_value() correctly handles Firebolt's
type-specific serialization patterns without requiring type-aware logic.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Delete the display_width() function and its test test_display_width()
as they were added but never used. The comfy_table library handles
ANSI escape sequence width calculation internally with
ContentArrangement::Dynamic.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add comprehensive documentation for Firebolt's type-specific JSON
serialization patterns:

1. Added detailed function documentation for format_value() and
   format_value_csv() explaining how each Firebolt type maps to JSON

2. Added new "Data Type Handling in JSONLines_Compact Format" section
   to CLAUDE.md with:
   - Complete type mapping table (INT, BIGINT, NUMERIC, DATE, etc.)
   - Explanation of why BIGINT/NUMERIC use strings (precision)
   - How BYTEA and GEOGRAPHY are hex-encoded
   - Client-side rendering behavior
   - Note that column_type field is available for future use

This documents the actual Firebolt serialization behavior validated
by the comprehensive test suite added in previous commits.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Change println! to print! for raw body output to match upstream behavior.
The server response already includes trailing newlines, so println! was
adding an extra unwanted newline.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
SQL NULL values are now rendered in dark gray color to distinguish
them from the string "NULL". This improves readability by making it
immediately clear which values are actual nulls vs string data.

Changes:
- NULL values displayed in Color::DarkGrey in both horizontal and
  vertical table rendering modes
- Added tests to verify NULL rendering doesn't crash
- Works in both render_table() and render_table_vertical()

To verify: Run a query with NULL values in a terminal:
  fb --core --format=auto "SELECT NULL as n, 'NULL' as s"

The real NULL will appear in darker gray while the string 'NULL'
will display in normal color.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add explicit client-side vs server-side rendering modes using prefix notation
(client:auto, client:vertical, client:horizontal for client rendering;
PSQL, JSON, CSV, etc. for server rendering). Interactive sessions default
to client:auto for pretty tables, while non-interactive sessions default to
PSQL for backward compatibility. Include helpful warnings when users
accidentally omit the client: prefix, and clarify Ctrl+V+Enter behavior
in documentation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace verbose multi-line statistics with a single clean line showing
only relevant metrics: row count with thousand separators and scanned
bytes with smart KB/MB/GB formatting broken down by local (cache) and
remote (storage). Statistics appear between Time and Request Id for
better readability. Respects --concise flag to suppress all metadata.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Document client-side rendering with client: prefix notation, interactive
result exploration via csvlens viewer, smart statistics formatting, and
updated keyboard shortcuts. Include practical example using
information_schema.engine_query_history.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The home crate was accidentally added but is not used anywhere in the
codebase. The project uses the dirs crate for home directory operations
instead.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…le-column results

- Apply row (10,000) and byte (1MB) limits only in interactive TTY mode;
  non-interactive output and csvlens viewer always receive the full result
- Single-column results get 5x the normal max cell length (5,000 vs 1,000)
- Track interactive mode via Context.is_interactive set from main

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements regex-based syntax highlighting for SQL queries in the
interactive REPL mode with industry-standard color scheme.

Features:
- Keywords (SELECT, FROM, WHERE): Bright Blue
- Functions (COUNT, AVG): Bright Cyan
- Strings ('text'): Bright Yellow
- Numbers (42, 3.14): Bright Magenta
- Comments (-- text): Bright Black (gray)
- Operators: Default (subtle)

Configuration:
- Auto-enabled in interactive TTY mode
- Disabled via --no-color flag
- Respects NO_COLOR environment variable
- Auto-disabled for piped/redirected output

Implementation:
- Regex-based highlighting (no new dependencies)
- 13 comprehensive unit tests
- Graceful error handling
- Colorblind-accessible color scheme based on DuckDB, pgcli, and
  accessibility research

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements regex-based syntax highlighting for SQL queries in the
interactive REPL mode with industry-standard color scheme.

Features:
- Keywords (SELECT, FROM, WHERE): Bright Blue
- Functions (COUNT, AVG): Bright Cyan
- Strings ('text'): Bright Yellow
- Numbers (42, 3.14): Bright Magenta
- Comments (-- text): Bright Black (gray)
- Operators: Default (subtle)

Configuration:
- Auto-enabled in interactive TTY mode
- Disabled via --no-color flag
- Respects NO_COLOR environment variable
- Auto-disabled for piped/redirected output

Implementation:
- Regex-based highlighting (no new dependencies)
- 13 comprehensive unit tests
- Graceful error handling
- Colorblind-accessible color scheme based on DuckDB, pgcli, and
  accessibility research

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implement context-aware auto-completion that suggests table names and
column names from the database schema. The completion system queries
information_schema on startup and caches results for fast lookups.

Key features:
- Auto-complete table names and column names
- Async schema cache refresh (non-blocking startup)
- Support for message-based query response format
- Configurable via --no-completion and --completion-cache-ttl flags
- Runtime control with 'set completion = on/off'
- Manual refresh with \refresh command

Implementation details:
- New completion module with SqlCompleter, SchemaCache, and context detector
- Queries information_schema.tables, columns, and routines
- Thread-safe cache using Arc<RwLock<T>>
- Graceful error handling and fallback

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements context-aware, frequency-based suggestion ordering with schema exploration support. Tables and columns now appear based on query context, usage frequency, and relevance, with system schemas appropriately deprioritized. Schema names can be completed directly and typing 'schema.' shows all tables in that schema.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements auto-completion for SQL functions with lowest priority (below columns, above system schemas). Functions complete with opening parenthesis for immediate argument typing. Operators are filtered out using routine_type != 'OPERATOR' from information_schema.routines.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Rewrites the interactive REPL using ratatui + tui-textarea, replacing
rustyline entirely. The headless (single-query) path is unchanged.

Layout: 3-pane vertical split — scrollable output pane (bottom-anchored),
multi-line input area with tui-textarea, 1-line status bar.

Key features:
- Multi-line SQL editing with Shift+Enter for explicit newlines
- File-backed history (same ~/.firebolt/fb_history format) with
  Up/Down and Ctrl+Up/Down navigation, deduplication, 10k cap
- Kitty keyboard protocol for Shift+Enter disambiguation
- Mouse scroll routed to output pane; Shift+drag still selects text
- Ctrl+C cancels input or in-flight query via CancellationToken
- Ctrl+V / \view opens csvlens (suspends ratatui, resumes after)
- Query output routed through TuiMsg channel (Line / StyledLines)
  to avoid ANSI round-trips for table rendering
- Custom TUI table renderer with Unicode box-drawing borders,
  smart column-width algorithm, auto horizontal/vertical switching,
  per-column header (cyan), NULL (dark gray), error (red) styling
- Control characters in cell values replaced with spaces to prevent
  ratatui misalignment (fixes query_text newline rendering bug)
- SQL echo in output pane: ❯ green+bold, SQL text yellow
- Stats lines (Time:/Scanned:) rendered in dark gray
- Spinner + elapsed time shown while query is running

New files: src/tui/{mod,layout,output_pane,history}.rs, src/tui_msg.rs
Removed:   src/repl_helper.rs (rustyline adapter no longer needed)
Added:     src/docs/{tui,output_pipeline,table_rendering,completion}.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements readline-style incremental reverse search over history:
- Ctrl+R activates search mode; subsequent presses cycle to older matches
- Characters appended to the search query narrow the match in real time
- Backspace removes the last search character and re-searches from most-recent
- Enter accepts the current match into the textarea
- Escape / Ctrl+G restores the textarea to its pre-search content
- Any other key (arrows, etc.) accepts the match then re-dispatches normally

The input pane is replaced by a two-line search overlay with a cyan border:
  (reverse-i-search)`query':
  <matched entry, truncated to pane width>

Status bar shows contextual hint: "Enter accept  Ctrl+R older  Esc cancel"
while search is active.

Search is case-insensitive substring match walking from most-recent entry
backward; HistorySearch struct lives in src/tui/history_search.rs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows live row count in the running pane while a query streams results.
The query thread sends TuiMsg::Progress(n) after each data batch; the
TUI updates progress_rows and renders "⠸ 1.4s  12,345 rows received".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keywords (cyan/bold), strings (yellow), numbers (magenta), comments (gray)
and functions (blue) are highlighted as the user types.  Implemented by
computing byte-range spans from the existing regex patterns in highlight.rs,
then post-processing the ratatui buffer after tui-textarea renders — this
works around tui-textarea 0.7 having no built-in multi-color highlight API.

Also adds the tree-sitter + devgen-tree-sitter-sql crates and a new
sql_parser.rs module (create_parser / sql_language) that Phase 6 will use
for AST-based completion context detection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tobias-fire and others added 30 commits February 24, 2026 18:19
1. push_custom_settings: exclude transaction_id and
   transaction_sequence_id from the "Settings: ..." echo line.
   These are server-managed internal params, not user-visible settings.

2. Schema refresh: suppress the DDL-triggered schema cache refresh when
   a transaction is open.  BEGIN returns zero columns (same DDL signal
   as CREATE TABLE), causing a schema query to fire inside the open
   transaction — which then fails.  The refresh defers naturally to
   after COMMIT/ROLLBACK, which is correct since BEGIN doesn't change
   any schema.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Context::without_transaction() which returns a clone with
transaction_id and transaction_sequence_id stripped from args.extra
and the URL rebuilt.

Use it in all three places that fire internal queries:
- schema cache auto-refresh (DDL-triggered, drain_query_output)
- schema cache manual refresh (/refresh command, do_refresh)
- setting validation (validate_setting in execute_queries)

This also removes the earlier in_transaction() guard on the DDL
schema refresh, which is no longer needed since the context passed to
refresh() is now always transaction-free.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The in_transaction() guard was already dropped in the previous commit
(without_transaction() makes the refresh safe regardless of whether a
transaction is open). Remove the now-incorrect comment that still
described that old special case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tui/mod.rs:
- slash commands: call history.reset_navigation() at entry so Up/Down
  navigation is consistent after /run, /benchmark, /watch, etc.
- execute_queries: fix extra_before baseline — capture it from the
  already-stripped test_ctx instead of self.context so the "did this
  set command add a new parameter" check isn't confused by transaction
  params being present in one but absent in the other
- complete_file_paths: replace unwrap() fallback on read_dir(".") with
  a graceful return of an empty Vec, preventing a panic if the working
  directory becomes unreadable
- do_refresh: route schema refresh errors through context.emit_err()
  instead of eprintln! so they appear in the TUI output pane rather
  than corrupting the terminal

schema_cache.rs:
- do_refresh: all warning/error messages now go through context.emit_err()
  so they are displayed correctly in TUI mode

viewer.rs:
- early-return conditions (no result, query errors, empty columns/rows)
  now return Err() so run_viewer can show them as flash messages instead
  of printing to the suspended terminal
- delete the temp CSV file after csvlens exits
- update tests to assert is_err() for the early-return cases

query.rs:
- emit "^C" to the output channel when a query is cancelled so the TUI
  output pane shows a visible cancellation indicator

args.rs:
- replace format!("&output_format=JSONLines_Compact") with a plain
  string (no interpolation arguments)

CLAUDE.md:
- replace stale rustyline/Ctrl+O/Ctrl+V descriptions with accurate
  ratatui TUI behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…connect pinger

- Set context.tui_output_tx = Some(bg_tx) in TuiApp::new() so all cloned
  contexts (schema refresh, do_refresh) inherit a real sender instead of
  falling back to eprintln! and corrupting the display
- Add bg_rx drain in event loop: routes Warning:/Error: lines to output pane
- Add ConnectionStatus(bool) TuiMsg variant emitted by every schema refresh
- On ConnectionStatus(false): show host/db in red in the status bar and
  spawn a background pinger (SELECT 1, once per second) until reconnected
- On ConnectionStatus(true): restore green indicator, stop pinger, trigger
  a silent schema refresh to populate completions after reconnection
- schema_cache::do_refresh now returns Err when the tables query fails so
  callers can distinguish "server unreachable" from partial failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rintln!

Removed the erroneous ctx.tui_output_tx = None that was stripping the output
channel from the reconnect-triggered schema refresh. When the server starts up
partially (e.g. "Cluster not yet healthy"), the pinger's SELECT 1 would succeed
triggering a reconnect refresh, but schema warnings would fall back to eprintln!
and corrupt the TUI display. Now all warnings from reconnect refresh appear in
the output pane like other background errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oter

- Initialize connected=true (optimistic); avoids spurious double-refresh when
  the startup refresh succeeds on the first try
- When pinger detects reconnection, emit "Reconnected. Refreshing schema cache..."
  to the output pane before spawning the schema refresh task
- Footer now shows " ✗ No server connection" appended to the red host|db span
  so the disconnected state is unambiguous

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the pinger used query_silent("SELECT 1") which returns Ok for any
HTTP 200 response, including {"errors": [{"description": "Cluster not yet
healthy"}]}. This caused premature ConnectionStatus(true) during cluster startup.

- Add ping_server() to query.rs: sends SELECT 42 via query_silent, then scans
  the response body for a top-level "errors" key — returns Err if found
- Startup schema refresh: ping_server first; only run cache.refresh() on Ok,
  otherwise send ConnectionStatus(false) and let the pinger handle it
- Pinger: replace query_silent with ping_server for the same reason

The schema refresh is now only attempted once the server can actually execute
queries, not merely accept TCP connections or return HTTP 200 with error JSON.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… queries succeed

The previous ping_server(SELECT 42) approach failed because SELECT 42 is a trivial
constant that the server handles immediately, while information_schema queries still
fail with "Cluster not yet healthy" during startup.

Changes:
- schema_cache::do_refresh: run tables query first; if the response body contains
  a top-level "errors" key (server starting up), return Err silently without
  emitting warnings. Only run columns/functions/signatures queries after tables
  succeeds, avoiding wasted requests against an unavailable cluster.
- Remove ping_server() from query.rs — no longer needed.
- Replace startup spawn + separate pinger with spawn_schema_retry_loop() that
  calls cache.refresh() directly every 1s until it returns Ok. On first failure
  sends ConnectionStatus(false) (red footer); on success sends ConnectionStatus(true).
  A /dev/null channel suppresses repeated retry warnings from the output pane.
- drain_bg_output ConnectionStatus(true): schema is already populated by the retry
  loop, so just show "Reconnected." without spawning an additional refresh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the startup retry loop sent ConnectionStatus(false), drain_bg_output
saw ping_active=false and spawned a second retry loop. The second loop
immediately hit is_refreshing()=true (set by the still-running first loop)
and returned Ok(()), sending ConnectionStatus(true) without populating the
schema — causing "Reconnected." to appear but completions to not work.

Fix: set ping_active=true before spawning the initial retry loop in run(),
so drain_bg_output's ConnectionStatus(false) handler skips the duplicate spawn.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After cache.refresh() returns Ok(()), check that functions or tables are
non-empty. If both are empty the refresh completed via the is_refreshing()
early-exit path without populating the cache, so retry instead of signalling
a successful reconnection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add src/lib.rs with all module declarations, run(), run_query(),
  exit_code_for(), and a #[no_mangle] fb_cli_main(argc, argv) C FFI
  entry point so fb-cli can be embedded in a C++ binary
- Add get_args_from(raw: &[String]) to args.rs so the caller can inject
  an explicit argv (e.g. ["fb", "--core", ...]) instead of reading
  std::env::args(); get_args() delegates to it
- Simplify main.rs to a thin shim that calls fb_cli_main via FFI
- Remove devgen-tree-sitter-sql and tree-sitter dependencies (unused);
  delete src/sql_parser.rs
- Change crate-type to ["staticlib", "rlib"] so both the C staticlib
  and the Rust binary can use the library

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add transport.rs module abstracting HTTP over both TCP (reqwest) and
Unix domain sockets (hyper). The --unix-socket flag routes all queries
through the given socket path, removing the need for TCP on port 3473.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When --unix-socket is set, the status bar was still displaying the HTTP
host (e.g. localhost:3473). Now it shows the socket path instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
set_textarea_content creates a fresh TextArea whose viewport starts at
(0,0), but ta_col_top/ta_row_top retained the old scroll offset.
next_scroll_top never resets, so a stale col_top caused
apply_textarea_highlights to map characters to wrong screen columns.
Reset both mirrors to 0 whenever the textarea content is replaced.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Passes positional parameters as Firebolt's query_parameters URL parameter.
First -p value becomes $1, second becomes $2, etc. Values are auto-typed:
integers, floats, booleans, and NULL are sent as their native JSON types;
everything else is sent as a JSON string.

Also fix pre-existing test_params_escaping failure: alias param() columns
so the column name is stable across server versions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, fmt_cell replaced all control characters (including \n) with
spaces, collapsing multi-line values into a single line and losing structure.

Changes:
- fmt_cell: preserve \n; only replace other control chars with spaces
- wrap_cell: split on \n first (hard line breaks), then wrap each segment
  by column width independently
- decide_col_widths: use cell_display_width() (max line width) instead of
  chars().count() for natural/min width calculation, and rows_needed_for_cell()
  for the too-tall check, so multi-line cells don't inflate estimates
- any_wrap / any_wrap_in_row: also trigger on embedded newlines

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two independent fixes:

1. Enable bracketed paste mode (EnableBracketedPaste / DisableBracketedPaste).
   The terminal now bundles the entire pasted string into a single
   Event::Paste(String) instead of sending one Event::Key per character.
   handle_paste() inserts the whole string before the next render.
   Bracketed paste is also disabled/re-enabled around suspend_tui/resume_tui
   so external programs (editor, csvlens) are not affected.

2. Drain all pending events before calling terminal.draw(). Previously the
   loop rendered after every single event; now it consumes all immediately-
   available events in a tight inner loop and renders once at the end.
   Pasting N characters without bracketed paste support now costs 1 render
   instead of N. update_signature_hint() is also called once per drain
   cycle rather than once per keystroke.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ture hints

Firebolt Core now returns routine_parameters as an array of structs
({type, name, default_value, comment}) instead of plain type strings,
and adds an is_variadic boolean column.

- Try new query (with is_variadic) first; fall back to old format on error
- Parse new struct format: show "name type" (or just "type" when unnamed)
  and append "..." to the last param when is_variadic is true
- Old format (plain string array) still detected and handled at parse time
- Fix != → <> in routines queries (new Firebolt parser rejects !=)
- Add unit tests for both old and new formats

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Type names now displayed in UPPERCASE (TEXT, INTEGER, TIMESTAMP, ...)
- Default values shown with => separator (e.g. `unit TEXT => 'day'`)
- Variadic marker (...) placed after the type, before any default
- Popup wraps to multiple lines when signature is wider than the terminal:
  each param on its own line, continuation lines aligned under the opening `(`
- Per-token coloring: function name=cyan, param names=white, types=yellow,
  `=>`=gray, default values=light-gray, parens=dim-white

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The popup height was not clamped, so with many overloads it could exceed
the terminal dimensions, causing a ratatui buffer bounds panic.

- Cap popup_h to input_area.y (space above the input pane, the most the
  popup can occupy without going off-screen)
- Also cap against total.height as a hard safety bound
- Truncate displayed lines to match the clamped height
- Return early when there are fewer than 3 rows available above the input

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of one-param-per-line, pack as many params as fit on each line
(greedy fill). Continuation lines align under the opening '('. This
produces fewer display lines per overload, so more overloads fit within
the vertical space before the height clamp kicks in.

Example with max_w=50:
  generate_series(start INTEGER, stop INTEGER,
                  step INTEGER)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the cursor is inside a function call with known signatures, Tab
completion now suggests named parameters (e.g. `unit => `, `value => `)
with priority just below in-query column suggestions.

- Param names are extracted from all overloads of the current function
- Unique names across overloads, in declaration order
- Filtered by the current partial word (prefix match, case-insensitive)
- Completing inserts `<name> => ` (with trailing space)
- Inserted after column items, before tables/functions in the popup
- Only appears when signatures are known (popup is visible)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- completion_popup::popup_area gains bottom_offset param so the popup
  positions itself above the signature hint when both are visible
- render_signature_hint now returns Option<Rect> so its height can be
  passed as bottom_offset to the completion popup
- Render order flipped: signature hint drawn first (lower z), then
  completion popup drawn on top (higher z)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tive mode

gumdrop sets help=true when --help is passed but does not auto-exit when the
struct declares a help field. Add an explicit check after the version check to
print the usage string and return 0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add fb_cli_usage_string() that returns the gumdrop options block as a
  malloc'd C string (caller frees with free()); used by embedding binaries
  to compose their own help output
- Fix --help handler to use raw_args[0] as the program name instead of
  hardcoded "fb"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The spinner character was being erased after the table was already
written to stdout. Since both stderr (spinner) and stdout (table) share
the terminal cursor, the backspace sequence landed in the wrong place,
leaving the spinner character visible at the start of the first table
line.

Cancel and await the spinner at the top of the response arm, before any
output is produced. The post-select cancel+await is still present to
handle the ^C arm where take() was never called.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant