Search Shortcut cmd + k | ctrl + k
differential

Versioned DuckDB databases - snapshots, time travel, branches, clones and S3 export over native storage

Maintainer(s): tobilg

Compatibility Matrix

This table covers all DuckDB releases that have not reached end-of-life, including LTS releases, and shows the 2 most recent patch releases of each. Older patch releases are not listed, but the extension may be available for them as well.

Platform 1.4.4 1.4.5 1.5.5 1.5.6 2.0.0
Linux AMD64
Linux ARM64
macOS AMD64
macOS ARM64
Windows AMD64
Windows ARM64
WebAssembly

Installing and Loading

INSTALL differential FROM community;
LOAD differential;

Example

-- Run in order in one DuckDB session, using fresh demo paths and database names.
-- Replace the absolute paths below; the S3 section also needs an existing bucket
-- and AWS credentials (environment variables, a profile, SSO or an instance role).
INSTALL differential FROM community;
LOAD differential;
SET differential_repository = '/absolute/path/differential-demo/repository';

-- LOCAL STORAGE: create, snapshot, read history, branch, clone, restore and clean up.
-- TYPE differential secrets hold storage defaults, never credentials.
CREATE SECRET local_storage (
    TYPE differential,
    DATA_PATH '/absolute/path/differential-demo/local-data'
);
ATTACH 'differential:sales' AS sales (SECRET local_storage);
CREATE TABLE sales.orders (id INTEGER PRIMARY KEY, amount INTEGER);
INSERT INTO sales.orders VALUES (1, 10), (2, 20);
CALL differential_create_snapshot('sales', name => 'baseline',
    message => 'initial load', request_id => 'sales-baseline'); -- snapshot 1
INSERT INTO sales.orders VALUES (3, 30);
CALL differential_create_snapshot('sales', name => 'three-orders'); -- snapshot 2

SELECT sum(amount) FROM sales.orders;                       -- 60
SELECT sum(amount) FROM sales.orders AT (VERSION => 1);     -- 30
ATTACH 'differential:sales' AS sales_history (SNAPSHOT_NAME 'baseline');
SELECT count(*) FROM sales_history.orders;                  -- 2
SELECT snapshot_id, snapshot_name FROM sales.snapshots();
DETACH sales_history;

-- A branch shares the database identity; a clone is an independent database.
CALL differential_create_branch('sales', 'dev', snapshot => 1);
ATTACH 'differential:sales' AS sales_dev (BRANCH 'dev');
INSERT INTO sales_dev.orders VALUES (4, 40);
CALL differential_create_snapshot('sales_dev', message => 'dev order'); -- snapshot 3
SELECT sum(amount) FROM sales_dev.orders;                   -- 70
DETACH sales_dev;
CALL differential_clone_database('sales', 'sales_copy', snapshot => 1);
ATTACH 'differential:sales_copy' AS sales_copy;
SELECT sum(amount) FROM sales_copy.orders;                  -- 30
DETACH sales_copy;

-- Restore requires a detached branch and preserves its previous contents.
-- DETACH also releases the hidden reader pins taken by AT queries.
DETACH sales;
CALL differential_restore('sales', 1);                      -- restored snapshot 4
ATTACH 'differential:sales' AS sales;
SELECT sum(amount) FROM sales.orders;                       -- 30
DETACH sales;
CALL differential_drop_branch('sales', 'dev');
CALL differential_expire_snapshots('sales', versions => [2, 3], dry_run => true);
CALL differential_expire_snapshots('sales', versions => [2, 3]);
CALL differential_cleanup_old_files(cleanup_all => true);
CALL differential_delete_orphaned_files(dry_run => true);
CALL differential_compact();
-- The main branch, baseline snapshot and independent clone remain usable.

