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.
Solutions
- 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
- Use `downcast_object::<AgateTable>()` result matching instead of `.expect` so an unexpected type falls back to `AgateTable::default()`
- Wrap the context execution in catch_unwind in test/tooling contexts to convert the panic into an error and capture the actual stored type
- 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
- Only store AgateTable or None into result slots
- Log concrete dynamic type names in custom macros/adapters
- Pin adapter and core versions together
- Add tests for custom macros that populate result state
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
- Adapter must be configured for the parse phase
- Adapter should be available during parse phase
- Athena
- Datafusion
- Dremio
AI-assisted analysis of dbt-labs/dbt-core@0267ce9170 (2026-09-07).
Data as JSON: /api/errors/2566e0305bc4f762.
Report an issue: GitHub.
Appendix: 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)