queryit is a keyboard-driven terminal UI (TUI) for relational databases, written in Go.
Supports PostgreSQL, MySQL/MariaDB, and SQLite. Uses Bubble Tea (Elm architecture) for the UI,
a driver interface for database backends, and x/crypto/ssh for SSH bastion tunneling.
- Go 1.25+, module
github.com/shammianand/queryit - TUI:
charmbracelet/bubbletea,charmbracelet/lipgloss,charmbracelet/bubbles - PostgreSQL:
jackc/pgx/v5withpgxpool(pool per tab, max 3 conns) - MySQL:
go-sql-driver/mysqlviadatabase/sql - SQLite:
modernc.org/sqlite(pure Go, no CGo) viadatabase/sql - CLI:
spf13/cobra - Config:
gopkg.in/yaml.v3, stored at~/.config/queryit/config.yaml
main.go entrypoint; injects ldflags version into cmd
cmd/
root.go cobra root; --profile flag; launches TUI
profile.go profile list/add/remove subcommands
internal/
config/config.go YAML config load/save; XDG paths; $ENV_VAR password expansion;
Profile.DriverName() returns "postgres" when driver field is absent
connection/
manager.go dispatches Connect() by driver name; returns *Conn{Driver, tunnel}
tunnel.go x/crypto/ssh local port forwarding (used by postgres + mysql)
db/
driver.go Driver interface: Execute, Introspect, Ping, Close, DriverName
executor.go Executor wrapper (SetPageSize propagation); shared Row/ResultSet types;
formatValue ([]byte→string if valid UTF-8; map/slice→JSON marshal), paginate
postgres.go PostgresDriver — pgxpool.Acquire per call
introspect_postgres.go Postgres-specific information_schema + pg_catalog queries;
filters partition children; tags partition roots
mysql.go MySQLDriver + introspectMySQL — database/sql, information_schema
sqlite.go SQLiteDriver + introspectSQLite — sqlite_master + PRAGMA table_info
cache/schema.go in-memory SchemaSnapshot (tables, columns, indexes, functions);
JSON disk cache per profile; Table.Partitioned bool
completion/
engine.go context-aware fuzzy suggest (tables/columns only, no keywords)
keywords.go SQL keyword list; backslash commands
tui/
app.go top-level Bubble Tea model; ProfileSelector list; ProfileForm modal
tab.go per-tab model; Focus enum; key routing; query execution via Driver
input.go multi-line editor; history cycling via up/down; autocomplete hook
results.go table + expanded views; currentCol cell cursor; colOffset horizontal scroll;
pagination; isJSON() detection; [J] tag; CurrentCell/CurrentRow/NextCol/PrevCol
jsonviewer.go JSONViewerModal — full-screen scrollable overlay for JSON cells
copymenu.go CopyMenuModal; copyToClipboard (pbcopy/xclip/wl-copy/clip); rowsToCSV/rowToCSV;
exportToFile to ~/queryit_<timestamp>.csv
schemabrowser.go left panel (ctrl+o); 50 cols; list + detail modes
recent.go session-only recent queries; collapses when unfocused
history.go JSONL disk history; searchable overlay (ctrl+r)
autocomplete.go dropdown; up/down navigate; enter accept
tabbar.go tab strip; active tab shows close marker
statusbar.go connection state; row count; elapsed time
styles.go Catppuccin Mocha lipgloss theme; all shared styles
keys.go key binding definitions (ResultsKeyMap: NextCol/PrevCol/OpenJSON/CopyMenu)
Each open database connection is a TabModel. Tabs are independent — separate driver, schema cache, session history, and focus state.
type Driver interface {
Execute(ctx, query) (*ResultSet, error)
Introspect(ctx) (*cache.SchemaSnapshot, error)
Ping(ctx) error
Close()
DriverName() string
}connection.Connect() reads profile.DriverName() and returns the appropriate implementation.
tab.go holds a db.Driver field and calls it directly — no pgx types leak into the TUI layer.
esc rotates: Input → Recent (if entries) → Results → Browser (if open) → Input
Panels skipped if not available. esc inside browser detail goes back to list before cycling out.
ctrl+t→ ProfileSelector → user picks profile or creates one via ProfileForm modaltab.Connect()fires async cmd: dials SSH tunnel if bastion present, then opens driverconnectDoneMsg→ tab storesconn.Driver, createsExecutor, starts schema introspect cmdschemaRefreshDoneMsg→ updates cache + browser
F5 in input → tab.executeQuery():
- Appends to disk history and
sessionQueries(in-memory, newest-first) - Clears input immediately
- Fires async
tea.Cmd→driver.Execute(ctx, query) queryDoneMsg→ results pane and status bar update
\dt, \d, \dn, \di, \df dispatch to driver-appropriate SQL in handleBackslash().
\refresh calls driver.Introspect() in a goroutine and updates the cache.
- One Driver per tab — drivers manage their own concurrency (pgxpool.Acquire, sql.DB pool). Never share a connection between goroutines.
- Width arithmetic uses plain string lengths, not
lipgloss.Width, for padding calculations. Apply styles after the string is built at the correct length. Mixing ANSI strings intolen()breaks layout (learned the hard way in schemabrowser.go). sessionQueries([]string, newest-first, per TabModel) feeds the Recent panel and inline up/down history.HistoryModel(JSONL on disk) feeds onlyctrl+rsearch. New tab = emptysessionQueries.- Partition children excluded from schema browser. Only parent tables shown; roots tagged
Partitioned: true. Postgres-only — MySQL and SQLite have no partitioned table concept. browserWidth = 50— all width math inschemabrowser.gouses plainlen().- Modal key routing order in
tab.go handleKey: history overlay → JSON viewer → copy menu → global keys → focus dispatch. Each modal captures all keys while open;escin the copy menu returnst, nilexplicitly to prevent leaking into the focus-cycle handler. - JSON detection (
isJSONinresults.go): first-byte check ({or[) thenjson.Unmarshal— called once per visible cell during render, no caching needed. lastRenderedLastColis set insideviewTable()each render and read byNextCol()to know when the cursor has scrolled past the viewport edge. Never set it manually.formatValuehandles JSON driver types: PostgreSQL/MySQL return JSON columns asmap[string]interface{}or[]interface{}; these are re-marshalled to JSON strings soisJSONworks. Valid-UTF-8[]byteis returned asstring(val); binary data falls back to hex.- Input box height is fixed (
inputVisibleLines = 4,inputBoxH = 6). Padded to exactlymaxVisibleLinesrows so it never shifts layout. driverfield in config isomitempty— defaults to"postgres"viaProfile.DriverName(). No existing config breaks.
- Create
internal/db/<name>.goimplementingdb.Driver - Use
database/sqlwith a pure-Go driver if possible - Add
connect<Name>()inconnection/manager.goand wire into theswitchinConnect() - Add backslash command SQL variants in
tab.go handleBackslash()for the new driver name - No changes needed in config, TUI, cache, or completion packages
- Implement
Height() intandView() string - Wire into
tab.SetSize()(subtract panel height from results) - Add a
FocusXconstant to theFocusenum - Handle in
setFocus()and theesccycle inhandleKey()
- Theme: Catppuccin Mocha. All colours and shared styles in
tui/styles.go. - No blink ticker — static solid cursor (
styleCursor: accent bg, dark fg). - Autocomplete renders above the input box inside the results area — input never shifts.
- No mouse support. No GUI.