-- S3 STORAGE: credentials, exports, history, remote reads and recovery.
-- Replace the bucket/prefix and region. The bucket must already exist.
INSTALL httpfs;
LOAD httpfs;
INSTALL aws;
LOAD aws;
-- TYPE s3 supplies credentials by matching SCOPE; these secrets are session-scoped.
-- REFRESH auto keeps temporary credentials current through the credential chain.
CREATE SECRET s3_auth (
    TYPE s3,
    PROVIDER credential_chain,
    REFRESH auto,
    REGION 'us-east-1',
    SCOPE 's3://my-bucket/differential-demo/'
);
CREATE SECRET s3_storage (
    TYPE differential,
    DATA_PATH 's3://my-bucket/differential-demo/'
);
ATTACH 'differential:archive' AS archive (SECRET s3_storage);
CREATE TABLE archive.events (id INTEGER PRIMARY KEY, payload VARCHAR);
INSERT INTO archive.events VALUES (1, 'one'), (2, 'two');
SELECT snapshot_id, export_state, export_locator
FROM differential_create_snapshot('archive', name => 'baseline',
    request_id => 'archive-baseline');                     -- snapshot 1, complete
-- Save the immutable export URI for recovery later in this session.
SET VARIABLE baseline_export = (
    SELECT 's3://my-bucket/differential-demo/' || export_locator
    FROM differential_snapshots('archive') WHERE snapshot_id = 1
);

-- Subsequent exports share unchanged data. Deferred exports can be retried safely.
INSERT INTO archive.events VALUES (3, 'three');
CALL differential_create_snapshot('archive', name => 'three-events',
    export_mode => 'deferred');                            -- snapshot 2, pending
CALL differential_retry_export('archive', 2);              -- snapshot 2, complete
SELECT snapshot_id, export_state FROM archive.snapshots();
SELECT count(*) FROM archive.events AT (VERSION => 1);     -- 2

-- Any host with the same scoped S3 credentials can open this branch head read-only.
-- It sees the latest exported snapshot at ATTACH time; reattach to refresh it.
ATTACH 'differential:s3://my-bucket/differential-demo/heads/archive/main' AS archive_latest;
SELECT count(*) FROM archive_latest.events;                 -- 3
DETACH archive_latest;
SELECT database_name, branch_name, snapshot_id
FROM differential_remote_heads('s3://my-bucket/differential-demo/');
DETACH archive;

-- Evict only an export-complete snapshot. Historical reads then fetch its data from S3.
CALL differential_evict_snapshots('archive', versions => [1]);
CALL differential_cleanup_old_files(cleanup_all => true);
ATTACH 'differential:archive' AS archive_history (SNAPSHOT_VERSION 1);
SELECT count(*) FROM archive_history.events;                -- 2
DETACH archive_history;

-- Recover the saved immutable export into a new writable LOCAL database.
-- The named differential secret chooses its destination; s3_auth reads the source.
CALL differential_recover_export(getvariable('baseline_export'), 'archive_recovered',
    secret => 'local_storage');
ATTACH 'differential:archive_recovered' AS archive_recovered;
SELECT count(*) FROM archive_recovered.events;              -- 2
INSERT INTO archive_recovered.events VALUES (9, 'recovered locally');
DETACH archive_recovered;

-- Branch/clone/restore work with S3 too; evicted data is rehydrated when needed.
-- Branches and clones inherit the source's S3 data path.
CALL differential_create_branch('archive', 'dev', snapshot => 1);
ATTACH 'differential:archive' AS archive_dev (BRANCH 'dev');
INSERT INTO archive_dev.events VALUES (4, 'dev');
CALL differential_create_snapshot('archive_dev');           -- snapshot 3, complete
DETACH archive_dev;
CALL differential_clone_database('archive', 'archive_copy', snapshot => 1);
ATTACH 'differential:archive_copy' AS archive_copy;
SELECT count(*) FROM archive_copy.events;                   -- 2
DETACH archive_copy;
CALL differential_restore('archive', 1);                    -- snapshot 4, complete
ATTACH 'differential:archive' AS archive;
SELECT count(*) FROM archive.events;                        -- 2
DETACH archive;

CALL differential_drop_branch('archive', 'dev');
CALL differential_expire_snapshots('archive', versions => [2, 3], dry_run => true);
CALL differential_expire_snapshots('archive', versions => [2, 3]);
CALL differential_cleanup_old_files(cleanup_all => true);
CALL differential_compact();
-- Expiry and garbage collection reclaim unreferenced LOCAL data, not S3 objects.
-- Existing immutable S3 exports remain readable; the main branch and clone remain.
SELECT database_name, data_path FROM differential_databases();
SELECT kind, state FROM differential_operations();
DROP SECRET s3_storage;
DROP SECRET s3_auth;
DROP SECRET local_storage;

About differential

