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.
| System | File |
|---|---|
| macOS 11 or later, Apple silicon or Intel | dbdesk-<version>-macos-universal.dmg |
| Linux x86_64, most distributions | dbdesk-<version>-x86_64.AppImage |
| Debian, Ubuntu and derivatives | dbdesk_<version>_amd64.deb |
| Windows 10 or 11, 64-bit Intel or AMD | dbdesk-<version>-windows-x64-setup.exe |
macOS
- Open the
.dmgand drag dbdesk onto Applications. - Open dbdesk from Applications. These builds aren’t signed by Apple yet, so the first launch is blocked with “Apple could not verify dbdesk”.
- 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
- Run
dbdesk-<version>-windows-x64-setup.exe. Your browser may first say the file isn’t commonly downloaded; choose to keep it. - 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.
- 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 dbdesk | dbdesk → 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@hostfor 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:
| Field | Default | Notes |
|---|---|---|
| Host | localhost | Name or IP address of the server. |
| Port | 5432 | A number from 1 to 65535. |
| Database | postgres | The database to connect to. The explorer lists the server’s other databases too. |
| User | postgres | |
| TLS | prefer | Whether to encrypt the connection; see below. |
| SSH | off | Connect through an SSH server; see SSH tunnels. |
| Password | none | Stored in the keychain when you save. See below. |
| MCP login | none | Another user, only for AI assistants; see The MCP login. |
| MCP password | none | Shown 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:
| Mode | Encrypts | Checks the server’s certificate |
|---|---|---|
disable | Never | — |
prefer | If the server offers it, otherwise connects unencrypted | No |
require | Always; fails if the server can’t | No |
verify-full | Always | Yes: 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:
| Field | Default | Notes |
|---|---|---|
| Host | localhost | Name or IP address of the server. |
| Port | 3306 | |
| Database | mysql | The database unqualified names refer to, as USE would set. The explorer lists every database you may see. |
| User | root | |
| TLS | prefer | As 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. |
| Password | none | Stored in the keychain when you save. |
| MCP login | none | Another user, only for AI assistants; see The MCP login. |
| MCP password | none | Shown 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.
| Field | Default | Notes |
|---|---|---|
| Host | localhost | Name or IP address of the server. For Azure SQL, the server’s name, such as myserver.database.windows.net. |
| Port | 1433 | Named instances aren’t looked up: give the instance’s port. |
| Database | master | The database the console works in. The explorer lists every database the login may open. |
| User | sa | A SQL Server login. Windows and Microsoft Entra ID sign-in aren’t supported yet. |
| TLS | prefer | SQL 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). |
| Password | none | Stored 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:
| Field | Default | Notes |
|---|---|---|
| Host | localhost | Name or IP address of the server. For ClickHouse Cloud, the service’s host name. |
| Port | 8123 | ClickHouse’s HTTP port: dbdesk speaks HTTP to ClickHouse, not its native protocol on 9000. |
| Database | default | The database unqualified names refer to, as USE would set. The explorer lists every database you may see. |
| User | default | |
| TLS | disable | disable, require or verify-full; see below. |
| Password | none | Stored 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,.sqlite3or 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.
- Browse… opens your system’s file dialog to pick an existing database. It shows every
file, because SQLite databases end in
-
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 (theduckdbcommand, 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.
| Field | Default | Notes |
|---|---|---|
| SSH host | none | The SSH server’s name or IP address. |
| SSH port | 22 | |
| SSH user | none | Whom to sign in to the SSH server as. |
| SSH login | SSH agent | How to sign in; see below. |
| Key file | none | For Key file: your private key, in OpenSSH’s format. Browse… opens ~/.ssh. |
| Passphrase | none | For an encrypted key. Stored in the keychain when you save. |
| SSH password | none | For 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
sshdoes: the agentSSH_AUTH_SOCKnames 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:
| Level | What happens |
|---|---|
| Normal | Changes run when asked. Only an UPDATE or DELETE without WHERE asks first (see Query consoles). |
| Careful | Every 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-only | Changes 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 inSTART 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,BEGINand 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
/mcpas its account. Onlydbdesk mcpreads it. - An SSH password or key passphrase has one too, with
/sshafter 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 item | What it does |
|---|---|
| Connect / Disconnect | Opens or closes the explorer’s connection. Expanding a data source also connects it. |
| Refresh | Reloads everything under the data source (while connected). |
| New query console | Opens a console on this data source. See Query consoles. |
| Open saved console | Reopens 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:
Choice What it does Import / Update Adds it, or makes this machine’s entry match the file. A project keeps its slug, so MCP clients set up with it keep working. Skip Leaves this machine as it is. A data source in a skipped new project is imported in No project. Import as a copy Adds 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 item | Shown for | What it does |
|---|---|---|
| Open data | Tables, views, materialized views | Opens a new console with SELECT * FROM the object and runs it. |
| Import CSV… | Tables | Asks for a CSV file and imports it into the table; see Importing a CSV file. |
| Copy name | Objects inside a schema | Copies 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 definition | Objects whose source the database keeps (see below) | Opens the source in a window. |
| Refresh | Anything with children | Reloads the node’s children and collapses everything below it. |
| Copy DDL | Tables, views, materialized views | Copies 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_DEFINITIONreturns), and synonyms. One createdWITH 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 MACROper parameter list. - ClickHouse: tables, views, materialized views and dictionaries, as ClickHouse keeps
their
CREATEstatement.
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_schemaand temporary schemas, and its predefinedpg_roles; - SQLite’s internal
sqlite_tables; - MySQL’s
mysql,information_schema,performance_schemaandsysdatabases; - SQL Server’s
master,model,msdbandtempdbdatabases, and in every database thesys,INFORMATION_SCHEMAandguestschemas and those of the fixed database roles; - DuckDB’s
systemandtempdatabases, and itsinformation_schemaandpg_catalogschemas; - ClickHouse’s
systemandinformation_schemadatabases (in either case), and the.innertables where a materialized view without aTOtable 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 on | Menu items |
|---|---|
| A schema, or its tables folder | New › Table… |
| A table | Modify table…, New index…, Rename…, Copy DDL, Truncate…, Drop… |
| A table’s indexes folder | New › Index… |
| An index | Drop 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 TABLEstatement 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,timestamptzand so on on PostgreSQL;UInt64,String,DateTimeon 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 NULLand 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
MergeTreeengine, sorted by the primary key’s columns (ORDER BY), and a nullable column’s type is wrapped inNullable(…). - 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
NULLon 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_INCREMENTcolumn) 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.
| Database | In place | Refused |
|---|---|---|
| PostgreSQL | everything | — |
| MySQL, MariaDB | everything; a new type or nullability restates the column (MODIFY COLUMN), so its comment and character set go back to the defaults | — |
| SQL Server | everything; a column’s default is a named constraint, dropped and added again around a change to the column | — |
| DuckDB | columns: rename, add, drop, type, nullability, default | keys and checks, the primary key |
| ClickHouse | columns (nullability is its Nullable(…) type) and checks | the primary key, which is the table’s sort order |
| SQLite | rename, 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:
- It creates the new table as
new_orders, copies every row into it, drops the old table and gives the new one its name. - It creates the table’s indexes and triggers again, from their definitions.
- 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,
jsonband 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 …, orRENAME TABLEon MySQL and ClickHouse, orsp_renameon SQL Server. - Truncate… deletes every row and keeps the table. SQLite has no
TRUNCATE, so it writesDELETE FROMthe 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 anALTER 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’sGENERATED … AS IDENTITY, SQL Server’sIDENTITY(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 on | Menu items |
|---|---|
| A data source whose schemas are databases (MySQL, MariaDB, ClickHouse) | New › Schema… |
| A database (PostgreSQL, SQL Server, DuckDB) | New › Schema… |
| A schema | New › Table…, New › View…, New › Materialized view… (PostgreSQL), Drop schema… |
| A schema’s views folder | New › View…, New › Materialized view… (PostgreSQL) |
| A schema’s materialized views folder (PostgreSQL) | New › Materialized view… |
| A view | Edit…, 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 SCHEMAmakes. 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
SELECTthe 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 REPLACEeither: 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 …(orALTER MATERIALIZED VIEW),RENAME TABLEon MySQL and ClickHouse,sp_renameon 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 on | Menu items |
|---|---|
| A PostgreSQL data source, or its roles folder | New › User…, New › Role… |
| A role | Copy 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_datalets 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\passworddoes, 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 ROLEfor the attributes, connection limit and expiry you changed; REVOKEfor the roles it leaves, andGRANTfor 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 …, thenDROP OWNED BY …for the privileges left, thenDROP 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, thenDROP 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 * FROMthat 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.ortable., that table’s columns; afterschema., that schema’s tables; - after
FROM,JOIN,UPDATEorINTO, tables and views; - after
SELECT,WHERE,ON,GROUP BY,ORDER BYorSET, the columns of the tables the statement uses, their aliases, and functions; - inside
INSERT INTO orders (…), the columns oforders.
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:
= NULLor<> NULL, which are never true: writeIS NULLorIS NOT NULL;UPDATEorDELETEwithoutWHERE, 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:
| Button | Shortcut | What 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 | ⌥⌘E | Shows 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 | ⌥⌘L | Formats 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 msfor a query,12 rows affected · 4 msforINSERT,UPDATE,DELETE,MERGE,REPLACEandCOPY,Done · 2 msfor anything else, such asCREATE 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, notNULL). - Changed cells are tinted. Submit writes them, one
UPDATEper 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.sqlstarts 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’slambda x: x + 1(after a keyword it is one:BETWEEN:loworks); - anything inside
[…]or{…}: array slices such asa[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 aUSEorSETit 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 aspsqldoes, 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
| Shortcut | Action |
|---|---|
| ⌘F | Find; the selected text, if it is on one line, is searched for |
| ⌥⌘F | Find and replace |
| ⌘G, F3, or Enter in the find field | Go to the next match |
| ⇧⌘G, ⇧F3, or ⇧Enter in the find field | Go to the previous match |
| Escape | Close 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,
ordersfindsORDERStoo. - W finds whole words only:
iddoesn’t find theidinorder_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) orNULL. 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.
- 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.
- 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.
- The first rows of the file show as they would go into the table, with
NULLfor empty values andDEFAULTfor columns left to their default. - 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 beNULL. 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
| Shortcut | Action | Works while |
|---|---|---|
| ⌘↵ | Run the selection, or the statement at the cursor | the console’s editor has focus |
| ⇧⌘↵ | Run every statement in the console | the editor has focus |
| ⌥⌘E | Explain the statement at the cursor | the editor has focus |
| ⌘. | Cancel the running statement, or discard rows not yet loaded | the editor has focus |
| ⌃Space | Suggest names and keywords at the cursor | the editor has focus |
| ⌥⌘L | Format the selection, or the statement at the cursor | the editor has focus |
| ⇧⌥⌘L | Format every statement in the console | the editor has focus |
| F8 | Go to the next error or warning | the editor has focus |
| ⇧F8 | Go to the previous error or warning | the editor has focus |
| ⌘F | Find in the console | the editor or the find bar has focus |
| ⌥⌘F | Find and replace in the console | the editor or the find bar has focus |
| ⌘G, F3 | Go to the next match | the editor or the find bar has focus |
| ⇧⌘G, ⇧F3 | Go to the previous match | the editor or the find bar has focus |
| ⌘, | Open the settings | always |
| ⌘O | Open a .sql file in a new console | always |
| ⌘S | Save the console to its .sql file | a console’s tab is selected |
| ⇧⌘S | Save the console to a new .sql file | a console’s tab is selected |
| Space | Show the selected cell’s whole value | no text field has focus |
| F2, Enter | Edit the selected cell | no text field has focus |
| ⌘C | Copy the selected cells | no 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:
| Shortcut | Action |
|---|---|
| ⌘A | Select all |
| ⌘C, ⌘X, ⌘V | Copy, cut, paste |
| ⌘Z, ⇧⌘Z | Undo, redo |
| ⇧ with arrow keys | Extend 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
idnames the action for good: lowercase letters, digits,-and_. It is howkeymap.tomlbinds it.nameis what the menus show.applies_tosays 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.
- explorer nodes by kind:
driverslimits it to some databases:postgres,sqlite,mysql,sqlserver,duckdb,clickhouse. Leave it out for every one.run = falseopens 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
| Placeholder | Becomes |
|---|---|
{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"
commandis 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.stdinsends the grid’s selected cells to the program: the selected columns of the selected rows, asjson(an array of objects, as Export writes it),csvortsv, each with the column names. Only the grid has a selection, so an action withstdinapplies only togrid.selection. Leave it out, or writenone, and stdin is empty.outputsays 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.
timeoutis how long it may run before dbdesk stops it, as"500ms","30s","5m"or"1h": a minute when left out.runis for SQL only, and a command can’t apply togrid.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:
| Placeholder | Becomes |
|---|---|
{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, sojqmay be “not found” even though your terminal finds it. The window says whichPATHwas searched. Write the full path instead:which jqin a terminal prints it, such as/opt/homebrew/bin/jq. - On Windows, a program is found on
PATHwith or without.exe, and runs without a console window. A.bator.cmdfile runs throughcmd.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 letcmd.exeread 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:
| Name | What it colours |
|---|---|
primary | Selection, focus, and the main button on a screen |
secondary | SQL keywords, links |
success, warning, error | What went right, what to watch, what went wrong |
base | Behind the panels, and the menu and status bars |
panel | The panels, windows and menus |
field | Text fields, the editor and the grid |
control, control_hover | Buttons at rest and under the pointer |
border | Lines around panels and windows |
text, text_weak | Text, and details, hints and comments |
selection | Behind selected text and rows |
on_primary | Text 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 thedbdesk-…entry inside itsmcpServers, then restart Claude Desktop. -
Cursor:
~/.cursor/mcp.json, or.cursor/mcp.jsonin a project folder. -
Claude Code: a project’s
.mcp.jsontakes 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
| Tool | What it does |
|---|---|
list_data_sources | The project’s data sources: name, engine, colour, and the server version once connected. Also the limits and what the project allows. |
list_objects | One level of a data source’s catalog, as the explorer shows it. Each object comes with the path that lists its children. |
describe_table | A table’s columns with their types, its primary key, indexes and constraints. |
get_definition | The source of a view, function, trigger or index. |
query | Runs one statement that only reads, and returns its rows as text. |
explain | Shows how the database would run one read statement, without running it. |
execute | Runs 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.
| Permission | Allows | Default |
|---|---|---|
| Read | Browsing the catalog, SELECT, EXPLAIN, SHOW | on |
| Write | INSERT, UPDATE, DELETE, MERGE, TRUNCATE | off |
| Schema | CREATE, ALTER, DROP, COMMENT ON and other DDL | off |
| Admin | Grants, roles and users, databases, server settings, COPY to or from files, killing sessions, procedures (CALL), and anything dbdesk can’t parse | off |
| Row limit | The most rows one call returns; the reply says "truncated": true when there were more | 500 |
| Statement timeout | How long one statement may run before it is cancelled | 30 s |
| Schemas | All, or only the schemas listed: the catalog tools show only those, and statements may only name those | all |
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:
- dbdesk reads the statement first. It parses it and works out what it does, looking
inside
WITHclauses,SELECT … INTO,EXPLAIN ANALYZEand the rest. It also notices well-known functions that end sessions, change settings or read files, such aspg_terminate_backendandset_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. - 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 ONLYon PostgreSQL,START TRANSACTION READ ONLYon MySQL and MariaDB. SQLite files are opened read-only. This stops aSELECTthat calls a function which writes, because dbdesk can’t see inside the function. - 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_permissionwhen 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
--projectdoesn’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
argshave 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:
| Platform | Settings | Window state and logs |
|---|---|---|
| macOS | ~/Library/Application Support/dbdesk/ | same folder |
| Linux | ~/.config/dbdesk/ | ~/.local/share/dbdesk/ |
| Windows | %APPDATA%\dbdesk\config\ | %APPDATA%\dbdesk\data\ |
| File | What 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
ididentifies 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.coloris optional:red,orange,yellow,green,teal,blue,violetorpink.projectis optional: theidof a project inprojects.toml. Left out, or naming a project that doesn’t exist, the data source is in No project.mcp_loginis 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.tlsis optional for PostgreSQL, andmodedefaults toprefer:disable,prefer,requireorverify-full.ca_fileis only read withverify-full.read_onlyis optional and defaults tofalse. It opens a SQLite file read-only;safetyis the setting for every engine.safetyis optional:normal(the default, left out),carefulorread-only. See Safety levels.host,port,database,user,pathandca_filemay 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"]
idis what data sources point at. Don’t change it.slugis lower-case letters, digits and single hyphens, and must be unique. It is kept when the project is renamed.mcp_enabledlets AI assistants use the project’s databases throughdbdesk mcp; see Connecting an AI assistant. A new project starts with itfalse.mcp_defaultssets what MCP clients may do in every project, and a[project.mcp]table under a project overrides it for that project.read,write,schemaandadminare the permissions;max_rowsandstatement_timeout(such as"30s"or"2m") limit each call;schemasis"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. Pickrequireor, better,verify-full. - server does not support TLS: TLS is
requireorverify-full, but the server has encryption turned off. Pickpreferordisableif 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_hostsholds 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 runssh-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 -llists the agent’s keys;ssh-addadds 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_SOCKyour 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 (
AllowTcpForwardingandPermitOpenin itssshd_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.