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 bdd4708aa08 Enhancement: AWS Provider sql to s3 operator pd.read_sql 
kwargs (#53399)
bdd4708aa08 is described below

commit bdd4708aa08d47d3bc6d997b936a82f6dfe7b7cd
Author: Bryan Corder <[email protected]>
AuthorDate: Fri Jul 18 02:18:51 2025 -0400

    Enhancement: AWS Provider sql to s3 operator pd.read_sql kwargs (#53399)
    
    * initial commit for changes
    
    * added unit test
---
 .../providers/amazon/aws/transfers/sql_to_s3.py    | 11 ++++-
 .../unit/amazon/aws/transfers/test_sql_to_s3.py    | 49 ++++++++++++++++++++--
 2 files changed, 54 insertions(+), 6 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 e31652af4d4..344935215b7 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
@@ -69,6 +69,7 @@ class SqlToS3Operator(BaseOperator):
     :param sql_hook_params: Extra config params to be passed to the underlying 
hook.
         Should match the desired hook constructor params.
     :param parameters: (optional) the parameters to render the SQL query with.
+    :param read_pd_kwargs: arguments to include in DataFrame when 
``pd.read_sql()`` is called.
     :param aws_conn_id: reference to a specific S3 connection
     :param verify: Whether or not to verify SSL certificates for S3 connection.
         By default SSL certificates are verified.
@@ -97,6 +98,7 @@ class SqlToS3Operator(BaseOperator):
     template_fields_renderers = {
         "query": "sql",
         "pd_kwargs": "json",
+        "read_pd_kwargs": "json",
     }
 
     def __init__(
@@ -108,6 +110,7 @@ class SqlToS3Operator(BaseOperator):
         sql_conn_id: str,
         sql_hook_params: dict | None = None,
         parameters: None | Mapping[str, Any] | list | tuple = None,
+        read_pd_kwargs: dict | None = None,
         replace: bool = False,
         aws_conn_id: str | None = "aws_default",
         verify: bool | str | None = None,
@@ -127,6 +130,7 @@ class SqlToS3Operator(BaseOperator):
         self.replace = replace
         self.pd_kwargs = pd_kwargs or {}
         self.parameters = parameters
+        self.read_pd_kwargs = read_pd_kwargs or {}
         self.max_rows_per_file = max_rows_per_file
         self.groupby_kwargs = groupby_kwargs or {}
         self.sql_hook_params = sql_hook_params
@@ -185,9 +189,12 @@ class SqlToS3Operator(BaseOperator):
     def execute(self, context: Context) -> None:
         sql_hook = self._get_hook()
         s3_conn = S3Hook(aws_conn_id=self.aws_conn_id, verify=self.verify)
-        data_df = sql_hook.get_df(sql=self.query, parameters=self.parameters, 
df_type="pandas")
+        data_df = sql_hook.get_df(
+            sql=self.query, parameters=self.parameters, df_type="pandas", 
**self.read_pd_kwargs
+        )
         self.log.info("Data from SQL obtained")
-        self._fix_dtypes(data_df, self.file_format)
+        if ("dtype_backend", "pyarrow") not in self.read_pd_kwargs.items():
+            self._fix_dtypes(data_df, self.file_format)
         file_options = FILE_OPTIONS_MAP[self.file_format]
 
         for group_name, df in self._partition_dataframe(df=data_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 61745829157..0d3791cc8d5 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
@@ -30,8 +30,9 @@ from airflow.providers.amazon.aws.transfers.sql_to_s3 import 
SqlToS3Operator
 
 
 class TestSqlToS3Operator:
+    @pytest.mark.parametrize("dtype_backend", ["numpy_nullable", "pyarrow"])
     @mock.patch("airflow.providers.amazon.aws.transfers.sql_to_s3.S3Hook")
-    def test_execute_csv(self, mock_s3_hook):
+    def test_execute_csv(self, mock_s3_hook, dtype_backend):
         query = "query"
         s3_bucket = "bucket"
         s3_key = "key"
@@ -49,6 +50,7 @@ class TestSqlToS3Operator:
             aws_conn_id="aws_conn_id",
             task_id="task_id",
             replace=True,
+            read_pd_kwargs={"dtype_backend": dtype_backend},
             pd_kwargs={"index": False, "header": False},
             dag=None,
         )
@@ -56,15 +58,18 @@ class TestSqlToS3Operator:
         op.execute(None)
 
         mock_s3_hook.assert_called_once_with(aws_conn_id="aws_conn_id", 
verify=None)
-        get_df_mock.assert_called_once_with(sql=query, parameters=None, 
df_type="pandas")
+        get_df_mock.assert_called_once_with(
+            sql=query, parameters=None, df_type="pandas", 
dtype_backend=dtype_backend
+        )
         file_obj = 
mock_s3_hook.return_value.load_file_obj.call_args[1]["file_obj"]
         assert isinstance(file_obj, io.BytesIO)
         mock_s3_hook.return_value.load_file_obj.assert_called_once_with(
             file_obj=file_obj, key=s3_key, bucket_name=s3_bucket, replace=True
         )
 
+    @pytest.mark.parametrize("dtype_backend", ["numpy_nullable", "pyarrow"])
     @mock.patch("airflow.providers.amazon.aws.transfers.sql_to_s3.S3Hook")
-    def test_execute_parquet(self, mock_s3_hook):
+    def test_execute_parquet(self, mock_s3_hook, dtype_backend):
         query = "query"
         s3_bucket = "bucket"
         s3_key = "key"
@@ -82,6 +87,7 @@ class TestSqlToS3Operator:
             sql_conn_id="mysql_conn_id",
             aws_conn_id="aws_conn_id",
             task_id="task_id",
+            read_pd_kwargs={"dtype_backend": dtype_backend},
             file_format="parquet",
             replace=True,
             dag=None,
@@ -90,7 +96,10 @@ class TestSqlToS3Operator:
         op.execute(None)
 
         mock_s3_hook.assert_called_once_with(aws_conn_id="aws_conn_id", 
verify=None)
-        get_df_mock.assert_called_once_with(sql=query, parameters=None, 
df_type="pandas")
+        get_df_mock.assert_called_once_with(
+            sql=query, parameters=None, df_type="pandas", 
dtype_backend=dtype_backend
+        )
+
         file_obj = 
mock_s3_hook.return_value.load_file_obj.call_args[1]["file_obj"]
         assert isinstance(file_obj, io.BytesIO)
         mock_s3_hook.return_value.load_file_obj.assert_called_once_with(
@@ -192,6 +201,38 @@ class TestSqlToS3Operator:
         assert dirty_df["strings"].values[2] == params["null_string_result"]
         assert dirty_df["ints"].dtype.kind == "i"
 
+    @mock.patch("airflow.providers.amazon.aws.transfers.sql_to_s3.S3Hook")
+    def test_fix_dtypes_not_called(self, mock_s3_hook):
+        query = "query"
+        s3_bucket = "bucket"
+        s3_key = "key"
+
+        mock_dbapi_hook = mock.Mock()
+        test_df = pd.DataFrame({"a": "1", "b": "2"}, index=[0, 1])
+
+        get_df_mock = mock_dbapi_hook.return_value.get_df
+        get_df_mock.return_value = test_df
+
+        op = SqlToS3Operator(
+            query=query,
+            s3_bucket=s3_bucket,
+            s3_key=s3_key,
+            sql_conn_id="mysql_conn_id",
+            aws_conn_id="aws_conn_id",
+            task_id="task_id",
+            read_pd_kwargs={"dtype_backend": "pyarrow"},
+            file_format="parquet",
+            replace=True,
+            dag=None,
+        )
+
+        op._get_hook = mock_dbapi_hook
+
+        with mock.patch.object(SqlToS3Operator, "_fix_dtypes") as 
mock_fix_dtypes:
+            op.execute(None)
+
+        mock_fix_dtypes.assert_not_called()
+
     def test_invalid_file_format(self):
         with pytest.raises(AirflowException):
             SqlToS3Operator(

Reply via email to