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

dbdesk is a desktop tool to browse and query databases. It connects to PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, DuckDB and ClickHouse (Oracle is planned), shows what is inside them as a tree, and gives you query consoles to write and run SQL, page through, sort and export the results, and edit rows.

The window has three areas, which you can drag around and resize:

  • Explorer, a column down the left: your saved connections (“data sources”) and, under each, its databases, schemas, tables and so on.
  • Consoles, the main area on the right: the Welcome tab and one tab per query console.
  • Log console, under the consoles: what dbdesk is doing — connections opening and closing, queries running and how long they took, errors.

The thin bar at the very bottom shows whether dbdesk’s background engine is running and how many data sources the explorer is connected to.

This manual describes dbdesk as it is today. It is young software: things that are planned but not built yet are not described here, apart from a few warnings in Troubleshooting.

Install and first launch

Download dbdesk from dbdesk.org, or from the releases page: pick the file for your system from the latest release’s Assets.

SystemFile
macOS 11 or later, Apple silicon or Inteldbdesk-<version>-macos-universal.dmg
Linux x86_64, most distributionsdbdesk-<version>-x86_64.AppImage
Debian, Ubuntu and derivativesdbdesk_<version>_amd64.deb
Windows 10 or 11, 64-bit Intel or AMDdbdesk-<version>-windows-x64-setup.exe

macOS

  1. Open the .dmg and drag dbdesk onto Applications.
  2. Open dbdesk from Applications. These builds aren’t signed by Apple yet, so the first launch is blocked with “Apple could not verify dbdesk”.
  3. Open System Settings › Privacy & Security, scroll down to the message about dbdesk, and click Open Anyway. macOS asks once more; confirm, and it opens normally from then on.

After each update, macOS asks again for access to each password dbdesk saved in the Keychain. Choose Always Allow. This happens because the app’s signature changes with every unsigned build; signed builds, planned for a later release, will remove both steps.

Linux

AppImage, on any distribution:

chmod +x dbdesk-*-x86_64.AppImage
./dbdesk-*-x86_64.AppImage

.deb, on Debian, Ubuntu and derivatives. This also adds dbdesk to the application menu:

sudo apt install ./dbdesk_*_amd64.deb

Saved passwords go to the Secret Service (GNOME Keyring, KWallet), which desktop environments already run. On a bare window manager without one, see Troubleshooting.

Windows

  1. Run dbdesk-<version>-windows-x64-setup.exe. Your browser may first say the file isn’t commonly downloaded; choose to keep it.
  2. The first time you run it, Microsoft Defender SmartScreen blocks it: a blue window says “Windows protected your PC” and offers only Don’t run. This is expected. The installer isn’t signed yet, and SmartScreen holds back programs it hasn’t seen downloaded often. To go on, click More info. The window then shows the file’s name and “Unknown publisher”. Check that the name is the file you downloaded from dbdesk.org or the releases page, then click Run anyway.
  3. Follow the installer. It installs dbdesk for your user alone, in %LOCALAPPDATA%\dbdesk, without asking for administrator rights, and adds dbdesk to the Start menu.

SmartScreen asks again for each new installer you download yourself, until dbdesk’s installers are signed and known to it. Updates that dbdesk installs (below) run without the warning. On Windows 11 with Smart App Control switched on (in Windows Security › App & browser control), Windows may block the unsigned installer with no Run anyway at all, and dbdesk can’t get round that until it is signed. To remove dbdesk, use Settings › Apps › Installed apps. Saved passwords go to the Windows Credential Manager.

Check the download and the version

Each release has a SHA256SUMS file. In the folder with your download:

shasum -a 256 -c SHA256SUMS --ignore-missing

On Windows, in PowerShell, print the installer’s checksum and compare it with its line in SHA256SUMS:

Get-FileHash dbdesk-*-windows-x64-setup.exe -Algorithm SHA256

Help › About dbdesk shows the version and the commit it was built from, and Copy version copies them for a bug report. From a terminal, dbdesk --version prints the same.

Updates

When dbdesk starts, it asks GitHub whether a newer release is out. If one is, the status bar says so, for example dbdesk 0.5.0 is available. Click it to open Updates; Help › Check for updates… asks at any time.

  • macOS, and the Linux AppImage: click Install and restart. dbdesk downloads the new version, checks that it was signed by the dbdesk release process, and puts it in place of the one you are running. Click Restart now to start it, or carry on and it starts next time. If a console has a transaction open, quitting asks first, as it always does.
  • Windows, installed with the installer: click Install and restart. dbdesk downloads the new version’s installer and checks its signature in the same way; the dialog then says the update is downloaded. Click Restart now: dbdesk quits (asking first about open transactions), the installer shows a progress bar while it replaces dbdesk, and the new version starts. Windows can’t replace a program that is running, so if you carry on instead, the update waits until you choose Restart now, or is downloaded again next time. The installer closes any other dbdesk window that is still open.
  • The .deb, a Windows copy that wasn’t installed with the installer, and builds from source: these can’t replace themselves, and the dialog says why. Open release page leads to the download; install it as above. Copies installed on Windows from version 0.12.0 or earlier are in this group too: install the next version from the release page once, and it updates itself from then on.

An update that isn’t signed with dbdesk’s key is refused before anything is replaced. On macOS, until dbdesk’s builds are signed by Apple, the first launch after an update asks again for access to each saved password; choose Always Allow.

The check sends nothing about you or your data: it reads the public list of releases. To stop the check at start-up, untick Check for a new release when dbdesk starts in View → Settings….

Build from source

For contributors, or to run the latest unreleased code. You need:

  • Rust 1.95 or newer, installed with rustup. Check with rustc --version.

  • Git, to fetch the source.

  • On Linux, the development headers for the windowing system. On Debian or Ubuntu:

    sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
    

Then:

git clone https://github.com/navasvarela/dbdesk.git
cd dbdesk
cargo run --release -p dbdesk-app

To install the dbdesk command so you can start it from anywhere:

cargo install --path crates/dbdesk-app

First launch

On first launch the explorer is empty (“No data sources yet”) and the Welcome tab offers three things to do:

  • New data source — add a connection to a database. See Data sources.
  • New query console — open a console on a data source. It is greyed out until you have one.
  • Open the manual — opens this manual in your browser.

The log console under the Welcome tab shows engine started, and the status bar reads Engine running · 0 connected. If it says the engine is stopped, see Troubleshooting.

Light and dark

dbdesk follows your system’s light or dark setting. To choose one yourself, open View → Theme and pick Match system, Light or Dark. The choice is remembered.

The layout

dbdesk opens maximized the first time; after that it remembers the window’s size and position.

Every panel is a tab you can drag: onto another panel’s tab bar to join it, or onto the edge of a panel to split it. Drag the gaps between panels to resize them. dbdesk remembers the layout and the window size when you quit. View → Reset layout puts everything back where it started.

On a Mac

On macOS dbdesk’s menus are in the menu bar at the top of the screen, like any Mac app’s, and the window has no separate title bar. On Linux and Windows the menus are in the window, under its title bar, and this manual names them that way (View → Settings…). On a Mac a few of them live where Mac apps keep them:

In the window (Linux, Windows)On a Mac
Help → About dbdeskdbdesk → About dbdesk
Help → Check for updates…dbdesk → Check for updates…
View → Settings…dbdesk → Settings… (⌘,)

The rest of the Mac’s menu bar:

  • dbdesk → Quit dbdesk (⌘Q) quits the way closing the window does: if a console has an open transaction or unsaved changes, dbdesk asks first. Hide, Hide Others, Show All and Services are the usual ones.
  • Edit has Undo, Redo, Cut, Copy, Paste and Select All for whatever has focus: the SQL editor, a text field, or the results grid (Copy copies the selected cells).
  • View → Enter Full Screen (⌃⌘F), and Window with Minimize (⌘M), Zoom and Bring All to Front.
  • Help → dbdesk manual opens this manual, and the Help menu’s search finds any menu item.

Items that need a console, such as File → Save or Code → Format, are greyed out until a console’s tab is selected, and every item shows its shortcut, including ones you changed in keymap.toml (see Keyboard shortcuts).

The strip along the top of the window, beside the close, minimize and zoom buttons, works as a title bar: drag it to move the window, and double-click it to zoom.

dbdesk also takes the Mac’s look: text is in the system font, SF Pro (SF Mono for SQL and results), and selected text, cells and rows are tinted with the accent colour chosen in System Settings → Appearance. Change the accent colour and dbdesk follows it when you come back to its window. The pictures in this manual were taken with dbdesk’s own fonts and colours, which it uses on Linux and Windows.

Data sources

A data source is a saved connection: which database to connect to and how. dbdesk keeps the list in a file (see Where dbdesk keeps its files) and passwords in your system’s keychain, never in that file.

Adding one

Click + at the top of the explorer (or New data source on the Welcome tab) and choose PostgreSQL, SQLite, MySQL / MariaDB, SQL Server, DuckDB or ClickHouse.

Every kind has:

  • Name — how it appears in the explorer. Leave it empty and dbdesk names it for you: database@host for a server, the file name for SQLite.
  • Colour — optional. The colour tints the data source’s row in the explorer, the tabs of its consoles and a band along the top of each console. Giving production a red and your local copy nothing makes it hard to run something in the wrong place.
  • Safety — how careful dbdesk is with the data. See Safety levels.

PostgreSQL adds:

FieldDefaultNotes
HostlocalhostName or IP address of the server.
Port5432A number from 1 to 65535.
DatabasepostgresThe database to connect to. The explorer lists the server’s other databases too.
Userpostgres
TLSpreferWhether to encrypt the connection; see below.
SSHoffConnect through an SSH server; see SSH tunnels.
PasswordnoneStored in the keychain when you save. See below.
MCP loginnoneAnother user, only for AI assistants; see The MCP login.
MCP passwordnoneShown once there is an MCP login. Stored in the keychain apart from the password.

TLS

TLS encrypts the connection, which hosted databases usually require. The modes are the ones PostgreSQL’s own tools call sslmode, so a provider’s connection guide reads the same:

ModeEncryptsChecks the server’s certificate
disableNever—
preferIf the server offers it, otherwise connects unencryptedNo
requireAlways; fails if the server can’tNo
verify-fullAlwaysYes: signed by an authority you trust, and naming the host you typed

prefer and require keep anyone on the network from reading what you send, but not from pretending to be the server. Use verify-full for anything that matters.

With verify-full, a CA file row appears. Leave it empty for servers whose certificate is signed by a public authority (the same ones web browsers trust). For a private or self-signed certificate, give the authority’s certificate as a PEM file, typed or picked with Browse…: for example the bundle your cloud provider publishes for its managed databases. The Host must be a name (or IP address) the certificate lists.

dbdesk encrypts with rustls, so there is no OpenSSL to install. Client certificates aren’t supported yet.

MySQL / MariaDB has the same fields as PostgreSQL, with these defaults:

FieldDefaultNotes
HostlocalhostName or IP address of the server.
Port3306
DatabasemysqlThe database unqualified names refer to, as USE would set. The explorer lists every database you may see.
Userroot
TLSpreferAs for PostgreSQL. MySQL 8 and MariaDB 11.4 encrypt by default, with a self-signed certificate unless the server is given one, so verify-full needs that server’s CA file.
PasswordnoneStored in the keychain when you save.
MCP loginnoneAnother user, only for AI assistants; see The MCP login.
MCP passwordnoneShown once there is an MCP login. Stored in the keychain apart from the password.

One data source type serves both: dbdesk tells MySQL and MariaDB apart once it connects, and Test connection says which it found (“Connected to MariaDB 11.4.4”).

SQL Server has the same fields too, for SQL Server and Azure SQL, with these defaults. It has no MCP login, since MCP clients can’t use SQL Server yet.

FieldDefaultNotes
HostlocalhostName or IP address of the server. For Azure SQL, the server’s name, such as myserver.database.windows.net.
Port1433Named instances aren’t looked up: give the instance’s port.
DatabasemasterThe database the console works in. The explorer lists every database the login may open.
UsersaA SQL Server login. Windows and Microsoft Entra ID sign-in aren’t supported yet.
TLSpreferSQL Server always offers encryption, with a certificate of its own making unless it is given one, so prefer and require encrypt without checking it, and verify-full needs that server’s CA file (or, for Azure SQL, nothing: its certificate is publicly signed).
PasswordnoneStored in the keychain when you save.

Test connection says which server it reached (“Connected to SQL Server 16.0.4305.5”; 16 is SQL Server 2022).

ClickHouse has the same fields, with these defaults:

FieldDefaultNotes
HostlocalhostName or IP address of the server. For ClickHouse Cloud, the service’s host name.
Port8123ClickHouse’s HTTP port: dbdesk speaks HTTP to ClickHouse, not its native protocol on 9000.
DatabasedefaultThe database unqualified names refer to, as USE would set. The explorer lists every database you may see.
Userdefault
TLSdisabledisable, require or verify-full; see below.
PasswordnoneStored in the keychain when you save.

ClickHouse serves HTTPS on a port of its own (8443) rather than offering encryption on the HTTP one, so there is no prefer: choosing require or verify-full moves the port to 8443, and disable moves it back, unless you typed a port of your own. ClickHouse Cloud only accepts HTTPS, with a publicly signed certificate, so verify-full with no CA file works there.

Test connection says which server it reached (“Connected to ClickHouse 25.8.33.6”).

SQLite adds:

  • File — the full path to the database file. Type or paste it, or use the buttons under the field:

    • Browse… opens your system’s file dialog to pick an existing database. It shows every file, because SQLite databases end in .db, .sqlite, .sqlite3 or nothing at all.
    • New… opens your system’s Save dialog and creates an empty database file where you choose, then fills in the path and clears Read-only. The message under the form says whether it worked; click Save to keep the data source. dbdesk never overwrites an existing file, even if the Save dialog asked whether to replace it and you said yes: pick another name instead.

    Connecting never creates a file, so a typo in the path is an error rather than a new, empty database. New… is the only way dbdesk creates one.

  • Read-only — opens the file read-only. Statements that would change it fail with SQLite’s error.

