22
33use pyo3:: prelude:: { Bound , PyAny , PyAnyMethods , PyResult , Python } ;
44use pyo3:: pyfunction;
5- use pyo3:: types:: { PyDict , PyDictMethods , PyString } ;
5+ use pyo3:: types:: { PyDict , PyDictMethods , PyList , PyListMethods , PyString } ;
66use sqlbuild_core:: dagster_invocation:: main:: asset_failure_text:: asset_failure_text;
77use sqlbuild_core:: dagster_invocation:: main:: diagnostic_tail:: diagnostic_tail;
8- use sqlbuild_core:: dagster_invocation:: models:: AssetFailure ;
8+ use sqlbuild_core:: dagster_invocation:: main:: failure_summary:: failure_summary;
9+ use sqlbuild_core:: dagster_invocation:: models:: { AssetFailure , FailedCheck } ;
910use sqlbuild_host:: entries:: models:: ThirdPartyHost ;
1011
1112use crate :: bindings:: _helpers:: boundary:: python_calls:: KeywordArguments ;
1213use crate :: bindings:: _helpers:: dagster_invocation:: access:: {
1314 constant_contains, get, items, text_of,
1415} ;
16+ use crate :: bindings:: _helpers:: dagster_invocation:: results:: { SKIPPED_CHECK_STATUS , warn_severity} ;
1517use crate :: bindings:: _helpers:: records:: fields:: field as native_field;
1618
19+ /// Failed checks listed in failure metadata; `failed_check_count` counts them all.
20+ const MAX_FAILED_CHECK_METADATA : usize = 100 ;
1721const LIVE_FAILURE_MESSAGE : & str =
1822 "SQLBuild resource attempt failed; see final execution diagnostics" ;
1923const UNKNOWN : & str = "unknown" ;
@@ -46,6 +50,43 @@ fn asset_failure(asset: &Bound<'_, PyAny>) -> PyResult<AssetFailure> {
4650 } )
4751}
4852
53+ /// A check of the execution document that failed with error severity, or `None`.
54+ fn failed_check ( check : & Bound < ' _ , PyAny > ) -> PyResult < Option < FailedCheck > > {
55+ if get ( check, "passed" ) ?. is_truthy ( ) ?
56+ || get ( check, "status" ) ?. eq ( SKIPPED_CHECK_STATUS ) ?
57+ || warn_severity ( check) ?
58+ {
59+ return Ok ( None ) ;
60+ }
61+ let row_count: Bound < ' _ , PyAny > = get ( check, "row_count" ) ?;
62+ Ok ( Some ( FailedCheck {
63+ kind : text_of ( check, "kind" ) ?,
64+ name : text_of ( check, "name" ) ?,
65+ asset_name : optional_text ( & get ( check, "asset_name" ) ?) ?,
66+ column_name : optional_text ( & get ( check, "attached_column_name" ) ?) ?,
67+ status : text_of ( check, "status" ) ?,
68+ row_count : row_count. extract ( ) ?,
69+ } ) )
70+ }
71+
72+ fn failed_check_metadata < ' py > (
73+ py : Python < ' py > ,
74+ checks : & [ FailedCheck ] ,
75+ ) -> PyResult < Bound < ' py , PyList > > {
76+ let listed: Bound < ' py , PyList > = PyList :: empty ( py) ;
77+ for check in checks. iter ( ) . take ( MAX_FAILED_CHECK_METADATA ) {
78+ let item: Bound < ' py , PyDict > = PyDict :: new ( py) ;
79+ item. set_item ( "kind" , & check. kind ) ?;
80+ item. set_item ( "name" , & check. name ) ?;
81+ item. set_item ( "asset" , check. asset_name . as_deref ( ) ) ?;
82+ item. set_item ( "column" , check. column_name . as_deref ( ) ) ?;
83+ item. set_item ( "status" , & check. status ) ?;
84+ item. set_item ( "failing_rows" , check. row_count ) ?;
85+ listed. append ( item) ?;
86+ }
87+ Ok ( listed)
88+ }
89+
4990/// The Dagster failure description and metadata of a failed invocation.
5091#[ pyfunction]
5192#[ pyo3( signature = ( * * options) ) ]
@@ -85,13 +126,14 @@ pub(crate) fn dagster_failure<'py>(
85126 let metadata: Bound < ' py , PyDict > = PyDict :: new ( py) ;
86127 metadata. set_item ( "command" , & command_text) ?;
87128 metadata. set_item ( "project_dir" , project_dir. str ( ) ?) ?;
88- metadata. set_item ( "selection " , selection. join ( " " ) ) ?;
129+ metadata. set_item ( "selection_count " , selection. len ( ) ) ?;
89130 metadata. set_item ( "selector_file" , selector_file) ?;
90131 let mut description: String =
91132 format ! ( "SQLBuild CLI command failed with exit code {returncode}: {command_text}" ) ;
92133 match execution_payload {
93134 Some ( payload) => {
94135 let mut incomplete: Vec < String > = Vec :: new ( ) ;
136+ let mut failures: Vec < AssetFailure > = Vec :: new ( ) ;
95137 let mut details: Vec < String > = Vec :: new ( ) ;
96138 let limits: Bound < ' py , PyDict > = PyDict :: new ( py) ;
97139 for asset in items ( & payload, "assets" ) ? {
@@ -102,13 +144,24 @@ pub(crate) fn dagster_failure<'py>(
102144 let failure: AssetFailure = asset_failure ( & asset) ?;
103145 incomplete. push ( format ! ( "{}:{} ({status})" , failure. kind, failure. name) ) ;
104146 details. push ( asset_failure_text ( & failure) ) ;
147+ failures. push ( failure) ;
105148 let microbatch: Bound < ' py , PyAny > = get ( & asset, "microbatch" ) ?;
106149 if microbatch. is_instance_of :: < PyDict > ( ) && !get ( & microbatch, "limit" ) ?. is_none ( ) {
107150 limits. set_item ( text_of ( & asset, "name" ) ?, microbatch) ?;
108151 }
109152 }
153+ let mut checks: Vec < FailedCheck > = Vec :: new ( ) ;
154+ for check in items ( & payload, "checks" ) ? {
155+ checks. extend ( failed_check ( & check) ?) ;
156+ }
110157 metadata. set_item ( "execution_status" , text_of ( & payload, "status" ) ?) ?;
111158 metadata. set_item ( "incomplete_assets" , incomplete. join ( ", " ) ) ?;
159+ metadata. set_item ( "failed_check_count" , checks. len ( ) ) ?;
160+ metadata. set_item ( "failed_checks" , failed_check_metadata ( py, & checks) ?) ?;
161+ let summary: String = failure_summary ( & checks, & failures) ;
162+ if !summary. is_empty ( ) {
163+ description = format ! ( "{summary}\n \n {description}" ) ;
164+ }
112165 if !limits. is_empty ( ) {
113166 metadata. set_item ( "microbatch_limits" , limits) ?;
114167 }
0 commit comments