User guide

The README covers installation. This guide is the full reference.

Getting started

squi                      # opens the connection manager
squi --connect prod-api   # opens a saved connection directly
squi --connect 'postgres://app@db.internal/api?sslmode=require&ssh=bastion'
squi --connect ~/data/app.db   # a SQLite file
squi --no-mouse           # keep native terminal text selection

A connection URL is used as given and not saved; see Connection URLs. :open <name|url> opens another one in a new tab, and ctrl+u in the connection manager prompts for one.

In the connection manager press ctrl+n to add a connection, ctrl+t to test it, ctrl+s to save, then enter to connect (ctrl+l on the form lists the server's databases to pick from). In the editor, type SQL and press ctrl+enter or F5 (or alt+enter) to run the statement under the cursor.

Open tabs and their queries are saved when you quit (and every few seconds) and are reopened on the next start; set restore_session = false in settings.toml to turn this off.

ctrl+enter needs a terminal that reports modified keys — kitty, Ghostty, WezTerm, iTerm2, foot, Alacritty and xterm do; macOS Terminal.app does not (use F5, alt+enter or ctrl+j there). In tmux add set -g extended-keys on. Set SQUI_LEGACY_KEYS=1 to switch the extended key handling off.

Press F1 (or ?) at any time for the full key reference, and ctrl+p for the command palette.

Layout

┌ 1 prod-api ● │ 2 staging ──────────────────────────────── ctrl+t new ┐
│ Explorer        │ Query · NORMAL                                     │
│ ▾ app           │ SELECT * FROM orders WHERE status = 'paid'         │
│   ▾ public      ├────────────────────────────────────────────────────┤
│     ▾ Tables    │ Messages │ Result │ ⓘ orders │ Plan                │
│       orders    │  id │ user_id │ status │ total                     │
├─────────────────┴────────────────────────────────────────────────────┤
│ PRODUCTION  postgres prod-api · app · ssh connected  TX 12s  1000 rows│
└──────────────────────────────────────────────────────────────────────┘

Move focus with tab / shift+tab or ctrl+w h/j/k/l; ctrl+w z zooms a pane.

Essential keys

Where Keys
Anywhere F1 help · ctrl+p palette · : command line · ctrl+o connections · ctrl+t new tab · alt+1…9 switch tab · ctrl+g transaction menu · ctrl+c cancel query · ctrl+q quit
Editor ctrl+enter / F5 run statement / selection · F6 run all · F7/F8 explain / analyze · F4 open in $EDITOR · tab complete · ctrl+r history · vim: space r/a/e/h/s/f/E
Explorer enter open data · t / double-click open in new tab · d details · D DDL · s insert SELECT · u use database · x export · i import CSV · / filter · * pin to favourites · r refresh
Results enter inspect cell · v record view · y/Y/c copy · W copy as WHERE · / filter (server-side on tables) · f filter loaded rows · s sort · # count rows · > follow foreign key · < referencing rows · backspace back · * bookmark view · x export · F load all · e edit · o add row · dd delete row · u/U revert · ctrl+s save · [ ] switch output tabs
Admin (:admin) 1…4 processes / variables / users / sizes · c cancel query · K kill session · a auto-refresh

Useful commands: :use [db] [schema], :commit, :rollback, :tx (manual transactions), :timeout 2m (this tab's statement timeout; 0 for none), :format, :edit, :open <name|url>, :consoles (DataGrip queries), :export, :snippet <name>, :snippets, :keys, :set novim, :set fetch=5000.

Every binding above can be changed; see Key bindings.

Browsing tables

Opening a table from the explorer starts a table view. Unlike an ad-hoc query result, its filter and sort run on the server, so they cover the whole table and not just the rows loaded so far:

Statement timeout

Each connection can stop statements that run too long: set Statement timeout in the connection form (30s, 2m, 0 for none). prod connections default to 30s when it is left empty; other environments have no limit. :timeout <value> changes it for the current tab, :timeout default restores the profile's, and the status bar shows ⏱ while one is active.

The timeout is enforced by squi: when it expires the statement is cancelled the same way as ctrl+c, so it works the same on every engine and also covers statements a server-side limit would not (MySQL's max_execution_time only applies to SELECT).

Query parameters

Placeholders in a statement ask for their values before it runs:

SELECT * FROM orders WHERE customer_id = :customer AND created_at > :since;

ctrl+enter opens a small form with one field per parameter; enter moves to the next field and runs from the last one, ctrl+s runs right away, esc cancels. The values are remembered for the rest of the session, so running the query again only needs enter.

Engine Placeholders
all :name
PostgreSQL $1, $2…
MySQL / MariaDB ? (each one is its own parameter: ?1, ?2…)
SQLite ?, ?1, :name, @name, $name

A value is inserted into the statement as written, so it is a SQL literal: 42, 'Ada', NULL, now() - interval '1 day'. Placeholders inside strings, comments, quoted identifiers and dollar-quoted bodies are ignored, as are :: casts, array slices such as a[1:n] and ClickHouse's {name:Type} parameters. The statement with the values filled in is what runs, what the safety checks see and what history records.

Set query_parameters = false in settings.toml to run statements as written.

Schema diff

squi diff SOURCE TARGET compares the tables and views of two schemas and prints what differs in TARGET, followed by the statements that make TARGET match SOURCE:

squi diff staging prod                         # saved connection names
squi diff ./old.db ./new.db                    # SQLite files
squi diff 'postgres://app@db/api' prod -target-schema reporting
squi diff dev dev -source-schema public -target-schema next   # two schemas, one database
-- staging → prod: 3 difference(s)
--   missing column       users.org_id
--   nullability          users.email: null → not null
--   missing index        users.users_org_idx: (none) → (org_id)

ALTER TABLE "public"."users" ADD COLUMN "org_id" bigint;
ALTER TABLE "public"."users" ALTER COLUMN "email" SET NOT NULL;
CREATE INDEX "users_org_idx" ON "public"."users" ("org_id");

It compares columns (type, nullability, default), the primary key, indexes and foreign keys, and whether views exist. Tables missing in TARGET are created from SOURCE's DDL. squi never runs the script: review it and run it yourself.

Flag Meaning
-sql / -report Print only the script / only the list of differences
-drop Write DROP statements for what only TARGET has (by default they are commented out)
-source-db, -target-db Database to compare (default: the connection's)
-source-schema, -target-schema Schema to compare (default: public, dbo, main, or the database)
-exit-code Exit with 1 when the schemas differ, for CI
-config DIR Use another config directory

Connections open as in the app: SSH tunnels, keychain and environment passwords work, and missing passwords and unknown SSH host keys are asked on the terminal. Both sides should use the same engine; across engines only the report is useful. Changes an engine can't make in place (a column change on SQLite, a SQL Server default, a primary key) are written as comments to handle by hand.

Formatting and external editor

:format (space f in vim mode) reformats the statement under the cursor, or the selection: keywords upper-cased, one clause per line, subqueries and CTEs indented. It only changes whitespace and keyword case — strings, comments and identifiers are kept byte for byte.

F4 (:edit, space E) opens the editor's text in $VISUAL or $EDITOR (falling back to vi, or notepad on Windows); when the editor exits, the text replaces the buffer.

Configuration

File Purpose
~/.config/squi/connections.toml Connection profiles (no secrets)
~/.config/squi/settings.toml Preferences: vim_mode, fetch_size, max_column_width, explorer_width, manual_transactions, confirm_all_writes_on_prod, history_limit, restore_session, query_parameters
~/.config/squi/snippets.toml Saved SQL snippets
~/.local/state/squi/history/ Query history (JSONL)
~/.local/state/squi/session.json Open tabs and their SQL, restored at start
~/.local/state/squi/favorites.json Pinned tables and bookmarked table views

XDG_CONFIG_HOME / XDG_STATE_HOME are honoured; --config DIR uses one directory for everything.

Passwords are never written to the config. With Save passwords enabled they go to the OS keychain (macOS Keychain, Secret Service on Linux, Windows Credential Manager); otherwise squi asks when connecting and keeps them in memory for the session. On headless servers without a keychain, set Password env var (password_env) to read the password from an environment variable instead.

Profiles may reference environment variables as ${VAR} or ${VAR:-default} in host, user, database, the TLS files, the SSH settings and params; they are expanded when connecting, and the profile keeps the reference.

Other per-connection settings:

Field Meaning
statement_timeout See Statement timeout
label, color A short tag (EU) and a colour (red…gray or #rrggbb) for tabs and the status bar, to tell connections of the same environment apart
params Extra driver parameters: Postgres connection keywords and runtime parameters, MySQL DSN parameters or session variables, ClickHouse settings, SQL Server connection-string keys, SQLite URI parameters
[[connection]]
name = "orders-eu"
driver = "postgres"
host = "${ORDERS_DB_HOST:-db.internal}"
user = "readonly"
database = "orders"
env = "prod"
label = "EU"
color = "orange"
statement_timeout = "1m"
password_env = "ORDERS_DB_PASSWORD"
[connection.params]
application_name = "squi-${USER}"

Connection URLs

--connect, :open and ctrl+u accept a URL wherever a connection name is expected; ctrl+u in the connection form fills the form from one, to save it.

postgres://app@db.internal:5432/api?sslmode=require   (also postgresql://)
mysql://root@127.0.0.1/shop                           (also mariadb://)
clickhouse://default@ch:8123/logs                     (?secure=true for HTTPS)
sqlserver://sa@mssql:1433/shop                        (also mssql://; ?database= works too)
sqlite:app.db · sqlite:///var/lib/app/data.db · ./app.db

Query parameters set the profile: sslmode, sslrootcert, sslcert, sslkey, env, read_only, statement_timeout (or timeout), fetch_size, name, label, color, host (e.g. a Unix socket directory), and an SSH tunnel with ssh=[user@]host[:port] (or an ~/.ssh/config alias), ssh_jump=a,b and ssh_key=PATH. Anything else becomes a driver parameter. A password in the URL is used for that connection only and never written to disk.

Importing from DataGrip

squi import datagrip --dry-run   # show what would be imported
squi import datagrip             # import, or update what was imported before
squi import datagrip --prune     # also remove those deleted in DataGrip
squi import datagrip --project ~/DataGripProjects/work   # a specific project

squi reads the newest DataGrip installation: its global data sources and those of its recent projects (.idea/dataSources.xml and dataSources.local.xml), with the SSH tunnels they use (options/sshConfigs.xml). --project also accepts the options directory of another JetBrains IDE, e.g. PhpStorm's global data sources. ctrl+g in the connection manager does the same after showing a preview.

Key bindings

Override any binding in settings.toml by its action name; :keys lists every action with its current keys.

[keys]
run = ["ctrl+r", "f5"]
"results.follow_fk" = ["gf", ">"]
analyze = []            # unbind

Global actions have plain names; actions of the result grid, the explorer and the output tabs start with results., explorer. and output.. Navigation keys (hjkl, gg, arrows) and the editor's own keys are fixed. Help (F1), the command palette and :keys show the bindings in effect, and conflicting bindings are reported at start-up.

SSH

A connection can tunnel through SSH. squi dials the database through the SSH connection (like ssh -W), so no local port is opened.

[[connection]]
name = "prod-api"
driver = "postgres"
host = "db.internal"      # resolved on the bastion side
port = 5432
user = "app"
database = "api"
env = "prod"              # red status bar, confirm every write
read_only = true          # block writes entirely
[connection.ssh]
enabled = true
alias = "prod-bastion"    # from ~/.ssh/config

ClickHouse

squi talks to ClickHouse over its HTTP interface: port 8123, or 8443 with TLS (set SSL mode to require, or verify-full to check the certificate). The native protocol port 9000 is not used. Over SSH, point the connection at the HTTP port as seen from the bastion.

Each tab is its own HTTP session, so USE and SET persist between statements. ClickHouse has no transactions, so manual transactions, inline row editing and EXPLAIN ANALYZE are not available; F7 shows EXPLAIN indexes = 1 as a tree. CSV import sends one INSERT … FORMAT TabSeparated per batch, and rows from earlier batches stay if a later one fails.

SQL Server

squi connects to SQL Server and Azure SQL (port 1433) with the pure-Go go-mssqldb driver, through SSH like any other engine. SSL mode maps to the driver's encryption settings: disable turns encryption off, empty / prefer / require encrypt without checking the certificate (as SSMS's Trust server certificate), and verify-ca / verify-full check it against CA cert.

SQLite

A SQLite connection opens a local file (or :memory:); host, port, user and SSH do not apply. The file must already exist, so a typo does not create an empty database; add ?mode=rwc to a sqlite: URL to create one. Foreign keys are enforced and a 5-second busy timeout waits for other writers.

Safety model

Development

make dev-up            # Postgres 16, MySQL 8.4, MariaDB 11, ClickHouse 25.8, SQL Server 2022, two SSH bastions (docker)
make dev-sqlite        # build dev/squi.db from dev/seed/sqlite.sql (no server needed)
make dev               # run squi against them (dev/config/connections.toml; passwords "squi")
make test              # unit tests (SQLite tests always run)
make test-integration  # all tests against the playground, with -race
make dev-reset         # recreate databases from dev/seed/*.sql
Package Responsibility
internal/db Driver-neutral interfaces (Conn, Session, ResultStream, Admin)
internal/db/postgres, internal/db/mysql, internal/db/clickhouse, internal/db/mssql, internal/db/sqlite Engine drivers: catalog queries, DDL, streaming, cancel, explain, admin, row estimates
internal/sshx SSH tunnels: auth chain, known_hosts, ssh_config, ProxyJump, keepalive/reconnect
internal/sqltext Tokenizer, statement splitter, write/destructive classifier
internal/sqlfmt SQL formatter on top of the sqltext tokens
internal/schema Metadata cache and context-aware autocomplete
internal/editsql DML generation for grid edits
internal/dataio Export formats and CSV import
internal/history Query history and snippets
internal/config, internal/secrets Profiles, settings, OS keychain
internal/datagrip DataGrip import: data sources, SSH configs, consoles, keychain passwords
internal/ui Bubble Tea application; internal/ui/components holds the editor, grid, picker and form widgets; internal/ui/keymap the remappable key bindings

Edit this page on GitHub