@vascent/lazysql
v0.1.21
Published
A lazygit-style TUI database manager.
Downloads
3,015
Readme
lazysql
English · 简体中文
A lazygit-style terminal database client (TUI). Keyboard-driven, panel-based, cross-database, and generates SQL from natural language — all without leaving the keyboard.
Work with databases in your terminal the way lazygit works with git: connect, browse, edit, query, manage schema.

✨ Features
- 🔌 Multiple data sources: PostgreSQL · MySQL/MariaDB · SQLite · MongoDB · Redis
- 📊 Browse / edit: pagination · column sort · column filter · row-level edit and delete (parameterized, run inside a real transaction, auto-rollback when
affected≠1) - ⌨️ SQL editor: multi-line editing · execute · per-connection persistent history (
^P/^N) · schema-aware completion (table names / column names scoped by FROM / keywords, toggle with^T) · collapses to a one-line echo bar (^O) so the grid keeps the screen - 📤 Data export: to CSV · JSON · SQL — the current grid view, a whole table, an entire schema/category, or a multi-select of tables (
vto mark); streamed to disk at constant memory, cancellable (esc), with a live row count. CSV writes one file per table; JSON and SQL each combine into a single file - 🧬 Schema introspection: tables / views / indexes / sequences / triggers / stored procedures / Postgres enums; inspect an object's columns (enum columns show their allowed values) and DDL definition
- 🛡️ Destructive-operation guard: a
WHERE-lessUPDATE/DELETE,DROP, orTRUNCATEalways pops a centered confirmation dialog echoing the full SQL to be run; when a PostgresDROPfails due to dependents, it offers aCASCADEretry and names the objects that would be dropped along with it - 🌳 Object tree: filter objects by name with
/orf(live-narrows as you type) · auto-refreshes after a successful DDL (CREATE/DROP/ALTER/…) - 🤖 NL→SQL: press
^G, type natural language, and the LLM generates SQL placed into the editor for review (never auto-executed); destructive statements are flagged with a red ⚠ - 🗂️ Connection management: multi-connection config · create / edit / test connections in-TUI · passwords stored separately from config (optional OS Keychain) · SSH tunnel to databases behind a bastion (key/agent auth,
~/.ssh/configaliases work) - 🖱️ Modern terminal UX: mouse / scroll wheel · system-clipboard copy · full-cell inspector (long text wraps by display width, no CJK truncation)
📦 Installation
Ready to run, no Bun required — prebuilt bun --compile native binaries are distributed via npm, one per platform; only the one matching your system gets installed. Supports macOS (Apple Silicon) · Linux (x64 / arm64) · Windows (x64).
Package managers (recommended)
| Method | Command |
|------|------|
| npm | npm i -g @vascent/lazysql |
| bun | bun add -g @vascent/lazysql |
| Try without installing | npx @vascent/lazysql --list |
Then just type lazysql.
When installing with
bun add -g, iflazysqlreports command not found, Bun's global directory~/.bun/binisn't on yourPATH—bun add -gusually prints a hint to add it (npm i -ggenerally doesn't have this issue).
From source (to try unreleased changes)
To run the latest unreleased code, or use your local edits right away:
git clone https://github.com/Yangeyu/lazysql && cd lazysql
bun install
bun link # register a global lazysql → ~/.bun/bin/lazysql (symlinked to this repo)bun link puts the command in Bun's global directory ~/.bun/bin; make sure it's on your PATH (command -v lazysql returns a path). If not, add one line — exactly the same config bun add -g needs:
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.zshrc && source ~/.zshrcThe symlink points at the repo, so git pull or code edits take effect immediately — no reinstall; undo with bun unlink inside the repo.
Upgrade
npm i -g @vascent/lazysql@latest # bun: bun add -g @vascent/lazysql@latestA from-source install upgrades via git pull — the symlink picks it up immediately.
Uninstall
npm un -g @vascent/lazysql # bun: bun remove -g @vascent/lazysql; from source: bun unlinkUninstalling only removes the program itself. Config and history (connections, passwords, SQL history) stay in ~/.config/lazysql/; to wipe them too:
rm -rf ~/.config/lazysqlOn macOS, if you ever set
LAZYSQL_SECRETS=keychain, passwords live in the system keychain and aren't removed by deleting~/.config/lazysql— search forlazysqlin Keychain Access and delete the matching entries manually.
🚀 Usage
lazysql # open the default (first saved) connection
lazysql <name> # open a saved connection by id / name
lazysql <file.db> # open an ad-hoc SQLite file
lazysql --list # list saved connections and exitThe first run auto-creates ~/.config/lazysql/connections.yml. Inside the TUI: press ? for the full keymap, ` to switch connections, and n to create one — see the Keybindings section below for the rest.
⌨️ Keybindings
vim-style, panel-based. Grouped by context below; the full list is in-app via ? (the footer and help both render from the same keymap table, so they never drift).
Global
| Key | Action |
|----|------|
| ` | Switch connection (back to the picker) |
| : | Open the SQL editor (expands it if collapsed) |
| ^O | Expand / collapse the SQL editor — collapsed it is a one-line echo of the SQL behind the grid |
| ^G | Ask in natural language (opens the SQL editor) |
| tab | Toggle focus between tree ↔ results |
| ^H / ^L | Focus the tree / the results grid directly |
| ^⇧- / ^⇧+ | Shrink / widen the connections sidebar |
| ! | Reopen the current error details |
| F12 | Toggle the runtime debug console (logs and unhandled errors) |
| ? | Toggle help |
| q | Quit |
Failures (a refused write, a lost connection, a failed export…) pop an error dialog with the full message, the driver's error code and its detail. esc / ⏎ dismisses it; ! reopens the retained details.
Sidebar (tree)
| Key | Action |
|----|------|
| k / j · ↑ / ↓ | Move selection |
| ⏎ / space | Expand / collapse / open object |
| → / l | Expand |
| ← / h | Collapse / jump to parent |
| a | Clean SELECT * browse of the selected table |
| / or f | Filter objects by name — live-narrows the tree (⏎ keep · esc clear) |
| v | Mark / unmark a table for a batch export (multi-select) |
| X | Export — marked tables, else all tables under the node (schema / category), else this one |
| esc | Clear the tree filter, else all export marks |
| g / G | Jump to first / last |
| D | View the object's DDL / structure |
| d | Draft a DROP into the editor |
| r | Refresh connection and object tree |
| n / e | New / edit connection |
| x | Remove the selected connection |
Results grid
| Key | Action |
|----|------|
| k / j | Move row cursor |
| h / l · ← / → | Move column cursor · horizontal scroll for wide tables |
| g / G | Jump to first / last row |
| ^U / ^D | Half page up / down |
| ⏎ | Inspect the full cell value |
| y | Copy the focused cell's full value to the clipboard |
| a | Browse the selected table (SELECT *) |
| s | Cycle sort (asc → desc → none) |
| / | Filter by column substring |
| esc | Undo the latest applied filter — restore its previous page, sort and focused cell |
| d | Delete row (⏎ inspect, then e edits the cell) |
| X | Export the view — a browsed table to CSV / JSON / SQL (filtered & sorted), a query result to CSV / JSON |
| n / p | Next / previous page |
| D | Toggle Data / DDL tab |
DDL / structure tab
| Key | Action |
|----|------|
| ↑/↓ k/j | Scroll the structure / DDL |
| g / G | Jump to the first / last line |
| D | Toggle Data / DDL tab |
SQL editor
| Key | Action |
|----|------|
| ⏎ | Run the query (results show in the grid) |
| ⇧⏎ | Insert a newline — compose multi-line SQL |
| tab | Accept completion, otherwise move to the next panel |
| ^P / ^N | Previous / next history entry |
| ^T | Toggle schema-aware completion |
| ^G | Generate SQL from natural language |
| ^C | Clear the draft |
| ^O | Collapse the editor to its one-line echo bar (the draft is kept, flagged (draft)) |
| esc | Back to the results grid (the editor stays open) |
Confirmation dialog
| Key | Action |
|----|------|
| y | Apply the pending write / run the export |
| n | Cancel |
| f | Cycle export format (CSV / JSON / SQL) — export confirm only |
Cell inspector
| Key | Action |
|----|------|
| j / k · ↑ / ↓ | Scroll the value |
| e | Edit the cell in place (^S save · esc back) |
| y | Copy the full value to the clipboard |
| q / esc / ⏎ | Close |
New / edit connection form
| Key | Action |
|----|------|
| ↑ / ↓ | Move between the driver, fields and buttons |
| ← / → | Cycle the driver / focused button (on that row) |
| ^R | Show / hide password |
| ^T | Test the connection (without saving) — reports the visible object count |
| ⏎ | Save — or press the focused button |
| esc | Cancel |
Port/DB fields accept digits only; required fields are validated on save (the error names the field). Everything is also clickable.
Paste a connection URL (postgres://, mysql://, mongodb://, redis://) into the URL row and press ⏎ to fill the whole form — driver, host, port, user, password and database are split out automatically; a name you typed is kept. mongodb+srv and rediss URLs are not supported by the form (use the url option in connections.yml).
For a database behind a bastion, fill the SSH row with user@host[:port] (or a ~/.ssh/config alias) and optionally an SSH key path — see SSH tunnel below.
⚙️ Configuration
All under ~/.config/lazysql/:
| File | Contents |
|------|------|
| connections.yml | Connection config, no passwords, editable by hand |
| secrets.json | Passwords (chmod 600) |
| config.yml | App settings (incl. the NL→SQL llm: block) |
| history.json | Per-connection SQL history (capped at 100 entries each) |
Passwords default to secrets.json; on macOS, set LAZYSQL_SECRETS=keychain to use the system keychain instead (zero native dependencies).
SSH tunnel
Reach a database behind a bastion by adding an ssh: block to a connection (or filling the SSH rows in the in-TUI form). lazysql runs the system ssh for a local port forward, so your ~/.ssh/config, keys and agent all apply — host can simply be a config alias:
connections:
- id: prod
name: prod
driver: postgres
options:
host: db.internal # as seen FROM the bastion
port: 5432
user: app
database: app
ssh:
host: bastion.example.com # or a ~/.ssh/config Host alias
user: ubuntu # optional
port: 22 # optional
keyFile: ~/.ssh/id_ed25519 # optional (-i); agent/config keys work without itApplies to PostgreSQL / MySQL / MongoDB / Redis, with discrete host/port options only — a url/connectionString option can't be tunneled (its embedded host can't be rewritten). Auth is key/agent only: the TUI owns the terminal, so an interactive SSH password prompt can't be answered (BatchMode is forced — a connection that would prompt fails fast with ssh's error instead of hanging).
A tunneled MongoDB connection is pinned with directConnection=true — replica-set member addresses only resolve from the far side of the tunnel. If the SSH link dies, keepalives make ssh give up within ~90s rather than hold a dead forward open; press r to reconnect.
NL→SQL (LLM)
The provider is isolated behind the SqlGenerator port; without an API key, ^G is silently disabled. Pin it in config.yml, or override temporarily with environment variables (env wins). The API key is read only from the environment, never written to config.yml.
config.yml:
llm:
provider: alibaba # anthropic | alibaba | openai | deepseek | moonshot
model: qwen3.7-plus # optional, overrides the default model
# baseUrl: https://... # optional, overrides the default base URL (e.g. an overseas endpoint)| Provider | id | API key env var | Default model |
|----------|----|------------------|----------|
| Alibaba Cloud (Qwen) | alibaba | DASHSCOPE_API_KEY | qwen3.7-plus |
| OpenAI | openai | OPENAI_API_KEY | gpt-4o |
| DeepSeek | deepseek | DEEPSEEK_API_KEY | deepseek-chat |
| Moonshot AI (Kimi) | moonshot | MOONSHOT_API_KEY | kimi-k2.6 |
| Anthropic (Claude) | anthropic | ANTHROPIC_API_KEY | claude-opus-4-8 |
Moonshot defaults to the mainland-China host (api.moonshot.cn); from outside China set baseUrl/LAZYSQL_LLM_BASE_URL to https://api.moonshot.ai/v1.
When no provider is set explicitly, it's auto-detected by "which API key exists" (Qwen first, Anthropic last). Each run can also override with LAZYSQL_LLM_PROVIDER / LAZYSQL_LLM_MODEL / LAZYSQL_LLM_BASE_URL.
export DASHSCOPE_API_KEY=sk-xxx && lazysql # Qwen by default
ANTHROPIC_API_KEY=sk-ant-xxx LAZYSQL_LLM_PROVIDER=anthropic lazysql # temporarily switch to Claude🏗️ Architecture
Tech stack: TypeScript (strict) · Bun · OpenTUI (@opentui/react + React 19) · Zustand · yaml. LLM via @anthropic-ai/sdk + any OpenAI-compatible backend (Qwen / OpenAI / DeepSeek / Kimi).
Clean / Hexagonal layering, built around a capability-segmented DataSource port — the UI asks a data source "which capabilities do you support" (Queryable / Browsable / RowEditable / Transactional / …) rather than "what type are you", so adding a database = adding one adapter, with zero changes to the core. Every layer boundary is crossed via a Result<T,E> + port handshake; inner layers never depend on outer ones.
src/
domain/ pure business rules / entities / value objects (no IO)
application/ ports (outbound interfaces) + usecases (use-case orchestration)
adapters/ datasource · llm · persistence · clipboard (real IO)
presentation/ app · components · keymap · tree (TUI, unidirectional data flow + Zustand)
shared/ cross-layer pure utilities (e.g. Result)The source of truth for the design is docs/ARCHITECTURE.md; key decisions are recorded in docs/adr/ (capability model, TUI framework, NL→SQL provider strategy, key dispatch, etc.).
🛠️ Development
Requires Bun. SQLite uses Bun's built-in
bun:sqlite— no native dependency.
bun install # install dependencies
bun run seed # generate the sample DB data/sample.db
bun start # run from source (the launch-arg forms from Usage work here too, e.g. bun start <name>)Tests
bun run typecheck # strict type check
bun test # unit + five-engine adapter contracts + persistence + headless TUI integrationThe objective judge of "done" is bun run typecheck && bun test all green. Every data-source adapter passes the same contract test suite (the executable acceptance of LSP); contract tests need a reachable real database and skip automatically when unreachable, so a machine without Docker won't fail. To run the full contracts, start the matching containers:
# PostgreSQL
docker run -d --name lazysql-pg -e POSTGRES_PASSWORD=lazysql \
-e POSTGRES_USER=lazysql -e POSTGRES_DB=lazysql -p 5432:5432 postgres:16-alpine
# MySQL / MariaDB
docker run -d --name lazysql-mysql -e MARIADB_ROOT_PASSWORD=lazysql -p 3306:3306 mariadb:11
# MongoDB
docker run -d --name lazysql-mongo -p 27017:27017 mongo:7
# Redis
docker run -d --name lazysql-redis -p 6379:6379 redis:7-alpineContributors: please read CLAUDE.md (working rules: layering, naming, commit conventions) and docs/ARCHITECTURE.md first. Commits follow Conventional Commits (enforced by .githooks + commitlint); on bun install, the prepare script points core.hooksPath at .githooks.
📄 License
MIT © yangwb
