Why 2.0 Is Not Just Another Feature Batch
The defining word of DuckDB 1.0 was stability and backward compatibility, and 1.5 laid the groundwork with the Quack protocol, VARIANT, and agent observability. DuckDB 2.0 LTS (codename “Cinnamon Teal”, after the Spatula cyanoptera, a reddish-brown duck of the western Americas) ships close to 20,000 commits since 1.5, and the team frames it as a feature release, not a patch.
A useful test for whether a release write-up is “deep” is not “how many features are listed”, but whether you can say which layer of the database each feature changes. The real shift in 2.0 is that DuckDB moves from an in-process analytical engine to a database that can run in embedded, server, and remote forms at the same time:
- Serving: the Quack protocol +
CONNECTlet any DuckDB process serve its databases, and it can also reach into PostgreSQL/MySQL with remote pushdown - Asynchronous I/O: the read layer decouples from the query layer, sharply improving throughput on object storage such as
s3:// - VARIANT as a first-class type: semi-structured data is “shredded” for columnar execution, so JSON performance now rivals native columns
- A pluggable parser: a PEG parser becomes the default, and extensions can define new SQL syntax
- Triggers:
CREATE TRIGGERfinally fills the last gap for audit tables and incremental materialized-view maintenance - Agent mode: when the CLI detects it is being driven by a coding agent, it switches to compact output and cut the text an LLM has to read by 59% on TPC-H
Below, each feature is broken down, with runnable code (all verified on the DuckDB 2.0.0 CLI).
1. Serving: the Quack Protocol and CONNECT
From day one, DuckDB was an in-process database. Since 1.5.3 introduced the Quack protocol, 2.0 polishes it into a first-class capability — any DuckDB process can serve its databases with quack_serve, and another process attaches with ATTACH ... 'quack:localhost'.
Server side (serve + write):
CALL quack_serve (token = 'my_token');
CREATE TABLE events (date DATE, name VARCHAR);
INSERT INTO events VALUES ('2027-04-07', 'DuckCon #8');
Client side (attach + switch + query):
ATTACH 'quack:localhost' AS qk (TOKEN 'my_token');
CONNECT qk;
SELECT * FROM events;
| date | name |
|---|---|
| 2027-04-07 | DuckCon #8 |
Security note: it listens on localhost by default. For cross-machine access, keep the server on localhost and put it behind a TLS-terminating reverse proxy;
allow_other_hostnameis for testing only.
The value of CONNECT is not limited to Quack — it points your session at any remote database that speaks the protocol, including PostgreSQL and MySQL. For those targets, the new remote pushdown optimizer ships the query to the remote server instead of pulling whole tables over the wire. That one step widens DuckDB from a “local analytical engine” into something that can do federated queries across data sources, which matters for multi-tenant SaaS and hybrid-data setups (further reading: DuckDB v2.0 Server Mode).
The CLI also has shortcuts: duckdb -serve to start a server, duckdb -connect to attach, saving you from hand-writing quack_serve.
2. Performance: Asynchronous I/O
DuckDB 2.0 adds asynchronous I/O for reading Parquet and CSV files as well as its native storage format: the I/O layer no longer shares the query layer’s thread pool, so reads run in the background with several in flight at once. On local disks it is incremental; on high-latency object storage such as s3:// it is a large gain — higher network-throughput utilization and shorter query runtimes.
The design is covered in depth in a dedicated engineering blog post (with before/after throughput curves); the key thing to remember is: when you read large remote files, 2.0’s async I/O is free performance you should be picking up.
3. VARIANT Becomes First-Class
VARIANT landed in 1.5, but 2.0 fully integrates it: it stores schema-less, semi-structured data (typically JSON) while keeping columnar execution — think of it as “JSON on steroids”. Each row can hold a different shape, but it is not a text format.
The core mechanism is shredding: 2.0 detects the common structure in your data and breaks it apart into columnar storage, so it compresses well and executes fast. The pipeline runs end to end — from shredded execution out of storage, to shredded VARIANT read/write in Parquet, backed by a family of variant_* functions:
CREATE TABLE events (payload VARIANT);
INSERT INTO events VALUES
('{"user": {"id": 42, "tags": ["a","b"]}}'::JSON::VARIANT),
('{"event":"click","x":10,"y":20}'::JSON::VARIANT);
SELECT variant_type(payload) AS t, variant_keys(payload) AS k FROM events;
| t | k |
|---|---|
| OBJECT | [user] |
| OBJECT | [event, x, y] |
For anyone doing event streams, telemetry, or semi-structured log analysis, this means: you no longer have to give up columnar performance for JSON flexibility.
4. Agent Mode: Designed for Coding Agents
This is the most “era-defining” feature. When the CLI detects it is being driven by a coding agent such as Claude Code, Codex, or Cursor, it switches to agent mode:
- compact Markdown tables instead of padded boxes
- explicit notes when a result was truncated
- early termination of runaway queries
- errors reported as JSON
- long-running queries report their estimated cost up front, so the agent can decide whether to wait
The team measured on TPC-H that agent mode cut the amount of DuckDB output the model had to read by 59%. You can test it manually with -agent, or tell an agent to turn it off with -no-agent:
duckdb -agent :memory: "SELECT 42 AS x;"
The measured output is the compact form:
| x:INTEGER |
|---|
| 42 |
This is consistent with DuckDB’s direction as a data interface for AI (further reading: Building an AI Agent Brain with DuckDB, DuckDB MCP Server Guide).
5. Extensibility: A Brand-New PEG Parser
DuckDB has always used a parser derived from PostgreSQL. In 2.0, the team’s own modern PEG parser (an opt-in feature in 1.5) becomes the default. Extensions can now hook into the grammar itself — the team expects extensions that expose entirely new SQL syntax. Errors now carry precise source locations.
Why the switch? The old parser was generated by YACC/Bison into an LALR(1) grammar, so as DuckSQL grew, even small additions risked colliding with existing rules and raising shift/reduce or reduce/reduce conflicts. A PEG grammar tries alternatives in written order, so adding syntax no longer risks breaking unrelated rules (Python 3.9 moved to PEG for the same reason). The new parser is compatible with the old one; if something regresses, SET enable_legacy_parser = true; switches it back (verified present and functional on 2.0).
Write Your Own Extensions and Host Them Yourself
The revamped C API v2 is considerably more powerful, exposing components such as file systems and table functions to extensions. You can also register and host trusted extension repositories — an organization can sign its own extensions and install/load them just like built-ins:
SET allow_extension_repositories = 'allowed';
CREATE EXTENSION REPOSITORY my_repo WITH PREFIX 'https://extensions.example.org';
INSTALL AND LOAD my_ext FROM my_repo;
At CREATE time DuckDB fetches the repository’s public keys and pins them into the repository definition, printing each key’s SHA-256 fingerprint so you can compare against one published out of band. This feature is still evolving and its syntax may change.
6. Core Extension Updates
2.0 ships a batch of updates and performance work across the core extensions; the production-relevant highlights:
- DuckLake 1.1: new specification plus reference implementation
- Iceberg: moving toward feature-complete, with new REST-catalog ATTACH options (derive a table location from its namespace, or read metadata from the catalog’s metadata-location file)
- Delta:
CREATE TABLE(including partitioning and Delta table properties), time travel, and improved VARIANT support
7. Triggers: Audit Tables and Incremental Materialized Views
2.0 delivers CREATE TRIGGER, with BEFORE/AFTER, FOR EACH ROW / FOR EACH STATEMENT, transition tables via REFERENCING OLD/NEW TABLE, multiple triggers per event, RETURNING on triggered tables, and DROP TRIGGER. The classic use case is audit tables — recording what changed in response to a table modification:
CREATE TRIGGER trg_audit AFTER UPDATE ON target
REFERENCING OLD TABLE AS o NEW TABLE AS n FOR EACH STATEMENT
INSERT INTO audit SELECT n.id, o.val, n.val FROM o JOIN n ON o.id = n.id;
Better still, triggers can incrementally maintain materialized views. The following pair (one INSERT, one DELETE) maintains an aggregate of average age per nationality:
CREATE TABLE people (name VARCHAR, nationality VARCHAR, age INTEGER);
CREATE TABLE age_statistics (nationality VARCHAR PRIMARY KEY,
sum_age INTEGER, num_people INTEGER, average_age DOUBLE);
CREATE TRIGGER people_insert AFTER INSERT ON people
REFERENCING NEW TABLE AS n FOR EACH STATEMENT
MERGE INTO age_statistics s USING
(SELECT nationality, sum(age) AS sum_age, count(*) AS num_people
FROM n GROUP BY nationality) d
ON s.nationality = d.nationality
WHEN MATCHED THEN UPDATE SET sum_age = s.sum_age + d.sum_age,
num_people = s.num_people + d.num_people,
average_age = (s.sum_age + d.sum_age) / (s.num_people + d.num_people)
WHEN NOT MATCHED THEN INSERT VALUES (d.nationality, d.sum_age, d.num_people, d.sum_age/d.num_people);
Measured result: after inserting 6 people, the aggregate table auto-generates:
| nationality | sum_age | num_people | average_age |
|---|---|---|---|
| HU | 45 | 1 | 45.0 |
| NL | 87 | 3 | 29.0 |
| US | 92 | 2 | 46.0 |
The team keeps stressing that triggers bring DuckDB one step closer to materialized views, a long-standing roadmap item (a few combinations such as BEFORE … FOR EACH ROW are not yet supported; see the official limitations).
8. SQL Dialect Additions
Practical additions to the dialect in 2.0:
$variable shorthand: write$xin most expression positions instead ofgetvariable('x')- Data-modifying CTEs:
INSERT/UPDATE/DELETE/COPYas pipeline steps - NEAREST joins: turn top-k similarity search into a join clause, which helps vector/embedding workloads
- Nested schemas:
CREATE SCHEMA finance.reports;— schemas within schemas - JSON mutation functions:
json_set/json_insert/json_replace/json_removereturn modified copies of JSON documents (see JSON Patch Functions in Practice)
Nested schema and JSON mutation, measured on the 2.0 CLI:
CREATE SCHEMA finance;
CREATE SCHEMA finance.reports;
SHOW SCHEMAS;
-- memory | finance / main / reports, three rows
SELECT json_set('{"a":1,"b":2}'::JSON, '$.c', '3'::JSON),
json_remove('{"a":1,"b":2}'::JSON, '$.b');
-- {"a":1,"b":2,"c":3} {"a":1}
Also: 2.0 supports the SQL-standard FETCH FIRST, and allows UNNEST in the GROUP BY clause; where multiple rows match, MERGE and UPDATE ... FROM now have well-defined semantics.
9. Migrating to 2.0: Breaking Changes and Caveats
Upgrading is straightforward for most users, but multi-process, production deployments should watch three things:
Storage format (the most likely footgun)
Versions before 2.0 cannot read database files created by 2.0; 2.0 itself can still read and write older files, and those remain readable by older versions. Test 2.0.0 on a shadow system before switching production; for mission-critical workloads, wait for the first November patch release.
Compiler requirements
After a long stint on C++11, 2.0 now requires a C++17 compiler; release builds switch to Clang by default (on Linux/macOS both DuckDB and its core extensions build with Clang; on Windows the CLI uses Clang while most core extensions still use MSVC).
Timezones, calendars, collations: ICU removed
2.0 drops the ICU library entirely — the icu extension now implements timezones, calendars, and collations itself, with timezone data built straight from the IANA database and compressed to roughly 45 kB. Everything behaves as before, and the native implementation is faster.
Deprecations
Old single-arrow lambda syntax (
x -> x + 1) is now disabled by default; use the Python-style form:SELECT list_transform([1, 2, 3], lambda x : x + 1); -- [2, 3, 4] -- The old syntax errors on 2.0: -- Binder Error: Deprecated lambda arrow (->) detected ... -- Re-enable temporarily with SET lambda_syntax='ENABLE_SINGLE_ARROW'; removed in v2.1Amalgamation builds (bundling the whole source into one header + one .c) are deprecated — they have grown too large to be practically useful
macOS x86_64 is being phased out: it is still built and supported for now, but community extensions no longer build for it, and v2.1 will stop supporting it
The Three LTS Lines
| Version | Supported until | Notes |
|---|---|---|
| 1.4 | late Nov 2026 | previous LTS |
| 1.5 | Mar 2027 | extended, effectively an LTS |
| 2.0 | Oct 2027 | new LTS, Cinnamon Teal |
Summary and Next Steps
The core of 2.0 is not “X more functions”; it is an expansion of the database’s form: embedded, server, and remote runtimes coexist; VARIANT + triggers + async I/O let an “analytical” database genuinely do “transactional” work; the PEG parser + C API v2 open up customization for extension authors.
- To land 2.0 serving in production: Server Mode Architecture
- To wire up AI workflows: AI Agent Brain, MCP Server Guide
- To get started: Complete Guide, Cheatsheet