DuckDB has two kinds of database, chosen under Database:

  • File — a DuckDB database file (.duckdb), with File, Browse…, New… and Read-only as for SQLite. Only one program at a time may have a DuckDB file open for writing, so connecting fails while another program (the duckdb command, say) has it open.
  • In memory — an empty database that lives as long as the connection, for querying files rather than storing data: DuckDB reads CSV, Parquet and JSON files directly. Each console has its own, and everything created in it is gone when it disconnects.

DuckDB is part of dbdesk, so there is nothing to install. Test connection says which version it is (“Connected to DuckDB 1.5.6”).

SSH tunnels

Many databases only accept connections from inside their own network, through a jump host (or bastion) you reach with SSH. Tick Connect through an SSH server and dbdesk opens an SSH tunnel for you before connecting: no ssh -L to run first, and nothing to install. Every server engine has it: PostgreSQL, MySQL and MariaDB, SQL Server and ClickHouse.

FieldDefaultNotes
SSH hostnoneThe SSH server’s name or IP address.
SSH port22
SSH usernoneWhom to sign in to the SSH server as.
SSH loginSSH agentHow to sign in; see below.
Key filenoneFor Key file: your private key, in OpenSSH’s format. Browse… opens ~/.ssh.
PassphrasenoneFor an encrypted key. Stored in the keychain when you save.
SSH passwordnoneFor Password. Stored in the keychain when you save.

Host, Port and Database above stay the database’s own, as the SSH server sees it: often a private address such as 10.0.4.12, or localhost when the database runs on the SSH server itself. TLS still checks the database’s certificate against that host, so verify-full works through a tunnel too.

The ways to sign in:

  • SSH agent uses the keys your running agent holds, as ssh does: the agent SSH_AUTH_SOCK names on macOS and Linux, and Windows’ OpenSSH agent or Pageant on Windows. Nothing is stored.
  • Key file signs in with a private key. An encrypted key’s passphrase is asked for the first time it is needed, like a password, and kept in the keychain; an unencrypted key never touches the keychain.
  • Password signs in with the SSH user’s password, kept in the keychain.

The SSH settings take placeholders like the server’s, and they travel in an export; the passphrase and password stay in each machine’s keychain.

The SSH server’s host key is checked against ~/.ssh/known_hosts, the file ssh itself keeps, so a server you already reach with ssh is trusted by dbdesk too. The first time dbdesk meets one it doesn’t know, it shows the key’s SHA-256 fingerprint and asks. Compare it with the fingerprint the server’s administrator gives you (ssh-keygen -lf on the server’s host key): Trust and connect adds it to known_hosts and connects. A server whose key has changed since is refused, with the line of known_hosts to remove if the change was expected; see Troubleshooting.

A data source’s consoles and its explorer share one tunnel, which closes with the last of them.

Testing the connection

Test connection connects with what is in the form right now, checks the connection works, and disconnects. The result appears under the form (“Connected to PostgreSQL 17.6”, or the error) and in the log console. Nothing is saved by testing, not even the password.

Saving

Save checks the form and saves the data source. Required fields left empty, or a port that isn’t a number, show a message under the form instead. The dialog stays open until the save has gone through; if it fails, the reason is shown in the dialog.

Cancel, Esc or a click outside the dialog closes it without saving.

Safety levels

A colour only marks a data source. Its safety level decides what its consoles may change:

LevelWhat happens
NormalChanges run when asked. Only an UPDATE or DELETE without WHERE asks first (see Query consoles).
CarefulEvery change asks first, naming the data source: statements that change data or the schema, and edits in the grid. New consoles start in Manual commit mode.
Read-onlyChanges are refused. dbdesk reads each statement before sending it and refuses anything but reading, then runs it in the database’s own read-only mode, which also stops a write hidden in a function. The grid can’t be edited.

Picking red for a data source whose level is Normal suggests Careful, since red usually marks production; Make it careful takes the suggestion. The level is its own setting, so a red data source can stay Normal, and a careful one can be any colour.

Read-only uses the same checks as an AI assistant’s read-only projects (see Connecting an AI assistant):

  • PostgreSQL runs each statement in BEGIN READ ONLY, MySQL and MariaDB in START TRANSACTION READ ONLY, rolled back once its rows are read. SQLite opens the file read-only.
  • SQL Server, DuckDB and ClickHouse have no read-only mode dbdesk uses yet: statements are still checked, but a write inside a procedure or function isn’t stopped. The dialog says so. (A DuckDB file can also be opened with its own Read-only box ticked.)
  • Anything that isn’t a plain read is refused, including SET, BEGIN and statements dbdesk can’t parse, since what they do can’t be told.

Placeholders: ${VARIABLE}

Host, port, database, user, a SQLite or DuckDB file and a CA file may hold ${NAME} placeholders, which dbdesk replaces with the environment variable NAME each time it connects. One shared list of data sources then works on machines whose settings differ: each machine sets PGHOST, say, and the data source’s host is ${PGHOST}. A placeholder can be part of a value (db-${ENV}.internal), and a path starting with ~ is in your home folder.

Under each field with a placeholder, the dialog shows what it resolves to now, or which variable isn’t set. Connecting with an unset variable stops with an error that names the setting: could not resolve the host: ${PGHOST} is not set.

Variables come from the environment dbdesk was started in. On macOS, an app opened from the Finder or the Dock doesn’t see what your shell profile sets; start it from a terminal (open -a dbdesk, after exporting the variables), or set them with launchctl setenv.

Passwords and the keychain

PostgreSQL, MySQL and SQL Server passwords are stored in your operating system’s credential store: the Keychain on macOS, Credential Manager on Windows, the Secret Service (GNOME Keyring, KWallet) on Linux. Each entry has the service name dbdesk and, as its account, the data source’s ID, a long string that never changes even if you rename the data source.

  • When you save, a typed password is written to the keychain. When you edit a data source the password field is empty and says unchanged: leave it empty to keep the saved password, or type a new one to replace it.
  • When dbdesk connects — the explorer, each console, and Test connection with an empty password field — it reads the password from the keychain. On macOS the system may ask whether to let dbdesk use it; see Troubleshooting.
  • When there is none, or the keychain can’t be read (locked, declined, or missing on this system), connecting asks for the password. Save to keychain is ticked, so you are asked once. Whatever you type is also kept in memory until dbdesk quits, so the data source still connects if the keychain refuses it or you untick the box. A server that needs no password (local trust login, say) works with the field left empty.
  • When you delete a data source, its keychain entry is removed too.
  • An MCP login’s password has an entry of its own, with the data source’s ID followed by /mcp as its account. Only dbdesk mcp reads it.
  • An SSH password or key passphrase has one too, with /ssh after the ID. It is asked for, and saved, the same way as the database password.

SQLite data sources have no password, so dbdesk never asks the keychain for them.

Editing, connecting and deleting

Right-click a data source in the explorer for:

Menu itemWhat it does
Connect / DisconnectOpens or closes the explorer’s connection. Expanding a data source also connects it.
RefreshReloads everything under the data source (while connected).
New query consoleOpens a console on this data source. See Query consoles.
Open saved consoleReopens a console you closed, by its file name. Shown when there are any.
New › Schema…Creates a database, on MySQL, MariaDB and ClickHouse, whose schemas are databases. See Views and schemas.
Edit…Opens the dialog with the saved settings.
Move to project ›Puts the data source in a project, in No project, or in a new project. See Projects.
Delete…Asks for confirmation, then removes the data source and deletes its saved password.

Hover over a data source to see its driver, where it connects to and its connection state. The dot after its name shows the state of the explorer’s connection: amber while connecting, green when connected, red when the last attempt failed, and no dot when disconnected. A failed connection’s error is in the tooltip and in the log console.

Moving data sources to another machine

File › Export data sources… writes data sources, and the projects they are in, to one file. File › Import data sources… adds them on another machine, or updates the ones it already has. Use it to set up a new computer, or to share a team’s databases. Passwords are never exported: each data source asks for its own the first time it connects (see Passwords and the keychain).

Exporting

Tick the data sources to export. Ticking a project ticks all of its data sources; a project is also exported whenever one of its data sources is, so each lands in its project on the other machine. Projects carry their MCP settings, and data sources their MCP login’s user, but not its password (see Connecting an AI assistant). Export… asks where to write the file.

The file is TOML, in the same format as your own data_sources.toml (see Where dbdesk keeps its files), plus a format_version and the projects. It is readable and diffable, so a team can keep it in a repository.

Paths travel with it: a SQLite or DuckDB file, or a CA file, next to the exported file (or in a folder under it) is written relative to it, and one in your home folder as ~/…. Both are turned back into full paths on import, so a repository holding the export and its databases works wherever it is checked out. Paths with a placeholder are kept as they are.

Importing

Import data sources… asks for a file, then shows what importing it would change before anything is saved:

  • Each project and data source is new, unchanged, or differs, with each setting that differs shown as this machine’s value → the file’s.

  • Entries are matched by their id, which every export keeps, so importing a file twice never duplicates anything. Projects are matched by id, and then by their slug.

  • A database or CA file this machine doesn’t have is shown in amber. Import it anyway, and change the path afterwards with Edit….

  • Choose, for each entry:

    ChoiceWhat it does
    Import / UpdateAdds it, or makes this machine’s entry match the file. A project keeps its slug, so MCP clients set up with it keep working.
    SkipLeaves this machine as it is. A data source in a skipped new project is imported in No project.
    Import as a copyAdds it next to this machine’s entry, under a new id, with “(copy)” after its name. A copied project gets a slug of its own.

New and changed entries start on Import or Update, unchanged ones on Skip, so importing a file you imported before changes nothing. Import saves the projects, then the data sources.

The explorer

The explorer shows each data source as a tree. Nothing is loaded until you open it: expanding a node asks the database for that node’s children, and a spinner shows while it waits. Expanding a data source that isn’t connected connects it first.

Click the arrow before a node, or double-click its name, to expand or collapse it.

Projects

Projects group data sources, say one per application you work on. Once there is at least one project, the explorer shows each project as a header you can fold, with its data sources under it, and the data sources in no project under No project. With no projects, it lists the data sources on their own, as before.

  • New project… is in the + menu, on every project header’s menu, and at the bottom of Move to project. From Move to project, the data source moves into the new project.
  • Move to project › on a data source’s menu lists the projects and No project; the one it is in now is marked.
  • Rename… on a project changes only its name.
  • Delete… on a project moves its data sources to No project first. No data source is deleted.
  • Project settings… on a project sets whether, and how, AI assistants may use its data sources; Copy MCP configuration copies what an assistant’s client needs to connect. See Connecting an AI assistant.

Each project also has a short slug, made from its first name: lower case, with hyphens for spaces (Web shop becomes web-shop). Hover over a project to see it. Renaming a project keeps its slug, so whatever refers to it by slug keeps working. A data source belongs to at most one project, and a project may hold data sources of any colour and engine.

What the tree contains

PostgreSQL

data source
├─ roles                  the server's users and groups (see Users and roles)
└─ database               every database on the server that accepts connections
   └─ schema
      ├─ tables
      │  └─ table
      │     ├─ columns    with their type, "not null" and "primary key"
      │     ├─ indexes
      │     ├─ constraints
      │     └─ triggers
      ├─ views
      ├─ materialized views
      ├─ functions        with their argument types, so overloads are told apart
      └─ sequences

SQLite

data source
└─ main
   ├─ tables
   │  └─ table
   │     ├─ columns
   │     ├─ indexes
   │     ├─ constraints   primary and foreign keys
   │     └─ triggers
   └─ views

MySQL and MariaDB, where a database and a schema are the same thing:

data source
└─ database               every database you may see, shown as a schema
   ├─ tables
   │  └─ table
   │     ├─ columns    with their type and "not null"
   │     ├─ indexes    with their columns, and whether they are unique
   │     ├─ constraints
   │     └─ triggers
   ├─ views
   ├─ functions
   ├─ procedures
   └─ events          scheduled with CREATE EVENT

SQL Server, which keeps schemas inside each database, as PostgreSQL does, and lets one connection browse them all:

data source
└─ database                 every database the login may open
   └─ schema
      ├─ tables
      │  └─ table
      │     ├─ columns      with their type, length and "not null"
      │     ├─ indexes      with their key columns, clustered or not, and whether unique
      │     ├─ constraints  primary key, unique, foreign key, check and default
      │     └─ triggers
      ├─ views
      ├─ functions
      ├─ procedures
      ├─ sequences
      └─ synonyms           with the object each one names

ClickHouse, where, as on MySQL, a database holds its tables directly:

data source
└─ database                 every database you may see, shown as a schema
   ├─ tables
   │  └─ table
   │     ├─ columns         with their type
   │     └─ indexes         data-skipping indexes, with their type and expression
   ├─ views
   ├─ materialized views
   │  └─ columns
   └─ dictionaries
      └─ columns

A ClickHouse table’s primary key is its sort order rather than an index, so it shows in the table’s definition, not under indexes. Functions aren’t listed: a ClickHouse function belongs to the whole server, not to one database.

DuckDB, where the database opened (named after its file, or memory) is listed with any attached with ATTACH:

data source
└─ database                 the one opened, and attached ones
   └─ schema                main, and any you create
      ├─ tables
      │  └─ table
      │     ├─ columns      with their type and "not null"
      │     ├─ indexes
      │     └─ constraints  primary key, unique, foreign key and check
      ├─ views
      ├─ functions          macros made with CREATE MACRO
      └─ sequences

Each kind of object has its own icon and colour, and folders that have been loaded show how many objects they hold (tables 5).

Browsing another PostgreSQL database than the one in the data source’s settings opens a separate connection to that database.

The context menu

Right-click an object for what you can do with it:

Menu itemShown forWhat it does
Open dataTables, views, materialized viewsOpens a new console with SELECT * FROM the object and runs it.
Import CSV…TablesAsks for a CSV file and imports it into the table; see Importing a CSV file.
Copy nameObjects inside a schemaCopies the name to use in SQL: schema-qualified for tables, views, sequences, indexes and functions (main."Order Items"), bare for columns, constraints and triggers. Quoted where needed.
Show definitionObjects whose source the database keeps (see below)Opens the source in a window.
RefreshAnything with childrenReloads the node’s children and collapses everything below it.
Copy DDLTables, views, materialized viewsCopies the statement that creates it; see Copy DDL.

