Skip to content

Commit 6594a2f

Browse files
authored
Add OpenLineage support to AthenaSQLHook (#66844)
* Add OpenLineage support to AthenaSQLHook Signed-off-by: Rahul Madan <madan.rahul9@gmail.com> * Added tests for athena sql hook Signed-off-by: Rahul Madan <madan.rahul9@gmail.com> * Address review: hook-constructor region wins + support aws_domain extra Signed-off-by: Rahul Madan <madan.rahul9@gmail.com> --------- Signed-off-by: Rahul Madan <madan.rahul9@gmail.com>
1 parent 75e481c commit 6594a2f

2 files changed

Lines changed: 124 additions & 0 deletions

File tree

providers/amazon/src/airflow/providers/amazon/aws/hooks/athena_sql.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,37 @@ def _get_conn_params(self) -> dict[str, str | None]:
177177
aws_domain=self.conn.extra_dejson.get("aws_domain", "amazonaws.com"),
178178
)
179179

180+
def get_openlineage_database_info(self, connection):
181+
"""Return Amazon Athena specific information for OpenLineage."""
182+
from airflow.providers.openlineage.sqlparser import DatabaseInfo
183+
184+
region_name = self.region_name or connection.extra_dejson.get("region_name")
185+
aws_domain = connection.extra_dejson.get("aws_domain", "amazonaws.com")
186+
authority = f"athena.{region_name}.{aws_domain}" if region_name else f"athena.{aws_domain}"
187+
188+
return DatabaseInfo(
189+
scheme="awsathena",
190+
authority=authority,
191+
information_schema_columns=[
192+
"table_schema",
193+
"table_name",
194+
"column_name",
195+
"ordinal_position",
196+
"data_type",
197+
"table_catalog",
198+
],
199+
database=connection.extra_dejson.get("catalog", "AwsDataCatalog"),
200+
is_information_schema_cross_db=True,
201+
)
202+
203+
def get_openlineage_database_dialect(self, _) -> str:
204+
"""Return Athena dialect. Athena uses Trino SQL engine."""
205+
return "trino"
206+
207+
def get_openlineage_default_schema(self) -> str | None:
208+
"""Return Athena default schema."""
209+
return self.conn.schema or "default"
210+
180211
def get_uri(self) -> str:
181212
"""Overridden to use the Athena dialect as driver name."""
182213
from airflow.providers.common.compat.sdk import AirflowOptionalProviderFeatureException

providers/amazon/tests/unit/amazon/aws/hooks/test_athena_sql.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,96 @@ def test_init_passes_valid_aws_kwargs(self):
181181
assert hook._verify is False
182182
assert hook._region_name == "us-west-2"
183183
assert hook._config is not None
184+
185+
186+
class TestAthenaSQLHookOpenLineage:
187+
"""Static tests for the OpenLineage methods on AthenaSQLHook."""
188+
189+
EXPECTED_INFORMATION_SCHEMA_COLUMNS = [
190+
"table_schema",
191+
"table_name",
192+
"column_name",
193+
"ordinal_position",
194+
"data_type",
195+
"table_catalog",
196+
]
197+
198+
@staticmethod
199+
def _make_hook(connection: Connection, hook_region: str | None = None) -> AthenaSQLHook:
200+
hook = AthenaSQLHook(region_name=hook_region) if hook_region else AthenaSQLHook()
201+
hook.get_connection = mock.Mock(return_value=connection) # type: ignore[method-assign]
202+
return hook
203+
204+
@pytest.mark.parametrize(
205+
("extras", "hook_region", "expected_authority"),
206+
[
207+
# region from connection extras when hook-constructor region not set
208+
({"region_name": "us-east-1"}, None, "athena.us-east-1.amazonaws.com"),
209+
# hook-constructor region (explicit user override) wins over extras region
210+
({"region_name": "eu-west-1"}, "us-east-2", "athena.us-east-2.amazonaws.com"),
211+
# hook-constructor region used when extras have none
212+
({}, "ap-south-1", "athena.ap-south-1.amazonaws.com"),
213+
# graceful fallback when neither is set
214+
({}, None, "athena.amazonaws.com"),
215+
# aws_domain extra changes the domain (AWS GovCloud / China / ISO partitions)
216+
(
217+
{"region_name": "cn-north-1", "aws_domain": "amazonaws.com.cn"},
218+
None,
219+
"athena.cn-north-1.amazonaws.com.cn",
220+
),
221+
# aws_domain still applied when region falls back
222+
({"aws_domain": "amazonaws.com.cn"}, None, "athena.amazonaws.com.cn"),
223+
],
224+
)
225+
def test_get_openlineage_database_info_region_extraction(self, extras, hook_region, expected_authority):
226+
conn = Connection(conn_type="athena", schema="default", extra=extras)
227+
hook = self._make_hook(conn, hook_region)
228+
info = hook.get_openlineage_database_info(conn)
229+
assert info.authority == expected_authority
230+
231+
def test_get_openlineage_database_info_returns_expected_fields(self):
232+
"""Snapshot of the DatabaseInfo shape so accidental changes are caught."""
233+
conn = Connection(
234+
conn_type="athena",
235+
schema="default",
236+
extra={"region_name": "us-east-1"},
237+
)
238+
hook = self._make_hook(conn)
239+
info = hook.get_openlineage_database_info(conn)
240+
assert info.scheme == "awsathena"
241+
assert info.authority == "athena.us-east-1.amazonaws.com"
242+
assert info.database == "AwsDataCatalog"
243+
assert info.is_information_schema_cross_db is True
244+
assert info.information_schema_columns == self.EXPECTED_INFORMATION_SCHEMA_COLUMNS
245+
246+
def test_get_openlineage_database_info_custom_catalog(self):
247+
conn = Connection(
248+
conn_type="athena",
249+
schema="default",
250+
extra={"region_name": "us-east-1", "catalog": "MyCatalog"},
251+
)
252+
hook = self._make_hook(conn)
253+
info = hook.get_openlineage_database_info(conn)
254+
assert info.database == "MyCatalog"
255+
256+
def test_get_openlineage_database_dialect_returns_trino(self):
257+
conn = Connection(conn_type="athena", extra={"region_name": "us-east-1"})
258+
hook = self._make_hook(conn)
259+
assert hook.get_openlineage_database_dialect(conn) == "trino"
260+
261+
@pytest.mark.parametrize(
262+
("connection_schema", "expected_schema"),
263+
[
264+
("mydb", "mydb"),
265+
(None, "default"),
266+
("", "default"),
267+
],
268+
)
269+
def test_get_openlineage_default_schema(self, connection_schema, expected_schema):
270+
conn = Connection(
271+
conn_type="athena",
272+
schema=connection_schema,
273+
extra={"region_name": "us-east-1"},
274+
)
275+
hook = self._make_hook(conn)
276+
assert hook.get_openlineage_default_schema() == expected_schema

0 commit comments

Comments
 (0)