Skip to content

Commit da2897a

Browse files
committed
refactor: implement ruleset-level semantic validation for DQ rules (#1169)
1 parent f84132b commit da2897a

4 files changed

Lines changed: 444 additions & 113 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
# Version changelog
22

3-
## 0.15.0
4-
5-
* Added semantic validation for DQ rule SQL expressions ([#1169](https://github.com/databrickslabs/dqx/issues/1169)). A new `SemanticValidator` class checks SQL expressions used in DQ rules for forbidden destructive keywords (DROP, TRUNCATE, DELETE, UPDATE, INSERT, ALTER) before rules are applied. Validation errors are collected into `ChecksValidationStatus`, consistent with the existing validation contract.
6-
73
## 0.14.0
84

95
* ML-based row-level anomaly detection ([#990](https://github.com/databrickslabs/dqx/issues/990), [#1055](https://github.com/databrickslabs/dqx/issues/1055), [#1062](https://github.com/databrickslabs/dqx/issues/1062)). DQX now offers ML-based row anomaly detection that automatically identifies unusual rows in data without requiring manually specified thresholds, enabling the detection of issues missed by rule-based checks. Users provide recent representative data, and DQX trains an Isolation Forest model that flags rows deviating from typical patterns at scoring time, with auto-discovery of relevant columns and segmentation where appropriate, plus per-row explanations of why a record was flagged. The feature integrates with MLflow for model registry, supports both training and scoring workflows, and complements existing rule-based and aggregate checks.

src/databricks/labs/dqx/engine.py

Lines changed: 63 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,9 @@
5353
from databricks.labs.dqx.errors import InvalidCheckError, InvalidConfigError, InvalidParameterError
5454
from databricks.labs.dqx.utils import list_tables, safe_strip_file_from_path, resolve_variables, VariableValue
5555
from databricks.labs.dqx.io import is_one_time_trigger
56-
from .semantic_validator import SemanticValidator
56+
from .semantic_validator import SemanticValidator, SemanticValidationMode
5757
logger = logging.getLogger(__name__)
5858

59-
6059
class DQEngineCore(DQEngineCoreBase):
6160
"""Core engine to apply data quality checks to a DataFrame.
6261
@@ -291,43 +290,46 @@ def apply_checks_by_metadata_and_split(
291290
return good_df, bad_df
292291

293292
@staticmethod
294-
def validate_checks(
293+
def validate_checks(
295294
checks: list[dict],
296295
custom_check_functions: dict[str, Callable] | None = None,
297296
validate_custom_check_functions: bool = True,
297+
semantic_validation_mode: str | None = SemanticValidationMode.WARN,
298298
) -> ChecksValidationStatus:
299299
"""
300300
Validate checks defined as metadata to ensure they conform to the expected
301-
structure and types.
302-
303-
This method validates the presence of required keys, the existence and
304-
callability of functions, the types of arguments passed to those functions,
305-
and performs semantic validation to reject destructive SQL expressions.
306-
301+
structure and types, and are semantically consistent as a ruleset.
302+
303+
Structural validation checks for required keys, callable functions, and
304+
correct argument types. Semantic validation detects duplicate rules and
305+
similar rules with conflicting arguments (e.g. two is_in_range checks on
306+
the same column with different thresholds).
307+
308+
Note:
309+
Rules using raw Spark SQL expressions are not deeply inspected during
310+
semantic validation — only structured metadata is compared.
311+
307312
Args:
308313
checks: List of checks to apply to the DataFrame. Each check should be a dictionary.
309314
custom_check_functions: Optional dictionary with custom check functions
310315
(e.g., *globals()* of the calling module).
311316
validate_custom_check_functions: If True, validate custom check functions.
312-
317+
semantic_validation_mode: Controls how semantic issues are surfaced.
318+
Use ``SemanticValidationMode.WARN`` (default) to log warnings,
319+
``SemanticValidationMode.FAIL`` to raise on any issue, or
320+
``None`` to skip semantic validation entirely.
321+
313322
Returns:
314-
ChecksValidationStatus indicating the validation result.
323+
ChecksValidationStatus indicating the structural validation result.
324+
325+
Raises:
326+
ValueError: If semantic_validation_mode is FAIL and issues are found.
315327
"""
316-
# Run structural validation first; collect all errors into the status object.
317328
status = ChecksValidator.validate_checks(checks, custom_check_functions, validate_custom_check_functions)
318-
319-
# Run semantic validation on top; add any findings into the same status.
320-
for check in checks:
321-
if not isinstance(check, dict):
322-
continue
323-
expr = check.get("kwargs", {}).get("expression") or check.get("expression")
324-
if not expr:
325-
continue
326-
try:
327-
SemanticValidator.validate_sql_expression(expr)
328-
except ValueError as e:
329-
status.add_error(str(e))
330-
329+
330+
if semantic_validation_mode is not None:
331+
SemanticValidator.apply(checks, mode=semantic_validation_mode)
332+
331333
return status
332334

333335
def get_invalid(self, df: DataFrame) -> DataFrame:
@@ -1205,44 +1207,55 @@ def save_results_in_table(
12051207
)
12061208

12071209
@telemetry_logger("engine", "load_checks")
1208-
def load_checks(
1209-
self, config: BaseChecksStorageConfig, variables: dict[str, VariableValue] | None = None
1210+
def load_checks(
1211+
self,
1212+
config: BaseChecksStorageConfig,
1213+
variables: dict[str, VariableValue] | None = None,
1214+
semantic_validation_mode: str | None = SemanticValidationMode.WARN, # <-- ADD THIS
12101215
) -> list[dict]:
12111216
"""Load DQ rules (checks) from the storage backend described by *config*.
1212-
1217+
12131218
This method delegates to a storage handler selected by the factory
12141219
based on the concrete type of *config* and returns the parsed list
12151220
of checks (as dictionaries) ready for *apply_checks_by_metadata*.
1216-
1221+
12171222
Supported storage configurations include, for example:
12181223
- *FileChecksStorageConfig* (local file);
12191224
- *WorkspaceFileChecksStorageConfig* (Databricks workspace file);
12201225
- *TableChecksStorageConfig* (table-backed storage);
12211226
- *LakebaseChecksStorageConfig* (Lakebase table);
12221227
- *InstallationChecksStorageConfig* (installation directory);
12231228
- *VolumeFileChecksStorageConfig* (Unity Catalog volume file);
1224-
1229+
12251230
Per-call *variables* are merged with engine-level defaults from
12261231
*ExtraParams.variables* (per-call values take precedence on conflict).
1227-
1232+
12281233
**Security note:** variable values substituted into **sql_expression** checks are
12291234
not sanitized. Callers must ensure that variable values come from trusted sources.
1230-
1235+
12311236
Args:
12321237
config: Configuration object describing the storage backend.
12331238
variables: Optional mapping of placeholder names to replacement values. Replaces placeholders
12341239
in all string values of the check definitions before returning.
1235-
1240+
semantic_validation_mode: Controls semantic validation behavior after loading.
1241+
Use ``SemanticValidationMode.WARN`` (default) to log warnings and continue,
1242+
``SemanticValidationMode.FAIL`` to raise if issues are found, or
1243+
``None`` to skip semantic validation entirely.
1244+
12361245
Returns:
12371246
List of DQ rules (checks) represented as dictionaries.
1238-
1247+
12391248
Raises:
12401249
InvalidConfigError: If the configuration type is unsupported.
1250+
ValueError: If semantic_validation_mode is FAIL and issues are found.
12411251
"""
12421252
handler = self._checks_handler_factory.create(config)
12431253
checks = handler.load(config)
12441254
merged_variables = self._merge_variables(variables)
1245-
return resolve_variables(checks=checks, variables=merged_variables)
1255+
resolved = resolve_variables(checks=checks, variables=merged_variables) # <-- RENAME from return value
1256+
if semantic_validation_mode is not None: # <-- ADD THIS
1257+
SemanticValidator.apply(resolved, mode=semantic_validation_mode) # <-- ADD THIS
1258+
return resolved # <-- CHANGE return
12461259

12471260
def _merge_variables(self, per_call: dict[str, VariableValue] | None) -> dict[str, VariableValue] | None:
12481261
"""Merge engine-level default variables with per-call overrides.
@@ -1259,46 +1272,54 @@ def _merge_variables(self, per_call: dict[str, VariableValue] | None) -> dict[st
12591272
return {**defaults, **per_call}
12601273

12611274
@telemetry_logger("engine", "save_checks")
1262-
def save_checks(
1275+
def save_checks(
12631276
self,
12641277
checks: list[dict],
12651278
config: BaseChecksStorageConfig,
12661279
variables: dict[str, VariableValue] | None = None,
1280+
semantic_validation_mode: str | None = SemanticValidationMode.WARN, # <-- ADD THIS
12671281
) -> None:
12681282
"""Persist DQ rules (checks) to the storage backend described by *config*.
1269-
1283+
12701284
The appropriate storage handler is resolved from the configuration
12711285
type and used to write the provided checks. Any write semantics
12721286
(e.g., append/overwrite) are controlled by fields on *config*
12731287
such as *mode* where applicable.
1274-
1288+
12751289
Supported storage configurations include, for example:
12761290
- *FileChecksStorageConfig* (local file);
12771291
- *WorkspaceFileChecksStorageConfig* (Databricks workspace file);
12781292
- *TableChecksStorageConfig* (table-backed storage);
12791293
- *LakebaseChecksStorageConfig* (Lakebase table);
12801294
- *InstallationChecksStorageConfig* (installation directory);
12811295
- *VolumeFileChecksStorageConfig* (Unity Catalog volume file);
1282-
1296+
12831297
Per-call *variables* are merged with engine-level defaults from
12841298
*ExtraParams.variables* (per-call values take precedence on conflict).
12851299
Variables are resolved before computing fingerprints and persisting,
12861300
ensuring that stored checks and their fingerprints are consistent.
1287-
1301+
12881302
Args:
12891303
checks: List of DQ rules (checks) to save (as dictionaries).
12901304
config: Configuration object describing the storage backend and write options.
12911305
variables: Optional mapping of placeholder names to replacement values. Replaces placeholders
12921306
in all string values of the check definitions before saving.
1293-
1307+
semantic_validation_mode: Controls semantic validation behavior before saving.
1308+
Use ``SemanticValidationMode.WARN`` (default) to log warnings and continue,
1309+
``SemanticValidationMode.FAIL`` to abort saving if issues are found, or
1310+
``None`` to skip semantic validation entirely.
1311+
12941312
Returns:
12951313
None
1296-
1314+
12971315
Raises:
12981316
InvalidConfigError: If the configuration type is unsupported.
1317+
ValueError: If semantic_validation_mode is FAIL and issues are found.
12991318
"""
13001319
merged_variables = self._merge_variables(variables)
13011320
resolved_checks = resolve_variables(checks=checks, variables=merged_variables)
1321+
if semantic_validation_mode is not None: # <-- ADD THIS
1322+
SemanticValidator.apply(resolved_checks, mode=semantic_validation_mode) # <-- ADD THIS
13021323
handler = self._checks_handler_factory.create(config)
13031324
handler.save(resolved_checks, config)
13041325

0 commit comments

Comments
 (0)