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;

-- Merge dev back into main: a three-way merge against their common ancestor
-- (snapshot 1). main added order 3 and dev added order 4, so nothing conflicts.
SELECT base_snapshot_id, ours_snapshot_id, theirs_snapshot_id, conflicts, validation_state
FROM differential_merge_branch_preview('sales', 'main', 'dev', validate => true); -- 1, 2, 3, 0, valid
-- Merging into an existing branch (destination = ours) needs it detached.
-- DETACH also releases the hidden reader pins taken by AT queries.
DETACH sales;
SELECT snapshot_id, merge_action FROM differential_merge_branch('sales', 'main', 'dev', 'main',
    request_id => 'sales-merge-dev');                       -- snapshot 4, merged
ATTACH 'differential:sales' AS sales;
SELECT sum(amount) FROM sales.orders;                       -- 100
DETACH sales;

-- Restore also requires a detached branch and preserves its previous contents.
CALL differential_restore('sales', 1);                      -- restored snapshot 5
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, 4], dry_run => true);
CALL differential_expire_snapshots('sales', versions => [2, 3, 4]);
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).

Branches can be merged in local and single-writer S3 databases: a three-way merge against the best common ancestor that reconciles rows and cells by primary key (every table needs one):

  • differential_merge_branch merges one branch into another, either into a new branch or into the existing ours branch (a merge snapshot, fast-forward or already up to date); differential_merge_snapshots merges three explicit snapshots into a new branch;
  • differential_merge_branch_preview, differential_merge_snapshots_preview and differential_merge_branch_preflight report conflicts and readiness without writing; the reviewed snapshot IDs can guard the merge (expected_*_snapshot_id);
  • conflicts are resolved with explicit resolution_tables; schema_mode 'strict', 'additive' or 'catalog' controls which schema changes merge;
  • differential_merge_ancestry explains base selection, differential_merge_status shows running merges, differential_merge_requests and differential_merge_resolutions inspect durable, retryable requests, and differential_abort_merge abandons a pending one;
  • SET differential_merge_max_concurrent limits concurrent merges per process.

Shared (WRITERS 'shared') databases cannot be merged.

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. Store format 2 has been used since 1.0.0, which refuses repositories and exports written by 0.1.0; there is no migration. 1.1.0 opens 1.0.0 repositories, but once 1.1.0 has written to a repository, downgrading to 1.0.0 is not supported.

Status: experimental. Linux (amd64, arm64) and macOS (Apple silicon) 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, branch merging and the storage format.

Added Functions

