-
Notifications
You must be signed in to change notification settings - Fork 128
Add documentation for using DQEngine in foreachBatch #948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a7e3133
d1e44d6
b3c6016
14db020
6ef1a10
6dc8aa6
954cb84
4906737
88cf19f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -418,6 +418,55 @@ You can define checks using DQX classes or as metadata (list of dictionaries) or | |||||||
| </TabItem> | ||||||||
| </Tabs> | ||||||||
|
|
||||||||
| ### Applying checks using foreachBatch | ||||||||
|
|
||||||||
| Spark's `foreachBatch` allows users to define an arbitrary processing function run for each streaming batch. DQX requires authentication with a Databricks SDK `WorkspaceClient` which is not supported inside `foreachBatch`. To use DQX in a `foreachBatch` function, create a `DQEngine` instance with a mock `WorkspaceClient`. | ||||||||
|
|
||||||||
| <Admonition type="warning" title="Engine functionality with mocked WorkspaceClient"> | ||||||||
| Some DQX engine methods require a valid Databricks `WorkspaceClient`. Methods must support local execution to be run in a `foreachBatch` function with a mocked `WorkspaceClient`. Check the **Supports local execution** column [here](/docs/reference/engine/#dqx-engine-methods) to verify that DQX engine methods can be executed with a mocked client. | ||||||||
| </Admonition> | ||||||||
|
|
||||||||
| ```python | ||||||||
| from unittest.mock import MagicMock | ||||||||
| from pyspark.sql import DataFrame | ||||||||
| from databricks.labs.dqx import check_funcs | ||||||||
| from databricks.labs.dqx.engine import DQEngine | ||||||||
| from databricks.labs.dqx.rule import DQRowRule | ||||||||
| from databricks.sdk import WorkspaceClient | ||||||||
|
|
||||||||
| # Define DQX checks | ||||||||
| checks = [ | ||||||||
| DQRowRule( | ||||||||
| name="col_timestamp_is_null", | ||||||||
| criticality="error", | ||||||||
| check_func=check_funcs.is_not_null, | ||||||||
| column="timestamp" | ||||||||
| ), | ||||||||
| DQRowRule( | ||||||||
| name="col_value_is_null", | ||||||||
| criticality="error", | ||||||||
| check_func=check_funcs.is_not_null, | ||||||||
| column="value" | ||||||||
| ) | ||||||||
| ] | ||||||||
|
|
||||||||
| # Define a `foreachBatch` function to check and write the data | ||||||||
| def validate_and_write_micro_batch_data(batch_df: DataFrame, _: int) -> None: | ||||||||
| # Create a mock `WorkspaceClient` | ||||||||
| mock_ws = MagicMock(spec=WorkspaceClient) | ||||||||
|
|
||||||||
| # Create the `DQEngine` | ||||||||
| dq_engine = DQEngine(mock_ws) | ||||||||
|
|
||||||||
| # Apply the DQX checks and write the data | ||||||||
| valid_and_invalid_df = dq_engine.apply_checks(batch_df, checks) | ||||||||
| valid_and_invalid_df.write.format("delta").saveAsTable("catalog.schema.output") | ||||||||
|
|
||||||||
| # Read the data and apply the `foreachBatch` function | ||||||||
| input_data = spark.readStream.format("rate-micro-batch").option("rowsPerBatch", 100).load() | ||||||||
| input_data.writeStream.foreachBatch(validate_and_write_micro_batch_data).start() | ||||||||
|
||||||||
| input_data.writeStream.foreachBatch(validate_and_write_micro_batch_data).start() | |
| query = input_data.writeStream.trigger(availableNow=True).foreachBatch(validate_and_write_micro_batch_data).start() | |
| query.awaitTermination() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Databricks notebook source | ||
|
|
||
| dbutils.widgets.text("test_library_ref", "", "Test Library Ref") | ||
| %pip install 'databricks-labs-dqx @ {dbutils.widgets.get("test_library_ref")}' chispa==0.10.1 | ||
|
|
||
| # COMMAND ---------- | ||
|
|
||
| dbutils.library.restartPython() | ||
|
|
||
| # COMMAND ---------- | ||
| # DBTITLE 1,test_apply_checks_foreachbatch | ||
|
|
||
| from unittest.mock import MagicMock | ||
| from pyspark.sql import DataFrame | ||
| from pyspark.sql.functions import lit | ||
| from chispa import assert_df_equality | ||
|
|
||
| from databricks.labs.dqx import check_funcs | ||
| from databricks.labs.dqx.engine import DQEngine | ||
| from databricks.labs.dqx.rule import DQRowRule | ||
| from databricks.labs.dqx.schema import dq_result_schema | ||
| from databricks.sdk import WorkspaceClient | ||
|
|
||
| def test_apply_checks_foreachbatch(): | ||
| input_df = spark.readStream.format("rate-micro-batch").option("rowsPerBatch", 100).load() | ||
| checks = [ | ||
| DQRowRule( # 'rate-micro-batch' source has a non-null 'value' column of type 'long' | ||
| name="value_is_null_or_empty", | ||
| criticality="warn", | ||
| check_func=check_funcs.is_not_null_and_not_empty, | ||
| column="value", | ||
| ), | ||
| ] | ||
|
|
||
| def batch_handler_function(batch_df: DataFrame, _: int) -> None: | ||
| mock_ws = MagicMock(spec=WorkspaceClient) | ||
| engine = DQEngine(mock_ws) | ||
| expected = batch_df.select( | ||
| "*", | ||
| lit(None).cast(dq_result_schema.simpleString()).alias("_errors"), | ||
| lit(None).cast(dq_result_schema.simpleString()).alias("_warnings"), | ||
| ) | ||
| actual = engine.apply_checks(batch_df, checks) | ||
| assert_df_equality(actual, expected) | ||
|
|
||
| input_df.writeStream.trigger(availableNow=True).foreachBatch(batch_handler_function).start().awaitTermination() | ||
|
|
||
| test_apply_checks_foreachbatch() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import logging | ||
| from pathlib import Path | ||
|
|
||
| from tests.e2e.conftest import run_notebook_job | ||
|
|
||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def test_run_apply_checks_foreachbatch_notebook(make_notebook, make_job, library_ref): | ||
| notebook_path = Path(__file__).parent / "notebooks" / "apply_checks_foreachbatch_notebook.py" | ||
| run_notebook_job( | ||
| notebook_path=notebook_path, | ||
| make_notebook=make_notebook, | ||
| make_job=make_job, | ||
| library_reference=library_ref, | ||
| task_key="observable_metrics_notebook", | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe add info that workspace client authentication is not supported by spark inside the foreach batch.