Below those come the object helpers, New › Table…, New index…, Rename…, Truncate…, Drop… and Drop index…, which Creating and changing tables describes, and those for views and schemas (New › View…, Edit…, Refresh…, New › Schema…, Drop schema…), which Views and schemas describes.

A data source’s own menu (Connect, New query console, Open saved console, Edit, Delete and so on) is described in Data sources.

Definitions

Show definition opens a window with the object’s SQL, which you can select and copy with the Copy button.

Which objects have one depends on the database:

  • PostgreSQL: views, materialized views, functions, indexes, constraints and triggers.
  • SQLite: tables, views, triggers, and indexes created with CREATE INDEX (not the ones SQLite makes for primary keys and unique constraints).
  • MySQL and MariaDB: tables, views, functions, procedures, events and triggers, as SHOW CREATE … gives them. A routine whose source you may run but not read shows a message instead.
  • SQL Server: views, functions, procedures and triggers, as SQL Server stores their source (what OBJECT_DEFINITION returns), and synonyms. One created WITH ENCRYPTION, or that your login may run but not read, shows a message instead.
  • DuckDB: tables, views, macros, indexes and sequences, as DuckDB keeps them. A macro’s definition is rebuilt from its parameters and body, one CREATE MACRO per parameter list.
  • ClickHouse: tables, views, materialized views and dictionaries, as ClickHouse keeps their CREATE statement.

Filtering

Type in the box at the top of the explorer to filter the tree by name, ignoring case. An object that matches is shown with everything under it, and the nodes leading to it stay open even if they don’t match themselves. Data sources and projects are matched by name too: a project whose name matches shows all its data sources, and data sources and projects with nothing matching are hidden while you type.

The filter only searches what has already been loaded: it never asks the database for more. Expand the parts you want to search first.

System objects

The explorer hides each database’s own objects:

  • PostgreSQL’s pg_catalog, information_schema and temporary schemas, and its predefined pg_ roles;
  • SQLite’s internal sqlite_ tables;
  • MySQL’s mysql, information_schema, performance_schema and sys databases;
  • SQL Server’s master, model, msdb and tempdb databases, and in every database the sys, INFORMATION_SCHEMA and guest schemas and those of the fixed database roles;
  • DuckDB’s system and temp databases, and its information_schema and pg_catalog schemas;
  • ClickHouse’s system and information_schema databases (in either case), and the .inner tables where a materialized view without a TO table keeps its rows.

The eye button next to + shows or hides them, for every data source at once.

When the connection closes

Disconnecting forgets everything the explorer had loaded for that data source. It is loaded again, fresh, the next time you expand it. Refresh does the same for a single node while connected, which is how you see a table you just created.

A connection can also be lost: the server restarted, an administrator ended the session, or the network or SSH tunnel dropped. dbdesk notices when a request fails that way, and, before using a connection that has sat unused for a minute, by checking that it still answers. The data source’s dot turns red and its tooltip says Disconnected and why. The tree stays as it was, and the next thing you expand or refresh connects again by itself.

Creating and changing tables

The explorer can create tables and indexes, and rename, empty and drop tables, without you writing the SQL. Each of these helpers is a small dialog that writes the statement, shows it to you, and runs it only when you click Execute. You can also take the SQL to a console and change it there first, or just copy it: the helpers are a quick way to learn how each database spells a statement.

Where to find them

Right-click in the explorer. What the menu offers depends on what you clicked and on what the database can do:

Right-click onMenu items
A schema, or its tables folderNew › Table…
A tableModify table…, New index…, Rename…, Copy DDL, Truncate…, Drop…
A table’s indexes folderNew › Index…
An indexDrop index…

  • ClickHouse has no index helpers: its data-skipping indexes need a type and a granularity, which a later version will ask for.
  • Copy DDL is offered where there is a CREATE TABLE statement to copy (see Copy DDL).
  • Views, materialized views and schemas have helpers of their own: see Views and schemas.
  • On PostgreSQL, SQL Server and DuckDB, the helpers appear only inside the database the data source connects to, which is where their statements run. Browse another database’s objects freely, but change them from a data source that connects to it.

Review and run

Every helper ends the same way, with the SQL it will run:

  • Execute runs the statement. If the database refuses it, its error shows in red under the SQL and the dialog stays open, so you can fix what you typed and try again. Once it succeeds the dialog closes, and the explorer reloads the folder that changed and opens it, so you see the new table or index straight away.
  • Open in console puts the SQL in a new console instead, to edit or run it there. The dialog closes.
  • Copy copies the SQL.

The SQL changes as you fill in the dialog. While something is missing (a table name, a type, the columns of a key) the dialog says what instead of showing SQL, and Execute waits.

Names are quoted only when they need to be: when they have capitals, spaces or other characters a bare name can’t have, or are a word any of the databases dbdesk supports reserves, such as order or user. customer_id stays customer_id; Order Items becomes "Order Items" (`Order Items` on MySQL and ClickHouse, [Order Items] on SQL Server).

When a change does several things

Some changes take more than one statement. On PostgreSQL, SQLite, SQL Server and DuckDB they run in one transaction: if one fails, the ones before it are undone and nothing has changed. MySQL, MariaDB and ClickHouse commit each statement as it runs, so the panel warns you before you run several, and after a failure says how many of them stay applied.

Safety levels and colours

Helpers follow the data source’s safety level:

  • Read-only: Execute is greyed out and the panel says why. Open in console and Copy still work, so you can take the SQL somewhere it may run.
  • Careful: Execute asks once more, naming the data source, before anything runs.
  • Normal: Execute runs at once.

Truncating or dropping something says exactly what goes: the table and every row in it, with its indexes, keys and triggers. On a data source coloured red, the colour that marks production, you also type the object’s name before Execute can be clicked:

Every statement a helper runs is kept in the query history, like those you run in a console.

New table

Right-click a schema, or its tables folder, then New › Table…. The dialog starts with one column, id, of the database’s most common type, as the primary key: change or remove it as you like.

  • Schema and Name: where the table goes, and what it is called. The schema is the one you clicked.
  • Columns, one row each:
    • Name.
    • Type, typed as the database spells it, so arrays, domains and extension types work too. The ▾ beside it lists the database’s common types (integer, text, timestamptz and so on on PostgreSQL; UInt64, String, DateTime on ClickHouse).
    • Nullable: whether the column may hold NULL. A primary key column never may.
    • Default: an SQL expression, written as it goes after DEFAULT: 0, 'new' (with quotes, for text), now(). SQLite and MySQL want parentheses around an expression that calls a function: (datetime('now')).
    • Primary key: tick each column of the key. The key’s columns are in table order.
    • Add column adds a row; the ✕ at the end of a row removes it.
  • Unique keys: an optional name, and the columns that must be unique together, in the order you tick them.
  • Foreign keys: an optional name, the columns, the table they point at, and that table’s columns, in the same order. The table is picked from those the explorer has loaded: expand a schema’s tables folder first to point at its tables, or pick This table for a key between the table’s own rows. Its columns load once you pick it. On delete and On update say what happens to these rows when the row they point at goes or changes: refuse (NO ACTION, the default), CASCADE, SET NULL and so on, as the database offers them.
  • Checks: an optional name and a condition every row must meet, such as quantity > 0.

What differs between databases:

  • ClickHouse has no unique or foreign keys, so those tabs aren’t shown. Its tables are created with the general-purpose MergeTree engine, sorted by the primary key’s columns (ORDER BY), and a nullable column’s type is wrapped in Nullable(…).
  • DuckDB’s foreign keys only refuse a change: there is no On delete or On update.
  • SQLite’s foreign keys can only point at a table in the same schema (the same database file). SQLite checks them only on connections that switch them on with PRAGMA foreign_keys = ON.
  • SQL Server gets an explicit NULL on nullable columns, since whether a column is nullable by default there depends on the connection’s settings.

Modify table

Modify table… on a table opens the New table dialog filled from the table as it is: its columns with their types, nullability and defaults, its primary key, and its unique keys, foreign keys and checks under the names the database gave them. Change what you like; the SQL below shows the statements that turn the table into what the dialog now says.

  • Rename a column by typing its new name. dbdesk remembers which row each column came from, so this is always a rename that keeps the column’s values, never a drop and an add. Keys that list the column follow its new name.
  • Drop a column with the ✕ at the end of its row, and add one with Add column. Dropping a column loses its values, so the dialog says which columns go before Execute, and on a red data source asks for the table’s name.
  • Change a column’s type, nullability or default in its row. On PostgreSQL and DuckDB, a retyped column gets a USING field below the grid, for an expression that converts the old values (CASE status WHEN 'new' THEN 0 ELSE 1 END); left empty, the database converts them its own way, or refuses.
  • Keys and checks: tick or untick primary key columns, and add or remove unique keys, foreign keys and checks in their tabs. One you leave alone is kept, even if one of its columns is renamed; one you change is dropped and added again.
  • Rename the table with Name. The table stays in its schema.
  • A column the database numbers itself (an identity or AUTO_INCREMENT column) says identity beside its type, and stays so.

The column order doesn’t change: a new column goes at the end. Statements run in an order that works everywhere: constraints that go are dropped first, then columns are dropped, renamed, added and changed, then the new keys and checks are added, and the table is renamed last.

Not every database can make every change in place. What one can’t do, the dialog says instead of showing SQL, and Execute waits; the rest of the change can still go to a console.

DatabaseIn placeRefused
PostgreSQLeverything—
MySQL, MariaDBeverything; a new type or nullability restates the column (MODIFY COLUMN), so its comment and character set go back to the defaults—
SQL Servereverything; a column’s default is a named constraint, dropped and added again around a change to the column—
DuckDBcolumns: rename, add, drop, type, nullability, defaultkeys and checks, the primary key
ClickHousecolumns (nullability is its Nullable(…) type) and checksthe primary key, which is the table’s sort order
SQLiterename, add a nullable column or one with a default, drop— (see below)

SQLite’s table rebuild

SQLite’s ALTER TABLE does little in place, so for anything else (a new type, a column made NOT NULL, a new key or check) dbdesk rebuilds the table the way SQLite’s documentation describes, in one transaction, and says so before Execute:

  1. It creates the new table as new_orders, copies every row into it, drops the old table and gives the new one its name.
  2. It creates the table’s indexes and triggers again, from their definitions.
  3. Only then does it rename and drop columns, in place, so that SQLite itself updates the indexes, triggers, views and other tables’ foreign keys that name a renamed column, and refuses to drop a column one of them still uses.

Foreign keys are switched off while it runs, so dropping the old table can’t delete rows that point at it (ON DELETE CASCADE). If your connection had them on, dbdesk checks them before committing: a change that would leave a row pointing at nothing is undone, and the dialog names the table.

A rebuild writes the table from what dbdesk reads of it, so a table with something it can’t read back is refused, with the reason: a column with its own COLLATE, a generated column, a WITHOUT ROWID or a STRICT table. Make those changes in a console.

New index

New index… on a table, or New › Index… on its indexes folder.

  • Columns: tick them in the order the index should have them.
  • Unique: the index also refuses two rows with the same values.
  • Method, where the database lets you choose: on PostgreSQL B-tree (the default), Hash, GIN (for arrays, jsonb and full-text search), GiST and BRIN; on MySQL and MariaDB, B-tree or Hash.
  • Name: filled in from the table and columns (orders_customer_idx) until you change it.

Drop index… on an index drops it, after saying so. The table and its rows stay. An index that a primary key or unique constraint owns can’t be dropped on its own; the database’s error says so.

Rename, truncate and drop

  • Rename… gives the table a new name in the same schema: ALTER TABLE … RENAME TO …, or RENAME TABLE on MySQL and ClickHouse, or sp_rename on SQL Server.
  • Truncate… deletes every row and keeps the table. SQLite has no TRUNCATE, so it writes DELETE FROM the table, which does the same.
  • Drop… drops the table. On PostgreSQL, Also drop what depends on it (CASCADE) drops the views that read the table and the foreign keys of other tables that point at it too; without it, PostgreSQL refuses while anything depends on the table.

Copy DDL

Copy DDL on a table copies its CREATE TABLE statement.

  • SQLite, MySQL, MariaDB, DuckDB and ClickHouse keep the statement, so dbdesk copies theirs, the same one Show definition shows.
  • PostgreSQL and SQL Server keep no such statement, so dbdesk writes one from the table’s columns (their types, defaults and NOT NULL) and primary key, the same way New table does, followed by an ALTER TABLE … ADD CONSTRAINT … for each of its unique keys, foreign keys, checks and (on PostgreSQL) exclusion constraints, as the database defines them. Running it recreates the table, identity columns included (PostgreSQL’s GENERATED … AS IDENTITY, SQL Server’s IDENTITY(seed, increment)). What it leaves out: indexes that aren’t constraints, comments, storage parameters and partitioning.

Views and schemas

The explorer’s helpers also create, change and drop views, materialized views and schemas. They work like the table helpers: each dialog writes the SQL, shows it, and runs it only when you click Execute, with the same Open in console and Copy, the same safety levels and confirmations, and every statement kept in the query history.

Where to find them

Right-click onMenu items
A data source whose schemas are databases (MySQL, MariaDB, ClickHouse)New › Schema…
A database (PostgreSQL, SQL Server, DuckDB)New › Schema…
A schemaNew › Table…, New › View…, New › Materialized view… (PostgreSQL), Drop schema…
A schema’s views folderNew › View…, New › Materialized view… (PostgreSQL)
A schema’s materialized views folder (PostgreSQL)New › Materialized view…
A viewEdit…, Rename…, Copy DDL, Drop…
A materialized view (PostgreSQL)Refresh…, Edit…, Rename…, Copy DDL, Drop…
  • SQLite has no schema helpers: its schemas are database files attached to the connection, not something CREATE SCHEMA makes. Its views can’t be renamed either: SQLite has no way to.
  • ClickHouse’s materialized views are a different thing (they copy each insert into a table), so dbdesk doesn’t create, edit or drop them; Copy DDL copies theirs.
  • As with tables, on PostgreSQL, SQL Server and DuckDB the helpers appear only inside the database the data source connects to.

