This is an automated email from the ASF dual-hosted git repository.
shahar1 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 4c4b9c2d330 Raise a clear error when SqlToS3Operator has no Parquet
engine (#74817)
4c4b9c2d330 is described below
commit 4c4b9c2d330278c84e99157e72b24625d56cc3d0
Author: Dev-iL <[email protected]>
AuthorDate: Sun Oct 18 17:25:49 2026 +0300
Raise a clear error when SqlToS3Operator has no Parquet engine (#74817)
Check pandas engine requirements before querying, requiring pyarrow for the
pyarrow dtype backend and either pyarrow or fastparquet for Parquet output.
Point missing dependencies to the Amazon provider's parquet extra.
---
.../providers/amazon/aws/transfers/sql_to_s3.py | 30 ++++++-
.../unit/amazon/aws/transfers/test_sql_to_s3.py | 96 +++++++++++++++++++++-
2 files changed, 122 insertions(+), 4 deletions(-)
diff --git
a/providers/amazon/src/airflow/providers/amazon/aws/transfers/sql_to_s3.py
b/providers/amazon/src/airflow/providers/amazon/aws/transfers/sql_to_s3.py
index 17ef7ef7c86..5313aa26c4e 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/transfers/sql_to_s3.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/transfers/sql_to_s3.py
@@ -19,6 +19,7 @@ from __future__ import annotations
import enum
import gzip
+import importlib.util
import io
import warnings
from collections import namedtuple
@@ -27,7 +28,12 @@ from typing import TYPE_CHECKING, Any, Literal, cast
from airflow.exceptions import AirflowProviderDeprecationWarning
from airflow.providers.amazon.aws.hooks.s3 import S3Hook
-from airflow.providers.common.compat.sdk import AirflowException, BaseHook,
BaseOperator
+from airflow.providers.common.compat.sdk import (
+ AirflowException,
+ AirflowOptionalProviderFeatureException,
+ BaseHook,
+ BaseOperator,
+)
if TYPE_CHECKING:
import pandas as pd
@@ -185,8 +191,6 @@ class SqlToS3Operator(BaseOperator):
import numpy as np
import pandas as pd
except ImportError as e:
- from airflow.providers.common.compat.sdk import
AirflowOptionalProviderFeatureException
-
raise AirflowOptionalProviderFeatureException(e)
for col in df:
@@ -221,7 +225,27 @@ class SqlToS3Operator(BaseOperator):
return path[: -len(suffix)]
return path
+ def _ensure_parquet_engine(self) -> None:
+ if self.df_type != "pandas":
+ return
+ engines: tuple[str, ...]
+ if ("dtype_backend", "pyarrow") in self.read_kwargs.items():
+ # pandas builds the frame through pyarrow itself here, so
fastparquet cannot stand in
+ # for it even when the output format is Parquet.
+ trigger, engines = 'read_kwargs={"dtype_backend": "pyarrow"}',
("pyarrow",)
+ elif self.file_format == FILE_FORMAT.PARQUET:
+ trigger, engines = 'file_format="parquet"', ("pyarrow",
"fastparquet")
+ else:
+ return
+ if any(importlib.util.find_spec(engine) for engine in engines):
+ return
+
+ raise AirflowOptionalProviderFeatureException(
+ f"{trigger} needs {' or '.join(engines)}. Install
apache-airflow-providers-amazon[parquet]."
+ )
+
def execute(self, context: Context) -> None:
+ self._ensure_parquet_engine()
sql_hook = self._get_hook()
s3_conn = S3Hook(aws_conn_id=self.aws_conn_id, verify=self.verify)
data_df = sql_hook.get_df(
diff --git a/providers/amazon/tests/unit/amazon/aws/transfers/test_sql_to_s3.py
b/providers/amazon/tests/unit/amazon/aws/transfers/test_sql_to_s3.py
index c1100ea1b79..b97ee952b42 100644
--- a/providers/amazon/tests/unit/amazon/aws/transfers/test_sql_to_s3.py
+++ b/providers/amazon/tests/unit/amazon/aws/transfers/test_sql_to_s3.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import gzip
+import importlib.util
import io
from unittest import mock
@@ -29,10 +30,30 @@ from packaging.version import Version
from airflow.exceptions import AirflowProviderDeprecationWarning
from airflow.models import Connection
from airflow.providers.amazon.aws.transfers.sql_to_s3 import SqlToS3Operator
-from airflow.providers.common.compat.sdk import AirflowException
+from airflow.providers.common.compat.sdk import AirflowException,
AirflowOptionalProviderFeatureException
PANDAS_3_PLUS = Version(pd.__version__).major >= 3
+PARQUET_ENGINES = ("pyarrow", "fastparquet")
+REAL_FIND_SPEC = importlib.util.find_spec
+
+
+def build_find_spec_stub(available_engines):
+ """
+ Fake only the Parquet engines and resolve every other name for real.
+
+ The patch is installed process-wide and stays active while the operator is
constructed and
+ executed, so a stub answering None for every name would also answer
optional-dependency
+ probes these tests say nothing about, and a test could then pass for the
wrong reason.
+ """
+
+ def find_spec(name, package=None):
+ if name in PARQUET_ENGINES:
+ return object() if name in available_engines else None
+ return REAL_FIND_SPEC(name, package)
+
+ return find_spec
+
class TestSqlToS3Operator:
@pytest.mark.parametrize(
@@ -248,6 +269,79 @@ class TestSqlToS3Operator:
mock_fix_dtypes.assert_not_called()
+ @pytest.mark.parametrize(
+ ("df_type", "file_format", "read_kwargs", "available_engines",
"should_raise"),
+ [
+ pytest.param("pandas", "parquet", {}, set(), True,
id="pandas-parquet-no-engine"),
+ pytest.param("pandas", "parquet", {}, {"fastparquet"}, False,
id="pandas-parquet-fastparquet"),
+ pytest.param(
+ "pandas", "csv", {"dtype_backend": "pyarrow"}, set(), True,
id="pandas-pyarrow-backend"
+ ),
+ pytest.param(
+ "pandas",
+ "csv",
+ {"dtype_backend": "pyarrow"},
+ {"fastparquet"},
+ True,
+ id="pandas-pyarrow-backend-fastparquet-only",
+ ),
+ pytest.param(
+ "pandas",
+ "parquet",
+ {"dtype_backend": "pyarrow"},
+ {"fastparquet"},
+ True,
+ id="pandas-parquet-pyarrow-backend-fastparquet-only",
+ ),
+ pytest.param("pandas", "csv", {}, set(), False,
id="pandas-csv-no-engine"),
+ pytest.param("polars", "parquet", {}, set(), False,
id="polars-parquet-no-engine"),
+ ],
+ )
+ @mock.patch("importlib.util.find_spec", autospec=True)
+ def test_parquet_engine_required(
+ self, mock_find_spec, df_type, file_format, read_kwargs,
available_engines, should_raise
+ ):
+ mock_find_spec.side_effect = build_find_spec_stub(available_engines)
+ op = SqlToS3Operator(
+ query="query",
+ s3_bucket="bucket",
+ s3_key="key",
+ sql_conn_id="mysql_conn_id",
+ task_id="task_id",
+ df_type=df_type,
+ file_format=file_format,
+ read_kwargs=read_kwargs,
+ dag=None,
+ )
+
+ if should_raise:
+ with pytest.raises(AirflowOptionalProviderFeatureException,
match=r"\[parquet\]"):
+ op._ensure_parquet_engine()
+ else:
+ op._ensure_parquet_engine()
+
+ @mock.patch("airflow.providers.amazon.aws.transfers.sql_to_s3.S3Hook")
+ @mock.patch("importlib.util.find_spec", autospec=True)
+ def test_execute_checks_parquet_engine_before_querying(self,
mock_find_spec, mock_s3_hook):
+ mock_find_spec.side_effect = build_find_spec_stub(set())
+ mock_dbapi_hook = mock.Mock()
+ mock_dbapi_hook.return_value.get_df.return_value = pd.DataFrame({"a":
[1]})
+ op = SqlToS3Operator(
+ query="query",
+ s3_bucket="bucket",
+ s3_key="key",
+ sql_conn_id="mysql_conn_id",
+ task_id="task_id",
+ file_format="parquet",
+ dag=None,
+ )
+ op._get_hook = mock_dbapi_hook
+
+ with pytest.raises(AirflowOptionalProviderFeatureException,
match=r"\[parquet\]"):
+ op.execute(None)
+
+ mock_dbapi_hook.return_value.get_df.assert_not_called()
+
def test_invalid_file_format(self):
with pytest.raises(AirflowException):
SqlToS3Operator(