DuckPlus

A DuckDB IDE, not a database IDE with DuckDB in it.

DuckPlus is a native Mac app written in Rust and drawn by the GPU, on gpui — the framework Zed is built with. It talks to a DuckDB server over Quack, or opens a .duckdb file straight off your disk. There is no Electron in it, no web view, and no driver list with DuckDB somewhere in the middle.

Coming soon for Mac

Nearly done · macOS 12+ · Apple silicon and Intel

Production analytics
1-- ⌘↵ runs the statement (or your selection)
2SELECT database_name, schema_name, table_name, estimated_size
3FROM duckdb_tables()
4ORDER BY estimated_size DESC;
3 rows22 ms
database_nameVARCHARschema_nameVARCHARtable_nameVARCHARestimated_sizeBIGINT
1analyticsmainevents48210332
2warehousebillingorders2906115
3analyticsmainusers184907

That window is drawn, not screenshotted. Open a table, run one of the server views, double-click a cell in users and save it, then open the query log to see what was sent. The data is invented. The layout, the SQL and both palettes are the app's.

Native, and you can tell.

The tree, the editor and the grid are shaped text and rectangles on a Metal layer. The app starts like a Mac app, scrolls like one, and has nothing running in it that was meant for a browser.

One database, all of its dialect.

It is built for DuckDB alone, so it doesn't reduce anything to the SQL that every database shares. FROM-first queries, DESCRIBE, table functions, DuckDB's types and its admin functions all work as written.

Tokens go to the keychain.

A saved connection writes its name, colour and endpoint to disk. The token goes to the macOS Keychain and nowhere else, so there's no file of credentials sitting in Application Support.

Connecting

Paste an endpoint, press ↵, and you're in.

The launcher has two tabs and asks for as little as it can. For a server, that's an endpoint and a token. For a file, it's the file. A connection made there is kept with a name and a colour for next time, while one made from the terminal with duckplus connects without saving anything.

Quack

A DuckDB server

-- on the server
CALL quack_serve('quack:localhost', token := 'super_secret');

-- on your Mac
duckplus quack:localhost -t super_secret

Endpoints are quack:host[:port], and the port defaults to 9494. localhost uses plain HTTP and every other host uses HTTPS, with a Plain HTTP switch for a server on a private network. Test checks the endpoint and the token before you commit to either.

File

A database on your disk

# or double-click it in Finder
duckplus ~/data/analytics.duckdb

# open it and run something straight away
duckplus ~/data/analytics.duckdb -c "FROM duckdb_tables()"

.duckdb and .ddb files open in DuckPlus from Finder. If another process already holds the write lock, it opens the file read-only rather than failing, and a second window on the same file shares the open database rather than locking itself out.

Underneath

The client is DuckDB too.

DuckPlus embeds an in-memory DuckDB whose only job is to speak Quack. Every statement you run goes to the server wrapped in quack_query, so the server's own parser handles it — DDL and multi-statement scripts included — and the results come back in DuckDB's own vector format instead of passing through a generic driver.

  • Schema lookups run on their own connection, so the tree never waits behind a long query
  • Every query runs on a fresh clone of the connection, so a cancelled one can't block the next
  • New query windows (⌘T) get their own connections to the same server
  • A local file is opened by the same embedded engine, so both kinds of connection behave the same way

DuckPlus

window

sql

DuckDB

in-memory

quack

DuckDB

your server

The results come back as Arrow, and stay Arrow until a cell is drawn.
-- what ⌘↵ actually sends
SELECT * FROM quack_query(
  'quack:db.internal:9494',
  '<your sql>',
  token := '…'
);

Results

A cell becomes text only when it's on screen.

Results stay as the Arrow batches DuckDB returned. The grid is virtualized, so it builds only the rows you can see, and it formats only the cells in them. A hundred-thousand-row result costs the same to scroll as a ten-row one, because the rows you aren't looking at never get turned into strings.

