Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 an INSERT, 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

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.toml pins 1.93.0, and rustup installs it automatically.

  • A C compiler, clang and libclang, for the pg_query crate: 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 Linux
    

    On Windows (untested outside CI), install LLVM and set LIBCLANG_PATH if 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

  1. Ctrl+O opens quick connect: type part of a profile’s name and press Enter. A console tab on that connection opens.
  2. Press i to type (the editor has vim keys; Esc goes back to Normal mode).
  3. Ctrl+E runs the statement under the cursor. Ctrl+C cancels a running statement.
  4. The result opens below the editor. n (or ]) and [ in the grid move to the next and previous page; Tab and F6 move 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 Space and wait: a popup lists the keys that may follow.
  • F1 or Space ? opens the keyboard help for the current pane, with search.
  • Space Space opens the action menu: what you can do with the selected thing, each with its key. A right click opens it too.
  • : (outside text input) or Ctrl+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 than F1), except where that key types text (in insert mode the help is F1).

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; Enter goes to a tab or brings a closed one back, Ctrl+D closes the selected tab. Space t u reopens the last closed tab.
  • Reorder tabs by dragging one with the mouse, with Space t g and then h / l (Enter keeps, Esc puts it back), with Space t < / Space t >, or with :tabmove as in Vim (:tabmove 0 first, :tabmove last, :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 (or n in the explorer) opens a new profile form; e in the explorer edits the selected profile, c duplicates it, d deletes it, m moves it to a folder and N makes 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 with mysql:// (or mariadb://). A URL with several hosts is refused.
  • Test (t in the explorer, Space c t for 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 (or Space c c) opens quick connect: a fuzzy list of the profiles. Enter goes to the profile’s most recently used tab, or connects and opens a console when it has none. Right on a profile lists its server’s databases (and on a database its schemas), Left goes back; Enter there opens a console in that database or schema.
  • :conn <profile> does the same from the command line, and datarig <profile> at start.
  • o on a profile in the explorer opens a new console on it.
  • Every tab has its own database and schema: :use db.schema or Space c d picks them. Space c s moves the tab to another connection.
  • Space c x disconnects the tab’s connection and Space c r reconnects 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:

SourceWhere the password is
keychain (the default)The OS credential store: macOS Keychain, Linux Secret Service, Windows Credential Manager.
filesecrets.toml next to the config file, readable by you only (mode 0600).
commandThe standard output of password_command, for example a password manager’s CLI.
envThe environment variable named by password_env, read at connect time.
promptAsked 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 ~. Write sh -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 password found in an older config file is moved to the profile’s store at launch and removed from the file.
  • DATARIG_SECRET_STORE=memory keeps 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 whether security may 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 example security find-generic-password -s datarig -g) without being asked. With direct, 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, account profile:<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 direct keeps every password working; items saved while security_tool was on trust only security, 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_CLIENT or SSH_TTY set), in security_tool mode, with DATARIG_SECRET_STORE=memory, or once answered.
  • The answer is kept in keychain.toml in 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

authWhat 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.
passwordThe bastion account’s password.
agentYour running ssh-agent.
keyboard-interactiveThe 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 600 it. PuTTY keys (.ppk) are named as such; convert them with puttygen.
  • 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: n makes one, e edits it (renaming takes the profiles that name it along), c copies it, d deletes it, t tests it, and Enter lists the profiles connected through it now. The command line also finds the action tunnel.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-tcpip channel to each database address of the profiles that use it, closing each again. Nothing is sent to a database.
  • Save as tunnel preset (Ctrl+B in 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

PostgreSQLMySQLEncryptedWhat is checked of the server’s certificate
disableDISABLEDnevernothing
preferPREFERREDwhen the server offers TLSnothing
requireREQUIREDalwaysnothing; with a CA file, as verify-ca
verify-caVERIFY_CAalwaysits chain, against the CA file (needed)
verify-fullVERIFY_IDENTITYalwaysits 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 / PREFERRED goes 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_IDENTITY checks 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 mode verify-full whatever 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: use verify-full, or channel_binding=require in the URL.
  • Cancel requests (PostgreSQL) and the KILL QUERY connections (MySQL) are encrypted as the session is.

Defaults

  • A PostgreSQL profile without sslmode connects with disable, as it always has; the form writes prefer for a new profile.
  • A MySQL profile without a mode uses PREFERRED, except one that names a server public key file, which uses DISABLED (see MySQL). In a MySQL profile, disable and prefer (what older versions of the form wrote) count as no mode: write DISABLED to 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-full against 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_wait is sent as SET LOCAL lock_timeout in each transaction instead of once for the session (see Environments and safety).
  • idle_disconnect gets no server-side idle_session_timeout behind a pooler; datarig’s own timer still closes the tab’s session. A Startup SQL’s idle_session_timeout is not sent there either, so the environment’s idle_disconnect stays 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 own SET and RESET run 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_rollback is set with SET 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, an UPDATE or DELETE without a real WHERE, ALTER TABLE … DROP COLUMN, ALTER … TYPE, MERGE that updates or deletes, DO and CALL, COPY to 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 with confirm = "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 a SELECT cannot 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 before y applies 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 ANALYZE of 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:

PresetRulesColourBadge
devthe defaultsgreen
testthe defaultsblue
stagingevery write asks, manual commitorangeSTG
prodread-only, every write asks, manual commitredPROD

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"
# ...
KeyValuesDefault
read_onlytrue, falsefalse
confirm"destructive", "writes""destructive"
commit"auto", "manual""auto"
tx_idle_warninga duration of at least 10 s, or "off""5m"
tx_idle_rollbacka duration of at least 10 s, or "off""30m"
paging"no_hold", "hold""no_hold"
paging_idle_timeoutseconds, or "90s", "5m", "1h", "off""30s"
spill_limitbytes, 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 outnone
colorred, orange, yellow, green, teal, cyan, blue, indigo, purple, pink, brown, gray, or #rrggbbnone
badgeup to 8 columns, no blanks around itnone

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 color or badge is 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. A color or badge in 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 pageThe cursor and its transaction end in the same round tripThe cursor stays open in a transaction of its own
Held on the server while you readNothing: no transaction, no lock, no snapshotThe 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 untilNothing is heldThe 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).

KeyValuesWhat 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), MySQL wait_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_sql may set that server limit itself (PostgreSQL SET idle_session_timeout = '10min', MySQL SET SESSION wait_timeout = 600): its value is then those sessions’ idle disconnect, and :session shows 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 is SET LOCAL in each transaction instead. On a direct connection, a RESET ALL or DISCARD ALL you 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) and innodb_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_timeout is on: then the whole transaction, and datarig says so).
  • A Startup SQL or a profile’s own startup_sql may set the lock wait itself (PostgreSQL SET lock_timeout = '3s'; MySQL SET 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’s 0 means 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.
  • :session shows 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 SELECT the 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, a SELECT that calls a function of yours or a volatile one, CALL, DO, an EXECUTE of a prepared write, text the parser rejects. Session settings, transaction control and EXPLAIN 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 the UPDATE, and the last SELECT sees the update. Results read in it are labelled uncommitted.
  • Your own BEGIN still works as before, with the server’s rules.
  • With read_only = true no 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

KeysCommandWhat it does
Space x c:commitCommit
Space x r:rollback (:rb)Roll back
Space x s:txWhat 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>]:

KeyValuesDefault
commit"auto", "manual""auto"
tx_idle_warninga duration ("90s", "5m", "1h"; at least 10 s) or "off""5m"
tx_idle_rollbacka 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 adds idle 5:00 · holding locks in 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 sends ROLLBACK on 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, unless tx_idle_warning already did. The limit is the one the transaction began with. Time asleep counts once the machine wakes. With a shorter idle_disconnect, that closes the session first. Your own BEGIN is not covered.
    • If the ROLLBACK fails 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_timeout with the BEGIN, 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.
  • 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 BEGIN block 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 :tx lists 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, RELEASE or ROLLBACK TO you 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:

  • c commits first, r rolls back first, y goes on anyway (the server rolls back), Enter or n stays. A failed transaction offers only r and y.
  • After c or r it 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 TABLES is refused with manual commit. A CALL of a procedure that runs DDL commits the transaction without asking (datarig says so afterwards). A SET autocommit = 0 of 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_rollback is 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_rollback needs PostgreSQL 9.6 or later; older servers refuse it and the first write fails. Set it to "off" there. Savepoint names starting with datarig_ 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:

  • Enter on 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/Down go from row to row, Tab leaves the list, Backspace on an empty row or Ctrl+D on 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 with SET. 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:

  1. datarig’s own limits: lock_wait, idle_disconnect, tx_idle_rollback (Environments and safety, Manual commit);
  2. the preset: the profile’s startup, else the one its environment maps for its database ([policy.default] may map one too);
  3. 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 0 turns 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’s idle_disconnect (the server does not count it inside a transaction). On MySQL wait_timeout must 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_disconnect stays there.
  • In a read-only environment PostgreSQL’s is allowed; MySQL’s wait_timeout is not (an idle limit is not among the settings a read-only session may change there).
  • MySQL’s interactive_timeout is 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 (or TO, TO DEFAULT), SET TIME ZONE …, SET SESSION CHARACTERISTICS AS TRANSACTION …; MySQL SET [SESSION | LOCAL] name = value[, …], SET @var = …, SET SESSION TRANSACTION …. Not SET LOCAL, a plain SET TRANSACTION, SET GLOBAL or PERSIST, RESET, DISCARD, a query or a call such as set_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; set wait_timeout (see Idle limit) or the environment’s idle_disconnect.
    • idle_in_transaction_session_timeout: use tx_idle_rollback.
    • the read-only settings (default_transaction_read_only, transaction_read_only, MySQL’s tx_read_only, … TRANSACTION READ ONLY/WRITE): use the environment’s read_only.
    • MySQL’s autocommit and completion_type: use the environment’s commit.
    • 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_connection and SET NAMES, DateStyle, IntervalStyle, bytea_output, extra_float_digits, standard_conforming_strings, application_name, MySQL’s sql_select_limit, resultset_metadata and session_track_*.

    The lock wait settings, the server’s idle limit (idle_session_timeout, MySQL’s wait_timeout) and MySQL’s max_execution_time are allowed. Settings of one transaction only are refused too, with where the session’s default is: PostgreSQL’s transaction_isolation and transaction_deferrable (use default_transaction_isolation, …), MySQL’s SET @@transaction_isolation without a scope (use SET SESSION …).

  • MySQL in every sql mode. A statement is read with and without ANSI_QUOTES and NO_BACKSLASH_ESCAPES; what any reading refuses is refused.

  • Who the session is. SET ROLE is allowed in a read-only environment (every transaction stays READ ONLY whatever the role); SET SESSION AUTHORIZATION is 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 (w b e, 0 ^ $, gg G, f t ;, %, { }, H M L, …), the operators d c y, gu gU g~ and > < with any motion or text object (iw aw, i( a(, i", ip, …), doubled for lines (dd, yy), x s D C p P r J ~, u and Ctrl+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, and I/A type on every line of the block.
  • Insert mode keeps the usual editing keys, plus Ctrl+W, Ctrl+U and Ctrl+R with a register. Enter keeps the line’s indent. Tab types 4 spaces, and Backspace after spaces deletes back to the tab stop in one press, as Vim with softtabstop=4 does ([editor] soft_tab_backspace = "off" always deletes one character).
  • Registers as in Vim ("a-"z, "0, the "1-"9 delete ring, "-, "_, "., "/, "+/"*). As with clipboard=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. "+p reads 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), n N, * #, 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), \b for a word boundary, and a match never spans two lines.
  • Marks: m{a-z}, 'a and `a, '', '< '> and '.. Marks follow their lines through edits and come back on undo.
  • Comments: gc comments lines out with -- or back in, as Neovim does (gcc, gc{motion}, gc in Visual mode; also Space 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/g or :'<,'>s/^/-- /. The pattern is a Rust regular expression, the replacement Vim’s (&, \1, \r, \u, ~; $1 is plain text). Flags: g, i, I, e; c (confirm) is not supported. :&& and & / g& in Normal mode repeat the last substitution.
  • :[range]format formats 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 WITH queries 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 (oi finds order_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 (or Ctrl+Enter where 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+C cancels. 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 .sql files in folders in the data directory: Space s s or :w saves (a console asks for a name), Space s a saves as, Space s o or :e opens one.
  • Every tab is autosaved, and the workspace (tabs, cursors, open folders) is restored at the next start. Closed consoles go to a trash: :recover brings one back.

Your own editor, and suspending

  • Ctrl+G opens the query in your own editor ($VISUAL, else $EDITOR, else vi, notepad on Windows; arguments are allowed, for example EDITOR="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’s fg brings 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

  • h j k l or the arrows move the cell; 0 and $ go to the first and last column, gg and G to the first and last row of the page, Ctrl+D / Ctrl+U scroll 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.
  • Enter shows 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 terminal theme the row keeps your terminal’s background: the row’s number and the underlined header show where the cursor is.
  • v selects a range of cells, V whole rows, Ctrl+A every 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 tokens grid_cursor_row and grid_cursor_header set 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 (or Space z) zooms the results pane to the whole workspace and back; + and - make it taller or shorter. Esc or q goes back to the editor.

/ 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, j and k (or a click on another column) move the grid’s cell there, the grid scrolling sideways to it; Enter or 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, system or osc52; osc52_max_bytes is 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.
  • insert writes SQL INSERT statements only when the rows are certainly of one table (the same rule as Copy as SQL INSERT). Otherwise y and Y copy TSV and the status bar says why.
  • g p and p paste the cells y and Y copied, 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. y on one cell, Y on 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 nameOptions
CSVcsvdelim= (one character, or tab, comma, semicolon, pipe), quote= (necessary, always, non_numeric, never), header= (on/off), null= (the text written for NULL)
TSVtsvheader=
Excelxlsx (also excel)null=
Parquetparquetnone
JSON, indented JSON, JSON Linesjson, json_pretty, jsonl (also ndjson)none
Markdown, HTML, XMLmarkdown (also md), html, xmlnone
SQL INSERT, UPDATEinsert (also sql), updatenone; 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 bigint past 15 digits, a numeric with 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 the null= 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 unsigned an 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’s timestamptz is converted to UTC; MySQL’s TIMESTAMP is 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 SELECT that is safe to repeat goes ahead, anything else asks first. Environments with read_only = true or confirm = "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 ONLY on PostgreSQL, START TRANSACTION READ ONLY on 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’s idle_in_transaction_session_timeout or 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.

  • Tab in the path field completes a name: first the longest text the matching names share, then (Tab again) a list of them, then each in turn.
  • Ctrl+O in the dialog (or its [Browse…] button) opens a tree of the file system. Up and Down (j/k) move, Right and Left (l/h) open and close a folder, ../ goes up. Enter on the top row (“Save here”) or Ctrl+S anywhere puts the selected folder in the field; Enter on a file takes that file. n makes a folder, . shows hidden names, / filters, Esc leaves 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 (y replaces it, Enter keeps it). A .partial file left by an earlier export is not written over; move or delete it.
  • A cancel stops the statement on the server and keeps the .partial file 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 .partial file; 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.

Limits

  • Excel needs room in TMPDIR for the sheets’ uncompressed XML until the workbook is made, as well as room for the workbook itself. Point TMPDIR at 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

KeysWhat they do
v / V, 1 2 3The kind: bars, horizontal bars, line
xChoose the X column
sChoose the value columns
bA column that splits the values into series
SLogarithmic scale on or off
arrows, h j k lMove a cursor over the bars or points, showing the exact values and their row
EnterShow that row in the grid
y / YCopy the chart’s numbers as TSV / the drawing as text
cBack 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 uses WITH; it reads more than one table (a join or a comma in FROM); it has a subquery in the select list or in FROM; it combines results (UNION, INTERSECT, EXCEPT); it groups rows (GROUP BY, HAVING, DISTINCT, DISTINCT ON); it uses a window function; FROM names no plain table (a function, LATERAL, VALUES, a sample) or there is none; its columns do not come from the table named in FROM; 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”); D on 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 utf8mb4 or a MySQL TIMESTAMP in a time zone that is not a fixed offset; a MySQL FLOAT column 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:

KeysWhat it does
e (Space d e)Edit the cell, or the selected cells of one column
Space d nSet the cell (the selected cells) to NULL
Space d DSet 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 rDrop 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 vPaste the system clipboard at the cursor (with clipboard = "system", or "auto" outside SSH)
Space d VRead 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.

  • Enter keeps the text. An empty text is the empty string, never NULL: Ctrl+N and Ctrl+D set NULL and DEFAULT again.
  • Esc cancels; so does a click outside the line.
  • Ctrl+L takes the text typed so far to the editor of several lines.
  • With cells of one column selected, each takes the value, as one change (u undoes it).
  • An enum or boolean column lists its values instead, then NULL and DEFAULT where 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: 0x and 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:

ColumnShown
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 NULLNULL
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.

ClipboardSelectionResult
one valueone cellsets that cell
one valueseveral cellssets every selected cell (each checked for its own column)
a blockthe cursorthe block from the cursor
a blockseveral cellsthe 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 NULL column 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 token edit_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. Each p adds another copy, one step u undoes 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.
    • Y then p duplicates rows with their key values as they are: a serial or BY DEFAULT identity key then repeats, so set it to its default (Space d D) before Apply.
    • A copy from another connection or database goes by position.
  • 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 than utf8mb4, a TIMESTAMP read in a time zone without a fixed offset (one with daylight saving, or SYSTEM when the server’s own zone is not known), and text or a TIMESTAMP of 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 TIMESTAMP of 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, in SYSTEM when 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’s time_zone to 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

  • u undoes the last change of the result and Ctrl+R redoes it. A change of several selected cells, a delete of several rows, a paste and a discard are one step each.
  • Space d r drops 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?”: y drops them, n or Enter keeps 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 text a\nb), and shown as it is elsewhere ('a\b'). This is only how the list shows them: the statements, and what c copies, 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’s NOT NULL column 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 NOWAIT that 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 DELETE and 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 NULL refusals 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. 1 changes one of them, a all of them (the write then expects 2).
  • the last Apply’s result is unknown (below): the first y warns, a second y applies 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 y warns, and only a second y applies 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_INCREMENT column shows the number it took, other defaults stay default.
  • 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.”).

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 writes DEFAULT there. A cell showing re-run to see is not copied, nor is one an Apply set to its default, shown default (“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

KeysCommandPostgreSQLMySQL
Space e x:explainEXPLAIN (FORMAT JSON)EXPLAIN FORMAT=TREE
Space e a:explain analyzeEXPLAIN (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 ANALYZE is 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 ANALYZE of 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 the TRIGGER privilege every EXPLAIN ANALYZE of a write is refused, and EXPLAIN without ANALYZE still 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 SELECT the 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 declared IMMUTABLE or STABLE can still run while the statement is planned.
  • Anything else asks first, saying what runs again (ANALYZE runs the statement, rolled back; an EXECUTE’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, an EXPLAIN of 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 ANALYZE the numbers are the planner’s estimated costs, and the tab says so.
  • Enter shows the selected node’s detail: conditions, filters and the rows they removed, buffers, workers, loops, output. h and l close 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.

KeyView
1The tree
2Summary 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
3An icicle (each node as wide as its share of its parent’s time)
4A flame graph
5A 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)
6The row flow: the rows each node passes up as a band, thick by their number, with the rows its filters removed
7A treemap (area = each node’s own time)
8A box diagram (boxes joined by lines; < and > and the wheel move around a large plan)
9Raw: 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 EXPLAIN runs 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.
  • p pauses (what was read stays for reading), r reads 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:

LineCounts
selectSELECT
insertINSERT, INSERT ... SELECT, REPLACE, REPLACE ... SELECT
updateUPDATE, including multi-table ones
deleteDELETE, 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 ?, whatever a says.

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 from pg_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). Only COMMIT PREPARED or ROLLBACK PREPARED with 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_locks would 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 (from INNODB_TRX, which needs PROCESS): a prepared XA transaction whose session went on to other work is shown as a transaction with no session, and without PROCESS only 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 (an ALTER TABLE waiting 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, b does 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_Source only 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_Source is 0 also while events of the same second wait. Without them, a channel is taken as caught up only while its applier runs with Seconds_Behind_Source NULL and no transactions left by the GTID sets. Where every transaction carries a GTID (gtid_mode ON) 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_activities off);

  • 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 y sends 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. Enter keeps the session as it is; only y sends.

  • 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 KILL right after (a few milliseconds apart): a cancel checks the statement’s event in performance_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:

ServerUsed when KILL is refusedTo act on other accounts’ sessions
PostgreSQLnonepg_signal_backend
Amazon RDS and Aurora PostgreSQLnonerds_superuser or pg_signal_backend
Google Cloud SQL for PostgreSQLnonecloudsqlsuperuser or pg_signal_backend
Google AlloyDBnonealloydbsuperuser or pg_signal_backend
Azure Database for PostgreSQLnoneazure_pg_admin or pg_signal_backend
MySQLnoneCONNECTION_ADMIN
Amazon RDS and Aurora MySQLmysql.rds_kill_query, mysql.rds_killCONNECTION_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:

ServerWithoutThe tab shows
PostgreSQLpg_read_all_stats (or superuser)other roles’ sessions without their state, statement and times, and their lock waits out of the Locks view
MySQLPROCESSonly the account’s own sessions
MySQLPROCESS, 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
MySQLenough performance_schema thread slots (performance_schema_max_thread_instances)that the list may miss sessions
MySQLSELECT on performance_schemathe Locks view without who blocks a row lock wait, and without metadata lock waits
MySQLthe metadata lock instrument (wait/lock/metadata/sql/mdl)the Locks view without metadata lock waits
MySQL8.0.38 or later in 8.0, or 8.4 or laterthe Locks view without who blocks a row lock wait (on older versions reading it can stall a busy server, so it is not read)
PostgreSQLpg_read_all_statsWAL senders and the WAL receiver without their state and positions (a standby’s received and replayed positions still show)
MySQLREPLICATION CLIENTthat whether the server takes changes from another is not known
MySQLPROCESSthat the servers taking changes from it are not known (only the account’s own sessions are listed)
MySQLSELECT on performance_schemathat 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 (SSH with icons off), and one with read-only rules a lock in blue (RO with 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: Enter on 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 a WHEN condition 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 CREATE text (see MySQL).
  • The text can be moved in, searched and yanked; edits are refused. o opens it in a new console on the same connection, and r reads 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, as l and h do: 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 Enter does: 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_password and mysql_native_password, and the test connection.
  • Running statements, one per request: datarig splits scripts itself, DELIMITER included. The client never asks the server to run several statements from one request, and LOAD DATA LOCAL is 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: DECIMAL exact, unsigned integers in full, BIT as b'…', binary values as 0x…, JSON, dates and times with their fractions, TIME as an interval (negative, or over 24 hours), zero dates (0000-00-00) as text, ENUM/SET as text, GEOMETRY as 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 an UPDATE (and INSERT … ON DUPLICATE KEY UPDATE, and ROW_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 db and 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: D on 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_mode than 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. o opens the text in a console in the object’s database with every string written as that console’s sql_mode reads it, so it makes the same object there; the DDL tab keeps the server’s text.
  • The DEFINER is kept, so running it again as another user needs SET_USER_ID (MySQL 8.0) or SET_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 r reads 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, ENUM and SET in utf8mb4, utf8mb3, utf8, latin1, ascii, ucs2, utf16, utf16le or utf32 (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 NULL unique 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 with LIMIT 1).
  • FLOAT is 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 than utf8mb4 or a TIMESTAMP in 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_INCREMENT column shows the number it took, other defaults show default, and a trigger’s change shows after a re-run.
  • A new row leaving a NOT NULL column 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). Over PREFERRED or REQUIRED without 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 of SHOW 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 EXPLAIN runs and shows its text. MariaDB is reached through the MySQL driver.
  • Editing data on MariaDB and on MySQL before 8.0.17.
  • A USE typed 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 TRIGGER privilege (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
themeWhat it does
terminalThe default: your terminal’s own colours and background (its 16 ANSI colours by role)
dark, light, high-contrastdatarig’s own truecolor themes
catppuccin, tokyo-night, gruvboxThe light or the dark variant, following the terminal’s background
catppuccin-latte, catppuccin-mocha, tokyo-night-day, tokyo-night-night, gruvbox-light, gruvbox-darkThat variant, whatever the background
nord, draculaThose palettes (dark)
any other nameYour 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> writes themes/<name>.toml with 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, else dark) with every token commented out, showing the value it inherits.
  • On the settings screen, c on 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 withThe notice says
Homebrewbrew 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 foldergit pull, then cargo install --locked --path crates/tui
A release archive, or anything elseWhere to download it (the latest release’s page)
KeyWhat it does
EnterPresses the button with the focus (Close at first)
Esc, qClose: the notice comes back after a week
sSkip this version: not told about it again (a newer one is)
oTurn off the checks (saved as [updates] check = "off")
Tab, the arrows, h, lMove 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);
  • o in the notice;
  • the environment variable DATARIG_NO_UPDATE_CHECK with 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 Space are 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’s f t F T r wait for is taken as typed.
  • R: repeats while the key is held (terminal auto-repeat).
  • Key guide: after Space a which-key popup lists the keys that may follow (after 300 ms; Backspace goes up a level, Esc closes). Space ? or F1 open 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/h or 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) or Ctrl+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. i starts typing and Esc stops (the hint line says so). Implemented, all with counts: the motions h 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, Home and End; the operators d c y, gu gU g~ (case) and > < (indent by 4 columns, written with spaces as Tab types 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 objects iw aw iW aW, i( a( (ib ab), i[ a[, i{ a{ (iB aB), i< a<, i" a" i' a', i` a` and ip 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 with Ctrl+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, Enter searches, Esc or any key of the app goes back, Ctrl+W deletes a word; an empty pattern takes the last one), n N, * # (the word under the cursor, as a whole word) and g* g# (also inside longer words), with counts and after an operator (d/from Enter, 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), \b for 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'a takes whole lines, y`a the 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); gc comments lines out with -- or back in, as Neovim’s built-in commenting does: gcc (with a count), gc with a motion or text object and gc in 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 action editor.comment_toggle, Space e c, is the same as gcc, or gc in Visual mode); editor commands on the : line (see the end); & runs the last :s again on the cursor’s line with its flags (as Neovim’s &, Vim’s :&&) and g& 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) and p P (a register in place of the selection; P keeps the replaced text out of the registers); Visual mode by block (Ctrl+V; v, V and Ctrl+V switch 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, o and O (the other corners), y Y d x X D (D to the ends of the lines), c s C, I and A (what Insert mode types goes on every line of the block when it ends; A fills short lines with blanks first, $A appends at each line’s end; a count types it that many times), r (r Enter breaks the lines), ~ u U gu gU g~, > < with a count, J gJ, S R (whole lines) and p 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 (after p only the delete); a paste from the terminal replaces the block; in Insert mode Ctrl+W (the word before the cursor), Ctrl+U (the line before the cursor), Ctrl+R {register} (its text), Enter keeping the indent, Tab (4 spaces) and Backspace, 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 with softtabstop=4 does 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 and Backspace deletes 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: "x before a command in Normal and Visual mode ("ayy, 2"ap), the unnamed one, "a-"z ("A-"Z append), "0 (the last yank), "1-"9 (the last deletes of lines; . after "1p puts "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 with clipboard=unnamedplus, a yank, delete or change without a register also goes to the system clipboard, through the clipboard setting’s way ([editor] clipboard = "off" keeps them in the editor); "a and "_ never do. "+p reads 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+G opens the tab’s query in your own editor ($VISUAL, else $EDITOR, else vi; split into words like a shell does, code -w included, 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 as Ctrl+Z stops Vim; the shell’s fg brings it back (Unix only; on Windows :suspend says it is not supported). Ctrl+C stays 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 after fg.
  • 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.

KeysActionDescriptionR
Ctrl+Qapp.quitQuit
Ctrl+Cquery.cancelCancel running query
Ctrl+Kcommands.openCommands

workspace

The workspace without a dialog, including text input in the editor. Inside root.

KeysActionDescriptionR
Ctrl+Enterquery.execute_currentRun statement under cursor
Ctrl+Equery.execute_currentRun statement under cursor
Ctrl+Oconn.quick_connectQuick connect (go to a profile’s tab)
Ctrl+Geditor.open_externalEdit the query in your own editor ($VISUAL, $EDITOR)
Ctrl+Zapp.suspendSuspend datarig (the shell’s fg brings it back)
Ctrl+Ttab.new_consoleNew console tab
Ctrl+Wtab.closeClose tab
Ctrl+Sscript.saveSave query
Ctrl+PageDowntab.nextNext tab✓
Ctrl+PageUptab.prevPrevious tab✓
F6pane.nextFocus next pane
Shift+F6pane.prevFocus previous pane
Shift+Tabpane.prevFocus previous pane
F1help.contextKeyboard help

Every pane that is not text input (explorer, results, vim Normal/Visual). Inside workspace.

KeysActionDescriptionR
Tabpane.nextFocus next pane
:commands.openCommands
g ttab.nextNext tab✓
g Ttab.prevPrevious tab✓
Space t ntab.new_consoleNew console tab
Space t ctab.closeClose tab
Space t utab.reopen_closedReopen closed tab
Space t ttab.listList tabs
Space t <tab.move_leftMove tab left✓
Space t >tab.move_rightMove tab right✓
Space t gtab.grabMove tab with keys
Space 1tab.goto.1Go to tab 1
Space 2tab.goto.2Go to tab 2
Space 3tab.goto.3Go to tab 3
Space 4tab.goto.4Go to tab 4
Space 5tab.goto.5Go to tab 5
Space 6tab.goto.6Go to tab 6
Space 7tab.goto.7Go to tab 7
Space 8tab.goto.8Go to tab 8
Space 9tab.goto.9Go to tab 9
Space c cconn.quick_connectQuick connect (go to a profile’s tab)
Space c nconn.newNew connection profile
Space c econn.edit_currentEdit this tab’s connection profile
Space c tconn.test_currentTest this tab’s connection
Space c xconn.disconnect_currentDisconnect this tab’s connection
Space c rconn.reconnect_currentReconnect this tab’s connection
Space c stab.set_connectionChange this tab’s connection
Space c dtab.set_contextChoose this tab’s database and schema
Space x ctx.commitCommit the open transaction
Space x rtx.rollbackRoll back the open transaction
Space x stx.summaryWhat the open transaction did
Space s sscript.saveSave query
Space s ascript.save_asSave query as…
Space s rscript.renameRename this saved query
Space s oscript.openOpen a saved query
Space s dscript.deleteDelete this saved query
Space e feditor.formatFormat the statement under the cursor or the selection (only its layout)
Space e ceditor.comment_toggleComment the line or the selected lines out with – (or back in)
Space e xquery.explainExplain: the statement’s plan (nothing runs)
Space e aquery.explain_analyzeExplain analyze: run and measure the statement (writes rolled back)
Space e presults.view_as_planView as plan: the text EXPLAIN again as a plan
Space Spacemenu.openAction menu
Shift+F10menu.openAction menu
Menumenu.openAction menu
Space t mtab.menuTab menu
Space zpane.zoomToggle pane zoom
Space bexplorer.toggleToggle explorer
Space >explorer.widerWiden explorer✓
Space <explorer.narrowerNarrow explorer✓
Space ,settings.openSettings
Space /commands.openCommands
Space ?help.contextKeyboard help
Space d eedit.cellEdit the cell (the selected cells of one column)
Space d nedit.set_nullSet the cell (the selected cells) to NULL
Space d Dedit.set_defaultSet the cell (the selected cells) to the column’s default
Space d uedit.undoUndo the last change of the result’s cells
Space d Uedit.redoRedo the change of the result’s cells undone last
Space d redit.revertDrop the pending changes of the cell (the selection)
Space d oedit.add_rowAdd a new row, shown after the rows read
Space d Oedit.clone_rowAdd a new row with the values of the cursor’s row
Space d dedit.delete_rowMark the row (the selected rows) to be deleted, or unmark it; a new row is removed
Space d pedit.previewShow changes
Space d aedit.applyApply the pending changes (asked first)
Space d xedit.discardDrop every pending change of the result
Space d vedit.pastePaste the system clipboard into the grid
Space d Vedit.paste_asPaste again, read as the next format
Space d yedit.paste_registerPaste the cells last copied in the grid over the cells at the cursor
Space d Yedit.paste_rowsPaste the cells last copied in the grid as new rows
Space r y tresults.copy.selection.tsvCopy selection: Without headers (TSV)
Space r y Tresults.copy.selection.tsv_headerCopy selection: With headers (TSV)
Space r y lresults.copy.selection.listCopy selection: Comma list
Space r y cresults.copy.selection.csvCopy selection: CSV
Space r y jresults.copy.selection.jsonCopy selection: JSON
Space r y Jresults.copy.selection.json_prettyCopy selection: JSON (pretty)
Space r y mresults.copy.selection.markdownCopy selection: Markdown
Space r y hresults.copy.selection.htmlCopy selection: HTML table
Space r y xresults.copy.selection.xmlCopy selection: XML
Space r y nresults.copy.selection.inCopy selection: SQL IN clause
Space r y iresults.copy.selection.insertCopy selection: SQL INSERT
Space r y uresults.copy.selection.updateCopy selection: SQL UPDATE
Space r y dresults.copy.defaultChoose the format y and Y copy in
Space r a tresults.copy.all.tsvCopy all fetched rows: Without headers (TSV)
Space r a Tresults.copy.all.tsv_headerCopy all fetched rows: With headers (TSV)
Space r a lresults.copy.all.listCopy all fetched rows: Comma list
Space r a cresults.copy.all.csvCopy all fetched rows: CSV
Space r a jresults.copy.all.jsonCopy all fetched rows: JSON
Space r a Jresults.copy.all.json_prettyCopy all fetched rows: JSON (pretty)
Space r a mresults.copy.all.markdownCopy all fetched rows: Markdown
Space r a hresults.copy.all.htmlCopy all fetched rows: HTML table
Space r a xresults.copy.all.xmlCopy all fetched rows: XML
Space r a nresults.copy.all.inCopy all fetched rows: SQL IN clause
Space r a iresults.copy.all.insertCopy all fetched rows: SQL INSERT
Space r a uresults.copy.all.updateCopy all fetched rows: SQL UPDATE
Space r iresults.detailResult detail → show / hide
Space r Iresults.detail_tabResult detail → Cell / Row
Space r hresults.panel.toggleResults pane → hide / show
Space r zresults.panel.maximizeResults pane → zoom (maximise) / restore
Space r +results.panel.growResults pane → taller✓
Space r -results.panel.shrinkResults pane → shorter✓
Space r cresults.chartResults → show as a chart / as rows
Space r nresults.page.nextResults → next page✓
Space r presults.page.prevResults → previous page✓
Space r #results.countResults → count every row (runs a count query)
Space r eresults.exportResults → export to a file…
Space r ]results.tab.nextResult tabs → next
Space r [results.tab.prevResult tabs → previous
Space m mmonitor.openMonitor → the connection’s server monitor

Leader groups (the which-key popup lists what follows):

KeysGroup
SpaceLeader
Space cConnection
Space tTabs
Space sSaved queries
Space rResults
Space r yCopy selection
Space r aCopy all fetched rows
Space mMonitor
Space xTransaction
Space eEditor
Space dData

explorer

The explorer: folders, connection profiles, their databases and schema trees. Inside nav.

KeysActionDescriptionR
jexplorer.downExplorer: next item✓
Downexplorer.downExplorer: next item✓
kexplorer.upExplorer: previous item✓
Upexplorer.upExplorer: previous item✓
lexplorer.expandExplorer: expand
Rightexplorer.expandExplorer: expand
hexplorer.collapseExplorer: collapse or go to parent
Leftexplorer.collapseExplorer: collapse or go to parent
Enterexplorer.activateExplorer: open or toggle
g gexplorer.topExplorer: first item
Gexplorer.bottomExplorer: last item
rexplorer.refreshExplorer: reload
/explorer.filterExplorer: filter by name
nconn.newNew connection profile
econn.editEdit connection profile
cconn.duplicateDuplicate connection profile
dexplorer.deleteExplorer: delete profile or empty folder
tconn.testTest connection
xconn.disconnectDisconnect
oconn.open_consoleNew console on this connection
mexplorer.moveExplorer: move profile to a folder
Nfolder.newNew folder
Rexplorer.renameExplorer: rename folder
Oexplorer.new_console_hereExplorer: new console in this database or schema
Dexplorer.show_ddlExplorer: show the DDL of the table, view, index or trigger
Fexplorer.show_function_ddlExplorer: show the DDL of the trigger’s function
Xtips.dismiss_onboardingHide the first steps of the welcome panel
Escexplorer.backExplorer: back (cancel a test or connecting, clear the filter)
qapp.quitQuit
Mmonitor.openMonitor → 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.

KeysActionDescriptionR
Escexplorer.filter_clearExplorer filter: clear
Enterexplorer.filter_acceptExplorer filter: done
Downexplorer.downExplorer: next item✓
Upexplorer.upExplorer: 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.

KeysActionDescriptionR
hgrid.leftResults: previous column✓
Leftgrid.leftResults: previous column✓
jgrid.downResults: next row✓
Downgrid.downResults: next row✓
kgrid.upResults: previous row✓
Upgrid.upResults: previous row✓
lgrid.rightResults: next column✓
Rightgrid.rightResults: next column✓
PageDowngrid.page_downResults: scroll one screen down✓
PageUpgrid.page_upResults: scroll one screen up✓
Ctrl+Dgrid.half_downResults: scroll half a screen down✓
Ctrl+Ugrid.half_upResults: scroll half a screen up✓
g ggrid.topResults: first row of the page
Homegrid.topResults: first row of the page
Ggrid.bottomResults: last row of the page
Endgrid.bottomResults: last row of the page
0grid.first_colResults: first column
$grid.last_colResults: last column
Entergrid.view_cellResults: view cell value
ygrid.copy_cellCopy the cell (a selected range)
Ygrid.copy_rowCopy the row (the selected rows)
vgrid.selectSelect a range of cells (again: cancel)
Vgrid.select_rowsSelect whole rows (again: cancel)
Ctrl+Agrid.select_allSelect every cell of the page
/grid.searchSearch the fetched rows forward (a regular expression)
?grid.search_backSearch the fetched rows backward (a regular expression)
eedit.cellEdit the cell (the selected cells of one column)
uedit.undoUndo the last change of the result’s cells
Ctrl+Redit.redoRedo the change of the result’s cells undone last
oedit.add_rowAdd a new row, shown after the rows read
Oedit.clone_rowAdd a new row with the values of the cursor’s row
Dedit.delete_rowMark the row (the selected rows) to be deleted, or unmark it; a new row is removed
pedit.paste_rowsPaste the cells last copied in the grid as new rows
g pedit.paste_registerPaste the cells last copied in the grid over the cells at the cursor
iresults.detailResult detail → show / hide
Iresults.detail_tabResult detail → Cell / Row
zresults.panel.maximizeResults pane → zoom (maximise) / restore
+results.panel.growResults pane → taller✓
-results.panel.shrinkResults pane → shorter✓
nresults.page.nextResults → next page✓
]results.page.nextResults → next page✓
[results.page.prevResults → previous page✓
#results.countResults → count every row (runs a count query)
Presults.view_as_planView as plan: the text EXPLAIN again as a plan
cresults.chartResults → show as a chart / as rows
Lresults.tab.nextResult tabs → next
Hresults.tab.prevResult tabs → previous
Escpane.backBack to the editor
qpane.backBack 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.

KeysActionDescriptionR
ngrid.search_nextGrid search: next match✓
Ngrid.search_prevGrid search: previous match✓
Escgrid.search_clearGrid 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.

KeysActionDescriptionR
jresults.detail_nextResult detail → next column✓
Downresults.detail_nextResult detail → next column✓
kresults.detail_prevResult detail → previous column✓
Upresults.detail_prevResult detail → previous column✓
Entergrid.view_cellResults: view cell value
Iresults.detail_tabResult detail → Cell / Row
iresults.detailResult detail → show / hide
Escpane.backBack to the editor
qpane.backBack 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.

KeysActionDescriptionR
jplan.downPlan: next node✓
Downplan.downPlan: next node✓
kplan.upPlan: previous node✓
Upplan.upPlan: previous node✓
g gplan.topPlan: first node
Homeplan.topPlan: first node
Gplan.bottomPlan: last node
Endplan.bottomPlan: last node
PageDownplan.page_downPlan: a page down✓
Ctrl+Dplan.page_downPlan: a page down✓
PageUpplan.page_upPlan: a page up✓
Ctrl+Uplan.page_upPlan: a page up✓
lplan.expandPlan: show the node’s children (or go to the first)
Rightplan.expandPlan: show the node’s children (or go to the first)
hplan.collapsePlan: hide the node’s children (or go to its parent)
Leftplan.collapsePlan: hide the node’s children (or go to its parent)
Enterplan.detailPlan: show or hide the selected node’s detail
iplan.detailPlan: show or hide the selected node’s detail
vplan.view.nextPlan: next view
Vplan.view.prevPlan: previous view
1plan.view.treePlan view: tree
2plan.view.summaryPlan view: summary cards
3plan.view.iciclePlan view: icicle (width = time)
4plan.view.flamePlan view: flame graph (width = time)
5plan.view.timelinePlan view: timeline (to the first and last row, needs ANALYZE)
6plan.view.rowsPlan view: row flow (rows between nodes)
7plan.view.treemapPlan view: treemap (area = own time)
8plan.view.boxesPlan view: box diagram
9plan.view.rawPlan view: raw (the server’s own words, or the text form of a JSON plan)
<plan.pan_leftPlan: move the view left✓
>plan.pan_rightPlan: move the view right✓
yplan.copy_textPlan: copy as the raw view shows it
Yplan.copy_jsonPlan: copy as the server sent it (its JSON, or its text)
zresults.panel.maximizeResults pane → zoom (maximise) / restore
+results.panel.growResults pane → taller✓
-results.panel.shrinkResults pane → shorter✓
Lresults.tab.nextResult tabs → next
Hresults.tab.prevResult tabs → previous
Escpane.backBack to the editor
qpane.backBack 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.

KeysActionDescriptionR
hchart.leftChart: cursor left✓
Leftchart.leftChart: cursor left✓
lchart.rightChart: cursor right✓
Rightchart.rightChart: cursor right✓
kchart.upChart: cursor up✓
Upchart.upChart: cursor up✓
jchart.downChart: cursor down✓
Downchart.downChart: cursor down✓
g gchart.firstChart: first point
Homechart.firstChart: first point
Gchart.lastChart: last point
Endchart.lastChart: last point
Enterchart.goto_rowChart: show the point’s row in the grid
vchart.kind.nextChart: next kind
Vchart.kind.prevChart: previous kind
1chart.kind.barChart kind: bars
2chart.kind.hbarChart kind: horizontal bars
3chart.kind.lineChart kind: line
xchart.pick_xChart: choose the X column
schart.pick_yChart: choose the value columns (Y)
bchart.pick_byChart: split the values into series by a column
Schart.logChart: logarithmic scale on or off
ychart.copy_dataChart: copy its numbers as TSV
Ychart.copy_textChart: copy the drawing as text
cresults.chartResults → show as a chart / as rows
zresults.panel.maximizeResults pane → zoom (maximise) / restore
+results.panel.growResults pane → taller✓
-results.panel.shrinkResults pane → shorter✓
Lresults.tab.nextResult tabs → next
Hresults.tab.prevResult tabs → previous
Escpane.backBack to the editor
qpane.backBack 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.

KeysActionDescriptionR
jchanges.downChanges: next line✓
Downchanges.downChanges: next line✓
kchanges.upChanges: previous line✓
Upchanges.upChanges: previous line✓
Ctrl+Dchanges.half_downChanges: half a page down✓
PageDownchanges.half_downChanges: half a page down✓
Ctrl+Uchanges.half_upChanges: half a page up✓
PageUpchanges.half_upChanges: half a page up✓
g gchanges.topChanges: first line
Homechanges.topChanges: first line
Gchanges.bottomChanges: last line
Endchanges.bottomChanges: last line
Enterchanges.gotoChanges: show the line’s row and column in the grid
schanges.viewChanges: switch between the list and the SQL
cchanges.copy_sqlChanges: copy the SQL as it would be sent
uedit.undoUndo the last change of the result’s cells
Ctrl+Redit.redoRedo the change of the result’s cells undone last
zresults.panel.maximizeResults pane → zoom (maximise) / restore
+results.panel.growResults pane → taller✓
-results.panel.shrinkResults pane → shorter✓
Lresults.tab.nextResult tabs → next
Hresults.tab.prevResult tabs → previous
Escpane.backBack to the editor
qpane.backBack 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.

KeysActionDescriptionR
1monitor.overviewMonitor: Overview
2monitor.sessionsMonitor: Sessions
3monitor.locksMonitor: Locks
4monitor.replicationMonitor: Replication
vmonitor.view.nextMonitor: next view
jmonitor.downMonitor: session below✓
Downmonitor.downMonitor: session below✓
kmonitor.upMonitor: session above✓
Upmonitor.upMonitor: session above✓
Ctrl+Dmonitor.page_downMonitor: page down✓
PageDownmonitor.page_downMonitor: page down✓
Ctrl+Umonitor.page_upMonitor: page up✓
PageUpmonitor.page_upMonitor: page up✓
g gmonitor.firstMonitor: first session
Homemonitor.firstMonitor: first session
Gmonitor.lastMonitor: last session
Endmonitor.lastMonitor: last session
pmonitor.pauseMonitor: pause / go on reading
rmonitor.readMonitor: read now
+monitor.fasterMonitor: read more often
-monitor.slowerMonitor: read less often
amonitor.idleMonitor: show / hide idle sessions
smonitor.sortMonitor: sort by the next column
Smonitor.sort_reverseMonitor: sort the other way
/monitor.filterMonitor: filter the sessions
Entermonitor.detailsMonitor: the session in full
omonitor.open_consoleMonitor: the session’s statement in a new console
ymonitor.copy_queryMonitor: copy the session’s statement
fmonitor.formatMonitor: show statements laid out for reading, or as sent
bmonitor.rootMonitor: the root blocker of the selected wait
imonitor.serverMonitor: the server in full
Escmonitor.backMonitor: back (details, filter)
cmonitor.cancelMonitor: cancel the session’s statement
Xmonitor.terminateMonitor: end the session
Lmonitor.actionsMonitor: 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.

KeysActionDescriptionR
Entermonitor.backMonitor: back (details, filter)
Escmonitor.backMonitor: 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.

KeysActionDescriptionR
nconn.newNew connection profile
Enterconn.newNew connection profile
qapp.quitQuit
Xtips.dismiss_onboardingHide 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.

KeysActionDescriptionR
Ctrl+Neditor.completeShow completions
F4editor.completeShow 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.

KeysActionDescriptionR
rddl.reloadDDL tab: read the DDL again
oddl.open_consoleDDL 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.

KeysActionDescriptionR
Ctrl+Oform.pick_key_filePick the SSH key file
Ctrl+Bform.save_as_tunnelProfile form: save this profile’s SSH tunnel as a tunnel preset
Ctrl+Eform.session_viewProfile 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.

KeysActionDescriptionR
Ctrl+Oexport.pick_pathExport 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.

KeysActionDescriptionR
qapp.quitQuit

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).

ActionDescription
tunnel.newNew SSH tunnel (shared by profiles)
env.newNew environment (rules, colour and badge that profiles share)
session.newNew Startup SQL (settings a profile’s sessions start with)
session.showWhat a profile’s sessions start with, and where each setting comes from
ui.language.enChange language → English
ui.language.koChange language → Korean
ui.language.autoChange language → Auto (system locale)
ui.icons.toggleNerd Font icons → on/off
ui.icons.onNerd Font icons → on
ui.icons.offNerd Font icons → off
ui.icons.autoNerd Font icons → ask again (with a preview)
secrets.default.autoNew profiles’ password → auto (keychain when available)
secrets.default.keychainNew profiles’ password → OS keychain
secrets.default.fileNew profiles’ password → secrets file
secrets.default.commandNew profiles’ password → command
secrets.default.envNew profiles’ password → environment variable
secrets.default.promptNew profiles’ password → prompt on every connect
help.allKeyboard help: expand all
app.whats_newWhat’s new: the release notes of this version and the ones before
app.updateUpdate: what the update check knows, and how to update
explorer.context_menuExplorer: actions of this node (menu)
ddl.copyDDL 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 \0 the match, \1-\9 a group (an absent one is empty), ~ the previous replacement, \r a line break (\n too; in Vim it is a NUL), \t a tab, \u \l the next character upper or lower case, \U \L up to \E or \e, a backslash before any other character that character (\&, \~, \\, the delimiter); $1 is 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#). :s alone and :& repeat the last :s without its flags, :&& with them. Nothing matched: “Pattern not found”.
  • :[range]format formats the range’s lines (:'<,'>format, :%format); :format without a range, and Space 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.

CommandAliasesDescription
:conn <profile>:connectConnect to a profile
:set <setting>=<value>Change a setting
:help:hKeyboard help
:runRun statement under cursor
:cancelCancel running query
:quit:qClose the tab (on the last tab: quit)
:qall:qa :quitallQuit
:w [name]:writeSave (a console asks for a name; with a name: save as)
:wq [name]:xSave, then close the tab (on the last tab: quit)
:e [name]:editOpen a saved query
:tabnewNew console tab
:tabcloseClose tab
:tabs:ls :buffersList tabs
:tabmove [N / +N / -N / $]:tabmMove this tab (:tabmove N: after tab N, 0 first; +N / -N: N places; alone: last)
:recoverBring back a closed console (also from earlier runs)
:settingsOpen 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 :nohlClear the search highlight in the editor (until the next search) and end the result grid’s search
:suspend:sus :stopSuspend datarig (the shell’s fg brings it back)
:formatFormat 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])
:commitCommit the open transaction
:rollback:rbRoll back the open transaction
:txWhat the open transaction did
:session:startupWhat a profile’s sessions start with, and where each setting comes from
:changesShow changes
:applyApply the pending changes (asked first)
:discardDrop every pending change of the result
:whatsnewWhat’s new: the release notes of this version and the ones before
:updateUpdate: what the update check knows, and how to update

Settings of :set (saved to config.toml like the matching actions):

SettingValuesDescription
languageen ko autoUI language
iconson off autoNerd Font icons
secrets.default_sourceauto keychain file command env promptPassword storage of new profiles
commands.positionpopup bottomCommand line position
detail_viewpanel statusbarCell detail
clipboardauto system osc52Clipboard
copy_headerauto on offColumn names in copies
editor.cursor_shapeon offCursor shape
editor.clipboardon offYanks to the clipboard
themeterminal 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 fileTheme
editor.format_keyword_casepreserve upper lowerFormatter: keywords
editor.format_indent4 2Formatter: indent
editor.auto_pairsoff onAuto-pairs
editor.soft_tab_backspaceon offBackspace to tab stop
editor.run_hintson offRun hints
secrets.macos_keychain_accessdirect security_toolmacOS keychain access
explorer.size_heat3 5 offExplorer: size colours
ui.yank_flashon offFlash copied cells
ui.changes_viewlist sqlChanges tab view
edit.paste_null\N NULL nonePaste NULL token
ui.tipsalways until_used offTips
ui.backgroundtheme terminalBackground
idle_disconnectoff 5m 10m 30m 1hClose idle connections
lock_waitoff 5s 10s 30s 1mLock wait limit
editor.completion_delay_ms60 0 30 100 150 250Completion delay (ms)
updates.checkon offCheck for updates
updates.whats_newon offWhat’s new
copy_formattsv csv json json_pretty markdown html xml insertFormat of y and Y copies
monitor.interval1s 2s 5s 10s 30s 60sServer 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.

KeyDefaultValuesDescription
version1, the original format1, 2The 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 fileThe 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_size5001 to 2147483647Rows a result fetches from the server at a time (one page of the grid).
result_window_rows100001000 to 10000000Rows 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 limitThe 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_usednonea profile’s idWritten 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_bytes100000a positive number of bytesThe 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.

KeyDefaultValuesDescription
modeanyRetired (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_indent44, 2How many spaces the formatter indents each level with. Also :set editor.format_indent.
completion_delay_ms6060, 0, 30, 100, 150, 250How 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.

KeyDefaultValuesDescription
size_heat33, 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.

KeyDefaultValuesDescription
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.

KeyDefaultValuesDescription
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.

KeyDefaultValuesDescription
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.

KeyDefaultValuesDescription
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.

KeyDefaultValuesDescription
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.

KeyDefaultValuesDescription
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.

KeyDefaultValuesDescription
ida new one, written by the launch-time upgradea UUIDThe 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"textThe 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 addressThe server’s host.
port5432a port numberThe server’s port. Left out it is 5432 whatever the driver, so a MySQL profile says 3306.
user""textThe database user.
passwordnonetextA 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""textThe database to connect to.
sslmodePostgreSQL "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.
sslrootcertnonea file pathThe CA file the server’s certificate chain is checked against.
sslcertnonea file pathThe client certificate file, used with sslkey.
sslkeynonea file pathThe client certificate’s private key file.
dsnnonea connection stringA 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.
folderthe top levela folder path ("work/prod")The explorer folder the profile is in. Folders only organise profiles.
colora 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.
iconthe driver’s iconthe name of one of datarig’s iconsThe Nerd Font icon next to the profile (an unknown name shows the driver’s icon).
policythe default rulesthe name of a [policy.<env>] tableThe 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_commandnonea commandWith password_source = "command" (required then): the command whose output is the password.
password_envnonean environment variable’s nameWith password_source = "env" (required then): the variable that holds the password.
statement_cachetruetrue, falseKeep 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_filenonea PEM file pathMySQL: 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_retrievalfalsetrue, falseMySQL: 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.
tunnela direct connectionthe name of a [tunnels.<name>] tableThe 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.
startupthe preset its environment maps for its database, if anythe 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 statementsThe 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.
sessionOld name of startup, read the same; a table may not have both.
session_sqlOld 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.

KeyDefaultValuesDescription
enabledfalsetrue, falseWhether the profile connects through this tunnel.
hostnone (required when enabled)a host name or addressThe bastion’s host.
port22a port number (0 is refused when enabled)The bastion’s SSH port.
usernone (required when enabled)textThe 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_filenone (required with auth = "key")a file path (~ is the home directory)The private key file.
secret_source"keychain"as password_sourceWhere the key’s passphrase or the password comes from. It is kept apart from the database password.
secret_commandnonea commandWith secret_source = "command" (required then).
secret_envnonean environment variable’s nameWith secret_source = "env" (required then).
keepalive15seconds; 0 for noneSeconds between keepalives.
timeout10seconds (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.

KeyDefaultValuesDescription
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 itWith paging = "hold": how long a result that has more rows may keep its cursor open without a fetch.
spill_limitthe top-level spill_limitas the top-level spill_limitThe most a result’s temporary file may take for its profiles.
read_onlyfalsetrue, falseRun 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_timeoutOld name of tx_idle_rollback, read the same; a table may not have both.
idle_disconnectthe top-level idle_disconnectas the top-level idle_disconnectHow long its profiles’ connections may sit unused before the app closes their sessions.
lock_waitthe top-level lock_waitas the top-level lock_waitHow 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.
startupnone{ postgres = "<name>", mysql = "<name>" }, either left outThe Startup SQL preset its profiles’ query sessions start with, for each database. A profile may name its own, or "none".
sessionOld name of startup, read the same; a table may not have both.
colornoneas a profile’s colorThe 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.
badgenonetext of at most 8 columns, no blanks around itA 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.

KeyDefaultValuesDescription
ida new one, written by datariga UUIDThe preset’s stable id: its secret is kept under it, so a rename keeps it.
hostnone (required)a host name or addressThe bastion’s host.
port221 to 65535The bastion’s SSH port.
usernone (required)textThe user on the bastion.
auth"key""key", "password", "agent", "keyboard-interactive"As in [connections.ssh].
key_filenone (required with auth = "key")a file path (~ is the home directory)The private key file.
secret_source"keychain"as password_sourceWhere the key’s passphrase or the password comes from.
secret_commandnonea commandWith secret_source = "command" (required then).
secret_envnonean environment variable’s nameWith secret_source = "env" (required then).
keepalive15seconds; 0 for noneSeconds between keepalives.
timeout10seconds (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.

KeyDefaultValuesDescription
dbmsnone (required)"postgres", "mysql"The kind of database the preset is for.
sql[]a list of statementsThe 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.

KeyDefaultValuesDescription
"<keys>"the default bindingsan action id, or "none" to unbindBinds 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) or default (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> writes themes/<name>.toml with every token of the theme <from> (a built-in theme or a theme file; a family such as catppuccin as the variant drawn now) set to its value, each under a comment saying what it colours. extends names 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> writes themes/<name>.toml extending the current theme when it is built in, else dark, 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, c on 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])

TokenWhat it colours
bgThe background of the panes: explorer, editor, results.
surfaceThe background of dialogs, popups, menus, the command line and the status bar.
surface_altA second surface: every other row of the result grid, form fields and buttons.
edit_inserted_rowThe 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.
borderBorders and rules between panes and around dialogs.
accentThe main accent: the focused pane’s border, dialog borders, labels and names to notice.
accent_warmThe warm accent: values on the settings screen, an open transaction, keys in menus.
fgBody text.
fg_mutedSecondary text: descriptions, labels and hints.
fg_dimFaint text: placeholders, empty lists and notes such as a database’s default.
current_stmt_barThe bar in the editor’s gutter next to the statement a run would take.
running_stmt_barThe bar and the spinner in the editor’s gutter next to the statement that runs now.
successSuccess: rows a run returned, a connected tunnel, a success message.
warningWarnings.
errorErrors.
null_fgNULL in the result grid and the inspector.
key_pkThe mark of a primary key column.
key_fkThe mark of a foreign key column.
key_uqThe mark of a unique key column.
mode_normalThe background of the status bar’s mode badge in Normal mode.
mode_insertThe background of the mode badge in Insert mode.
mode_visualThe background of the mode badge in Visual mode.
mode_commandThe background of the mode badge while the command line is open.
mode_fgThe text of the mode badge (bold, on the mode’s colour).
chart_1The first series of a chart.
chart_2The second series of a chart.
chart_3The third series of a chart.
chart_4The fourth series of a chart.
chart_5The fifth series of a chart.
chart_6The sixth series of a chart.

Styles ([styles])

TokenWhat it styles
selectionThe selected row or item, and the grid’s cursor cell.
rangeCells and headers of a selected range in the result grid (the cursor’s cell keeps selection).
cursor_lineThe 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_rowThe 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_headerThe 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.
hoverWhat is under the mouse pointer: a list row, a grid cell, a tab, a button.
yank_flashCells 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_stmtA faint tint behind the statement a run would take.
running_stmtA tint behind the statement that runs now.
run_hintThe hint after a statement’s last line that says what its last run did.
syn_keywordSQL keywords.
syn_functionSQL function names.
syn_stringSQL strings and dollar-quoted bodies.
syn_numberSQL numbers.
syn_commentSQL comments.
syn_operatorSQL operators and punctuation.
syn_identifierSQL names, parameters and variables.
syn_quoted_identSQL quoted names.
syn_exec_commentThe opening and closing of a MySQL executable comment (/*! … */).
search_matchA match of the editor’s search on the lines on screen.
match_parenThe bracket matching the one at the editor’s cursor (not drawn yet).
read_only_markThe mark of read-only rules (its fg): the lock on a profile’s row and tabs, and the rule’s words.
danger_markThe mark of something that changes or deletes data, or of a production profile (not drawn yet).
plan_hotThe plan nodes that take the most time, in every view of a query plan and its summary.
plan_misestimateHow far a plan node’s estimated rows are from the actual ones, in the plan, its summary, detail and funnel.
size_warmExplorer size estimates of 1 MiB or more (with five levels: 100 MiB or more).
size_hotExplorer size estimates of 1 GiB or more (with five levels: 10 GiB or more).
size_mildWith five size levels, explorer size estimates of 1 MiB or more.
size_heatedWith five size levels, explorer size estimates of 1 GiB or more.
tab_activeThe 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_matchA 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_currentThe grid’s cursor cell while it is a match of the grid’s search.
edit_changedA 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_pendingHow many changes of a result are pending, on the result tab strip and the hint line.
edit_insertedThe 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_deletedA row of the result grid to be deleted, not applied yet (its text; crossed out, so it shows without colour).
edit_appliedA 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_invalidA 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

VariableWhat it does
DATARIG_NO_UPDATE_CHECKAny value (but an empty one): no update check in this process, whatever [updates] check says. Nothing is sent to GitHub.
CISet (not empty): no update check, as on a CI service.
HTTPS_PROXYThe 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_PROXYAs HTTPS_PROXY, when neither it nor https_proxy is set; all_proxy when it is not set.
NO_PROXYHost 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_HOMECargo’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_STOREmemory 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_HOMEThe config file is $XDG_CONFIG_HOME/datarig/config.toml when it is set (and --config is not given).
HOMEThe 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.
USERPROFILEThe home directory where HOME is not set (Windows).
COLORTERMtruecolor or 24bit: the terminal draws 24-bit colours, and the themes’ tints are drawn as they are.
TERMA 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_DIRThe data directory (saved queries, datarig’s own known_hosts), over every other rule.
DATARIG_STATE_DIRThe 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_HOMEThe 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_HOMEThe 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.
APPDATAWindows: the data directory’s default is %APPDATA%\datarig.
LOCALAPPDATAWindows: the state directory’s default is %LOCALAPPDATA%\datarig.
LC_ALLWith 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_MESSAGESAs LC_ALL, when that is not set.
LANGAs LC_ALL, when neither it nor LC_MESSAGES is set.
VISUALThe editor a query is opened in to edit it outside datarig, split into words as a shell would.
EDITORAs VISUAL, when that is not set; without either, vi (notepad on Windows).
SSH_CONNECTIONSet (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_TTYAs SSH_CONNECTION.
SSH_CLIENTSet: the macOS keychain offer is never made (as in an SSH session).
SSH_AUTH_SOCKThe ssh-agent an SSH tunnel with auth = "agent" logs in with.
TMUXSet: 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-night and gruvbox follow 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 terminal until 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 SELECT of one plain table: a join, WITH, GROUP BY or DISTINCT, 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 V reads 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?

WhatWhere
Config file--config <path>, else $XDG_CONFIG_HOME/datarig/config.toml, else ~/.config/datarig/config.toml (on every OS)
Secrets file, theme filesNext 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: f shows it as the server sent it (or laid out again), and y and o always 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: primary or standby on PostgreSQL, server takes writes or server read-only on 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 (M on 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. b goes 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. i shows 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 with L.

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_format sets the format y and Y copy in: TSV (the default), CSV, JSON, indented JSON, Markdown, HTML, XML or SQL INSERT. Choose it in the settings, with :set copy_format=csv, or with Space 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. SQL INSERT is used only for rows certainly of one table, else the copy is TSV and says why. g p still pastes the copied values exactly, whatever the clipboard got.
  • :theme update lists 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 (TERM such as xterm-256color without COLORTERM=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_PROXY when one is set). A newer one is told about at the next start, with the command that updates your install; Esc closes it (it comes back after a week), s skips that version, o turns the checks off. :update says what the last check found. Turn it off with [updates] check = "off" (settings screen, Updates), :set updates.check=off or DATARIG_NO_UPDATE_CHECK=1; there is no check on CI (CI set).
  • 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). :whatsnew shows 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 update adds them; again only when the tokens it does not list change.
  • New setting ui.background = "theme" | "terminal" (settings screen, :set ui.background=terminal): with terminal the 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, 0 for 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_row and grid_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 y or Y in the result grid paste back exactly as the grid showed them (NULL stays NULL), in any result in any tab, without reading any clipboard: p adds them as new rows (each press another copy), g p pastes them over the cells at the cursor. Each is one step u undoes.
  • 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 V reads it again keeping that line.

Changed

  • Previous page moved from p to [ in the result grid (and ] is next page, as n still is); p now 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=5 or row 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, \t or \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 needed in the invalid-value colour instead of the dim default, 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 dim default: the default’s value (ACTIVE, 0), <generated> for a number the table draws or a generated column, <default: now()> for another default, NULL where 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, and Space d V reads 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 step u undoes. 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. \N pastes NULL (setting edit.paste_null); an empty field is an empty value. Values a column does not take are kept, marked ! in the gutter (theme token edit_invalid) and listed first in the Changes tab, and the changes are not applied until they are fixed. Space d v pastes the system clipboard with clipboard = "system", or "auto" outside SSH. For about a second after a status-line question or a waiting Apply ends without its answer, y, n, c and 1 do nothing in the results and say they were ignored, so a late y never 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; Enter shows the cell in the grid, s switches to the statements that apply them, c copies them exactly as they are sent. Space d p and :changes show the tab. ui.changes_view picks the view the tab opens in.
  • Space d a and :apply ask on the status line instead of opening a dialog: only y applies, 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 with confirm = "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=on is taken as always, as the config file already reads tips = "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; y there runs them in one transaction, each row checked first. See Editing data in the guide.

Changed

  • MySQL: the rows affected by an UPDATE (and by INSERT … 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] tips is now "always" (the default), "until_used" (the earlier behaviour) or "off"; "on" in an existing config reads as "always", and true and false read 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 Space for the action menu), and F1 where Space types text.
  • Startup SQL may set the server’s idle limit (PostgreSQL idle_session_timeout, MySQL wait_timeout). Its value is then the idle disconnect of those sessions in place of the environment’s idle_disconnect: the app closes the tab after that long, warns about an open transaction a minute before, and :session shows 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.