Skip to content

Commit 98c3ef1

Browse files
mwojtyczkaCopilot
andauthored
Update demo and docs for custom checks (#362)
## Changes * Improved demo and docs in various places for custom checks ### Tests - [x] manually tested - [x] added unit tests - [ ] added integration tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent fcb37f9 commit 98c3ef1

5 files changed

Lines changed: 136 additions & 36 deletions

File tree

demos/dqx_demo_library.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -458,9 +458,9 @@
458458
from pyspark.sql import Column
459459
from databricks.labs.dqx.check_funcs import make_condition
460460

461-
def ends_with_foo(column: str) -> Column:
461+
def not_ends_with(column: str, suffix: str) -> Column:
462462
col_expr = F.col(column)
463-
return make_condition(col_expr.endswith("foo"), f"Column {column} ends with foo", f"{column}_ends_with_foo")
463+
return make_condition(col_expr.endswith(suffix), f"Column {column} ends with {suffix}", f"{column}_ends_with_{suffix}")
464464

465465
# COMMAND ----------
466466

@@ -471,16 +471,20 @@ def ends_with_foo(column: str) -> Column:
471471

472472
from databricks.labs.dqx.engine import DQEngine
473473
from databricks.sdk import WorkspaceClient
474+
from databricks.labs.dqx.rule import DQRowRule
474475
from databricks.labs.dqx.check_funcs import is_not_null_and_not_empty, sql_expression
475476

476-
# use built-in, custom and sql expression checks
477+
477478
checks = [
478-
DQRowRule(criticality="error", check_func=is_not_null_and_not_empty, column="col1"),
479-
DQRowRule(criticality="warn", check_func=ends_with_foo, column="col1"),
479+
# custom check
480+
DQRowRule(criticality="warn", check_func=not_ends_with, column="col1", check_func_kwargs={"suffix": "foo"}),
481+
# sql expression check
480482
DQRowRule(criticality="warn", check_func=sql_expression, check_func_kwargs={
481483
"expression": "col1 like 'str%'", "msg": "col1 not starting with 'str'"
482484
}
483485
),
486+
# built-in check
487+
DQRowRule(criticality="error", check_func=is_not_null_and_not_empty, column="col1"),
484488
]
485489

486490
schema = "col1: string, col2: string"
@@ -502,25 +506,26 @@ def ends_with_foo(column: str) -> Column:
502506
from databricks.labs.dqx.engine import DQEngine
503507
from databricks.sdk import WorkspaceClient
504508

505-
# use built-in, custom and sql expression checks
509+
506510
checks = yaml.safe_load(
507511
"""
508-
- criticality: error
509-
check:
510-
function: is_not_null_and_not_empty
511-
arguments:
512-
column: col1
513512
- criticality: warn
514513
check:
515-
function: ends_with_foo
514+
function: not_ends_with
516515
arguments:
517516
column: col1
517+
suffix: foo
518518
- criticality: warn
519519
check:
520520
function: sql_expression
521521
arguments:
522522
expression: col1 like 'str%'
523523
msg: col1 not starting with 'str'
524+
- criticality: error
525+
check:
526+
function: is_not_null_and_not_empty
527+
arguments:
528+
column: col1
524529
"""
525530
)
526531

@@ -529,7 +534,7 @@ def ends_with_foo(column: str) -> Column:
529534

530535
dq_engine = DQEngine(WorkspaceClient())
531536

532-
custom_check_functions = {"ends_with_foo": ends_with_foo}
537+
custom_check_functions = {"not_ends_with": not_ends_with}
533538
# alternatively, you can also use globals to include all available functions
534539
#custom_check_functions = globals()
535540

docs/dqx/docs/reference/quality_rules.mdx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ This page provides a reference for the quality checks (rule functions) available
1010

1111
The following built-in row-level checks are currently available in DQX.
1212
These checks are applied to each row of a PySpark DataFrame and report quality issues as additional columns.
13-
You can also define your own custom checks (see [Creating custom user-defined checks](#creating-custom-user-defined-checks)).
13+
You can also define your own custom checks (see [Creating custom checks](#creating-custom-checks)).
1414

1515
Some of the checks such as `is_unique`, `is_aggr_not_greater_than`, or `is_aggr_not_less_than` are performed over a group of rows using window functions, while others operate on individual rows.
1616
In all cases, the quality check results are always reported for individual rows in the reporting columns.
@@ -1021,7 +1021,7 @@ For example, to check that a column `a` is not null only when a column `b` is po
10211021
column: col1
10221022
```
10231023
1024-
## Creating Custom User-Defined Checks
1024+
## Creating Custom Checks
10251025
10261026
### Using SQL Expression
10271027
@@ -1048,7 +1048,7 @@ SQL Expressions are also useful if you need to make cross-column validation, for
10481048

10491049
### Using Python function
10501050

1051-
If you need a reusable check or want to implement more complex logic which is challenging to implement using SQL expression, you can define your own custom check function.
1051+
If you need a reusable check or want to implement more complex logic which is challenging to implement using SQL expression, you can define your own custom check function in Python.
10521052
A check function is a callable that uses `make_condition` to return `pyspark.sql.Column`.
10531053

10541054
#### Custom check example
@@ -1060,9 +1060,10 @@ A check function is a callable that uses `make_condition` to return `pyspark.sql
10601060
from pyspark.sql import Column
10611061
from databricks.labs.dqx.check_funcs import make_condition
10621062
1063-
def ends_with_foo(column: str) -> Column:
1063+
def not_ends_with(column: str, suffix: str) -> Column:
10641064
col_expr = F.col(column)
1065-
return make_condition(col_expr.endswith("foo"), f"Column {column} ends with foo", f"{column}_ends_with_foo")
1065+
return make_condition(col_expr.endswith(suffix), f"Column {column} ends with {suffix}", f"{column}_ends_with_{suffix}")
1066+
10661067
```
10671068
</TabItem>
10681069
</Tabs>
@@ -1079,8 +1080,10 @@ A check function is a callable that uses `make_condition` to return `pyspark.sql
10791080
from databricks.labs.dqx.check_funcs import is_not_null
10801081
10811082
checks = [
1083+
# custom check
1084+
DQRowRule(criticality="error", check_func=not_ends_with, column="col1", check_func_kwargs={"suffix": "foo"}),
1085+
# built-in check
10821086
DQRowRule(criticality="error", check_func=is_not_null, column="col1"),
1083-
DQRowRule(criticality="error", check_func=ends_with_foo, column="col1"),
10841087
]
10851088
10861089
dq_engine = DQEngine(WorkspaceClient())
@@ -1089,7 +1092,7 @@ A check function is a callable that uses `make_condition` to return `pyspark.sql
10891092
valid_df, quarantine_df = dq_engine.apply_checks_and_split(input_df, checks)
10901093
10911094
# Option 2: apply quality rules on the dataframe and report issues as additional columns
1092-
valid_and_quarantine_df = dq_engine.apply_checks_by_metadata(input_df, checks)
1095+
valid_and_quarantine_df = dq_engine.apply_checks(input_df, checks)
10931096
```
10941097
</TabItem>
10951098
</Tabs>
@@ -1106,9 +1109,10 @@ A check function is a callable that uses `make_condition` to return `pyspark.sql
11061109
checks = yaml.safe_load("""
11071110
- criticality: error
11081111
check:
1109-
function: ends_with_foo
1112+
function: not_ends_with
11101113
arguments:
11111114
column: col1
1115+
suffix: foo
11121116
- criticality: error
11131117
check:
11141118
function: is_not_null
@@ -1118,7 +1122,7 @@ A check function is a callable that uses `make_condition` to return `pyspark.sql
11181122
11191123
dq_engine = DQEngine(WorkspaceClient())
11201124
1121-
custom_check_functions = {"ends_with_foo": ends_with_foo} # list of custom check functions
1125+
custom_check_functions = {"not_ends_with": not_ends_with} # list of custom check functions
11221126
# or include all functions with globals() for simplicity
11231127
#custom_check_functions=globals()
11241128

src/databricks/labs/dqx/engine.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,15 @@ def apply_checks_and_split(self, df: DataFrame, checks: list[DQRule]) -> tuple[D
8686
def apply_checks_by_metadata_and_split(
8787
self, df: DataFrame, checks: list[dict], custom_check_functions: dict[str, Any] | None = None
8888
) -> tuple[DataFrame, DataFrame]:
89-
dq_rule_checks = self.build_checks_by_metadata(checks, custom_check_functions)
89+
dq_rule_checks = self.build_quality_rules_by_metadata(checks, custom_check_functions)
9090

9191
good_df, bad_df = self.apply_checks_and_split(df, dq_rule_checks)
9292
return good_df, bad_df
9393

9494
def apply_checks_by_metadata(
9595
self, df: DataFrame, checks: list[dict], custom_check_functions: dict[str, Any] | None = None
9696
) -> DataFrame:
97-
dq_rule_checks = self.build_checks_by_metadata(checks, custom_check_functions)
97+
dq_rule_checks = self.build_quality_rules_by_metadata(checks, custom_check_functions)
9898

9999
return self.apply_checks(df, dq_rule_checks)
100100

@@ -219,7 +219,7 @@ def build_dataframe_from_quality_rules(
219219
if spark is None:
220220
spark = SparkSession.builder.getOrCreate()
221221

222-
dq_rule_checks = DQEngineCore.build_checks_by_metadata(checks)
222+
dq_rule_checks = DQEngineCore.build_quality_rules_by_metadata(checks)
223223

224224
dq_rule_rows = []
225225
for dq_rule_check in dq_rule_checks:
@@ -246,7 +246,9 @@ def build_dataframe_from_quality_rules(
246246
return spark.createDataFrame(dq_rule_rows, DQEngineCore.CHECKS_TABLE_SCHEMA)
247247

248248
@staticmethod
249-
def build_checks_by_metadata(checks: list[dict], custom_checks: dict[str, Any] | None = None) -> list[DQRule]:
249+
def build_quality_rules_by_metadata(
250+
checks: list[dict], custom_checks: dict[str, Any] | None = None
251+
) -> list[DQRule]:
250252
"""Build checks based on check specification, i.e. function name plus arguments.
251253
252254
:param checks: list of dictionaries describing checks. Each check is a dictionary
@@ -327,9 +329,9 @@ def build_checks_by_metadata(checks: list[dict], custom_checks: dict[str, Any] |
327329
return dq_rule_checks
328330

329331
@staticmethod
330-
def build_checks(*rules_col_set: DQRowRuleForEachCol) -> list[DQRule]:
332+
def build_quality_rules_foreach_col(*rules_col_set: DQRowRuleForEachCol) -> list[DQRule]:
331333
"""
332-
Build rules from dq rules and rule sets.
334+
Build rules for each column from DQRowRuleForEachCol sets.
333335
334336
:param rules_col_set: list of dq rules which define multiple columns for the same check function
335337
:return: list of dq rules

tests/integration/test_apply_checks.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,13 @@ def custom_check_func_global(column: str) -> Column:
12571257
return check_funcs.make_condition(col_expr.isNull(), "custom check failed", f"{column}_is_null_custom")
12581258

12591259

1260+
def custom_check_func_custom_args(column_custom_arg: str) -> Column:
1261+
col_expr = F.col(column_custom_arg)
1262+
return check_funcs.make_condition(
1263+
col_expr.isNull(), "custom check with custom args failed", f"{column_custom_arg}_is_null_custom_args"
1264+
)
1265+
1266+
12601267
def custom_check_func_global_a_column_no_args() -> Column:
12611268
col_expr = F.col("a")
12621269
return check_funcs.make_condition(col_expr.isNull(), "custom check without args failed", "a_is_null_custom")
@@ -1275,8 +1282,13 @@ def test_apply_checks_with_custom_check(ws, spark):
12751282
checks = [
12761283
DQRowRule(criticality="warn", check_func=check_funcs.is_not_null_and_not_empty, column="a"),
12771284
DQRowRule(criticality="warn", check_func=custom_check_func_global, column="a"),
1285+
DQRowRule(criticality="warn", check_func=custom_check_func_global, check_func_kwargs={"column": "a"}),
12781286
DQRowRule(criticality="warn", check_func=custom_check_func_global_annotated, column="a"),
1287+
DQRowRule(criticality="warn", check_func=custom_check_func_global_annotated, check_func_kwargs={"column": "a"}),
12791288
DQRowRule(criticality="warn", check_func=custom_check_func_global_a_column_no_args),
1289+
DQRowRule(
1290+
criticality="warn", check_func=custom_check_func_custom_args, check_func_kwargs={"column_custom_arg": "a"}
1291+
),
12801292
]
12811293

12821294
checked = dq_engine.apply_checks(test_df, checks)
@@ -1309,6 +1321,15 @@ def test_apply_checks_with_custom_check(ws, spark):
13091321
"run_time": RUN_TIME,
13101322
"user_metadata": {},
13111323
},
1324+
{
1325+
"name": "a_is_null_custom",
1326+
"message": "custom check failed",
1327+
"columns": None,
1328+
"filter": None,
1329+
"function": "custom_check_func_global",
1330+
"run_time": RUN_TIME,
1331+
"user_metadata": {},
1332+
},
13121333
{
13131334
"name": "a_is_null_custom",
13141335
"message": "custom check annotated failed",
@@ -1318,6 +1339,15 @@ def test_apply_checks_with_custom_check(ws, spark):
13181339
"run_time": RUN_TIME,
13191340
"user_metadata": {},
13201341
},
1342+
{
1343+
"name": "a_is_null_custom",
1344+
"message": "custom check annotated failed",
1345+
"columns": None,
1346+
"filter": None,
1347+
"function": "custom_check_func_global_annotated",
1348+
"run_time": RUN_TIME,
1349+
"user_metadata": {},
1350+
},
13211351
{
13221352
"name": "a_is_null_custom",
13231353
"message": "custom check without args failed",
@@ -1327,6 +1357,15 @@ def test_apply_checks_with_custom_check(ws, spark):
13271357
"run_time": RUN_TIME,
13281358
"user_metadata": {},
13291359
},
1360+
{
1361+
"name": "a_is_null_custom_args",
1362+
"message": "custom check with custom args failed",
1363+
"columns": None,
1364+
"filter": None,
1365+
"function": "custom_check_func_custom_args",
1366+
"run_time": RUN_TIME,
1367+
"user_metadata": {},
1368+
},
13301369
],
13311370
],
13321371
[
@@ -1353,6 +1392,15 @@ def test_apply_checks_with_custom_check(ws, spark):
13531392
"run_time": RUN_TIME,
13541393
"user_metadata": {},
13551394
},
1395+
{
1396+
"name": "a_is_null_custom",
1397+
"message": "custom check failed",
1398+
"columns": None,
1399+
"filter": None,
1400+
"function": "custom_check_func_global",
1401+
"run_time": RUN_TIME,
1402+
"user_metadata": {},
1403+
},
13561404
{
13571405
"name": "a_is_null_custom",
13581406
"message": "custom check annotated failed",
@@ -1362,6 +1410,15 @@ def test_apply_checks_with_custom_check(ws, spark):
13621410
"run_time": RUN_TIME,
13631411
"user_metadata": {},
13641412
},
1413+
{
1414+
"name": "a_is_null_custom",
1415+
"message": "custom check annotated failed",
1416+
"columns": None,
1417+
"filter": None,
1418+
"function": "custom_check_func_global_annotated",
1419+
"run_time": RUN_TIME,
1420+
"user_metadata": {},
1421+
},
13651422
{
13661423
"name": "a_is_null_custom",
13671424
"message": "custom check without args failed",
@@ -1371,6 +1428,15 @@ def test_apply_checks_with_custom_check(ws, spark):
13711428
"run_time": RUN_TIME,
13721429
"user_metadata": {},
13731430
},
1431+
{
1432+
"name": "a_is_null_custom_args",
1433+
"message": "custom check with custom args failed",
1434+
"columns": None,
1435+
"filter": None,
1436+
"function": "custom_check_func_custom_args",
1437+
"run_time": RUN_TIME,
1438+
"user_metadata": {},
1439+
},
13741440
],
13751441
],
13761442
],
@@ -1391,6 +1457,10 @@ def test_apply_checks_by_metadata_with_custom_check(ws, spark):
13911457
"criticality": "warn",
13921458
"check": {"function": "custom_check_func_global_annotated", "arguments": {"column": "a"}},
13931459
},
1460+
{
1461+
"criticality": "warn",
1462+
"check": {"function": "custom_check_func_custom_args", "arguments": {"column_custom_arg": "a"}},
1463+
},
13941464
]
13951465