01

A limit you set

The grid stops pulling batches at 10,000 rows by default, and the status bar says “limit reached” when it does. Change it in Settings.

02

Sorting that knows what it has

Click a header to cycle ascending, descending and off. If every row is loaded, it sorts them in place. If a table didn't fit, it asks the server again with ORDER BY.

03

Look inside a cell

Space opens the whole value in a dialog. Long values and anything that looks like JSON get a button in the cell, and JSON is pretty-printed.

04

Take it with you

⌘⇧C copies the selected cell's value, or the selected row, column or whole result as CSV with a header line. The clipboard button copies TSV for pasting into a spreadsheet.

Editing

Edit in the grid. Every save is one transaction.

Double-click a cell, or press ↵, and type. Edits are staged in the grid until you press ⌘S, and then they all go in one transaction. Esc first cancels the cell you're typing in, then discards everything staged.

Each UPDATE is paired with a check that its WHERE clause matches exactly one row. If it doesn't, the check raises an error inside the transaction and nothing is saved. A key column that turns out not to be unique would otherwise change several rows, and a row someone else deleted in the meantime would otherwise be skipped without telling you.

  • Rows are found by the primary key, then a UNIQUE constraint, then a column you pick, then a column named id
  • A plain SELECT … FROM one_table from the editor is editable too, WHERE, ORDER BY and LIMIT included
  • Joins, GROUP BY, DISTINCT, CTEs and computed columns make a result read-only, because a row no longer maps to a single table row
  • After a save, the query that produced the grid runs again, so your filter and ordering survive
BEGIN TRANSACTION;
SELECT CASE WHEN count(*) <> 1
  THEN error('Expected 1 row where "id" = …') END
FROM "analytics"."main"."users" WHERE "id" = '10437';
UPDATE "analytics"."main"."users"
SET "plan" = 'team', "mrr" = '49'
WHERE "id" = '10437';
COMMIT;

What ⌘S sends for two cells changed in one row. The query log records the whole transaction, so you can see exactly what was sent.

Guardrails

DROP, DELETE and TRUNCATE need a second ⌘↵.

The statement doesn't run. The status bar says what it is and waits. ⌘↵ again runs it and ⌘. drops it. The check looks at words, not substrings, so a column called dropped_at doesn't trigger it, and it can be turned off in Settings once it gets in your way.

This statement changes data. Press ⌘↵ again to run, ⌘. to cancel.

Cancel is always there

⌘. gives you the window back straight away and ignores whatever the query returns later. A timer counts up in the status bar while you wait, so you can tell a slow query from a stuck one.

The row limit protects you

A SELECT * against a billion-row table fetches the first 10,000 rows and stops. Opening a table from the tree fetches 500, which is a separate setting.

Colour-code production

Every saved connection has a colour, and it shows in the title bar of every window on that connection. Red means production, and you'll see it before you run anything.

The server

Seven admin screens. Each one is a query.

Under the tree sits a SERVER section: databases, storage, extensions, settings, memory, secrets and running Quack servers. Clicking one doesn't open a special panel. It puts the SQL in the editor and runs it, so the screen you wanted is also the starting point for the query you actually need.

Databases

SELECT database_name, type, path, readonly, internal, comment
FROM duckdb_databases()
ORDER BY internal, database_name;

Storage

SELECT *
FROM pragma_database_size();

Extensions

SELECT extension_name, loaded, installed, extension_version, install_mode, description
FROM duckdb_extensions()
ORDER BY loaded DESC, installed DESC, extension_name;

Settings

SELECT name, value, input_type, scope, description
FROM duckdb_settings()
ORDER BY name;

Memory

SELECT tag, memory_usage_bytes, temporary_storage_bytes
FROM duckdb_memory()
ORDER BY memory_usage_bytes DESC;

Secrets

SELECT name, type, provider, persistent, storage, scope
FROM duckdb_secrets();

