Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion docs/dqx/docs/reference/quality_checks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1654,7 +1654,7 @@ You can also define your own custom dataset-level checks (see [Creating custom c
| `sql_query` | Checks whether the condition column produced by a SQL query is satisfied. The check supports two modes: **Row-level validation** (when `merge_columns` is provided) - query results are joined back to the input DataFrame to mark specific rows; **Dataset-level validation** (when `merge_columns` is None or empty) - the check result applies to all rows (or filtered rows if `row_filter` is used), making it ideal for aggregate validations with custom metrics. The query must return a boolean condition column (True = fail, False = pass). For row-level checks: if merge columns aren't unique, multiple query rows can attach to a single input row, potentially causing false positives. Performance tip: for complex queries, writing a custom dataset-level rule is usually more performant than `sql_query` check. | `query`: query string, must return condition column (and merge columns if provided); `input_placeholder`: name to be used in the sql query as `{{ input_placeholder }}` to refer to the input DataFrame, optional reference DataFrames are referred by the name provided in the dictionary of reference DataFrames (e.g. `{{ ref_df_key }}`, dictionary of DataFrames can be passed when applying checks); `merge_columns`: (optional) list of columns used for merging with the input DataFrame which must exist in the input DataFrame and be present in output of the sql query; when not provided (None or empty list), the check result applies to all rows in the dataset (dataset-level validation); `condition_column`: name of the column indicating a violation (False = pass, True = fail); `msg`: (optional) message to output; `name`: (optional) name of the resulting check (it can be overwritten by `name` specified at the check level); `negate`: if the condition should be negated |
| `compare_datasets` | Compares two DataFrames at both row and column levels, providing detailed information about differences, including new or missing rows and column-level changes. Only columns present in both the source and reference DataFrames are compared. Use with caution if `check_missing_records` is enabled, as this may increase the number of rows in the output beyond the original input DataFrame. The comparison does not support Map types (any column comparison on map type is skipped automatically). Comparing datasets is valuable for validating data during migrations, detecting drift, performing regression testing, or verifying synchronization between source and target systems. | `columns`: columns to use for row matching with the reference DataFrame (can be a list of string column names or column expressions, but only simple column expressions are allowed such as 'F.col("col1")'), if not having primary keys or wanting to match against all columns you can pass 'df.columns'; `ref_columns`: list of columns in the reference DataFrame or Table to row match against the source DataFrame (can be a list of string column names or column expressions, but only simple column expressions are allowed such as 'F.col("col1")'), if not having primary keys or wanting to match against all columns you can pass 'ref_df.columns'; note that `columns` are matched with `ref_columns` by position, so the order of the provided columns in both lists must be exactly aligned; `exclude_columns`: (optional) list of columns to exclude from the value comparison but not from row matching (can be a list of string column names or column expressions, but only simple column expressions are allowed such as 'F.col("col1")'); the `exclude_columns` field does not alter the list of columns used to determine row matches (columns), it only controls which columns are skipped during the value comparison; `ref_df_name`: (optional) name of the reference DataFrame (dictionary of DataFrames can be passed when applying checks); `ref_table`: (optional) fully qualified reference table name; either `ref_df_name` or `ref_table` must be provided but never both; the number of passed `columns` and `ref_columns` must match and keys are checks in the given order; `check_missing_records`: perform a FULL OUTER JOIN to identify records that are missing from source or reference DataFrames, default is False; use with caution as this may increase the number of rows in the output, as unmatched rows from both sides are included; `null_safe_row_matching`: (optional) treat NULLs as equal when matching rows using `columns` and `ref_columns` (default: True); `null_safe_column_value_matching`: (optional) treat NULLs as equal when comparing column values (default: True) |
| `is_data_fresh_per_time_window` | Freshness check that validates whether at least X records arrive within every Y-minute time window. | `column`: timestamp column (can be a string column name or a column expression); `window_minutes`: time window in minutes to check for data arrival; `min_records_per_window`: minimum number of records expected per time window; `lookback_windows`: (optional) number of time windows to look back from `curr_timestamp`, it filters records to include only those within the specified number of time windows from `curr_timestamp` (if no lookback is provided, the check is applied to the entire dataset); `curr_timestamp`: (optional) current timestamp column (if not provided, current_timestamp() function is used) |
| `has_valid_schema` | Schema check that validates whether the DataFrame schema matches an expected schema. In non-strict mode, validates that all expected columns exist with compatible types (allows extra columns). In strict mode, validates exact schema match (same columns, same order, same types) for all columns by default or for all columns specified in `columns`. This check is applied at the dataset level and reports schema violations for all rows in the DataFrame when incompatibilities are detected. | `expected_schema`: (optional) expected schema as a DDL string (e.g., "id INT, name STRING") or StructType object; `ref_df_name`: (optional) name of the reference DataFrame to load the schema from (dictionary of DataFrames can be passed when applying checks); `ref_table`: (optional) fully qualified reference table name to load the schema from (e.g. "catalog.schema.table"); exactly one of `expected_schema`, `ref_df_name`, or `ref_table` must be provided; `columns`: (optional) list of columns to validate (if not provided, all columns are considered); `strict`: (optional) whether to perform strict schema validation (default: False) - False: validates that all expected columns exist with compatible types, True: validates exact schema match |
| `has_valid_schema` | Schema check that validates whether the DataFrame schema matches an expected schema. In non-strict mode, validates that all expected columns exist with compatible types (allows extra columns). In strict mode, validates exact schema match (same columns, same order, same types) for all columns by default or for all columns specified in `columns`. This check is applied at the dataset level and reports schema violations for all rows in the DataFrame when incompatibilities are detected. | `expected_schema`: (optional) expected schema as a DDL string (e.g., "id INT, name STRING") or StructType object; `ref_df_name`: (optional) name of the reference DataFrame to load the schema from (dictionary of DataFrames can be passed when applying checks); `ref_table`: (optional) fully qualified reference table name to load the schema from (e.g. "catalog.schema.table"); exactly one of `expected_schema`, `ref_df_name`, or `ref_table` must be provided; `columns`: (optional) list of columns to validate (if not provided, all columns are considered); `strict`: (optional) whether to perform strict schema validation (default: False) - False: validates that all expected columns exist with compatible types, True: validates exact schema match; `ignore_columns`: (optional) list of columns to ignore during validation (if not provided, all columns are considered); |
| `has_no_outliers` | Checks whether the values in the input column contain any outliers. This function implements a median absolute deviation (MAD) algorithm to find outliers. | `column`: column of type numeric to check (can be a string column name or a column expression); |

**Compare datasets check**
Expand Down Expand Up @@ -2001,6 +2001,16 @@ Complex data types are supported as well.
- id
- name

# has_valid_schema check with specific ignored columns
- criticality: warn
check:
function: has_valid_schema
arguments:
expected_schema: "id INT, name STRING, age INT, contact_info STRUCT<email: STRING, phone: STRING, address: STRING>"
ignore_columns:
- last_update_date
- last_updated_by

# has_valid_schema check using reference table
- criticality: error
check:
Expand Down Expand Up @@ -2456,6 +2466,25 @@ checks = [
},
),

# has_valid_schema check with specific ignored columns, expected schema defined using StructType
DQDatasetRule(
criticality="warn",
check_func=check_funcs.has_valid_schema,
check_func_kwargs={
"expected_schema": StructType([
StructField("id", IntegerType(), True),
StructField("name", StringType(), True),
StructField("age", IntegerType(), True),
StructField("contact_info", StructType([
StructField("email", StringType(), True),
StructField("phone", StringType(), True),
StructField("address", StringType(), True),
]), True)
]),
"columns": ["last_update_date", "last_updated_by"],
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
},
),

# has_valid_schema check using reference table
DQDatasetRule(
criticality="error",
Expand Down
12 changes: 11 additions & 1 deletion src/databricks/labs/dqx/check_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1931,6 +1931,7 @@ def has_valid_schema(
ref_table: str | None = None,
columns: list[str | Column] | None = None,
strict: bool = False,
ignore_columns: list[str] | None = None,
) -> tuple[Column, Callable]:
"""
Build a schema compatibility check condition and closure for dataset-level validation.
Expand All @@ -1948,6 +1949,8 @@ def has_valid_schema(
strict: Whether to perform strict schema validation (default: False).
- False: Validates that all expected columns exist with compatible types (allows extra columns)
- True: Validates exact schema match (same columns, same order, same types)
ignore_columns: Optional list of column names in the checked DataFrame schema to
ignore for validation.

Returns:
A tuple of:
Expand Down Expand Up @@ -1976,6 +1979,12 @@ def has_valid_schema(
if columns:
column_names = [get_column_name_or_alias(col) if not isinstance(col, str) else col for col in columns]

ignore_column_names: list[str] | None = None
if ignore_columns:
ignore_column_names = [
get_column_name_or_alias(col) if not isinstance(col, str) else col for col in ignore_columns
]

expected_schema = _get_schema(expected_schema or types.StructType(), column_names)

unique_str = uuid.uuid4().hex # make sure any column added to the dataframe is unique
Expand Down Expand Up @@ -2003,7 +2012,8 @@ def apply(df: DataFrame, spark: SparkSession, ref_dfs: dict[str, DataFrame]) ->
else:
_expected_schema = expected_schema

actual_schema = df.select(*columns).schema if columns else df.schema
base_df = df.select(*columns) if columns else df
actual_schema = base_df.drop(*(ignore_column_names or [])).schema
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated

if strict:
errors = _get_strict_schema_comparison(actual_schema, _expected_schema)
Expand Down
13 changes: 13 additions & 0 deletions src/databricks/labs/dqx/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pyspark.sql.streaming import StreamingQuery

from databricks.labs.dqx.base import DQEngineBase, DQEngineCoreBase
from databricks.labs.dqx import check_funcs
from databricks.labs.dqx.checks_resolver import resolve_custom_check_functions_from_path
from databricks.labs.dqx.checks_serializer import deserialize_checks
from databricks.labs.dqx.config_serializer import ConfigSerializer
Expand Down Expand Up @@ -345,6 +346,17 @@ def _all_are_dq_rules(checks: list[DQRule]) -> bool:
"""Check if all elements in the checks list are instances of DQRule."""
return all(isinstance(check, DQRule) for check in checks)

def _get_checks_with_ignored_result_columns(self, checks: list[DQRule]) -> list[DQRule]:
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
"""Get checks with ignored result columns for schema validation checks."""
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
for check in checks:
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
if check.check_func is not check_funcs.has_valid_schema:
continue
existing_ignored = check.check_func_kwargs.get("ignore_columns") or []
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
check.check_func_kwargs["ignore_columns"] = list(
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
set(existing_ignored + list(self._result_column_names.values()))
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
)
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
return checks
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated

def _append_empty_checks(self, df: DataFrame) -> DataFrame:
"""Append empty checks at the end of DataFrame.

Expand Down Expand Up @@ -385,6 +397,7 @@ def _create_results_array(
empty_result = F.lit(None).cast(dq_result_schema).alias(dest_col)
return df.select("*", empty_result)

checks = self._get_checks_with_ignored_result_columns(checks)
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
check_conditions = []
current_df = df

Expand Down
24 changes: 24 additions & 0 deletions tests/integration/test_dataset_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2774,3 +2774,27 @@ def test_has_valid_schema_with_ref_df_name(spark: SparkSession):
"a string, b string, has_invalid_schema string",
)
assert_df_equality(actual_condition_df, expected_condition_df, ignore_nullable=True)


def test_has_valid_schema_with_ignore_columns(spark: SparkSession):
test_df = spark.createDataFrame(
[
["str1", 1, 100.0, "extra"],
["str2", 2, 200.0, "data"],
],
"a string, b int, c double, d string",
)

expected_schema = "a string, b int, c string"
condition, apply_method = has_valid_schema(expected_schema, ignore_columns=["d"], strict=True)
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
actual_apply_df = apply_method(test_df, spark, {})
actual_condition_df = actual_apply_df.select("a", "b", "c", "d", condition)

expected_condition_df = spark.createDataFrame(
[
["str1", 1, 100.0, "extra", None],
["str2", 2, 200.0, "data", None],
],
"a string, b int, c double, d string, has_invalid_schema string",
)
assert_df_equality(actual_condition_df, expected_condition_df, ignore_nullable=True)
Comment thread
mwojtyczka marked this conversation as resolved.
Outdated
Loading