function_name function_type description comment examples
differential_abort_merge table Aborts an unpublished merge by operation_id, releasing its target fence, reservation and retained result. Recovers a published S3 result instead; uncertain remote outcomes retain state. Serialized with merge attempts. NULL [SELECT * FROM differential_abort_merge('sales', 'merge-operation-uuid', request_id => 'abort-1');]
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_metadata table Deletes completed operation records and history entries of expired snapshots older than older_than; request IDs of deleted operations no longer replay. NULL [SELECT table_name, rows FROM differential_cleanup_metadata(older_than => now() - INTERVAL 30 DAY, dry_run => true);]
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_cleanup_remote_files table Garbage-collects an S3 location in two phases: condemns data no branch head or unexpired snapshot references, and deletes what stayed condemned and unreachable since before older_than (default: a week ago). NULL [SELECT condemned_objects, deleted_bytes FROM differential_cleanup_remote_files('s3://my-bucket/lake/', dry_run => 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_followers table Lists this process's followers (ATTACH … FOLLOW true): what each follows, the state it reads, when it last checked S3 and its last error. NULL [SELECT alias, follows, epoch, checked_at FROM differential_followers();]
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_merge_ancestry table Inspects recorded local snapshot parents and best common ancestor candidates, including expired history. Incomplete and ambiguous history is reported without guessing a base. NULL [SELECT * FROM differential_merge_ancestry('sales', 2, 3);]
differential_merge_branch table Merges published local or single-writer S3 branch heads using a unique proven best common ancestor. Destination equal to ours updates that detached, clean target; otherwise creates a new branch. Returns merge_action (created_branch, merged, fast_forward, already_up_to_date). Optional expected_base_snapshot_id/expected_ours_snapshot_id/expected_theirs_snapshot_id must be supplied together and guard the reviewed selection. The request records its input snapshot selection and schema_mode once; source live changes are excluded. schema_mode => 'additive' allows supported new tables and nullable column additions; 'catalog' also merges table/nullable-column removals and view definitions. NULL [SELECT * FROM differential_merge_branch('sales', 'ours', 'theirs', 'merged', request_id => 'merge-1');]
differential_merge_branch_preflight table Checks local or single-writer S3 branch merge readiness without publishing or recording a request. Returns selected input IDs, expected action, destination/target state, conflict counts and bounded diagnostics including pending recovery operations. Native final-state validation defaults to true. Checks are advisory; use expected snapshot IDs when executing. NULL [SELECT * FROM differential_merge_branch_preflight('sales', 'ours', 'theirs', 'ours');]
differential_merge_branch_preview table Previews published branch heads using a unique proven best common ancestor. Returns resolved input IDs, counts and bounded typed conflicts; validate => true checks final constraints. schema_mode => 'additive' allows supported new tables and nullable column additions; 'catalog' also merges table/nullable-column removals and view definitions. Catalog conflicts are reported separately. NULL [SELECT * FROM differential_merge_branch_preview('sales', 'ours', 'theirs', validate => true);]
differential_merge_requests table Inspects a bounded page of durable merge requests without S3 access. Includes inputs, destination, retained result, frozen schema policy, original branch identities, input guards, target fences and recovery guidance. Latest-attempt diagnostics include a count, phase, safe error code and timestamps; incomplete does not imply liveness. Ordered by operation_id; after_operation_id is an exclusive pagination cursor. NULL [SELECT * FROM differential_merge_requests('sales', request_id => 'merge-1');]
differential_merge_resolutions table Reads the frozen typed resolution choices of a merge request. Does not read mutable resolution source tables. NULL [SELECT * FROM differential_merge_resolutions('sales', 'merge-1');]
differential_merge_snapshots table Merges three explicit snapshots into a new branch after native constraint validation and checkpointing. Supports local and single-writer S3 storage; S3 publication is synchronous and recoverable. Conflicts prevent publication. schema_mode => 'catalog' also merges table/nullable-column removals and view definitions; 'additive' only allows supported table/column additions. The default is strict. NULL [SELECT * FROM differential_merge_snapshots('sales', 1, 2, 3, 'merged', request_id => 'merge-1');]
differential_merge_snapshots_preview table Compares three explicit snapshots of a local or single-writer S3 database using typed, cell-level merge rules. Returns per-table counts and byte-bounded typed conflict details; validate => true checks final constraints in disposable staging. schema_mode => 'additive' allows supported new tables and nullable column additions; 'catalog' also merges table/nullable-column removals and view definitions. Catalog conflicts are reported separately. NULL [SELECT * FROM differential_merge_snapshots_preview('sales', 1, 2, 3);]
differential_merge_status table Lists active merges and previews in this process and repository, with phase, progress and resource counters. Durable request outcomes are in differential_merge_requests(). NULL [SELECT phase, tables_completed, tables_total FROM differential_merge_status();]
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_branch table Recovers the newest shipped state of a branch (SHIP_WAL) from S3 into a new local database: its chain's base, read on demand, plus every shipped epoch; DuckDB replays the WAL at the first ATTACH. same_identity => true keeps the original identities (a failover of the writer). NULL [SELECT database_id, shipped_epoch FROM differential_recover_branch('s3://my-bucket/lake/heads/sales/main', 'sales_recovered');]
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');]

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_merge_max_concurrent Concurrent merge/preview/abort executions per process (1-16); excess calls fail with merge_busy BIGINT GLOBAL [] 2
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
© 2026 DuckDB Foundation, Amsterdam NL
DuckDB Home Code of Conduct Trademark Use Blog