@@ -1232,6 +1232,26 @@ def generateCheck(self) -> DuckDBColumnCheck:
12321232 )
12331233
12341234 schema = schema_entry ["Schema" ]
1235+
1236+ # Validate the schema up front so a malformed model schema fails fast as
1237+ # an InvalidRuleException instead of surfacing mid-run. If jsonschema is
1238+ # not installed, defer to the executor's clear RuntimeError at run time.
1239+ try :
1240+ from jsonschema import Draft202012Validator # type: ignore[import-untyped]
1241+ from jsonschema .exceptions import ( # type: ignore[import-untyped]
1242+ SchemaError ,
1243+ )
1244+ except ModuleNotFoundError :
1245+ pass
1246+ else :
1247+ try :
1248+ Draft202012Validator .check_schema (schema )
1249+ except SchemaError as exc :
1250+ raise InvalidRuleException (
1251+ f"SchemaId '{ schema_id } ' referenced by rule { self .rule_id } "
1252+ f"has an invalid JSON schema: { exc .message } "
1253+ ) from exc
1254+
12351255 path = getattr (self .params , "Path" , "$" )
12361256 col = self .params .ColumnName
12371257 where_clauses = [f"{ col } IS NOT NULL" ]
@@ -1251,11 +1271,9 @@ def _exec_json_schema(conn):
12511271 "CheckJSONSchema requires the 'jsonschema' package to be installed"
12521272 ) from exc
12531273
1254- Draft202012Validator .check_schema (schema )
12551274 validator = Draft202012Validator (schema )
12561275 table_name = getattr (self .params , "table_name" , "focus_data" )
12571276 sql = query .replace ("{table_name}" , table_name )
1258- sql = sql .replace ("{table_name}" , table_name )
12591277 try :
12601278 rows = conn .execute (sql ).fetchall ()
12611279 except (duckdb .BinderException , duckdb .CatalogException ) as exc :
@@ -1288,6 +1306,8 @@ def _exec_json_schema(conn):
12881306
12891307 failure_messages : list [str ] = []
12901308 violations = 0
1309+ # row_num counts position within the filtered result set (non-null,
1310+ # row-condition-matching rows), not the source data row number.
12911311 for row_num , row in enumerate (rows , start = 1 ):
12921312 raw_value = row [0 ] if isinstance (row , (tuple , list )) else row
12931313 try :
@@ -1298,7 +1318,9 @@ def _exec_json_schema(conn):
12981318 )
12991319 except Exception as exc :
13001320 violations += 1
1301- failure_messages .append (f"row { row_num } : invalid JSON ({ exc } )" )
1321+ failure_messages .append (
1322+ f"matching row { row_num } : invalid JSON ({ exc } )"
1323+ )
13021324 continue
13031325
13041326 instance = self ._extract_path_value (payload , path )
@@ -1307,7 +1329,9 @@ def _exec_json_schema(conn):
13071329 )
13081330 if errors :
13091331 violations += 1
1310- failure_messages .append (f"row { row_num } : { errors [0 ].message } " )
1332+ failure_messages .append (
1333+ f"matching row { row_num } : { errors [0 ].message } "
1334+ )
13111335
13121336 ok = violations == 0
13131337 details = {
@@ -1918,23 +1942,34 @@ class CheckColumnComparisonGenerator(DuckDBCheckGenerator):
19181942
19191943 _VALID_COMPARATORS : ClassVar [Set [str ]] = {"=" , "!=" , "<>" , ">" , ">=" , "<" , "<=" }
19201944
1921- def generateSql (self ) -> SQLQuery :
1922- col_a = self .params .ColumnAName
1923- col_b = self .params .ColumnBName
1945+ def _validated_comparator (self ) -> str :
19241946 comparator = self .params .Comparator
1925- keyword = self ._get_validation_keyword ()
1926-
19271947 if comparator not in self ._VALID_COMPARATORS :
19281948 raise InvalidRuleException (
19291949 f"Unsupported comparator for { self .rule_id } : { comparator } "
19301950 )
1951+ return comparator
1952+
1953+ def _violation_condition (self ) -> str :
1954+ col_a = self .params .ColumnAName
1955+ col_b = self .params .ColumnBName
1956+ comparator = self ._validated_comparator ()
1957+ return (
1958+ f"{ col_a } IS NOT NULL AND { col_b } IS NOT NULL "
1959+ f"AND NOT ({ col_a } { comparator } { col_b } )"
1960+ )
1961+
1962+ def generateSql (self ) -> SQLQuery :
1963+ col_a = self .params .ColumnAName
1964+ col_b = self .params .ColumnBName
1965+ comparator = self ._validated_comparator ()
1966+ keyword = self ._get_validation_keyword ()
19311967
19321968 message = self .errorMessage or f"{ col_a } { keyword } be { comparator } { col_b } ."
19331969 msg_sql = message .replace ("'" , "''" )
19341970
19351971 pass_predicate = f"{ col_a } IS NOT NULL AND { col_b } IS NOT NULL AND { col_a } { comparator } { col_b } "
1936- condition = f"{ col_a } IS NOT NULL AND { col_b } IS NOT NULL AND NOT ({ col_a } { comparator } { col_b } )"
1937- condition = self ._apply_condition (condition )
1972+ condition = self ._apply_condition (self ._violation_condition ())
19381973
19391974 requirement_sql = f"""
19401975 WITH invalid AS (
@@ -1956,9 +1991,7 @@ def generateSql(self) -> SQLQuery:
19561991 def get_sample_sql (self ) -> str :
19571992 col_a = self .params .ColumnAName
19581993 col_b = self .params .ColumnBName
1959- comparator = self .params .Comparator
1960- condition = f"{ col_a } IS NOT NULL AND { col_b } IS NOT NULL AND NOT ({ col_a } { comparator } { col_b } )"
1961- condition = self ._apply_condition (condition )
1994+ condition = self ._apply_condition (self ._violation_condition ())
19621995
19631996 return f"""
19641997 SELECT { col_a } , { col_b }
@@ -2166,6 +2199,7 @@ def _exec_reference(_conn):
21662199 chk .special_executor = _exec_reference
21672200 chk .exec_mode = "reference"
21682201 chk .referenced_rule_id = target_id
2202+ chk .meta ["special_executor_kind" ] = "reference"
21692203 return chk
21702204
21712205
@@ -6451,6 +6485,7 @@ def _explain_check_sql(self, check) -> dict:
64516485 return {
64526486 "rule_id" : rid ,
64536487 "type" : "special" ,
6488+ "special_kind" : special_kind ,
64546489 "check_type" : ctype ,
64556490 "generator" : meta .get ("generator" ),
64566491 "row_condition_sql" : meta .get ("row_condition_sql" ),
@@ -6541,7 +6576,7 @@ def print_sql_map(self, sql_map: dict):
65416576 print (
65426577 f"Composite: { info .get ('aggregate' )} with { len (info .get ('children' , []))} items"
65436578 )
6544- elif t == "reference" :
6579+ elif t == "special" and info . get ( "special_kind" ) == " reference" :
65456580 print (f"Reference to: { info .get ('referenced' )} " )
65466581 elif t == "skipped" :
65476582 print (f"Skipped: { info .get ('reason' )} " )
0 commit comments