dbt-labs/dbt-core · error

agate_table

Error message

agate_table

What it means

This is a Rust panic from `.expect("agate_table")` in `store_result` (crates/dbt-adapter/src/load_store.rs:69). The code downcasts a dynamically-typed `TaintedValue` object to a concrete `AgateTable`; if the value is neither None nor actually an `AgateTable`, the downcast fails and the process panics instead of degrading gracefully. The surrounding code explicitly guards the tainted case, so the panic only fires on genuinely unexpected value types.

Source

Thrown at crates/dbt-adapter/src/load_store.rs:69

                // never downcasts to one -- the taint wrapper itself is
                // what's stored, not its contents. Degrade to a default
                // response instead of erroring via `AdapterResponse::
                // try_from`'s downcast/string fallback below.
                AdapterResponse::default()
            } else {
                AdapterResponse::try_from(response_value)?
            };

            let table: Option<Value> = iter.next_kwarg::<Option<Value>>("agate_table")?;
            let table = if let Some(t) = table {
                if t.is_introspective_stub() {
                    // Same rationale as `response` above: `.expect(
                    // "agate_table")` below would otherwise hard-panic
                    // instead of degrading gracefully, since a tainted
                    // value can never downcast to a concrete `AgateTable`.
                    Some(AgateTable::default())
                } else if !t.is_none() {
                    Some((*t.downcast_object::<AgateTable>().expect("agate_table")).clone())
                } else {
                    Some(AgateTable::default())
                }
            } else {
                Some(AgateTable::default())
            };

            // Record rows_affected on the NodeEvaluated span if non-negative.
            // dbt-core uses -1 to indicate unknown rows affected. Telemetry uses `None` for unknown.
            let rows_affected = response.rows_affected_i64();
            if rows_affected >= 0 {
                find_and_update_span_attrs::<_, NodeEvaluated>(|attrs| {
                    attrs.rows_affected = Some(rows_affected as u64);
                });
            }

            let value = Value::from_object(ResultObject::new(response, table));
            iter.finish()?;

View on GitHub (pinned to 0267ce9170)

Solutions

  1. Ensure the value stored in the result slot is an `AgateTable` (or None) before calling store_result; fix the producer (macro/adapter) that stores the wrong type
  2. Use `downcast_object::<AgateTable>()` result matching instead of `.expect` so an unexpected type falls back to `AgateTable::default()`
  3. Wrap the context execution in catch_unwind in test/tooling contexts to convert the panic into an error and capture the actual stored type
  4. Pin adapter/dbt-core versions so producers and consumers agree on the dynamic object type

Example fix

// before
Some((*t.downcast_object::<AgateTable>().expect("agate_table")).clone())
// after
Some(t.downcast_object::<AgateTable>().map(|v| (*v).clone()).unwrap_or_default())
Defensive patterns

Strategy: fallback

Validate before calling

// before calling store_result
if !tainted_value.is_none() && tainted_value.downcast_object::<AgateTable>().is_none() {
    // producer stored a non-AgateTable object; fix or log the type
    eprintln!("unexpected result object type: {}", tainted_value.type_name());
}

Type guard

fn is_agate_table(v: &TaintedValue) -> bool {
    v.is_none() || v.downcast_object::<AgateTable>().is_some()
}

Try / catch

// panics are not catchable via Result; wrap in catch_unwind for tooling
let result = std::panic::catch_unwind(AssertUnwindSafe(|| ctx.store_result(...)));

Prevention

When it happens

Trigger: Calling `store_result` with a tainted value whose inner type is neither `None` nor an `AgateTable` (e.g. a plugin or macro stored a `ResultObject`/string/other dynamic object into the result slot), via any CompileNodeCtx/ResolveModelCtx/extend_base_context_stateful_fn execution path.

Common situations: Custom or third-party materializations/macros that stuff non-table values into `this` or result state; version mismatches where an adapter writes a different dynamic object type; tainted-value handling introduced by untrusted SQL writing unexpected types.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of dbt-labs/dbt-core@0267ce9170 (2026-09-07). Data as JSON: /api/errors/2566e0305bc4f762. Report an issue: GitHub.