{"record":{"id":"c59e3b98b08ee342","repo":"dbt-labs/dbt-core","slug":"no-expression-for-col","errorCode":null,"errorMessage":"no expression for '{col}'","messagePattern":"no expression for '(.+?)'","errorType":"validation","errorClass":"IndexError","httpStatus":null,"severity":"error","filePath":"crates/dbt-index-core/src/info_schema/parse_safe.rs","lineNumber":711,"sourceCode":"\n    /// `dbt.dag_nodes`: the node set unioned with the tables holding the DAG-participating\n    /// resource types that are not in it.\n    ///\n    /// Mirrors `info_schema::build_dag_nodes` rather than inventing a rule: keep the node\n    /// rows whose `resource_type` is a DAG type, keep every row of the side tables (their\n    /// type is one by definition), and treat a missing `enabled` as enabled, matching the\n    /// config default. The side tables' `enabled` is in the index even though the\n    /// information schema does not publish it — a view reads the table, not the artifact.\n    fn dag_nodes_sql(&self) -> Result<String, IndexError> {\n        // Written per output column so a column added to the information schema's\n        // `dag_nodes` fails here instead of silently not appearing.\n        let expr = |col: &str, resource_type: Option<&str>| -> Result<String, IndexError> {\n            Ok(match (col, resource_type) {\n                (\"resource_type\", Some(literal)) => format!(\"'{literal}'\"),\n                (\"unique_id\" | \"resource_type\" | \"ingested_at\", _) => {\n                    format!(\"{T}.{}\", quote(col))\n                }\n                _ => return Err(self.err(format!(\"no expression for '{col}'\"))),\n            })\n        };\n        let select = |table: &str, resource_type: Option<&str>| -> Result<String, IndexError> {\n            let cols = self\n                .cols\n                .iter()\n                .map(|col| Ok(format!(\"{} AS {}\", expr(col, resource_type)?, quote(col))))\n                .collect::<Result<Vec<_>, IndexError>>()?\n                .join(\", \");\n            Ok(format!(\n                \"SELECT {cols} FROM {BASE_SCHEMA}.{} AS {T} WHERE COALESCE({T}.\\\"enabled\\\", TRUE)\",\n                quote(table),\n            ))\n        };\n\n        let types = DAG_RESOURCE_TYPES\n            .iter()\n            .map(|t| format!(\"'{t}'\"))","sourceCodeStart":693,"sourceCodeEnd":729,"githubUrl":"https://github.com/dbt-labs/dbt-core/blob/0267ce9170576975b76b64ce856b2e5848e96617/crates/dbt-index-core/src/info_schema/parse_safe.rs#L693-L729","documentation":"When generating SQL for the dbt.dag_nodes view (a UNION ALL over the nodes table plus DAG side tables), each requested output column must be mapped to a SQL expression. The expr closure only knows how to render 'resource_type' (as a literal for side tables), and 'unique_id', 'resource_type', 'ingested_at' (as node-table columns). Any other column declared in the view's cols has no expression, so view construction fails loudly instead of silently dropping a column from the information schema.","triggerScenarios":"Adding a new column to the dag_nodes information-schema view's `cols` list (VIEWS entry) without extending the match in dag_nodes_sql's expr closure to render that column, then calling create_view_sql() on that view.","commonSituations":"A contributor extends the information schema's dag_nodes output (e.g. adding 'enabled' or a new metadata field) and updates VIEWS but not the per-column expression builder; the module's own tests then fail with this error.","solutions":["Extend the match arm in the expr closure (parse_safe.rs:706) to produce an expression for the new column, e.g. (\"new_col\", _) => format!(\"{T}.{}\", quote(\"new_col\")).","If the column only exists on side tables as a literal, add a (\"new_col\", Some(literal)) arm that emits the constant.","If the column should not be in dag_nodes, remove it from that view's cols entry in VIEWS."],"exampleFix":"// before\n(\"unique_id\" | \"resource_type\" | \"ingested_at\", _) => {\n    format!(\"{T}.{}\", quote(col))\n}\n_ => return Err(self.err(format!(\"no expression for '{col}'\"))),\n// after\n(\"unique_id\" | \"resource_type\" | \"ingested_at\" | \"new_col\", _) => {\n    format!(\"{T}.{}\", quote(col))\n}\n_ => return Err(self.err(format!(\"no expression for '{col}'\"))),","handlingStrategy":"validation","validationCode":"// Check every dag_nodes column is renderable before generating SQL\nconst DAG_NODES_EXPR_COLS: &[&str] = &[\"unique_id\", \"resource_type\", \"ingested_at\"];\nfn dag_nodes_cols_covered(cols: &[&str]) -> Vec<String> {\n    cols.iter()\n        .filter(|c| !DAG_NODES_EXPR_COLS.contains(c) && **c != \"resource_type\")\n        .map(|c| format!(\"no expression for '{c}'\"))\n        .collect()\n}","typeGuard":null,"tryCatchPattern":"match view.create_view_sql() {\n    Ok(sql) => execute(sql),\n    Err(IndexError::Other(msg)) if msg.contains(\"no expression for\") => {\n        eprintln!(\"view spec error (developer fix needed): {msg}\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Treat the expr closure's match arms as the contract for dag_nodes columns; update it in the same PR as any VIEWS cols change.","Add a unit test asserting every column in the dag_nodes view entry maps to an expression.","Derive cols from a shared constant also referenced by the expr builder to keep them in sync."],"tags":["internal-invariant","sql-generation","column-mapping"],"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"}