differential stores ordinary DuckDB databases in a durable, content-addressed differential store, so every version shares unchanged data. Native DuckDB SQL, transactions and indexes are unchanged; the extension adds immutable snapshots with commit metadata, AT (VERSION/TIMESTAMP) time travel, writable branches, independent clones, restore, expiry with garbage collection, and incremental, content-addressed S3 export that any host can attach read-only, including following a branch's latest snapshot through S3 branch heads. Several processes, and optionally several local users, can share one repository.

S3 databases can also:

  • ship every commit to S3 continuously (SHIP_WAL 'async' | 'sync'), so another host can recover the branch with differential_recover_branch;
  • be followed by read-only replicas that move to each new commit on their own (FOLLOW true on a branch head);
  • be written by several hosts (WRITERS 'shared'), with optimistic, conflict-checked publication through conditional S3 writes;
  • have unreachable S3 data collected (differential_cleanup_remote_files).

The SQL surface follows DuckLake's conventions: ATTACH 'differential:<db>' options (BRANCH, SNAPSHOT_VERSION, SNAPSHOT_TIME, SNAPSHOT_NAME, DATA_PATH), SET differential_* settings and database-first differential_* functions.

Built for DuckDB 2.0. Version 1.0.0 uses store format 2 and refuses repositories and exports written by 0.1.0; there is no migration.

Status: experimental. Linux and macOS only. Commits are durable at DuckDB's sync points; exports are durable once complete. S3 is tested against MinIO and exercised on AWS; power-loss behaviour is not yet verified. See the repository for the SQL reference, the lifecycle guide and the storage format.

Added Functions