New view

Right-click a schema or its views folder, then New › View….

  • Schema and Name: where the view goes, and what it is called.
  • Query: the SELECT the view saves, in an editor highlighted the way consoles are. A ; at the end is left out of the statement.
  • If a view of that name is already in the folder, a tick box offers to replace it instead, as Edit… would.

New › Materialized view… on PostgreSQL adds one choice: Fill it now. Unticked, the view is created WITH NO DATA, empty until its first refresh, which is quicker when the query is slow.

Edit a view

Edit… on a view loads its definition, takes the query out of it, and puts that in the editor. Change it, and the SQL shows how the database will save it:

  • PostgreSQL, MySQL, MariaDB, DuckDB and ClickHouse: CREATE OR REPLACE VIEW. PostgreSQL refuses to drop, rename or retype a view’s columns this way (only adding columns at the end works); its error says so, and Open in console lets you drop and create the view yourself.
  • SQL Server: ALTER VIEW, which keeps the view’s permissions.
  • SQLite: it has neither, so the view is dropped and created again, in one transaction.
  • Materialized views (PostgreSQL) have no OR REPLACE either: they are dropped and created again, in one transaction, and their indexes go with them. The dialog says so before you run it, and a view that other views read can’t be dropped this way.

What dbdesk takes out of the definition is the query alone: options written before AS, such as SQL Server’s WITH SCHEMABINDING, SQLite’s column list, or MySQL’s DEFINER and SQL SECURITY, aren’t kept. MySQL shows a view’s query as it stored it, with every name qualified and quoted. If dbdesk can’t find the query, the whole definition is in the editor, for you to cut down.

Refresh a materialized view

Refresh… runs the materialized view’s query again and stores the new rows: REFRESH MATERIALIZED VIEW. Keep the old rows readable meanwhile (CONCURRENTLY) lets queries read the view while it refreshes; PostgreSQL allows it only on a view with a unique index, so the tick box waits until the view’s indexes show one.

Rename, drop and copy views

  • Rename… gives the view a new name in its schema: ALTER VIEW … RENAME TO … (or ALTER MATERIALIZED VIEW), RENAME TABLE on MySQL and ClickHouse, sp_rename on SQL Server. SQL Server keeps the old name in the view’s stored definition, which Show definition and Copy DDL then show.
  • Drop… drops the view; the tables it reads stay. On PostgreSQL, Also drop what depends on it (CASCADE) drops the views that read it too.
  • Copy DDL copies the view’s definition, the statement Show definition shows.

New schema

New › Schema… on a database, or on the data source where its schemas are databases.

  • Name.
  • Owner, on PostgreSQL and SQL Server: the role or user that owns the schema (AUTHORIZATION). Left empty, you own it.
  • On MySQL, MariaDB and ClickHouse the schemas in the tree are databases, so this writes CREATE DATABASE.

Drop a schema

Drop schema… on a schema. The confirmation counts what the schema holds, from its folders in the tree, and says what happens to it:

  • PostgreSQL and DuckDB refuse to drop a schema that isn’t empty, unless you tick Also drop everything in it (CASCADE), which drops its tables, views, functions and the rest with it.
  • SQL Server has no CASCADE: drop what is in the schema first.
  • MySQL, MariaDB and ClickHouse drop the database and everything in it.

On a data source coloured red, you type the schema’s name before Execute can be clicked.

Users and roles

On PostgreSQL the explorer also lists the server’s roles and creates, changes and drops them. PostgreSQL has one kind of object for both users and groups: a role, which is a user when it may log in. The dialogs work like the table helpers: each one writes the SQL, shows it, and runs it only when you click Execute, with the same safety levels and confirmations.

Other engines have no users or roles in the explorer yet: MySQL’s 'name'@'host' accounts and SQL Server’s logins are planned on their own.

The roles folder

A PostgreSQL data source starts with a roles folder, before its databases, because roles belong to the whole server rather than to one database. Each role says whether it can log in and whether it is a superuser, and which roles it is a member of:

roles
  app_readers
  report_reader    login, member of app_readers
  postgres         login, superuser

The roles PostgreSQL predefines (pg_read_all_data, pg_monitor and the rest) are system objects, hidden until you click the eye button in the explorer’s toolbar.

Right-click onMenu items
A PostgreSQL data source, or its roles folderNew › User…, New › Role…
A roleCopy name, Copy DDL, Edit…, Rename…, Drop…

Copy DDL copies the role’s CREATE ROLE statement, rebuilt from what the catalog says about it: its attributes, limit, expiry and memberships. Its password isn’t in it.

New user and New role

New › User… and New › Role… open the same dialog; New user starts with Can log in ticked.

  • Name.
  • Password and Confirm password. Leave both empty for a role nobody logs in as.
  • Connections: tick Limit to to cap how many connections the role may have open at once (CONNECTION LIMIT).
  • Valid until: when the password stops working, as PostgreSQL reads a date or timestamp, e.g. 2027-01-01 (VALID UNTIL). Empty means never.
  • Member of: the roles whose privileges it gets (IN ROLE). PostgreSQL’s own roles are offered too: pg_read_all_data lets a role read every table, for example.
  • The attributes, each with its keyword in its tooltip: Can log in (LOGIN), Superuser, Create databases (CREATEDB), Create roles (CREATEROLE), Inherit privileges (INHERIT, on unless you untick it), Replication and Bypass row security (BYPASSRLS).

The statement lists only what differs from PostgreSQL’s defaults, e.g. CREATE ROLE report_reader WITH LOGIN CONNECTION LIMIT 10 IN ROLE pg_read_all_data PASSWORD '<redacted>'. The password comes last, so the expiry before it stays readable in the panel and the history.

How the password is sent

The password never leaves your computer as you typed it.

  • dbdesk hashes it into a SCRAM-SHA-256 verifier as soon as both fields match, the same way psql’s \password does, and sends the verifier in its place. PostgreSQL stores a verifier as it is, and checks the password against it when the user logs in.
  • The review panel shows PASSWORD '<redacted>', never the verifier: a verifier is not the password, but whoever holds it can try to guess the password offline.
  • Copy and Open in console are off while the SQL holds a password. The redacted text would set a password nobody knows, and the real statement would put the verifier on the clipboard.
  • The query history and the log keep the statement as the panel shows it, with '<redacted>' in place of the password, as they do for any statement holding one (see Passwords stay out).

This needs PostgreSQL 10 or later, which accepts a verifier whatever its password_encryption setting says.

Edit a role

Edit… loads the role and fills the same fields, then writes what changed:

  • one ALTER ROLE for the attributes, connection limit and expiry you changed;
  • REVOKE for the roles it leaves, and GRANT for the ones it joins.

The password stays as it is unless you type a new one (hashed the same way) or tick Remove password (PASSWORD NULL), after which password logins fail until a new one is set. With nothing changed, there is nothing to execute.

Rename a role

Rename… writes ALTER ROLE … RENAME TO …. A password stored as MD5 is cleared, because PostgreSQL salts those with the role’s name; the SCRAM passwords dbdesk sets are kept.

Drop a role

PostgreSQL refuses to drop a role that still owns something or holds privileges. Drop… counts what the role owns and holds privileges on in the data source’s database, names the databases it owns and the roles that lose its privileges, and asks what to do with what it owns:

  • Keep it: just DROP ROLE, which fails if the role owns anything.
  • Give it to another role: REASSIGN OWNED BY … TO …, then DROP OWNED BY … for the privileges left, then DROP ROLE. This also hands over the databases it owns.
  • Drop it: DROP OWNED BY …, which drops the tables, views and other objects it owns, with their data, then DROP ROLE.

A role that owns something starts on Give it to, and Execute waits until you pick the new owner. Everything runs in one transaction, so a refusal leaves the role and its objects as they were. REASSIGN OWNED and DROP OWNED act only in the database the data source connects to: what the role owns in another database still stops the drop, and the database’s error says so.

On a data source coloured red, you type the role’s name before Execute can be clicked.

Query consoles

A query console is a SQL editor tied to one data source, with the results of its latest run underneath.

Opening a console

  • Right-click a data source in the explorer and choose New query console, or use the New query console card on the Welcome tab.
  • Right-click a table, view or materialized view and choose Open data: this opens a console with SELECT * FROM that object and runs it straight away.
  • Right-click a data source and choose Open saved console to reopen one you closed (see below).

Consoles open as tabs next to the Welcome tab, named after their data source. A second console on the same data source is named shop (2), and so on. If the data source has a colour, the tab and a band along the top of the console carry it.

Each console has its own connection, separate from the explorer’s and from other consoles’, so a long query in one tab never blocks another. It connects the first time you run something (“Connecting…” in the toolbar), and closing the console’s tab closes the connection.

Consoles are saved

Every console is a file, consoles/<data source id>/console.sql (then console_2.sql, and so on; see Where dbdesk keeps its files). dbdesk writes your text a second after you stop typing, and when you quit. Open consoles reopen with their text the next time you start dbdesk.

Closing a console keeps its file if you typed anything in it, and deletes it otherwise (an empty console, or one opened by Open data and left as it was). Kept consoles are listed under Open saved console in their data source’s context menu. If a console can’t be saved, its toolbar says Not saved, with the reason on hover.

A console can show a .sql file of your own instead, saved only when you ask; see Files, parameters and find.

The editor

The editor highlights SQL keywords, strings, numbers, comments and quoted names, numbers the lines, and gives the statement that Run would run a faint background.

Completion. Typing a name, or a ., opens a list of suggestions at the cursor; ⌃Space opens it anywhere. Use ↑ and ↓ to pick, Enter or Tab to accept, Esc to close.

It suggests what fits where the cursor is:

  • after alias. or table., that table’s columns; after schema., that schema’s tables;
  • after FROM, JOIN, UPDATE or INTO, tables and views;
  • after SELECT, WHERE, ON, GROUP BY, ORDER BY or SET, the columns of the tables the statement uses, their aliases, and functions;
  • inside INSERT INTO orders (…), the columns of orders.

The statement’s own names count too: aliases, WITH queries (CTEs) and subqueries in FROM, with the columns they return. Functions come with their parentheses, the cursor between them. Each suggestion shows an icon for what it is and, for a column, its type.

You don’t have to type the start of a name: cid finds customer_id, matching the first letters of its words. Names that start with what you typed come first, and the ones you picked lately come before the rest.

Names come from what the explorer has loaded for the data source. When a suggestion needs a part that isn’t loaded yet, dbdesk loads it in the background (the list says “Loading the catalog…”), which connects the explorer if it wasn’t.

Formatting. ⌥⌘L (or Format in the toolbar, or Code → Format) lays out the selected text, or the statement at the cursor; ⇧⌥⌘L (Code → Format all) formats every statement in the console:

SELECT
    id,
    name
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.active
    AND o.total > 10
ORDER BY name

Each clause starts a line, lists of several items put one per line, and every AND or OR of a WHERE starts a line. Subqueries are indented inside their parentheses. Statements that aren’t queries, such as CREATE FUNCTION, keep their line breaks; a CREATE TABLE gets one column per line.

Formatting only changes spaces, line breaks and the case of keywords. Strings, comments, quoted names and function bodies between $$ are never touched, and dbdesk checks the result before using it: if it would read differently to the database, the text stays as it was and the toolbar says Not formatted. The cursor stays where it was in the SQL, and ⌘Z undoes the whole format at once.

Problems while typing. A moment after you stop typing, dbdesk underlines mistakes: a red wave for errors (SQL the database can’t read), an amber one for warnings (SQL that runs but probably doesn’t do what you meant). Hover over one to read it. The toolbar counts them; click the count, or press F8 (⇧F8 for the previous one), to move the cursor to the next.

Warnings point out:

  • = NULL or <> NULL, which are never true: write IS NULL or IS NOT NULL;
  • UPDATE or DELETE without WHERE, which change every row;
  • INSERT … SELECT *, which depends on the column order of the source;
  • a table, or alias.column, the explorer has loaded and doesn’t have. These only appear for parts of the catalog the explorer has already loaded, so expand the data source to get them.

Only queries and data changes (SELECT, INSERT, UPDATE, DELETE, WITH, VALUES) are checked for syntax: dbdesk would rather miss an error in a CREATE FUNCTION than flag one that isn’t there. Once a console has run something, its own connection also asks the database whether it would accept each statement, without running it, so the database’s own messages show up too (“The database says: …”). This never happens while results are still loading or a transaction is open.

View → Settings… switches each kind of warning off, and asking the database too.

View → Settings… chooses how keywords are written (UPPER, lower, or as you typed them) and how many spaces each level is indented. They’re saved in settings.toml (see Where dbdesk keeps its files).

Running SQL

The toolbar’s buttons, and their shortcuts while the editor has focus:

ButtonShortcutWhat it does
Run (▷)⌘↵Runs the selected text if there is a selection, otherwise the statement at the cursor.
Run all (▷▷)⇧⌘↵Runs every statement in the console, in order, stopping at the first one that fails.
Explain⌥⌘EShows how the database would run the statement at the cursor: EXPLAIN on PostgreSQL and MariaDB, EXPLAIN QUERY PLAN on SQLite, EXPLAIN FORMAT=TREE on MySQL 8 once the console has connected, on SQL Server the estimated plan as XML (SET SHOWPLAN_XML), which View value shows indented, EXPLAIN on DuckDB, whose plan is drawn as a tree in text, and EXPLAIN on ClickHouse, one step of the plan per row.
Cancel (□)⌘.Stops the running statement, or discards rows not yet loaded.
History—Opens the data source’s query history.
Format⌥⌘LFormats the selected text, or the statement at the cursor (see The editor).

On Linux and Windows, use Ctrl instead of ⌘. Hover over a button to see its current shortcut; all of them can be changed (see Keyboard shortcuts).

Which statement is “at the cursor”? dbdesk splits the text into statements at semicolons, the way the database would: semicolons inside strings, quoted names, comments, PostgreSQL’s dollar-quoted function bodies, and BEGIN … END or CASE … END blocks don’t end a statement. The statement at the cursor is the one the cursor is in, or the one just before it, so you can press ⌘↵ with the cursor right after a semicolon.

