This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 83149aaf222 Validate DatabricksSQLStatementsSensor exclusivity at
__init__ (#70831)
83149aaf222 is described below
commit 83149aaf22233bd6be2b644ab3ce8a262b57ba2e
Author: ahilashsasidharan <[email protected]>
AuthorDate: Tue Sep 8 20:19:05 2026 -0400
Validate DatabricksSQLStatementsSensor exclusivity at __init__ (#70831)
* Validate DatabricksSQLStatementsSensor exclusivity at __init__
* Change DatabricksSQLStatementsSensor init exclusivity check to raise
ValueError over AirflowException
* Enhance tests and replace nearby AirflowExceptions with ValueError
* Add additional test coverage for render_template_as_native_obj=True
scenario and update error message to hold for all failure scenarios
---
generated/known_airflow_exceptions.txt | 2 +-
.../providers/databricks/sensors/databricks.py | 13 +--
.../unit/databricks/sensors/test_databricks.py | 93 ++++++++++++++++++++--
3 files changed, 95 insertions(+), 13 deletions(-)
diff --git a/generated/known_airflow_exceptions.txt
b/generated/known_airflow_exceptions.txt
index fd54d247222..cd9b8e3c99c 100644
--- a/generated/known_airflow_exceptions.txt
+++ b/generated/known_airflow_exceptions.txt
@@ -179,7 +179,7 @@
providers/databricks/src/airflow/providers/databricks/operators/databricks_repos
providers/databricks/src/airflow/providers/databricks/operators/databricks_sql.py::8
providers/databricks/src/airflow/providers/databricks/operators/databricks_workflow.py::4
providers/databricks/src/airflow/providers/databricks/plugins/databricks_workflow.py::7
-providers/databricks/src/airflow/providers/databricks/sensors/databricks.py::4
+providers/databricks/src/airflow/providers/databricks/sensors/databricks.py::1
providers/databricks/src/airflow/providers/databricks/sensors/databricks_partition.py::4
providers/databricks/src/airflow/providers/databricks/sensors/databricks_sql.py::1
providers/databricks/src/airflow/providers/databricks/utils/databricks.py::3
diff --git
a/providers/databricks/src/airflow/providers/databricks/sensors/databricks.py
b/providers/databricks/src/airflow/providers/databricks/sensors/databricks.py
index 729d84af783..8ab7d327af3 100644
---
a/providers/databricks/src/airflow/providers/databricks/sensors/databricks.py
+++
b/providers/databricks/src/airflow/providers/databricks/sensors/databricks.py
@@ -69,9 +69,11 @@ class
DatabricksSQLStatementsSensor(DatabricksSQLStatementsMixin, BaseSensorOper
include_airflow_query_tags: bool = True,
**kwargs,
):
- # Handle the scenario where either both statement and statement_id are
set/not set
+ if statement is not None and statement_id is not None:
+ raise ValueError("Provide exactly one of statement or
statement_id.")
+
if not warehouse_id:
- raise AirflowException("warehouse_id must be provided.")
+ raise ValueError("warehouse_id must be provided.")
super().__init__(**kwargs)
@@ -107,10 +109,11 @@ class
DatabricksSQLStatementsSensor(DatabricksSQLStatementsMixin, BaseSensorOper
)
def execute(self, context: Context):
- if self.statement and self.statement_id:
- raise AirflowException("Cannot provide both statement and
statement_id.")
+ # Both fields are templated, so "neither resolves to a value" is only
knowable
+ # after rendering — __init__ cannot catch it. The both-provided case
is a pure
+ # provision error and is checked there instead.
if not self.statement and not self.statement_id:
- raise AirflowException("One of either statement or statement_id
must be provided.")
+ raise ValueError("One of either statement or statement_id must be
provided.")
if not self.statement_id:
# Otherwise, we'll go ahead and "submit" the statement
tags = build_query_tags(context, self.query_tags,
self.include_airflow_query_tags)
diff --git
a/providers/databricks/tests/unit/databricks/sensors/test_databricks.py
b/providers/databricks/tests/unit/databricks/sensors/test_databricks.py
index 08517a17f88..c0a40738246 100644
--- a/providers/databricks/tests/unit/databricks/sensors/test_databricks.py
+++ b/providers/databricks/tests/unit/databricks/sensors/test_databricks.py
@@ -22,7 +22,8 @@ from unittest import mock
import pytest
from tenacity import stop_after_attempt, wait_incrementing
-from airflow.providers.common.compat.sdk import AirflowException, TaskDeferred
+from airflow.models.dag import DAG
+from airflow.providers.common.compat.sdk import AirflowException,
TaskDeferred, timezone
from airflow.providers.databricks.hooks.databricks import SQLStatementState
from airflow.providers.databricks.sensors.databricks import
DatabricksSQLStatementsSensor
from airflow.providers.databricks.triggers.databricks import
DatabricksSQLStatementExecutionTrigger
@@ -69,17 +70,95 @@ class TestDatabricksSQLStatementsSensor:
assert op.warehouse_id == WAREHOUSE_ID
@pytest.mark.parametrize(
- ("kwargs", "match"),
+ ("statement", "statement_id"),
[
- ({"statement": STATEMENT, "statement_id": STATEMENT_ID}, "Cannot
provide both"),
- ({}, "One of either statement or statement_id"),
+ (STATEMENT, STATEMENT_ID),
+ (STATEMENT, ""),
+ ("", ""),
],
)
- def test_statement_combination_validated_at_execute(self, kwargs, match):
- op = DatabricksSQLStatementsSensor(task_id=TASK_ID,
warehouse_id=WAREHOUSE_ID, **kwargs)
- with pytest.raises(AirflowException, match=match):
+ def test_both_statements_included_validated_at_init(self, statement,
statement_id):
+ with pytest.raises(ValueError, match="Provide exactly one of"):
+ DatabricksSQLStatementsSensor(
+ statement=statement,
+ statement_id=statement_id,
+ task_id=TASK_ID,
+ warehouse_id=WAREHOUSE_ID,
+ )
+
+ @pytest.mark.parametrize(
+ ("statement", "statement_id"),
+ [
+ ("{{ None }}", STATEMENT_ID),
+ (STATEMENT, "{{ None }}"),
+ ("{{ None }}", "{{ None }}"),
+ ],
+ )
+ def
test_both_provided_with_template_renders_to_none_validated_at_init(self,
statement, statement_id):
+ """
+ Both statement and statement_id are provided; at least one is a
template that
+ would render to None under render_template_as_native_obj=True. The
constructor
+ must raise before any rendering occurs, so the check is pinned to
__init__.
+ If the exclusivity check were moved to execute(), this test would fail
because
+ the constructor would succeed and if rendered before execute() the
template
+ would render to None making it appear only one value was provided so
the moved
+ check would not catch the exclusivity violation
+ """
+ dag = DAG(
+ dag_id="test_native_obj_dag",
+ start_date=timezone.datetime(2025, 1, 1),
+ schedule=None,
+ render_template_as_native_obj=True,
+ )
+ with pytest.raises(ValueError, match="Provide exactly one of"):
+ DatabricksSQLStatementsSensor(
+ task_id=TASK_ID,
+ warehouse_id=WAREHOUSE_ID,
+ statement=statement,
+ statement_id=statement_id,
+ dag=dag,
+ )
+
+ @pytest.mark.parametrize(
+ ("statement", "statement_id"),
+ [
+ (None, None),
+ ("", None),
+ ],
+ )
+ def test_both_statements_missing_validated_at_execute(self, statement,
statement_id):
+ op = DatabricksSQLStatementsSensor(
+ task_id=TASK_ID, warehouse_id=WAREHOUSE_ID, statement=statement,
statement_id=statement_id
+ )
+ with pytest.raises(ValueError, match="One of either statement or
statement_id"):
op.execute(None)
+ @pytest.mark.parametrize(
+ ("statement", "statement_id"),
+ [
+ (None, "{{ None }}"),
+ ("{{ None }}", None),
+ ],
+ )
+ def test_both_missing_after_template_rendered_validated_at_execute(self,
statement, statement_id):
+ dag = DAG(
+ dag_id="test_native_obj_dag",
+ start_date=timezone.datetime(2025, 1, 1),
+ schedule=None,
+ render_template_as_native_obj=True,
+ )
+ op = DatabricksSQLStatementsSensor(
+ task_id=TASK_ID,
+ warehouse_id=WAREHOUSE_ID,
+ statement=statement,
+ statement_id=statement_id,
+ dag=dag,
+ )
+ context = {"dag": dag}
+ op.render_template_fields(context)
+ with pytest.raises(ValueError, match="One of either statement or
statement_id"):
+ op.execute(context)
+
@mock.patch("airflow.providers.databricks.sensors.databricks.DatabricksHook")
def test_exec_success(self, db_mock_class):
"""