Quack servers

FROM quack_server_list();

A local file shows six of them. quack_server_list() needs the quack extension, and a database you opened from disk doesn't load it.

The rest of it

Small things that save time every day.

A log of every query (⌘J)

Everything the window sends is logged: editor runs, tables opened from the tree, admin views, re-sorts and saves, each with where it came from, how long it took and what came back. Collapsed, it's one line showing the latest query. It keeps the last 500.

Formatting that only touches whitespace (⌘L)

⌘L formats the selection, or the whole buffer, and ⌘Z undoes it. Keyword case is left alone, because uppercasing keywords also uppercases identifiers like events or date. If the result differs from your SQL by anything other than whitespace, it's thrown away and nothing changes.

Focus one database

On a server with several attached databases, the dropdown in the title bar picks one. Queries then run with USE and the tree shows only that database. A saved connection can remember which one it opens into.

The schema tree

Databases, then schemas, then tables and views, with estimated row counts. ⌘P filters it. Clicking a table opens it in the grid without touching the editor, and there's a Structure button for DESCRIBE.

The same type on every Mac

JetBrains Mono is built into the app and used for the editor, the grid and table names, so a screenshot from one machine looks the same as one from another. Fonts, theme and row striping are all in Settings, and changes apply to every open window immediately.

Two themes, made for this app

DuckPlus Night and DuckPlus Day follow macOS, or can be fixed in Settings. The yellow is reserved for keywords, the caret, the selection and the one button you're meant to press, which is why it still stands out.

Connections

Six colours, and you know which one is red.

Saved connections go in folders you can drag between, sorted by when you last used them. The metadata goes in connections.json, which is safe to read because the token isn't in it. The token goes to the Keychain under its own entry, and deleting the connection deletes the token too.

Duck
Local
Dev
Staging
Testing
Production

Paste a Quack endpoint and token. Press ↵ to connect.Tokens are kept in your system keychain.

Keys

Everything is on the keyboard.

Nearly every command is also in the menu bar, so if you forget a shortcut, the menu shows it.

Queries

Run the statement or selection⌘↵
Run the whole buffer⌘⇧↵
Cancel⌘.
Format SQL⌘L
Toggle comment⌘/
New query window⌘T

Results

Edit the selected cell↵
Inspect the full valueSpace
Save staged edits⌘S
Discard staged editsEsc
Copy results as CSV⌘⇧C
Query log⌘J

Around

Filter tables⌘P
Focus the editor⌘E
Refresh the schema⌘R
Connections⌘N⌘⇧O
Settings⌘,
Bigger · smaller · reset⌘+⌘−⌘0

Fine print

What it will not do, yet or ever.

It's better to know these before you install it.

Quack is in beta

DuckDB's own client/server protocol is marked beta until DuckDB 2.0. DuckPlus goes through quack_query rather than ATTACH 'quack:…', because attaching still has catalog gaps.

Cancel is local

Quack has no remote cancel yet. ⌘. frees the window at once and throws the result away, but the server finishes the statement it was given.

It is DuckDB only

No Postgres, no MySQL, no SQLite tab. That is the point of it, but it is worth saying before you go looking for a driver list.

First launch needs the network once

The embedded engine downloads the quack extension into ~/.duckdb/extensions the first time. Local files do not need it.

Editing wants a key

Rows are updated by their primary key, a UNIQUE constraint, or a column you pick. DuckLake tables declare none, so you pick one — and every save checks it matched exactly one row.

Mac first

The keychain code already speaks Windows and Linux, and gpui runs on both. The installer, the bundle and the Finder integration are macOS today.

Almost ready.

DuckPlus is in its last round of polish before the first release. When it ships, you'll get a Mac app and a duckplus command for your terminal, and all it needs is a DuckDB server or a file to open.

Coming soon for Mac
macOS 12+ · Apple silicon and IntelTell me when it ships