On MySQL and MariaDB, scripts can change what ends a statement with DELIMITER, as the mysql client does, so a procedure’s body can hold semicolons:

DELIMITER //
CREATE PROCEDURE touch(n INT)
BEGIN
  UPDATE items SET price = price + 1 WHERE id = n;
END//
DELIMITER ;

The DELIMITER lines are dbdesk’s to read and are never sent. MySQL’s own quoting applies too: "text" is a string, as it is without ANSI_QUOTES, names are quoted with backticks, and a backslash inside a string escapes the next character.

On SQL Server, GO alone on a line ends a batch, as in SQL Server Management Studio and sqlcmd, and is never sent; a count after it (GO 3) is ignored, and the batch runs once. Semicolons still split statements, except inside BEGIN … END, BEGIN TRY … END CATCH and the body of a procedure, function or trigger, which SQL Server reads up to the next GO:

CREATE OR ALTER PROCEDURE dbo.touch @n int AS
SET NOCOUNT ON;
UPDATE dbo.items SET price = price + 1 WHERE id = @n;
GO
EXEC dbo.touch @n = 1;

Each statement is sent as a batch of its own, so a variable lives only as long as the statement that declares it, even when both are selected and run together: DECLARE @n int = 1; SELECT @n is two batches, and the second doesn’t know @n. Leave out the semicolon (DECLARE @n int = 1 SELECT @n), or put the statements inside BEGIN … END, to send them as one. Names are quoted with brackets ([order]), and the values dbdesk writes, such as a grid edit’s, are N'…' strings so that any character survives. A statement that returns several result sets shows the first.

On DuckDB, a console can query files without loading them first, by name or through DuckDB’s table functions:

SELECT customer, sum(total) FROM 'orders/*.parquet' GROUP BY ALL;
SELECT * FROM read_csv('people.csv', header = true) WHERE age > 30;

A relative path is resolved from the folder dbdesk was started in, which for an app started from the Dock or the Start menu is not your home folder, so give files their full path. Statements that only report how they went show no result grid: CREATE TABLE, SET and ATTACH just finish, and an INSERT, UPDATE, DELETE or COPY says how many rows it wrote.

On ClickHouse, a console is a session on the server: USE, SET and temporary tables last from one run to the next, until the console disconnects or has been idle for an hour, when the server ends the session. Names are case-sensitive (toDate, not TODATE), so Format leaves the case of every word as you typed it and completion keeps ClickHouse’s spelling of functions. # starts a comment, as -- does. Results can’t be edited: see Editing data.

The toolbar shows how the last run went: Done in 12 ms, Failed or Cancelled. While a statement runs, Run, Run all and Explain are disabled.

Transactions

Below the toolbar, each console has a transaction mode:

  • Auto-commit (the default): every statement is committed as soon as it succeeds, unless you start a transaction yourself with BEGIN.
  • Manual commit: dbdesk starts a transaction before your first statement, and changes stay pending until you click Commit or Rollback.

ClickHouse consoles have no transaction mode: ClickHouse’s transactions are experimental and off unless the server is set up for them, so every statement takes effect as it runs.

While a transaction is open, the console says ● Transaction open, in either mode, and you can’t switch modes. On PostgreSQL, a statement that fails inside a transaction leaves it ● Transaction failed: the database then only accepts a rollback.

Closing a console, disconnecting its data source or quitting dbdesk with a transaction open asks whether to roll it back first. Closing the connection is what rolls it back.

When the connection is lost

When a console’s connection is lost (the server restarted, an administrator ended the session, the network or the SSH tunnel dropped), the console says so in its toolbar: Disconnected: its transaction, temporary tables and session settings are gone, with the reason on hover. The statement that found out fails with the server’s error. dbdesk also checks a connection that has sat unused for a minute before running anything on it, so a statement isn’t sent down one that is gone.

A lost console doesn’t reconnect by itself: a script that relies on its open transaction, a temporary table or a SET would otherwise run on a new session without them. Until you click Reconnect, running, committing or applying grid edits says the connection was lost. Reconnect starts a new session on the same data source.

Results

Under the editor, a summary line describes the result, followed by the rows:

  • 8 rows · 3 ms for a query,
  • 12 rows affected · 4 ms for INSERT, UPDATE, DELETE, MERGE, REPLACE and COPY,
  • Done · 2 ms for anything else, such as CREATE TABLE,
  • or the error message, in red.

Drag the line between the editor and the results to give either more room.

In the grid, numbers are right-aligned and NULL is shown in grey italics, so it can’t be mistaken for the text “NULL”. Drag the edges of a column header to resize it. Hovering over a column header shows the column’s type when the database reports it (SQLite does; PostgreSQL doesn’t, yet).

  • Selecting and copying. Click a cell to select it; shift-click or drag to select a block. ⌘C (while no text field has focus) copies the selection, tab-separated, ready to paste into a spreadsheet. Right-click a cell for Copy selection, Copy value, Copy row and View value.
  • Viewing a long value. Double-click a cell, or select it and press Space, to see the whole value in a window. JSON and XML are shown indented; Copy copies it as stored.
  • Sorting and filtering. Click a column header to sort by it (ascending, descending, off), and type in Filter loaded rows to keep the rows with a cell containing the text. Both work on the rows loaded so far, without asking the database again, and the grid says so. Tick In the database to sort and filter every row instead; see Sorting and filtering in the database.
  • Exporting. Export → Loaded rows as CSV… or as JSON… saves the rows as shown, sorted and filtered, to a file you pick. All rows as CSV… and as JSON… run the query again and write every row; see Exporting every row.

Large results

Rows arrive 500 at a time. When there are more, the summary says so, and you get the next 500 by clicking Load more rows or by scrolling to the bottom of the grid.

A query that is still sending rows keeps its connection busy. Cancel discards the rows that haven’t been loaded (the summary then says rest discarded), and so does running something else in the same console.

Several statements

When a run includes more than one statement, numbered tabs above the summary pick which statement’s result to show. dbdesk shows the latest one that returned rows, or the one that failed; failed statements’ tabs are red. Hover over a tab to see its statement.

Editing data

You can change values in the grid when the result is a SELECT from a single table (no joins, GROUP BY or DISTINCT), the table has a primary key, and the result includes the key’s columns. Otherwise the grid says Read-only; hover over it to see why. ClickHouse results are always read-only: a ClickHouse table’s primary key is its sort order, which several rows may share, so nothing tells one row from another.

  • Double-click a cell, or select it and press F2 or Enter, to edit it. Enter or a click elsewhere keeps the new text; Esc drops it. Set NULL in the cell’s menu sets NULL (emptying a cell gives an empty string, not NULL).
  • Changed cells are tinted. Submit writes them, one UPDATE per changed row, all or none; Revert forgets them. You can also add and delete rows, and follow foreign keys: see Working with table data.
  • If a row changed or disappeared since it was loaded, or a value doesn’t fit its column, nothing is written, your changes stay, and the error names the row.
  • In manual commit mode, submitted changes join the open transaction and stay pending until you commit. DuckDB has no savepoints to undo a failed edit with, so on DuckDB, changes can’t be submitted while a transaction is open: commit or roll back first.
  • On a careful data source, Submit asks first, naming the data source and what will change. On a read-only one, the grid says Read-only.

Values are sent as text and converted by the database, so type them the way the grid shows them (t for a PostgreSQL boolean, {1,2} for an array).

History

Every statement a console runs is recorded with its time, duration, row count and outcome. History in the toolbar lists the data source’s statements, newest first; type to search them, and click Insert (or double-click) to put one into the console on a line of its own. The query history covers the rest: statements from the grid, the explorer and AI assistants, a grid edit’s old values, searching every data source, exporting, and how long statements are kept.

Errors

When a statement fails, its error appears in red in the summary line and in the log console. If the database says where in the statement the error is, the editor underlines that spot in red until you edit the text. PostgreSQL reports a position for most errors; SQLite only for syntax errors.

With Run all, the statements before the failing one have already run (and, in auto-commit mode, are committed); the ones after it don’t run at all.

Statements that change every row

Before running an UPDATE or a DELETE without a WHERE, dbdesk asks first:

Nothing in the run starts until you answer: Run anyway runs everything as asked, Cancel runs nothing. The check looks at how each statement starts, so a DELETE inside a WITH query isn’t caught.

On MySQL and MariaDB, the same dialog warns before a statement that commits by itself (CREATE, ALTER, DROP, RENAME, TRUNCATE, GRANT, REVOKE) when you might be counting on Rollback: in Manual commit mode, or with a transaction open. MySQL commits such a statement at once, together with whatever the transaction held, so Rollback can’t undo either. Temporary tables are the exception, and don’t get the warning.

Careful and read-only data sources

The toolbar shows a data source’s safety level unless it is Normal; hover over it for what it means.

On a careful data source, every statement that changes data or the schema asks first, naming the data source, in the same dialog. Reading, COMMIT and ROLLBACK don’t ask. New consoles on it start in Manual commit mode.

On a read-only data source, a statement that would change anything isn’t sent at all, and the result says why. A write the check can’t see, inside a function, is refused by the database, with its own error.

Files, parameters and find

A console doesn’t have to live in dbdesk’s own folder: it can show a .sql file from a project, which you save when you choose. Statements can take :name parameters, which dbdesk asks for before they run. And the editor finds and replaces text.

Opening a .sql file

  • File › Open… (⌘O) asks for a file and opens it in a new console.
  • File › Open recent lists the last ten files; Clear recent files empties the list.
  • Drop a file on the window to open it.
  • From a terminal, dbdesk report.sql starts dbdesk with the file open. Several files can be named.

A console runs on a data source, so the file opens on the one it was opened on last time. The first time, if there is more than one data source, a small dialog asks which, starting from the data source of the console you were in. A file that is already open isn’t opened twice: its tab comes to the front.

The console’s tab is titled with the file’s name, and hovering over the tab shows its path. The file must be UTF-8 text; dbdesk refuses a file in another encoding rather than garbling it, and says why.

Saving

A console showing a file is saved only when you ask:

  • File › Save (⌘S) writes it. While the console has changes the file doesn’t, its tab title ends with •. Undoing back to what the file holds counts as saved.
  • File › Save as… (⇧⌘S) writes it to another file, which the console shows from then on.

Closing the tab of a file with unsaved changes asks Save the changes to report.sql?: Save, Don’t save, or Cancel (Escape). Quitting asks the same about every file with unsaved changes. If a save fails, the tab stays open, and its toolbar says Not saved, with the reason on hover.

Save or Save as… on a console from dbdesk’s own folder (see Consoles are saved) asks where to save it, and the console moves there: it stops saving itself in dbdesk’s folder, and its copy there is removed once the file is written.

Tabs showing files reopen the next time you start dbdesk, with the file as it is then.

When the file changes elsewhere

dbdesk looks at open files every two seconds, and as soon as its window comes back to the front, so an edit made in another editor, or by git checkout, shows up:

  • No unsaved changes in the console: it shows the file’s new text. One undo goes back.
  • Unsaved changes: the console keeps your text and says report.sql changed on disk, and you have unsaved changes. Reload shows the file’s text instead; Keep mine keeps yours, and the next save replaces the file.
  • The file was deleted: the console says so, and Save writes it again.

Parameters

A statement can name values it needs with a colon, :since:

SELECT * FROM orders
WHERE placed_at >= :since
  AND status = :status;

Running it opens Parameter values, with a field for each parameter in the order they appear (a parameter used twice is asked once). Type the values and press Enter or Run; Escape or Cancel runs nothing. Tick NULL for a NULL; an empty field is an empty string.

The dialog starts from the values the console used last, so running a statement again is Enter. Running a shown statement again without typing it, as when the grid reloads rows after an insert, sorts and filters in the database, or exports every row, reuses the values without asking.

What counts as a parameter: : followed at once by a name (letters, digits and _, not starting with a digit), outside strings, comments and quoted names. These aren’t parameters:

  • casts, x::int, and := assignments;
  • a : written right after a name, a number or a closing bracket, as in a label (retry:) or DuckDB’s lambda x: x + 1 (after a keyword it is one: BETWEEN:lo works);
  • anything inside […] or {…}: array slices such as a[1:n], DuckDB’s struct and map literals, and ClickHouse’s own {name:Type} parameters.

How values reach the database

Each value is sent as text, which the database converts as it would a string: :id with 2 compares with a number column, :since with 2026-01-01 with a date.

  • SQLite, DuckDB, MySQL and MariaDB, SQL Server: the values are bound parameters, never part of the SQL. On SQL Server the statement runs through sp_executesql, so a temporary table it creates, or a USE or SET it runs, ends with it.
  • PostgreSQL and ClickHouse: each value is written into the statement as a quoted string, '2026-01-01', which the database reads from its context just as it would a bound parameter. (dbdesk’s PostgreSQL consoles use the protocol that shows every value as psql does, and that protocol has no parameters.)
  • Explain always writes the values into the statement.

A read-only data source checks the statement with its values in place, so it refuses exactly what it would refuse typed out. An error’s position points at the statement as you typed it.

The history records the statement as typed, followed by a comment with its values:

SELECT * FROM orders WHERE placed_at >= :since
-- :since = '2026-01-01'

Find and replace

ShortcutAction
⌘FFind; the selected text, if it is on one line, is searched for
⌥⌘FFind and replace
⌘G, F3, or Enter in the find fieldGo to the next match
⇧⌘G, ⇧F3, or ⇧Enter in the find fieldGo to the previous match
EscapeClose the find bar, back to the editor

The bar searches the console’s text as you type, highlights every match and the selected one more strongly, and says 3 of 12, No results, or why a regular expression is invalid.

  • Aa matches case; without it, orders finds ORDERS too.
  • W finds whole words only: id doesn’t find the id in order_id.
  • .* reads the text as a regular expression, in Rust’s syntax. In the replacement, $1 (or ${name}) stands for what a group matched.

Replace (or Enter in the replace field) replaces the selected match and goes to the next; Replace all replaces every match and says how many. Each is one step for ⌘Z to undo.

All these shortcuts can be changed in keymap.toml; see Keyboard shortcuts.

Working with table data

