dbt Iceberg Version Guessing Error - Investigation Report
Date: December 7, 2025
Status: ✅ FIXED - Implemented Option 1 (unsafe_enable_version_guessing)
Fix Applied
Section titled “Fix Applied”Added SET unsafe_enable_version_guessing = true; to bronze model pre-hooks in dbt_project.yml.
This is safe in our environment because:
- Dagster controls all writes (no concurrent external writers)
- Writes are sequential (one sync at a time per partition)
- dbt runs AFTER bronze sync completes (via sensors)
See detailed research: debt/bug-report-analysis/iceberg-version-handling-research-2025-12-07.md
Original Status: 🔴 Critical Issue - All Bronze dbt Models Failing
Executive Summary
Section titled “Executive Summary”All 5 bronze layer dbt models are failing with the same error when attempting to read from Iceberg tables. The error occurs because DuckDB’s iceberg_scan() function requires either a version parameter or unsafe_enable_version_guessing to be enabled, but neither is currently configured.
Impact: ⚠️ CRITICAL - Blocks entire dbt transformation pipeline (bronze → silver → gold)
Error Details
Section titled “Error Details”Error Message
Section titled “Error Message”Runtime Error in model raw_products (models/bronze/raw_products.sql)Failed to read iceberg table. No version was provided and no version-hint could be found,globbing the filesystem to locate the latest version is disabled by default as this isconsidered unsafe and could result in reading uncommitted data. To enable this use'SET unsafe_enable_version_guessing = true;'Affected Models
Section titled “Affected Models”All 5 bronze layer models are failing:
raw_cross_references- ERROR in 0.26sraw_customers- ERROR in 0.21sraw_products- ERROR in 0.22sraw_ship_to_links- ERROR in 0.21sraw_shipping_addresses- ERROR in 0.22s
Error Pattern
Section titled “Error Pattern”Completed with 5 errors, 0 partial successes, and 0 warnings:Failure in model raw_<table> (models/bronze/raw_<table>.sql) Runtime Error in model raw_<table> (models/bronze/raw_<table>.sql) Failed to read iceberg table. No version was provided and no version-hint could be found...Root Cause Analysis
Section titled “Root Cause Analysis”Current Configuration
Section titled “Current Configuration”1. DuckDB Settings (profiles.yml):
settings: unsafe_enable_version_guessing: false # ← Disabled for security/stability2. iceberg_source Macro (macros/iceberg_source.sql):
iceberg_scan( 's3://{{ env_var("R2_DATA_CATALOG_BUCKET") }}/{{ dataset }}/{{ table_name }}', allow_moved_paths=true -- ← No version parameter specified)3. Pre-hooks (dbt_project.yml):
bronze: +pre-hook: - 'INSTALL httpfs; LOAD httpfs;' - 'INSTALL iceberg; LOAD iceberg;' # ← No SET unsafe_enable_version_guessing = trueThe Problem
Section titled “The Problem”DuckDB’s iceberg_scan() function has two modes:
- Explicit version mode: Specify a version number/snapshot ID
- Version guessing mode: Let DuckDB automatically find the latest version (requires
unsafe_enable_version_guessing = true)
Current state:
- ❌ No version parameter in
iceberg_scan()calls - ❌
unsafe_enable_version_guessingisfalsein profiles.yml - ❌ No pre-hook to enable version guessing for the session
Result: DuckDB refuses to read the Iceberg tables because it can’t determine which version to use.
Why This Happened
Section titled “Why This Happened”The error message indicates this is a safety feature in DuckDB:
- Version guessing is disabled by default to prevent reading uncommitted data
- This is a recent change in DuckDB Iceberg extension behavior
- The codebase was likely working before, but a DuckDB update changed the default behavior
Impact Assessment
Section titled “Impact Assessment”Immediate Impact
Section titled “Immediate Impact”- ✅ Bronze Python assets: Working (write to Iceberg)
- ❌ Bronze dbt models: All failing (can’t read from Iceberg)
- ❌ Silver dbt models: Skipped (depend on bronze)
- ❌ Gold dbt models: Skipped (depend on silver)
- ❌ Typesense syncs: Not triggered (no gold assets materialized)
Pipeline Status
Section titled “Pipeline Status”Bronze Python Assets → ✅ Working ↓Bronze dbt Models → ❌ FAILING (5 errors) ↓Silver dbt Models → ⏭️ SKIPPED (depends on bronze) ↓Gold dbt Models → ⏭️ SKIPPED (depends on silver) ↓Typesense Assets → ⏭️ NOT TRIGGERED (no gold materializations)Solution Options
Section titled “Solution Options”Option 1: Enable Version Guessing via Pre-hook (RECOMMENDED)
Section titled “Option 1: Enable Version Guessing via Pre-hook (RECOMMENDED)”Approach: Add a pre-hook to bronze models to enable version guessing for the session.
Pros:
- ✅ Minimal code changes
- ✅ Only affects bronze models (where needed)
- ✅ Keeps profiles.yml secure (global setting stays false)
- ✅ Session-scoped (doesn’t affect other connections)
Cons:
- ⚠️ Enables “unsafe” behavior (but safe in our controlled environment)
Implementation:
models: erp_pipeline: bronze: +materialized: view +tags: - bronze +pre-hook: - 'INSTALL httpfs; LOAD httpfs;' - 'INSTALL iceberg; LOAD iceberg;' - 'SET unsafe_enable_version_guessing = true;' # ← ADD THISOption 2: Enable Version Guessing in profiles.yml
Section titled “Option 2: Enable Version Guessing in profiles.yml”Approach: Change unsafe_enable_version_guessing: false to true in profiles.yml.
Pros:
- ✅ Simple one-line change
- ✅ Applies to all models automatically
Cons:
- ⚠️ Global setting (affects all DuckDB connections)
- ⚠️ Less granular control
- ⚠️ Comment in profiles.yml says “Only enable for controlled testing”
Implementation:
settings: unsafe_enable_version_guessing: true # ← Change from falseOption 3: Specify Explicit Version in iceberg_scan() Calls (PREFERRED IF FEASIBLE)
Section titled “Option 3: Specify Explicit Version in iceberg_scan() Calls (PREFERRED IF FEASIBLE)”Approach: Modify iceberg_source macro to query metadata and pass explicit snapshot ID to iceberg_scan().
Pros:
- ✅ Most explicit and safe
- ✅ No “unsafe” flags needed
- ✅ Deterministic (always uses specific snapshot)
- ✅ Better for reproducibility
Cons:
- ❌ Requires version discovery logic (complex)
- ❌ Need to query Iceberg metadata JSON to find latest snapshot ID
- ❌ More code changes
- ❌ Performance overhead (metadata lookup before each scan)
- ❌ Requires DuckDB to read JSON from S3 (or Python helper)
Implementation Complexity:
Step 1: Create macro to get latest snapshot ID
-- macros/get_iceberg_snapshot.sql{% macro get_iceberg_snapshot(table_name, dataset='erp_data_native_v2') %} {# Query metadata to get latest snapshot ID. This requires: 1. Listing S3 objects to find latest metadata.json 2. Reading the JSON file 3. Extracting current_snapshot_id #} {%- set bucket = env_var("R2_DATA_CATALOG_BUCKET", "erp-data-catalog") -%} {%- set metadata_path = f"{dataset}/{table_name}/metadata/" -%}
{# Use DuckDB to read metadata JSON from S3 #} WITH metadata_files AS ( SELECT name FROM glob('s3://{{ bucket }}/{{ metadata_path }}*.metadata.json') ORDER BY name DESC LIMIT 1 ), metadata_json AS ( SELECT * FROM read_json_auto( (SELECT name FROM metadata_files), format='newline_delimited' ) ) SELECT current_snapshot_id FROM metadata_json{% endmacro %}Step 2: Update iceberg_source macro
{% macro iceberg_source(table_name, dataset='erp_data_native_v2') %} {%- set connection_id = var('connection_id', none) -%} {%- set snapshot_id = get_iceberg_snapshot(table_name, dataset) -%}
{%- if connection_id -%} ( SELECT * FROM iceberg_scan( 's3://{{ env_var("R2_DATA_CATALOG_BUCKET", "erp-data-catalog") }}/{{ dataset }}/{{ table_name }}', snapshot_id={{ snapshot_id }}, # ← Explicit version allow_moved_paths=true ) WHERE connection_id = '{{ connection_id }}' ) {%- else -%} iceberg_scan( 's3://{{ env_var("R2_DATA_CATALOG_BUCKET", "erp-data-catalog") }}/{{ dataset }}/{{ table_name }}', snapshot_id={{ snapshot_id }}, # ← Explicit version allow_moved_paths=true ) {%- endif -%}{% endmacro %}Challenges:
- DuckDB JSON reading: Need to verify DuckDB can read JSON from S3 paths
- Metadata structure: Need to understand Iceberg metadata JSON structure to extract
current_snapshot_id - Performance: Adds a metadata lookup query before each iceberg_scan() call
- Error handling: What if metadata file doesn’t exist? What if snapshot_id is null?
Alternative: Pass version from Dagster
- Dagster could query metadata and pass snapshot_id as dbt variable
- Requires coupling between Dagster and dbt
- More complex but avoids dbt-side metadata queries
Key Question: Does DuckDB iceberg_scan() actually support a snapshot_id parameter?
Research Needed:
- Check DuckDB Iceberg extension documentation for
iceberg_scan()signature - Verify if parameter is
snapshot_id,version, or something else - Test if passing explicit snapshot ID works
If DuckDB doesn’t support snapshot_id parameter:
- Option 3 is not feasible
- Must use Option 1 (unsafe flag) or Option 2 (global setting)
- This is likely why the unsafe flag exists - as a workaround when explicit versions aren’t supported
Investigation: Can We Use Explicit Versions?
Section titled “Investigation: Can We Use Explicit Versions?”Critical Question: Does DuckDB’s iceberg_scan() function support a snapshot_id or version parameter?
Action Required:
- Check DuckDB Iceberg extension documentation
- Test
iceberg_scan()function signature - Verify if we can pass explicit snapshot ID
If DuckDB supports explicit snapshot IDs:
- ✅ Use Option 3 (explicit versions) - Best practice, no unsafe flags
- Implementation: Query metadata to get snapshot ID, pass to iceberg_scan()
If DuckDB does NOT support explicit snapshot IDs:
- ⚠️ Use Option 1 (unsafe flag) - Only viable option
- The unsafe flag exists specifically for this case
Recommended Solution
Section titled “Recommended Solution”First: Investigate DuckDB API, then decide between Option 3 (if supported) or Option 1 (if not)
Decision Matrix
Section titled “Decision Matrix”| Factor | Option 1 (Unsafe Flag) | Option 3 (Explicit Versions) |
|---|---|---|
| Safety | ⚠️ Uses “unsafe” flag | ✅ Explicit, deterministic |
| Complexity | ✅ Simple (1 line) | ❌ Complex (metadata queries) |
| Performance | ✅ No overhead | ⚠️ Metadata lookup overhead |
| Maintainability | ✅ Easy | ⚠️ More code to maintain |
| Reproducibility | ⚠️ Uses latest (may change) | ✅ Uses specific snapshot |
| Implementation Time | ✅ 5 minutes | ❌ 1-2 hours |
Recommendation
Section titled “Recommendation”Short-term (Immediate fix): Use Option 1 (pre-hook) to unblock the pipeline quickly.
Long-term (Best practice): Implement Option 3 (explicit versions) for better safety and reproducibility.
Rationale for Option 1 (Short-term):
- Controlled environment: We control the data source (R2), so version guessing is safe
- Minimal changes: Only affects bronze models where needed
- Security: Keeps global setting disabled, only enables for specific models
- Maintainability: Clear and explicit in dbt_project.yml
- Quick fix: Unblocks pipeline immediately
Rationale for Option 3 (Long-term):
- Explicit is better than implicit: Always know which snapshot you’re reading
- Reproducibility: Same snapshot ID = same data, even if new writes happen
- No “unsafe” flags: Follows best practices
- Better for debugging: Can reference specific snapshot in logs
Implementation Plan
Section titled “Implementation Plan”Step 0: Investigate DuckDB iceberg_scan() API (IF USING OPTION 3)
Section titled “Step 0: Investigate DuckDB iceberg_scan() API (IF USING OPTION 3)”Before implementing Option 3, verify DuckDB supports explicit snapshot IDs:
-- Test in DuckDB console-- Check function signatureDESCRIBE iceberg_scan;
-- Or try calling with snapshot_id parameterSELECT * FROM iceberg_scan( 's3://bucket/path/table', snapshot_id=123456789 -- Test if this parameter exists);If snapshot_id parameter is NOT supported:
- DuckDB iceberg_scan() may only support version guessing
- Must use Option 1 (unsafe flag) - this is the intended workaround
- Proceed with Step 1 below
If snapshot_id parameter IS supported:
- Implement Option 3 (explicit versions)
- Create macro to query metadata and extract snapshot ID
- Update iceberg_source macro to use explicit snapshot_id
Step 1: Update dbt_project.yml (Option 1 - Quick Fix)
Section titled “Step 1: Update dbt_project.yml (Option 1 - Quick Fix)”Add the pre-hook to enable version guessing:
models: erp_pipeline: bronze: +materialized: view +tags: - bronze +pre-hook: - 'INSTALL httpfs; LOAD httpfs;' - 'INSTALL iceberg; LOAD iceberg;' - 'SET unsafe_enable_version_guessing = true;' # ← ADD THIS LINEStep 2: Test
Section titled “Step 2: Test”-
Run dbt build for one bronze model:
Terminal window cd apps/dagster/dbtuv run dbt build --select raw_products --vars '{"connection_id": "996523e0-aee0-452e-8ac7-35a44f2e8812"}' -
Verify it succeeds without errors
-
Run full bronze layer:
Terminal window uv run dbt build --select tag:bronze --vars '{"connection_id": "996523e0-aee0-452e-8ac7-35a44f2e8812"}' -
Verify all 5 bronze models succeed
Step 3: Verify Pipeline Flow
Section titled “Step 3: Verify Pipeline Flow”- Trigger bronze sync → Should succeed
- Verify dbt transform runs → Should succeed (bronze models work)
- Verify silver/gold models run → Should succeed (depends on bronze)
- Verify Typesense sync triggers → Should trigger (gold assets materialized)
Alternative: Why Not Use Explicit Versions?
Section titled “Alternative: Why Not Use Explicit Versions?”While specifying explicit versions would be the “safest” approach, it’s impractical because:
- Version discovery complexity: Would need to query Iceberg metadata files to find latest version
- Performance overhead: Extra S3 calls to discover version before each scan
- Maintenance burden: More complex code, harder to maintain
- Not necessary: In our controlled environment, version guessing is safe
Related Code Locations
Section titled “Related Code Locations”Files Involved
Section titled “Files Involved”apps/dagster/dbt/dbt_project.yml- Pre-hook configurationapps/dagster/dbt/profiles.yml- DuckDB settingsapps/dagster/dbt/macros/iceberg_source.sql- Iceberg scan macroapps/dagster/dbt/models/bronze/*.sql- Bronze layer models (all 5 affected)
Error Location
Section titled “Error Location”- Function:
iceberg_scan()in DuckDB Iceberg extension - Trigger: When DuckDB tries to read Iceberg table without version info
- Context: dbt models calling
{{ iceberg_source('table_name') }}
Testing Checklist
Section titled “Testing Checklist”After implementing the fix:
-
raw_productsmodel builds successfully -
raw_customersmodel builds successfully -
raw_cross_referencesmodel builds successfully -
raw_ship_to_linksmodel builds successfully -
raw_shipping_addressesmodel builds successfully - Silver layer models build (depends on bronze)
- Gold layer models build (depends on silver)
- Typesense sync triggers after gold materialization
- Full pipeline runs end-to-end
Logs Reference
Section titled “Logs Reference”Run ID: c4d8fd66-0131-40d3-8d9a-48c2e37d229a
Connection ID: 996523e0-aee0-452e-8ac7-35a44f2e8812
Timestamp: 2025-12-07 13:09:41 UTC
dbt Command: dbt build --vars {"connection_id": "996523e0-aee0-452e-8ac7-35a44f2e8812"} --select tag:bronze tag:silver tag:gold --exclude tag:snapshot
Error Count:
- 5 errors (all bronze models)
- 0 partial successes
- 0 warnings
- 35 skipped (silver/gold models skipped due to bronze failures)
Report Generated: 2025-12-07 13:15 UTC
Investigator: AI Assistant
Status: Ready for Fix Implementation