function_name function_type description comment examples
differential_branches table Lists the branches of a differential database with their head snapshot and, for shared databases, their state relative to the S3 branch head. NULL [SELECT branch_name, head_snapshot_id, remote_state FROM differential_branches('sales');]
differential_claim_branch_heads table Takes a single-writer database's S3 branch heads over after a copy of the repository wrote them. NULL [SELECT branch_name, snapshot_id FROM differential_claim_branch_heads('sales');]
differential_cleanup_old_files table Collects local data that no snapshot, branch, pin or pending export references any more. NULL [SELECT deleted_objects FROM differential_cleanup_old_files(cleanup_all => true);]
differential_clone_database table Creates an independent database from a snapshot of another one, on the same data path and sharing its data. NULL [SELECT database_id FROM differential_clone_database('sales', 'sales_copy');]
differential_compact table Returns free pages of the repository's metadata store to the file system. NULL [SELECT pages_reclaimed FROM differential_compact(max_pages => 1000);]
differential_create_branch table Creates a writable branch from a snapshot, sharing all data with it. NULL [SELECT branch_id FROM differential_create_branch('sales', 'dev', snapshot => 1);]
differential_create_snapshot table Checkpoints an attached writable branch and publishes it as an immutable snapshot with optional commit metadata; exports it to S3 and, for shared databases, moves the S3 branch head. NULL [SELECT snapshot_id, export_state FROM differential_create_snapshot('sales', name => 'v1', message => 'first load');]
differential_current_snapshot table Returns the snapshot an attachment reads, or the latest snapshot of the attached branch or database. NULL [SELECT id FROM differential_current_snapshot('sales');]
differential_databases table Lists the databases of the differential repository with their data path and writer mode. NULL [SELECT database_name, data_path, writers FROM differential_databases();]
differential_delete_orphaned_files table Removes files in local pools that no mapping references, left behind by crashes. NULL [SELECT orphaned_files FROM differential_delete_orphaned_files(dry_run => true);]
differential_drop_branch table Preserves a detached branch's contents as a snapshot and drops the branch (in a shared database, for every writer). NULL [SELECT preservation_snapshot_id FROM differential_drop_branch('sales', 'dev');]
differential_evict_snapshots table Drops the local copy of snapshots whose S3 export completed; they are then read through S3. NULL [SELECT snapshot_id FROM differential_evict_snapshots('sales', versions => [1], dry_run => true);]
differential_expire_snapshots table Retires snapshots selected by version list and/or age so their local data can be collected; S3 exports remain. NULL [SELECT snapshot_id FROM differential_expire_snapshots('sales', versions => [1], dry_run => true);]
differential_import_database table Copies an offline native DuckDB file into a new differential database and publishes it as snapshot 1. NULL [SELECT database_id FROM differential_import_database('/data/native.duckdb', 'imported');]
differential_operations table Lists the durable operation records of the repository, used to replay requests by request_id. NULL [SELECT kind, state, snapshot_id FROM differential_operations();]
differential_providers table Lists the repository's storage locations (metadata store, local pools, S3 locations) with their S3 configuration and credential source, never credentials. NULL [SELECT data_path, credential_source FROM differential_providers();]
differential_recover_export table Copies an exported snapshot from S3 into a new writable differential database, verifying every chunk. NULL [SELECT database_id FROM differential_recover_export('s3://my-bucket/lake/descriptors/', 'restored');]
differential_remote_heads table Lists the branch heads exported to an S3 location, each with the path to ATTACH its latest snapshot. NULL [SELECT database_name, branch_name, head FROM differential_remote_heads('s3://my-bucket/lake/');]
differential_resolve_conflict table Reconciles a diverged branch of a shared database with its S3 head, keeping local work as a new branch or as a local snapshot. NULL [SELECT kept_snapshot_id, kept_branch FROM differential_resolve_conflict('team', keep => 'branch');]
differential_restore table Resets a detached branch to a snapshot after preserving its current contents as a snapshot. NULL [SELECT snapshot_id, preservation_snapshot_id FROM differential_restore('sales', 1, branch => 'dev');]
differential_retention_roots table Lists why each stored database image is retained (snapshots, branches, pins, exports). NULL [SELECT reason, identity FROM differential_retention_roots();]
differential_retry_export table Finishes pending S3 exports of a database (all, or one snapshot) and republishes its branch heads. NULL [SELECT snapshot_id, export_state FROM differential_retry_export('sales');]
differential_snapshots table Lists every snapshot of a differential database (for a shared database also other writers' snapshots, read from S3) with commit metadata, publication and S3 export state. NULL [SELECT snapshot_id, snapshot_name, export_state FROM differential_snapshots('sales');]
differential_status table Reports the extension's capabilities, durability profiles, limits and last background publication error as capability/status rows. NULL [SELECT capability, status FROM differential_status();]
differential_storage_stats table Returns storage, S3, cache and timing counters of the repository and process as metric/value rows. NULL [SELECT metric, value FROM differential_storage_stats() WHERE metric = 'local_payload_bytes';]
differential_writers table Lists the advisory 'being written' markers of a shared database in S3: which hosts are writing which branches. NULL [SELECT branch_name, host, live FROM differential_writers('team');]
differential_cleanup_metadata table NULL NULL NULL
differential_recover_branch table NULL NULL NULL
differential_cleanup_remote_files table NULL NULL NULL
differential_followers table NULL NULL NULL

Overloaded Functions

This extension does not add any function overloads.

Added Types

This extension does not add any types.

Added Settings

name description input_type scope aliases typed_value
differential_auto_gc Run bounded differential garbage collection after native checkpoints BOOLEAN GLOBAL [] true
differential_export_staging Where exports stage objects before uploading them: 'disk' (the repository's staging directory) or 'memory'. Process-wide VARCHAR GLOBAL [] disk
differential_export_verification How exports verify each uploaded object: 'checksum' (the S3 service checks its SHA-256) or 'read_back' (also download and hash it). Process-wide VARCHAR GLOBAL [] checksum
differential_group_commit Commit concurrent native syncs of different databases in one durable transaction (one device flush). Process-wide BOOLEAN GLOBAL [] true
differential_remote_cache_size Size of the repository's disk cache for data read from S3 (e.g. '4GB'); 0 = off. Read when a remote file is opened VARCHAR GLOBAL [] 0
differential_repository Differential repository root (relative paths resolve against the working directory); set before first use VARCHAR GLOBAL [] /home/runner/.duckdb/differential
differential_repository_shared Create the repository for several local users (group-writable); set before first use BOOLEAN GLOBAL [] false
differential_s3_max_requests Parallel S3 requests per process (1-64), shared by all S3 locations and DuckDB instances in the process BIGINT GLOBAL [] 16
differential_snapshot_on_checkpoint Publish every completed native checkpoint of a writable branch as a public snapshot BOOLEAN GLOBAL [] false
temp_directory Set the directory to which temporary files are written. VARCHAR GLOBAL [] post.db.tmp