The results grid is more than a view of a query. When the rows come from one table, you can add, change and delete them, follow foreign keys from one table’s rows to another’s, sort and filter every row in the database rather than only those loaded, export every row, and import a CSV file into the table.

Editing needs a result whose rows stand for a table’s rows: a SELECT from a single table (no joins, GROUP BY or DISTINCT) that includes the table’s primary key. Open data on a table in the explorer gives you one. Editing data explains when a result is Read-only, and why.

Adding, changing and deleting rows

  • Change a value by double-clicking its cell (or selecting it and pressing F2 or Enter). Changed cells are tinted.
  • Add a row with Add row in the grid’s toolbar. The new row appears under the loaded ones, marked +. Each of its cells says what the database will put there if you leave it alone: DEFAULT (hover to see the default) or NULL. Type into the cells you want to set.
  • Delete rows by right-clicking a row’s cell and choosing Delete row, or by selecting cells in several rows and clicking Delete rows. Rows marked for deletion are struck through; Restore row in the same menu takes the mark away. A row you added and no longer want goes with Remove row.

The toolbar counts what is waiting, for example 1 changed cell · 1 new row · 1 deleted row. Nothing reaches the database until you click Submit; Revert forgets it all.

Submit writes everything at once, all or none: the deletions first, then the changes, then the new rows, so a new row can take the key of one you deleted. If anything fails (a value doesn’t fit its column, a row changed since it was loaded, a key is already taken), nothing is written, your changes stay as they were, and the message names the row: Row 12 for a loaded row, New row 2 for one you added.

After a submit, deleted rows leave the grid and changed cells keep their values. If you added rows, the query runs again, so they show what the database filled in for them, such as a generated id.

On a careful data source, Submit first asks, naming the data source and what will happen: this inserts 1 row, updates 2 edited rows and deletes 1 row. A read-only data source’s rows can’t be changed. Every statement Submit sends is in the console’s History, with the values it replaced (see The query history).

Following foreign keys

A value in a foreign key column is underlined: it refers to a row of another table.

  • Open the row it refers to with ⌘-click (Ctrl-click on Windows and Linux), or with Open customers row in the cell’s menu. A new console opens with a query for that row, and runs it.
  • Find the rows that refer to this one with Referencing rows in a row’s menu. It lists the tables whose foreign keys point at this table, as table.column; pick one to open its rows that point at this row, again in a new console.

Each opens in a new tab, so the tab you came from is the way back. Following keys only reads, so it works on read-only data sources and on tables without a primary key too. A key whose value is NULL refers to nothing, so it isn’t underlined; and a key is only followed when the result includes all of its columns. An entry under Referencing rows is disabled, saying which columns to add to the query, when the result lacks the ones the other table refers to.

Sorting and filtering in the database

Clicking a column header, or typing in the filter, normally sorts and filters only the rows already loaded, and the grid says so. To sort and filter every row, tick In the database next to the filter. Then:

  • clicking a column header sorts by it (ascending, descending, off), and
  • pressing Enter in Filter every row keeps the rows where any value contains the text, ignoring case;

each by running the query again, wrapped in one that sorts and filters it:

SELECT * FROM (
SELECT * FROM orders
) AS dbdesk_rows
WHERE … any value contains the text …
ORDER BY 3 DESC

The grid shows Sorted and filtered by the database, rows page in 500 at a time as usual, and the rows can still be edited. Untick In the database to run the query as you wrote it again.

Only queries that start with SELECT, WITH or VALUES can be wrapped, and only when no two columns have the same name (SQL Server can’t wrap a WITH either). Otherwise the switch is disabled; hover over it to see why. It is also disabled while changes are waiting to be submitted, since running the query again would lose them. The database sorts by its own rules, which may differ from the grid’s: where NULLs go, for example.

Exporting every row

Export in the grid’s toolbar offers:

  • Loaded rows as CSV… and as JSON…: the rows loaded so far, in the order and with the filter the grid shows them.
  • All rows as CSV… and as JSON…: every row the query returns. The query runs again, sorted and filtered if the database is doing that, and its rows are written to the file as they arrive, so even a very large result never has to fit in memory.

While every row is being exported, the toolbar counts them (Exporting… 125000 rows), and Cancel export stops it. A cancelled or failed export leaves no file behind, and never replaces a file that was there: the rows go to a file named like the one you chose, plus .partial, which takes its name only when complete.

Running the query again needs the console’s connection, so a result that was still loading more rows stops loading (its summary says rest discarded); the rows already in the grid stay. The export runs in the console’s open transaction, if there is one, and sees its temporary tables. CSV files have a header line and leave NULL empty; JSON files are a list of objects, with values as text.

Importing a CSV file

Right-click a table in the explorer and choose Import CSV…, then pick the file.

  1. The file. Say whether its First line is a header (on by default) and which Separator it uses: comma, semicolon or tab. Choose file… picks another.
  2. Columns. Each of the table’s columns takes its values from a column of the file, or (default) to leave it to the database. dbdesk matches them by name when the file has a header (ignoring case), or by position when it doesn’t; change any of them in its menu.
  3. The first rows of the file show as they would go into the table, with NULL for empty values and DEFAULT for columns left to their default.
  4. Warnings point at values in those first rows that probably won’t fit: text in a number column, a date that isn’t YYYY-MM-DD, an empty value for a column that can’t be NULL. They are only warnings: the database decides.

Import then reads the whole file and inserts it in batches of 500 rows, all in one transaction. The dialog counts the rows as they go in. If the database refuses a row, the whole import is rolled back, nothing is imported, and the dialog names the line of the file: Line 703: duplicate key value violates unique constraint. On DuckDB it names the lines of the batch that failed instead. Empty values are imported as NULL; everything else is sent as text for the database to convert to the column’s type, as when you type into a cell.

A careful data source asks before importing. A read-only one can’t be imported into, and nor can a ClickHouse table, since ClickHouse has no transaction to undo a failed import with; the dialog says so.

The query history

dbdesk keeps every statement it sends to a database: what you run in a console, what the grid submits, what the explorer’s helpers run, the statement behind an export, a CSV import, and what an AI assistant runs through dbdesk mcp. Each is kept with its data source, time, duration, row count and outcome, and with where it came from. Only the statements are kept, never the rows they returned.

Open it with History in a console’s toolbar. It lists the console’s data source’s statements, newest first.

Finding a statement

The row above the list narrows it down:

  • This data source / Every data source: every data source’s statements at once, each named beside its statement. A data source you have since removed shows as Removed data source.
  • All sources or one kind: Consoles, Grid edits, Object helpers, Imports, Exports, MCP clients, or Earlier dbdesk for statements recorded before dbdesk kept where they came from.
  • Any time, or the last hour, 24 hours, 7 days or 30 days.
  • Search statements: any part of the statement, ignoring case.

The list shows the newest 500 matches; narrow the search to reach older ones. Each statement has a tag saying where it came from. MCP stands out, since nobody at the keyboard ran those; hover it for the assistant’s name.

Insert (or a double-click) puts a statement into the console, on a line of its own.

What a statement did

Click a statement to see it whole below the list, with where it came from: which assistant, which file an import read, which table the grid changed.

For a change made in the grid, the details also show the row’s key and each value Before and After; for a deleted row, every value it had. To put it back is the statement that would restore the row: an UPDATE with the old values, or an INSERT of the deleted row. Copy it, or Insert into console to read it over and run it yourself. Nothing is undone for you, and the row may have changed again since. A row added in the grid has none: the database chose its key.

A CSV import is one entry, not one per row: the INSERT with the table’s columns, how many rows went in, and the file.

Passwords stay out

