{"record":{"id":"2566e0305bc4f762","repo":"dbt-labs/dbt-core","slug":"agate-table","errorCode":null,"errorMessage":"agate_table","messagePattern":"agate_table","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/dbt-adapter/src/load_store.rs","lineNumber":69,"sourceCode":"                // never downcasts to one -- the taint wrapper itself is\n                // what's stored, not its contents. Degrade to a default\n                // response instead of erroring via `AdapterResponse::\n                // try_from`'s downcast/string fallback below.\n                AdapterResponse::default()\n            } else {\n                AdapterResponse::try_from(response_value)?\n            };\n\n            let table: Option<Value> = iter.next_kwarg::<Option<Value>>(\"agate_table\")?;\n            let table = if let Some(t) = table {\n                if t.is_introspective_stub() {\n                    // Same rationale as `response` above: `.expect(\n                    // \"agate_table\")` below would otherwise hard-panic\n                    // instead of degrading gracefully, since a tainted\n                    // value can never downcast to a concrete `AgateTable`.\n                    Some(AgateTable::default())\n                } else if !t.is_none() {\n                    Some((*t.downcast_object::<AgateTable>().expect(\"agate_table\")).clone())\n                } else {\n                    Some(AgateTable::default())\n                }\n            } else {\n                Some(AgateTable::default())\n            };\n\n            // Record rows_affected on the NodeEvaluated span if non-negative.\n            // dbt-core uses -1 to indicate unknown rows affected. Telemetry uses `None` for unknown.\n            let rows_affected = response.rows_affected_i64();\n            if rows_affected >= 0 {\n                find_and_update_span_attrs::<_, NodeEvaluated>(|attrs| {\n                    attrs.rows_affected = Some(rows_affected as u64);\n                });\n            }\n\n            let value = Value::from_object(ResultObject::new(response, table));\n            iter.finish()?;","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/dbt-labs/dbt-core/blob/0267ce9170576975b76b64ce856b2e5848e96617/crates/dbt-adapter/src/load_store.rs#L51-L87","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nSome((*t.downcast_object::<AgateTable>().expect(\"agate_table\")).clone())\n// after\nSome(t.downcast_object::<AgateTable>().map(|v| (*v).clone()).unwrap_or_default())","handlingStrategy":"fallback","validationCode":"// before calling store_result\nif !tainted_value.is_none() && tainted_value.downcast_object::<AgateTable>().is_none() {\n    // producer stored a non-AgateTable object; fix or log the type\n    eprintln!(\"unexpected result object type: {}\", tainted_value.type_name());\n}","typeGuard":"fn is_agate_table(v: &TaintedValue) -> bool {\n    v.is_none() || v.downcast_object::<AgateTable>().is_some()\n}","tryCatchPattern":"// panics are not catchable via Result; wrap in catch_unwind for tooling\nlet result = std::panic::catch_unwind(AssertUnwindSafe(|| ctx.store_result(...)));","preventionTips":["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"],"tags":["rust","panic","downcast","adapter"],"backgroundTag":"internal-invariant-violation","analyzedSha":"0267ce9170576975b76b64ce856b2e5848e96617","analyzedAt":"2026-09-07T21:53:39.732Z","contentChangedAt":"2026-09-07T21:53:39.732Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}