A technical breakdown of intelligent query result caching and architectural cost optimization.
DuckSync is a DuckDB extension that provides intelligent query result caching between DuckDB and Snowflake. It uses DuckLake for storage (PostgreSQL catalog + Parquet files) and supports explicit Quack serving, two-stage freshness checks, and transparent cached-table reads for DuckDB-native clients.
- Quack Server Lifecycle: Start and stop a DuckDB Quack endpoint with
ducksync_serve(...)andducksync_stop(...) - Transparent Query Routing:
ducksync_query(...)routes fully cached queries to local DuckLake data - Transparent Cached-Table Reads: After a cache is refreshed,
SELECT * FROM orderscan resolve directly toorders_cache - Two-Stage Invalidation: Use
invalidation_mode = 'two_stage'withmetadata_secretto skip warehouse-backed checks whenSHOW TABLESrows/bytes are unchanged - Smart Refresh: Refreshes only when source tables have changed
- TTL Support: Configurable cache expiration with time-to-live
- DuckLake Storage: Uses DuckLake for efficient Parquet-based storage
- PostgreSQL Catalog: Metadata stored in DuckLake's PostgreSQL catalog
DuckSync automatically installs the required extensions it needs on demand: DuckLake and Snowflake for storage/querying, and Quack when you start a server with ducksync_serve(...).
You'll need:
- PostgreSQL database - for DuckLake catalog storage
- Snowflake account - with a configured DuckDB secret
- ADBC Snowflake driver - native library for Snowflake connectivity
- Create a DuckDB secret with your Snowflake credentials
- Install the ADBC Snowflake driver - see ADBC Driver Setup
For more details: Snowflake Extension Docs
INSTALL ducksync FROM community;
LOAD ducksync;If you already have DuckLake attached:
-- Your existing DuckLake setup
ATTACH 'ducklake:postgres:host=localhost dbname=mydb...' AS my_lake (DATA_PATH '/data');
-- Initialize DuckSync with your existing catalog
SELECT * FROM ducksync_init('my_lake');
-- Register Snowflake source (using your existing secret)
SELECT * FROM ducksync_add_source('prod', 'snowflake', 'my_snowflake_secret');
-- Create a cache
SELECT * FROM ducksync_create_cache(
'sales_summary',
'prod',
'SELECT region, SUM(amount) as total FROM sales GROUP BY region',
['PROD.PUBLIC.SALES'],
3600
);
-- Refresh the cache
SELECT * FROM ducksync_refresh('sales_summary');
-- Query via ducksync_query (smart routing)
SELECT * FROM ducksync_query('SELECT * FROM PROD.PUBLIC.SALES WHERE region = ''US''', 'prod');
-- Or read the cached table transparently once refreshed
SELECT * FROM SALES;If you don't have DuckLake configured yet:
-- Setup DuckLake + DuckSync in one step
SELECT * FROM ducksync_setup_storage(
'host=localhost port=5432 dbname=ducklake user=postgres password=secret',
'/data/ducksync'
);
-- Then continue with add_source, create_cache, etc.Initialize DuckSync with an existing DuckLake catalog. Recommended approach.
Parameters:
catalog_name: Name of your attached DuckLake catalog
Example:
SELECT * FROM ducksync_init('my_ducklake');Full setup - attaches DuckLake and initializes DuckSync. Use if you don't have DuckLake configured yet.
Parameters:
pg_connection_string: PostgreSQL connection string (libpq format)data_path: Local path or S3 path for Parquet data files
Start a Quack listener explicitly for DuckDB-native clients.
Example:
SELECT * FROM ducksync_serve('quack:localhost');
SELECT * FROM ducksync_serve('quack:localhost', token := 'mytoken');Then connect from another DuckDB client with Quack using the same listen URI.
Stop a previously started Quack listener.
Example:
SELECT * FROM ducksync_stop('quack:localhost');Register a Snowflake data source.
Parameters:
source_name: Unique identifier for the sourcedriver_type: Currently only'snowflake'secret_name: Name of existing DuckDB secret with Snowflake credentialspassthrough_enabled(optional): Allow passthrough for uncached tables (default: false)
Define a cached query result.
Parameters:
cache_name: Unique identifier (used in SQL queries)source_name: Source to execute query againstsource_query: SQL query to cache results frommonitor_tables: List of tables to monitor for changes (e.g.,['DB.SCHEMA.TABLE'])ttl_seconds(optional): Cache TTL in seconds (NULL = no expiration)invalidation_mode(named, optional):last_altered,two_stage,ttl_only, ormanualmetadata_secret(named, required fortwo_stage): no-warehouse Snowflake secret for Stage 1SHOW TABLES
Two-stage invalidation example:
SELECT * FROM ducksync_create_cache(
'orders_cache',
'prod',
'SELECT * FROM ORDERS',
['DUCKSYNC_TEST.TEST_DATA.ORDERS'],
invalidation_mode := 'two_stage',
metadata_secret := 'sf_meta'
);Refresh a cache with smart check logic.
Parameters:
cache_name: Cache to refreshforce(optional): Skip smart check and force refresh (default: false)
Returns:
result: SKIPPED, REFRESHED, or ERRORmessage: Status messagerows_refreshed: Number of rows (if refreshed)duration_ms: Refresh duration in milliseconds
The main query interface. Executes queries with smart routing - returns actual data (not status messages).
Parameters:
sql_query: SQL query to execute (use Snowflake table names)source_name: Source to use for execution
Routing Logic:
- Parses SQL using DuckDB's parser to extract all table references
- Checks if each table is cached (by cache name or monitored table)
- All tables cached → Rewrites query to use local DuckLake tables
- Any table not cached → Passes entire query to Snowflake
Examples:
-- Cache hit via monitored table name → executes locally
SELECT * FROM ducksync_query(
'SELECT * FROM PROD.PUBLIC.CUSTOMERS WHERE region = ''US''',
'prod'
);
-- Cache hit via named query (use cache_name directly)
-- Useful for complex queries cached under a friendly name
SELECT * FROM ducksync_query(
'SELECT * FROM customers_cache LIMIT 10',
'prod'
);
-- Table NOT cached → passthrough to Snowflake
SELECT * FROM ducksync_query(
'SELECT * FROM PROD.PUBLIC.RAW_ORDERS LIMIT 100',
'prod'
);
-- JOIN with mixed tables (1 cached, 1 not) → passthrough to Snowflake
SELECT * FROM ducksync_query(
'SELECT c.*, o.total FROM CUSTOMERS c JOIN ORDERS o ON c.id = o.customer_id',
'prod'
);
-- Complex aggregation from cache
SELECT * FROM ducksync_query(
'SELECT region, COUNT(*) as cnt FROM PROD.PUBLIC.CUSTOMERS GROUP BY region',
'prod'
);Named Queries: You can query by cache name directly (e.g., customers_cache) instead of the Snowflake table name. This is useful for caching complex joins or aggregations under a friendly name that doesn't exist as a table in Snowflake.
Best Practice: Create caches with monitor_tables matching your Snowflake table names for automatic routing.
Once a cache has been refreshed, DuckSync also installs a ReplacementScan hook for DuckDB-native reads:
SELECT * FROM ducksync_create_cache(
'orders_cache',
'prod',
'SELECT * FROM ORDERS',
['DUCKSYNC_TEST.TEST_DATA.ORDERS']
);
SELECT * FROM ducksync_refresh('orders_cache');
-- Transparent cache hit
SELECT * FROM ORDERS;
SELECT * FROM main.orders;
SELECT * FROM TEST_DATA.ORDERS;Naming convention: keep the source table name and cache table name distinct, e.g. orders routes to orders_cache.
Miss behavior: if the monitored table has not been refreshed yet, DuckSync raises an explicit error telling you to run ducksync_refresh(...) or use ducksync_query(...). Unknown tables still return the normal DuckDB catalog error.
Cached data is stored in standard DuckLake tables. Query them directly with normal DuckDB SQL:
-- Create and refresh a cache
SELECT * FROM ducksync_create_cache('customers', 'prod', 'SELECT * FROM CUSTOMERS', ['CUSTOMERS'], 3600);
SELECT * FROM ducksync_refresh('customers');
-- Query the DuckLake table directly (standard DuckDB - no magic)
SELECT * FROM ducksync.prod.customers;
SELECT * FROM ducksync.prod.customers WHERE region = 'US';
-- Join with local tables
SELECT c.*, l.segment
FROM ducksync.prod.customers c
JOIN local_segments l ON c.id = l.customer_id;Path format: {catalog}.{source_name}.{cache_name} (e.g., ducksync.prod.customers)
When to use each approach:
| Method | Use When |
|---|---|
ducksync_query() |
Smart routing with TTL checks and auto-refresh |
| Direct DuckLake | Fast queries, no TTL checks, joining with local data |
DuckSync uses a "smart check" approach to minimize unnecessary data transfers:
- TTL Check: If
expires_at < now(), trigger refresh - Stage 1 (optional): With
invalidation_mode = 'two_stage', runSHOW TABLESusingmetadata_secretand compare storedrows/bytes - Stage 2: If needed, query
information_schema.tables.last_alteredwith the warehouse-backed data secret - Hash Comparison: Compare hash of current metadata vs stored
source_state_hash - Skip if Match: If hashes match and TTL not expired, skip refresh
- Refresh if Changed: Execute query, write to DuckLake, update state
This approach means:
- Zero warehouse wake-up when Stage 1 rows/bytes are unchanged
- Lower-cost false-positive filtering with Stage 2
last_altered - Automatic refresh when source tables are modified
┌─────────────────────────────────────────────────────────────┐
│ DuckSync Extension │
├─────────────────────────────────────────────────────────────┤
│ ducksync_query() MetadataManager StorageManager │
│ (smart routing) (DuckLake tables) (DuckLake) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌───────────┐ ┌───────────┐ │
│ │ TTL + │ │ ducksync. │ │ DuckLake │ │
│ │ Routing │◄─────►│ sources │ │ Parquet │ │
│ └──────────┘ │ caches │ │ Files │ │
│ │ │ state │ └───────────┘ │
│ │ └───────────┘ ▲ │
│ │ │ │ │
│ │ PostgreSQL Catalog ─────────┘ │
│ ┌──────▼───────────────────────────────────────┐ │
│ │ RefreshOrchestrator │ │
│ │ • TTL check + auto-refresh │ │
│ │ • Source metadata query (snowflake_query) │ │
│ │ • State hash comparison │ │
│ │ • Query execution & storage │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ Snowflake Extension │
│ (snowflake_query) │
└───────────────────────┘
- Docker (for PostgreSQL)
- DuckDB v1.5.4+
- ADBC Snowflake driver (for Snowflake integration tests)
# Build and run integration tests
make test
# Reset test environment (clean slate)
make reset-test
# Or manually:
cd test && docker compose up -d postgres
./test/run_tests.sh
# Cleanup
make test-docker-down # Stop PostgreSQL
make clean-test-data # Remove local test files
make clean-all # Full cleanup (Docker + data + build)For Snowflake integration tests, see test/README.md.
# Clone with submodules
git clone --recurse-submodules https://github.com/danjsiegel/ducksync.git
cd ducksync
# Build
make release
# Run
./build/release/duckdb- DuckDB v1.5.4+
- DuckLake extension - auto-installed (docs)
- Snowflake extension - auto-installed (docs)
- Snowflake-only: Currently only supports Snowflake as a data source
- Manual monitor_tables: Tables to monitor for changes must be explicitly specified when creating a cache
- All-or-nothing routing: If a query references multiple tables and any one is not cached, the entire query passes through to Snowflake
- SELECT only: Query rewriting only handles SELECT statements; DDL and DML pass through unchanged
MIT License - see LICENSE file for details.