Passwords never reach the history file, whoever sends the statement. In CREATE ROLE … PASSWORD '…', CREATE LOGIN … WITH PASSWORD = '…', CREATE USER … IDENTIFIED BY '…', DuckDB’s CREATE SECRET, foreign servers’ user mappings, and connection strings (password=…, Pwd=…, postgresql://user:password@…), the secret is stored as '<redacted>', and the log shows it the same way. A statement put back from the history then needs its password typed in again.

dbdesk errs on the side of hiding: after the word PASSWORD it hides every quoted value to the end of that clause, so VALID UNTIL '2027-01-01' after a password shows as '<redacted>' too.

Exporting

Export history › As CSV… or As JSON… writes everything the current search finds, not just the 500 listed, with one record per statement: executed_at (UTC), data_source, data_source_id, source (console, grid, object, import, export, mcp, or app for earlier dbdesk), detail (the assistant or the file), outcome, duration_ms, rows, sql, and change, a grid edit’s values as JSON.

How long it is kept

By default the history keeps everything. View › Settings… › Query history limits it:

  • Keep statements for 30 days, 90 days, a year or two years;
  • Keep per data source the newest 10,000, 100,000 or 1,000,000 statements.

Older statements are deleted when dbdesk starts and as soon as you change the setting, a little at a time so a dbdesk mcp writing the same file never waits. Other values can be set by hand in settings.toml:

[history]
max_age_days = 180
max_per_data_source = 25000

Not a tamper-proof audit

The history is a file in your settings folder (see Where dbdesk keeps its files): anyone who can open it can change or delete it, and statements sent by other tools never reach it. When you need an audit you can rely on, set it up on the server: pgaudit on PostgreSQL, SQL Server Audit, or the audit plugins of MySQL and MariaDB.

Keyboard shortcuts

On Linux and Windows, read Ctrl wherever this page says ⌘.

Defaults

ShortcutActionWorks while
⌘↵Run the selection, or the statement at the cursorthe console’s editor has focus
⇧⌘↵Run every statement in the consolethe editor has focus
⌥⌘EExplain the statement at the cursorthe editor has focus
⌘.Cancel the running statement, or discard rows not yet loadedthe editor has focus
⌃SpaceSuggest names and keywords at the cursorthe editor has focus
⌥⌘LFormat the selection, or the statement at the cursorthe editor has focus
⇧⌥⌘LFormat every statement in the consolethe editor has focus
F8Go to the next error or warningthe editor has focus
⇧F8Go to the previous error or warningthe editor has focus
⌘FFind in the consolethe editor or the find bar has focus
⌥⌘FFind and replace in the consolethe editor or the find bar has focus
⌘G, F3Go to the next matchthe editor or the find bar has focus
⇧⌘G, ⇧F3Go to the previous matchthe editor or the find bar has focus
⌘,Open the settingsalways
⌘OOpen a .sql file in a new consolealways
⌘SSave the console to its .sql filea console’s tab is selected
⇧⌘SSave the console to a new .sql filea console’s tab is selected
SpaceShow the selected cell’s whole valueno text field has focus
F2, EnterEdit the selected cellno text field has focus
⌘CCopy the selected cellsno text field has focus

Console shortcuts are taken before the editor sees them, so ⌘↵ runs a statement instead of inserting a new line. History, Commit and Rollback have no shortcut by default.

On a Mac the menu bar adds the keys every Mac app has: ⌘Q quits, ⌘H hides dbdesk, ⌥⌘H hides the other apps, ⌘M minimizes the window and ⌃⌘F enters full screen. They aren’t in keymap.toml.

Changing them

Shortcuts live in keymap.toml, next to data_sources.toml (see Where dbdesk keeps its files). View → Keymap… lists every action with its id, its shortcuts and what it does, and has two buttons:

  • Create keymap.toml writes a file listing every action with its default, commented out. It never replaces a file that exists.
  • Reload applies your edits without restarting dbdesk.

In the file, list only the actions you want to change:

[bindings]
"console.run" = "cmd+r"                    # one shortcut
"console.explain" = ["cmd+alt+e", "f5"]    # or several
"console.cancel" = []                      # or none

A shortcut is modifiers and a key joined by +, ignoring case. cmd is ⌘ on macOS and Ctrl elsewhere; ctrl is the Control key everywhere; alt (or option) and shift are what they say. Keys are letters, digits, enter, space, tab, escape, f1 to f35, period, comma, pagedown and so on, or the character itself (cmd+.).

Your own actions from actions.toml are bound the same way, as custom.<id>; see Actions, snippets and themes.

A mistake doesn’t cost you the rest of the file: an unknown action, a shortcut that can’t be read, or one that two actions share, is skipped and listed as a problem in the Keymap window, and the status bar shows keymap.toml: N problems. A file that isn’t valid TOML keeps every default and shows why.

Editing text

The SQL editor and the other text fields use the usual editing keys:

ShortcutAction
⌘ASelect all
⌘C, ⌘X, ⌘VCopy, cut, paste
⌘Z, ⇧⌘ZUndo, redo
⇧ with arrow keysExtend the selection
⌥← / ⌥→ (Ctrl on Linux and Windows)Move by word

Dialogs

Esc closes the data source dialog, the delete confirmation and the “Run anyway?” check without doing anything.

Actions, snippets and themes

Three files let you add to dbdesk without rebuilding it: actions.toml adds your own SQL, or your own programs, to the explorer’s and the grid’s menus, snippets.toml adds snippets to completion, and theme.toml changes the colours. They sit next to keymap.toml (see Where dbdesk keeps its files), and none of them exists until you write it.

They are read when dbdesk starts. After editing one, click Reload in View → Actions…, or in the window the status bar opens for the file: that reads all three again.

A mistake costs only the entry it is in: an action, snippet or colour that can’t be used is skipped, and the status bar shows actions.toml: N problems (or snippets.toml, theme.toml). Click it to see each problem. A file that isn’t valid TOML adds nothing, and says on which line it went wrong.

Actions

An action is SQL you attach to what you right-click:

[[action]]
id = "row-count"
name = "Row count"
applies_to = ["table", "view"]
drivers = ["postgres", "mysql"]
sql = "SELECT count(*) FROM {qualified_name}"

[[action]]
id = "latest"
name = "Last 100 rows"
applies_to = ["table"]
sql = "SELECT * FROM {qualified_name} ORDER BY {column:created_at} DESC LIMIT 100"
run = false
  • id names the action for good: lowercase letters, digits, - and _. It is how keymap.toml binds it.
  • name is what the menus show.
  • applies_to says where the action appears:
    • explorer nodes by kind: database, schema, table, view, materialized_view, function, procedure, sequence, event, synonym, dictionary, column, index, constraint, trigger, role;
    • data_source, a data source’s own row;
    • grid.selection, the grid’s selected cells, acted on once;
    • grid.row, the grid’s selected rows, acted on once each.
  • drivers limits it to some databases: postgres, sqlite, mysql, sqlserver, duckdb, clickhouse. Leave it out for every one.
  • run = false opens the SQL in a new console for you to edit and run. Otherwise it runs at once.

Choosing an action opens a new console on the same data source with the SQL filled in. Running it is the same as running SQL you typed: :name parameters are asked for, a DELETE without WHERE asks first, a careful data source asks before any change, and a read-only one refuses them. The history marks the statements Action, with the action’s id.

Placeholders

PlaceholderBecomes
{qualified_name}The object’s name, with its schema: "public"."orders"
{database}, {schema}, {table}The database, schema or table the object is in, or is
{column}The column right-clicked, or the first selected column in the grid
{column:created_at}The same, or created_at where there is no column
{data_source}The data source’s name in dbdesk, as a string: 'shop'
{value}The first selected cell, as a literal: '42', or NULL
{values}Every selected cell, as literals separated by commas, for IN (…)
{row.email}The email column of the first selected row (of each row, with grid.row)

A placeholder is never pasted in as text. Names are quoted as the database quotes them ("…", `…` or […]), and values become string literals, so a table called x; DROP TABLE y stays one name. Don’t put quotes around a placeholder yourself.

A placeholder the action could never fill is a problem with the file: {value} on a table, or {column} on a table without a default. One that can’t be filled this time, such as {table} on grid cells that don’t come from one table, is said in the status bar when you choose the action. A { that doesn’t start a placeholder, as in a JSON literal, stays as written; write {{ for one that would.

Keys and the Actions menu

Every action is in the Actions menu, and can have a shortcut in keymap.toml as custom.<id>:

[bindings]
"custom.row-count" = "cmd+shift+c"

From the menu or a key, an action acts on the focused console’s selected cells, or else on the explorer’s selected node: click a node to select it. The item is greyed out when what is selected isn’t something the action applies to. A shortcut that one of dbdesk’s own actions already has stays dbdesk’s, and is listed as a problem in View → Keymap…, which also lists your actions’ ids.

View → Actions… lists every action with its id, what it applies to, its drivers, its keys and its SQL or command, and the file’s problems. Create actions.toml writes the examples above, commented out, to start from; it never replaces a file you have.

Commands

An action can run a program instead of SQL: write command where an SQL action has sql.

[[action]]
id = "rows-to-jq"
name = "Send rows to jq"
applies_to = ["grid.selection"]
command = ["jq", "-r", ".[] | .email"]
stdin = "json"
output = "viewer"

[[action]]
id = "pgcli"
name = "Open in pgcli"
applies_to = ["data_source"]
drivers = ["postgres"]
command = ["gnome-terminal", "--", "pgcli", "postgres://{user}@{host}:{port}/{database}"]
output = "none"

[[action]]
id = "dump-table"
name = "Dump to SQL"
applies_to = ["table"]
drivers = ["postgres"]
command = ["pg_dump", "--table={qualified_name}", "--host={host}", "--port={port}", "--username={user}", "{database}"]
output = "console"
  • command is the program, then its arguments, as a list. There is no shell: each item reaches the program as one argument, exactly as written, with its placeholders filled in. A table name with spaces, quotes and semicolons stays one argument. To use a shell anyway, say so: ["sh", "-c", "…"]. The program itself can’t have placeholders, so what runs never depends on what you selected.
  • stdin sends the grid’s selected cells to the program: the selected columns of the selected rows, as json (an array of objects, as Export writes it), csv or tsv, each with the column names. Only the grid has a selection, so an action with stdin applies only to grid.selection. Leave it out, or write none, and stdin is empty.
  • output says where what the program writes goes:
    • viewer, the default: a window like the cell viewer’s, with JSON and XML laid out for reading, and Copy;
    • console: a new console on the same data source, for a program that writes SQL, to read and run yourself;
    • log: the log console, a line an entry, with what it wrote to stderr as warnings;
    • none: nowhere, for a program that opens a window of its own.
  • timeout is how long it may run before dbdesk stops it, as "500ms", "30s", "5m" or "1h": a minute when left out.
  • run is for SQL only, and a command can’t apply to grid.row: it already gets every selected row at once.

The terminal in the pgcli example is GNOME’s, which runs what follows --. On a Mac, use a terminal that takes a command in the same way, such as ["alacritty", "-e", …] or ["kitty", …]: Terminal and iTerm can’t be given a command to run this way.

What a command gets

Its placeholders are the SQL ones, filled in as plain text, with nothing quoted, plus the data source’s own settings:

PlaceholderBecomes
{qualified_name}The name as the database’s SQL writes it: "public"."orders"
{database}, {schema}, {table}, {column}As for SQL, unquoted. {database} is the one connected to where the object has none
{data_source}The data source’s name in dbdesk: shop
{value}, {row.email}The cell’s text; nothing for NULL
{host}, {port}, {user}Where a server data source connects, with ${NAME} variables filled in
{file}A SQLite or DuckDB data source’s file

{values} is for SQL only: a command gets the selection on stdin. A placeholder nothing fills this time, such as {host} on a SQLite data source, opens a window saying so instead of running anything.

The program also gets dbdesk’s own environment, with DBDESK_DATA_SOURCE, DBDESK_DRIVER (postgres, sqlite, …), DBDESK_DATABASE, DBDESK_SCHEMA and DBDESK_TABLE where there is one, and DBDESK_FORMAT, the stdin format. It runs in your home folder.

It never gets a password. There is no placeholder or variable for one, and none of the data source’s secrets (its password, its SSH password or passphrase) reach a program in any way. A program that needs one reads it from its own configuration, as ~/.pgpass or ~/.my.cnf, or from the keychain itself.

The first run

Before a command runs for the first time, dbdesk shows it in full and asks Run a program?, with what it gets on stdin, where its output goes and its timeout. Run runs it, and from then on it runs without asking. Change anything in its command or stdin, even one argument, and it asks again. Cancel, or Esc, runs nothing.

View → Actions… says of each command whether it is trusted or asks before it next runs. dbdesk keeps what you trusted in trusted_commands.toml, in its window state folder rather than beside actions.toml (see Where dbdesk keeps its files). So an actions.toml you copy from someone, or sync from another machine, still asks before each of its commands first runs. Delete an entry, or the file, to be asked again.

Actions only ever come from your own actions.toml. A data source export never carries any, even one someone added to the file by hand.

While it runs

The status bar says Running Send rows to jq… with Stop. ⌘. (Ctrl+. on Linux and Windows) stops it too, unless the console you are in is running SQL: then ⌘. stops that instead. When its timeout passes, it is stopped the same way.

A program that ends with an error opens a window naming it, how it ended (exit status: 3) and what it wrote to stderr, whatever output says. So does one that can’t start or runs past its timeout. One that writes more than 4 MB is stopped there, and you get what it wrote first, with a note.

Stopping a program stops that program only. One that starts programs of its own, as sh -c "… &" does, may leave them running.

On a Mac and on Windows

  • A Mac app opened from the Finder doesn’t see your shell’s PATH. It gets /usr/bin:/bin:/usr/sbin:/sbin, without Homebrew’s folders, so jq may be “not found” even though your terminal finds it. The window says which PATH was searched. Write the full path instead: which jq in a terminal prints it, such as /opt/homebrew/bin/jq.
  • On Windows, a program is found on PATH with or without .exe, and runs without a console window. A .bat or .cmd file runs through cmd.exe, which reads some characters in its arguments as its own; dbdesk refuses to start it with an argument it can’t pass safely, rather than let cmd.exe read it.

Snippets

[[snippet]]
trigger = "sel"
description = "Select with a filter"
body = "SELECT ${1:*}\nFROM ${2:table}\nWHERE ${3:condition};"
drivers = ["postgres"]

While you type a word in the editor, completion lists the snippets whose trigger starts with it, marked with braces, after the names that start with it. Enter or Tab inserts the body with its first field selected: type over it, then Tab goes to the next field and ⇧Tab back. After the last field the cursor goes to $0, or to the end, and Tab is the editor’s again. Esc, or clicking or typing outside the field, leaves the fields.

The syntax is the one other editors use, so snippets can be copied from them: ${1:default} or $1 for a field, $0 for where the cursor ends, and \$, \} and \\ for the characters themselves. A $ that starts no field, as in PostgreSQL’s $$, stays as written; write \$1 for a positional parameter. Fields inside fields aren’t supported. trigger takes letters, digits and _; description and drivers are optional.

Themes

[light]
primary = "#5b3fe0"

[dark]
primary = "#9a8bff"
base = "#14121a"

[light] and [dark] replace colours of the light and dark themes, so View → Theme still offers Match system, Light and Dark, each with your colours. Colours are written #rrggbb:

NameWhat it colours
primarySelection, focus, and the main button on a screen
secondarySQL keywords, links
success, warning, errorWhat went right, what to watch, what went wrong
baseBehind the panels, and the menu and status bars
panelThe panels, windows and menus
fieldText fields, the editor and the grid
control, control_hoverButtons at rest and under the pointer
borderLines around panels and windows
text, text_weakText, and details, hints and comments
selectionBehind selected text and rows
on_primaryText on a primary button

A colour that leaves text too faint to read on what it is drawn on (below WCAG AA’s 4.5:1) still applies, and is listed as a problem, naming the text and the background.

On a Mac, selection and focus take your accent colour from System Settings, as they do without a theme, unless theme.toml sets that theme’s primary or selection.

Connecting an AI assistant

dbdesk can serve a project’s databases to an AI assistant or coding agent through the Model Context Protocol (MCP). The assistant can then list the project’s data sources, browse their tables, describe them and run queries, using the data sources, drivers and saved passwords dbdesk already has. Where you allow it, it can also change data or the schema.

By default an assistant can only read. Every read runs in the database’s own read-only mode and is rolled back, and anything that would change data or the schema is refused before it is sent. Each project can allow more; see Permissions.

Data leaves your machine. Whatever a query returns goes to the assistant, and from there to the company that runs its model. Only allow projects whose data you are happy to share that way. Keep sensitive databases in projects of their own, or limit a project to some of its schemas.

Setting up a client

1. Allow the project

Right-click the project’s header in the explorer and choose Project settings…. Tick Allow MCP clients to use this project and click Save. New projects start with this off, so nothing is ever exposed by accident.

The dialog also shows the project’s slug, which is what the client names it by.

2. Copy the configuration

Right-click the project’s header again and choose Copy MCP configuration.

dbdesk copies the entry an MCP client needs, with this machine’s path to dbdesk filled in. On macOS that is the program inside the app bundle, and on Linux it is the AppImage itself:

{
  "mcpServers": {
    "dbdesk-analytics": {
      "command": "/Applications/dbdesk.app/Contents/MacOS/dbdesk",
      "args": ["mcp", "--project", "analytics"]
    }
  }
}

3. Add it to the client

Paste the entry into the client’s MCP settings:

  • Claude Desktop: Settings › Developer › Edit Config opens claude_desktop_config.json. Add the dbdesk-… entry inside its mcpServers, then restart Claude Desktop.

  • Cursor: ~/.cursor/mcp.json, or .cursor/mcp.json in a project folder.

  • Claude Code: a project’s .mcp.json takes the same JSON. You can also add the server from the command line, with the command and arguments from the copied entry:

    claude mcp add dbdesk-analytics -- /Applications/dbdesk.app/Contents/MacOS/dbdesk mcp --project analytics
    

One entry serves one project. For several projects, add one entry per project.

The client starts dbdesk itself, as dbdesk mcp --project <slug>, and talks to it over standard input and output. The dbdesk window doesn’t need to be open.

On macOS, the first time the server reads a saved password, the system asks whether dbdesk may use it, as it does for the app; choose Always Allow. Until dbdesk’s builds are signed, it asks again after each update.

4. Try it

Ask the assistant to list the project’s data sources, then to describe a table and query it. Each statement it runs shows up in the data source’s History, marked MCP (see The query history).

What the assistant can do

ToolWhat it does
list_data_sourcesThe project’s data sources: name, engine, colour, and the server version once connected. Also the limits and what the project allows.
list_objectsOne level of a data source’s catalog, as the explorer shows it. Each object comes with the path that lists its children.
describe_tableA table’s columns with their types, its primary key, indexes and constraints.
get_definitionThe source of a view, function, trigger or index.
queryRuns one statement that only reads, and returns its rows as text.
explainShows how the database would run one read statement, without running it.
executeRuns statements that change something, in one transaction. Only offered when the project allows write, schema or admin.

Every tool but execute is marked read-only, and execute is marked destructive. Clients that ask before running tools can therefore let the others run freely and ask before each execute.

Permissions

Each project allows MCP clients some of these. Each one starts at the global default and can be overridden in Project settings…, and Reset goes back to the default. Change the defaults in View › Settings… › MCP; they apply to every project that doesn’t override them.

PermissionAllowsDefault
ReadBrowsing the catalog, SELECT, EXPLAIN, SHOWon
WriteINSERT, UPDATE, DELETE, MERGE, TRUNCATEoff
SchemaCREATE, ALTER, DROP, COMMENT ON and other DDLoff
AdminGrants, roles and users, databases, server settings, COPY to or from files, killing sessions, procedures (CALL), and anything dbdesk can’t parseoff
Row limitThe most rows one call returns; the reply says "truncated": true when there were more500
Statement timeoutHow long one statement may run before it is cancelled30 s
SchemasAll, or only the schemas listed: the catalog tools show only those, and statements may only name thoseall

With Write, Schema or Admin on, the assistant gets the execute tool. It takes one or more statements and checks every one before running any. If one needs a permission the project lacks, the whole call is refused, and the log says which permission was missing. Otherwise the statements run in one transaction, committed only if they all succeed. The exception is MySQL and MariaDB, which commit DDL on their own; there the reply says the call wasn’t atomic. query still only reads, whatever the project allows.

A running server keeps the settings it started with. After changing them, restart the server: most clients have a button for it, or restart the client.

How the permissions are enforced

dbdesk checks each statement in up to three ways:

  1. dbdesk reads the statement first. It parses it and works out what it does, looking inside WITH clauses, SELECT … INTO, EXPLAIN ANALYZE and the rest. It also notices well-known functions that end sessions, change settings or read files, such as pg_terminate_backend and set_config. A statement that needs a permission the project lacks is refused before it reaches the database. So is one dbdesk can’t parse, unless the project allows Admin, since what it would do is unknown.
  2. When only reading is allowed, the database refuses writes on its own. Every read runs in a read-only transaction that is then rolled back: BEGIN READ ONLY on PostgreSQL, START TRANSACTION READ ONLY on MySQL and MariaDB. SQLite files are opened read-only. This stops a SELECT that calls a function which writes, because dbdesk can’t see inside the function.
  3. The database’s own grants, through an MCP login. This is the only layer that is airtight beyond reading.

Beyond reading, the first layer is all dbdesk has of its own, and it can be fooled: a function or a procedure can do anything, whatever the statement that calls it looks like. That is why Project settings warns when a project allows changes and one of its data sources has no MCP login.

SQL Server has no read-only transaction, so dbdesk doesn’t serve SQL Server data sources to MCP clients yet. The server’s log says so when it starts.

The MCP login

A data source can have a second login, used only by dbdesk mcp. Create a database user with exactly the grants an assistant should have, then enter it in the data source’s dialog (Edit…) as MCP login and MCP password. Every MCP call then signs in as that user, and the database enforces its grants, whatever dbdesk’s own checks miss.

For example, a PostgreSQL role for a project that allows Write, with nothing beyond what that needs:

CREATE ROLE assistant LOGIN PASSWORD '…';
GRANT CONNECT ON DATABASE shop TO assistant;
GRANT USAGE ON SCHEMA public TO assistant;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO assistant;

The MCP password is kept in the keychain under its own entry, separate from the data source’s password. If it is missing, the server refuses to connect; it never falls back to the data source’s own login. Clear the MCP login field to remove it, together with its password.

SQLite files have no logins, so for them dbdesk’s own checks are all there is.

What dbdesk records

  • The log. Every call is a line with the project, data source, tool and the kind of statement. Refusals are logged with their reason, and with missing_permission when the project lacked one. The server logs to its own daily files, dbdesk-mcp.YYYY-MM-DD.log, next to the app’s (see Where dbdesk keeps its files). It also logs to standard error, which most clients keep in their own logs.
  • The query history. Every statement the server sends to a database is in the data source’s history, marked MCP with the assistant’s name, beside your own. The History window can show just those, and export them (see The query history).

When it doesn’t work

The client’s log for the server shows what dbdesk said when it stopped:

  • “there is no project with the slug …”: the --project doesn’t match any project’s slug. The message lists the slugs there are. Copy the configuration again.
  • “… is not allowed for MCP clients”: tick Allow MCP clients to use this project in Project settings, as in step 1.
  • “the MCP login … has no saved password”: set the MCP password in the data source’s dialog.
  • “unknown argument”: the client’s args have a typo; each one is checked.

The server reads projects.toml and data_sources.toml when it starts. After changing either, restart the server.

Where dbdesk keeps its files

dbdesk keeps everything in one folder per platform:

PlatformSettingsWindow state and logs
macOS~/Library/Application Support/dbdesk/same folder
Linux~/.config/dbdesk/~/.local/share/dbdesk/
Windows%APPDATA%\dbdesk\config\%APPDATA%\dbdesk\data\
FileWhat it holds
data_sources.toml (settings folder)Your data sources. No passwords.
projects.toml (settings folder)Your projects. Which project a data source is in is written in data_sources.toml.
consoles/<data source id>/*.sql (settings folder)Your query consoles, one file each. See Query consoles.
history.sqlite (settings folder)Every statement dbdesk and MCP clients sent, with its time, duration, outcome and where it came from (see The query history).
keymap.toml (settings folder)Your shortcuts, if you changed any. See Keyboard shortcuts.
settings.toml (settings folder)Your choices in View → Settings…, such as how SQL is formatted and whether to check for updates.
actions.toml, snippets.toml, theme.toml (settings folder)Your own actions, snippets and colours, if you wrote any. See Actions, snippets and themes.
trusted_commands.toml (window state folder)The command actions you chose Run for, each as it was then. See Commands.
app.ron (window state folder)The window’s size and position, the panel layout, the theme you picked, the recent .sql files and which file each console tab shows.
logs/dbdesk.YYYY-MM-DD.log (window state folder)One log file per day. The last seven are kept.
logs/dbdesk-mcp.YYYY-MM-DD.log (window state folder)The same for dbdesk mcp, the server AI assistants use.

Passwords are not in any of these files: they are in your system’s keychain, under the service name dbdesk. See Data sources.

SSH tunnels share ~/.ssh/known_hosts with ssh: dbdesk reads it to check SSH servers’ host keys, and adds a line when you trust a new one. See SSH tunnels.

data_sources.toml

You can edit this file by hand while dbdesk is closed. dbdesk reads it at start-up and rewrites it every time you save or delete a data source, so changes made while it runs are overwritten, and comments are not kept.

[[data_source]]
id = "6f1c0d9e-3b8a-4c3e-9f57-2d1e8a7b5c40"
name = "local"
color = "teal"
project = "5b0c7d1e-2f3a-4b5c-8d6e-7f8091a2b3c4"
driver = "postgres"
host = "localhost"
port = 5432
database = "app"
user = "juan"

[[data_source]]
id = "b3c9e1f2-7a4d-4b8e-9c2f-1d5e6a7b8c90"
name = "production"
color = "red"
driver = "postgres"
host = "db.example.com"
port = 5432
database = "app"
user = "juan"

[data_source.tls]
mode = "verify-full"
ca_file = "/Users/juan/certs/example-ca.pem"

[[data_source]]
id = "0b6a4f8e-91d2-4e7a-a1c3-5f0e2d9b7c18"
name = "chinook.db"
driver = "sqlite"
path = "/Users/juan/data/chinook.db"
read_only = true
  • id identifies the data source for good, including its keychain entry. Don’t change it; for a new data source by hand, any unique UUID will do.
  • color is optional: red, orange, yellow, green, teal, blue, violet or pink.
  • project is optional: the id of a project in projects.toml. Left out, or naming a project that doesn’t exist, the data source is in No project.
  • mcp_login is optional, for network engines: mcp_login = { user = "assistant" } is the user AI assistants sign in as (see The MCP login). Its password is in the keychain.
  • tls is optional for PostgreSQL, and mode defaults to prefer: disable, prefer, require or verify-full. ca_file is only read with verify-full.
  • read_only is optional and defaults to false. It opens a SQLite file read-only; safety is the setting for every engine.
  • safety is optional: normal (the default, left out), careful or read-only. See Safety levels.
  • host, port, database, user, path and ca_file may hold ${NAME} placeholders, resolved from the environment when connecting (see Placeholders). A port with one is a string: port = "${PGPORT}".

An export (File › Export data sources…) is the same [[data_source]] tables, with a format_version = 1 line and the projects as [[project]] tables, as projects.toml writes them. See Moving data sources to another machine.

If the file can’t be read — say, a typo while editing it — the explorer shows the error and dbdesk refuses to save or delete anything, so it never overwrites your file with an empty list. Fix the file and restart dbdesk.

projects.toml

Like data_sources.toml, edit it only while dbdesk is closed; comments are not kept.

[mcp_defaults]
read = true
write = false
schema = false
admin = false
max_rows = 500
statement_timeout = "30s"
schemas = "all"

[[project]]
id = "5b0c7d1e-2f3a-4b5c-8d6e-7f8091a2b3c4"
name = "Web shop"
slug = "web-shop"
mcp_enabled = true

[project.mcp]
write = true
schemas = ["public"]
  • id is what data sources point at. Don’t change it.
  • slug is lower-case letters, digits and single hyphens, and must be unique. It is kept when the project is renamed.
  • mcp_enabled lets AI assistants use the project’s databases through dbdesk mcp; see Connecting an AI assistant. A new project starts with it false.
  • mcp_defaults sets what MCP clients may do in every project, and a [project.mcp] table under a project overrides it for that project. read, write, schema and admin are the permissions; max_rows and statement_timeout (such as "30s" or "2m") limit each call; schemas is "all" or a list such as ["public", "reports"]. Project settings and View › Settings › MCP edit all of these.

Unknown keys are refused rather than ignored, so a setting that is misspelt, or one written by a newer dbdesk, never goes unnoticed. If the file can’t be read, the explorer lists every data source ungrouped and dbdesk refuses to change projects until the file is fixed, so it never overwrites it.

Starting over

To reset the layout, use View → Reset layout. To go back to the default shortcuts, delete keymap.toml and click Reload in View → Keymap…; for the default settings, delete settings.toml and click Reload in View → Settings…. To forget everything, quit dbdesk and delete its folders; the passwords stay in the keychain until you delete the dbdesk entries there too.

Troubleshooting

Where to look first

The log console, under the consoles, shows what dbdesk did and what went wrong, with the newest entries at the bottom. The drop-down on its left picks how much detail to show (Error, Warn, Info, Debug, Trace); the search box filters entries by text; the bin icon clears the list. It keeps the latest 10,000 entries.

The same entries, from dbdesk itself and warnings from the libraries it uses, go to a log file per day: see Where dbdesk keeps its files. Attach the day’s file when you report a problem; it never contains passwords.

TLS/SSL errors

The data source’s TLS setting decides whether dbdesk encrypts the connection (see Data sources). The usual errors, and what to change:

  • pg_hba.conf rejects connection … no encryption (or no pg_hba.conf entry … SSL off): the server only accepts encrypted connections and TLS is disable. Pick require or, better, verify-full.
  • server does not support TLS: TLS is require or verify-full, but the server has encryption turned off. Pick prefer or disable if the network is one you trust, such as a database in a local container, or turn TLS on in the server.
  • invalid peer certificate: UnknownIssuer: with verify-full, the certificate isn’t signed by an authority dbdesk trusts. Set CA file to your provider’s or your company’s authority certificate (a PEM file).
  • certificate not valid for name …; certificate is only valid for …: with verify-full, the certificate doesn’t list the Host you typed. Use one of the names the message lists, often the one in the provider’s connection string, instead of an IP address or an alias.
  • could not read the CA file … or … holds no usable certificate: the CA file is missing, or isn’t a PEM file with a certificate in it (-----BEGIN CERTIFICATE-----).

Don’t turn off encryption on a server just for dbdesk.

The keychain keeps asking

On macOS, the system asks whether dbdesk may use the password it saved in your keychain the first time dbdesk reads it. Always Allow stops the questions, but macOS ties that answer to the exact program. Because you build dbdesk yourself, every rebuild is a new program to macOS, and it asks again.

dbdesk reads the password each time it opens a connection: when the explorer connects, when a console runs its first statement, and when you test a connection without typing the password. If you deny access, the connection fails with could not read the saved password.

To see or delete the saved passwords, open Keychain Access and search for dbdesk. Each entry’s account is the ID of a data source in data_sources.toml.

On Linux, dbdesk needs a running Secret Service (GNOME Keyring or KWallet) to save passwords. Without one, saving a PostgreSQL data source with a password fails with could not store the password.

A connection takes long, then fails

dbdesk gives up on a connection attempt after 15 seconds and reports could not connect within 15s. Check the host and port, and that nothing (a VPN, a firewall) is in the way. Test connection in the data source dialog is the quickest way to try again.

SSH tunnels

  • “the SSH server … host key has changed … refusing to connect”: the key the server presents isn’t the one ~/.ssh/known_hosts holds for it. That is what someone impersonating the server would look like, so dbdesk never connects. If the server’s key really changed (it was reinstalled, say), ask its administrator for the new fingerprint, remove the line the message names from ~/.ssh/known_hosts (or run ssh-keygen -R '[host]:port'), and connect again to be asked about the new key.
  • “the SSH server refused to sign in … with the keys in the SSH agent”: the agent holds no key the server accepts. ssh-add -l lists the agent’s keys; ssh-add adds yours. On Windows, start the OpenSSH Authentication Agent service, or Pageant.
  • “could not use the SSH agent”: no agent is running where dbdesk can find it. An app started from the Dock or the Start menu may not see the SSH_AUTH_SOCK your terminal has; choose Key file instead.
  • “the SSH server could not forward to host:port”: you signed in, but the SSH server can’t reach the database at that address, or isn’t allowed to forward there (AllowTcpForwarding and PermitOpen in its sshd_config). Host and Port are the database as the SSH server sees it.

“unable to open database file” for SQLite

The file in the data source’s File field doesn’t exist, or dbdesk can’t read it. Connecting never creates a SQLite file, so check the path, or pick the file with Browse… in the data source dialog. To start a new database, use New… there (see Data sources).

“Can’t read data sources”

data_sources.toml exists but isn’t valid, usually after editing it by hand. dbdesk shows the error in the explorer and refuses to change the file until it is fixed, so nothing is lost. Correct the file (the error says which line), then restart dbdesk.

The status bar says the engine stopped

The engine is the part of dbdesk that talks to databases. If it stops, nothing can connect or run, and the reason is in the log console and the log file. Restart dbdesk; if it happens again, please report it with the day’s log file.

“keymap.toml: N problems” in the status bar

Some lines of keymap.toml couldn’t be used: an unknown action, a shortcut that can’t be read, or a shortcut two actions share. Click the message to open View → Keymap…, which lists each problem; fix the file and click Reload. Everything else in the file still applies.

“actions.toml: N problems” in the status bar

The same for actions.toml, snippets.toml or theme.toml: an entry that couldn’t be used was skipped, and the rest of the file applies. Click the message to see each problem, fix the file and click Reload. For theme.toml it can also be a colour that leaves some text too faint to read; that colour still applies. See Actions, snippets and themes.

A command action says its program “was not found”

dbdesk looked for the program in the folders of its PATH, which the window lists. A Mac app opened from the Finder doesn’t get the PATH your shell sets, so programs installed with Homebrew aren’t on it. Write the program’s full path in actions.toml (which jq in a terminal prints it) and click Reload. See Commands.

The panels are in a mess

View → Reset layout puts every panel back where it started. If you closed the Welcome tab, this brings it back too.