English | 中文
Duckling is a lightweight desktop application built using Tauri, designed for quickly browsing parquet/csv/json file data and various databases. Beyond browsing, it ships with a SQL editor, pivot tables, column profiling, value inspection, and data export to help you explore and analyze data without leaving the app.
It supports DuckDB / SQLite natively, and provides experimental support for the following databases (not heavily tested):
- PostgreSQL
- MySQL
- ClickHouse (HTTP interface, usually port
8123) - Doris / StarRocks (MySQL protocol, usually port
9030)
Note: The current objective of this project is not to develop a fully functional database management tool, but rather to facilitate quick browsing and lightweight analysis of various types of data.
- Data browsing — Canvas-rendered result grid with pagination, column hiding, transpose, result filtering, and per-cell value inspection.
- SQL editor — Monaco-based editor with schema-aware autocomplete, run / format /
EXPLAINactions, SQL bookmarks, and an optional per-statement split mode (toolbar toggle; default off). - SQL template variables — Write Jinja2-style
{{ variable }}placeholders in SQL, declare values via a/* @vars */YAML block comment, and run multi-value queries (cartesian product → multiple result tabs) in one click. See SQL Templates below. - Pivot table — Build pivots from row/column dimensions and measures (
count/sum/avg/min/max), with high-cardinality warnings and copy-SQL support. - Column profile — Per-column statistics: total, null ratio, distinct count, min/max, and top values.
- Count by column — Value distribution for a column, shown as a table plus a bar chart.
- Value viewer — Inspect a cell in raw / JSON form, with a "Calculate" tab for per-selection statistics (min/max/mean/…) exported as Markdown.
- Export — Export the current result to CSV / TSV / JSON / Parquet / XLSX with delimiter, header, and compression options.
- Sidebar — Database explorer, query history, and favorites (pinned tables + saved SQL), all searchable.
- Settings suite — Appearance & language, SSH profiles, keyboard shortcuts, SQL formatting, import/export options, and in-app updates.
- Keyboard shortcuts — A categorized shortcut overlay (
Mod+/) and a reconfigurable hotkey settings page.
Implemented connectors:
- DuckDB — open a
*.duckdbfile (plus an optional working path). - DuckDB (Quack) — connect via URI + token (in-memory / remote Quack).
- Data folder — pick a directory to browse
parquet/csv/ etc. - SQLite — open a database file.
- MySQL — host / port / database / user / password.
- PostgreSQL — host / port / database / user / password + SSL mode.
- ClickHouse — host / port / database / user / password (HTTP interface).
Experimental (not heavily tested):
- Doris / StarRocks — via the MySQL protocol, usually port
9030.
Additional entry points:
- File association — opening a
.duckdbor.parquetfile launches Duckling directly. - SSH tunnel — available for MySQL / PostgreSQL, using reusable SSH profiles or manual configuration.
- Connection transfer — export / import one or all connections as JSON, with optional master-password-encrypted secrets.
From the releases page, download the latest installer for your platform.
For Windows, if you cannot install WebView2 due to network issues, you can install WebView2 offline.
Note: When selecting an installation path, choose an empty folder or create a new one. Do not put data files in the installation path, and avoid selecting a non-empty folder. During uninstallation, if you choose to clear data files, the entire folder is deleted — even files that do not belong to the software.
Open a data folder, a *.duckdb file, or a database connection.
Write a template once and run it against different tables or values without copying.
/*
@vars
schema: public
table:
- orders
- order_items
- payments
*/
SELECT COUNT(*) FROM {{ schema }}.{{ table }}- Run normally (
Mod+Enter/ toolbar ▶). The backend reads@vars, finds{{ schema }}and{{ table }}, expands each combination, and opens a result tab for each row. - Missing variables — If a placeholder has no value yet, a dialog pops up to fill in one value per line. Check "Save values as @vars comment above SQL" to write the filled values back as a
@varsblock — next run needs no dialog. - Toolbar
{}button — Select SQL, click{}in the toolbar to auto-generate an@varsscaffold for all{{ name }}placeholders found in the selection. Fill in values directly in the editor.
Single variable — batch across tables
/*
@vars
table:
- customers
- orders
- products
*/
SELECT COUNT(*) AS cnt FROM {{ table }}→ 3 result tabs: count of customers, orders, products.
Multiple variables — cartesian product
/*
@vars
schema: public
region:
- us
- eu
*/
SELECT region, COUNT(*) AS cnt
FROM {{ schema }}.{{ table_prefix }}_{{ region }}
GROUP BY regionWith table_prefix set to sales in the dialog, this expands to 2 SQLs (public.sales_us, public.sales_eu), each producing one result tab.
Date range in WHERE
/*
@vars
table: events
start_date: '2024-01-01'
end_date: '2024-12-31'
*/
SELECT date, COUNT(*)
FROM {{ table }}
WHERE dt BETWEEN {{ start_date }} AND {{ end_date }}
GROUP BY dateColumn names in aggregation
/*
@vars
measure: revenue
segment: country
*/
SELECT {{ segment }}, SUM({{ measure }}) AS total
FROM sales
GROUP BY {{ segment }}
ORDER BY total DESC| Construct | Meaning |
|---|---|
{{ name }} |
Variable placeholder (Jinja2 interpolation syntax). Name must be a simple identifier. |
/* @vars … */ |
Leading block comment declaring YAML variable values. Supports scalars (single value) and sequences (multi-value). |
| Single value | table: orders |
| Multi value | table: then - orders / - items (one per line, YAML list) |
| Empty scaffold | table: '' — generated by {} button when no value exists yet |
- Backend (Rust + minijinja): Parses the
@varsYAML block, renders the Jinja template withUndefinedBehavior::StrictandAutoEscape::None, and performs a cartesian product when several variables have multiple values (max 50 combinations). - Frontend (TypeScript): Pure helpers (
src/lib/sql/macros.ts) for formatting YAML blocks, merging dialog overrides, and building empty scaffolds. - No effect on existing SQL without
{{ }}placeholders.
When a variable has multiple values (YAML list or multi-line in dialog), the system generates one statement per combination and opens a result tab for each. All tabs execute in parallel. A soft limit (20) asks for confirmation; a hard limit (50) rejects.
Write multiple SQL statements in one editor and use the toolbar toggle to choose whether runs and highlighting split by statement (default off).
Click the statement split button (forked-arrows icon) next to Run:
| State | Run (Mod+Enter / ▶) |
Highlight |
|---|---|---|
| Off (default) | Execute the entire editor buffer | No current-statement highlight |
| On | Execute only the statement at the cursor | Rectangle around that statement (width follows the widest line of text, not full editor width) |
- With a non-empty selection, the selection is always executed (toggle ignored).
- Toggle state applies to the current editor tab; refreshing the app resets to off.
When split is on, a semicolon lexer (ignoring ; inside strings, line comments, # comments, block comments, and $tag$ dollar quotes) is combined with tree-sitter to locate statement nodes. When tree-sitter truncates complex SQL (Chinese paths, read_parquet(...), WITH/CTE, etc.), the lexer result is preferred so half-statements are not executed.
Statements must be separated by ;. Multiple statements separated only by blank lines are not split (treated as one block).
SELECT 1;
SELECT 2;With the cursor on the second line, Run executes only SELECT 2 and highlights that statement.
WITH a AS (SELECT * FROM t)
SELECT * FROM a;With the cursor on the outer SELECT, the full outer statement including WITH is highlighted and executed (not just the inner CTE subquery).
The {} button (next to EXPLAIN) scans the current selection (or the full buffer if no selection) for {{ name }} placeholders, then inserts or updates a /* @vars */ block with empty values for missing variables. Edit the values directly and run — no dialog.
Mod+B— toggle sidebarMod+/— keyboard shortcuts helpMod+Enter— run SQL (split off: full buffer; split on: current statement)Mod+Shift+Enter— run SQL in a new tabShift+Alt+F— format documentMod+KthenMod+F— format selectionMod+W— close tabF2— rename connectionF3— connection propertiesF4— open SQL editorF5— refreshDelete— delete connection
(Mod = Ctrl on Windows/Linux, Cmd on macOS.)
- Node.js and pnpm (the repo uses
pnpm-lock.yaml;npm/yarnwill fail). - Rust toolchain for Tauri (
cargo).
pnpm install # install dependencies (pnpm only)
pnpm dev # start the Vite dev server (http://localhost:5173)
pnpm tauri dev # run the Tauri app in development
pnpm build # build the frontend (Vite -> dist)
pnpm tauri build # build the installable app
pnpm test # run the Vitest test suite
pnpm lint # type-check (tsc)
pnpm i18n:extract # extract i18n messages (Lingui)
pnpm i18n:compile # compile i18n catalogs (Lingui)Optional: to use the shandy-sqlfmt SQL formatting engine, install it with uv tool install shandy-sqlfmt.
Tauri 2 (Rust workspace) · React 19 + TypeScript + Vite · Monaco editor · VisActor VTable (Apache Arrow transport) · Zustand + Jotai · Tailwind CSS v4 · Lingui i18n (English / Simplified Chinese) · cross-platform (Windows / macOS / Linux).
On Windows, DuckDB requires the Microsoft Visual C++ Redistributable at runtime. If DuckDB-related features misbehave, this dependency is the likely cause — download and repair it. See Building DuckDB on Windows.

