Introduction
A full-featured database client that lives in your terminal: light, fast, and safe by default.
datarig is written in Rust with Ratatui. It keeps several connections open side by side, each with its own tabs, a schema explorer, a SQL editor with vim keys, and a results grid that pages through large results explicitly instead of loading them whole. A result of one table can be edited in the grid: the changes stay pending until a preview of their statements applies them.
It is careful by default: statements that may do harm ask before they run, read-only profiles are checked before anything is sent and, on PostgreSQL, also enforced by the server, and paging a result leaves no transaction, lock or snapshot open on the server unless you ask for it.
Status
datarig is early and pre-1.0. The configuration format, key bindings and behaviour may still change between releases.
- PostgreSQL is implemented. CI tests it against PostgreSQL 17. Not supported before
PostgreSQL 12: a table’s structure, its DDL, the key marks and copying rows as
INSERT(datarig says the server is too old). - MySQL is implemented in part; MySQL says what it supports and what not. MariaDB is a best effort.
Platforms. datarig is developed and tested on macOS. CI runs the full test suite on Linux. On Windows, CI builds datarig, lints it and runs the tests that need no database, but running datarig on Windows is untested: wherever this manual mentions Windows, it describes how datarig is meant to behave there, not something anyone has tried.
Performance
Performance is held by budgets that CI enforces (crates/bench/budgets.toml, explained in docs/perf.md):
- One round trip for the first page of a prepared
SELECT, one for each later page and one for anINSERT, also through an SSH tunnel. - Paging through all 4,000,000 rows of the test table stays under 96 MiB of resident memory, growing less than 16 MiB after the in-memory window is full.
- Keystroke to frame under 25 ms (p95) in a 5 MB SQL file, in a plan of hundreds of nodes and in a chart of 100,000 fetched rows.
- Idle: under 48 MiB and 1% CPU, and fewer than 0.2 wakeups a second when nothing is waiting.
- First frame under 250 ms (p95); the release binary is at most 25 MiB.
Where to go next
- Install datarig, then follow Getting started.
- The user guide explains each part of the workspace, task by task.
- The reference pages list every key,
:command, configuration key and command-line option. They are generated from the code, and the site is published from each release.
Source code, issues and releases are on GitHub: https://github.com/DokaDev/datarig.
Install
Homebrew
On macOS and Linux (arm64 and x86_64):
brew install dokadev/tap/datarig
Or add the tap once and use the short name from then on:
brew tap dokadev/tap
brew install datarig
brew upgrade datarig # later, for a new release
The tap installs stable releases only. datarig is not in homebrew-core.
Release archives
Download the archive for your platform from
GitHub Releases and put the datarig binary on
your PATH. There are archives for macOS and Linux (arm64 and x86_64) and for Windows (x86_64,
untested). SHA256SUMS lists their checksums, for example:
shasum -a 256 -c SHA256SUMS --ignore-missing
Each archive also contains the licences and the third-party notices.
From source
On Linux, the Homebrew tap and the release archives above need no build tools. To build datarig yourself you need:
-
Rust 1.93 or newer, from rustup (the Rust of a Linux distribution is often older). The repository’s
rust-toolchain.tomlpins 1.93.0, and rustup installs it automatically. -
A C compiler, clang and libclang, for the
pg_querycrate: it compiles PostgreSQL’s parser and generates its bindings with bindgen, which loads libclang and needs clang’s own headers. On macOS the Xcode Command Line Tools have all three. On Linux, with git:sudo apt install build-essential clang libclang-dev git # Debian, Ubuntu sudo dnf install gcc clang clang-devel git # Fedora sudo pacman -S --needed base-devel clang git # Arch LinuxOn Windows (untested outside CI), install LLVM and set
LIBCLANG_PATHif bindgen does not find it.
git clone https://github.com/DokaDev/datarig
cd datarig
cargo install --locked --path crates/tui # installs the datarig binary
# or: cargo build --release # the binary is target/release/datarig
CI checks these commands by building datarig with them in clean Debian, Ubuntu, Fedora and Arch Linux containers.
The build stops at pg_query
fatal error: 'stdbool.h' file not found
...
Unable to generate bindings
bindgen found libclang but not the headers that come with clang: clang is not installed, or its version is not the one of libclang. Install clang and libclang together with the command for your distribution above, which installs the same version of both, then build again.
Requirements
- PostgreSQL 12 or newer for a table’s structure, its DDL, the key marks and copying rows as
INSERT; MySQL 8.0 or newer; MariaDB 10.6 or newer (a best effort). See Introduction and MySQL. - A terminal of at least 80×24. A smaller window shows only a note saying how large it must be.
- A Nerd Font is optional. datarig asks once whether your terminal shows its icons (see FAQ).
Versions
Releases follow Semantic Versioning, as 0.MINOR.PATCH until 1.0. A
patch release only fixes things. A minor release before 1.0 may change the configuration
format; datarig keeps reading older files and migrates them itself. For example, a version 1
config file is copied to config.toml.v1.bak before it is upgraded. A release candidate
(vX.Y.0-rc.N), when there is one, is a GitHub pre-release only. The Homebrew tap installs
stable releases.
See What’s new for the changes in each release. When a newer release is out, datarig says so at its next start, with how to update your install (the Homebrew command, the commands for a build from the source, or the download page); see Updates, which also says how to turn the check off.
Getting started
Start datarig
datarig # open the workspace
datarig my-profile # connect to a profile and open a console
datarig --config path/to/config.toml
Without a profile on the command line nothing connects until you pick one. The full list of options is on the command line page.
The welcome panel
With no connection profile yet, the workspace shows a welcome panel with four first steps: add
a connection, run a query, open the action menu, and open the keyboard help. Each is marked
once you have done it, however you did it. X hides the steps for good.
╭ Explorer ────────────╮╭ Welcome ─────────────────────────────────────────────╮
│ + New connection ││ │
│▾ Connections 0 ││ datarig │
│ ││ Dive into any database, right from your terminal. │
│ No connection ││ │
│ profiles yet ││ No connection profiles yet. │
│ Press n to create ││ │
│ one, or paste a ││ First steps │
│ postgres:// or ││ [ ] n Add a connection │
│ mysql:// URL ││ [ ] Ctrl+E Run your first query │
│ ││ [ ] Space Space Open the action menu │
│ ││ [ ] Space ? Open the keyboard help │
│ ││ X Hide these steps │
│ ││ : Commands │
│ ││ q Quit │
│ ││ Paste a connection URL (postgres://, mysql://) │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
╰──────────────────────╯╰──────────────────────────────────────────────────────╯
│ Enter open · Tab next pane · : commands · Space ? help · q quit EN
Add a connection
Press n (or Enter) on the welcome panel, or Space c n anywhere, to open the profile
form. Fill in the host, port, user and database, or paste a connection URL such as
postgres://me@127.0.0.1:5432/app into it. Test connects once and shows the server
version, the latency and how the connection is encrypted. Saving writes the profile to the
config file; the password goes to the password store you chose, never into the file. See
Connections.
Connect and run a query
Ctrl+Oopens quick connect: type part of a profile’s name and pressEnter. A console tab on that connection opens.- Press
ito type (the editor has vim keys;Escgoes back to Normal mode). Ctrl+Eruns the statement under the cursor.Ctrl+Ccancels a running statement.- The result opens below the editor.
n(or]) and[in the grid move to the next and previous page;TabandF6move the focus between the panes.
A statement that may do harm (a DROP, an UPDATE without a real WHERE, …) asks first,
with Cancel focused. See Environments and safety.
Find your way
- Press
Spaceand wait: a popup lists the keys that may follow. F1orSpace ?opens the keyboard help for the current pane, with search.Space Spaceopens the action menu: what you can do with the selected thing, each with its key. A right click opens it too.:(outside text input) orCtrl+K(everywhere) opens the command line. It runs commands and finds every action by name.- The hint line at the bottom, the menus, the tips and the messages all name the same key for
an action: a key of your own when you bound one, else one that needs no function key
(
Space ?rather thanF1), except where that key types text (in insert mode the help isF1).
The workspace
╭ Explorer ────────────────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ───────────────────────────────────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-pg ││ 1 -- datarig 🐘 Ctrl+E: run the statement under the cursor │
│ ▾ datarig default ││ 2▎SELECT * FROM shop.users WHERE id <= 8; │
│ ▸ analytics ││ 3 │
│ ▸ public ││ 4 SELECT u.name, o.status, o.total_amount │
│ ▸ shop ││ 5 FROM shop.orders o │
│ ││ 6 JOIN shop.users u ON u.id = o.user_id │
│ ││ 7 WHERE o.status = 'paid'; │
│ ││ 8 │
│ ││ 9 SELECT * FROM analytics.events; │
│ ││ 10 │
│ ││ 11 SELECT * FROM analytics.events ORDER BY created_at DESC; │
│ ││ 12 │
│ ││ 13 SELECT analytics.slow(30); │
│ ││ 14 │
│ ││ 15 SELECT $$a body; with a semicolon$$ AS dollar, 'it''s; fine' AS quoted; │
│ ││ 16 │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
╰──────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
NORMAL local-pg │ Connected to local-pg │ i type · Ctrl+E run · Tab next pane · : commands · Space ? help · Space more EN
Tabs. Ctrl+T (or :tabnew) opens a new console, Ctrl+W closes the tab, Space 1 ..
Space 9 go to a tab, Ctrl+PageDown / Ctrl+PageUp (or g t / g T) to the next and
previous one.
Space t t(or:tabs,:ls,:buffers) lists the open tabs, the most recently used first with the previous one selected, then the tabs closed in this session. Type to filter by name, kind, connection, database or schema;Entergoes to a tab or brings a closed one back,Ctrl+Dcloses the selected tab.Space t ureopens the last closed tab.- Reorder tabs by dragging one with the mouse, with
Space t gand thenh/l(Enterkeeps,Escputs it back), withSpace t </Space t >, or with:tabmoveas in Vim (:tabmove 0first,:tabmovelast,:tabmove +1). The order is kept across restarts.
Panes. Space z zooms the focused pane (explorer, editor, results or inspector) to the
whole workspace and back, as tmux does (ZOOM in the status bar). Space b hides or shows the
explorer, Space < / Space > or a drag of its border resize it, and a drag of the results’
top border resizes them.
The action menu. A right click on an explorer node, the result grid, a tab or the editor
opens it at the pointer; Space Space, Shift+F10 or the Menu key open it next to the
selection, and Space t m under the active tab. It lists what you can do with the selected
thing, then with the pane, each with its key. An action that would act there but can’t now,
and has a reason to give, is shown dimmed with why in a few words (a read-only environment,
a table tab’s Save, paging that closed); the keys step over it, and a click on it says the
reason in full. Type to filter it;
when nothing in it matches, every action is searched.
The mouse. Besides the menu, the mouse works in the dialogs (the profile and tunnel forms,
the settings, confirmations, the password prompt, pick lists, quick connect, the command line).
A button or a row acts when it is pressed and released on it. The pointer only highlights; it
never moves the focus or the selection, so Enter in a confirmation still keeps. Text inputs
have no mouse selection. Under the pointer, the tab bar’s × turns red, tabs, result tabs,
paging arrows and plan view names light up, and a grid cell and the inspector entry of its
column light each other faintly.
Settings. Space , (or :settings) opens the settings screen; every setting there is also
:set <setting>=<value> (see the commands reference) and a key in the
config file. The UI is in English or Korean:
:set language=ko (or auto for the system locale).
Remapping keys. [keymap.<context>] tables in the config file bind keys to action ids, or
unbind them with "none"; see the keybindings reference.
Tips
By default a dim line about a key shows where the screen is empty anyway: the workspace without
tabs, an empty console, a result with nothing to show or still running, and the status bar’s
spare room. The key named is the one bound now, remapped keys included. The tips of a place
take turns as you move between tabs and panes and run queries, and they keep coming back; a key
you have already pressed comes up less often than the ones you have not (where a place has only
two tips, they simply alternate). [ui] tips (also on
the settings screen, or :set ui.tips=until_used) picks how they behave: "always" (the
default), "until_used" (a tip about a key you have pressed once is not shown again) or
"off". An action you ran three times from the action menu or the command line without its
key gets one status line naming its key, once. Nothing is sent anywhere; see
where datarig keeps its files.
What’s new
On the first start after an update, datarig shows what changed: the notes of every version
since the one you ran before, newest first (if you used a version from before 0.39.0, the notes
of the version you run now). A first start of a new install shows nothing. :whatsnew shows the
notes of every version any time. The notes come with the program, so this works offline and
nothing is fetched; they are the same as the What’s new page.
In the dialog, j/k scroll, Ctrl+D/Ctrl+U half a page, PageDown/PageUp a page,
g/G go to the top or the end, ]/[ to the next or previous version, and Esc closes.
[updates] whats_new = "off" (also on the settings screen, under Updates, or
:set updates.whats_new=off) stops it from showing by itself; :whatsnew still works. When a
newer release is out, the start after datarig learnt it shows a notice; see
Updates.
Quit
:q closes the tab (on the last tab, it quits). :qa or Ctrl+Q quits. When a tab holds an
uncommitted transaction, datarig asks first.
The workspace (tabs, their text, cursors, open folders) is saved and restored at the next start. A second datarig started while the first runs restores no tabs and writes none of the workspace files.
Try it against a test database
The repository’s dev/ directory has Docker Compose services for development. Start its
PostgreSQL (see the
development notes) and paste
postgres://datarig@127.0.0.1:55432/datarig into the new-connection form; the password is
datarig.
Connections
A connection profile says how to reach one database server: driver, host, port, user, database, how to get the password, and optionally TLS, an SSH tunnel and an environment. The explorer’s Connections section lists every profile, in nested folders, each with its own colour and icon.
╭ Explorer ────────────────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ───────────────────────────────────────────────────────────────────────────────────────────────────╮
│▾ Connections 3 ││ local-pg / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-pg ││ 1 -- datarig 🐘 Ctrl+E: run the statement under the cursor │
│ ▾ datarig default ││ 2▎SELECT * FROM shop.users WHERE id <= 8; │
│ ▸ analytics ││ 3 │
│ ▸ public ││ 4 SELECT u.name, o.status, o.total_amount │
│ ▸ shop ││ 5 FROM shop.orders o │
│ ▸ ○ 分析-replica ││ 6 JOIN shop.users u ON u.id = o.user_id │
│ ▸ ○ 分析-replica ││ 7 WHERE o.status = 'paid'; │
│ ││ 8 │
│ ││ 9 SELECT * FROM analytics.events; │
│ ╭ Edit connection: 分析-replica ───────────────────────────────────────────────────────────────────╮ │
│ │ Basic SSH Advanced Ctrl+N/Ctrl+P section │ │
│ │ │ │
│ │ Driver PostgreSQL ▾ ←/→ change · Enter list │ │
│ │ Name 分析-replica │ │
│ │ Host analytics.internal │ │
│ │ Port 6432 │ │
│ │ User report │ │
│ │ Password storage Keychain ▾ ←/→ change · Enter list │ │
│ │ Password kept in the OS keychain │ │
│ │ Database warehouse │ │
│ │ │ │
│ │ DSN postgres://report@analytics.internal:6432/warehouse?sslmode=require │ │
│ │ │ │
│ │ │ │
│ │ Test Save Cancel │ │
│ │ │ │
│ │ │ │
│ ╰──────────────────────── Tab/↑↓ move · Ctrl+N/P section · Ctrl+T test · Ctrl+S save · Esc cancel ─╯ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
╰──────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
NORMAL local-pg │ Connected to local-pg EN
Make and edit profiles
Space c n(ornin the explorer) opens a new profile form;ein the explorer edits the selected profile,cduplicates it,ddeletes it,mmoves it to a folder andNmakes a folder.- A connection URL can be pasted into the form, for example
postgres://me@db.internal:5432/app?sslmode=verify-full. MySQL URLs start withmysql://(ormariadb://). A URL with several hosts is refused. - Test (
tin the explorer,Space c tfor the active tab’s connection) connects once and shows the server version, the latency, how the connection is encrypted and, through a tunnel, the tunnel’s time.
Connect and switch
Ctrl+O(orSpace c c) opens quick connect: a fuzzy list of the profiles.Entergoes to the profile’s most recently used tab, or connects and opens a console when it has none.Righton a profile lists its server’s databases (and on a database its schemas),Leftgoes back;Enterthere opens a console in that database or schema.:conn <profile>does the same from the command line, anddatarig <profile>at start.oon a profile in the explorer opens a new console on it.- Every tab has its own database and schema:
:use db.schemaorSpace c dpicks them.Space c smoves the tab to another connection. Space c xdisconnects the tab’s connection andSpace c rreconnects it.
Several connections can be open at once. Each has its own metadata session, schema tree and
completion catalog, and each tab belongs to one connection. A tab’s number shows its
connection’s colour while connected, is muted while not, and turns into a spinner while a
statement runs. ◆ after a tab’s name marks an open transaction of yours, ! an aborted
transaction or a lost connection.
Passwords
Passwords are never written to the config file. Each profile picks a source with
password_source:
| Source | Where the password is |
|---|---|
keychain (the default) | The OS credential store: macOS Keychain, Linux Secret Service, Windows Credential Manager. |
file | secrets.toml next to the config file, readable by you only (mode 0600). |
command | The standard output of password_command, for example a password manager’s CLI. |
env | The environment variable named by password_env, read at connect time. |
prompt | Asked on every connect, never stored. |
- Every keychain call runs off the UI thread with a 10-second limit, so a locked or slow keychain cannot freeze the app. When the keychain does not work, datarig asks for the password when connecting and keeps it in memory only.
- A password command is split into words like a shell would, but runs without a shell:
no pipes, variables or
~. Writesh -c '…'when you need them. It gets 10 seconds, and one trailing newline is removed from its output. [secrets] default_source(or:set secrets.default_source=prompt) picks the source new profiles start with;auto, the default, is the keychain when it works.- A plaintext
passwordfound in an older config file is moved to the profile’s store at launch and removed from the file. DATARIG_SECRET_STORE=memorykeeps every password in memory only, for this run; it replaces both the keychain and the secrets file.
macOS: keychain access through security
datarig’s release binaries are not signed. macOS ties the “Always Allow” you give a keychain item to the program that asked, so after every upgrade it sees a new program and asks for your login password again for each saved password.
To avoid that, datarig can reach its keychain items through Apple’s own /usr/bin/security
tool, which is signed by Apple and does not change when datarig is upgraded:
[secrets]
macos_keychain_access = "security_tool" # direct (the default) | security_tool
The same setting is on the settings screen (Space ,, Secrets group) and is
:set secrets.macos_keychain_access=security_tool. It applies at once; turning it on asks
first.
- What to expect. The first time each existing password is read or changed through
security, macOS may ask once whethersecuritymay use it: enter your login password and choose Always Allow. After that, upgrades no longer ask. - The trade-off. Any program running as your user can then read these passwords with
security(for examplesecurity find-generic-password -s datarig -g) without being asked. Withdirect, macOS asks before another program reads them. - Limits. In this mode a password containing a line break, or longer than about 3.5 KB, cannot be saved (datarig says so and saves nothing). Passwords saved earlier still read.
- The items are the same in both modes (generic passwords, service
datarig, accountprofile:<id>, in your default keychain), so switching moves nothing. Passwords are handed to the tool on its standard input, never on its command line. - Switching back to
directkeeps every password working; items saved whilesecurity_toolwas on trust onlysecurity, so macOS may ask once per item. - The setting is accepted on every platform and has no effect outside macOS.
The one-time offer. In direct mode on macOS, when a keychain call most likely waited for
you to approve it after an upgrade, datarig asks once whether to switch to security_tool,
after the operation that used the keychain has finished. y switches, n keeps direct for
good, Esc or Enter keep direct and decide later.
- A call counts as such a prompt when it took at least one second (or hit the 10-second limit) and the binary is not the one that last read a saved password directly. A slow keychain with an unchanged binary does not count.
- If you answer the macOS dialog in under a second, the prompt is missed and the offer comes at the next one.
- It is never offered outside macOS, over SSH (
SSH_CONNECTION,SSH_CLIENTorSSH_TTYset), insecurity_toolmode, withDATARIG_SECRET_STORE=memory, or once answered. - The answer is kept in
keychain.tomlin the state directory, not in the config file. Deleting that file makes datarig forget it.
A signed release would be the proper fix; until releases are signed, this setting is the workaround.
The config file
The config file is --config <path>, else $XDG_CONFIG_HOME/datarig/config.toml, else
~/.config/datarig/config.toml, on every OS. The app creates and edits it (comments and key
order are kept), and you can write it by hand. Every key is listed on the
configuration reference page. A profile looks like this:
[[connections]]
name = "prod-replica"
driver = "postgres" # postgres | mysql
host = "db.internal"
port = 5432
user = "readonly"
database = "app"
color = "red"
folder = "work/prod" # nested folders are written with /
policy = "prod" # its environment (see Environments and safety)
password_source = "command"
password_command = "my-password-tool get prod-db" # your password manager's command line
tunnel = "office" # a tunnel preset (see SSH tunnels)
sslmode = "verify-full" # see TLS
Related pages: SSH tunnels, TLS, Poolers, Environments and safety, Startup SQL.
SSH tunnels
A profile can reach its database through one SSH bastion. datarig opens the tunnel itself (in
Rust, without the ssh program) and dials every connection of the profile through
direct-tcpip channels: each tab’s session, the explorer’s metadata session, cancel requests
and test connections. Nothing else is requested from the SSH server, and no local port is
opened.
Pick a tunnel in the profile form
The form’s SSH section has three choices: off, a tunnel preset, or this profile
only (its own bastion fields). Enter on the value lists them with each preset’s bastion,
filtered as you type. Its last entry, New SSH tunnel…, opens the tunnel form and comes back
with the new preset picked. In a key file field, Ctrl+O picks the file in a tree.
Authentication
auth | What it uses |
|---|---|
key (the default) | A private key file: OpenSSH format (ed25519, ECDSA, RSA) or PEM (PKCS#1, PKCS#8, EC, including AWS .pem files), encrypted or not. A certificate next to the key (<file>-cert.pub) is used too. |
password | The bastion account’s password. |
agent | Your running ssh-agent. |
keyboard-interactive | The server’s questions, such as one-time codes, asked in a dialog. |
- As with OpenSSH, a key file that others may read is refused on Unix:
chmod 600it. PuTTY keys (.ppk) are named as such; convert them withputtygen. - The key’s passphrase or the SSH password comes from the same sources as database passwords
(
secret_source, default the keychain; see Connections). It is stored apart from the database password.
Host keys
Host keys are checked against ~/.ssh/known_hosts (read only, never written: hashed names,
* and ? wildcards, negations, [name]:port and @revoked lines are understood) and
against datarig’s own known_hosts in its data directory, the only one datarig writes. An
unknown or changed key is always asked about, never accepted silently.
Tunnel presets
A tunnel preset is a named tunnel that profiles share, written as [tunnels.<name>]; a profile
names it with tunnel = "<name>".
[tunnels.office]
host = "bastion.example.com"
port = 22
user = "ec2-user"
auth = "key"
key_file = "~/.ssh/prod.pem"
keepalive = 15 # seconds between keepalives (the default); 0: none
timeout = 10 # seconds each step of opening the tunnel may take (the default)
[[connections]]
name = "prod"
tunnel = "office"
# ...
[[connections]]
name = "lab"
# ...
[connections.ssh] # a tunnel of this profile only
enabled = true
host = "lab-bastion.example.com"
user = "me"
auth = "agent"
- Presets live in the explorer’s Tunnels section, below the connections:
nmakes one,eedits it (renaming takes the profiles that name it along),ccopies it,ddeletes it,ttests it, andEnterlists the profiles connected through it now. The command line also finds the actiontunnel.new. - Each preset shows whether its SSH connection is closed, opening, open (with how many
profiles are on it) or lost (and why) by the colour of its icon (
○,●and✕with icons off). - Profiles that use the same preset at the same time share one SSH connection: one login, one host key question and one secret prompt for every profile waiting. It closes when the last of them disconnects. When it is lost, every profile on it says so and connects again on its next use.
- A preset’s secret is kept once, under the preset’s own account, which a rename keeps. Deleting a preset asks first, lists the profiles that use it and removes its saved secret.
- A preset changed while profiles are connected through it: they keep their connection until they connect again, and then use the new settings.
- Testing a preset opens its SSH connection and then one
direct-tcpipchannel to each database address of the profiles that use it, closing each again. Nothing is sent to a database. - Save as tunnel preset (
Ctrl+Bin the form, on the profile’s own tunnel) turns the profile’s own tunnel into a preset when the form is saved. The stored secret is copied to the preset and read back before the old copy is removed.
When something is wrong
- A profile whose preset does not exist, or that names a preset and also has its own tunnel turned on, does not connect and says why. It never connects directly instead.
- Keepalives detect a dead tunnel; the explorer marks the profile until its next use reconnects.
- The app never rewrites tunnels on its own: presets change only when you save a form, rename or delete one.
Not supported
- Several hops (a bastion behind a bastion).
- Reading hosts from
~/.ssh/config: the bastion is always entered in the profile or preset.
TLS
PostgreSQL and MySQL connections are encrypted with TLS 1.2 or 1.3 (rustls with ring) as
the profile’s SSL mode says, in each database’s own words. The profile form’s Advanced
section has the mode, the CA file, the client certificate and its key. In the
config file they are sslmode, sslrootcert, sslcert and sslkey, and a PostgreSQL URL
takes the same four as parameters:
postgres://me@db/app?sslmode=verify-full&sslrootcert=...
A MySQL URL takes no TLS parameters; use the form’s fields.
Modes
| PostgreSQL | MySQL | Encrypted | What is checked of the server’s certificate |
|---|---|---|---|
disable | DISABLED | never | nothing |
prefer | PREFERRED | when the server offers TLS | nothing |
require | REQUIRED | always | nothing; with a CA file, as verify-ca |
verify-ca | VERIFY_CA | always | its chain, against the CA file (needed) |
verify-full | VERIFY_IDENTITY | always | its chain, and that it is for the profile’s host |
PostgreSQL’s allow is not supported. MySQL’s mode names are read in any case.
prefer/PREFERREDgoes on unencrypted only when the server says it has no TLS. A server that offers TLS and then fails the handshake is an error, never a plain connection.- The CA file is the only trust. With one, only its certificates are trusted (a server’s own
self-signed certificate copied to your machine works too). Without one,
verify-full/VERIFY_IDENTITYchecks the certificate against the certificates your system trusts: the macOS trust settings, the Windows certificate store, the system’s certificate files on Linux.sslrootcert = "system"names those explicitly and makes the modeverify-fullwhatever the profile’s mode says (unless TLS is off). - The CA, certificate and key files are absolute paths or start with
~/; a relative path is an error of the profile. - The name checked is the profile’s host, also through an SSH tunnel (never
127.0.0.1). An IP address is checked against the certificate’s IP addresses. - The client certificate and key are PEM files (PKCS#8, RSA or EC keys). A key protected by a passphrase is refused, with how to make an unprotected copy.
- PostgreSQL’s SCRAM login binds itself to the TLS connection (
SCRAM-SHA-256-PLUS) where the server offers it, so a server in between that passes the login on is found out. One that answers itself can still ask for the password (cleartext or MD5) unless the certificate is checked: useverify-full, orchannel_binding=requirein the URL. - Cancel requests (PostgreSQL) and the
KILL QUERYconnections (MySQL) are encrypted as the session is.
Defaults
- A PostgreSQL profile without
sslmodeconnects withdisable, as it always has; the form writespreferfor a new profile. - A MySQL profile without a mode uses
PREFERRED, except one that names a server public key file, which usesDISABLED(see MySQL). In a MySQL profile,disableandprefer(what older versions of the form wrote) count as no mode: writeDISABLEDto turn TLS off. - A mode the driver does not know is an error of the profile.
What a connection says
Each connection says what it got: Connected to prod · TLS 1.3, server verified (or
“certificate not checked”, “CA checked”), a shield and the TLS version in the editor’s
connection line, and the same in the form’s test. A connection to another machine without TLS,
not through a tunnel, says it is not encrypted.
Failures say which: the server has no TLS; the certificate is not trusted, is for another name, has expired or is not valid yet, or is not meant for a server; or the server refused the client certificate. A file that cannot be read is named.
Tested, and not
Tested: every mode against PostgreSQL 17 and MySQL 8.4 with certificates made for the tests
(trusted, another CA’s, another name; client certificates accepted and refused on PostgreSQL),
through a dialer as a tunnel uses one, and PostgreSQL verify-full through a real SSH bastion;
in-process tests for expired and not-yet-valid certificates, other purposes, a self-signed
certificate as its own CA file, and TLS 1.2.
Not tested end to end: a server the system’s certificates trust (only their refusal of an
unknown CA is tested), sslrootcert = "system", real company servers, PgBouncer with TLS
between datarig and the pooler, and Windows.
Limits
- Not supported: passphrase-protected client keys, certificate revocation lists, and
sslnegotiation=direct(refused). - Only TLS 1.2 and 1.3 are offered: no TLS 1.0 or 1.1.
- On macOS,
verify-fullagainst the system’s certificates follows Apple’s rules, which refuse server certificates valid for more than 825 days from a CA you added yourself: name that CA as the profile’s CA file instead.
Poolers
datarig works through PgBouncer and other PostgreSQL connection poolers, including transaction pooling, where consecutive transactions of one client may run on different server connections.
How a pooler is detected
When a query session connects, datarig checks in the same round trip whether the server process answering is the one the connection started with. If it is not, the connection is treated as pooled for the rest of the session. MySQL connections are never treated as pooled; a proxy that multiplexes them (such as ProxySQL) is not detected.
Prepared statements
-
Nothing outside the query session relies on prepared statements, and statement names are unique per process.
-
The query session keeps prepared statements between transactions (the server-side statement cache). Turn it off for a pooler in transaction mode that does not support them (PgBouncer before 1.22, or one with
max_prepared_statements = 0):[[connections]] name = "app-via-pgbouncer" statement_cache = false -
When the server keeps losing prepared statements, the session turns the cache off by itself and says so.
What changes behind a pooler
Settings that would outlive a transaction would stay on a server connection other clients share, so behind a pooler datarig keeps them inside each transaction:
lock_waitis sent asSET LOCAL lock_timeoutin each transaction instead of once for the session (see Environments and safety).idle_disconnectgets no server-sideidle_session_timeoutbehind a pooler; datarig’s own timer still closes the tab’s session. A Startup SQL’sidle_session_timeoutis not sent there either, so the environment’sidle_disconnectstays the limit.- Startup SQL statements go first in every transaction as
SET LOCAL; statements that have no such form are not sent, and the tab names them (see Startup SQL). - Statements that cannot run in a transaction (
VACUUM,CREATE INDEX CONCURRENTLY, …),CALL,DO, and your ownSETandRESETrun without these per-transaction settings.
In datarig, SHOW lock_timeout and current_setting('lock_timeout') show the limit in effect.
Another client of the same pooled server connection sees the server’s value, because nothing
stays on it.
Manual commit and pooling
- In transaction pooling, from the first write to the commit one server connection is yours, and the open transaction holds that pool slot for as long as it stays open.
- Statement pooling (PgBouncer’s
pool_mode = statement) refuses transactions of several statements, so manual commit does not work through it. - The server-side limit of
tx_idle_rollbackis set withSET LOCAL, so it ends with the transaction and nothing stays on the pooled connection.
Environments and safety
What datarig checks
- What asks. Every statement is parsed before it is sent, with PostgreSQL’s own parser
(libpg_query) for PostgreSQL and datarig’s MySQL classifier for MySQL, never with a regular
expression. These open a confirmation with Cancel focused:
DROP,TRUNCATE, anUPDATEorDELETEwithout a realWHERE,ALTER TABLE … DROP COLUMN,ALTER … TYPE,MERGEthat updates or deletes,DOandCALL,COPYto or from server files or programs, server-side built-ins that act on files or other backends, and any text the parser rejects. An environment withconfirm = "writes"asks before every write too. - What is refused. A read-only profile refuses anything that is not a read, transaction
control or a safe session setting before sending it, and the server refuses the rest:
every transaction of a read-only profile is opened
READ ONLY, so even a function called from aSELECTcannot write. (MySQL works differently; see MySQL.) - Editing data. A read-only environment refuses editing a result (“Read-only:
the connection’s environment is read-only”), and so does a tab whose session opened while it
was. With
confirm = "writes"an Apply shows its statements in the Apply dialog beforeyapplies them (elsewhere it asks on the status line); that dialog is the confirmation, no second one opens. - What it is not. The confirmation is a guardrail against mistakes in what the text shows. Functions called from a query, triggers, views and rules are not inspected; a read-only environment is the guarantee.
- Paging leaves nothing open by default: datarig keeps no transaction, lock or snapshot open while you read a result, unless your environment asks for it (see Paging and locks).
EXPLAIN ANALYZEof a write is rolled back (see Plans).
╭ Explorer ────────────────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ───────────────────────────────────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 · careful Space c s switch connection │
│ ▾ ● local-pg ││ 1 T 1; │
│ ▾ datarig default ││ 2 E FROM shop.orders; │
│ Loading… ││ 3 E shop.users SET name = 'x' WHERE true; │
│ Loading… ││ 4 TABLE IF EXISTS shop.reviews CASCADE; │
│▾ Environments 2 ││ 5▎T INTO shop.audit_log (note) VALUES ('中文 備註'); │
│ ○ Default rules (no environment) ││ │
│ ○ prod read-only ││ │
│ ▸ ○ careful asks before every write││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ╭ Run 4 statements that may do harm? ──────────────────────────────────────────────────────────────────────────────────╮ │
│ │ ● local-pg · careful │ │
│ │ │ │
│ │ 2/5 deletes every row · no WHERE target: shop.orders │ │
│ │ DELETE FROM shop.orders │ │
│ │ 3/5 updates every row · its WHERE is always true target: shop.users │ │
│ │ UPDATE shop.users SET name = 'x' WHERE true │ │
│ │ 4/5 drops it target: shop.reviews │ │
│ │ DROP TABLE IF EXISTS shop.reviews CASCADE │ │
│ │ 5/5 writes target: shop.audit_log │ │
│ │ INSERT INTO shop.audit_log (note) VALUES ('中文 備註') │ │
│ │ │ │
│ │ [ Cancel ] Run │ │
│ ╰────────────────────────────────────────────────────────────────── y run · Enter chooses · Tab switch · n/Esc cancel ─╯ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
╰──────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
NORMAL local-pg · careful │ Connected to local-pg EN
Environments
An environment is a named set of rules that profiles share: read-only or not, which
statements ask first, whether you commit by hand (Manual commit), how
results page, the spill limit, idle and lock limits, a Startup SQL per
database, and an optional colour and badge (such as PROD). The badge shows on the profile’s
tabs, in the explorer and in the status bar, and the profile’s name takes the environment’s
colour in the explorer, the tab list, quick connect and the tab bar.
Pick one in a profile’s form (Advanced section, Enter on Environment). The list has “None
(default rules)”, every environment with its colour, badge and rules, and “New environment…”.
The Environments section
The explorer’s Environments section starts with Default rules (no environment): the
rules of every profile without an environment. e edits them and Enter lists those
profiles; they cannot be renamed, copied or deleted, and have no colour or badge.
Below it each environment shows its colour, its badge and its rules in a few words: read-only in
blue, “asks before every write” and “manual commit” in the warning colour, “holds locks while
paging” in the error colour. n makes one, e edits, c copies, R renames, d deletes,
and Enter or l lists the profiles that use it. A rename takes those profiles along;
deleting one that profiles use asks first and names them. The command line also finds the
action env.new.
While there is no environment, the section offers Add presets: dev, test, staging, prod, and the profile form’s list offers the same four:
| Preset | Rules | Colour | Badge |
|---|---|---|---|
dev | the defaults | green | |
test | the defaults | blue | |
staging | every write asks, manual commit | orange | STG |
prod | read-only, every write asks, manual commit | red | PROD |
Nothing is written on its own: presets are added only when you ask, and once written they are ordinary environments. No behaviour depends on an environment’s name; a preset only gives the first values of its rules, colour and badge.
In the config file
An environment is a [policy.<env>] table, every key optional:
[policy.prod]
read_only = true
confirm = "writes"
commit = "manual"
color = "red"
badge = "PROD"
lock_wait = "5s"
[policy.local]
paging = "hold"
paging_idle_timeout = "10s"
[[connections]]
name = "prod-replica"
policy = "prod"
# ...
| Key | Values | Default |
|---|---|---|
read_only | true, false | false |
confirm | "destructive", "writes" | "destructive" |
commit | "auto", "manual" | "auto" |
tx_idle_warning | a duration of at least 10 s, or "off" | "5m" |
tx_idle_rollback | a duration of at least 10 s, or "off" | "30m" |
paging | "no_hold", "hold" | "no_hold" |
paging_idle_timeout | seconds, or "90s", "5m", "1h", "off" | "30s" |
spill_limit | bytes, or "512MB", "2GB", "off" | the top-level spill_limit |
idle_disconnect | "1m" to "24h", or "off" | the top-level idle_disconnect |
lock_wait | "1s" to "24h", or "off" | the top-level lock_wait |
startup | { postgres = "<name>", mysql = "<name>" }, either left out | none |
color | red, orange, yellow, green, teal, cyan, blue, indigo, purple, pink, brown, gray, or #rrggbb | none |
badge | up to 8 columns, no blanks around it | none |
The idle-transaction keys are explained on Manual commit, and
startup on Startup SQL. Older names are still read: tx_idle_timeout for
tx_idle_rollback, and session for startup; a table with both names is an error.
- A wrong
colororbadgeis reported when the file is read, like any other value. - Saving from the app changes only what you changed. An untouched table, and an untouched value in an edited one, keep their text and comments; a value set back to its default is removed; a renamed table keeps its comments and its place. Environments written as dotted keys or as an inline table stay that way.
[policy.default]holds the default rules. It is not an environment: it is the section’s first row, and the app writes it only when you save a change in its form. Acolororbadgein it is not shown anywhere. Without it the built-in defaults apply: writable, dangerous statements ask, automatic commit, no held pages.
A profile naming an environment that does not exist is an error of that profile, not a fallback to the default rules. Its explorer line shows the environment mark in the error colour, connecting is refused with a notice that names the missing environment, and a connected profile whose environment was deleted or renamed away runs nothing until another one is picked.
Paging and locks
A result with more rows than a page (page_size, 500 rows by default) can be paged two ways,
chosen per environment with paging. Inside your own BEGIN … COMMIT block both behave the
same: the result’s cursor lives in your transaction and ends with it.
paging = "no_hold" (default) | paging = "hold" | |
|---|---|---|
| After the first page | The cursor and its transaction end in the same round trip | The cursor stays open in a transaction of its own |
| Held on the server while you read | Nothing: no transaction, no lock, no snapshot | The transaction, its snapshot, and an ACCESS SHARE lock on every table the statement read |
Next page (n) | Runs the statement again and skips the rows you have, only for a plain SELECT that is safe to repeat (said each time; the rows may differ if the data changed) | Read from the open cursor |
| A statement not safe to repeat (a write, a volatile function, a view, …) | You see the first page, and why; page it yourself with LIMIT/OFFSET, or use "hold" | Read from the open cursor |
| Held until | Nothing is held | The result is read to its end, another statement runs in the tab, or paging_idle_timeout passes without a page |
Why nothing is held by default: an ACCESS SHARE lock held by an idle result makes a deploy’s
ALTER TABLE wait, and every query after the ALTER TABLE waits behind it. A held
transaction also keeps vacuum from cleaning up. Use "hold" for a local database, or when you
walk a large result page by page (without it each page runs the statement again and skips the
rows before it, which costs more the deeper you go). A row count (#) never keeps anything
open. MySQL pages its own way; see MySQL.
Rows already fetched stay in a memory window (result_window_rows, 10,000 rows by default) and
spill to a private temporary file past it, up to spill_limit (1 GB by default; an environment
may set its own).
Idle connections and lock waits
Two settings say how long a connection may wait, the same for every database (datarig turns
them into each server’s own settings). Both are off unless you set them: at the top level of
the config, on the settings screen (Connections group), with :set idle_disconnect=5m or
:set lock_wait=5s (any duration in range, or off), and per environment (its form’s Idle
disconnect and Lock wait fields; empty takes the config’s value, "off" turns it off there).
| Key | Values | What it does |
|---|---|---|
idle_disconnect | "1m" to "24h", or "off" | Closes a connection nothing used for that long |
lock_wait | "1s" to "24h", or "off" | A statement waits at most that long for a lock another session holds |
Idle disconnect
- A tab’s session closes once nothing ran in it (a run, a page, a commit) for that long, and the tab says so. Once the whole connection sat unused that long (the explorer’s reads count too), its metadata sessions and its SSH tunnel close as well (a shared tunnel once no other connection uses it), and the explorer shows it.
- The next run or explorer action connects again. A password that would have to be asked for (the prompt, the keychain) is reused from the first connect; a password command, variable or secrets file is read again.
- A tab with an open transaction is warned a minute before (half the limit under two minutes), in the status bar and its Messages. When the session closes, the server rolls the transaction back, and datarig says so.
- Time asleep counts: a session left overnight closes at the first key, answer or timer after the machine wakes.
- The server gets its own limit for each query session, a minute longer, for when datarig
cannot close the session itself (it was killed, the machine slept): PostgreSQL
idle_session_timeout(version 14 or later, only on a direct connection; behind a pooler it would end server connections other clients share), MySQLwait_timeout. A session the server ended that way is reported as closed for being idle, not as a lost connection. - A Startup SQL or a profile’s own
startup_sqlmay set that server limit itself (PostgreSQLSET idle_session_timeout = '10min', MySQLSET SESSION wait_timeout = 600): its value is then those sessions’ idle disconnect, and:sessionshows it with what it overrides. - A change of the setting applies to sessions opened after it; the app’s own timer follows it at once.
Lock waits
- For the query sessions only; the explorer’s lookups keep their own two seconds.
- PostgreSQL:
lock_timeout, set for the session on a direct connection. Behind a pooler it isSET LOCALin each transaction instead. On a direct connection, aRESET ALLorDISCARD ALLyou run clears it (back to the server’s default) until the session connects again; behind a pooler the next transaction gets it again. - MySQL:
lock_wait_timeout(metadata locks) andinnodb_lock_wait_timeout(row locks), in whole seconds. - A statement that waited too long fails with “Waited longer than 5s for a lock another session holds, so the server cancelled the statement”, followed by the server’s own error.
- It does not change how commits work: with automatic commit only that statement failed; in a
manual transaction on PostgreSQL only that run is undone (its savepoint) and the transaction
keeps what earlier runs did; on MySQL the server undoes the statement only (unless
innodb_rollback_on_timeoutis on: then the whole transaction, and datarig says so). - A Startup SQL or a profile’s own
startup_sqlmay set the lock wait itself (PostgreSQLSET lock_timeout = '3s'; MySQLSET SESSION lock_wait_timeout = 3, innodb_lock_wait_timeout = 3). It runs after datarig’s own limit, so it overrides the environment’s for those sessions, and the error names its value. PostgreSQL’s0means no limit. When datarig cannot read the value (DEFAULT, an expression, a unit it does not read; on MySQL the two settings at different values), the error says the statement waited too long, without a time. :sessionshows the lock wait that applies and what it overrides.
Idle transactions
With manual commit, an environment also says when an idle open transaction is warned about
(tx_idle_warning) and rolled back (tx_idle_rollback). See
Manual commit.
Manual commit
With commit = "manual" in an environment, what you change waits for you:
datarig opens a transaction right before the first statement that may write, and it stays open
until you commit or roll it back. datarig never sends a COMMIT on its own; on MySQL,
statements that commit by themselves are covered below. The default,
commit = "auto", commits each statement that runs outside your own BEGIN block by itself.
[policy.staging]
commit = "manual"
tx_idle_warning = "5m"
tx_idle_rollback = "30m"
The staging and prod presets commit by hand in a new setup. Environments you already have
(and any without the key) stay automatic until you change them.
When the transaction opens
- Reads before the first write keep nothing open. A plain read (a
SELECTthe paging allowlist takes,SHOW, …) runs as it does with automatic commit. - Anything not known to be a plain read opens the transaction: writes, DDL (PostgreSQL),
SELECT … FOR UPDATE, aSELECTthat calls a function of yours or a volatile one,CALL,DO, anEXECUTEof a prepared write, text the parser rejects. Session settings, transaction control andEXPLAIN ANALYZE(rolled back anyway) never open it. - Once open, every statement of the tab runs in it, reads too: in
SELECT …; UPDATE …; SELECT …the transaction opens before theUPDATE, and the lastSELECTsees the update. Results read in it are labelled uncommitted. - Your own
BEGINstill works as before, with the server’s rules. - With
read_only = trueno transaction ever opens, because every write is refused first.
PostgreSQL. DDL is transactional, so CREATE, ALTER and DROP roll back with the rest.
Until then they hold their locks, and Messages says so: the lock mode for the common cases
(ALTER TABLE … ADD COLUMN, DROP TABLE and TRUNCATE hold an ACCESS EXCLUSIVE lock, so
other sessions’ reads of the table wait; CREATE INDEX a SHARE lock, so their writes wait).
Opening the transaction goes out with the statement, without an extra round trip.
MySQL. DDL (and LOCK TABLES, START TRANSACTION, …) commits the open transaction by
itself and cannot be rolled back. It never opens a transaction. With changes pending it asks
first, saying what it would commit (“first commits the open transaction: 3 statements (120
rows)”; Enter leaves it unrun, y runs it). START TRANSACTION costs one more round trip, on
the first write of each transaction only.
Commit, roll back, look
| Keys | Command | What it does |
|---|---|---|
Space x c | :commit | Commit |
Space x r | :rollback (:rb) | Roll back |
Space x s | :tx | What the open transaction did: its statements with their rows and times, how long it has been open and idle; c commits, r rolls back, Esc closes |
They work for your own BEGIN too, and are in the action menu. They are refused while a
statement runs in the tab, and a transaction that failed can only be rolled back.
While a transaction is open the tab shows ◆, the explorer shows ◆ after its profile, and the
status bar shows something like TX open · 3 changes · 120 rows · 4:12: the statements that may
have written, the rows they changed and the time since it opened (with idle 1:30 once it has
sat idle a minute).
╭ Explorer ────────────────────────────╮ 1 console 1 ◆ ×
│ + New connection │╭ Query · console 1 ───────────────────────────────────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-pg ◆ ││ 1▎SELECT x FROM t ✓ 3 rows · 3ms · 14:03 │
│ ▾ datarig default ││ │
│ ▸ analytics ││ │
│ ▸ public ││ │
│ ▸ shop ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ │╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
│ │╭ Results · 3 rows ────────────────────────────────────────────────────╮╭ Cell [Row] · I ──────────────────────────────╮
│ ││ [Result 1] Chart Messages uncommitted (in tx) ││ x: 0 │
│ ││ │ x ││ │
│ ││ │ int4 ││ │
│ ││ 1 │ 0 ││ │
│ ││ 2 │ 1 ││ │
│ ││ 3 │ 2 ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
╰──────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────╯
NORMAL local-pg │ TX open · 0 changes · 0 rows · 0:00 │ 3 rows · 3ms │ i type · Ctrl+E run · Tab next pane · : commands · Space ? help · Space more EN
Settings
Per environment, in its form (Commit, Idle warning, Idle rollback) or in [policy.<env>]:
| Key | Values | Default |
|---|---|---|
commit | "auto", "manual" | "auto" |
tx_idle_warning | a duration ("90s", "5m", "1h"; at least 10 s) or "off" | "5m" |
tx_idle_rollback | a duration (at least 10 s) or "off" | "30m" |
tx_idle_warning: once an open transaction has sat idle that long, datarig warns once (again only after it ran something and sat idle again), its◆turns red in the tab bar and the explorer, and the status bar addsidle 5:00 · holding locksin the error colour. It never ends anything.tx_idle_rollback: a transaction opened for manual commit that sat idle that long is rolled back, and the connection stays. datarig sendsROLLBACKon the tab’s session, which releases its locks, and once the server confirms it says how many changes were undone. A minute before (half the limit under two minutes) datarig warns, unlesstx_idle_warningalready did. The limit is the one the transaction began with. Time asleep counts once the machine wakes. With a shorteridle_disconnect, that closes the session first. Your ownBEGINis not covered.- If the
ROLLBACKfails or gets no answer in time (10 s; 60 s on MySQL, whose rollback can take about as long as the writes did), datarig closes the tab’s session instead and says so; the server rolls back once it sees the session end. - PostgreSQL also gets a server-side limit 30 s later than datarig’s
(
SET LOCAL idle_in_transaction_session_timeoutwith theBEGIN, so"30m"sets 1830 s). It ends with the transaction, so nothing stays on a pooled connection, and acts only when datarig could not: the laptop asleep, datarig killed or stuck, the tunnel gone."off"leaves the server’s setting. - MySQL has no server-side limit for a transaction, so there the rule holds only while datarig runs and the machine is awake.
- If the
- The rule is read at each run. Switched to
"auto"while a transaction is open, that transaction stays until you end it, and datarig says so.
When a run fails
- PostgreSQL: each run in the open transaction goes under a savepoint of its own (no extra
round trip). A run that fails or is cancelled is undone, and the transaction keeps what
earlier runs did; it does not become aborted. Your own
BEGINblock has no such savepoint: a failure aborts it, as PostgreSQL does. - MySQL: the server undoes only the statement that failed. A deadlock (or a lock wait
timeout with
innodb_rollback_on_timeout) rolls back the whole transaction, and datarig says how many changes are gone. - A run that opened the transaction and then fails rolls it back at once: nothing of it is kept.
Statements that cannot run in a transaction (PostgreSQL VACUUM,
CREATE INDEX CONCURRENTLY, CREATE DATABASE, ALTER SYSTEM, …) are refused while a
transaction is open (“Commit (c) or roll back (r) first”). With manual commit and none open they ask
first: they run at once and cannot be rolled back. A procedure or DO block that ends
transactions itself fails inside the transaction; when it was the first write, datarig rolls
back and asks whether to run it once outside one.
How a commit ends: committed, with the count, rows and time in Messages; or refused by the server (a deferred constraint, a serialisation failure), which then rolls back, with its error. A PostgreSQL transaction that had failed is reported as rolled back, never as committed. When the connection ends while a commit is on its way, whether it happened is not known, and datarig says so: check the data.
Editing data
An Apply of edited data follows the same rules. With manual commit and no
transaction open, it opens one and leaves it open (“Opens a transaction; nothing is
committed.”). Inside an open transaction, yours or one opened for manual commit, it runs under
the savepoint datarig_apply and commits nothing (“Runs inside the open transaction; nothing is
committed.”).
- The status line question says so: “Apply 1 change to shop.users (1 update)? Opens a transaction; nothing is committed. y apply · any other key cancels”.
- The status bar counts the Apply’s writes in the open transaction, and
:txlists it as one entry, “Apply (3 changes)”. - Until the transaction ends the result shows
applied · not committed. A commit keeps the values shown (applied · re-run to refresh). - A rollback (yours, the idle rollback, the server’s, a lost connection) drops them: “The
transaction was rolled back; the applied changes are gone”. A transaction that ended in a way
the app cannot tell (a
PREPARE TRANSACTION, a procedure) drops them too. - A
SAVEPOINT,RELEASEorROLLBACK TOyou run drops them as well (“A savepoint statement ran; re-run the query to see the table”).
Closing, disconnecting, quitting
Closing a tab, disconnecting, quitting, switching a tab’s connection or database with :use,
or deleting the profile with a transaction open asks first, with what it holds:
ccommits first,rrolls back first,ygoes on anyway (the server rolls back),Enterornstays. A failed transaction offers onlyrandy.- After
corrit goes on only once the server confirmed; one that fails stops there. - Quitting with several tabs ends them one at a time and stops at the first that fails.
A signal (SIGTERM, SIGHUP, the terminal gone), a crash or a kill cannot ask: the connection closes and the server rolls back. No transaction comes back with a restored workspace.
Limits
- MySQL cannot run a plain read outside a transaction read-only without an extra round trip, so it does not: a write hidden behind a read (a view that calls a stored function that writes) commits by itself. PostgreSQL runs those reads read-only, so such a write fails.
- MySQL:
LOCK TABLESis refused with manual commit. ACALLof a procedure that runs DDL commits the transaction without asking (datarig says so afterwards). ASET autocommit = 0of yours shows “autocommit off (session)”. - Poolers: see Poolers.
- SSH tunnels: when the tunnel is lost, the link from the bastion to the database may stay
open, and the server may hold the transaction and its locks until it notices. On PostgreSQL
the server-side part of
tx_idle_rollbackis the safety net; on MySQL there is none. - PostgreSQL: each run that writes under its savepoint is a subtransaction, kept until the transaction ends. Past about 64 in one transaction, other sessions’ snapshots get slower: commit long transactions of many runs.
- PostgreSQL:
tx_idle_rollbackneeds PostgreSQL 9.6 or later; older servers refuse it and the first write fails. Set it to"off"there. Savepoint names starting withdatarig_are datarig’s own and are refused in a transaction opened for manual commit.
Startup SQL
A Startup SQL is a named list of settings that a profile’s query sessions (each tab’s) start with, for one kind of database. It is optional: a profile without any of these keys starts its sessions with the database’s own defaults.
[startup.reporting]
dbms = "postgres" # postgres | mysql
sql = ["SET statement_timeout = '30s'", "SET work_mem = '64MB'"] # one statement per item
[policy.prod]
startup = { postgres = "reporting", mysql = "my_reporting" } # per database, for its profiles
[[connections]]
name = "orders"
startup = "reporting" # this profile's own choice, instead of its environment's
# startup = "none" # none, even when its environment maps one
startup_sql = ["SET work_mem = '256MB'"] # its own statements, after the Startup SQL's
none is not a preset name: [startup.none] is an error of the file. The keys were named
differently in older versions and are still read: [sessions.<name>] for [startup.<name>], an
environment’s session for startup, and a profile’s session and session_sql for
startup and startup_sql. A place that has a key and its old name is an error. Saving from
the app renames the old names in place once what they say changes.
Manage presets
The explorer’s Startup SQL section lists the presets with their database and how many
statements and profiles each has (the statements in the warning colour when one changes a setting
a read-only environment refuses, such as MySQL’s foreign_key_checks = 0), and under each
preset the first statement that is not allowed and why. n makes one, e edits, c copies,
d deletes, t tests, and Enter or l lists the profiles that use it (their own or through
their environment). The / filter finds a preset by its name or a statement. The command line
also finds session.new.
Its form has the name, the database and the statements, one per row:
Enteron a row with a statement adds an empty row below; on an empty row it removes the row and moves to the next field.Up/Downgo from row to row,Tableaves the list,Backspaceon an empty row orCtrl+Don any row removes it.- A paste of several statements makes a row of each: a
;ends one, and so does a line break before a line that starts withSET. A row that still holds two statements (SET a = 1 SET b = 2) is refused. - With the mouse, a click puts the cursor in a row,
×removes it and + Add statement adds one at the end.
Under each row that is not allowed the form says why, and under each one a read-only environment would refuse, that it would. A rename takes the profiles and environments that name the preset along; deleting one asks first and names them.
Test opens one session on a connected profile that uses the preset (else any connected profile of its database; with several you pick one), runs the preset exactly as a tab’s session would, and closes the session again, saying what was applied or which statement the server refused. A profile that is not connected is not connected for it.
In a profile’s form (Advanced section, Enter on Startup SQL) you pick its environment’s
preset (the default), None, a preset of its database, or “New Startup SQL…”. Its own
statements are the Own startup SQL list below it. The environment form has one choice per
database.
What a session starts with
In order, a later setting of the same name winning:
- datarig’s own limits:
lock_wait,idle_disconnect,tx_idle_rollback(Environments and safety, Manual commit); - the preset: the profile’s
startup, else the one its environment maps for its database ([policy.default]may map one too); - the profile’s
startup_sql.
So a preset’s or the profile’s own SET of the lock wait overrides the environment’s
lock_wait for its sessions, and one of the server’s idle limit (see
Idle limit) its idle_disconnect. The explorer’s lookups (the metadata
sessions) never get these settings.
Idle limit
A statement may set the server’s idle limit: PostgreSQL SET idle_session_timeout = '10min'
(version 14 or later), MySQL SET SESSION wait_timeout = 600. That value is then the idle
disconnect of those sessions instead of the environment’s idle_disconnect:
- datarig’s own timer follows it: a tab’s session closes after that long unused, a tab with an open transaction is warned a minute before, and the message names that time. The server has the same limit, so either one may end the session first; a session the server ended is reported as closed for being idle.
- The connection’s other sessions and its SSH tunnel stay open as long as its longest tab limit, so a tab never loses its tunnel before its own limit.
- PostgreSQL’s
0turns the limit off for those sessions. - On PostgreSQL a value datarig does not read (
DEFAULT, an expression, a unit it does not read) still goes to the server, and datarig keeps closing the session after the environment’sidle_disconnect(the server does not count it inside a transaction). On MySQLwait_timeoutmust be a whole number of seconds from 1: the server counts it inside a transaction too, so another value (DEFAULT,0, an expression) is refused. - Behind a pooler PostgreSQL’s is not sent (see
Behind a connection pooler): the environment’s
idle_disconnectstays there. - In a read-only environment PostgreSQL’s is allowed; MySQL’s
wait_timeoutis not (an idle limit is not among the settings a read-only session may change there). - MySQL’s
interactive_timeoutis refused: the server reads it only when a session starts, so it has no effect on an open one.
:session shows the limit that applies and what it overrides.
Seeing what applies
:session (or :startup) shows what the query sessions of the explorer’s selected profile
(else the active tab’s) start with. Ctrl+E in a profile’s form shows the same for the form’s
values, saved or not. The view lists, in the order they are applied, the app’s limits with
their values and where each comes from, then each statement under where it comes from. Under a
PostgreSQL statement a dim line says how it would go behind a pooler. A profile whose plan has
an error shows the words its connect would refuse with.
╭ Explorer ──────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ─────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 · prod Space c s switch connection │
│ ▾ ● local-pg ││ 1 -- datarig 🐘 Ctrl+E: run the statement under the cursor │
│ ▾ datarig default ││ 2▎SELECT * FROM shop.users WHERE id <= 8; │
│ ▸ public ││ 3 │
│ Loading… ││ 4 SELECT u.name, o.status, o.total_amount │
│▾ Environments 1 ││ 5 FROM shop.orders o │
│ ╭ Session settings: local-pg ────────────────────────────────────────────────────────────────────────────────╮ │
│ ▸ │ local-pg · PostgreSQL · environment prod │ │
│▾ St│ │ │
│ ▸ │ Lock wait 5s environment prod │ │
│ │ Idle disconnect never app setting │ │
│ │ Idle rollback 30m environment prod │ │
│ │ │ │
│ │ From Startup SQL “reporting” of environment prod: │ │
│ │ SET statement_timeout = '30s' │ │
│ │ behind a pooler: per transaction as SET LOCAL statement_timeout = '30s' │ │
│ │ SET work_mem = '64MB' │ │
│ │ behind a pooler: per transaction as SET LOCAL work_mem = '64MB' │ │
│ │ From its own startup SQL: │ │
│ │ SET work_mem = '256MB' │ │
│ │ behind a pooler: per transaction as SET LOCAL work_mem = '256MB' │ │
│ │ SET ROLE reporting │ │
│ │ behind a pooler: not sent (it changes who the session is) │ │
│ │ │ │
│ │ Applied in this order: a later setting of the same name wins │ │
│ │ │ │
│ │ [ Close ] │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────── j/k scroll · Esc close ─╯ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
╰────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────────────╯
NORMAL local-pg · prod │ Connected to local-pg EN
Rules for the statements
-
Only settings. Each item is one statement that sets settings of the session and nothing else: PostgreSQL
SET [SESSION] name = value(orTO,TO DEFAULT),SET TIME ZONE …,SET SESSION CHARACTERISTICS AS TRANSACTION …; MySQLSET [SESSION | LOCAL] name = value[, …],SET @var = …,SET SESSION TRANSACTION …. NotSET LOCAL, a plainSET TRANSACTION,SET GLOBALorPERSIST,RESET,DISCARD, a query or a call such asset_config(), nor several statements in one item. In a read-only environment only the settings a read-only session may change at all. -
Not datarig’s own settings. What datarig manages itself is refused, with why and where to set it instead:
- MySQL’s
interactive_timeout: it has no effect on an open session; setwait_timeout(see Idle limit) or the environment’sidle_disconnect. idle_in_transaction_session_timeout: usetx_idle_rollback.- the read-only settings (
default_transaction_read_only,transaction_read_only, MySQL’stx_read_only,… TRANSACTION READ ONLY/WRITE): use the environment’sread_only. - MySQL’s
autocommitandcompletion_type: use the environment’scommit. search_path: the tab’s schema sets it (:use).- what datarig reads results in, writes literals with or tells sessions apart by:
client_encoding, MySQL’s character sets,collation_connectionandSET NAMES,DateStyle,IntervalStyle,bytea_output,extra_float_digits,standard_conforming_strings,application_name, MySQL’ssql_select_limit,resultset_metadataandsession_track_*.
The lock wait settings, the server’s idle limit (
idle_session_timeout, MySQL’swait_timeout) and MySQL’smax_execution_timeare allowed. Settings of one transaction only are refused too, with where the session’s default is: PostgreSQL’stransaction_isolationandtransaction_deferrable(usedefault_transaction_isolation, …), MySQL’sSET @@transaction_isolationwithout a scope (useSET SESSION …). - MySQL’s
-
MySQL in every sql mode. A statement is read with and without
ANSI_QUOTESandNO_BACKSLASH_ESCAPES; what any reading refuses is refused. -
Who the session is.
SET ROLEis allowed in a read-only environment (every transaction staysREAD ONLYwhatever the role);SET SESSION AUTHORIZATIONis not. -
Errors are the profile’s. A preset name that does not exist, a preset for the other database, or a statement that is not allowed: the profile does not connect and says which preset or statement, from where, and why. It never connects without the preset instead. Statements are checked when a profile connects and before each tab’s session opens, not when the file is read.
-
Sent is not applied. Each statement runs alone, in order, when a tab’s session opens (and when it reconnects). One the server refuses closes that session before anything else runs, and the tab quotes the server’s error. The run that opened the session does not happen.
Behind a connection pooler
On a PostgreSQL connection behind a pooler (see Poolers) nothing is set for the
session. Each SET goes first in every transaction as SET LOCAL instead, and the tab says so.
Statements that cannot run in a transaction, CALL, DO, and your own SET and RESET run
without them.
These are not sent there at all (the profile still connects, and the tab names what was not
sent): statements that change who the session is (SET ROLE, SET SESSION AUTHORIZATION:
a statement run outside a transaction would run as someone else), statements with no
SET LOCAL form (SET SESSION CHARACTERISTICS …), defaults of the session’s transactions
(default_transaction_isolation, default_transaction_deferrable), and values that depend on
what the pooled connection did before (temp_buffers, PostgreSQL 17’s transaction_timeout),
and idle_session_timeout (the server counts it only outside a transaction, and set for the
session it would end server connections other clients share).
Behind a pooler, set the isolation in your own block instead (BEGIN ISOLATION LEVEL …).
Details
- On MySQL the classifier reads the session in the sql mode a preset set, and a read-only
session is checked after its statements ran. A value with a function call or a subquery
(
SET @x = (SELECT …)) runs at connect like any other. - A row shows its statement on one line, without its line comments, and a line break inside a
string as
⏎. A row you do not edit is saved as the file has it; one you edit is saved as its one line, so a comment in it is gone.
Editor
Each console and table tab has a SQL editor with vim keys, syntax highlighting, completion and a formatter you run yourself. The statement under the cursor is marked in the gutter.
╭ Explorer ─────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ──────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-pg ││ 1 -- datarig 🐘 Ctrl+E: run the statement under the cursor │
│ ▾ datarig default ││ 2 SELECT * FROM shop.users WHERE id <= 8; │
│ ▸ analytics ││ 3 │
│ ▸ public ││ 4▎SELECT u.name, o.status, o.total_amount │
│ ▸ shop ││ 5▎FROM shop.orders o │
│ ││ 6▎JOIN shop.users u ON u.id = o.user_id │
│ ││ 7▎WHERE o.status = 'paid'; │
│ ││ 8 │
│ ││ 9 SELECT * FROM analytics.events; │
│ ││ 10 │
│ ││ 11 SELECT * FROM analytics.events ORDER BY created_at DESC; │
│ ││ 12 │
│ ││ 13 SELECT analytics.slow(30); │
│ ││ 14 │
│ ││ 15 SELECT $$a body; with a semicolon$$ AS dollar, 'it''s; fine' AS quote│
│ ││ 16 │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
╰───────────────────────╯╰─────────────────────────────────────────────────────────────────────────╯
NORMAL local-pg │ Connected to local-pg │ i type · Ctrl+E run · Tab next pane · : commands EN
Vim keys
The editor has vim keys only. i starts typing and Esc stops; the status bar says so. What
is implemented, all with counts:
- Normal mode: the usual motions (
wbe,0^$,ggG,ft;,%,{},HML, …), the operatorsdcy,gugUg~and><with any motion or text object (iwaw,i(a(,i",ip, …), doubled for lines (dd,yy),xsDCpPrJ~,uandCtrl+R, and.repeating the last change. One command is one undo step. - Visual mode by character (
v), by line (V) and by block (Ctrl+V). The block works in screen columns, so wide characters line up, andI/Atype on every line of the block. - Insert mode keeps the usual editing keys, plus
Ctrl+W,Ctrl+UandCtrl+Rwith a register.Enterkeeps the line’s indent.Tabtypes 4 spaces, andBackspaceafter spaces deletes back to the tab stop in one press, as Vim withsofttabstop=4does ([editor] soft_tab_backspace = "off"always deletes one character). - Registers as in Vim (
"a-"z,"0, the"1-"9delete ring,"-,"_,".,"/,"+/"*). As withclipboard=unnamedplus, every yank, delete or change without a register also goes to the system clipboard, or through OSC 52 over SSH;[editor] clipboard = "off"keeps them in the editor."+preads the system clipboard only when you type it. - Search with
/and?(a prompt on the editor’s last line; the cursor shows the match as you type),nN,*#,g*g#, also after an operator (d/from). Matches on screen stay highlighted until:noh. Patterns are Rust regular expressions, not Vim’s: case sensitive unless the pattern starts with(?i),\bfor a word boundary, and a match never spans two lines. - Marks:
m{a-z},'aand`a,'','<'>and'.. Marks follow their lines through edits and come back on undo. - Comments:
gccomments lines out with--or back in, as Neovim does (gcc,gc{motion},gcin Visual mode; alsoSpace e c). - Auto-pairs are off by default, as in Vim.
[editor] auto_pairs = "on"(or:set editor.auto_pairs=on) makes brackets and quotes put their closing character after the cursor (not inside strings or comments, not before a word, never for a paste). - A paste from the terminal goes in at the cursor in every mode.
- Commands keep working with a Korean (2-set) input source: the jamo are read as the QWERTY keys they sit on.
The complete list, with every key’s exact behaviour, is in the
keybindings reference. Not supported: macros (their keys
are reserved), gv, and Ex commands other than line numbers, :s and :format (such as delete and
global).
Ex commands
On the : line the editor takes:
- A line range alone goes to its last line:
:12,:$,:'a,:+3. :[range]s/pattern/replacement/[flags]replaces in the range (the cursor’s line without one), as one undo step, for example:%s/old/new/gor:'<,'>s/^/-- /. The pattern is a Rust regular expression, the replacement Vim’s (&,\1,\r,\u,~;$1is plain text). Flags:g,i,I,e;c(confirm) is not supported.:&&and&/g&in Normal mode repeat the last substitution.:[range]formatformats the range’s lines (see below).
In Visual mode : starts the line with '<,'>, as Vim does. The full syntax is on the
commands reference.
Completion
Ctrl+N or F4 in Insert mode shows completions of schemas, tables and columns (aliases
included) at once.
The popup also opens by itself while you type: once a name has two characters, after a . or
after a string’s opening quote, as soon as no key came for [editor] completion_delay_ms
(60 ms; 0, 30, 100, 150 or 250 in the settings screen or with
:set editor.completion_delay_ms=…). Nothing opens before that; with 0 it opens with the
key itself. While it is open, typing narrows it at once. Esc closes it, and it does not come
back by itself until you type again.
- Names go in as SQL: a name that is not a plain lower-case word (
MixedCase,Order Lines) or is a reserved word goes in quoted. - The statement’s
WITHqueries are offered as tables, with their columns when the query names them. - Names that start with what you typed come first, then names that contain its letters in
order (
oifindsorder_items); keywords only by their start.
Values in a string literal compared with a column (status = ', <>, !=,
status IN ('paid', ', NOT IN, UPDATE … SET status = ') complete to the column’s allowed
values. They come from its type (PostgreSQL enums, also through domains; MySQL ENUM), then
from its table’s CHECK constraints of the forms col IN ('a', 'b'), col = 'a' and an OR of
those. The checks are read with the table’s structure (PostgreSQL 12+, MySQL 8.0.16+), so their
values show a moment after the first use. A value goes in escaped as the literal reads it, and
an unclosed literal gets its closing quote. A value inside a function call
(lower(status) = ') gets none; INSERT … VALUES is not covered.
Formatting
Space e f, :format or :'<,'>format lays out the statement under the cursor or the
selection, as one undo step. It never runs on its own.
- It changes only the layout: line breaks, the indent (
[editor] format_indent, 4 or 2), and keyword case if you ask for it ([editor] format_keyword_case = "upper"or"lower"). Names, strings, comments and dollar-quoted bodies stay exactly as written. - Every result is checked token by token against the original. When anything else would change
(an operator split, strings joined across a line break,
U&'...', backslash meta-commands) it refuses, says on which line, and leaves the text alone.
Running statements
Ctrl+E(orCtrl+Enterwhere the terminal reports it) runs the statement under the cursor, or the selection. Several statements run in a row: each outcome is listed in a Messages tab, and every row result gets a result tab of its own (see Results).Ctrl+Ccancels. When a PostgreSQL cancel cannot reach the server (a tunnel that refuses it, a network failure), datarig closes the tab’s connection and says why. What became of the statement is then unknown: it may run to its end and commit if it was not in a transaction block; an open block is rolled back when the server notices the closed connection.- While a run goes on, the statement it executes has a tint of its own and a spinner in the gutter, wherever you move or edit.
- When it ends, a dim hint after each statement’s last line says what it did (rows and time,
rows affected, the error, rolled back, cancelled). The hint is not part of the text and goes
when you edit the statement.
[editor] run_hints = "off"turns the hints off.
Saved queries and the workspace
- Saved queries are plain
.sqlfiles in folders in the data directory:Space s sor:wsaves (a console asks for a name),Space s asaves as,Space s oor:eopens one. - Every tab is autosaved, and the workspace (tabs, cursors, open folders) is restored at the
next start. Closed consoles go to a trash:
:recoverbrings one back.
Your own editor, and suspending
Ctrl+Gopens the query in your own editor ($VISUAL, else$EDITOR, elsevi,notepadon Windows; arguments are allowed, for exampleEDITOR="vim -n"), run directly without a shell on a private temporary file. What you save replaces the text as one undo step; quitting without saving (or Vim’s :cq) changes nothing. A table tab’s query opens as a copy that comes back in a new console.Ctrl+Z(or:suspend) suspends datarig as Vim does, and the shell’sfgbrings it back (not on Windows). A running query keeps running on the server meanwhile.
Results
A run’s results open in the results pane below the editor. Each row result gets a result tab
of its own, before the Plan, Chart and Messages tabs (Messages holds each statement’s outcome). While
the shown result has pending changes, a Changes tab follows the result tabs (see
The Changes tab). In the results pane H and L move between the
tabs; Space r [ and Space r ] do it from anywhere.
╭ Explorer ────────────────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ───────────────────────────────────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-pg ││ 1 -- datarig 🐘 Ctrl+E: run the statement under the cursor │
│ ▾ datarig default ││ 2▎SELECT * FROM shop.users WHERE id <= 8; ✓ 3 rows · 3ms · 14:03 │
│ ▸ analytics ││ 3 │
│ ▸ public ││ 4 SELECT u.name, o.status, o.total_amount │
│ ▸ shop ││ 5 FROM shop.orders o │
│ ││ 6 JOIN shop.users u ON u.id = o.user_id │
│ ││ 7 WHERE o.status = 'paid'; │
│ ││ 8 │
│ ││ 9 SELECT * FROM analytics.events; │
│ ││ 10 │
│ ││ 11 SELECT * FROM analytics.events ORDER BY created_at DESC; │
│ ││ 12 │
│ ││ 13 SELECT analytics.slow(30); │
│ ││ 14 │
│ │╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
│ │╭ Results · 3 rows ────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ││ [Result 1] Chart Messages H/L │
│ ││ │ PK id │ FK user_id │ UQ email │ name │ PK FK order_id │ PK line_no │ FK product_id │ doubled │
│ ││ │ int8 │ int8 │ text │ text │ int8 │ int4 │ int8 │ numeric │
│ ││ 1 │ 1 │ 1 │ user01@example.com │ ユーザー 1 │ 1 │ 1 │ 7 │ 2 │
│ ││ 2 │ 2 │ 1 │ user02@example.com │ ユーザー 2 │ 2 │ 1 │ 7 │ 2 │
│ ││ 3 │ 3 │ 1 │ user03@example.com │ ユーザー 3 │ 3 │ 1 │ 7 │ 2 │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
╰──────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
NORMAL local-pg │ 3 rows · 3ms │ Enter view cell · n next page · y copy · v select · i detail · Esc back · Tab next pane · : commands · Space ? help EN
Pages
The grid shows one page at a time (page_size, 500 rows by default): n (or ]) and [ move to the
next and previous page. By default nothing stays open on the server between pages: the first
page ends its transaction, and the next page runs the statement again when it is safe to repeat
(said each time); otherwise you see the first page and why. An environment can keep the cursor
open instead; see Paging and locks.
The rows already fetched stay in a bounded memory window and spill to a private temporary file
past it. # counts every row on demand (it runs a count query), with a label that says whether
the count matches the pages you saw.
Moving around
hjklor the arrows move the cell;0and$go to the first and last column,ggandGto the first and last row of the page,Ctrl+D/Ctrl+Uscroll half a screen.- The wheel scrolls the rows, three a notch. A trackpad’s sideways swipe, a tilting wheel or
Shift with the wheel scrolls sideways by character, four a step: the columns at the edges show
the part that fits, and the headers scroll with the rows. A value longer than a column’s 40
characters is cut with
…. The selected cell stays where it is until a key or a click moves it; then the view moves just enough to show that cell whole. Entershows the cell’s value whole in the cell viewer.- The cursor’s row is tinted (its number in the accent), its cell more strongly, and so is its
column’s header (a selected range tints its columns’ headers instead). A new row keeps its
own tint and shows the cursor by its number. On the
terminaltheme the row keeps your terminal’s background: the row’s number and the underlined header show where the cursor is. vselects a range of cells,Vwhole rows,Ctrl+Aevery cell of the page; the mouse selects too. The selection is one tinted block, the separators between its columns included. The rows of a range of cells or of whole rows take the row tint and a tinted number; the selected columns’ headers are tinted too (whole columns tint their headers and cells, not the row numbers). The theme tokensgrid_cursor_rowandgrid_cursor_headerset the tints.- While a selection is there and the grid has the focus, the status line sums it up (a fresh
message comes first; in a narrow window the least and greatest values, then NULL, then the
average, are left out): its size,
then for numbers (every selected column an integer, decimal or floating-point one) the sum,
average, least and greatest value, else how many distinct values; and how many are NULL, as in
5 rows × 1 column · sum 1,013.00 · avg 253.25 · min 0.25 · max 1,000 · NULL 1. Integers and decimals add up exactly. The values are those the grid shows, a pending change over the value read, as a copy takes them; a cell with no value yet (a pending DEFAULT) is counted apart, new rows are left out, and whole columns say “fetched rows only” when the server has more. The rows are read in the background (a big selection says “summing…” at first), so keys never wait for it. A selection that grows at its end (whole columns while every row is fetched, or a range extended downwards) keeps its figures: only the new rows are read and added, and the figures so far stay on the line, marked “summing…”, until they are. Distinct values are counted up to 100,000. z(orSpace z) zooms the results pane to the whole workspace and back;+and-make it taller or shorter.Escorqgoes back to the editor.
Search
/ searches forward and ? backward: a prompt under the grid, the cursor on the first matching
cell as you type, Enter to search. Then n and N go from match to match, across pages and
around the ends with a warning, and Esc ends the search (n is the next page again; ] is
the next page all along). :noh ends it too.
The search covers every row already fetched (the spilled ones too), each cell’s full value as
the server sent it, not the cut the grid shows. NULL never matches, and nothing is fetched or
run again for it: when the server has more rows, the line under the grid says so. Patterns are
the editor’s (Rust regular expressions, case sensitive unless they start with (?i)); matching
cells are tinted.
╭ Explorer ──────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ─────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-pg ││ 1 -- datarig 🐘 Ctrl+E: run the statement under the cursor │
│ ▾ datarig default ││ 2▎SELECT * FROM shop.users WHERE id <= 8; ✓ 8 rows · 12ms · 14:03 │
│ ▸ analytics ││ 3 │
│ ▸ public ││ 4 SELECT u.name, o.status, o.total_amount │
│ ▸ shop ││ 5 FROM shop.orders o │
│ ││ 6 JOIN shop.users u ON u.id = o.user_id │
│ ││ 7 WHERE o.status = 'paid'; │
│ ││ 8 │
│ ││ 9 SELECT * FROM analytics.events; │
│ ││ 10 │
│ ││ 11 SELECT * FROM analytics.events ORDER BY created_at DESC; │
│ ││ 12 │
│ │╰────────────────────────────────────────────────────────────────────────────────────────╯
│ │╭ Results · 8 rows ──────────────────────────────────╮╭ Cell [Row] · I ──────────────────╮
│ ││ [Result 1] Chart Messages H/L ││ id: 5 │
│ ││ │ profile ││ name: 山田太郎 │
│ ││ │ jsonb ││ nickname: やまだ │
│ ││ 1 01大樓 … │ {"lang": "ko", "tags": ["dev", "db"]} ││ address: 東京都渋谷区道玄坂1-2-3 │
│ ││ 2 │ {} ││ profile: {"lang": "ja"} │
│ ││ 3 │ {"vip": true, "level": 7} ││ │
│ ││ 4 │ {"flags": ["🇰🇷", "🇺🇸"]} ││ │
│ ││ 5 │ {"lang": "ja"} ││ │
│ ││ 6 │ {"lang": "zh"} ││ │
│ ││ 7 View, … │ {"lang": "en"} ││ │
│ ││ 8 │ {"multiline": true} ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ /lang 2/4 ││ │
╰────────────────────────────╯╰────────────────────────────────────────────────────╯╰──────────────────────────────────╯
NORMAL local-pg │ 8 rows · 12ms │ n next match · Esc clear · Enter view cell · y copy · q back · Tab next pane EN
The inspector
i shows or hides the inspector next to the grid; I switches between its Row and Cell tabs.
It starts on the Row tab, and the tab you pick stays for the session. JSON values are
pretty-printed.
- The Row tab follows the grid’s column: it highlights that column and scrolls to it.
- After a click on the inspector,
jandk(or a click on another column) move the grid’s cell there, the grid scrolling sideways to it;Enteror a double-click shows the value whole. detail_view = "statusbar"(or:set detail_view=statusbar) shows a preview of the cell in the status bar instead of the panel.
╭ Explorer ────────────────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ───────────────────────────────────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-pg ││ 1 -- datarig 🐘 Ctrl+E: run the statement under the cursor │
│ ▾ datarig default ││ 2▎SELECT * FROM shop.users WHERE id <= 8; ✓ 3 rows · 3ms · 14:03 │
│ ▸ analytics ││ 3 │
│ ▸ public ││ 4 SELECT u.name, o.status, o.total_amount │
│ ▸ shop ││ 5 FROM shop.orders o │
│ ││ 6 JOIN shop.users u ON u.id = o.user_id │
│ ││ 7 WHERE o.status = 'paid'; │
│ ││ 8 │
│ ││ 9 SELECT * FROM analytics.events; │
│ ││ 10 │
│ ││ 11 SELECT * FROM analytics.events ORDER BY created_at DESC; │
│ ││ 12 │
│ ││ 13 SELECT analytics.slow(30); │
│ ││ 14 │
│ │╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
│ │╭ Results · 3 rows ────────────────────────────────────────────────────╮╭ Cell [Row] · I ──────────────────────────────╮
│ ││ [Result 1] Chart Messages H/L ││ PK id: 1 │
│ ││ │ PK id │ FK user_id │ UQ email │ name │ PK FK orde││ FK user_id: 1 │
│ ││ │ int8 │ int8 │ text │ text │ int8 ││ UQ email: user01@example.com │
│ ││ 1 │ 1 │ 1 │ user01@example.com │ ユーザー 1 │ ›││ name: ユーザー 1 │
│ ││ 2 │ 2 │ 1 │ user02@example.com │ ユーザー 2 │ ›││ PK FK order_id: 1 │
│ ││ 3 │ 3 │ 1 │ user03@example.com │ ユーザー 3 │ ›││ PK line_no: 1 │
│ ││ ││ FK product_id: 7 │
│ ││ ││ doubled: 2 │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
╰──────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────╯
NORMAL local-pg │ 3 rows · 3ms │ Enter view cell · n next page · y copy · v select · i detail · I cell/row · Esc back · Tab next pane · : commands EN
Copy
y copies the cell (a selected range) and Y the row (the selected rows), as TSV unless
copy_format says otherwise. The
Space r y group copies the selection, and Space r a every fetched row, as:
TSV with or without headers, CSV, JSON, indented JSON, Markdown, HTML, XML, an SQL IN list,
or SQL INSERT/UPDATE statements (only when the target table is certain). :copy <format>
does the same from the command line (for example :copy csv all).
- Copies go to the system clipboard, or through OSC 52 over SSH (
clipboard:auto,systemorosc52;osc52_max_bytesis the longest copy sent through OSC 52). - The status bar names the format:
Copied 5 rows as CSV (OSC 52). - The copied cells flash briefly and fade back (with the terminal’s own colours the flash goes
off at once);
[ui] yank_flash = "off"turns that off.
The format of y and Y
copy_format is the format y and Y copy in: tsv (the default), csv, json,
json_pretty, markdown, html, xml or insert, the same names as :copy. Set it in the
settings screen, with :set copy_format=csv, or from the grid: Space r y d (or the menu’s
“Default format for y and Y”) lists the formats with the current one marked, and picking one
makes it the default. The Space r y group and the menu’s copy formats mark the one y and Y
use.
- One cell without a selection is always copied as its value, whatever the format.
insertwrites SQLINSERTstatements only when the rows are certainly of one table (the same rule as Copy as SQL INSERT). OtherwiseyandYcopy TSV and the status bar says why.g pandppaste the cellsyandYcopied, value for value, whatever format the clipboard got.
Column names in copies
copy_header says whether a copy starts with a row of column names:
auto(the default): when more than one row is copied.yon one cell,Yon one row and a selection inside one row copy no header; a selection of two or more rows, or whole columns, starts with one.on: always, even for one cell.off: never.
It applies to TSV copies from y and Y. A CSV copy always starts with the column names
unless copy_header = "off". Copy as TSV without or with headers ignores the setting, and JSON,
Markdown, HTML, XML and the SQL formats are written the same way whatever it says.
To write a result to a file instead, see Export. To draw it, see Charts.
Editing
A result of one table can be edited in the grid: e edits a cell, o and O add rows, D
marks rows to be deleted, and the changes stay pending until you apply them. After the first
edit key that gets as far as the table, the result tab strip says whether the rows can be edited
(editable · shop.users · key id) or why not (read-only · <reason>; a reason about the
connection, such as a read-only environment, is said by the key only), then how many changes are
pending (3 pending) and, after an Apply, applied · re-run to refresh. See Editing
data.
Transactions in the results
Results read inside an open transaction are labelled uncommitted. The tab bar and the status bar show an open transaction, an aborted one, and “rollback required”; see Manual commit.
Export
Space r e, the results menu, or :export alone opens the export dialog for the active tab’s
result: which rows, the format, CSV’s options, and the file.
╭ Explorer ────────────────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ───────────────────────────────────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-pg ││ 1 -- datarig 🐘 Ctrl+E: run the statement under the cursor │
│ ▾ datarig default ││ 2▎SELECT * FROM shop.users WHERE id <= 8; ✓ 5+ rows · 0ms · 14:03 │
│ ▸ analytics ││ 3 │
│ ▸ public ││ 4 SELECT u.name, o.status, o.total_amount │
│ ▸ shop ││ 5 FROM shop.orders o │
│ ││ 6 JOIN shop.users u ON u.id = o.user_id │
│ ││ 7 WHERE o.status = 'paid'; │
│ ││ 8 │
│ ││ 9 SELECT * FROM analytics.events; │
│ ││ 10 │
│ ││ 11 SELECT * FROM analytics.events ORDER BY created_at DESC; │
│ ││ 12 │
│ ╭ Export console 1 ────────────────────────────────────────────────────────────────────────────────╮ │
│ │ │ │
│ │ Rows All rows ▾ │─────────────────────────────╯
│ │ Runs the statement again, once: until its last row is written it holds a read lock on │─────────────────────────────╮
│ │ the tables it reads (a schema change on them waits) and one snapshot. │ │
│ │ Format CSV ▾ .csv │ │
│ │ Delimiter , one character; \t for a tab │ │
│ │ Quotes As needed ▾ │ │
│ │ Header [x] column names on the first line │ │
│ │ NULL as empty: NULL is an empty field │ │
│ │ File ~/Downloads/console 1-YYYYMMDD-HHMMSS.csv [Browse…] │ │
│ │ │ │
│ │ [ Export ] Cancel │ │
│ ╰─────────────────────────────────── Tab/↑↓ field · Enter list or export · ←→ change · Esc cancel ─╯ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
│ ││ ││ │
╰──────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────╯
NORMAL local-pg │ 5+ rows (more pages) · 0ms EN
Without the dialog:
:export [format] [all|fetched|selection] [delim= quote= header= null=] [path]
Every row is the default. With no format, the file’s extension names it (.xlsx and
.parquet included). Examples:
:export csv fetched delim=semicolon ~/Downloads/orders.csv
:export ~/Downloads/orders.parquet
An export runs in the background, one per tab, with its rows, size and time in the tab’s status.
Formats
| Format | :export name | Options |
|---|---|---|
| CSV | csv | delim= (one character, or tab, comma, semicolon, pipe), quote= (necessary, always, non_numeric, never), header= (on/off), null= (the text written for NULL) |
| TSV | tsv | header= |
| Excel | xlsx (also excel) | null= |
| Parquet | parquet | none |
| JSON, indented JSON, JSON Lines | json, json_pretty, jsonl (also ndjson) | none |
| Markdown, HTML, XML | markdown (also md), html, xml | none |
SQL INSERT, UPDATE | insert (also sql), update | none; only where the result’s keys allow them, as with copy |
Excel (.xlsx)
- The header row is bold and stays in view. A sheet holds 1,048,575 rows under its header; the
rows after that go on in
Sheet2,Sheet3, …, each with the header again. - A value of a number column is a number only when Excel holds it exactly: at most 15
significant digits, within a double’s normal range. Anything else (a
bigintpast 15 digits, anumericwith more digits,NaN,Infinity) is text, every digit kept. Booleans are booleans. Everything else, dates and timestamps included, is the text the database wrote. NULL is an empty cell, or thenull=text. - A sheet holds 16,384 columns: a wider result is refused before anything is written. A cell holds 32,767 characters as Excel counts them (UTF-16 units: an emoji counts two); a longer value stops the export there, naming its column and row, and the workbook keeps the rows before it.
- Rows go to temporary files in the system’s temporary folder (
TMPDIR) as they come, so memory stays at about one row. The workbook is made from them at the end (“Making the workbook…” in the status). It is a ZIP64 container, so a workbook past 4 GiB is fine.
Parquet
- Typed columns, every one nullable, compressed with Snappy. Integers are INT32 or INT64
(MySQL’s
bigint unsignedan unsigned INT64), floats FLOAT or DOUBLE, booleans BOOLEAN, dates DATE, times without a zone TIME (a time with a zone is text) and timestamps TIMESTAMP in microseconds, binary data its bytes. PostgreSQL’stimestamptzis converted to UTC; MySQL’sTIMESTAMPis the session’s local time and is not converted. Everything else, decimals included, is the text the database wrote, every digit kept. - A column of a PostgreSQL domain is known by the domain’s name only, so a domain over an integer is INT64 and one over a float DOUBLE.
- A value the column’s type cannot hold (
infinity, MySQL’s zero date,24:00:00) stops the export there, naming its column and row; it is never written as NULL. - Rows are written in row groups of at most 65,536 rows or 32 MiB of values, so memory stays at about one group.
- Two columns of one name are written as they are; some readers (Spark, some pandas paths) refuse or rename them, so give them aliases.
Where the rows come from
- Fetched rows and the selection are read from what is already loaded; nothing is sent to the server. A result with pending or applied changes is not exported this way: see Editing data.
- All rows of a result with more on the server need the statement to run again.
- A table tab showing its whole table, with a primary key or a unique key on NOT NULL columns and no open transaction, is read in chunks by that key, each a read of its own that waits at most 5 s for a lock (rows changed meanwhile may be read in a later state).
- Any other result runs its statement once and streams it: a plain
SELECTthat is safe to repeat goes ahead, anything else asks first. Environments withread_only = trueorconfirm = "writes"ask first too, because the statement holds a read lock and a snapshot until the export finishes.
- An export only reads: a statement that writes or locks rows is refused unsent. Outside a
transaction of yours each statement it runs is a read-only transaction of its own
(
BEGIN READ ONLYon PostgreSQL,START TRANSACTION READ ONLYon MySQL), so its rows are one snapshot and a function that writes fails. Inside your open transaction it reads there and sees what you changed; on PostgreSQL under a savepoint, so a failed or cancelled export does not abort your transaction. An aborted transaction refuses an export until you roll it back. - A slow file holds the statement back instead of piling rows up in memory. A server may end a
slow export (MySQL’s
net_write_timeout, 60 s by default; PostgreSQL’sidle_in_transaction_session_timeoutor a pooler’s idle timeout).
Choosing where to save
The path starts as ~/Downloads/<table or tab>-<YYYYMMDD-HHMMSS>.<ext> (in the home folder when
there is no ~/Downloads), and while it is that name a format change gives it the new
extension. ~ is the home folder; a relative path goes in ~/Downloads.
Tabin the path field completes a name: first the longest text the matching names share, then (Tabagain) a list of them, then each in turn.Ctrl+Oin the dialog (or its[Browse…]button) opens a tree of the file system.UpandDown(j/k) move,RightandLeft(l/h) open and close a folder,../goes up.Enteron the top row (“Save here”) orCtrl+Sanywhere puts the selected folder in the field;Enteron a file takes that file.nmakes a folder,.shows hidden names,/filters,Escleaves the field as it was. Files are dimmed: they are what a file of that name would replace.- A folder that cannot be listed is never saved into. A path that names a folder is refused: name a file.
When an export stops
- The file is written as
<path>.partial, created new, and renamed to the path when the export succeeds. An existing file is never replaced without asking (yreplaces it,Enterkeeps it). A.partialfile left by an earlier export is not written over; move or delete it. - A cancel stops the statement on the server and keeps the
.partialfile with the rows written so far, ended so that it reads (a JSON array closed, an Excel workbook of those rows, a Parquet file with its footer), except while an Excel workbook is being made: a cancel then keeps nothing. An export that wrote no rows leaves no file. - A failure says why and where the rows are:
- text formats keep the
.partialfile; one that failed to be written (a full disk) may end in the middle of a row; - a Parquet value that does not fit, or an Excel value too long for a cell, keeps a file of the rows before it that reads, and so does an Excel or Parquet export stopped by the server or the session;
- a Parquet file that could not be written, or an Excel workbook that could not be made, is removed.
- text formats keep the
Limits
- Excel needs room in
TMPDIRfor the sheets’ uncompressed XML until the workbook is made, as well as room for the workbook itself. PointTMPDIRat a larger disk for a very large export. - Excel’s dates and timestamps are text, not Excel dates. Parquet has no DECIMAL columns; decimals are exact text.
- A local export cannot be cancelled while the tab runs another statement.
- Exports are tested on macOS and Linux; running datarig on Windows is untested.
Charts
The Chart tab of the results pane draws the rows a result has fetched as vertical bars,
horizontal bars or lines (in braille dots). Open it with c in the grid, Space r c, its tab
in the strip, or the action menu. Nothing is asked of the server and nothing runs again; nothing
is read for a chart until its tab is viewed.
╭ Explorer ──────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ─────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-pg ││ 1 -- datarig 🐘 Ctrl+E: run the statement under the cursor │
│ ▾ datarig default ││ 2▎SELECT * FROM shop.users WHERE id <= 8; │
│ ▸ analytics ││ 3 │
│ ▸ public ││ 4 SELECT u.name, o.status, o.total_amount │
│ ▸ shop ││ 5 FROM shop.orders o │
│ ││ 6 JOIN shop.users u ON u.id = o.user_id │
│ ││ 7 WHERE o.status = 'paid'; │
│ ││ 8 │
│ ││ 9 SELECT * FROM analytics.events; │
│ ││ 10 │
│ │╰────────────────────────────────────────────────────────────────────────────────────────╯
│ │╭ Chart · Bars ──────────────────────────────────────────────────────────────────────────╮
│ ││ Result 1 [Chart] Messages H/L │
│ ││ [Bars] Horizontal bars Line first 30 fetched rows · more on the server │
│ ││ X day · Y orders, refunds · by none · linear scale │
│ ││ 100┤ ▃ ▆ │
│ ││ │ ▃ █ ▆ █ █ │
│ ││ │ ▃ █ █ ▆ █ █ █ █ │
│ ││ │ █ █ █ ▆ █ █ █ █ █ █ │
│ ││ 50┤ █ █ ▆ █ █ █ █ █ █ █ █ ▂ █ │
│ ││ │ █ ▆ █ █▁ █ █ ▂ █ █▄ █ █ █▆ █ ▂▇ █ █ █ │
│ ││ │▆ █ ▇ █ █ ██ █ █ █ █ █ ▂ ██ █ █▃ ██ ▂ █ ▅ ██ █ █▆ █ │
│ ││ │█ █▅ █ █ █ ▆ ██ █ █ ██ █ █ █ █ ██ █ ▂▁ ██ ██ █ █▂ █ ██ █ ▄ ██ █ │
│ ││ │█▂ ██ █ █ █▄ ██ ██ █ █▆ ██ █ █ █▇ ▂█ ██ █ ██ ██ ██ █ ██ █ ██ █▂ ▅█ ██ █ │
│ ││ 0┤██ ██ ██ █▂ ██ ██ ██ █▃ ██ ██ ▂█ █▅ ██ ██ ██ █▆ ██ ██ ██ ██ ██ ▅█ ██ ██ ██ ██ █▁ │
│ ││ └────────────────────────────────────────────────────────────────────────────────── │
│ ││ 2026-09-01 2026-09-05 2026-09-09 2026-09-13 2026-09-17 2026-09-21 2026-09-2› │
│ ││ ■ orders ■ refunds │
│ ││ ▸ 2026-09-01 · orders 37 · refunds 13 · row 1 │
╰────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────────────╯
NORMAL local-pg │ 30+ rows (more pages) · 3ms │ v kind · x X · s values · y copy · c rows · : commands EN
What it draws
The chart picks its columns at first sight:
- a date or time column and numbers: lines over time;
- a text column and numbers: bars (horizontal with more than 12 of them or labels longer than 12 characters);
- two or more numbers alone: lines over the first one; a single number alone: bars (lines past 40 rows).
Several number columns are several series, and one number with a text column of 2 to 10 values (in the first 500 rows) gives a series per value. The chart says how many fetched rows it draws and whether the server has more: fetch more pages first to chart them.
- Rows with the same X are summed.
- NULLs are skipped and counted under the chart.
- More than 50 bars or 6 series keep the largest and sum the rest into “others”.
- A result without rows or numbers, or with a single point, says why instead of drawing.
- A result with pending or applied changes draws nothing until the query runs again: see Editing data.
Keys
| Keys | What they do |
|---|---|
v / V, 1 2 3 | The kind: bars, horizontal bars, line |
x | Choose the X column |
s | Choose the value columns |
b | A column that splits the values into series |
S | Logarithmic scale on or off |
arrows, h j k l | Move a cursor over the bars or points, showing the exact values and their row |
Enter | Show that row in the grid |
y / Y | Copy the chart’s numbers as TSV / the drawing as text |
c | Back to the rows |
The mouse works too: a click picks a point, a kind or a column choice.
The series’ colours are the theme tokens chart_1 to chart_6 (see
theme files).
Not supported: writing the SQL of a chart into a console.
Editing data
The rows of a result can be changed in the grid: a cell’s value, new rows, rows to delete. Nothing is sent while you edit. The changes stay pending on the result until you look at the statements that apply them and say yes; then they run in one go, each row checked first.
What can be edited
The first key that changes a cell or a row reads the table’s details fresh (the status bar
says “Loading the table’s details…”) and then does what was pressed. From then on the result tab
strip says what the rows are to editing: editable · shop.users · key id (the key that finds a
row), rows matched by values (no key in the result: a row is found by the values it shows), or
read-only · <reason> for a reason about the statement, the table or finding the rows. The
other reasons below, the connection’s among them, are said by the key only.
A result is editable when its statement is one SELECT of one plain table, its connection is
open and may write, and its rows can be found again. A pressed key that cannot do its work says
“Read-only: …” or “Can’t edit: …” and changes nothing. The reasons are:
- The statement. It is not a single
SELECT; it usesWITH; it reads more than one table (a join or a comma inFROM); it has a subquery in the select list or inFROM; it combines results (UNION,INTERSECT,EXCEPT); it groups rows (GROUP BY,HAVING,DISTINCT,DISTINCT ON); it uses a window function;FROMnames no plain table (a function,LATERAL,VALUES, a sample) or there is none; its columns do not come from the table named inFROM; it reads a view. - The table. Its columns are not known yet (the keys are still being read, or could not be read), or it is not known at all (created outside the app: the explorer’s refresh reads it again); no column of the result is a column of the table; the rows are not of a plain table (a view, a materialised view or a foreign table); the table changed since the query ran; its details could not be read.
- Finding the rows. No column of the result compares exactly, so its rows can’t be found again (see How a row is found).
- A selection. Its cells are of more than one column (“Select cells of one column to edit
them together”); it holds both new rows and rows read (“Select new rows or rows read, not both,
to edit them together”);
Don whole columns (“Select rows (V) to delete them, not whole columns”). - The connection. The tab’s connection is not open; the connection’s environment is read-only (or was when the tab’s session opened); this connection type can’t edit data; the connection changed since these rows were read (run the query again).
- A column. It is computed in the query, not a column of the table; it is generated by the
database; it is
GENERATED ALWAYS AS IDENTITY(it takes no new value in a row read, but a new row may give one); it is shown more than once. - A value. A row is not shown whole, so it can’t be found again; a column’s values (or one
value) are not shown as stored, for example text read in another character set than
utf8mb4or a MySQLTIMESTAMPin a time zone that is not a fixed offset; a MySQLFLOATcolumn is shown rounded, so its selected cells are not typed together (one at a time is); a row could not be read; a row is marked for deletion; no column of the result takes a value in a new row. - While applying or after. The pending changes are being applied; changes were applied to these rows (re-run the query to edit them); the table’s details are no longer known while changes are pending (run the query again).
Keys
In the grid:
| Keys | What it does |
|---|---|
e (Space d e) | Edit the cell, or the selected cells of one column |
Space d n | Set the cell (the selected cells) to NULL |
Space d D | Set the cell (the selected cells) to the column’s default |
o (Space d o) | Add a new row, every value its column’s default |
O (Space d O) | Add a new row with the values of the cursor’s row |
D (Space d d) | Mark the row (the selected rows) to be deleted, or unmark it; a new row is removed |
Space d r | Drop the pending changes of the cell (the selection) |
u (Space d u), Ctrl+R (Space d U) | Undo, redo |
Space d p (:changes) | Show the pending changes in the Changes tab |
Space d a (:apply) | Apply them, once asked on the status line (in a dialog when one is needed) |
Space d x (:discard) | Drop every pending change of the result (it asks first) |
Your terminal’s paste (Cmd+V, Ctrl+Shift+V) | Paste a range at the cursor |
Space d v | Paste the system clipboard at the cursor (with clipboard = "system", or "auto" outside SSH) |
Space d V | Read the last paste again as the next format, in its place |
The full lists are in the keybindings reference: the grid,
the Space d group under nav, the
cell editor, the
editor of several lines, the
Changes tab, the
status line question and the
Apply dialog.
While changes are pending the strip shows 3 pending and a Changes tab, and the hint line
offers Space d a apply and u undo. A pending change is a row: a row read with changed cells
(two cells of one row are one change), a new row or a row to be deleted. The row number’s
gutter marks them ~, + and -, or ! when an Apply is refused for the row. Changed cells
take their own colour, new rows another on a background of their own, rows to be deleted are
crossed out, and a new row’s cell left to its default shows what the server will put there, or
! value needed where the column has none (see New rows). The theme tokens are
edit_changed, edit_inserted, edit_inserted_row, edit_deleted, and edit_pending for the
count (see Theme files).
With the mouse
A right click on a cell selects it and opens the action menu; inside a selected range it keeps
the range, and the menu is the selection’s (Space Space opens the same one next to the
selection). Its first section lists the edits after the copies, each with its key: edit the
cell, set it to NULL or to the column’s default, add a row, add a copy of the row, mark the row
to be deleted; once changes are pending, undo, showing the changes (the Changes tab) and
dropping them. On a new row it lists only its edits: nothing there reads a row of the result. An item
runs as its key does.
The menu offers the edits whose keys would act: each is checked as its key checks before it
changes anything. An edit that would refuse is shown dimmed, with why in a few words where its
key would be; the keys and the wheel step over it, and a click on it says the reason in full
(as its key does) and keeps the menu open. A result that can’t be edited (a join, a read-only
environment, a connection that is not open, a connection type that can’t edit data) shows that
once, on “Edit the cell” (for example read-only, not connected, not editable), and offers
no other edit but undo, the Changes tab and dropping the changes already pending (none of them
while those are being applied). On a result that can be edited, an edit that does not act on
this cell or selection is dimmed not here: for example setting NULL on a NOT NULL column,
setting the default of a column that has none, editing cells of several columns at once,
editing a column the server computes, deleting whole columns, or changing a cell of a row
marked to be deleted. Adding a row to a table none of whose columns shown takes a value is
dimmed no column to fill. Undo, the Changes tab and dropping the changes are listed only
while changes are pending. Until the first edit key has read the table’s details, what its columns take is not
known yet: the menu then offers NULL and DEFAULT on any column, and the key says why when one
refuses it.
The cell editor
e opens a line over the cell with its pending value, or its value as read. A NULL or a
pending DEFAULT opens as an empty line.
Enterkeeps the text. An empty text is the empty string, never NULL:Ctrl+NandCtrl+Dset NULL and DEFAULT again.Esccancels; so does a click outside the line.Ctrl+Ltakes the text typed so far to the editor of several lines.- With cells of one column selected, each takes the value, as one change (
uundoes it). - An enum or boolean column lists its values instead, then NULL and
DEFAULTwhere the column takes them.
NULL is refused in a NOT NULL column. In a row read, DEFAULT is refused when the column has
no default, and when its default would take a new number (an identity, AUTO_INCREMENT, a
sequence): type a value instead. A new row takes any column’s default.
Values of several lines
A value with line breaks, and a JSON column’s, opens in a box with the query editor’s vim keys
instead (Ctrl+L gets there from the line too). In Normal mode Enter or :w (:wq, :x)
keeps the text, Esc or :q (:q!) cancels; / searches and :s substitutes as in the
query editor. A value whose line breaks are all \r\n keeps them; one that mixes kinds of line
break is refused (“mixes kinds of line break; it can’t be edited here”). The one-line editor
does not take a paste with line breaks.
Value checks
A typed value must be a value of the column’s type, in the form the grid shows it; it is refused, with the reason, when it would be stored as something else. Some of the reasons:
- “not a whole number”, “out of the range of smallint”, “not a number”, “more digits than numeric(6,2) holds”
- “not true or false”, “not a UUID”, “not one of the column’s values”
- “not a date (YYYY-MM-DD)”, “not a date and time (YYYY-MM-DD HH:MM:SS)”, “more than 3 digits of
a second” (a PostgreSQL value with a time zone needs its offset, such as
+09) - “longer than 20 characters”, “ends in spaces, which a CHAR column drops” (MySQL)
- “not bytes as shown: \x and hexadecimal digits in pairs” (MySQL:
0xand upper-case digits) - “real would store it rounded, as …” (a floating-point value must read back as typed)
What the app cannot judge, such as a check constraint, is left to the server.
New rows
o adds a row with every value its column’s default. O copies the cursor’s row (its pending
values, else as read), except what the table gives a new row itself: the key’s columns, a
number drawn (an identity, AUTO_INCREMENT, a sequence), generated and computed columns are
left to their default. A value that is not shown whole is not copied: the clone is refused,
naming the column.
New rows appear at the end of the result, after the rows read, in the order they were added
(on a new row, o and O put the next one right after it). The cursor goes to the new row.
D on a new row removes it. A new row is tinted as a whole (theme token edit_inserted_row:
its cells, the separators and its number), and the gutter numbers it after the rows read
(4+, 5+ after three rows; rows an Apply added are numbered the same way). The number counts
the rows read so far: when the next page is read, the new rows’ numbers move after it. The
messages name a new row as the Changes tab does, by its place among the new rows (new 1).
A cell left to its column’s default shows, muted, what the server will put there:
| Column | Shown |
|---|---|
a constant default ('ACTIVE'::character varying, 0, '{}'::jsonb) | its value: ACTIVE, 0, {}; a text that reads like one of these marks (empty, NULL, <…>) in quotes |
an identity, AUTO_INCREMENT, a sequence (serial), a generated column | <generated> |
another default (now(), CURRENT_TIMESTAMP) | <default: now()>, a long one cut short |
| no default, takes NULL | NULL |
no default, NOT NULL | ! value needed (below) |
This is only shown: the Apply leaves the cell out of the INSERT, so the server applies its own
default, and the cell editor opens empty. The column widens to fit what it shows, up to the
grid’s widest column. Copies of new rows are refused, so nothing shown here is ever copied.
The Changes tab lists, and an Apply refuses, a new row that leaves a NOT NULL column without
a default to its default (“new 1 · name is NOT NULL and has no default; give it a value”), and
one of a table with such a column that the result does not show (“…is not in the result; add
it to the SELECT”).
The grid shows both where they are. A cell that needs a value reads ! value needed in the
theme token edit_invalid (its column widens to fit it, up to the grid’s widest column), and
the row’s gutter shows ! after its number instead of +. The cell editor opens
saying “Can’t edit: name has no default”, and the inspector gives the Changes tab’s reason. A
column the result does not show has no cell to mark: the row’s gutter shows !, and the Changes
tab names the column. The badge counts both with the invalid values. Typing the values clears
the marks.
Pasting a range
Cells copied from a spreadsheet, another grid or a text file paste into the grid with the
terminal’s own paste (Cmd+V, Ctrl+Shift+V): put the cursor on the top-left cell the range
goes to and paste. The terminal sends the text itself, so the app reads no clipboard: this works
over SSH and in tmux, whatever clipboard is set to. The paste lands as pending changes, like
typed values: marked in the grid, listed in the Changes tab, and nothing is sent until
Apply.
The status line says how the text was read and what it did:
Pasted as TSV · 3 rows × 4 columns · 1 invalid (id=14 · price) · 5 cells changed · 1 new row · u undo
Where the status bar is too narrow for all of it, the counts of cells and new rows go first,
then the invalid cell’s name: the format, the size, how many values are invalid and the undo key
stay (Pasted as TSV · 3 rows × 4 columns · 1 invalid · u undo); at its narrowest the format goes too (Pasted · 3×4 · 1 invalid · u undo).
A first line whose fields are exactly the names of the columns the paste lands in, in order
from the cursor’s column (a copy with its header line, from the app or a spreadsheet), is a
header, not values: it is skipped, and the status line says so first
(Pasted as TSV · Header row skipped · Space d V keeps it · …). Space d V reads the text again
keeping that line, in the paste’s place. A single line is never skipped.
Formats
The text is read as:
- TSV when it has a tab outside quotes (a spreadsheet’s copy),
- else, when it has a comma outside quotes, CSV or one column (one value for a single line): which one is asked, never guessed (below),
- else one column, a value per line, when it has several lines,
- else one value.
Text with commas and no tab can be CSV, or a column whose values hold commas (1,5, Smith, John). The status line asks:
Read as CSV (3 columns) or one column? c / 1 · any other key cancels
c pastes it as CSV, 1 as one column (… or one value? for a single line). Esc, any other
key, a click or another paste cancels (“Paste cancelled”) and does nothing else. The answer then
pastes as any paste does: its refusals, its question about many rows and the status line naming
the format. When only one of the two reads the text, only that key is offered: Not CSV (row 2 has 2 values, row 1 has 1); read as one column? 1. When neither does, the paste is refused
with the CSV reason. The question is for the grid as it was: a change, another tab or a
dialog drops it (“Paste cancelled: the rows changed”). It works the same for the
terminal’s paste and for Space d v.
Both TSV and CSV take quoted fields ("a""b", line breaks inside quotes), as spreadsheets write
cells that hold tabs, line breaks or quotes. A quote in the middle of a field that does not start
with one is part of the text. Line ends may be \n or \r\n; a carriage return alone outside
quotes is refused. Empty lines inside the block are rows of empty values. Every row must have as
many values as the first: row 3 has 4 values, row 1 has 3 is refused, never padded or shifted.
When the text was read the wrong way, Space d V reads the last paste again as the next format, TSV → CSV → one value → TSV: it undoes
the paste and pastes the same text again at the same cursor and selection. It works while that
paste is the newest change, again after undoing a later one; otherwise it says “Nothing to
re-read: the last change was not a paste”. When the new reading is refused, or its question
about many rows is cancelled, the paste stays as it was and the next Space d V tries the
format after that one. Text that can’t be read at all (a quote never closed, rows of different widths, a lone carriage
return) names Space d V and the format it tries next.
Where it goes
Columns go by the grid’s column order from the cursor’s column; rows go down the grid’s rows: the rows read, then the new rows.
| Clipboard | Selection | Result |
|---|---|---|
| one value | one cell | sets that cell |
| one value | several cells | sets every selected cell (each checked for its own column) |
| a block | the cursor | the block from the cursor |
| a block | several cells | the block from the selection’s top-left corner; the selection’s size does not matter, and the status line says pasted 3×4 at the selection's corner |
Rows past the last row become new rows at the end of the result, as o adds them: the
columns outside the pasted range are left to their default, and the key’s columns, identities
and AUTO_INCREMENT columns too unless the range sets them. A value equal to the cell’s value
as read leaves the cell unchanged; if the cell had a pending edit, that edit is dropped. The cursor goes to the range’s first cell.
When a paste is refused
A paste that does not fit is refused as a whole, saying why, and nothing changes:
- wider than the columns from the cursor to the last one: “the paste is 5 columns wide; 3 columns from the cursor to the last column”
- a column in the range that takes no value (computed in the query, generated, an identity that is always generated, a column the result has twice)
- a row in the range marked for deletion: “row 12 is marked for deletion”
- rows past the last row read while the server has more rows: “the result has more rows on the server; fetch them (n) or paste fewer rows”. Otherwise rows meant to overwrite the rows not fetched yet would become new rows. When the result’s cursor is no longer open (the next page runs the query again, or there is none), it says “the result has more rows on the server, not read here; paste fewer rows”. One value filling a selection adds no row, so it is not refused for this.
- a row in the range that can’t be found again (its values are not shown whole)
- rows past the end where no row can be added: no column of the result takes a value in a new
row, or the table has a
NOT NULLcolumn without a default that the result does not show - a result that can’t be edited, or whose changes are being applied
- more than 5,000 rows or 1 MiB of text
In the Changes tab, or with the messages, a plan or a chart shown, a paste says “Paste into the grid”. With the cell editor open, a paste goes into the editor as before (the one-line editor takes no line breaks).
A paste that changes more than 1,000 rows (rows with a value set to another one, and new
rows) asks first on the status line: Paste 2,400 rows (1,900 new)? y paste · any other key cancels. Any other key, a click or
another paste cancels it and does nothing else.
NULL and empty values
An unquoted field that is exactly \N is NULL, the form both servers’ own dumps write NULL in.
The setting edit.paste_null (:set edit.paste_null=NULL, or the settings screen) makes the
word NULL the token instead, or none for no token. A quoted "\N" is always the text.
An empty field is an empty value, never NULL. For a text column that is the empty string; a
column of another type does not take it, so it is marked invalid with “empty value (paste \N
for NULL)”. A paste never gives DEFAULT: the text DEFAULT is the text.
Invalid values
Every pasted value is checked as a typed one is (see Value checks), and NULL
against NOT NULL. Nothing is converted: a value its column does not take is kept, so you see
it, and marked:
- the row’s gutter shows
!(over~and+), the cell is in the theme tokenedit_invalid, its value still shown, also under the cursor; the gutter’s!also marks a new row an Apply is refused for (see New rows) - the badge counts them:
5 pending · 1 invalid - the Changes tab lists each under its row, with the reason: “price: not a number: ‘abc’” (elsewhere with the row named: “id=14 · price: not a number: ‘abc’”)
- the changes are not applied while any value is invalid (Apply shows the Changes tab instead)
Fix a value with e: the editor opens with the pasted text and says why it is not valid (“Not
valid: not a number: ‘abc’”). Or
drop it with Space d r, or undo the paste.
Undo
A paste is one change: u undoes all of it, the new rows too, and Ctrl+R redoes it.
Space d v and the clipboard setting
Space d v reads the system clipboard and pastes its text at the cursor, the same way. It reads
the clipboard only when you press it, and only with clipboard = "auto" (outside SSH) or
"system"; with clipboard = "osc52" (and auto over SSH) it says “Reading the clipboard needs
clipboard = “system”; paste with your terminal instead“. A clipboard that does not answer within
2 seconds pastes nothing.
A terminal without bracketed paste sends a paste as typed keys instead: they act as keys in the
grid. Use Space d v there, where a system clipboard exists.
Pasting the app’s own copies
The grid’s own TSV and CSV copies (Space r y) paste back as the same cells, with three
exceptions: a text cell that is exactly the NULL token (\N by default) pastes back as NULL; NULL is copied as an empty
field, so it pastes back as an empty value; and in a copy of one column, a last cell that is
NULL (in a TSV copy, an empty text too) is lost, since the copy does not end its last line.
To move cells within the app without these exceptions, use y and g p
(below).
Copy and paste inside the grid
y (a cell, or the selected range) and Y (the row, or the selected rows) copy to the
clipboard as before, and the app also keeps the cells themselves: each value exactly as the
grid shows it, a pending change included, NULL as NULL, and never a header line. They stay,
for this result or any other, in any tab, until you copy again or quit. No clipboard is read or
written for them, so this works over SSH and in tmux, whatever clipboard and
[editor] clipboard are set to. Two keys paste them:
p(Space d Y) adds them as new rows after the last row, wherever the cursor is. Eachpadds another copy, one stepuundoes each.- Copied from this result’s table, each value goes to its own column, never by position: a
column not copied is left to its default (one that needs a value is marked), a column the
table computes or numbers itself (
GENERATED,GENERATED ALWAYS AS IDENTITY) is left to the server (“… · 1 generated column left to the server”), and the paste is refused when a copied column is not in this result, is copied twice, or is computed or of another table (a join): it names the column. - Copied from another table, or from a result of computed columns only, the values go by position from the first column, each checked like a typed value, and a copy wider than the result is refused.
Ythenpduplicates rows with their key values as they are: a serial orBY DEFAULTidentity key then repeats, so set it to its default (Space d D) before Apply.- A copy from another connection or database goes by position.
- Copied from this result’s table, each value goes to its own column, never by position: a
column not copied is left to its default (one that needs a value is marked), a column the
table computes or numbers itself (
g p(Space d y) pastes them over the cells at the cursor, the way a pasted range does: from the cursor (one cell copied fills every selected cell), and rows past the last one are new rows.
Both are refused the way a paste is (a read-only result, more rows on the server, a table with
a required column the result does not show, …), are one step u undoes, and ask first when
more than 1,000 rows change. Nothing is read as text, so there is no question about commas, and
Space d V has nothing to read again.
Pasted the copied cells as new rows · 2 rows × 2 columns · 2 new rows · u undo
Pasted the copied cells · 2 rows × 2 columns · 2 cells changed · 1 new row · u undo
Each value is checked against the column it lands in, as a typed one: a text pasted into a number column is kept and marked invalid, and the changes are not applied until it is fixed.
What is not kept says why, and the copy to the clipboard is done (or fails) anyway:
- more than 5,000 rows, the most a paste takes (“Not kept for p: more than 5,000 rows, the most a paste takes”);
- more than 32 MiB of values (“Not kept for p: more than 32 MiB of values”);
- a row not read whole;
- a MySQL value the grid does not show as stored: a
FLOAT(the server sends it rounded), bytes or bits not in their form, text read in a character set other thanutf8mb4, aTIMESTAMPread in a time zone without a fixed offset (one with daylight saving, orSYSTEMwhen the server’s own zone is not known), and text or aTIMESTAMPof a result whose session’s character set and time zone are not known, or changed while its rows were read.
A copy the grid refuses (a pending DEFAULT, a value only a run shows, a new row) or asks about
keeps nothing either, so g p never pastes an older copy; with nothing kept, it says which keys
copy.
A MySQL TIMESTAMP is shown in the session’s time zone, without an offset, so the same text is
another instant in another zone. Its cells keep the fixed offset they were read in (SYSTEM is
the server’s own zone), and g p pastes them into a time column with a zone only when the
instant stays the same; else it says why and pastes nothing:
- a MySQL
TIMESTAMPof a result read in another offset (“Not pasted: column “created_at” was copied in time zone +09:00; this result was read in +00:00”), in a zone without a fixed offset, inSYSTEMwhen the server’s own zone is not known, in a session whose zone is not known, or one whose zone changed while its rows were read: set the session’stime_zoneto the copy’s and run the query again; - a PostgreSQL
timestamptz, which reads such text in its session’s zone.
A TIMESTAMP value pending in a result read in a zone without a fixed offset, in a zone not
known, or in a session whose zone changed while its rows were read is not kept either. Into a column without a zone (DATETIME, timestamp, text) the text pastes as
it was shown.
Undo, revert, discard
uundoes the last change of the result andCtrl+Rredoes it. A change of several selected cells, a delete of several rows, a paste and a discard are one step each.Space d rdrops the pending changes of the cell, or of the selection: a value goes back to the value as read, a row marked to be deleted is no longer marked, and a new row’s cell goes back to its default (a cloned row’s too, not to the copied value).Space d x(:discard) asks “Drop the pending changes?”:ydrops them,norEnterkeeps them.
The Changes tab
While the shown result has pending changes, the result tab strip has a Changes tab after its
result tabs: Changes 3, or Chg3 where the strip is narrow. A narrow strip keeps it while it
fits next to the shown tab and Messages. Space d p (:changes) shows it with the focus; H
and L (Space r [ and Space r ]) reach it as any result tab. It shows the changes of the
result last shown in the grid. After an Apply it stays, as Applied (Appl), to show what was
applied (see After an Apply). A plain Changes stays after the applied
changes were rolled back, and after an Apply whose result is not known once its pending changes
are dropped. It goes once nothing is pending and nothing was applied, or when the query runs
again. When another result of the tab
has pending changes too, a line says so: Result 2 also has 1 pending.
Its first line names the result and the table, and says how many updates, inserts, deletes and problems there are. Then one line per change, deletes first, then updates (a line per changed cell), then new rows, each row’s problems under it:
Changes: Result 1 · shop.users 2 updates · 2 inserts · 1 delete · 1 problem
- id=2 delete
~ id=1 email 'a@x' → 'z@x'
~ id=3 nickname 'cy' → NULL
+ new 1 email='n@x' · DEFAULT ×2
+ new 2 DEFAULT ×3
! email is NOT NULL and has no default; give it a value
- The marks are the gutter’s:
~a changed cell,+a new row,-a row to be deleted, in their colours (edit_changed,edit_inserted,edit_deleted); they read without colours too. - A row found by its key is named by the key’s values (
id=2,a=1,b=2); a row found by its values by its row in the grid,row 31 (located by its values); a new row by its place among the new rows,new 1. A row is named the same way wherever a problem is about it: in the statements, the Apply’s messages, the cell editor and the inspector (new 1 · email is NOT NULL …). - Values are literals as they were read and as they are to be (
'text',NULL,DEFAULT), a long text cut at 40 characters with…. A new row lists the values set, then how many of its columns are left to their default (DEFAULT ×2, left out when none is); a column left to a default it lacks is named in the problem under the row. A line longer than the pane is cut with…. - Control characters in values, column names and the table’s name are spelled out:
\n,\r,\t,\x07(\u{85}for one above\x7f), and so are the characters that turn the direction of text or are not seen (\u{202e},\u{200b}). A value cut short is cut before an escape, never in it. A backslash is doubled where what follows it would read as one of these ('a\\nb'is the texta\nb), and shown as it is elsewhere ('a\b'). This is only how the list shows them: the statements, and whatccopies, carry the values as they are. - Every reason the changes can’t be applied yet is marked
!, for every row (not only the first): under the row’s lines, without its name, for a new row’sNOT NULLcolumn without a default, a row that matches no row or several rows; first, for what is about no row, such as a count that failed. A row that matches several identical rows is listed too: which of them to change is asked when you apply.
j and k select a line, Ctrl+D and Ctrl+U move half a page, g g and G go to the first
and the last. Enter shows the line’s row and column in the grid (a row the grid no longer has
is said). u and Ctrl+R undo and redo, and the Space d keys work as in the grid: an edit of
a cell goes to the grid at the selected line’s cell first. Under the list the tab says how the
Apply stands to a transaction (as the Apply dialog’s footer does), and its keys where they fit.
The SQL view
s switches between the list and the statements that apply the changes, for the rest of the
run. ui.changes_view = "sql" (:set ui.changes_view=sql, or the settings screen) opens the tab
in the statements; "list" is the default. The statements are shown as the Apply dialog shows
them: in the order they run, in the editor’s syntax colours, each with the count it must give
(expect 1) at the end of its first line, a statement longer than the pane wrapped under its
first line.
On MySQL a text value is compared by its UTF-8 bytes, so the statement holds it as
_utf8mb4 X'…'. A statement with such values is followed by a dim line that starts with ↳
and reads them back in order, column = 'text' (a long text cut with …; bytes that are not
UTF-8 text say how many there are), grouped by where they are: set for the values an update
writes, where for those a row is found by, values for those an insert adds. An update can
name a column in both, its new value and the one it is found by:
UPDATE `shop`.`people` SET `note` = _utf8mb4 X'615C62' WHERE expect 1
`id` <=> 1 AND `name` = _utf8mb4 X'706C61696E206E616D65' AND
CONVERT(`name` USING utf8mb4) COLLATE utf8mb4_0900_bin <=> _utf8mb4
X'706C61696E206E616D65' AND `note` = _utf8mb4 X'6F6C64' AND
CONVERT(`note` USING utf8mb4) COLLATE utf8mb4_0900_bin <=> _utf8mb4
X'6F6C64'
↳ set note = 'a\b' · where name = 'plain name', note = 'old'
That line is not SQL: it is never sent to the server, and c copies the statements exactly as
they are sent, without it. A literal longer than a few hundred bytes is also shown shortened, its
start and end around how many characters are left out (…786400 more characters…); it is copied and sent
whole.
- Row checks (lock, no waiting) first: for every row read that changes, a
SELECT … FOR UPDATE NOWAITthat finds and locks it. A row another session holds fails at once instead of waiting. - Writes: the deletes, then the updates, then the inserts. A
DELETEand a statement that expects more than one row are marked!, their count in the warning colour.
c copies every statement exactly as it is sent, each followed by ; and a line break (through
the clipboard setting), in both views. While the statements are not final it copies nothing
and says why: “Not copied: the SQL is not final yet: 2 problems”, or “…: the rows are still
being counted”.
Rows found by their values are counted on the tab’s own session while the tab is shown, half a second after the last change, once for each state of the changes (“Counting matching rows…” until the answers come; the count sees the tab’s open transaction, as the Apply does). The list needs no count.
Apply
Space d a (:apply, or “Apply the pending changes (asked first)” in the action menu) works from the grid
and from the Changes tab. The rows found by their values are counted afresh first, every time,
whatever the Changes tab counted before; the count sees the tab’s open transaction, as the
Apply does. The status bar says Checking 3 rows… until the answers come. Meanwhile any key
(Esc too), a click, the wheel or a paste stops waiting (“Apply cancelled”) and does nothing
else, so none of them is taken by the question that follows; a dialog that opens or a run in the
tab cancels it too. Changes that move before the answers come drop the Apply: “Changes changed:
press Space d a again”.
When the changes can’t be applied yet, nothing is asked: the Changes tab takes the focus with the first problem selected, and the status bar says “Can’t apply: 2 problems (see Changes)”. The problems are those of the list above, for instance:
- “row 3 · matches no row of the table now: it was changed or deleted. Re-run the query.”
- “row 3 · matches 2 rows; add the key to the SELECT to edit it”: the rows may differ in what the result does not show.
- “row 3 · email is not shown whole, so the row can’t be found again”, and the
NOT NULLrefusals of new rows above.
Otherwise the status bar asks, on one line in the warning color:
Apply 3 changes to shop.users (2 updates · 1 delete)? y apply · any other key cancels
Only y applies. Any other key (Enter, Esc, n, any letter), a click or a paste cancels
(“Apply cancelled”) and does nothing else: it never reaches the grid. Where the window is narrow
the question leaves out the table, then the kinds but the deletes, and at its shortest is
Apply 3 changes (1 delete)? y/n, the deletes always named. With
manual commit the question also says how the Apply stands to a transaction
(“Opens a transaction; nothing is committed.”, or “Runs inside the open transaction; nothing is
committed.”); the shortest form says it too: Apply 3 changes (1 delete), nothing committed? y/n
or …, in the open transaction? y/n. The question is for the changes as they were asked about: a change, another tab,
a run, a transaction that begins or ends, or a lost connection drops it (“Apply cancelled: the
changes changed”).
When a question on the status line (this one, a paste of many rows, CSV or one column) or the
wait for the counts ends without its answer, cancelled by a key or dropped because something
moved, the keys that answer questions (y, n, c, 1, on a Korean layout too) do nothing in
the results for about a second, and say so: “Apply cancelled: the changes changed ·
y ignored”, or “Apply cancelled · y ignored”. A y pressed just as the question went never copies
a cell over the clipboard. Other keys work as usual.
The Apply dialog
A dialog, “Apply changes: shop.users”, takes the place of the question when the statements
must be seen before y:
- a row matches several identical rows: “row 3 · matches 2 identical rows”, with “[1] just one
[a] all 2”. The table has no key, the result shows every column, each comparing exactly, and
the rows are the same in all of them.
1changes one of them,aall of them (the write then expects 2). - the last Apply’s result is unknown (below): the first
ywarns, a secondyapplies again. - the profile’s environment asks before every write (
confirm = "writes"): the dialog is that question, no second one opens.
Cancel has the focus: only y applies. The dialog shows how many updates, inserts and deletes,
then the statements as the SQL view shows them; j and k scroll line by line, c copies them,
Enter, Esc or q closes. It closes by itself when its result, its rows’ connection or the
changes change. Its footer says how the Apply stands to a transaction:
- “Runs in one transaction; commits at the end.” (automatic commit)
- “Opens a transaction; nothing is committed.” (manual commit, none open)
- “Runs inside the open transaction; nothing is committed.” (your own
BEGIN, or a manual commit transaction already open)
How a row is found
A row read is found by the table’s primary key, or a NOT NULL unique key, when the result
shows all its columns and each of them compares exactly; else by the values of its columns that
compare exactly. A table others inherit from is always found by values. Floating-point numbers,
JSON, XML, geometry and types the app does not know do not compare exactly, nor (PostgreSQL)
arrays, intervals, a numeric without a scale, times with a time zone and network addresses,
nor (MySQL) text in a character set that does not convert one to one. Values are compared byte
for byte, never by the column’s collation. An update also checks that each changed column that
compares exactly still holds the value read, and a delete every column that compares exactly: a
row changed elsewhere in those columns since is not overwritten.
While it runs
y sends every statement on the tab’s own session. While it runs the strip (in the grid view) says applying…,
the Changes tab says “Applying 3 changes…” over the list (or the statements) being sent, edit keys are
refused, and the query’s cancel key (Ctrl+C) cancels it. When the result’s pages are held
open, the cursor is closed first; the rows already read stay.
Either every statement gives its count and the changes are applied (“Applied 3 changes”), or
nothing is (“Nothing was applied”, the changes stay pending and may be applied again). Why is said
on the Changes tab’s second line, and in its SQL view the statement that failed is marked ! and
scrolled to. A row is named as the Changes tab names it (id=5, row 3, new 1), and a
statement in a message is its first line, cut short when long:
- “id=5 · was changed or deleted since it was read. Nothing was applied. Re-run the query to see the current values.”
- “row 3 · now matches 2 rows, not 1. Nothing was applied.”
- “id=5 · is locked by another session. Nothing was applied.”
- “new 1 · <the server’s error>. Nothing was applied. Statement:
” - “Apply cancelled; nothing was applied.”
- “Apply result unknown: the connection was lost before the server confirmed. Re-run the query
to check.” The changes may be in the table already: the next Apply opens the dialog, whose
first
ywarns, and only a secondyapplies them again.
After an Apply
The result shows what the Apply did over the rows as read, until the query runs again: written
values in italic (theme token edit_applied), deleted rows crossed out, the rows added after the
rows read. The strip says applied · re-run to refresh (applied · not committed while its
transaction is open; applied and in open tx where the strip is narrow). These rows are not edited again: run the query first.
The Changes tab stays as Applied, until the query runs again: its list is what was applied,
its SQL view the statements that were sent (c still copies them), and its second line says
“Applied · re-run to refresh”, or “Applied · not committed” while the transaction is open.
- PostgreSQL returns each written row as stored, so a value a trigger set shows too. A value in
a form the grid shows differently (dates and times, floats, intervals, bytea, network
addresses, most arrays) shows a dim
re-run to see. - MySQL returns no rows: a new row’s
AUTO_INCREMENTcolumn shows the number it took, other defaults staydefault. - Paging on past the rows already read is refused (“Changes were applied to these rows: re-run the query to see the rest”): the rows the server would skip have moved.
With manual commit, or inside your own transaction, the Apply is not committed (“Applied 3
changes; not committed yet”): the status bar
counts it in the open transaction, and :tx lists it as “Apply (3 changes)”. A commit keeps the
values shown; a rollback drops them (“The transaction was rolled back; the applied changes are
gone”), and the Changes tab says so until the query runs again. See
Manual commit.
Leaving with changes pending
Running a statement in the tab (its results are replaced), closing the tab and quitting ask
first when changes are pending, saying how many: y drops them and goes on, n or Enter
keeps them and does nothing. Changes already applied do not ask; an open transaction asks as
usual. Deleting a connection profile or a saved query closes its tabs without another question:
its own question says how many pending changes go (“2 pending changes of the results are
dropped.”).
Copy and search
Copying (y, Y, the Space r y group, :copy), the search (/, ?), the cell viewer
(Enter) and the inspector take the values the grid shows: a pending value or what an Apply
wrote, not the value read. A copy with pending values says “(with pending changes)”, and counts
the copied rows marked to be deleted, which are copied as shown.
- A pending DEFAULT has no value yet: a copy of it is refused, except as SQL
INSERT, which writesDEFAULTthere. A cell showingre-run to seeis not copied, nor is one an Apply set to its default, showndefault(“Not copied: nickname shows “default”; run the query again to copy its value”). - New rows are not copied (“A new row is not copied: it is not a row of the result yet”) and the search does not look at them.
Export and the chart
An export to a file and the chart do not take the values the grid shows over the rows:
- An export of the rows held (the selection, the fetched rows, or every row of a result fetched to its end) is refused while the result has pending or applied changes (“Not exported: this result has pending changes; apply or discard them, or run the query again (see Changes)”; with changes applied, “Not exported: this result has pending or applied changes; apply or discard them, or run the query again”), in every format.
- An export of every row of a result the server has more of reads the rows again from the server, so it holds what an Apply wrote. With changes pending it goes on, and says “The pending changes are not in the file: every row is read again from the server”.
- The chart draws nothing while the result has pending or applied changes: it says “Not drawn: this result has pending changes. Apply or discard them, or run the query again (see Changes)”, or, once they are applied, “Not drawn: this result has pending or applied changes. Run the query again to chart the edited values.”
Plans
datarig shows a statement’s query plan as a tree and as several other views. It works on PostgreSQL and on MySQL 8.0.16 and later; not on MariaDB.
Getting a plan
| Keys | Command | PostgreSQL | MySQL |
|---|---|---|---|
Space e x | :explain | EXPLAIN (FORMAT JSON) | EXPLAIN FORMAT=TREE |
Space e a | :explain analyze | EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) | EXPLAIN ANALYZE FORMAT=TREE (8.0.18 and later) |
Both work on the statement under the cursor, through the same checks as any run. An
EXPLAIN (FORMAT JSON …) on PostgreSQL, or an EXPLAIN ANALYZE or EXPLAIN FORMAT=TREE on
MySQL, that you type yourself shows as a plan the same way.
ANALYZE runs the statement
- PostgreSQL: a write under
ANALYZEis rolled back, and a read-only profile refuses it. - MySQL: a write runs in a transaction of its own that is rolled back (inside your open
transaction, under a savepoint rolled back to). A read-only profile refuses it for a write or
a call of a function that is not built in. What a rollback cannot undo is not run:
EXPLAIN ANALYZEof a write that names a table of another engine than InnoDB (MyISAM, MEMORY, …; NDB is fine), a view, or a table with triggers is refused, saying which; so is one whose table’s triggers your account cannot see (without theTRIGGERprivilege everyEXPLAIN ANALYZEof a write is refused, andEXPLAINwithoutANALYZEstill shows the plan). A call of a function that is not built in asks first.
A text EXPLAIN you ran
A text EXPLAIN stays rows, with a line under them. P in the results (or Space e p)
plans that same statement again as a plan: with FORMAT JSON on PostgreSQL, keeping its other
options; with EXPLAIN FORMAT=TREE (never with ANALYZE) on MySQL. It never happens by itself.
- It goes without a question only when the wrapped statement is a plain
SELECTthe app would also run again for a next page (the same allowlist, its server check included: no view, no user operator or overloaded name). Like that allowlist it trusts functions to be labelled honestly, so a function wrongly declaredIMMUTABLEorSTABLEcan still run while the statement is planned. - Anything else asks first, saying what runs again (
ANALYZEruns the statement, rolled back; anEXECUTE’s parameters or what a view hides are evaluated to plan it, not rolled back). - It is refused when the tab ran other statements since, when the result came with other statements, or when the tab’s connection, database or session changed.
- On MySQL,
INTO @v,FOR CONNECTION, anEXPLAINof a table and an executable comment are refused.
Reading the plan
The Plan tab shows the plan as a tree with each node’s estimated and actual rows (and loops), its self and total time, a bar of its share of the whole coloured by heat, and its buffers.
- Nodes that take a fifth of the time or more are marked hot.
- An estimate ten times off or more is marked with how far and which way. Fewer rows than
estimated under a
LIMIT, or on the inner side of a semi or anti join, is no misestimate: the node was stopped early. - Without
ANALYZEthe numbers are the planner’s estimated costs, and the tab says so. Entershows the selected node’s detail: conditions, filters and the rows they removed, buffers, workers, loops, output.handlclose and open its children.
╭ Explorer ────────────────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ───────────────────────────────────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-pg ││ 1▎SELECT * FROM t; ✓ plan · 3ms · 14:03 │
│ ▾ datarig default ││ │
│ ▸ analytics ││ │
│ ▸ public ││ │
│ ▸ shop ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ │╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
│ │╭ Plan · Tree ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ││ Result 1 [Plan] Chart Messages H/L │
│ ││ [Tree] Summary Icicle Flame Timeline Row flow Treemap Boxes … execution 133.0 ms · planning 7.803 ms │
│ ││ Node est rows rows ×loops self total share hit/read│
│ ││ ▾ Finalize HashAggregate 1000 1000 11.3 ms 132.6 ms ▊ 8% 1274/0│
│ ││ └─▾ Gather hot 2400 3000 40.6 ms 121.3 ms ██▊ 30% 1274/0│
│ ││ └─▾ Partial HashAggregate hot 1000 1000 ×3 54.7 ms 80.7 ms ███▊ 41% 1274/0│
│ ││ └── Parallel Seq Scan on zz_orders 77.4k 61.9k ×3 26.0 ms 26.0 ms █▊ 19% 1274/0│
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
╰──────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
NORMAL local-pg │ 1 row · 3ms │ Enter detail · v views · h fold · y copy · Esc back · : commands · Space ? help · Space more EN
Views
v and V cycle the views; the digits pick one.
| Key | View |
|---|---|
1 | The tree |
2 | Summary cards (the slowest node and its share, the worst row estimate, disk reads and the cache hit ratio, planning and execution time) above a compact tree |
3 | An icicle (each node as wide as its share of its parent’s time) |
4 | A flame graph |
5 | A timeline: each node’s time to its first and its last row, per loop (needs ANALYZE; else the estimated startup and total cost, said so) |
6 | The row flow: the rows each node passes up as a band, thick by their number, with the rows its filters removed |
7 | A treemap (area = each node’s own time) |
8 | A box diagram (boxes joined by lines; < and > and the wheel move around a large plan) |
9 | Raw: the plan as PostgreSQL’s text EXPLAIN writes it (written from the JSON, nothing run again), or MySQL’s own text |
The summary view (2) of a PostgreSQL plan:
╭ Explorer ────────────────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ───────────────────────────────────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-pg ││ 1▎SELECT * FROM t; ✓ plan · 3ms · 14:03 │
│ ▾ datarig default ││ │
│ ▸ analytics ││ │
│ ▸ public ││ │
│ ▸ shop ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ │╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
│ │╭ Plan · Summary ──────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ││ Result 1 [Plan] Chart Messages H/L │
│ ││ Tree [Summary] Icicle Flame Timeline Row flow Treemap Boxes … execution 86.6 ms · planning 0.785 ms │
│ ││╭ Slowest node ──────────────────────────────────────────╮ ╭ Worst row estimate ────────────────────────────────────╮ │
│ │││ HashAggregate │ │ none: every estimate within ×10 │ │
│ │││ 47% of the whole · 41.2 ms itself │ │ │ │
│ ││╰────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────╯ │
│ ││╭ Disk reads ────────────────────────────────────────────╮ ╭ Planning and execution ────────────────────────────────╮ │
│ │││ 0 blocks read · cache hit 100% │ │ planning 0.785 ms · execution 86.6 ms │ │
│ │││ │ │ │ │
│ ││╰────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────╯ │
│ ││ Node share │
│ ││ ▾ Sort ▏ <1%│
│ ││ └─▾ HashAggregate hot ████▎ 47%│
│ ││ └─▾ Hash Join hot ██▋ 28%│
│ ││ ├── Seq Scan on zz_orders o hot ██▏ 23%│
│ ││ └─▾ Hash ▏ <1%│
│ ││ └── Seq Scan on zz_customers c ▏ <1%│
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
╰──────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
NORMAL local-pg │ 1 row · 3ms │ Enter detail · v views · h fold · y copy · Esc back · : commands · Space ? help · Space more EN
y copies the plan as the raw view shows it, Y as the server sent it (its JSON, or its
text). Large plans stay readable and fast: names are cut, layouts are bounded, and only what is
on screen is drawn.
On MySQL
The plan is the server’s tree. The raw view and both copies are the server’s text as it came;
there are no buffers and no planning or execution time (the measured total is shown), and a
node the server gives no cost (a sort, a subquery’s head) shows its children’s together marked
≈, never as a number of the server’s.
╭ Explorer ──────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ─────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-my / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-my ││ 1 SELECT 1; │
│ Loading… ││ 2▎SELECT c.city, SUM(o.amount) FROM c JOIN o ON o.c_id=c.id WHERE c.city IN ('city1','│
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ │╰────────────────────────────────────────────────────────────────────────────────────────╯
│ │╭ Plan · Tree ───────────────────────────────────────────────────────────────────────────╮
│ ││ Result 1 [Plan] Chart Messages │
│ ││ [Tree] Summary … estimated cost, not time (no ANALYZE) · ≈ = sum of its children │
│ ││ Node est rows self cost total cost share │
│ ││ ▾ Sort ≈0 ≈273 0%│
│ ││ └─▾ Stream results 7 0 273 0%│
│ ││ └─▾ Group aggregate hot 7 58 273 █▉ 21%│
│ ││ └─▾ Nested loop inner join hot 580 200 215 ██████▋ 73%│
│ ││ ├─▾ Filter 58 0 12 0%│
│ ││ │ └── Covering index range scan on… 58 12 12 ▍ 4%│
│ ││ └── Index lookup on o using c_id (… 10 2.5 2.5 ▏ <1%│
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
╰────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────────────╯
NORMAL local-my │ 1 row · 3ms │ i type · Ctrl+E run · Tab next pane · : commands · Space ? help · Space more EN
Not supported
- Plans on MariaDB (its plans are shaped otherwise): the plan actions are not offered there,
and
EXPLAINruns and shows its text. - Comparing two plans of a statement (before and after a change).
Server monitor
The Monitor tab shows what a connection’s server says about its load and its sessions:
PostgreSQL 12 or later and MySQL 8.0.17 or later (Amazon Aurora MySQL from version 3),
self-managed or a managed service. On an older server, or on MariaDB, the tab says there is no
server monitor there and why; r tries again. It reads, and acts on the server only when you
ask it to: c cancels a session’s statement and X ends
a session (see Cancelling and ending sessions).
Open it with M on a connection in the explorer, its menu’s “Monitor” item, Space m m, or
:monitor (the explorer’s connection or the active tab’s; :monitor <name> for another one).
There is one Monitor tab per connection: opening it again goes to the one that is open.
How it reads
The tab has a connection of its own, opened when the tab is shown (a profile that is not
connected yet connects then). It never uses a tab’s query session or the explorer’s, and it
never sends anything you typed. When you disconnect the profile, a shown Monitor tab says it is
not connected and stays so until r; Monitor tabs open when you quit are opened again with the
workspace and read once shown.
Each read is one read-only transaction (PostgreSQL) or a few read-only statements (MySQL), with
a short lock wait and a statement limit, so it never waits behind other sessions or holds them
up.
- It reads only while the tab is on screen. A Monitor tab in the background, or under a dialog, sends nothing, and when it is shown again it reads at once; its charts show a gap for the time it was not looking, never a line across it. (The reads that follow a cancel or an end are the exception: see below.)
- One read at a time: the next one starts an interval after the answer to the last, so a slow
server is never asked twice at once. A new Monitor tab starts with
[monitor] interval(2 seconds by default; 1, 2, 5, 10, 30 or 60 seconds);+and-change it for that tab only, and the change is not saved. ppauses (what was read stays for reading),rreads at once (also while paused).- After three failures in a row (a failed read, or a connection lost or not opened) the delay doubles, and doubles again after each three more, up to a minute (or the interval, when that is longer); the next good read goes back to the interval. A failed read says why; what is shown is from the read before.
- Rates are worked out on the server’s own clock. A restart or a statistics reset is a gap, never a negative rate or a spike, and the monitor’s own reads are taken out of the counts.
Overview
The server’s version, provider (when the server says it is a managed service), its role (on
PostgreSQL primary or standby; on MySQL server takes writes or server read-only, from
read_only and super_read_only: the server’s own setting, not the profile’s), its uptime
and its connection limit; then a chart of each metric the server has (PostgreSQL’s commits,
MySQL’s statements, sessions, the page cache’s hit share, rows read and written, pages read
from storage, and more), and the longest running sessions: those running a statement, and
those idle with a transaction open (the ten longest). A click on one shows it in the Sessions
view.
What the account cannot see is never shown as quiet. The sessions count and its active part
are the server’s own counters on MySQL (Threads_connected, Threads_running), so they cover
every session; the longest running come from the session list, which on MySQL without
PROCESS holds only the account’s own sessions, so the list is headed “Longest running (this
account)” and, when empty, says that no session of this account is running. On PostgreSQL
without pg_read_all_stats other roles’ sessions show no state: the sessions count is the
least there are (≥41/200), the Sessions tile says how many are hidden, the list is headed
“Longest running (visible sessions)” and, when empty, says how many are hidden and what would
show them.
Under the metrics, two charts with a line per kind, coloured as the legend below each says; the legend gives the latest figure of each line. On a narrow or short screen they become two lines of figures in the list of metrics.
Statements by type (MySQL). Statements per second by type, from the server’s statement counters:
| Line | Counts |
|---|---|
| select | SELECT |
| insert | INSERT, INSERT ... SELECT, REPLACE, REPLACE ... SELECT |
| update | UPDATE, including multi-table ones |
| delete | DELETE, including multi-table ones |
Prepared statements count by their type, and so do statements run inside stored programs and
statements the server refused. LOAD DATA, CALL itself and other statements are in none of
the lines. The monitor’s own SELECTs are taken out. After the monitor ends a session through
a stored procedure (on a managed service), the next point of the select line is a gap: the
procedure’s own statements cannot be counted.
Rows by operation (PostgreSQL). PostgreSQL does not count statements by type, so its chart
counts rows instead, and says so: rows read (rows scanned and index entries returned, for any
statement, including the scans of an UPDATE or a DELETE), inserted, updated and deleted per
second. One UPDATE of ten thousand rows is ten thousand; rows of rolled-back transactions,
of triggers and of cascades count too, and the lines are not parts of one total. A session
publishes its counts after its transaction ends (at most about once a second), so a long one
shows when it is done. The read line includes a few rows per read of the monitor’s own (about
one per database). With track_counts off the chart says it is not available, rather than
drawing zero.
Sessions by state. The sessions of each state at each read, counted by the server (not from
the list, which leaves idle sessions out). On PostgreSQL: running, waiting for a lock, idle in
a transaction, idle, background processes, and unknown (sessions the account cannot see, or
with activity tracking off; shown only when there are some, never counted as idle). On MySQL,
by the session’s command: query (Query, Execute), sleep, daemon, connect (Connect,
Connect Out), binlog dump (replicas reading from this server) and other. Without the
PROCESS privilege MySQL lists only the account’s own sessions, and the chart’s title says
“this account only”. The monitor’s own session is left out.
Sessions
Every session the account may see, the longest running statement first. s sorts by the next
column and S the other way (a click on a column’s name sorts by it), / filters by user,
database, application, client, state or statement, j/k select. Sessions waiting for a lock,
and transactions idle for over a minute, are marked; the monitor’s own connection is dim.
Idle sessions are not listed until a lists them too:
- PostgreSQL counts them (“12 idle not listed”). An idle session that blocks a waiting session (holding an advisory lock, say) is listed anyway.
- MySQL says that idle sessions are not listed, without a count. A session idle with a
transaction or a lock open is listed anyway. Where the account cannot tell whether an idle
session holds one (see What the account sees), every idle session is
listed, as
idle ?, whateverasays.
Enter shows the selected session in full: its client, application, kind, what it waits for,
when its transaction and statement started, and its statement. The list holds the first 256
characters of a statement; the details read the rest of a longer one, once, by a read of its
own. The whole text is shown only while it is still that session’s statement:
- PostgreSQL tells it by the session’s and the statement’s start, to the microsecond. An idle session’s last statement counts too.
- MySQL tells a statement’s start in whole seconds, so it reads the whole text only of a statement that is running and had run for 5 seconds when it was listed (until then the details say it is read once the statement has run longer), and takes it as the same statement when its start is within 2 seconds of the listed one. A new run of the same statement that soon after cannot be told apart, and the details say so.
When the session has ended, runs another statement, or the account cannot tell, the details say
which and show only the start. Where the server itself may have cut a statement (PostgreSQL
keeps only track_activity_query_size bytes, MySQL’s process list the first 64 KiB), that is
said.
The statement is shown in the editor’s syntax colours (the connection’s dialect) and laid out
for reading by the editor’s SQL formatter: only the blanks between its words change, and a line
above it says so. f shows it as the server sent it, and f again laid out. It is shown as sent,
with a line saying why, when only its start is known (the list’s 256 characters, or a statement
the server may have cut: a layout would hide where it stops), when a layout would change more
than its blanks, and when it is over 64 KiB.
The monitor’s own connection is listed as this monitor and its details name it: its
statement is the monitor reading the server, and it cannot be cancelled or ended.
o opens the statement in a new console of the connection, in that session’s database, without
running it (the other session’s search path and settings are not known, and the status line
says so). y copies it. Both take the server’s text (never the layout), the whole of it once
the details have read it, else the start; o then says that only the start was read, y does
not.
Locks
3 shows who waits for whom as a tree: each session or transaction on one line, under the one
it waits for, the roots (those that wait for nobody) first, the one with the most sessions
behind it at the top. A waiter’s line says what it waits for (the lock mode and what it is
on: a table, a row, a transaction) and since when; a root’s says what it holds and how long
its transaction has been open. A session waiting for more than one says which others it also
waits for. j/k or a click select a line, the selected line is shown in full below the tree
(up to three lines) when it is wider than the view, b goes to the root blocker of the
selected wait, Enter shows the session in full, and o and y take its statement as in the
Sessions view.
The tree comes from what the server says:
- PostgreSQL names each waiter’s blockers (
pg_blocking_pids), asked for the 50 longest waits only; when more sessions wait, the view says how many are left out. What each waits for and what its blockers hold come frompg_locks, read only while the Locks view is shown and at most every 5 seconds (or every interval when it is longer), and not at all while no session the account sees waits: each read asks the server’s lock manager for all its locks. The view says when that was last read. - A prepared transaction (
PREPARE TRANSACTION) holds its locks with no session: when a waiting session waits for it, it is a root of its own, with its id, owner, database and age (prepared transactions that block nobody are not listed). OnlyCOMMIT PREPAREDorROLLBACK PREPAREDwith its id ends it; the monitor does not offer them. - MySQL names the blockers of row lock waits (
performance_schema.data_lock_waits); what such a wait is on is not read (data_lockswould make the server go through every lock it holds, millions in a large transaction), so the line says it waits for a row lock and the statement shows on which table. The blocking transaction’s session is the one that has it now (fromINNODB_TRX, which needsPROCESS): a prepared XA transaction whose session went on to other work is shown as a transaction with no session, and withoutPROCESSonly the transaction is named. Row lock waits are read only from MySQL 8.0.38 in 8.0, and from 8.4 (on older versions reading them can stall a busy server). For metadata locks (anALTER TABLEwaiting for a long transaction,FLUSH TABLES WITH READ LOCK) MySQL does not name the blockers: the tree shows the sessions holding a lock on the same object that the request cannot be granted beside, as the server’s lock manager decides it, and says that they may block it. With more than one such holder,bdoes not pick one. A request waiting behind another request rather than a granted lock is shown waiting, its holder not found. Both lock tables are read only while the Locks view is shown, at most every 5 seconds, and only when a session may be waiting for a lock. - A wait in a cycle (a deadlock the server has not broken yet) is shown once, from its smallest session, and marked.
Replication
4 shows what the server says about its replication. It is read when the view is shown, with
r, and then at most every 10 seconds (the interval when it is longer) while the view stays
shown, on a read of its own: when it fails, the view says why and keeps the last good one, and
the other views read on. j/k scroll.
- Role: a replica (a PostgreSQL standby in recovery, a MySQL server with replication channels or a secondary of its replication group), a PostgreSQL logical subscriber, a primary (servers take changes from it and it takes none, or its group’s primary), or not replicating. When the account may not read what would tell, the view says it cannot tell, never “not replicating”.
- What it takes changes from: on a PostgreSQL standby, where its WAL receiver streams from and through which slot, whether replay runs or is paused, the log positions received and replayed, and when the source was last heard from. Without a WAL receiver the standby replays only what it has, or reads from an archive, and the view says so. On a MySQL replica, each channel’s source, whether its receiver (I/O thread) and applier (SQL thread) run and what they are doing, the last heartbeat, the last transaction fetched and applied, the GTID sets received and executed (their first 600 characters), a delay set on purpose, and the last error of each side.
- The servers that take changes from it: a PostgreSQL primary lists its WAL senders: the standby’s name and address, its state and sync state, how much it has not replayed yet, the write, flush and replay lags while they are measured, and its last reply. A MySQL source lists the replicas connected to it (their address, account and what the sending thread does); their lag is read on each replica.
- Slots (PostgreSQL): each replication slot, whether its consumer is connected, how much log
it keeps, and its
wal_status(from 13). A slot whose consumer is away keeps log that grows, and is marked. - Subscriptions (PostgreSQL logical replication): each subscription of the server, whether its apply worker runs, the position it received and when the publisher was last heard from.
- Group replication (MySQL): the members of the group, their state and role.
How far behind is said only as far as the server can tell:
- PostgreSQL: a standby that has replayed all it received says so; else how many bytes it has received and not replayed yet. The age of the last replayed transaction is shown apart, on the primary’s clock, because it grows on a primary with nothing to write too: it is never given as the lag.
- MySQL:
Seconds_Behind_Sourceonly measures the applier against what the receiver fetched, so 0 can hide a receiver that is behind or stalled. The view shows the receiver’s state and its last heartbeat next to it (a source sends one only while it has nothing else to send), and says when the receiver does not run. Where the server gives the receiver’s and the applier’s places in the source’s binary log (shown as “source log read to … applied to …”), a channel has applied all it received only where they are the same:Seconds_Behind_Sourceis 0 also while events of the same second wait. Without them, a channel is taken as caught up only while its applier runs withSeconds_Behind_SourceNULL and no transactions left by the GTID sets. Where every transaction carries a GTID (gtid_modeON) it counts the transactions received and not applied yet from the GTID sets, which never prove that nothing is left (a channel may take transactions without GTIDs); a NULL is never shown as 0.
On Amazon Aurora the cluster’s own replicas share its storage and are not listed by these views; the view says so, and gives the role as cannot tell rather than not replicating.
The server in full
i shows what the server is, over any view: its whole version line, its provider (read from
the server, never guessed from its host name), its role (as in the Overview), when it started, its
connections now and its limit, the size of its page cache, and the settings that decide what
the monitor can read (PostgreSQL track_activities, track_counts,
track_activity_query_size, track_io_timing and wal_level; MySQL performance_schema,
server_id, gtid_mode, log_bin and super_read_only). A setting that leaves the monitor
less to read (track_activities, track_counts or performance_schema off) is marked with
what it hides; track_activities and track_counts are read as the monitor’s own connection
has them, and another session’s may differ. i or Esc closes it.
Cancelling and ending sessions
In the Sessions view, c cancels the selected session’s statement (the session stays) and X
ends the session (its open transaction is rolled back); in the other views they ask you to
select a session in the Sessions view first. The action menu (Space Space, a right click)
lists both, dimmed with the reason when they cannot be sent:
-
the profile is read-only (or its environment cannot be read), or the row is the monitor’s own connection;
-
a cancel of a session that runs no statement;
-
a server process, a replication connection or a parallel worker (cancel its leader instead);
-
on PostgreSQL, a session whose start and statement the account may not see (without
pg_read_all_stats), and an end of a session whose statements are not tracked (track_activitiesoff); -
an end of an idle session behind a pooler (below);
-
a row read before the monitor reconnected: read it again first.
-
A cancel on PostgreSQL asks one line on the status line: only
ysends it, any other key sends nothing. Ending a session, and a MySQL cancel of anything but a plain read, opens a dialog that names the session, its client, state, transaction and statement, and what the action can leave behind.Enterkeeps the session as it is; onlyysends. -
Both are refused on a read-only environment, and never sent to the monitor’s own connection.
-
When the session is one of your own query tabs, the question names the tab; the cancel is the same guarded cancel, sent by the monitor, and the tab’s statement ends with the server’s cancel error. The name is a hint: each query tab of the connection reports its session id while a Monitor tab of it is open, and the id is checked against the row’s user, database and application name. Where the server does not list the application, as MySQL, the tab is not named. Ending your own tab’s session closes that tab’s session, and the dialog says so.
-
Nothing is sent to a session other than the one you acted on. The action carries the session and its statement as the row showed them, and is sent only if they are still there. PostgreSQL checks the session’s start and the statement’s start in the same statement that sends the signal. MySQL reads the session first and sends
KILLright after (a few milliseconds apart): a cancel checks the statement’s event inperformance_schema, or, without it, the statement’s text and times (a weak check: the same text may be a new run of it, and the dialog says so); an end checks the session only. An action asked for before the monitor reconnected is dropped.
Sent is not done. The answer says what the server said: refused (with the privilege it needs),
nothing sent (the session ended, another session took its id, it runs another statement),
sent, or sent with its answer lost. A sent action is then confirmed by reads of the session 1, 3
and 10 seconds after the answer, whether the tab is shown or not. A session ended is confirmed
when a read no longer finds it; the reads stop once the outcome is known. A statement is called
cancelled only when the server says so: MySQL error 1317 for that statement, read from
performance_schema’s statement history when its consumers are on. Otherwise, and always on
PostgreSQL, the read says the statement ended after the cancel was sent, since it may have
finished on its own.
On managed services:
| Server | Used when KILL is refused | To act on other accounts’ sessions |
|---|---|---|
| PostgreSQL | none | pg_signal_backend |
| Amazon RDS and Aurora PostgreSQL | none | rds_superuser or pg_signal_backend |
| Google Cloud SQL for PostgreSQL | none | cloudsqlsuperuser or pg_signal_backend |
| Google AlloyDB | none | alloydbsuperuser or pg_signal_backend |
| Azure Database for PostgreSQL | none | azure_pg_admin or pg_signal_backend |
| MySQL | none | CONNECTION_ADMIN |
| Amazon RDS and Aurora MySQL | mysql.rds_kill_query, mysql.rds_kill | CONNECTION_ADMIN |
The provider is read from the server: on PostgreSQL Amazon RDS, Aurora, Google Cloud SQL,
AlloyDB and Azure are recognised, on MySQL Amazon RDS and Aurora. Another managed MySQL service
is treated as a self-managed server: plain KILL, and a refusal names CONNECTION_ADMIN (or
SYSTEM_USER, when the session’s account holds it).
When the server refuses to signal a session of the provider’s own service accounts
(rdsadmin, cloudsqladmin and azuresu on PostgreSQL, rdsadmin and azure_superuser on
MySQL), the answer says so. On PostgreSQL a session of your own role can be signalled without
these, and a superuser’s session only by a superuser.
Behind a pooler (PostgreSQL), a server session can serve any client. Unless the monitor’s own connection is known to reach the server directly, ending a session is refused unless it runs a statement, and the dialog warns that it may serve another client by the time it ends.
L shows the action list under the sessions (in the Sessions view, when there is room): the
newest six actions, each with when it was sent, what it was, the session and the start of its
statement, the server’s answer and what the reads after it showed. It is kept for the tab’s life
and never written to disk.
What the account sees
What the account may not see is said in the tab, with what would show it, never shown as nothing:
| Server | Without | The tab shows |
|---|---|---|
| PostgreSQL | pg_read_all_stats (or superuser) | other roles’ sessions without their state, statement and times, and their lock waits out of the Locks view |
| MySQL | PROCESS | only the account’s own sessions |
| MySQL | PROCESS, SELECT on performance_schema, or the metadata lock instrument (wait/lock/metadata/sql/mdl) | an idle session as idle ?: whether it holds a transaction open is not known |
| MySQL | enough performance_schema thread slots (performance_schema_max_thread_instances) | that the list may miss sessions |
| MySQL | SELECT on performance_schema | the Locks view without who blocks a row lock wait, and without metadata lock waits |
| MySQL | the metadata lock instrument (wait/lock/metadata/sql/mdl) | the Locks view without metadata lock waits |
| MySQL | 8.0.38 or later in 8.0, or 8.4 or later | the Locks view without who blocks a row lock wait (on older versions reading it can stall a busy server, so it is not read) |
| PostgreSQL | pg_read_all_stats | WAL senders and the WAL receiver without their state and positions (a standby’s received and replayed positions still show) |
| MySQL | REPLICATION CLIENT | that whether the server takes changes from another is not known |
| MySQL | PROCESS | that the servers taking changes from it are not known (only the account’s own sessions are listed) |
| MySQL | SELECT on performance_schema | that each channel’s last heartbeat and last transaction, and a group’s members, are not known |
Explorer
The explorer on the left lists, in sections that open and close (and stay closed across restarts): Connections, Tunnels (SSH tunnels), Environments (Environments and safety), Startup SQL (Startup SQL) and Saved queries. Each section’s head shows its name and how many it has.
Under a connected profile it lists the server’s databases, their schemas, tables and views
(materialised views with an icon of their own). j/k move, l/h open and close, Enter
opens or toggles, r reloads. The full key list is in the
keybindings reference.
╭ Explorer ──────────────────╮ 1 console 1 ×
│ + New connection │╭ Query · console 1 ─────────────────────────────────────────────────────────────────────╮
│▾ Connections 1 ││ local-pg / datarig 127.0.0.1:55432 Space c s switch connection │
│ ▾ ● local-pg ││ 1 -- datarig 🐘 Ctrl+E: run the statement under the cursor │
│ ▾ datarig default ││ 2▎SELECT * FROM shop.users WHERE id <= 8; │
│ ▸ analytics ││ 3 │
│ ▸ public ││ 4 SELECT u.name, o.status, o.total_amount │
│ ▾ shop ││ 5 FROM shop.orders o │
│ ▾ Tables ││ 6 JOIN shop.users u ON u.id = o.user_id │
│ ▸ order_items ││ 7 WHERE o.status = 'paid'; │
│ ▾ orders ~8 KB││ 8 │
│ ▾ Columns (3) ││ 9 SELECT * FROM analytics.events; │
│ PK FK id b…││ 10 │
│ FK user_id …││ 11 SELECT * FROM analytics.events ORDER BY created_at DESC; │
│ status tex…││ 12 │
│ ▸ Primary Key ││ 13 SELECT analytics.slow(30); │
│ ▾ Foreign Keys …││ 14 │
│ ▾ orders_even…││ 15 SELECT $$a body; with a semicolon$$ AS dollar, 'it''s; fine' AS quoted; │
│ id → anal…││ 16 │
│ ▸ Columns (…││ │
│ ▸ orders_user…││ │
│ Indexes ││ │
│ Unique Constr…││ │
│ ▸ Check Constra…││ │
│ Triggers ││ │
│ ▾ users ~11k rows││ │
│ ▸ Columns (5) ││ │
│ ▸ Primary Key ││ │
│ Foreign Keys ││ │
│ ▾ Indexes (3) ││ │
│ ▸ users_email…││ │
│ ▸ users_nickn…││ │
│ ▸ users_pkey …││ │
│ ▸ Unique Constr…││ │
│ Check Constra…││ │
│ ▾ Triggers (2) ││ │
│ users_audit…││ │
│ ▸ users_touch…││ │
╰────────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────────────╯
NORMAL local-pg │ orders rows unknown · ~8 KB │ Enter open · Tab next pane · : commands · Space ? help · q quit EN
Marks
- A profile through an SSH tunnel carries the tunnel mark (
SSHwith icons off), and one with read-only rules a lock in blue (ROwith icons off). That comes from the rules alone, never from an environment’s name. ◆after a profile marks an open transaction.
Row and size estimates
Each table and materialised view shows the server’s estimates of its rows and its size on disk,
dim and on the right (orders ~50k rows · 19 MB). They are read with the schema’s list in the
same round trip, from the statistics that VACUUM and ANALYZE keep, without opening the table
and without a lock on it. A table that has grown to more than twice the rows its last ANALYZE
saw shows the live rows of the server’s statistics instead. A table with no statistics yet shows
nothing, and the status bar says “rows and size unknown (no statistics yet)”.
The largest tables of each schema stand out. With [explorer] size_heat = "3" (the default),
the largest tenth of those with a known size (one at least) shows its estimates in amber from
1 MiB and in red from 1 GiB. "5" (or :set explorer.size_heat=5) uses five levels: yellow
from 1 MiB, amber from 100 MiB, orange from 1 GiB, red from 10 GiB. "off" keeps them all dim.
A table’s structure
An open table shows its structure, read from the catalog in one round trip the first time it
opens (r reads it again; the table itself is never read). The groups:
- Columns: key marks, type,
not null, default. - Primary Key and Foreign Keys:
Enteron a foreign key goes to the referenced table. - Indexes: columns or expressions with their order, operator class and collation (as in
(created DESC)),UNIQUE, the method, a partial predicate, and whether a key or constraint owns it. - Unique and Check Constraints.
- Triggers: timing, events (with the columns of
UPDATE OF), row or statement, the function, disabled; a trigger with aWHENcondition opens to it.
Each key, index and check opens to the columns it covers, in its order (a foreign key’s columns
with the column each references, as user_id → users.id). Views show their columns and
triggers, materialised views their columns and indexes.
The groups start closed, one line each (Columns (27)); what you open stays open for the
session. Because the explorer is narrow, the status bar shows the whole line under the cursor.
A lookup never waits on another session. A table another session locks (an ALTER TABLE,
a VACUUM FULL, a migration waiting for its lock) says the structure is unavailable because
the table is locked, without queueing behind that lock: at once when the lock is already there,
after at most 2 s when it is taken while the structure is read.
Filter
/ filters the explorer by a part of a name, ignoring case: profiles (by name, host or
database), folders (a matching folder shows everything in it), tunnel presets, environments,
Startup SQL presets and saved queries, and under a connected profile the databases, schemas,
tables, views and structure items the explorer has already read. Nothing is asked of the
server: a schema whose tables were never listed, a table whose structure was never opened, and
the tree of a profile that is not connected are not searched. Matches show with the way to them
opened; Esc gives the tree back as it was.
Show DDL
D on a table, view, materialised view, index or trigger (or its menu, or :ddl [schema.name])
opens its CREATE statements in a read-only tab; F on a trigger shows its function’s.
- On PostgreSQL the DDL is rebuilt from the catalog in one round trip, and its first line says so. It covers owned sequences, columns with their collations, defaults, identity and generated expressions, constraints, partitioning or inheritance, storage parameters, owner, indexes, triggers, row-level security and policies, comments and grants. Not included: rows, sequence values, rules, extended statistics, security labels. Every name is qualified.
- On MySQL it is the server’s own
SHOW CREATEtext (see MySQL). - The text can be moved in, searched and yanked; edits are refused.
oopens it in a new console on the same connection, andrreads it again. - Reading never waits on another session: when the object, or a table its definition reads, is locked, the tab says so and shows nothing.
Not supported: DDL of objects the explorer does not list (functions on their own, sequences, types).
Mouse
- A click on a row selects it. A click on its arrow (
▸/▾) also opens or closes it, aslandhdo: on a table that shows its structure without opening a tab. The arrow under the pointer is highlighted. - A double click on a row does what
Enterdoes: a table opens in a tab, any other row opens or closes. - The wheel scrolls the list without moving the selection.
- A right click opens the row’s menu.
Layout
Space b hides or shows the explorer, and Space < / Space > (or a drag of its right border)
change its width; both are kept across restarts.
MySQL
Versions. MySQL 8.0, 8.4 and 9.x. CI runs the driver’s tests against MySQL 8.4. MySQL 5.7 and older are refused with their version. MariaDB 10.6 and newer is a best effort: it connects and runs statements, and is tested only lightly.
A MySQL profile has driver = "mysql", and a URL starts with mysql:// (or mariadb://).
What works
- Connecting directly or through an SSH tunnel or tunnel preset (the
driver talks over the tunnel’s channel; no local port is opened), with
caching_sha2_passwordandmysql_native_password, and the test connection. - Running statements, one per request: datarig splits scripts itself,
DELIMITERincluded. The client never asks the server to run several statements from one request, andLOAD DATA LOCALis off. The editor and the risk checks read the text in the session’s sql mode (ANSI_QUOTES,NO_BACKSLASH_ESCAPES). - Values as the server writes them:
DECIMALexact, unsigned integers in full,BITasb'…', binary values as0x…,JSON, dates and times with their fractions,TIMEas an interval (negative, or over 24 hours), zero dates (0000-00-00) as text,ENUM/SETas text,GEOMETRYas bytes. - Messages: rows affected, the last insert id, warnings and the server’s info line; errors
with the server’s error number and SQL state (
ERROR 1146 (42S02): …). Rows affected by anUPDATE(andINSERT … ON DUPLICATE KEY UPDATE, andROW_COUNT()) are the rows it matched, as in PostgreSQL: a row set to the values it already had counts too (the server’s info line still says how many changed). - Transactions as the server reports them after each statement. A statement that commits your open transaction by itself (DDL and others) is noted in Messages, and with manual commit asks first (Manual commit).
- The explorer: the server’s databases right under the profile (a MySQL database is the
schema), their tables and views with the server’s estimates, and a table’s structure
(columns, keys, indexes, constraints, triggers); completion;
:use dband the database picker (Space c d). - The row count (
#), key marks, run hints, charts, export, plans (MySQL 8.0.16 and later), and copying rows in every format.
Cancel
Ctrl+C sends KILL QUERY from a connection of its own, never KILL. A SLEEP() that is
stopped returns 1. A cancel the server takes is waited for, however
long the statement then takes to roll back.
A statement that runs as another account (in a SQL SECURITY DEFINER routine; stopping it
needs CONNECTION_ADMIN) refuses the cancel. If it sends no rows it runs on to its end whatever
datarig does, and the tab says so and keeps waiting (ask a DBA or the routine’s owner to KILL
it). datarig closes the tab’s connection instead of reading on, and the next run connects again,
when:
- the server refuses the cancel and the statement keeps sending rows;
- the cancel cannot reach the server (its connection does not open);
- the server takes the cancel and the statement still sends rows more than 10 seconds later.
Whether the statement then stops is the server’s: it may run on, and outside a transaction block commit what it changes. An open transaction is rolled back once the server ends the session.
Show DDL
D on a table, view or trigger (or its menu), or :ddl [kind] name (name or db.name, in
the tab’s database; kind is table, view, procedure, function, trigger or event,
needed only when one name names several), opens the server’s own SHOW CREATE text.
- MySQL has no DDL of an index of its own:
Don an index row shows its table’s. - A trigger’s, routine’s or event’s text is wrapped in delimiter statements
(
DELIMITER ;;…;;/DELIMITER ;), so it runs again as one statement. - An object made under another
sql_modethan the connection’s default starts with a-- sql_mode: …line: its body is read in that mode. - The server writes the strings of a table’s or view’s text with backslash escapes whatever
the mode, and says so in a first line when there is a backslash.
oopens the text in a console in the object’s database with every string written as that console’ssql_modereads it, so it makes the same object there; the DDL tab keeps the server’s text. - The
DEFINERis kept, so running it again as another user needsSET_USER_ID(MySQL 8.0) orSET_ANY_DEFINER(8.2 and later). A routine you may run but not see says its definition is hidden. - Reading waits at most 2 s on another session: an object whose table is locked shows
“locked”, and
rreads it again.
Copy as SQL
INSERT and UPDATE use backtick names and MySQL’s literals (bytes X'…', bits b'…'), and
leave generated columns out. A string with a backslash or a NUL is written as its UTF-8 bytes
(_utf8mb4 X'…'), so the statements read the same in any sql mode; a TIMESTAMP is written
with the offset of the session’s time zone. UPDATE needs the table’s primary key in the
result.
A copy is refused, naming the column, where a value would not be written whole: a FLOAT (the
server sends it rounded to about six digits), a TIMESTAMP while the session’s time zone is not
a fixed offset (SET time_zone = '+00:00' first), text while the session’s results are not
utf8mb4, and an AUTO_INCREMENT value of 0. A whole result is refused when it was read while
a procedure, an EXECUTE or an executable comment ran, before the session said its values, or
when a SET time_zone or SET NAMES ran while it was read: run the statement again.
The keys this needs are read for every database when the connection opens (and again after DDL
run in datarig), so a table created elsewhere later needs r on the connection in the explorer
before its rows can be copied as SQL. Column names compare without case, and database and table
names too when lower_case_table_names is 1 or 2. A temporary table that shadows a table of the
same name is not told apart from it.
Paging and locks
As on PostgreSQL, nothing is held open while you read a result by default. The query session
keeps sql_select_limit at a page and one row, so the first page is read to the end of the
statement and its metadata lock is released; the next page runs the statement again and skips
the rows you have (only for a statement that is safe to repeat; one that names a view is checked
on the server first). SHOW and DESCRIBE are read whole.
With paging = "hold", and inside your own transaction, a result is read whole into datarig’s
local spill (up to its limit) and the server is free at once. So is a locking read
(FOR UPDATE, FOR SHARE, LOCK IN SHARE MODE), which under the page limit would lock fewer
rows than it asks for.
The explorer and completion read in autocommit with lock_wait_timeout = 2 and a 10-second
max_execution_time: behind a waiting ALTER TABLE they give up after two seconds instead of
queueing behind it. A read that opens a table’s definition (SHOW CREATE TABLE) takes a
high-priority shared metadata lock for the moment it reads, which MySQL grants past a waiting
ALTER TABLE.
Read-only profiles
Two layers, as on PostgreSQL. datarig refuses everything that is not a read before it is sent,
including CALL, executable comments (/*! … */), functions that are not built in, and what
MySQL’s own read-only mode still allows (FOR SHARE, LOCK TABLES, GET_LOCK(),
SET GLOBAL, …). And the session runs with transaction_read_only = ON, checked after every
statement (a session that loses it is closed).
Unlike PostgreSQL, MySQL lets a statement turn its read-only off, so the server’s layer depends
on datarig’s: for a guarantee that does not, use an account with SELECT privileges only.
Views, triggers, generated columns and DEFAULT expressions are not inspected; the server
still refuses their writes.
Editing data
Editing a result needs MySQL 8.0.17 or later; MariaDB and older servers show their results read-only (“this connection type can’t edit data”).
- Finding a row. Without the key in the result, a row is found by its values that compare
exactly: integers,
DECIMAL,BIT,YEAR, dates and times, bytes, and text,ENUMandSETinutf8mb4,utf8mb3,utf8,latin1,ascii,ucs2,utf16,utf16leorutf32(compared byte for byte, not by the collation).FLOAT,DOUBLE,JSON, geometry and text in other character sets (sjis,cp932, …) are not used. - Identical rows. In a table without a primary key or a
NOT NULLunique key, when the result shows every column, each comparing exactly, rows the same in all of them can be changed all at once or just one (“[1] just one”, written withLIMIT 1). FLOATis sent rounded, so its cells are edited one at a time, and a row is not cloned with one. A typed value is refused while text is read in another character set thanutf8mb4or aTIMESTAMPin a time zone that is not a fixed offset, as Copy as SQL refuses them.- Counts are the rows matched (see Messages above), so an update to the value a row already has still gives its expected count.
- After an Apply MySQL returns no rows: a new row’s
AUTO_INCREMENTcolumn shows the number it took, other defaults showdefault, and a trigger’s change shows after a re-run. - A new row leaving a
NOT NULLcolumn without a default to its default is refused before anything is sent, so a server that is not in strict mode never stores an empty value for it.
Passwords on a direct connection
A MySQL profile uses TLS when the server offers it (PREFERRED; see
TLS). A caching_sha2_password login the server has not cached (the first one,
or after a restart) needs the password itself:
- Over TLS it goes inside TLS, but to another machine only when the server’s certificate
was checked (
VERIFY_CA,VERIFY_IDENTITY), or the connection goes through an SSH tunnel, or the profile allows key retrieval (below). OverPREFERREDorREQUIREDwithout a CA file the certificate is not checked, so the login fails with an explanation before anything about the password is sent. - Unencrypted (a server without TLS, or
DISABLED), the password goes encrypted with the server’s RSA public key. Asked for over the same plain connection, that key could come from anyone in between, so on a direct connection to another machine datarig does not ask for it. Name the server’s key file in the profile (server_public_key_file, the PEM ofSHOW STATUS LIKE 'Caching_sha2_password_rsa_public_key'), or allow asking for it (allow_public_key_retrieval = true). A profile that names a key file and no TLS mode stays unencrypted (DISABLED).
A server on your own machine, or one reached through an SSH tunnel, gets the password either
way. A mysql_native_password login over plain TCP can be cracked offline from a capture, and
so can a weak password from any captured login exchange: use VERIFY_IDENTITY (or an SSH
tunnel) for any server that is not on your own machine.
ERROR 1045 may also mean the account requires TLS (REQUIRE SSL) on a connection without it.
After a typed password is refused, datarig shows the error instead of asking again.
Not supported
- The plan view on MariaDB: the plan actions are not offered there, and
EXPLAINruns and shows its text. MariaDB is reached through the MySQL driver. - Editing data on MariaDB and on MySQL before 8.0.17.
- A
USEtyped in a console is not followed by the tab’s header and completion; use:use. - The estimates are the server’s (
TABLE_ROWS), which can be far off for InnoDB. - A view whose table was dropped is listed without columns, and a database that does not exist or that you may not read shows as empty.
- On a server with tens of thousands of tables, the explorer’s catalog and key reads may hit their 10-second limit; copy as SQL then says the keys could not be read.
- Check constraints need MySQL 8.0.16 or later. A user with grants on some columns only sees
those columns. Triggers are listed only with the
TRIGGERprivilege (without it the group says they are not known).lower_case_table_names = 2(macOS, Windows) is not tested.
Themes
One setting, theme, picks the colors: :set theme=nord, the config file’s top-level theme
key, or the settings screen (Space ,), where moving over the names previews them (Enter
keeps one, Esc goes back).
╭ Explorer ────────────╮ 1 console 1 ×
│ ╭ Settings ────────────────────────────────────────────────────────────────╮─╮
│▾│ Display │ │
│ │ Nerd Font icons ‹ on › Nerd Font icons → on │e│
│ │ Command line position ‹ popup › A popup near the top of … │ │
│ │ Theme ‹ terminal › Your terminal's own colo… │ │
│ │ Explorer: size colours ‹ 3 › 3 levels: amber from 1 M… │ │
│ │ Tips ‹ always › Always, in empty places │ │
│ │ Background ‹ theme › The theme's │ │
│ │ Editor │ │
│ │ Cursor shape ‹ on › By mode (block or bar) │ │
│ │ Yanks to the clipboard ‹ on › Yes, as Vim with clipboa… │ │
│ │ Formatter: keywords ‹ preserve › As written │ │
│ │ Formatter: indent ‹ 4 › 4 spaces │t│
│ │ Auto-pairs ‹ off › Off, every key types its… │ │
│ │ Backspace to tab stop ‹ on › On, spaces back to the t… │ │
│ │ Run hints ‹ on › On, the outcome after ea… │ │
│ │ Completion delay (ms) ‹ 60 › Opens once no key came f… │t│
│ │ Results │ │
│ │ Cell detail ‹ panel › A panel on the right of … │ │
│ │ Flash copied cells ‹ on › On, copied cells flash b… │ │
│ ╰────────────────────────────────────── j/k move · h/l change · Esc close ─╯ │
╰──────────────────────╯╰──────────────────────────────────────────────────────╯
NORMAL local-pg │ Connected to local-pg EN
theme | What it does |
|---|---|
terminal | The default: your terminal’s own colours and background (its 16 ANSI colours by role) |
dark, light, high-contrast | datarig’s own truecolor themes |
catppuccin, tokyo-night, gruvbox | The light or the dark variant, following the terminal’s background |
catppuccin-latte, catppuccin-mocha, tokyo-night-day, tokyo-night-night, gruvbox-light, gruvbox-dark | That variant, whatever the background |
nord, dracula | Those palettes (dark) |
| any other name | Your theme file themes/<name>.toml, next to the config file |
Background detection
The terminal’s background is asked once at startup (OSC 11). A terminal that does not answer
counts as dark, and the settings screen says which it found. This only matters for the
families that follow the background (catppuccin, tokyo-night, gruvbox).
The terminal’s background
ui.background = "terminal" (:set ui.background=terminal, or the settings screen’s
Background row) leaves your terminal’s own background where the theme would paint the panes and
the result grid’s alternate rows, so a translucent or image background shows through. Text,
selections, the grid’s cursor cell and column header and the marks of edits keep the theme’s
colours; the status bar, the grid’s headers, dialogs, menus and popups keep the theme’s surface,
and the screen behind a dialog stays see-through (only its text is dimmed). The default,
"theme", paints everything.
Wide highlight bands, which would show as opaque blocks, are left out and their cue moves to the gutter:
- In the editor, the statement a run would take is an accent bar
▎beside its lines, and their line numbers are in the accent; the cursor’s line number is bold. A selection keeps its fill. - In the result grid, the cursor’s row has no band: the cursor’s cell keeps its fill and its row number is in the accent and bold. A selected range keeps its fill.
Switching to it while the theme has a background of its own (every built-in theme but
terminal) says so in one line: the theme’s text colours were made for its background, so on a
terminal background far from it some of them may be hard to read.
Terminals of 256 colours
A terminal that draws 256 colours shows each of a theme’s colours as the nearest one of its
palette, so a faint tint can come out as the very background it sits on. When COLORTERM is
not truecolor or 24bit and TERM names 256 colours (such as xterm-256color), the tints
that mark the grid’s cursor row and column header, the cursor’s line, the statement a run would
take, the selection, a selected range and the item under the pointer each get a palette colour
apart from their background, lighter on a dark theme and darker on a light one. Other colours
are left to the terminal. A terminal that draws 24-bit colours but does not say so in
COLORTERM can set COLORTERM=truecolor to keep the theme’s own tints.
Your own theme
A theme file sets some tokens and takes the rest from extends (a built-in theme; terminal
without it). Colours are #rrggbb, an ANSI name (red, bright-black, …) or default:
# themes/mine.toml next to config.toml, used with theme = "mine"
extends = "dark"
[colors]
accent = "#89b4fa"
warning = "yellow"
[styles]
selection = { bg = "#45475a", modifiers = ["bold"] }
Every token, with what it colours, is listed on the
theme file format page. A mistake in the file (an unknown token,
a bad colour), an unknown name, or a file named like a built-in theme is reported with the file
and line, and the app draws with terminal until it is fixed.
To start a theme file, let datarig write it:
:theme copy <from> <name>writesthemes/<name>.tomlwith every token of<from>(a built-in theme or a theme file) set to its value, each under a comment saying what it colours.:theme new <name>writes one that extends the current theme (when it is built in, elsedark) with every token commented out, showing the value it inherits.- On the settings screen,
con the theme row copies the theme shown there and asks for a name.
None of them replaces a file: a taken name, a built-in theme’s name, a Windows device name
(con, nul, com1, …) or a name that is not letters, digits, - and _ is refused. The new
theme is then used and saved, and its path is shown.
Tokens added since your file was written
When a version of datarig adds tokens, a theme file written before does not list them: they
take their value from the theme it extends (except the grid’s cursor tints, made from the
file’s own colours when it sets its own bg or selection, for the header surface or
selection, and those are RGB colours). The first time such a file is used, at launch or when you choose it, the status
line says how many tokens it does not list. It says so once, and again only when the tokens it
does not list change (the note is kept in the state folder; where that cannot be read or
written, it is said each time).
:theme update (the current theme) or :theme update <name> adds them to the file, each under
its description and commented out with the value it takes now, at the end of its section
(the [colors] or the [styles] table, added when the file has none). Nothing the file sets changes: no
line of it is touched, and nothing is written unless the file reads exactly as before. A
built-in theme, a file with an error, one that cannot be written, or one written with
colors.<token> = … keys instead of a [colors] table is refused and left as it is. A file
that lists every token already says so. In the command line, :theme update lists your theme
files (never a built-in theme), the current theme’s first, and typing narrows them.
Updates
datarig tells you when a newer release is out, and shows what changed after you update. Both can be turned off.
The update check
Once a day at most, when it starts, datarig asks GitHub for its latest release. It asks after the screen is drawn, so starting is never slower, and quitting never waits for it.
What is sent. One HTTPS request:
GET https://api.github.com/repos/DokaDev/datarig/releases/latest, with the headers
User-Agent: datarig, Accept: application/vnd.github+json, X-GitHub-Api-Version and,
after the first answer, If-None-Match with the tag GitHub gave last time. Nothing else: no
identifier, no version, no operating system, no configuration, nothing about your connections
or databases. GitHub sees your IP address, as for any request (see GitHub’s privacy statement).
The server’s certificate is checked against the certificates your system trusts.
Proxies. The request goes through the HTTP proxy in HTTPS_PROXY (or https_proxy), else
ALL_PROXY (or all_proxy), with the user name and password the address gives, except for
hosts NO_PROXY lists. A proxy of another kind (SOCKS, HTTPS) means no check at all, never a
direct connection. The SSH tunnels and settings of your connections are never used.
When it fails. Offline, behind a firewall, without an answer within a few seconds, or when GitHub limits requests from your address: nothing is shown, and datarig tries again the next day. There is one attempt a day, whether it worked or not, and no retries.
datarig keeps what it learnt in update.toml in its state directory (see
where datarig keeps its files): when it last
asked and how that ended, GitHub’s tag for the answer, the latest version with its date and the
opening paragraph of its notes (as one line, at most 300 characters), the newest version that
ran here, a version you skipped, and when you last closed the notice.
The notice
When a newer release is known, the next start shows a notice, unless you skipped that version, closed its notice in the last week, or turned the checks off. It shows the new version and yours, the opening paragraph of its notes (when they open with one), how to update your install, and where What’s new is.
| Installed with | The notice says |
|---|---|
| Homebrew | brew upgrade dokadev/tap/datarig |
From the source: a binary in Cargo’s bin folder ($CARGO_HOME/bin, else ~/.cargo/bin) or in a target/release or target/debug folder | git pull, then cargo install --locked --path crates/tui |
| A release archive, or anything else | Where to download it (the latest release’s page) |
| Key | What it does |
|---|---|
Enter | Presses the button with the focus (Close at first) |
Esc, q | Close: the notice comes back after a week |
s | Skip this version: not told about it again (a newer one is) |
o | Turn off the checks (saved as [updates] check = "off") |
Tab, the arrows, h, l | Move the focus between the buttons |
The notice never covers the welcome panel’s first steps, and comes after What’s new when both are due. A newer release found while datarig runs is said on the status line only; the notice waits for the next start.
:update shows the notice when a newer release is known (even one you skipped or closed), and
otherwise says what is known: that the checks are off and why, that datarig is up to date and
when it last asked, or that it could not ask and why (it never says “up to date” after a check
that failed).
Turning it off
Any of these turns the check off:
[updates] check = "off"in the config file,:set updates.check=off, or the settings screen (Space ,, Updates);oin the notice;- the environment variable
DATARIG_NO_UPDATE_CHECKwith any value but an empty one (it wins over the config): for a shell profile, a shared machine or scripts.
datarig does not check either when CI is set to a value that is not empty (a CI service), in a
development build (cargo build without --release), without a state directory, or when
update.toml cannot be read (it is then left as it is). A second datarig open at the same time does not ask: it shows the
notice from what the first one learnt, but does not keep your answer to it (closing it or
skipping the version is done in the first one).
What’s new
On the first start after an update, datarig shows the notes of every version since the one you
ran before. The notes come with the program: nothing is fetched, and they are the
What’s new page of this site. :whatsnew shows them any time, and
[updates] whats_new = "off" stops them from showing by themselves. See
Getting started for its keys.
Keybindings
Keys resolve from the current context outwards to root; the first binding found wins. Dialogs (overlay.*) sit directly under root, so only root keys pass through them. The cell viewer is not modal: it sits under workspace, so the workspace keys (run, …) keep working.
- [text] text input: letters, digits, symbols and
Spaceare typed. No leader, no Hangul mapping. - [editor] the query editor: its keys may hide keys of the contexts around it, except protected keys.
- grid.matches the result grid while a search is on: its keys (
n,N,Esc) hide the grid’s until the search ends. - Hangul: outside text input a Korean 2-set jamo or syllable counts as the QWERTY key(s) at its place (U+3153 =
j, U+D558 =g k); the status bar then shows the Korean input-source mark. The character vim’sf t F T rwait for is taken as typed. - R: repeats while the key is held (terminal auto-repeat).
- Key guide: after
Spacea which-key popup lists the keys that may follow (after 300 ms;Backspacegoes up a level,Esccloses).Space ?orF1open the keyboard help, the same way everywhere (?alone is left to the backward search of the editor and of the result grid). The help is one list: the sections of the current context first and open, every other context closed below (Enter/l/hor a click opens and closes a section);/searches all of them. The status bar shows the most relevant keys of the current context. - Commands:
:(outside text input) orCtrl+K(everywhere) opens the command line: a popup near the top of the screen, or the last line with[commands] position = "bottom"(the settings screen,:set commands.position=bottom). It runs the commands listed at the end (:conn <profile>,:set <setting>=<value>, …) and finds every action by name; see the tables at the end. - Vim: the editor has vim keys.
istarts typing andEscstops (the hint line says so). Implemented, all with counts: the motionsh j k l w b e W B E ge gE 0 ^ $ gg G,f F t T ; ,(the character is taken as typed, Hangul included),%(the matching bracket of( ) [ ] { }, brackets in strings and comments left out;50%goes to the middle line),{ },H M L, the arrows,HomeandEnd; the operatorsd c y,gu gU g~(case) and> <(indent by 4 columns, written with spaces asTabtypes them) with any motion or text object, doubled for whole lines (dd cc yy guu gUU g~~ >> <<), counts before and after (3dd,d2w,2d3w); the text objectsiw aw iW aW,i( a((ib ab),i[ a[,i{ a{(iB aB),i< a<,i" a" i' a',i` a`andip ap;x X s S D C Y p P r J gJ ~ u Ctrl+R;.(the last change again, what was typed in Insert mode included;3.with a new count, except for a Visual mode operator, which keeps its own as in Vim);i a I A o O(a count types the text that many times); scrolling withCtrl+D Ctrl+U Ctrl+F Ctrl+B zz zt zb; search with/and?(a prompt on the editor’s last line: the cursor shows the match while the pattern is typed,Entersearches,Escor any key of the app goes back,Ctrl+Wdeletes a word; an empty pattern takes the last one),nN,*#(the word under the cursor, as a whole word) andg*g#(also inside longer words), with counts and after an operator (d/fromEnter,yn: up to the match, without it; a match at the end of a line stops on its last character, as in Vim); the matches on screen stay highlighted until:nohlsearch(:noh) and the next search shows them again. Search patterns are Rust regular expressions (https://docs.rs/regex/latest/regex/#syntax), not Vim’s: case-sensitive ((?i)at the start ignores case),\bfor a word boundary (Vim’s\<\>),.*+?()|without a backslash; a match lies within one line, and search offsets (/foo/e) are not supported; marks:m{a-z}sets one at the cursor,'{a-z}goes to its line (the first non-blank) and`{a-z}to its place, also after an operator (d'atakes whole lines,y`athe characters up to it);''and``go back to where the last jump left from (G,gg,%,{,},H,M,L, a search, a mark;m'sets it),'<'>(`<`>) to the ends of the last Visual selection and'.to the last change; marks move with their lines as lines are added or deleted above them and go with a deleted line, an undo puts them back, and'A-'Z,'0-'9,'[']'^are not kept (they say so);gccomments lines out with--or back in, as Neovim’s built-in commenting does:gcc(with a count),gcwith a motion or text object andgcin Visual mode take whole lines; when every line that is not blank starts with--they lose it, otherwise each gets--after the smallest indent (a blank line--), and.repeats it (the actioneditor.comment_toggle,Space e c, is the same asgcc, orgcin Visual mode); editor commands on the:line (see the end);&runs the last:sagain on the cursor’s line with its flags (as Neovim’s&, Vim’s:&&) andg&on every line with the last search pattern; Visual mode by character (v) and by line (V) with the motions and text objects,y d x c,Y D X C S(whole lines),r J gJ u U ~ > <,o(the other end) andp P(a register in place of the selection;Pkeeps the replaced text out of the registers); Visual mode by block (Ctrl+V;v,VandCtrl+Vswitch between the three, the same key again leaves): screen columns, a wide character or a tab partly inside handled as Vim does,$to the end of every line,oandO(the other corners),y Y d x X D(Dto the ends of the lines),c s C,IandA(what Insert mode types goes on every line of the block when it ends;Afills short lines with blanks first,$Aappends at each line’s end; a count types it that many times),r(r Enterbreaks the lines),~ u U gu gU g~,> <with a count,J gJ,S R(whole lines) andp P(a block register as a block, lines below or above, text of one line on every line of the block);.repeats a block operator on a block of the same size from the cursor (afterponly the delete); a paste from the terminal replaces the block; in Insert modeCtrl+W(the word before the cursor),Ctrl+U(the line before the cursor),Ctrl+R {register}(its text),Enterkeeping the indent,Tab(4 spaces) andBackspace, which deletes the spaces before the cursor back to the tab stop (every 4 columns) at once when there is nothing else between, as Vim withsofttabstop=4does then (with anything else between it deletes one character;[editor] soft_tab_backspace = "off"always one); with[editor] auto_pairs = "on"(off by default, as Vim types)( [ { ' " ```` also put their closing character after the cursor, typing it there steps over it andBackspacedeletes the empty pair (only pairs it put in; never inside strings, quoted names, comments or dollar bodies, before a word, or for a paste;.repeats what it did). One command is one undo step. Registers as in Vim:"xbefore a command in Normal and Visual mode ("ayy,2"ap), the unnamed one,"a-"z("A-"Zappend),"0(the last yank),"1-"9(the last deletes of lines;.after"1pputs"2),"-(small deletes),"_(nothing kept),".(the text last typed in Insert mode),"/(the last search pattern) and"+"*(the system clipboard);":"%"#are not kept (a put from them says the register is empty). As in Vim withclipboard=unnamedplus, a yank, delete or change without a register also goes to the system clipboard, through theclipboardsetting’s way ([editor] clipboard = "off"keeps them in the editor);"aand"_never do."+preads the system clipboard only when typed, and not over OSC 52 (the terminal’s own paste works there). A paste from the terminal goes in at the cursor in every mode (over the selection in Visual mode). Unlike Vim, a tab is 4 columns wide and a completion taken from the popup is not part of what.repeats. The other reserved vim keys do nothing yet. - Your editor and suspending:
Ctrl+Gopens the tab’s query in your own editor ($VISUAL, else$EDITOR, elsevi; split into words like a shell does,code -wincluded, but no shell runs it) on a private file in the state directory, removed afterwards. What it saves replaces the text as one undo step; quitting without saving changes nothing, and an editor that fails (Vim’s:cq) leaves the text as it was. A table tab’s query goes as a copy, and an edited copy opens in a new console.Ctrl+Z(and:suspend,:sus,:stop) stops datarig asCtrl+Zstops Vim; the shell’sfgbrings it back (Unix only; on Windows:suspendsays it is not supported).Ctrl+Cstays the query cancel. Meanwhile a running query goes on on the server: with the editor open its results arrive and wait; while suspended the whole program is stopped, so they are read afterfg. - Reserved keys belong to a widget (vim, a dialog) or to an action that is not implemented yet. They cannot be remapped.
Protected keys (no inner context may hide them): Ctrl+Q Ctrl+K Ctrl+E Ctrl+Enter Ctrl+S Ctrl+T Ctrl+O Ctrl+G F1 F6 Shift+F6 Ctrl+PageDown Ctrl+PageUp Space.
Remap in config.toml with [keymap.<context>] tables: "<keys>" = "<action id>", or "none" to unbind. Key notation: ctrl+e, shift+tab, f4, pagedown, space c n (a space separates the keys of a sequence), G = shift+g. Invalid entries are skipped and reported at startup.
[keymap.explorer]
"x" = "explorer.refresh"
"q" = "none"
root
Works everywhere, also while a dialog is open.
| Keys | Action | Description | R |
|---|---|---|---|
Ctrl+Q | app.quit | Quit | |
Ctrl+C | query.cancel | Cancel running query | |
Ctrl+K | commands.open | Commands |
workspace
The workspace without a dialog, including text input in the editor. Inside root.
| Keys | Action | Description | R |
|---|---|---|---|
Ctrl+Enter | query.execute_current | Run statement under cursor | |
Ctrl+E | query.execute_current | Run statement under cursor | |
Ctrl+O | conn.quick_connect | Quick connect (go to a profile’s tab) | |
Ctrl+G | editor.open_external | Edit the query in your own editor ($VISUAL, $EDITOR) | |
Ctrl+Z | app.suspend | Suspend datarig (the shell’s fg brings it back) | |
Ctrl+T | tab.new_console | New console tab | |
Ctrl+W | tab.close | Close tab | |
Ctrl+S | script.save | Save query | |
Ctrl+PageDown | tab.next | Next tab | ✓ |
Ctrl+PageUp | tab.prev | Previous tab | ✓ |
F6 | pane.next | Focus next pane | |
Shift+F6 | pane.prev | Focus previous pane | |
Shift+Tab | pane.prev | Focus previous pane | |
F1 | help.context | Keyboard help |
nav
Every pane that is not text input (explorer, results, vim Normal/Visual). Inside workspace.
| Keys | Action | Description | R |
|---|---|---|---|
Tab | pane.next | Focus next pane | |
: | commands.open | Commands | |
g t | tab.next | Next tab | ✓ |
g T | tab.prev | Previous tab | ✓ |
Space t n | tab.new_console | New console tab | |
Space t c | tab.close | Close tab | |
Space t u | tab.reopen_closed | Reopen closed tab | |
Space t t | tab.list | List tabs | |
Space t < | tab.move_left | Move tab left | ✓ |
Space t > | tab.move_right | Move tab right | ✓ |
Space t g | tab.grab | Move tab with keys | |
Space 1 | tab.goto.1 | Go to tab 1 | |
Space 2 | tab.goto.2 | Go to tab 2 | |
Space 3 | tab.goto.3 | Go to tab 3 | |
Space 4 | tab.goto.4 | Go to tab 4 | |
Space 5 | tab.goto.5 | Go to tab 5 | |
Space 6 | tab.goto.6 | Go to tab 6 | |
Space 7 | tab.goto.7 | Go to tab 7 | |
Space 8 | tab.goto.8 | Go to tab 8 | |
Space 9 | tab.goto.9 | Go to tab 9 | |
Space c c | conn.quick_connect | Quick connect (go to a profile’s tab) | |
Space c n | conn.new | New connection profile | |
Space c e | conn.edit_current | Edit this tab’s connection profile | |
Space c t | conn.test_current | Test this tab’s connection | |
Space c x | conn.disconnect_current | Disconnect this tab’s connection | |
Space c r | conn.reconnect_current | Reconnect this tab’s connection | |
Space c s | tab.set_connection | Change this tab’s connection | |
Space c d | tab.set_context | Choose this tab’s database and schema | |
Space x c | tx.commit | Commit the open transaction | |
Space x r | tx.rollback | Roll back the open transaction | |
Space x s | tx.summary | What the open transaction did | |
Space s s | script.save | Save query | |
Space s a | script.save_as | Save query as… | |
Space s r | script.rename | Rename this saved query | |
Space s o | script.open | Open a saved query | |
Space s d | script.delete | Delete this saved query | |
Space e f | editor.format | Format the statement under the cursor or the selection (only its layout) | |
Space e c | editor.comment_toggle | Comment the line or the selected lines out with – (or back in) | |
Space e x | query.explain | Explain: the statement’s plan (nothing runs) | |
Space e a | query.explain_analyze | Explain analyze: run and measure the statement (writes rolled back) | |
Space e p | results.view_as_plan | View as plan: the text EXPLAIN again as a plan | |
Space Space | menu.open | Action menu | |
Shift+F10 | menu.open | Action menu | |
Menu | menu.open | Action menu | |
Space t m | tab.menu | Tab menu | |
Space z | pane.zoom | Toggle pane zoom | |
Space b | explorer.toggle | Toggle explorer | |
Space > | explorer.wider | Widen explorer | ✓ |
Space < | explorer.narrower | Narrow explorer | ✓ |
Space , | settings.open | Settings | |
Space / | commands.open | Commands | |
Space ? | help.context | Keyboard help | |
Space d e | edit.cell | Edit the cell (the selected cells of one column) | |
Space d n | edit.set_null | Set the cell (the selected cells) to NULL | |
Space d D | edit.set_default | Set the cell (the selected cells) to the column’s default | |
Space d u | edit.undo | Undo the last change of the result’s cells | |
Space d U | edit.redo | Redo the change of the result’s cells undone last | |
Space d r | edit.revert | Drop the pending changes of the cell (the selection) | |
Space d o | edit.add_row | Add a new row, shown after the rows read | |
Space d O | edit.clone_row | Add a new row with the values of the cursor’s row | |
Space d d | edit.delete_row | Mark the row (the selected rows) to be deleted, or unmark it; a new row is removed | |
Space d p | edit.preview | Show changes | |
Space d a | edit.apply | Apply the pending changes (asked first) | |
Space d x | edit.discard | Drop every pending change of the result | |
Space d v | edit.paste | Paste the system clipboard into the grid | |
Space d V | edit.paste_as | Paste again, read as the next format | |
Space d y | edit.paste_register | Paste the cells last copied in the grid over the cells at the cursor | |
Space d Y | edit.paste_rows | Paste the cells last copied in the grid as new rows | |
Space r y t | results.copy.selection.tsv | Copy selection: Without headers (TSV) | |
Space r y T | results.copy.selection.tsv_header | Copy selection: With headers (TSV) | |
Space r y l | results.copy.selection.list | Copy selection: Comma list | |
Space r y c | results.copy.selection.csv | Copy selection: CSV | |
Space r y j | results.copy.selection.json | Copy selection: JSON | |
Space r y J | results.copy.selection.json_pretty | Copy selection: JSON (pretty) | |
Space r y m | results.copy.selection.markdown | Copy selection: Markdown | |
Space r y h | results.copy.selection.html | Copy selection: HTML table | |
Space r y x | results.copy.selection.xml | Copy selection: XML | |
Space r y n | results.copy.selection.in | Copy selection: SQL IN clause | |
Space r y i | results.copy.selection.insert | Copy selection: SQL INSERT | |
Space r y u | results.copy.selection.update | Copy selection: SQL UPDATE | |
Space r y d | results.copy.default | Choose the format y and Y copy in | |
Space r a t | results.copy.all.tsv | Copy all fetched rows: Without headers (TSV) | |
Space r a T | results.copy.all.tsv_header | Copy all fetched rows: With headers (TSV) | |
Space r a l | results.copy.all.list | Copy all fetched rows: Comma list | |
Space r a c | results.copy.all.csv | Copy all fetched rows: CSV | |
Space r a j | results.copy.all.json | Copy all fetched rows: JSON | |
Space r a J | results.copy.all.json_pretty | Copy all fetched rows: JSON (pretty) | |
Space r a m | results.copy.all.markdown | Copy all fetched rows: Markdown | |
Space r a h | results.copy.all.html | Copy all fetched rows: HTML table | |
Space r a x | results.copy.all.xml | Copy all fetched rows: XML | |
Space r a n | results.copy.all.in | Copy all fetched rows: SQL IN clause | |
Space r a i | results.copy.all.insert | Copy all fetched rows: SQL INSERT | |
Space r a u | results.copy.all.update | Copy all fetched rows: SQL UPDATE | |
Space r i | results.detail | Result detail → show / hide | |
Space r I | results.detail_tab | Result detail → Cell / Row | |
Space r h | results.panel.toggle | Results pane → hide / show | |
Space r z | results.panel.maximize | Results pane → zoom (maximise) / restore | |
Space r + | results.panel.grow | Results pane → taller | ✓ |
Space r - | results.panel.shrink | Results pane → shorter | ✓ |
Space r c | results.chart | Results → show as a chart / as rows | |
Space r n | results.page.next | Results → next page | ✓ |
Space r p | results.page.prev | Results → previous page | ✓ |
Space r # | results.count | Results → count every row (runs a count query) | |
Space r e | results.export | Results → export to a file… | |
Space r ] | results.tab.next | Result tabs → next | |
Space r [ | results.tab.prev | Result tabs → previous | |
Space m m | monitor.open | Monitor → the connection’s server monitor |
Leader groups (the which-key popup lists what follows):
| Keys | Group |
|---|---|
Space | Leader |
Space c | Connection |
Space t | Tabs |
Space s | Saved queries |
Space r | Results |
Space r y | Copy selection |
Space r a | Copy all fetched rows |
Space m | Monitor |
Space x | Transaction |
Space e | Editor |
Space d | Data |
explorer
The explorer: folders, connection profiles, their databases and schema trees. Inside nav.
| Keys | Action | Description | R |
|---|---|---|---|
j | explorer.down | Explorer: next item | ✓ |
Down | explorer.down | Explorer: next item | ✓ |
k | explorer.up | Explorer: previous item | ✓ |
Up | explorer.up | Explorer: previous item | ✓ |
l | explorer.expand | Explorer: expand | |
Right | explorer.expand | Explorer: expand | |
h | explorer.collapse | Explorer: collapse or go to parent | |
Left | explorer.collapse | Explorer: collapse or go to parent | |
Enter | explorer.activate | Explorer: open or toggle | |
g g | explorer.top | Explorer: first item | |
G | explorer.bottom | Explorer: last item | |
r | explorer.refresh | Explorer: reload | |
/ | explorer.filter | Explorer: filter by name | |
n | conn.new | New connection profile | |
e | conn.edit | Edit connection profile | |
c | conn.duplicate | Duplicate connection profile | |
d | explorer.delete | Explorer: delete profile or empty folder | |
t | conn.test | Test connection | |
x | conn.disconnect | Disconnect | |
o | conn.open_console | New console on this connection | |
m | explorer.move | Explorer: move profile to a folder | |
N | folder.new | New folder | |
R | explorer.rename | Explorer: rename folder | |
O | explorer.new_console_here | Explorer: new console in this database or schema | |
D | explorer.show_ddl | Explorer: show the DDL of the table, view, index or trigger | |
F | explorer.show_function_ddl | Explorer: show the DDL of the trigger’s function | |
X | tips.dismiss_onboarding | Hide the first steps of the welcome panel | |
Esc | explorer.back | Explorer: back (cancel a test or connecting, clear the filter) | |
q | app.quit | Quit | |
M | monitor.open | Monitor → the connection’s server monitor |
explorer.filter [text]
The / filter of the explorer: profiles (name, host, database), folders, tunnels, environments, Startup SQL (name, statements) and saved queries, and under connected profiles the databases, schemas, tables, views and structure items already loaded. Inside workspace.
| Keys | Action | Description | R |
|---|---|---|---|
Esc | explorer.filter_clear | Explorer filter: clear | |
Enter | explorer.filter_accept | Explorer filter: done | |
Down | explorer.down | Explorer: next item | ✓ |
Up | explorer.up | Explorer: previous item | ✓ |
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
grid
The result grid. The wheel scrolls the rows; a sideways wheel (a trackpad) or Shift+wheel scrolls the view sideways, four characters a step, the columns at its edges partly shown, until a key or a click moves the cell (h/l bring its column into view, moving the view as little as it takes). / searches the fetched rows forward, ? backward (a Rust regular expression, case sensitive unless it starts with (?i); NULL never matches). Inside nav.
| Keys | Action | Description | R |
|---|---|---|---|
h | grid.left | Results: previous column | ✓ |
Left | grid.left | Results: previous column | ✓ |
j | grid.down | Results: next row | ✓ |
Down | grid.down | Results: next row | ✓ |
k | grid.up | Results: previous row | ✓ |
Up | grid.up | Results: previous row | ✓ |
l | grid.right | Results: next column | ✓ |
Right | grid.right | Results: next column | ✓ |
PageDown | grid.page_down | Results: scroll one screen down | ✓ |
PageUp | grid.page_up | Results: scroll one screen up | ✓ |
Ctrl+D | grid.half_down | Results: scroll half a screen down | ✓ |
Ctrl+U | grid.half_up | Results: scroll half a screen up | ✓ |
g g | grid.top | Results: first row of the page | |
Home | grid.top | Results: first row of the page | |
G | grid.bottom | Results: last row of the page | |
End | grid.bottom | Results: last row of the page | |
0 | grid.first_col | Results: first column | |
$ | grid.last_col | Results: last column | |
Enter | grid.view_cell | Results: view cell value | |
y | grid.copy_cell | Copy the cell (a selected range) | |
Y | grid.copy_row | Copy the row (the selected rows) | |
v | grid.select | Select a range of cells (again: cancel) | |
V | grid.select_rows | Select whole rows (again: cancel) | |
Ctrl+A | grid.select_all | Select every cell of the page | |
/ | grid.search | Search the fetched rows forward (a regular expression) | |
? | grid.search_back | Search the fetched rows backward (a regular expression) | |
e | edit.cell | Edit the cell (the selected cells of one column) | |
u | edit.undo | Undo the last change of the result’s cells | |
Ctrl+R | edit.redo | Redo the change of the result’s cells undone last | |
o | edit.add_row | Add a new row, shown after the rows read | |
O | edit.clone_row | Add a new row with the values of the cursor’s row | |
D | edit.delete_row | Mark the row (the selected rows) to be deleted, or unmark it; a new row is removed | |
p | edit.paste_rows | Paste the cells last copied in the grid as new rows | |
g p | edit.paste_register | Paste the cells last copied in the grid over the cells at the cursor | |
i | results.detail | Result detail → show / hide | |
I | results.detail_tab | Result detail → Cell / Row | |
z | results.panel.maximize | Results pane → zoom (maximise) / restore | |
+ | results.panel.grow | Results pane → taller | ✓ |
- | results.panel.shrink | Results pane → shorter | ✓ |
n | results.page.next | Results → next page | ✓ |
] | results.page.next | Results → next page | ✓ |
[ | results.page.prev | Results → previous page | ✓ |
# | results.count | Results → count every row (runs a count query) | |
P | results.view_as_plan | View as plan: the text EXPLAIN again as a plan | |
c | results.chart | Results → show as a chart / as rows | |
L | results.tab.next | Result tabs → next | |
H | results.tab.prev | Result tabs → previous | |
Esc | pane.back | Back to the editor | |
q | pane.back | Back to the editor |
grid.matches
The result grid while a search is on (after / or ?): n goes to the next matching cell in the search’s direction, N the other way, from page to page through every fetched row (in the spill file too), around the ends with a warning; Esc ends the search, and n shows the next page again. Inside grid.
| Keys | Action | Description | R |
|---|---|---|---|
n | grid.search_next | Grid search: next match | ✓ |
N | grid.search_prev | Grid search: previous match | ✓ |
Esc | grid.search_clear | Grid search: end it (n shows the next page again) |
grid.search [text]
The search prompt of / and ? on the line under the grid: the cursor goes to the first matching cell while the pattern is typed, Enter searches (an empty pattern: the last one), Esc goes back to where the cursor was, Ctrl+W deletes the word before the cursor. The full values are searched as fetched (not the shown cut), cell by cell, row by row. Inside workspace.
Reserved (grid search): Esc Enter Ctrl+W
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
grid.edit [text]
The cell editor (e in the grid) over the cell: one line, the cell’s pending value or its value as read. Enter keeps the text (an empty text is the empty string, never NULL), Esc cancels, Ctrl+N sets NULL, Ctrl+D the column’s default. With cells of one column selected, each takes the value, as one change (u undoes it). Ctrl+L takes the text typed so far to the editor of several lines. A value of several lines, and a JSON column’s, opens that editor instead; an enum or boolean column lists its values. Inside workspace.
Reserved (cell editor): Esc Enter Ctrl+N Ctrl+D Ctrl+L
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
inspector
The result inspector next to the grid, after a click on it: j/k move between the columns and the grid’s cell follows (on the Row tab, a click on a column moves it there too), Enter shows the value whole. Inside nav.
| Keys | Action | Description | R |
|---|---|---|---|
j | results.detail_next | Result detail → next column | ✓ |
Down | results.detail_next | Result detail → next column | ✓ |
k | results.detail_prev | Result detail → previous column | ✓ |
Up | results.detail_prev | Result detail → previous column | ✓ |
Enter | grid.view_cell | Results: view cell value | |
I | results.detail_tab | Result detail → Cell / Row | |
i | results.detail | Result detail → show / hide | |
Esc | pane.back | Back to the editor | |
q | pane.back | Back to the editor |
plan
The Plan tab of the results pane (a plan result: PostgreSQL’s JSON, MySQL’s tree): j/k select a node, h/l close and open its children, Enter shows its detail, v/V and the digits pick a view, y copies the plan as the raw view shows it, Y as the server sent it. Inside nav.
| Keys | Action | Description | R |
|---|---|---|---|
j | plan.down | Plan: next node | ✓ |
Down | plan.down | Plan: next node | ✓ |
k | plan.up | Plan: previous node | ✓ |
Up | plan.up | Plan: previous node | ✓ |
g g | plan.top | Plan: first node | |
Home | plan.top | Plan: first node | |
G | plan.bottom | Plan: last node | |
End | plan.bottom | Plan: last node | |
PageDown | plan.page_down | Plan: a page down | ✓ |
Ctrl+D | plan.page_down | Plan: a page down | ✓ |
PageUp | plan.page_up | Plan: a page up | ✓ |
Ctrl+U | plan.page_up | Plan: a page up | ✓ |
l | plan.expand | Plan: show the node’s children (or go to the first) | |
Right | plan.expand | Plan: show the node’s children (or go to the first) | |
h | plan.collapse | Plan: hide the node’s children (or go to its parent) | |
Left | plan.collapse | Plan: hide the node’s children (or go to its parent) | |
Enter | plan.detail | Plan: show or hide the selected node’s detail | |
i | plan.detail | Plan: show or hide the selected node’s detail | |
v | plan.view.next | Plan: next view | |
V | plan.view.prev | Plan: previous view | |
1 | plan.view.tree | Plan view: tree | |
2 | plan.view.summary | Plan view: summary cards | |
3 | plan.view.icicle | Plan view: icicle (width = time) | |
4 | plan.view.flame | Plan view: flame graph (width = time) | |
5 | plan.view.timeline | Plan view: timeline (to the first and last row, needs ANALYZE) | |
6 | plan.view.rows | Plan view: row flow (rows between nodes) | |
7 | plan.view.treemap | Plan view: treemap (area = own time) | |
8 | plan.view.boxes | Plan view: box diagram | |
9 | plan.view.raw | Plan view: raw (the server’s own words, or the text form of a JSON plan) | |
< | plan.pan_left | Plan: move the view left | ✓ |
> | plan.pan_right | Plan: move the view right | ✓ |
y | plan.copy_text | Plan: copy as the raw view shows it | |
Y | plan.copy_json | Plan: copy as the server sent it (its JSON, or its text) | |
z | results.panel.maximize | Results pane → zoom (maximise) / restore | |
+ | results.panel.grow | Results pane → taller | ✓ |
- | results.panel.shrink | Results pane → shorter | ✓ |
L | results.tab.next | Result tabs → next | |
H | results.tab.prev | Result tabs → previous | |
Esc | pane.back | Back to the editor | |
q | pane.back | Back to the editor |
chart
The Chart tab of the results pane (c in the grid, or its tab in the strip; it is drawn only once viewed): the fetched rows of the shown result as bars or lines, nothing run again. The arrows and h/j/k/l move the cursor along the bars or points (the other direction picks a series), Enter shows the point’s row in the grid, v/V and the digits pick the kind, x/s/b choose the X column, the values and a column that splits them into series, S a logarithmic scale, y/Y copy the numbers as TSV or the drawing as text; a click picks a point, a kind or a column choice. Inside nav.
| Keys | Action | Description | R |
|---|---|---|---|
h | chart.left | Chart: cursor left | ✓ |
Left | chart.left | Chart: cursor left | ✓ |
l | chart.right | Chart: cursor right | ✓ |
Right | chart.right | Chart: cursor right | ✓ |
k | chart.up | Chart: cursor up | ✓ |
Up | chart.up | Chart: cursor up | ✓ |
j | chart.down | Chart: cursor down | ✓ |
Down | chart.down | Chart: cursor down | ✓ |
g g | chart.first | Chart: first point | |
Home | chart.first | Chart: first point | |
G | chart.last | Chart: last point | |
End | chart.last | Chart: last point | |
Enter | chart.goto_row | Chart: show the point’s row in the grid | |
v | chart.kind.next | Chart: next kind | |
V | chart.kind.prev | Chart: previous kind | |
1 | chart.kind.bar | Chart kind: bars | |
2 | chart.kind.hbar | Chart kind: horizontal bars | |
3 | chart.kind.line | Chart kind: line | |
x | chart.pick_x | Chart: choose the X column | |
s | chart.pick_y | Chart: choose the value columns (Y) | |
b | chart.pick_by | Chart: split the values into series by a column | |
S | chart.log | Chart: logarithmic scale on or off | |
y | chart.copy_data | Chart: copy its numbers as TSV | |
Y | chart.copy_text | Chart: copy the drawing as text | |
c | results.chart | Results → show as a chart / as rows | |
z | results.panel.maximize | Results pane → zoom (maximise) / restore | |
+ | results.panel.grow | Results pane → taller | ✓ |
- | results.panel.shrink | Results pane → shorter | ✓ |
L | results.tab.next | Result tabs → next | |
H | results.tab.prev | Result tabs → previous | |
Esc | pane.back | Back to the editor | |
q | pane.back | Back to the editor |
changes
The Changes tab of the results pane (:changes, Space d p, or its tab in the strip while the shown result has pending changes): one line per changed cell, every reason they can’t be applied first, or (s) the statements that apply them. j/k select a line (scroll the statements), Ctrl+D/Ctrl+U half a page, g g/G the first and the last, Enter shows the line’s row and column in the grid, s switches between the list and the statements for this run, c copies the statements as they would be sent. The Space d keys work here as in the grid. Inside nav.
| Keys | Action | Description | R |
|---|---|---|---|
j | changes.down | Changes: next line | ✓ |
Down | changes.down | Changes: next line | ✓ |
k | changes.up | Changes: previous line | ✓ |
Up | changes.up | Changes: previous line | ✓ |
Ctrl+D | changes.half_down | Changes: half a page down | ✓ |
PageDown | changes.half_down | Changes: half a page down | ✓ |
Ctrl+U | changes.half_up | Changes: half a page up | ✓ |
PageUp | changes.half_up | Changes: half a page up | ✓ |
g g | changes.top | Changes: first line | |
Home | changes.top | Changes: first line | |
G | changes.bottom | Changes: last line | |
End | changes.bottom | Changes: last line | |
Enter | changes.goto | Changes: show the line’s row and column in the grid | |
s | changes.view | Changes: switch between the list and the SQL | |
c | changes.copy_sql | Changes: copy the SQL as it would be sent | |
u | edit.undo | Undo the last change of the result’s cells | |
Ctrl+R | edit.redo | Redo the change of the result’s cells undone last | |
z | results.panel.maximize | Results pane → zoom (maximise) / restore | |
+ | results.panel.grow | Results pane → taller | ✓ |
- | results.panel.shrink | Results pane → shorter | ✓ |
L | results.tab.next | Result tabs → next | |
H | results.tab.prev | Result tabs → previous | |
Esc | pane.back | Back to the editor | |
q | pane.back | Back to the editor |
monitor
The Monitor tab of a profile (monitor.open: M on a connection in the explorer, :monitor, the connection’s menu): what its server says about its load and its sessions, read by a connection of the tab’s own, only while the tab is shown. 1 the Overview (the server, its load as charts, the longest running sessions), 2 the Sessions, 3 the Locks (who waits for whom, root blockers first; b goes to the root blocker of the selected wait), 4 the Replication (what the server takes changes from, the servers that take changes from it, slots), v the next view; i shows the server in full over any view (i or Esc closes it); j/k select a session (a line in the Locks view; they scroll the Replication view, the server pane and the details), s sorts by the next column (S the other way; a click on a column’s name sorts by it), / filters, a shows idle sessions too, Enter shows the session in full, o opens its statement in a new console (never run), y copies it (both the server’s text), f shows statements laid out for reading or as the server sent them; c cancels its statement and X ends it (asked first: y sends, a dialog’s Enter keeps it; refused on a read-only environment and on the monitor’s own connection; on PostgreSQL the question names your own tab), L shows or hides the list of what was sent and what became of it; p pauses, r reads at once, +/- read more or less often. Inside nav.
| Keys | Action | Description | R |
|---|---|---|---|
1 | monitor.overview | Monitor: Overview | |
2 | monitor.sessions | Monitor: Sessions | |
3 | monitor.locks | Monitor: Locks | |
4 | monitor.replication | Monitor: Replication | |
v | monitor.view.next | Monitor: next view | |
j | monitor.down | Monitor: session below | ✓ |
Down | monitor.down | Monitor: session below | ✓ |
k | monitor.up | Monitor: session above | ✓ |
Up | monitor.up | Monitor: session above | ✓ |
Ctrl+D | monitor.page_down | Monitor: page down | ✓ |
PageDown | monitor.page_down | Monitor: page down | ✓ |
Ctrl+U | monitor.page_up | Monitor: page up | ✓ |
PageUp | monitor.page_up | Monitor: page up | ✓ |
g g | monitor.first | Monitor: first session | |
Home | monitor.first | Monitor: first session | |
G | monitor.last | Monitor: last session | |
End | monitor.last | Monitor: last session | |
p | monitor.pause | Monitor: pause / go on reading | |
r | monitor.read | Monitor: read now | |
+ | monitor.faster | Monitor: read more often | |
- | monitor.slower | Monitor: read less often | |
a | monitor.idle | Monitor: show / hide idle sessions | |
s | monitor.sort | Monitor: sort by the next column | |
S | monitor.sort_reverse | Monitor: sort the other way | |
/ | monitor.filter | Monitor: filter the sessions | |
Enter | monitor.details | Monitor: the session in full | |
o | monitor.open_console | Monitor: the session’s statement in a new console | |
y | monitor.copy_query | Monitor: copy the session’s statement | |
f | monitor.format | Monitor: show statements laid out for reading, or as sent | |
b | monitor.root | Monitor: the root blocker of the selected wait | |
i | monitor.server | Monitor: the server in full | |
Esc | monitor.back | Monitor: back (details, filter) | |
c | monitor.cancel | Monitor: cancel the session’s statement | |
X | monitor.terminate | Monitor: end the session | |
L | monitor.actions | Monitor: show / hide the action list |
monitor.filter [text]
The filter of the Monitor tab’s sessions (/): the sessions whose user, database, application, client, state or statement contain the text, as it is typed; Enter keeps it, Esc too (Esc again in the list clears it). Inside workspace.
| Keys | Action | Description | R |
|---|---|---|---|
Enter | monitor.back | Monitor: back (details, filter) | |
Esc | monitor.back | Monitor: back (details, filter) |
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
welcome
The welcome panel shown while there is no connection profile. Inside nav.
| Keys | Action | Description | R |
|---|---|---|---|
n | conn.new | New connection profile | |
Enter | conn.new | New connection profile | |
q | app.quit | Quit | |
X | tips.dismiss_onboarding | Hide the first steps of the welcome panel |
editor.vim.normal [editor]
Query editor, vim Normal mode (i starts typing). Inside nav.
Reserved (vim): h j k l w W b B e E g e g E 0 ^ $ g g G f F t T ; , % { } ( ) [ ] H M L Left Right Up Down Home End d c y > < = g ~ g u g U 1 2 3 4 5 6 7 8 9 . " x X D C Y s S r R p P u Ctrl+R J g J ~ i a I A o O v V Ctrl+V Esc / ? n N * # g * g # m ' ` g c & g & Ctrl+D Ctrl+U Ctrl+F Ctrl+B z z z t z b z Enter q
editor.vim.visual [editor]
Query editor, vim Visual mode (v by character, V by line, Ctrl+V by block). Inside nav.
Reserved (vim): h j k l w W b B e E g e g E 0 ^ $ g g G f F t T ; , % { } ( ) [ ] H M L Left Right Up Down Home End d c y > < = g ~ g u g U 1 2 3 4 5 6 7 8 9 . " x X D C Y s S r R p P u Ctrl+R J g J ~ i a I A o O v V Ctrl+V Esc / ? n N * # g * g # m ' ` g c & g & Ctrl+D Ctrl+U Ctrl+F Ctrl+B z z z t z b z Enter q
editor.vim.insert [editor] [text]
Query editor, vim Insert mode: typing (Esc goes back to Normal). Inside workspace.
| Keys | Action | Description | R |
|---|---|---|---|
Ctrl+N | editor.complete | Show completions | |
F4 | editor.complete | Show completions |
Reserved (vim Insert): Esc Enter Tab Backspace Delete Left Right Up Down Home End Ctrl+W Ctrl+U Ctrl+R Ctrl+P
editor.vim.search [editor] [text]
The search prompt of / and ? on the editor’s last line: Enter searches, Esc goes back to where the cursor was. Inside workspace.
Reserved (vim search): Esc Enter Ctrl+W
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
editor.ddl [editor]
A DDL tab in vim Normal mode: its text is read-only (moving, selecting, searching and yanking work; edits are refused); r reads it again, o opens it in a new console. Inside editor.vim.normal.
| Keys | Action | Description | R |
|---|---|---|---|
r | ddl.reload | DDL tab: read the DDL again | |
o | ddl.open_console | DDL tab: open the DDL in a new console |
overlay.commands [text]
The : command line: commands with arguments, and a search over every action; a click runs an entry, the wheel moves the selection. Inside root.
Reserved (command line): Esc Enter Up Down Ctrl+P Ctrl+N Tab Shift+Tab
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
overlay.quick_connect [text]
Quick connect: a fuzzy list of the connection profiles; a click picks a row (on ▸ it lists the databases), the wheel selects, the pointer only highlights. Inside root.
Reserved (quick connect): Esc Enter Up Down Ctrl+P Ctrl+N
Reserved (quick connect: databases and schemas): Right Left
Reserved (text input): Backspace Delete Home End Ctrl+U Ctrl+A
overlay.tab_list [text]
The tab list (Space t t, :tabs, :ls, :buffers): the open tabs, the most recently active first, then the tabs closed in this run. Typing filters by name, connection, database, schema, kind or number; the arrows, Ctrl+N/Ctrl+P and Tab move, Enter goes to the tab (brings a closed one back), Ctrl+D closes the selected tab (asking as Ctrl+W does), Esc closes the list (Backspace on an empty filter does not). A click picks an entry, the wheel selects, the pointer only highlights. Inside root.
Reserved (tab list): Down Up Ctrl+N Ctrl+P Tab Shift+Tab Enter Esc Ctrl+D
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
overlay.profile_form [text]
Connection profile form (also the SSH tunnel, environment, default rules and Startup SQL forms). Esc or Cancel closes it; with changes not saved it asks first (Enter keeps editing, y discards), and so does quitting the app while it is open (y discards and the quit goes on). With the mouse: a click focuses a field (in a text, the cursor goes there), picks a value, opens the list of a value ▾, steps a ‹ value › (its arrows; the value opens a list), switches the section or presses a button. A Startup SQL list (the preset form’s statements, a profile’s own startup SQL) has one statement per row: Enter after a filled row adds one below, on an empty row removes it and moves on; ↑/↓ go from row to row and leave the list at its first and last; Tab leaves it; Backspace on an empty row and Ctrl+D remove a row; a paste of several statements makes a row of each. With the mouse: a click on a row puts the cursor there, × removes it, “+ Add statement” adds one at the end (both on the release), the wheel scrolls a list longer than its room. Inside root.
| Keys | Action | Description | R |
|---|---|---|---|
Ctrl+O | form.pick_key_file | Pick the SSH key file | |
Ctrl+B | form.save_as_tunnel | Profile form: save this profile’s SSH tunnel as a tunnel preset | |
Ctrl+E | form.session_view | Profile form: what its sessions would start with (its values, saved or not) |
Reserved (profile form): Tab Shift+Tab Down Up Enter Esc Ctrl+S Ctrl+T Ctrl+N Ctrl+P
Reserved (profile form: Startup SQL list): Ctrl+D
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
overlay.settings
The settings screen: one list by category, each setting with its description; a click selects a row and its ‹/› change the value, the wheel selects, the pointer only highlights. Inside root.
Reserved (settings screen; c on the theme row: copy the theme to a new theme file): j k Down Up h l Left Right Enter Space Esc q c
overlay.export [text]
The export dialog (results.export, :export alone): the rows (every row, the rows fetched, the selection while there is one), the format, CSV’s options and the file. Tab/Shift+Tab and the arrows move between the fields, Enter or Space on a value lists what it can be (←/→ step through them), Space, Enter or ←/→ tick the header line, Enter in a text field or Enter/Space on Export exports (on Cancel: closes), Esc cancels. Under the rows it says how they will be read, under the format what it cannot do here (a format that comes later, an INSERT or UPDATE the result’s keys do not allow, struck through in the list). In the file’s field Tab first completes a name (again: lists the names, then puts each in turn); [Browse…] picks where the file goes in a tree. A click focuses a field, puts the cursor in a text, opens a value’s list or ticks the checkbox; Export and Cancel act on the release. Inside root.
| Keys | Action | Description | R |
|---|---|---|---|
Ctrl+O | export.pick_path | Export dialog: pick where the file goes |
Reserved (export dialog): Tab Shift+Tab Down Up Enter Esc
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
overlay.chooser
A list to pick from: a profile’s colour, icon or folder, a chart’s columns (the values’ list stays open: Enter adds or removes one); a click picks a row, the wheel selects, the pointer only highlights. Inside root.
Reserved (chooser): j k Down Up Enter / Esc q
overlay.chooser.filter [text]
The / filter of a list to pick from. Inside root.
Reserved (chooser filter): Esc Enter Down Up
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
overlay.picker [text]
A list to pick from with a typed filter: a field of the profile form (Enter on it: its driver, SSL mode, password storage, bastion login and its secret’s storage, SSH tunnel or environment), each value with what it means; the tunnel’s and the environment’s end with the entry that makes a new one (“New SSH tunnel…”, “New environment…”), and a value that cannot be picked (a driver the app has none for, the keychain while it does not work) is struck through. Typing filters it as the action menu is filtered; the arrows, Ctrl+N/Ctrl+P and Tab move, Enter picks, Esc closes it and changes nothing. A click picks a row, the wheel selects, the pointer only highlights. Inside root.
Reserved (list picker): Down Up Ctrl+N Ctrl+P Tab Shift+Tab Enter Esc
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
overlay.name_input [text]
A name to type: a new or renamed folder; OK and Cancel take a click. Inside root.
Reserved (name input): Enter Esc
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
overlay.script_tree
The folder tree of the saved queries (save as, open), of the file system (the SSH key file, where an export goes) or of folders only (the folder chooser): the tree has the keyboard. j/k and the arrows move; l/Right opens the selected folder (one of the file system is read first); h/Left closes an open folder, else selects the folder the row is in. In a file system tree, h/Left on ../ or on the folder shown (and l/Right on ../) goes up to the parent of the folder shown. Enter: save as saves into the selected folder, the folder chooser picks it; the other trees open or close a folder, go up on ../, and pick a file (the saved query to open, the key file, the file the export replaces). In the export tree the top row (Enter) and Ctrl+S (any folder) save there, and . shows hidden names; n makes a folder (save as, folder chooser, export). Tab (and / in the folder chooser and the export tree) gives the keyboard to the name or filter field; Esc closes (q too in the folder chooser and the export tree). The mouse: a click selects a row, one on a folder’s arrow opens or closes it; a second click on the selected row does what Enter does there, except in save as, where it only opens or closes a folder, and in the key file tree, where a path typed in the field is taken only by Enter; the wheel scrolls. Inside root.
Reserved (tree): j k h l Up Down Left Right Enter n / q Tab Shift+Tab Esc . Ctrl+S
overlay.script_tree.name [text]
The folder tree of the saved queries: its name field (save as) or filter (open); of the folder chooser: its filter; of the file system: its filter, where a text with / or a leading ~ is a path Enter goes to. While the field is empty, Left/Right on a folder or file act as in the tree: they close or open the folder, Left selects the folder the row is in. Inside root.
Reserved (tree: name or filter): Up Down Enter Tab Shift+Tab Esc Ctrl+S
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
overlay.context_menu [text]
The action menu: a right click (an explorer node, the grid, a tab, the editor) opens it at the pointer; Space Space, Shift+F10 or the Menu key next to the selection, Space t m under the active tab. The selection’s actions come first, then the pane’s, each with its key in that pane. Typing filters it (when nothing in it matches, every action is searched); the arrows, Ctrl+N/Ctrl+P and Tab move, Enter runs, Esc closes (Backspace on an empty filter does not); a shown key that is not a character (Ctrl+…, F…) runs its item; the pointer selects the item under it. Inside root.
Reserved (action menu): Down Up Ctrl+N Ctrl+P Tab Shift+Tab Enter Esc
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
overlay.password [text]
Password prompt; a click on the field, the checkbox, OK or Cancel. Inside root.
Reserved (password prompt): Esc Enter Tab Shift+Tab
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
overlay.confirm
Yes/no confirmation: y does it; Enter keeps what the answer would lose (quit, delete, disconnect, close, switch, replace, discard); for a copy it says yes. A button acts on its release (pressed and released on it, not right after the question appeared) and does what its key does; the pointer only lights its background (Enter stays on the safe one). Inside root.
Reserved (confirmation (Enter keeps what would be lost)): y n Enter Esc
overlay.run_confirm
Before statements that may do harm run: each is listed; Cancel has the focus, y or Enter on Run runs them, as a click on Run does (the pointer does not move the focus). Inside root.
Reserved (run confirmation (Cancel has the focus)): y n Enter Esc Tab Shift+Tab Left Right h l
overlay.tx_summary
What the open transaction did (its statements that may have written, rows, time; how long it has been open and idle): c commits, r rolls back, Esc closes; a click on a button does what its key does. Inside root.
Reserved (transaction summary): c r Esc q Enter j k Down Up
overlay.edit_preview
The statements an Apply is about to send, when they must be seen first (a choice among identical rows, an Apply whose result is not known, an environment that confirms writes); Cancel has the focus: y applies, c copies the statements, 1/a answer which of several identical rows a change is for, j/k scroll, Esc or Enter closes; a click on a button does what its key does. While the changes are applied the dialog says so, and the query’s cancel key cancels; it closes once they are applied, else it says why nothing was. After an Apply whose result is not known, the first y warns and the second applies again. Inside root.
Reserved (pending changes preview): y c 1 a Esc q Enter j k Down Up
overlay.session_view
What a profile’s query sessions start with (:session or :startup; in the profile form Ctrl+E, for its unsaved values), in the order they are applied, each with where it comes from: the app’s limits, the Startup SQL’s statements, the profile’s own; behind a pooler in transaction mode, how each PostgreSQL statement goes there. j/k scroll, Esc closes; a click on Close closes too. Inside root.
Reserved (session settings): Esc q Enter j k Down Up
overlay.whats_new
The release notes this build carries (:whatsnew; once by itself after an update, unless [updates] whats_new = "off"), newest version first: j/k scroll a row, Ctrl+D/Ctrl+U half a page, PageDown/Space/PageUp a page, g/G to the top or the end, ]/[ to the next or previous version, Esc, q or Enter closes; the wheel scrolls and a click on Close closes. Inside root.
Reserved (what’s new): Esc q Enter j k Down Up Ctrl+D Ctrl+U PageDown PageUp Space g G Home End ] [
overlay.update_notice
A newer release is out (at launch, from the last check; or :update): the version, the command that updates this install, and how to turn the checks off. Close has the focus: Enter presses the focused button, Esc or q closes (told again after a week), s skips this version, o turns the checks off ([updates] check = "off"); Tab, the arrows and h/l move the focus; a click on a button does what its key does. Inside root.
Reserved (the update notice (Close has the focus)): Esc q Enter s o Tab Shift+Tab Left Right h l
overlay.icons_ask
Whether the terminal shows the Nerd Font icons (asked once, with a preview); No has the focus; a click on Yes or No answers. Inside root.
Reserved (the icons question (No has the focus)): y n Enter Esc Tab Shift+Tab Left Right h l
overlay.busy
A notice that waits for background work (the launch-time move of passwords to the keychain); Ctrl+C quits there too. Inside root.
| Keys | Action | Description | R |
|---|---|---|---|
q | app.quit | Quit |
overlay.cell_viewer
Cell value viewer. Inside workspace.
Reserved (cell viewer): j k Down Up PageDown PageUp Space g g G Esc q
overlay.cell_editor [text]
The editor of a cell’s value of several lines (a value with line breaks, a JSON column’s, or Ctrl+L in the cell editor), in a box over the screen: the query editor’s vim modes, its / search, :s and registers (a yank goes to the system clipboard as [editor] clipboard says). In Normal mode Enter or :w (:wq, :x) keeps the text, as Enter in the cell editor does (line breaks as typed); Esc or :q (:q!) cancels. With cells of one column selected, each takes the value, as one change. A click outside the box or the wheel closes it, changing nothing. Inside root.
Reserved (cell editor of several lines): Esc Enter Tab Backspace Delete Left Right Up Down Home End Ctrl+W Ctrl+U Ctrl+R Ctrl+P Ctrl+V Ctrl+D Ctrl+F Ctrl+B
overlay.completion [text]
Completion popup in the editor (keys it does not use go to the editor). Inside root.
Reserved (completion popup): Up Down Ctrl+P Ctrl+N Tab Enter Esc
overlay.which_key
Which-key popup of an unfinished leader sequence (other keys continue the sequence). Inside root.
Reserved (which-key popup): Esc Backspace
overlay.help
Keyboard help: one list of every context; the sections of the context it was opened from come first, open; the pointer selects the row under it. Inside root.
Reserved (keyboard help): j k Down Up PageDown PageUp Enter l Right h Left / Esc q
overlay.help.filter [text]
The / filter of the keyboard help. Inside root.
Reserved (keyboard help filter): Esc Enter Down Up
Reserved (text input): Backspace Delete Left Right Home End Ctrl+U Ctrl+A
status.ask
The status bar’s one-line question before an Apply of a result’s changes: y applies them; any other key, a click or a paste cancels, and does nothing else. Inside root.
Reserved (the status bar’s question: y does it (c or 1: text with commas read as CSV or one column), any other key cancels): y c 1
Command line only
Actions without a default key (the command line finds them by name).
| Action | Description |
|---|---|
tunnel.new | New SSH tunnel (shared by profiles) |
env.new | New environment (rules, colour and badge that profiles share) |
session.new | New Startup SQL (settings a profile’s sessions start with) |
session.show | What a profile’s sessions start with, and where each setting comes from |
ui.language.en | Change language → English |
ui.language.ko | Change language → Korean |
ui.language.auto | Change language → Auto (system locale) |
ui.icons.toggle | Nerd Font icons → on/off |
ui.icons.on | Nerd Font icons → on |
ui.icons.off | Nerd Font icons → off |
ui.icons.auto | Nerd Font icons → ask again (with a preview) |
secrets.default.auto | New profiles’ password → auto (keychain when available) |
secrets.default.keychain | New profiles’ password → OS keychain |
secrets.default.file | New profiles’ password → secrets file |
secrets.default.command | New profiles’ password → command |
secrets.default.env | New profiles’ password → environment variable |
secrets.default.prompt | New profiles’ password → prompt on every connect |
help.all | Keyboard help: expand all |
app.whats_new | What’s new: the release notes of this version and the ones before |
app.update | Update: what the update check knows, and how to update |
explorer.context_menu | Explorer: actions of this node (menu) |
ddl.copy | DDL tab: copy the whole DDL |
Commands
Type them after : (or Ctrl+K). Tab/Shift+Tab, ↑/↓ or Ctrl+N/Ctrl+P pick an entry, Enter runs it (a command that still needs its argument is completed instead), Esc or Backspace on an empty line closes. Text that is not a command searches the actions by name. A command that cannot run shows an error and the line stays open.
Editor commands (from the editor, : starts the line with '<,'> in Visual mode and with .,.+2 after a count of 3, as Vim does; the command runs in the active tab’s editor):
- A line range alone goes to its last line (the first non-blank):
:12,:$,:+3,:-,:'a. Addresses are a number,.,$,'x(a mark:'a-'z,'<,'>,'','.), each with+n/-n;a,b,a;b(the second counted from the first) and%(every line). :[range]s/pattern/replacement/[flags](:substitute) replaces in the range’s lines (the cursor’s line without one) as one undo step; the cursor goes to the last line changed. The pattern is a Rust regular expression, as in search (an empty one is the last search’s), and becomes the last search. The replacement is Vim’s:&or\0the match,\1-\9a group (an absent one is empty),~the previous replacement,\ra line break (\ntoo; in Vim it is a NUL),\ta tab,\u\lthe next character upper or lower case,\U\Lup to\Eor\e, a backslash before any other character that character (\&,\~,\\, the delimiter);$1is plain text. Flags:g(every match in a line),i/I(ignore case, or not),e(no error when nothing matches),&first (the last flags again);c(confirm) is not supported and says so. Any delimiter that is not a letter, digit, blank,\,"or|works (:s#a/b#c#).:salone and:&repeat the last:swithout its flags,:&&with them. Nothing matched: “Pattern not found”.:[range]formatformats the range’s lines (:'<,'>format,:%format);:formatwithout a range, andSpace e f(editor.format), format the statement under the cursor or the Visual selection. The formatter is never run on its own. It changes only the layout: where lines break, the indent ([editor] format_indent) and, if asked, the case of keywords ([editor] format_keyword_case). Names, strings, comments and dollar-quoted bodies stay as written. When its result would differ in anything else (an operator written apart, strings joined across a line break,U&'…', a psql\command) it changes nothing and says on which line. One format is one undo step.
An editor command closes the line; what went wrong is said in the status bar.
| Command | Aliases | Description |
|---|---|---|
:conn <profile> | :connect | Connect to a profile |
:set <setting>=<value> | Change a setting | |
:help | :h | Keyboard help |
:run | Run statement under cursor | |
:cancel | Cancel running query | |
:quit | :q | Close the tab (on the last tab: quit) |
:qall | :qa :quitall | Quit |
:w [name] | :write | Save (a console asks for a name; with a name: save as) |
:wq [name] | :x | Save, then close the tab (on the last tab: quit) |
:e [name] | :edit | Open a saved query |
:tabnew | New console tab | |
:tabclose | Close tab | |
:tabs | :ls :buffers | List tabs |
:tabmove [N / +N / -N / $] | :tabm | Move this tab (:tabmove N: after tab N, 0 first; +N / -N: N places; alone: last) |
:recover | Bring back a closed console (also from earlier runs) | |
:settings | Open the settings | |
:copy <format> | Copy the selected range, or every fetched row, as… (:copy <format> [selection|all]) | |
:use [db][.schema] | Choose this tab’s database and schema | |
:nohlsearch | :noh :nohl | Clear the search highlight in the editor (until the next search) and end the result grid’s search |
:suspend | :sus :stop | Suspend datarig (the shell’s fg brings it back) |
:format | Format the statement under the cursor or the selection (only its layout) | |
:ddl [schema.name] | Show the DDL of the explorer’s object, the table tab’s table, or a name (:ddl [schema.name]) | |
:monitor [profile] | Open the server monitor of the explorer’s or the active tab’s connection, or of one by name (:monitor [connection]) | |
:explain [analyze] | The plan of the statement under the cursor (:explain analyze runs and measures it; a write is rolled back) | |
:theme copy <from> <name> / new <name> / update [<name>] | Write a theme file: a full copy of a theme, a new one to fill in, or the tokens a file does not list | |
:export [format] [all / fetched / selection] [delim= quote= header= null=] [path] | Export the result to a file: alone, the export dialog; else every row by default, the format from the file’s extension (:export [format] [all|fetched|selection] [delim=… quote=… header=… null=…] [path]) | |
:commit | Commit the open transaction | |
:rollback | :rb | Roll back the open transaction |
:tx | What the open transaction did | |
:session | :startup | What a profile’s sessions start with, and where each setting comes from |
:changes | Show changes | |
:apply | Apply the pending changes (asked first) | |
:discard | Drop every pending change of the result | |
:whatsnew | What’s new: the release notes of this version and the ones before | |
:update | Update: what the update check knows, and how to update |
Settings of :set (saved to config.toml like the matching actions):
| Setting | Values | Description |
|---|---|---|
language | en ko auto | UI language |
icons | on off auto | Nerd Font icons |
secrets.default_source | auto keychain file command env prompt | Password storage of new profiles |
commands.position | popup bottom | Command line position |
detail_view | panel statusbar | Cell detail |
clipboard | auto system osc52 | Clipboard |
copy_header | auto on off | Column names in copies |
editor.cursor_shape | on off | Cursor shape |
editor.clipboard | on off | Yanks to the clipboard |
theme | terminal dark light high-contrast catppuccin catppuccin-latte catppuccin-mocha tokyo-night tokyo-night-day tokyo-night-night gruvbox gruvbox-light gruvbox-dark nord dracula or the name of a theme file | Theme |
editor.format_keyword_case | preserve upper lower | Formatter: keywords |
editor.format_indent | 4 2 | Formatter: indent |
editor.auto_pairs | off on | Auto-pairs |
editor.soft_tab_backspace | on off | Backspace to tab stop |
editor.run_hints | on off | Run hints |
secrets.macos_keychain_access | direct security_tool | macOS keychain access |
explorer.size_heat | 3 5 off | Explorer: size colours |
ui.yank_flash | on off | Flash copied cells |
ui.changes_view | list sql | Changes tab view |
edit.paste_null | \N NULL none | Paste NULL token |
ui.tips | always until_used off | Tips |
ui.background | theme terminal | Background |
idle_disconnect | off 5m 10m 30m 1h | Close idle connections |
lock_wait | off 5s 10s 30s 1m | Lock wait limit |
editor.completion_delay_ms | 60 0 30 100 150 250 | Completion delay (ms) |
updates.check | on off | Check for updates |
updates.whats_new | on off | What’s new |
copy_format | tsv csv json json_pretty markdown html xml insert | Format of y and Y copies |
monitor.interval | 1s 2s 5s 10s 30s 60s | Server monitor interval |
Configuration
The config file is --config <path>, else $XDG_CONFIG_HOME/datarig/config.toml, else ~/.config/datarig/config.toml (on every OS). Without it datarig starts with no profiles and creates it on the first save. The app edits the file in place: comments and key order are kept, a setting is written only when it differs from its default, and passwords are never written. A file with an unknown key, or a value a key does not take, is not used at all: datarig says which key and runs with the defaults, saving nothing.
Keys marked :set can also be changed from the : command line (:set <key>=<value>) and on the settings screen, which save them here.
Top level
Keys before the first table header.
| Key | Default | Values | Description |
|---|---|---|---|
version | 1, the original format | 1, 2 | The file’s format version. datarig writes 2 (every profile has an id) and upgrades a version 1 file once at launch; older versions of datarig cannot read version 2. |
language | "en" | "en", "ko", "auto" | The language of the whole UI; English unless set. Auto follows LC_ALL, LC_MESSAGES or LANG. Data and database messages are never translated. Also :set language. |
icons | "auto" | "auto", "on", "off" | Nerd Font glyphs for connections, key columns and marks. Needs a Nerd Font as the terminal font. Auto: not decided yet, so text marks until you answer the question with a preview (choosing auto asks again). Also :set icons. |
theme | "terminal" | a built-in theme’s name, or the name of a theme file | The colours of the whole UI. terminal uses your terminal’s own colours. catppuccin, tokyo-night and gruvbox follow the terminal’s background (light or dark); a variant name such as gruvbox-light pins one. Your own themes: themes/<name>.toml next to the config file. Also :set theme. |
page_size | 500 | 1 to 2147483647 | Rows a result fetches from the server at a time (one page of the grid). |
result_window_rows | 10000 | 1000 to 10000000 | Rows of a result kept in memory; the rest go to a temporary file on disk. |
spill_limit | "1GB" | bytes as a number, or "512MB", "2GB" (KB, MB and GB are powers of 1024); 0 or "off" for no limit | The most a result’s temporary file may take; fetching stops when it is reached. An environment may set its own. |
idle_disconnect | "off" | a duration from "1m" to "24h" ("300s", "5m"), or 0 or "off" | How long a connection may sit unused before the app closes it: each tab’s session on its own, then the connection’s other sessions and its SSH tunnel. The next run or explorer action connects again. A tab with an open transaction is warned a minute before; the server rolls the transaction back when its session closes. The server also gets a limit a minute longer for each query session (PostgreSQL 14 or later on a direct connection, MySQL). An environment can set its own. Startup SQL may set the server’s idle limit itself (PostgreSQL idle_session_timeout, MySQL wait_timeout): that value is then the limit of those sessions. Also :set idle_disconnect. |
lock_wait | "off" | a duration from "1s" to "24h" ("5s", "1m"), or 0 or "off" | How long a statement of a query session waits for a lock another session holds before the server cancels it (PostgreSQL lock_timeout; MySQL lock_wait_timeout and innodb_lock_wait_timeout). Off keeps the server’s setting. Explorer lookups keep their own two seconds. An environment can set its own, and a Startup SQL or a profile’s own startup SQL its own for its sessions. Also :set lock_wait. |
folders | [] | a list of folder paths ("work/prod") | Every folder of the explorer, so that empty folders are kept. A profile’s own folder adds its folder too. |
last_used | none | a profile’s id | Written by datarig: the profile connected to most recently, where the explorer’s cursor starts. A version 1 file has the profile’s name here. |
detail_view | "panel" | "panel", "statusbar" | How the result inspector (i in the grid) shows the selected cell: a panel with Cell and Row tabs, or a one-line preview in the status bar. Also :set detail_view. |
clipboard | "auto" | "auto", "system", "osc52" | How copies reach the clipboard. OSC 52 goes through the terminal, so it also works over SSH; inside tmux it needs set -g set-clipboard on. Also :set clipboard. |
copy_header | "auto" | "auto", "on", "off" | Whether TSV copies of y and Y and CSV copies start with a row of column names. Auto: when more than one row is copied (CSV: always, unless off). Also :set copy_header. |
copy_format | "tsv" | "tsv", "csv", "json", "json_pretty", "markdown", "html", "xml", "insert" | The format y and Y copy a selection or a row in; one cell without a selection is copied as its value. SQL INSERT needs rows that are certainly of one table, else the copy is TSV and says why. g p pastes the copied cells whatever the format. Also :set copy_format. |
osc52_max_bytes | 100000 | a positive number of bytes | The longest copy sent through OSC 52 (bytes of base64). A longer one is not sent: terminals and tmux drop long OSC 52 sequences without a word. |
[editor]
The query editor.
| Key | Default | Values | Description |
|---|---|---|---|
mode | any | Retired (the editor has vim keys only): accepted with any value, ignored, and dropped the next time the app saves the file. | |
cursor_shape | "on" | "on", "off" | The cursor shows the editor’s mode: a block in Normal and Visual, a bar in Insert and while typing into any text field. Off leaves the terminal’s own cursor alone. Also :set editor.cursor_shape. |
clipboard | "on" | "on", "off" | Whether y, d, c, x and the other yanks and deletes that name no register also go to the system clipboard, as in Vim with clipboard=unnamedplus (the clipboard setting decides how they get there). “+ and “* always do, “a to “z and “_ never. “+p reads the clipboard only when you type it. Also :set editor.clipboard. |
format_keyword_case | "preserve" | "preserve", "upper", "lower" | How the formatter (Space e f, :format) writes the words the editor highlights as keywords. Names, strings and comments are never changed. Also :set editor.format_keyword_case. |
format_indent | 4 | 4, 2 | How many spaces the formatter indents each level with. Also :set editor.format_indent. |
completion_delay_ms | 60 | 60, 0, 30, 100, 150, 250 | How long the completion popup waits after the last key typed in Insert mode before it opens by itself (once a name has 2 characters, after a dot or an opening quote). Nothing opens before it; the completion key (editor.complete) opens the popup at once whatever the delay. Also :set editor.completion_delay_ms. |
auto_pairs | "off" | "off", "on" | Whether ( [ { ’ “ and typed in Insert mode also put their closing character after the cursor (typing it there steps over it, Backspace deletes the empty pair). Never inside strings or comments, before a word, or for a paste. Off is how Vim types. Also:set editor.auto_pairs`. |
soft_tab_backspace | "on" | "on", "off" | Whether Backspace in Insert mode deletes the spaces before the cursor back to the tab stop (every 4 columns) at once, as Tab types them, when nothing but spaces lies between (as Vim’s softtabstop=4 does then). With anything else between, such as a letter or a tab character, Backspace deletes one character. Also :set editor.soft_tab_backspace. |
run_hints | "on" | "on", "off" | Whether a dim hint after a statement’s last line says what its last run did (rows, rows affected, the error, rolled back or cancelled, how long it took and when). The hint is not part of the text: it is never yanked, saved or searched, and it goes when the statement is edited. Also :set editor.run_hints. |
[explorer]
The explorer.
| Key | Default | Values | Description |
|---|---|---|---|
size_heat | 3 | 3, 5, "off" ("on", its old value, reads as 3) | Whether, and in how many levels, the explorer colours the estimates of the largest tables of each schema: the largest tenth by size (one at least). 3 levels: amber from 1 MiB, red from 1 GiB. 5 levels: yellow from 1 MiB, amber from 100 MiB, orange from 1 GiB, red from 10 GiB. Only the estimates already read are used; nothing more is asked of the server. Also :set explorer.size_heat. |
[ui]
Things the whole screen shows.
| Key | Default | Values | Description |
|---|---|---|---|
yank_flash | "on" | "on", "off" | Whether the cells a copy took from the results grid flash briefly once the copy was sent to the clipboard. A copy that fails or is refused does not flash. Also :set ui.yank_flash. |
tips | "always" | "always", "until_used", "off" ("on", its old value, reads as "always"; true and false read as "always" and "off") | Whether empty places show a tip about a key (an empty editor or result, the workspace without tabs, spare room in the status bar), the welcome panel its first steps, and an action run from a menu a few times names its key once. Always: the tips of a place take turns and keep coming back; a tip about a key you already used shows less often. Until used: a tip goes away once you used its key. Tips follow the key bindings, remapped keys included. Also :set ui.tips. |
changes_view | "list" | "list", "sql" | What the Changes tab of a result shows first: one line per changed cell, or the statements that apply the changes. In the Changes tab, s switches between them for this run. Also :set ui.changes_view. |
background | "theme" | "theme", "terminal" | Whether the theme paints the panes’ background, or leaves your terminal’s own showing (a translucent or image background shows through). Text, selection and edit colours stay the theme’s; the editor’s current line and statement and the grid’s cursor row show in the gutter instead of as a band; dialogs, menus and popups keep the theme’s surface. Also :set ui.background. |
[edit]
Editing a result’s rows.
| Key | Default | Values | Description |
|---|---|---|---|
paste_null | "\\N" | "\\N", "NULL", "none" | The text an unquoted field of a range pasted into a result reads as NULL. An empty field is an empty value, never NULL. Also :set edit.paste_null. |
[updates]
The update check and What’s new.
| Key | Default | Values | Description |
|---|---|---|---|
check | "on" | "on", "off" | Once a day, ask GitHub whether a newer release of datarig exists: one HTTPS request to api.github.com, with nothing about you or your databases. :update says what the last check found. Also :set updates.check. |
whats_new | "on" | "on", "off" | Whether the release notes of the new version show once by themselves on the first start after an update. :whatsnew shows them any time. They come with the program: nothing is fetched. Also :set updates.whats_new. |
[monitor]
The server monitor.
| Key | Default | Values | Description |
|---|---|---|---|
interval | "2s" | "1s", "2s", "5s", "10s", "30s", "60s" | How often a new Monitor tab reads the server while it is shown (+ and - change it for one tab). Also :set monitor.interval. |
[secrets]
Passwords and the OS keychain.
| Key | Default | Values | Description |
|---|---|---|---|
default_source | "auto" | "auto", "keychain", "file", "command", "env", "prompt" | Where a new profile keeps its password. Auto uses the OS keychain when it works, else asks on every connect. Also :set secrets.default_source. |
macos_keychain_access | "direct" | "direct", "security_tool" | macOS only (no effect elsewhere). Direct: macOS ties Always Allow to the datarig binary, and releases are unsigned, so it asks for your login password again after each upgrade. Through /usr/bin/security: Always Allow given to Apple’s tool lasts across upgrades, but any program running as you can then read these passwords with it, without being asked. Also :set secrets.macos_keychain_access. |
[commands]
The : command line.
| Key | Default | Values | Description |
|---|---|---|---|
position | "popup" | "popup", "bottom" | Where the : command line opens: a box near the top with the matches below the input, or the last line with the matches above it. Also :set commands.position. |
[[connections]]
One table per connection profile, in the explorer’s order. Passwords are never written here: password_source says where a profile’s password comes from.
| Key | Default | Values | Description |
|---|---|---|---|
id | a new one, written by the launch-time upgrade | a UUID | The profile’s stable id: its keychain entries and last_used refer to it, so a rename keeps them. datarig assigns it; two profiles with one id are an error. |
name | "local-pg" | text | The name the explorer shows and datarig <profile> takes. |
driver | "postgres" | "postgres" (also "postgresql", "pg"), "mysql" (also "mariadb") | The kind of database. |
host | "127.0.0.1" | a host name or address | The server’s host. |
port | 5432 | a port number | The server’s port. Left out it is 5432 whatever the driver, so a MySQL profile says 3306. |
user | "" | text | The database user. |
password | none | text | A plaintext password from an older file. At launch it is moved to the profile’s password store (the keychain or the secrets file) and removed from the file; datarig never writes it. |
database | "" | text | The database to connect to. |
sslmode | PostgreSQL "disable"; MySQL "PREFERRED" ("DISABLED" with server_public_key_file) | PostgreSQL: "disable", "prefer", "require", "verify-ca", "verify-full"; MySQL: "DISABLED", "PREFERRED", "REQUIRED", "VERIFY_CA", "VERIFY_IDENTITY" | How the connection uses TLS, spelled as the database’s own client spells it. |
sslrootcert | none | a file path | The CA file the server’s certificate chain is checked against. |
sslcert | none | a file path | The client certificate file, used with sslkey. |
sslkey | none | a file path | The client certificate’s private key file. |
dsn | none | a connection string | A connection string kept as written because it cannot be turned into fields (not a URL, or parameters other than sslmode, sslrootcert, sslcert and sslkey). A URL of the profile’s driver with only those becomes fields when the file is read. |
folder | the top level | a folder path ("work/prod") | The explorer folder the profile is in. Folders only organise profiles. |
color | a colour picked from the name | "red", "orange", "yellow", "green", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown", "gray", or "#rrggbb" | The profile’s colour in the explorer and on its tabs. |
icon | the driver’s icon | the name of one of datarig’s icons | The Nerd Font icon next to the profile (an unknown name shows the driver’s icon). |
policy | the default rules | the name of a [policy.<env>] table | The profile’s environment. A name no environment has is an error of the profile: it does not connect. |
password_source | "keychain" | "keychain", "file" (the secrets file), "command", "env", "prompt" | Where the profile’s password comes from. |
password_command | none | a command | With password_source = "command" (required then): the command whose output is the password. |
password_env | none | an environment variable’s name | With password_source = "env" (required then): the variable that holds the password. |
statement_cache | true | true, false | Keep prepared statements on the server between transactions. Turn it off for a server behind a pooler in transaction mode that does not support them (PgBouncer before 1.22, or with max_prepared_statements = 0); the driver also turns it off for a connection when the server keeps losing them. |
server_public_key_file | none | a PEM file path | MySQL: the server’s RSA public key. A caching_sha2_password login over an unencrypted connection encrypts the password with it and never asks the server for a key. |
allow_public_key_retrieval | false | true, false | MySQL: whether a direct, unencrypted connection to a server that is not on this machine may ask the server for its public key. Whoever sits in between could send theirs and read the password, so such a login is refused when off. A loopback server and one reached through an SSH tunnel are asked either way. |
tunnel | a direct connection | the name of a [tunnels.<name>] table | The tunnel preset the profile connects through. A preset that is not there, or a preset and an enabled [connections.ssh] at once, is an error of the profile. |
startup | the preset its environment maps for its database, if any | the name of a [startup.<name>] table, or "none" | The Startup SQL preset its query sessions start with; "none": none, whatever its environment maps. A preset that is not there, or one for the other database, is an error of the profile. |
startup_sql | [] | a list of statements | The profile’s own session settings, run after the preset’s. Each must be one setting of the session; they are checked when the profile connects. |
session | Old name of startup, read the same; a table may not have both. | ||
session_sql | Old name of startup_sql, read the same; a table may not have both. |
[connections.ssh]
The SSH tunnel of the profile above it only (a preset several profiles share is a [tunnels.<name>] table). One bastion, never read from ~/.ssh/config. The table stays when the tunnel is turned off.
| Key | Default | Values | Description |
|---|---|---|---|
enabled | false | true, false | Whether the profile connects through this tunnel. |
host | none (required when enabled) | a host name or address | The bastion’s host. |
port | 22 | a port number (0 is refused when enabled) | The bastion’s SSH port. |
user | none (required when enabled) | text | The user on the bastion. |
auth | "key" | "key", "password", "agent", "keyboard-interactive" | How the tunnel logs in: a private key file (OpenSSH or PEM, maybe with a passphrase), a password, the keys of the ssh-agent, or the bastion’s own questions (a one-time code). |
key_file | none (required with auth = "key") | a file path (~ is the home directory) | The private key file. |
secret_source | "keychain" | as password_source | Where the key’s passphrase or the password comes from. It is kept apart from the database password. |
secret_command | none | a command | With secret_source = "command" (required then). |
secret_env | none | an environment variable’s name | With secret_source = "env" (required then). |
keepalive | 15 | seconds; 0 for none | Seconds between keepalives. |
timeout | 10 | seconds (0 counts as 1) | Seconds each step of opening the tunnel may take. |
[policy.<env>]
An environment that profiles name with policy = "<env>". Every key is optional; a key left out has its built-in default (not the one of [policy.default]). [policy.default] is not an environment: it holds the rules of every profile without one (its color and badge are not shown anywhere). A profile naming an environment that is not defined does not connect.
| Key | Default | Values | Description |
|---|---|---|---|
paging | "no_hold" | "no_hold", "hold" | What a result with more rows than a page keeps on the server, outside your own BEGIN block. "no_hold" keeps nothing: the next page runs the statement again (only a plain SELECT; any other statement shows its first page only). "hold" keeps the cursor, its transaction and its locks open until the result is read to its end or closed after paging_idle_timeout. |
paging_idle_timeout | "30s" | seconds as a number, or "90s", "5m", "1h"; 0 or "off" never closes it | With paging = "hold": how long a result that has more rows may keep its cursor open without a fetch. |
spill_limit | the top-level spill_limit | as the top-level spill_limit | The most a result’s temporary file may take for its profiles. |
read_only | false | true, false | Run only reads, transaction control and safe session settings, and refuse the rest before sending anything; the server also makes every transaction of the query sessions read-only. |
confirm | "destructive" | "destructive", "writes" | Which statements ask before they run: destructive ones (DROP, TRUNCATE, an UPDATE or DELETE of every row, …), or also every other statement that may write. Dangerous statements always ask. |
commit | "auto" | "auto", "manual" | "auto": each statement outside your own BEGIN block commits by itself. "manual": the first statement that may write opens a transaction that stays open until you commit or roll it back. A driver that cannot runs it as "auto". |
tx_idle_warning | "5m" | a duration as paging_idle_timeout takes, at least 10 seconds, or 0 or "off" | How long an open transaction may sit idle before the app warns that it holds locks. |
tx_idle_rollback | "30m" | a duration as paging_idle_timeout takes, at least 10 seconds, or 0 or "off" | How long a transaction opened for manual commit may sit idle before the app rolls it back. PostgreSQL’s server also gets the limit, a little later, so it ends there when the app is gone. |
tx_idle_timeout | Old name of tx_idle_rollback, read the same; a table may not have both. | ||
idle_disconnect | the top-level idle_disconnect | as the top-level idle_disconnect | How long its profiles’ connections may sit unused before the app closes their sessions. |
lock_wait | the top-level lock_wait | as the top-level lock_wait | How long a statement of its profiles’ query sessions waits for another session’s lock. A Startup SQL statement that sets the server’s own limit overrides it for those sessions. |
startup | none | { postgres = "<name>", mysql = "<name>" }, either left out | The Startup SQL preset its profiles’ query sessions start with, for each database. A profile may name its own, or "none". |
session | Old name of startup, read the same; a table may not have both. | ||
color | none | as a profile’s color | The environment’s color: its profiles’ names take it in the explorer, the tab list and the quick switcher, and its tabs’ titles in the tab bar. |
badge | none | text of at most 8 columns, no blanks around it | A short text its profiles’ tabs and the status bar show ("PROD"). |
[tunnels.<name>]
An SSH tunnel preset that profiles name with tunnel = "<name>": the keys of [connections.ssh] without enabled (a preset is always on), and an id.
| Key | Default | Values | Description |
|---|---|---|---|
id | a new one, written by datarig | a UUID | The preset’s stable id: its secret is kept under it, so a rename keeps it. |
host | none (required) | a host name or address | The bastion’s host. |
port | 22 | 1 to 65535 | The bastion’s SSH port. |
user | none (required) | text | The user on the bastion. |
auth | "key" | "key", "password", "agent", "keyboard-interactive" | As in [connections.ssh]. |
key_file | none (required with auth = "key") | a file path (~ is the home directory) | The private key file. |
secret_source | "keychain" | as password_source | Where the key’s passphrase or the password comes from. |
secret_command | none | a command | With secret_source = "command" (required then). |
secret_env | none | an environment variable’s name | With secret_source = "env" (required then). |
keepalive | 15 | seconds; 0 for none | Seconds between keepalives. |
timeout | 10 | seconds (0 counts as 1) | Seconds each step of opening the tunnel may take. |
[startup.<name>]
A Startup SQL preset: settings a query session of one kind of database starts with. Profiles name it with startup = "<name>", environments with startup = { postgres = "<name>", mysql = "<name>" }. The name none is reserved.
| Key | Default | Values | Description |
|---|---|---|---|
dbms | none (required) | "postgres", "mysql" | The kind of database the preset is for. |
sql | [] | a list of statements | The statements a query session starts with, in order. Each must be one setting of the session (in a read-only environment one a read-only session may change); they are checked when a profile connects. |
[sessions.<name>]
The old name of [startup.<name>], read the same; a file may not have both.
[keymap.<context>]
Key remapping for one context (see Keybindings for the context names and action ids). Invalid entries are skipped and reported at startup.
| Key | Default | Values | Description |
|---|---|---|---|
"<keys>" | the default bindings | an action id, or "none" to unbind | Binds the keys ("ctrl+e", "space c n": a space separates the keys of a sequence) in that context. |
datarig themes
The setting theme (:set theme=<name>, or the settings screen, Space ,) names a built-in theme or a theme file. A theme file is themes/<name>.toml next to the config file ($XDG_CONFIG_HOME/datarig/themes/, else ~/.config/datarig/themes/); its name is ASCII letters, digits, - and _ (at most 64), and never a built-in theme’s.
Built-in themes: terminal, dark, light, high-contrast, catppuccin, catppuccin-latte, catppuccin-mocha, tokyo-night, tokyo-night-day, tokyo-night-night, gruvbox, gruvbox-light, gruvbox-dark, nord, dracula.
The file
extends names the built-in theme the file starts from (terminal without it). [colors] sets colour tokens and [styles] style tokens; a token the file does not set keeps the value of the theme it extends, and a style it sets replaces that one whole. The dimming of the screen behind a dialog is not a token: it is the extended theme’s. An unknown token, colour or modifier is an error with its line, and the app then draws with terminal and says why.
- A colour is
#rrggbb, an ANSI name (black,red,green,yellow,blue,magenta,cyan,white,bright-black,bright-red,bright-green,bright-yellow,bright-blue,bright-magenta,bright-cyan,bright-white) ordefault(the terminal’s own). - A style is
{ fg = <color>, bg = <color>, modifiers = [...] }, each part optional; what it leaves out is not set. - Modifiers:
bold,dim,italic,underlined,reversed,crossed-out.
# themes/mine.toml, used with theme = "mine"
extends = "dark"
[colors]
accent = "#89b4fa"
warning = "yellow"
[styles]
selection = { bg = "#45475a", modifiers = ["bold"] }
syn_comment = { fg = "bright-black", modifiers = ["italic"] }
Writing a theme file
:theme copy <from> <name>writesthemes/<name>.tomlwith every token of the theme<from>(a built-in theme or a theme file; a family such ascatppuccinas the variant drawn now) set to its value, each under a comment saying what it colours.extendsnames the built-in theme<from>starts from, for the dimming behind dialogs; every token is in the file, so it stands on its own.:theme new <name>writesthemes/<name>.tomlextending the current theme when it is built in, elsedark, with every token commented out and showing the value it takes from there: remove the#in front of a key to set it.- On the settings screen,
con the theme row copies the theme shown there (the one being previewed, else the current one) and asks for the new name.
Each writes a new file and never replaces one: a name that is taken, that is not a valid file name, that is a built-in theme’s or a Windows device name such as nul (ignoring case) is refused. The themes folder is created when it is missing. Then datarig draws with the new theme, saves theme = "<name>" to the config file and shows the file’s path.
:theme update [<name>] adds to the theme file <name> (the current theme without one) the tokens it does not list, such as tokens added after it was written: each under its description at the end of its section, commented out with the value it takes now. No line of the file changes, and the file is replaced only when it reads exactly as before; the theme drawn and the config file stay as they are. A theme file that does not list every token is said once in the status line when it is used, and again only when the tokens it does not list change.
Colours ([colors])
| Token | What it colours |
|---|---|
bg | The background of the panes: explorer, editor, results. |
surface | The background of dialogs, popups, menus, the command line and the status bar. |
surface_alt | A second surface: every other row of the result grid, form fields and buttons. |
edit_inserted_row | The background of a new row of the result grid, not applied yet: every cell, the separators and the row’s number. Its values, its defaults (muted), the mark of a cell that needs a value and the gutter’s marks must read on it; the cursor and a selection stand apart from it. |
border | Borders and rules between panes and around dialogs. |
accent | The main accent: the focused pane’s border, dialog borders, labels and names to notice. |
accent_warm | The warm accent: values on the settings screen, an open transaction, keys in menus. |
fg | Body text. |
fg_muted | Secondary text: descriptions, labels and hints. |
fg_dim | Faint text: placeholders, empty lists and notes such as a database’s default. |
current_stmt_bar | The bar in the editor’s gutter next to the statement a run would take. |
running_stmt_bar | The bar and the spinner in the editor’s gutter next to the statement that runs now. |
success | Success: rows a run returned, a connected tunnel, a success message. |
warning | Warnings. |
error | Errors. |
null_fg | NULL in the result grid and the inspector. |
key_pk | The mark of a primary key column. |
key_fk | The mark of a foreign key column. |
key_uq | The mark of a unique key column. |
mode_normal | The background of the status bar’s mode badge in Normal mode. |
mode_insert | The background of the mode badge in Insert mode. |
mode_visual | The background of the mode badge in Visual mode. |
mode_command | The background of the mode badge while the command line is open. |
mode_fg | The text of the mode badge (bold, on the mode’s colour). |
chart_1 | The first series of a chart. |
chart_2 | The second series of a chart. |
chart_3 | The third series of a chart. |
chart_4 | The fourth series of a chart. |
chart_5 | The fifth series of a chart. |
chart_6 | The sixth series of a chart. |
Styles ([styles])
| Token | What it styles |
|---|---|
selection | The selected row or item, and the grid’s cursor cell. |
range | Cells and headers of a selected range in the result grid (the cursor’s cell keeps selection). |
cursor_line | The row under the cursor of an unfocused list or plan, the editor’s line, and (its background) the chart’s cursor while focused. |
grid_cursor_row | The result grid’s cursor row (its bg): every cell, the separators and the row’s number, and every row of a selected range of cells or of whole rows. Keep it subtle: apart from bg and surface_alt (the alternate rows), and from selection (the cursor’s cell), range and hover, with body and muted text reading on it. A file that leaves it out but sets bg or selection gets, when both are RGB, the selection blended into its bg. |
grid_cursor_header | The header of the result grid’s cursor column (both lines; patched over the header). Keep it apart from surface (the header) and range (the headers of selected columns), with body and muted text and the key marks reading on it. A file that leaves it out but sets surface or selection gets, when both are RGB, the selection blended into its surface. |
hover | What is under the mouse pointer: a list row, a grid cell, a tab, a button. |
yank_flash | Cells copied from the result grid, for a moment after the copy was sent to the clipboard (set fg and bg: it covers the cursor’s cell too). With RGB colours it fades back into each cell’s own colours; with terminal colours it goes off at once. |
current_stmt | A faint tint behind the statement a run would take. |
running_stmt | A tint behind the statement that runs now. |
run_hint | The hint after a statement’s last line that says what its last run did. |
syn_keyword | SQL keywords. |
syn_function | SQL function names. |
syn_string | SQL strings and dollar-quoted bodies. |
syn_number | SQL numbers. |
syn_comment | SQL comments. |
syn_operator | SQL operators and punctuation. |
syn_identifier | SQL names, parameters and variables. |
syn_quoted_ident | SQL quoted names. |
syn_exec_comment | The opening and closing of a MySQL executable comment (/*! … */). |
search_match | A match of the editor’s search on the lines on screen. |
match_paren | The bracket matching the one at the editor’s cursor (not drawn yet). |
read_only_mark | The mark of read-only rules (its fg): the lock on a profile’s row and tabs, and the rule’s words. |
danger_mark | The mark of something that changes or deletes data, or of a production profile (not drawn yet). |
plan_hot | The plan nodes that take the most time, in every view of a query plan and its summary. |
plan_misestimate | How far a plan node’s estimated rows are from the actual ones, in the plan, its summary, detail and funnel. |
size_warm | Explorer size estimates of 1 MiB or more (with five levels: 100 MiB or more). |
size_hot | Explorer size estimates of 1 GiB or more (with five levels: 10 GiB or more). |
size_mild | With five size levels, explorer size estimates of 1 MiB or more. |
size_heated | With five size levels, explorer size estimates of 1 GiB or more. |
tab_active | The active tab’s fill (its bg; the alternate surface without one). An fg replaces every text colour on it but the environment’s badge (a connected tab’s number becomes a chip of its profile’s colour); without one the tab’s colours stay, made to read on it. A file that leaves it out but sets bg or accent, both RGB, gets that accent blended into that bg; else the fill of the theme it extends stays. |
grid_match | A cell of the result grid that the grid’s search matches (the whole cell is tinted: its value may be cut). Keep it apart from the selection, a selected range, the alternate rows, the hover and the copy flash. |
grid_match_current | The grid’s cursor cell while it is a match of the grid’s search. |
edit_changed | A cell of the result grid changed and not applied yet (its text: the cell keeps its background, so it must read on the alternate rows, the selection and a selected range). |
edit_pending | How many changes of a result are pending, on the result tab strip and the hint line. |
edit_inserted | The cells of a new row of the result grid, not applied yet (their text: the cell keeps its background, so it must read on the alternate rows, the selection and a selected range). |
edit_deleted | A row of the result grid to be deleted, not applied yet (its text; crossed out, so it shows without colour). |
edit_applied | A value of the result grid an Apply wrote, until the query runs again (its text: the cell keeps its background; italic, so it shows without colour). |
edit_invalid | A value a paste put in a cell of the result grid that its column does not take, not applied yet, and a new row’s cell that needs a value its column has no default for (its text: the cell keeps its background; underlined, so it shows without colour). |
Command line
datarig --help prints:
datarig: a full-featured database client that lives in your terminal (PostgreSQL and MySQL)
usage: datarig [--config <path>] [<profile>]
<profile> connect to this connection profile and open a console
(without it, nothing connects until you pick a profile)
--config <path> config file (default: $XDG_CONFIG_HOME/datarig/config.toml,
else ~/.config/datarig/config.toml)
-h, --help show this help
-V, --version show the version
environment:
DATARIG_SECRET_STORE=memory keep passwords in memory only, never touch the OS
keychain (default: keychain)
datarig --version (or -V) prints datarig and the version, as datarig 1.2.3. An option it does not know prints what is wrong and the usage line, and exits with status 2.
Environment variables
| Variable | What it does |
|---|---|
DATARIG_NO_UPDATE_CHECK | Any value (but an empty one): no update check in this process, whatever [updates] check says. Nothing is sent to GitHub. |
CI | Set (not empty): no update check, as on a CI service. |
HTTPS_PROXY | The HTTP proxy of the update check (http://[user:password@]host:port, or without http://); https_proxy when it is not set. Any other kind of proxy: no check, never a direct connection. |
ALL_PROXY | As HTTPS_PROXY, when neither it nor https_proxy is set; all_proxy when it is not set. |
NO_PROXY | Host names (with their subdomains) the update check reaches without the proxy, separated by commas; * for every host. no_proxy when it is not set. |
CARGO_HOME | Cargo’s home (else .cargo in the home directory): a datarig in its bin was built from the source, and the update notice names the commands for that. |
DATARIG_SECRET_STORE | memory keeps passwords in memory only: the OS keychain and the secrets file are never touched. keychain, or unset (or empty): the profiles’ own password stores. Any other value is an error and datarig does not start. |
XDG_CONFIG_HOME | The config file is $XDG_CONFIG_HOME/datarig/config.toml when it is set (and --config is not given). |
HOME | The home directory: the config file is ~/.config/datarig/config.toml without XDG_CONFIG_HOME, the data and state directories default to places under it, and ~ in an SSH key file is it. |
USERPROFILE | The home directory where HOME is not set (Windows). |
COLORTERM | truecolor or 24bit: the terminal draws 24-bit colours, and the themes’ tints are drawn as they are. |
TERM | A name with 256color in it (such as xterm-256color), and COLORTERM not saying 24-bit: a terminal of 256 colours, where the cursor and selection tints that the palette would draw as their background get a palette colour of their own. |
DATARIG_DATA_DIR | The data directory (saved queries, datarig’s own known_hosts), over every other rule. |
DATARIG_STATE_DIR | The state directory (restored tabs, console buffers, one-time notices, the log of deleted passwords, the keychain offer’s answer, what the tips remember), over every other rule. |
XDG_DATA_HOME | The data directory is $XDG_DATA_HOME/datarig when it is set, on every OS. Otherwise ~/.local/share/datarig (Linux and other Unix), ~/Library/Application Support/datarig (macOS) or %APPDATA%\datarig (Windows). |
XDG_STATE_HOME | The state directory is $XDG_STATE_HOME/datarig when it is set, on every OS. Otherwise ~/.local/state/datarig, ~/Library/Application Support/datarig/state or %LOCALAPPDATA%\datarig. |
APPDATA | Windows: the data directory’s default is %APPDATA%\datarig. |
LOCALAPPDATA | Windows: the state directory’s default is %LOCALAPPDATA%\datarig. |
LC_ALL | With language = "auto", the first of LC_ALL, LC_MESSAGES and LANG that is set picks the language: Korean when it starts with ko, else English. |
LC_MESSAGES | As LC_ALL, when that is not set. |
LANG | As LC_ALL, when neither it nor LC_MESSAGES is set. |
VISUAL | The editor a query is opened in to edit it outside datarig, split into words as a shell would. |
EDITOR | As VISUAL, when that is not set; without either, vi (notepad on Windows). |
SSH_CONNECTION | Set (or SSH_TTY): datarig runs in an SSH session, so clipboard = "auto" copies through OSC 52 (the terminal), and the macOS keychain offer is never made. |
SSH_TTY | As SSH_CONNECTION. |
SSH_CLIENT | Set: the macOS keychain offer is never made (as in an SSH session). |
SSH_AUTH_SOCK | The ssh-agent an SSH tunnel with auth = "agent" logs in with. |
TMUX | Set: the status bar says that an OSC 52 copy needs set -g set-clipboard on in tmux. |
A profile with password_source = "env" reads its password from the variable its password_env names, and an SSH tunnel with secret_source = "env" its secret from the one its secret_env names.
FAQ and troubleshooting
macOS asks for my login password after every upgrade
datarig’s release binaries are not signed, so macOS sees each upgrade as a new program and asks
again before it may read each saved password. Switch keychain access to Apple’s security tool:
:set secrets.macos_keychain_access=security_tool. Read the trade-off first; see
macOS: keychain access through security.
datarig also offers the switch once when it notices such a prompt.
The keychain does not work at all (headless Linux, SSH sessions)
When the OS keychain is unavailable, datarig asks for the password when connecting and keeps it
in memory only. To avoid the prompt, give the profile another source: password_source = "file"
(secrets.toml next to the config file), "command" or "env". See
Passwords.
Icons look wrong
The explorer, tab bar and status bar can use Nerd Font icons. datarig asks once, with a preview,
whether your terminal shows them, because a terminal’s name says nothing about its font. If you
answered wrongly, :set icons=off (or on) changes it, and :set icons=auto asks again. With
icons off, text marks are used instead (SSH, RO, ○, ●, ✕).
The screen says it must be larger
datarig needs a terminal of at least 80×24. A smaller window shows only that note until it is resized.
Colours look wrong, or the light/dark variant is wrong
- The default theme,
terminal, uses your terminal’s own 16 colours and background. Pick a truecolor theme with:set theme=dark(or any name on Themes). catppuccin,tokyo-nightandgruvboxfollow the terminal’s background, which datarig asks once at startup (OSC 11). A terminal that does not answer counts as dark; the settings screen (Space ,) says what it found. Pick the variant directly (gruvbox-light, …) to override it.- An error in a theme file is reported with the file and line, and the app draws with
terminaluntil it is fixed.
Ctrl+Enter does nothing
Ctrl+Enter reaches datarig only in terminals that support the kitty keyboard protocol.
Ctrl+E runs the statement under the cursor everywhere.
Copying does not reach my clipboard over SSH
With clipboard = "auto" (the default), datarig uses OSC 52 in an SSH session and the system
clipboard otherwise. Your terminal must allow OSC 52; osc52_max_bytes caps how much is sent.
"+p in the editor cannot read the clipboard through OSC 52; use your terminal’s paste.
MySQL or MariaDB: something is missing
MySQL support is partial and MariaDB is a best effort (10.6 and newer). MySQL 5.7 and older are
refused. See MySQL for what it supports and what not, including the plan
view (not on MariaDB) and a typed USE (use :use).
Why is my result read-only?
The edit key (e, o, D, …) says why in the status bar: “Read-only: …” or “Can’t edit: …”.
Once the table’s details are read, the result tab strip also says read-only · <reason> for a
reason about the statement, the table or the rows; a reason about the connection (a read-only
environment, a server that can’t edit, a changed connection, no connection) is said by the key
only. The usual causes:
- The statement is not one
SELECTof one plain table: a join,WITH,GROUP BYorDISTINCT, a subquery,UNION, a view. - The result has no key of the table and no column that compares exactly, so its rows can’t be
found again: add the primary key to the
SELECT. - The profile’s environment is read-only, or the server cannot edit (MariaDB, MySQL before 8.0.17).
- The tab’s connection changed since the rows were read, or changes were already applied to them: run the query again.
Every reason is listed on Editing data.
Why was my paste refused?
A range pasted into the grid is refused as a whole, never shifted or cut, and the status line says why (“Not pasted: …”). The usual causes:
- It is wider than the columns from the cursor to the last column: put the cursor further left.
- A column in the range takes no value (computed, generated, an identity always generated), or a row in it is marked for deletion.
- It runs past the last row read while the server has more rows: fetch them first, or paste fewer rows (otherwise the rows meant to be overwritten would become new rows). Where the result’s cursor is no longer open, paste fewer rows.
- It runs past the end where the table can’t take new rows from this result.
- The text can’t be read: rows of different widths, a quote never closed, a carriage return
alone.
Space d Vreads the same text again as the next format (TSV, CSV, one value). - Text with commas and no tab is not pasted until you answer: the status line asks whether to
read it as CSV (
c) or as one column (1), since the commas can be the values’ own. - It is larger than 5,000 rows or 1 MiB.
- The result can’t be edited at all (see above).
A paste that went in but shows ! in the gutter has values their columns don’t take: they are
kept, listed first in the Changes tab, and block the Apply until fixed (e), dropped
(Space d r) or undone. An empty field is an empty value: paste \N for NULL. See
Pasting a range.
COPY … FROM STDIN or COPY … TO STDOUT fails
They are not sent: their data would go through PostgreSQL’s COPY sub-protocol, which the session does not carry. Use Export to write a result to a file.
A profile does not connect and names its environment, preset or tunnel
A profile that names an environment, a Startup SQL preset or a tunnel preset that does not exist is an error of that profile. It never connects with the default rules, without the preset, or directly instead. Pick an existing one in the profile’s form.
A second datarig does not restore my tabs
Only one instance keeps the workspace (tabs, console buffers) at a time. A second instance started while the first runs restores no tabs and writes none of the workspace files.
Does datarig contact the internet?
Only to check for a newer release: once a day at most, one HTTPS request to api.github.com
with nothing about you, your connections or your databases, through your HTTPS_PROXY when one
is set. Turn it off with :set updates.check=off or DATARIG_NO_UPDATE_CHECK=1; see
Updates for exactly what is sent. What’s new comes with the program and needs
no network. Everything else datarig connects to is what you configure: your databases, and the
SSH hosts of your tunnels.
Where does datarig keep its files?
| What | Where |
|---|---|
| Config file | --config <path>, else $XDG_CONFIG_HOME/datarig/config.toml, else ~/.config/datarig/config.toml (on every OS) |
| Secrets file, theme files | Next to the config file: secrets.toml, themes/<name>.toml |
Data (saved queries, datarig’s own known_hosts) | ~/.local/share/datarig on Linux, ~/Library/Application Support/datarig on macOS, %APPDATA%\datarig on Windows |
State (tabs, console buffers, usage.toml, update.toml, keychain.toml) | ~/.local/state/datarig on Linux, ~/Library/Application Support/datarig/state on macOS, %LOCALAPPDATA%\datarig on Windows |
$XDG_DATA_HOME and $XDG_STATE_HOME, when set, take precedence on every OS (as
<dir>/datarig), and DATARIG_DATA_DIR / DATARIG_STATE_DIR override both. Windows paths are
untested.
usage.toml is what the tips remember: per action how often it ran
by key, from the action menu and from the command line, and which first steps are done. It
holds no times, statements or names of yours, is never sent anywhere, and is not written with
[ui] tips = "off". With [ui] tips = "until_used", delete it while datarig is not running to see every tip
again (with connections or a saved workspace there, the first steps stay done). If you used datarig before tips existed, the first
steps count as done.
update.toml keeps the newest version of datarig that ran on this machine, so
What’s new shows once after an update, and what the
update check learnt: when it last asked and how that ended, the latest
release, a version you skipped and when you last closed the notice. A second datarig open at the
same time reads it but never writes it.
The config file has an [editor] mode key
It is from an older version: the editor has vim keys only. It is ignored and dropped the next time the app saves the file.
Does datarig run on Windows?
CI builds it, lints it and runs the tests that need no database, but nobody has used it on Windows yet. The Windows notes in this manual describe how it is meant to work.
What’s new
What changed in each release, newest first. For 0.36.0 and earlier, see the release notes on GitHub Releases.
0.40.2 (2026-11-09)
Install now lists the packages for building datarig from source on Debian, Ubuntu, Fedora and Arch Linux, and the Monitor tab’s Locks view reads many MySQL metadata lock waits faster. Otherwise datarig works as before: its code is reorganised for easier development.
0.40.1 (2026-11-09)
The server monitor’s Overview charts statements by type, rows by operation and sessions by state, says when it sees only some sessions, and shows its statements in syntax colours.
Added
- The server monitor’s Overview charts statements per second by type on MySQL (select, insert, update, delete), rows per second by operation on PostgreSQL (read, inserted, updated, deleted), and the sessions of each state on both, with the monitor’s own statements taken out.
- The Monitor tab’s session details show the statement in the editor’s syntax colours and laid
out for reading by the SQL formatter, for the eyes only:
fshows it as the server sent it (or laid out again), andyandoalways take the server’s text. A statement known only in part (the list’s first 256 characters, or cut by the server), one the formatter would change more than its blanks, or one over 64 KiB is shown as sent, and the details say why.
Fixed
- The server monitor no longer reads as idle when the account sees only some sessions: the
longest running sessions are headed “this account” (MySQL without
PROCESS) or “visible sessions” (PostgreSQL with other roles’ sessions hidden), their empty text says the same, and a PostgreSQL sessions count with hidden sessions shows as the least there are. - The server monitor names the server’s role in words that cannot be taken for the profile’s
read-only mode:
primaryorstandbyon PostgreSQL,server takes writesorserver read-onlyon MySQL. - The details of the monitor’s own connection in the Monitor tab looked like any active session: they now name it (“this monitor”) and say that its statement is the monitor reading the server and that it cannot be cancelled or ended.
0.40.0 (2026-11-08)
A Monitor tab per connection shows sessions, locks and replication, and can cancel or end a session safely.
Added
- The Monitor tab (
Mon a connection,:monitor) shows a PostgreSQL or MySQL server’s load as charts and its sessions as a list you can sort and filter, read by a connection of its own only while the tab is shown. What the account may not see is said, with the privilege that would show it. - The Monitor tab’s session details read a long statement in whole, and show it only while it is still that session’s statement (on MySQL, once it has run for a few seconds); open in console and copy take the whole text too.
- The Monitor tab’s Locks view (
3) shows who waits for whom as a tree, root blockers first: what each session waits for and how long, what its blocker holds, prepared transactions that hold locks, and on MySQL the holders of metadata locks a request cannot be granted beside.bgoes to the root blocker of the selected wait. - The Monitor tab has a Replication view (
4): whether the server takes changes from another and how far behind it is (said only as far as the server can tell), the servers that take changes from it, PostgreSQL replication slots with the log they keep and logical subscriptions, and MySQL group replication members.ishows what the server is in full, with the settings that decide what the monitor can read. - The Monitor tab cancels a session’s statement (
c) or ends a session (X), asked first, never on a read-only environment; what was sent is checked by later reads and listed withL.
0.39.1 (2026-11-07)
Small fixes: explorer arrows open with a click, no opaque bands on a see-through terminal background, cursor tints that stay visible on 256 colours, and a copy format of your choice.
Added
copy_formatsets the formatyandYcopy in: TSV (the default), CSV, JSON, indented JSON, Markdown, HTML, XML or SQLINSERT. Choose it in the settings, with:set copy_format=csv, or withSpace r y d(also in the grid’s menu); the copy formats mark the current one, and the status bar names the format of each copy. SQLINSERTis used only for rows certainly of one table, else the copy is TSV and says why.g pstill pastes the copied values exactly, whatever the clipboard got.:theme updatelists your theme files to pick from (never a built-in theme), the current theme’s first.
Changed
- The selection summary on the status line keeps its figures while a whole-column selection grows as every row is fetched (or a range is extended downwards): only the new rows are read and added, and the figures so far stay on the line, marked “summing…”, instead of starting over with each page.
Fixed
- On terminals of 256 colours (
TERMsuch asxterm-256colorwithoutCOLORTERM=truecolor) the result grid’s cursor row, the cursor’s line and the other cursor and selection tints no longer vanish into the background: each gets a palette colour of its own. - A click on an explorer row’s arrow opens or closes the row, for every kind of row: on a table it shows the table’s structure. The arrow under the pointer is highlighted.
- With
ui.background = "terminal"the editor no longer paints its current line and statement as opaque bands: the statement a run would take is an accent bar beside its lines with accent line numbers, and the cursor’s line number is bold. The result grid’s cursor row has no band either; its cell keeps its fill and its row number is bold.
0.39.0 (2026-11-07)
datarig can tell you when a newer release is out, and shows what’s new after an update.
Added
- Once a day datarig asks GitHub whether a newer release exists (one HTTPS request to
api.github.com, with nothing about you or your databases; through
HTTPS_PROXYwhen one is set). A newer one is told about at the next start, with the command that updates your install;Esccloses it (it comes back after a week),sskips that version,oturns the checks off.:updatesays what the last check found. Turn it off with[updates] check = "off"(settings screen, Updates),:set updates.check=offorDATARIG_NO_UPDATE_CHECK=1; there is no check on CI (CIset). - What’s new: on the first start after an update, a dialog shows the notes of every version
since the one that ran before (from the program itself, so it works offline).
:whatsnewshows it any time;[updates] whats_new = "off"(settings screen, Updates) stops it from showing by itself.
0.38.3 (2026-11-06)
Screen text uses British spelling, and titles use a colon in place of a dash; the docs site shows new recordings of the editor, charts and plans.
Changed
- Screen text uses British spelling (colour, materialised view; the connection form’s row reads
“Colour”) and no longer uses the em dash: titles read “Changes: Result 1 · shop.users”,
“Apply changes: shop.users” and “Edit connection: name”. Config keys, theme tokens and commands are unchanged (
color,[colors],:explain analyze).
0.38.2 (2026-11-05)
The grid shows where its cursor is and sums up a selected range on the status line; menus show what cannot run now, dimmed with why.
Added
- While a range, rows or columns of the result grid are selected, the status line sums them up: their size, then for numbers the sum, average, least and greatest value (integers and decimals exactly), else how many distinct values, and how many are NULL. It takes the values the grid shows (pending changes too) and reads the rows in the background, so a big selection never holds up the keys.
:theme update [<name>]adds to a theme file the tokens it does not list (such as tokens added after it was written), each under its description at the end of its section, commented out with the value it takes now. No line of the file changes, and nothing is written unless the file reads exactly as before.- A theme file that does not list every token is said once in the status line when it is used,
with how many and that
:theme updateadds them; again only when the tokens it does not list change. - New setting
ui.background = "theme" | "terminal"(settings screen,:set ui.background=terminal): withterminalthe panes and the result grid’s alternate rows leave the terminal’s own background showing, so a translucent terminal shows through; text, selections and edit marks keep the theme’s colours, and dialogs, menus and popups keep its surface. Turning it on with a theme that has a background of its own says so in one line.
Changed
- The editor’s completion popup opens by itself 60 ms after the last key (it waited 120 ms and
then up to a tenth of a second more for the next timer tick), exactly at that moment. New
setting
[editor] completion_delay_ms(60,0for at once,30,100,150,250; settings screen and:set). - The result grid shows where the cursor is: its row is tinted, its cell more strongly, and its
column’s header too. A selected range is drawn as one block (the separators between its
columns included), with its rows’ numbers and its columns’ headers tinted. New theme tokens
grid_cursor_rowandgrid_cursor_header; a theme file that sets its own background, surface or selection without them gets tints made of its own colours (when those are RGB). - The action menu shows an action that would act there but can’t now dimmed, with why in a few words where its key would be (a read-only environment, a result that can’t be edited, an edit that doesn’t fit the cell, a table tab’s Save, paging that closed), instead of leaving it out. The keys and the wheel step over it; a click on it says the reason in full and keeps the menu open.
0.38.1 (2026-11-05)
Copy and paste inside the grid: y keeps the copied cells as they are, and p pastes them as new rows (again and again); previous page moved from p to [.
Added
- The cells last copied with
yorYin the result grid paste back exactly as the grid showed them (NULL stays NULL), in any result in any tab, without reading any clipboard:padds them as new rows (each press another copy),g ppastes them over the cells at the cursor. Each is one stepuundoes. - A range pasted into the grid skips a first line that names the columns it lands in (a copy
with its header line), and says so;
Space d Vreads it again keeping that line.
Changed
- Previous page moved from
pto[in the result grid (and]is next page, asnstill is);pnow pastes the grid’s copy as new rows. - The Changes tab lists each problem under its row’s line, without the row’s name the line
above gives, and names a row the same way wherever a problem is about it (the statements,
the Apply’s messages and failures, a paste’s message, the cell editor and the inspector):
new 1,id=5orrow 31, not the row’s number in the grid. A new row lists only the values set, then how many columns are left to their default (DEFAULT ×2). Control characters in values, column names and the table’s name show as\n,\r,\tor\x07, and characters that turn the text’s direction as\u{202e}, never raw; the statements copied are unchanged. - The result grid marks every new row an Apply is refused for: a cell left to a default its
column lacks (
NOT NULL, no default) reads! value neededin the invalid-value colour instead of the dimdefault, its column widened to fit, and the row’s gutter shows!instead of+, as for a value its column does not take; so does a new row of a table with such a column the result does not show. The badge counts these cells, and the cell editor and the inspector say why. Invalid values keep their colour under the cursor. - A new row in the result grid is tinted as a whole (theme token
edit_inserted_row) and numbered after the rows read, with+(or!) after its number; rows an Apply added are numbered too. Its cells left to their default show what the server will put there instead of a dimdefault: the default’s value (ACTIVE,0),<generated>for a number the table draws or a generated column,<default: now()>for another default,NULLwhere the column takes it. The Apply still leaves them to the server.
0.38.0 (2026-11-04)
Pending changes get their own Changes tab, Apply asks on the status line, and a range copied elsewhere can be pasted into the result grid.
Added
- A range pasted into the result grid with the terminal’s own paste (
Cmd+V,Ctrl+Shift+V) becomes pending changes from the cursor or the selection’s corner: read as TSV, CSV, one column or one value (the status line names the format, andSpace d Vreads it again as the next one; text with commas and no tab asks first whether it is CSV,c, or one column,1), rows past the last added as new rows, one stepuundoes. One value fills every selected cell. A paste that does not fit (too wide, a column that takes no value, a row marked for deletion, rows past the last row read while the server has more) is refused whole, with the reason; one of more than 1,000 rows asks first.\Npastes NULL (settingedit.paste_null); an empty field is an empty value. Values a column does not take are kept, marked!in the gutter (theme tokenedit_invalid) and listed first in the Changes tab, and the changes are not applied until they are fixed.Space d vpastes the system clipboard withclipboard = "system", or"auto"outside SSH. For about a second after a status-line question or a waiting Apply ends without its answer,y,n,cand1do nothing in the results and say they were ignored, so a lateynever copies a cell over the clipboard.
Changed
- A result with pending changes has a Changes tab in its result strip: one line per changed
cell, every reason they can’t be applied yet first, each with its row;
Entershows the cell in the grid,sswitches to the statements that apply them,ccopies them exactly as they are sent.Space d pand:changesshow the tab.ui.changes_viewpicks the view the tab opens in. Space d aand:applyask on the status line instead of opening a dialog: onlyyapplies, any other key, a click or a paste cancels. Rows found by their values are counted again first (any key, a click or a paste meanwhile stops waiting); problems show in the Changes tab instead of a question. The dialog opens only for a choice among identical rows, an Apply whose result is unknown, and environments withconfirm = "writes". After an Apply the Changes tab shows what was applied and the statements sent until the query runs again, or why nothing was, the failing statement marked.
0.37.1 (2026-11-04)
This patch adds the edits of a cell to the grid’s right-click menu and makes the preview of pending changes readable.
Added
- The grid’s right-click menu (and
Space Space) lists the edits of the cell: edit it, set it to NULL or DEFAULT, add or copy a row, mark it to be deleted, and, with changes pending, undo, preview and discard. Only the edits that can act on the cell are listed.
Changed
- The preview of pending changes shows each statement in the editor’s syntax colors: a long one
wraps over as many lines as it needs, its expected count on its first line, and a literal of
more than a few hundred bytes shows its start and end around how many characters are left
out. On MySQL, a statement that compares text by its bytes (
_utf8mb4 X'…') is followed by a dim↳ set column = 'text' · where column = 'text'line that reads the values back; that line is not SQL, and Copy SQL copies the statements whole without it.
Fixed
- The keys on a dialog’s bottom border no longer run into its corner: a border cell is left on
each side, and keys too long for the dialog are cut with
…. :set ui.tips=onis taken asalways, as the config file already readstips = "on".
0.37.0 (2026-11-03)
This release adds editing a result’s data in the grid: change cells, add, copy and delete rows, then apply the changes after a preview of their exact statements.
Added
- Edit a result’s data in the grid: change cells (one line, or a box for values of several
lines and JSON), add and clone rows, mark rows to delete, undo and redo. The changes stay
pending until a preview shows the statements that apply them;
ythere runs them in one transaction, each row checked first. See Editing data in the guide.
Changed
- MySQL: the rows affected by an
UPDATE(and byINSERT … ON DUPLICATE KEY UPDATE) are the rows it matched, as with PostgreSQL, so a row set to the values it already had counts too;ROW_COUNT()counts them the same way. The server’s info line still says how many changed. - Tips keep showing: the tips of an empty place take turns and come back, and a key you have
already pressed comes up less often instead of never again.
[ui] tipsis now"always"(the default),"until_used"(the earlier behaviour) or"off";"on"in an existing config reads as"always", andtrueandfalseread as"always"and"off".
Fixed
- MySQL: Copy as SQL no longer refuses text and JSON values of a tab’s first result as “read across a change of the session’s time zone or character set”: the values a session reports as it connects no longer count as a change. An export of that result as SQL to a file is no longer refused for it either.
0.36.2 (2026-10-31)
Changed
- The hint line, the menus, the welcome panel and the messages name the same key for an action
as the tips do: a key of your own first, else one that needs no function key (
Space ?for the keyboard help,Space Spacefor the action menu), andF1whereSpacetypes text. - Startup SQL may set the server’s idle limit (PostgreSQL
idle_session_timeout, MySQLwait_timeout). Its value is then the idle disconnect of those sessions in place of the environment’sidle_disconnect: the app closes the tab after that long, warns about an open transaction a minute before, and:sessionshows the limit with what it overrides. Behind a pooler PostgreSQL’s is not sent and the environment’s limit stays.
0.36.1 (2026-10-30)
The documentation now lives at https://dokadev.github.io/datarig/, and datarig --help names
MySQL next to PostgreSQL.