13961466
checked = dq_engine.apply_checks_by_metadata(
@@ -1399,6 +1469,7 @@ def test_apply_checks_by_metadata_with_custom_check(ws, spark):
13991469
{
14001470
"custom_check_func_global": custom_check_func_global,
14011471
"custom_check_func_global_annotated": custom_check_func_global_annotated,
1472+
"custom_check_func_custom_args": custom_check_func_custom_args,
14021473
},
14031474
)
14041475
# or for simplicity use globals
@@ -1441,6 +1512,15 @@ def test_apply_checks_by_metadata_with_custom_check(ws, spark):
14411512
"run_time": RUN_TIME,
14421513
"user_metadata": {},
14431514
},
1515+
{
1516+
"name": "a_is_null_custom_args",
1517+
"message": "custom check with custom args failed",
1518+
"columns": None,
1519+
"filter": None,
1520+
"function": "custom_check_func_custom_args",
1521+
"run_time": RUN_TIME,
1522+
"user_metadata": {},
1523+
},
14441524
],
14451525
],
14461526
[
@@ -1476,6 +1556,15 @@ def test_apply_checks_by_metadata_with_custom_check(ws, spark):
14761556
"run_time": RUN_TIME,
14771557
"user_metadata": {},
14781558
},
1559+
{
1560+
"name": "a_is_null_custom_args",
1561+
"message": "custom check with custom args failed",
1562+
"columns": None,
1563+
"filter": None,
1564+
"function": "custom_check_func_custom_args",
1565+
"run_time": RUN_TIME,
1566+
"user_metadata": {},
1567+
},
14791568
],
14801569
],
14811570
],

0 commit comments

Comments
 (0)