A lightweight TUI SQL client for data observation — quickly see, sort, and explore raw data to spot anomalies and form hypotheses. Built with Bubble Tea. Supports SQLite, MySQL, and PostgreSQL.
asql is not an analytics platform — it's a data observation tool.
Heavy lifting belongs in the cloud. asql is for the moment before that: quickly touching raw data, noticing anomalies, and forming hypotheses. It stays light, stays quiet, and becomes indispensable.
Download a prebuilt binary from GitHub Releases.
Or install with Go:
go install github.com/kwrkb/asql@latestOr build from source:
git clone https://github.com/kwrkb/asql
cd asql
go build -o asql .# SQLite
asql <path-to-sqlite-file>
# MySQL
asql "mysql://user:password@host:3306/dbname"
# PostgreSQL
asql "postgres://user:password@host:5432/dbname"
# Connect via saved profile
asql @myprofile
# Save a connection as a profile
asql --save-profile myprofile "postgres://user:pass@host:5432/db"
# No arguments — select from saved profiles interactively
asql
# Read-only session
asql --readonly @production
# Help / version
asql --help
asql --version- Type-aware headers — column types displayed alongside names (
name text,age int) - NULL / empty distinction — NULL stays
NULL, empty strings shown as""so you never confuse them - In-place sorting — press
sto cycle sort (None → Asc → Desc) on the selected column; NULLs always sort last - Detail View — press
Enterto inspect a row field-by-field in an overlay; navigate fields withj/k, rows withn/N - Horizontal scrolling — wide tables scroll column-by-column with
h/l; status bar shows[3/12]column position - Tab completion — press
Tabin INSERT mode for context-aware table/column name completion - Query history — recall previous queries with
Ctrl+P/Ctrl+N; search history withCtrl+R - Saved queries (Snippets) — save frequently used queries with
Ctrl+S; browse withSin NORMAL mode - Connection profiles — save/load database connections; switch between them with
Pin NORMAL mode - Multi-connection — connections stay open when switching profiles; no re-connect overhead
- Side-by-side compare mode — press
cto pin current result and split the screen into left (pinned) / right (active) panes; useTabto switch focus. Row-count differences and mismatched cells are highlighted immediately - Fast re-execution across connections — press
Rto re-run the current query; in profile mode,xswitches connection and immediately re-runs - Column statistics — press
dfor a per-column overlay: NULL rate, distinct count, min/max, plus a sparkline for date columns and a histogram for numeric ones - Bring & Join — press
bto copy the current result into a local SQLite database,Jto switch to it, then JOIN results that came from different databases - Paging indicator — status bar shows current position and column info (
col:name 1/100) - Table sidebar — browse tables, insert SELECT with one key
- Export — copy results as CSV / JSON / Markdown, or save to file
- AI assistant — generate SQL from natural language via any OpenAI-compatible API
- Read-only sessions —
--readonlyrefuses statements that would write, so a production connection survives a mistypedDELETE
Start asql with --readonly when you are connecting to a database you only
mean to look at:
asql --readonly @production
asql --readonly "postgres://user:pass@db.example.com:5432/app"Every statement is classified before it is sent. Anything not recognized as read-only is refused with a message naming what was rejected:
readonly: DELETE is not allowed (asql --readonly)
The status bar marks the connection with ro (production:POSTGRES ro) so the
mode is never invisible. SQLite databases are additionally opened through the
driver's read-only mode.
What the guard refuses, beyond the obvious INSERT / UPDATE / DELETE /
DROP:
SELECTforms that write their result somewhere — PostgreSQL'sSELECT ... INTO backup, MySQL'sSELECT ... INTO OUTFILE- multiple statements in one submission (
SELECT 1; DELETE FROM t) - data-modifying CTEs (
WITH gone AS (DELETE FROM t RETURNING *) SELECT * FROM gone) EXPLAIN ANALYZEof a writing statement — PostgreSQL runs its target- pragmas outside schema inspection, including the function form
PRAGMA query_only(0) - any keyword it does not recognize
- anything it cannot read confidently — see below
asql reads one portable subset of SQL rather than each dialect's own lexical
rules, and refuses what falls outside it — whatever database you are connected
to. The subset is '...' / "..." / `...` closed by doubling the quote,
[...], -- with a space after the dashes, and plain /* ... */.
So these are refused everywhere, including where they are harmless:
| Refused | Because dialects disagree | Write instead |
|---|---|---|
'it\'s', "a\"b" |
a backslash closes the quote or not, depending on NO_BACKSLASH_ESCAPES / standard_conforming_strings / ANSI_QUOTES |
'it''s', "a""b" |
SELECT 1 # 2 |
# is a comment on MySQL, the bitwise-XOR operator on PostgreSQL |
SELECT 1 -- 2 or # inside a string |
SELECT 1--1 |
-- needs a following space on MySQL but not elsewhere |
SELECT 1 - -1 |
/*! ... */ |
MySQL runs the contents instead of ignoring them | a plain /* ... */ |
The reason is not pedantry: where a quoted run or a comment ends is what decides where every later keyword falls, so a scanner that guesses wrong reads a write as a read. Refusing the ambiguous spelling ends that whole class of mistake, and the portable spelling is always available.
This is not a sandbox. It is there for the DELETE you did not mean to
run, not for a user who means to write. asql does not promise that a determined
statement cannot get through, and a read-only connection is not a substitute
for database permissions.
Bring & Join keeps working in a read-only session: the local bring database is
asql's own scratch space and stays writable, so you can still press b to
copy a result into it and J to join there.
Spot the diff between prod and staging in 3 seconds — right in your terminal.
| Key | Action |
|---|---|
i |
Enter INSERT mode |
q / Ctrl+C |
Quit |
j / k |
Navigate result rows |
h / l / Left / Right |
Scroll columns horizontally |
PgUp / PgDn |
Page through results |
s |
Toggle sort on selected column (None → Asc → Desc) |
d |
Open column statistics overlay |
Enter |
Open Detail View for current row |
R |
Re-execute current query |
c |
Toggle compare mode (pin current result / close) |
Tab |
Switch focused pane in compare mode (left/right) |
t |
Toggle table sidebar |
e |
Open export menu |
S |
Open saved snippets |
Ctrl+S |
Save current query as snippet |
P |
Open connection profiles |
Ctrl+K |
Open AI assistant |
b |
Bring current result into the local SQLite database (Bring & Join) |
J |
Switch to the local Bring & Join database |
| Key | Action |
|---|---|
Esc |
Return to NORMAL mode |
Ctrl+Enter / Ctrl+J |
Execute query |
Tab |
Autocomplete table/column name |
Ctrl+P / Ctrl+N |
Previous / next query history |
Ctrl+R |
Search query history |
Ctrl+S |
Save current query as snippet |
Ctrl+L |
Clear editor |
Completion popup (when active):
| Key | Action |
|---|---|
Tab / Ctrl+N / Down |
Next completion item |
Ctrl+P / Up |
Previous completion item |
Enter |
Accept selected completion |
Esc |
Close popup |
| Key | Action |
|---|---|
j / k / Down / Up |
Navigate fields |
n / l |
Next row |
N / h |
Previous row |
q / Esc / Enter |
Close Detail View |
| Key | Action |
|---|---|
j / k / Down / Up |
Navigate columns |
q / Esc |
Close statistics overlay |
| Key | Action |
|---|---|
j / k / Down / Up |
Navigate tables |
Enter |
Insert SELECT * FROM <table> LIMIT 100; into editor and switch to INSERT mode |
t / Esc |
Close sidebar |
| Key | Action |
|---|---|
j / k |
Navigate items |
Enter |
Connect (PROFILE) / Load into editor (SNIPPET) |
x |
Switch connection and re-execute query (PROFILE only) |
a |
Add current connection / new snippet |
d |
Delete selected item |
Esc |
Close |
| Key | Action |
|---|---|
j / k / Down / Up |
Navigate export options |
Enter |
Execute selected export |
Esc |
Close |
Press d in NORMAL mode to open the statistics overlay. It is computed in memory from the rows you already have — no extra query is sent.
| Field | Meaning |
|---|---|
NULL% |
Share of rows where the column is SQL NULL |
Distinct |
Number of distinct non-NULL values |
Min → Max |
Smallest and largest value (numeric when the values are numeric) |
The column under the cursor gets one extra line:
- Sparkline for date/timestamp columns — row counts bucketed by year, month or day, e.g.
▁▂▃▅▇▅▂▁ by month - Histogram for numeric columns — value distribution with the covered range, e.g.
▁▂▅█▇▃▁ 0–100
Both are skipped above 10,000 rows to keep the overlay instant.
asql does not federate databases. It brings data to you instead: run a query anywhere, copy the result into a local SQLite database, and JOIN it there.
- Run a query on the first connection and press
b— the result becomes local tablet1. - Switch connection (
P), run another query, pressbagain — that becomest2. - Press
Jto switch to the local database, then query across them:
SELECT t1.name, t2.score
FROM t1 JOIN t2 ON t1.id = t2.id;Values keep their types across the copy: numbers stay numbers, so they sort and JOIN numerically rather than as strings, and SQL NULL stays distinguishable from the text NULL.
Every bring is recorded in a _asql_bring table inside the local database, so you can always ask where a table came from:
SELECT * FROM _asql_bring;| Column | Meaning |
|---|---|
n |
Creation order (matches the number in t1, t2, …) |
table_name |
Local table name |
source |
Connection the result came from |
row_count / col_count |
Size of the brought result |
truncated |
1 when the source result hit the 10,000-row scan limit — the table is a partial view |
query |
The query that produced the data |
The local database lives in memory and is gone when asql exits. Export anything you want to keep with e.
Press e in NORMAL mode after executing a query to open the export menu. Supported formats:
- Copy as CSV — clipboard
- Copy as JSON — clipboard (array of objects)
- Copy as Markdown — clipboard (GFM table)
- Save to File (CSV) — writes
result_YYYYMMDD_HHMMSS.csvto current directory
asql can generate SQL from natural language using any OpenAI-compatible API.
Create a config file at ~/.config/asql/config.yaml:
ai:
ai_endpoint: http://localhost:11434/v1 # OpenAI-compatible API endpoint
ai_model: llama3 # model name
ai_api_key: "" # API key (optional for local models)All config fields:
| Field | Description | Environment variable override |
|---|---|---|
ai.ai_endpoint |
OpenAI-compatible API base URL | ASQL_AI_ENDPOINT |
ai.ai_model |
Model name (e.g. gpt-4o, llama3) |
ASQL_AI_MODEL |
ai.ai_api_key |
API key | ASQL_AI_API_KEY |
Environment variables take precedence over the config file. Both ai_endpoint and ai_model must be set (via file or env) to enable the AI feature.
Examples:
# OpenAI
ai:
ai_endpoint: https://api.openai.com/v1
ai_model: gpt-4o
ai_api_key: sk-...# Ollama (local)
ai:
ai_endpoint: http://localhost:11434/v1
ai_model: llama3
# ai_api_key not neededPress Ctrl+K in NORMAL mode to open the AI prompt. The database schema is automatically included in the context for accurate table/column names.
If no config file is present and no environment variables are set, AI features are silently disabled.
go test ./...
go build
go vet ./...MIT — see LICENSE

