apache-airflow-providers-common-sql 2.3.0__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/PKG-INFO +8 -8
  2. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/README.rst +4 -4
  3. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/changelog.rst +12 -0
  4. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/index.rst +4 -4
  5. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/operators.rst +15 -3
  6. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/provider.yaml +2 -1
  7. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/pyproject.toml +4 -4
  8. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/__init__.py +1 -1
  9. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/config.py +11 -8
  10. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/datafusion/base.py +9 -0
  11. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/datafusion/engine.py +17 -6
  12. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/datafusion/object_storage_provider.py +69 -1
  13. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/datafusion/test_engine.py +92 -8
  14. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/datafusion/test_object_storage_provider.py +153 -0
  15. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/operators/test_analytics.py +1 -1
  16. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/test_config.py +2 -0
  17. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/.gitignore +0 -0
  18. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/LICENSE +0 -0
  19. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/NOTICE +0 -0
  20. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/.latest-doc-only-change.txt +0 -0
  21. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/commits.rst +0 -0
  22. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/conf.py +0 -0
  23. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/connections.rst +0 -0
  24. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/dataframes.rst +0 -0
  25. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/dialects.rst +0 -0
  26. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/installing-providers-from-sources.rst +0 -0
  27. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/integration-logos/sql.png +0 -0
  28. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/security.rst +0 -0
  29. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/supported-database-types.rst +0 -0
  30. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/__init__.py +0 -0
  31. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/__init__.py +0 -0
  32. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/__init__.py +0 -0
  33. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/README_API.md +0 -0
  34. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/datafusion/__init__.py +0 -0
  35. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/datafusion/exceptions.py +0 -0
  36. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/datafusion/format_handlers.py +0 -0
  37. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/decorators/__init__.py +0 -0
  38. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/decorators/analytics.py +0 -0
  39. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/decorators/sql.py +0 -0
  40. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/dialects/__init__.py +0 -0
  41. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/dialects/dialect.py +0 -0
  42. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/dialects/dialect.pyi +0 -0
  43. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/doc/adr/0001-record-architecture-decisions.md +0 -0
  44. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/doc/adr/0002-return-common-data-structure-from-dbapihook-derived-hooks.md +0 -0
  45. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/doc/adr/0003-introduce-notion-of-dialects-in-dbapihook.md +0 -0
  46. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/example_dags/__init__.py +0 -0
  47. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/example_dags/example_analytics.py +0 -0
  48. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/get_provider_info.py +0 -0
  49. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/get_provider_info.pyi +0 -0
  50. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/__init__.py +0 -0
  51. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/handlers.py +0 -0
  52. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/handlers.pyi +0 -0
  53. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/lineage.py +0 -0
  54. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/lineage.pyi +0 -0
  55. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/sql.py +0 -0
  56. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/sql.pyi +0 -0
  57. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/operators/__init__.py +0 -0
  58. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/operators/analytics.py +0 -0
  59. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/operators/generic_transfer.py +0 -0
  60. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/operators/generic_transfer.pyi +0 -0
  61. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/operators/sql.py +0 -0
  62. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/sensors/__init__.py +0 -0
  63. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/sensors/sql.py +0 -0
  64. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/sensors/sql.pyi +0 -0
  65. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/triggers/__init__.py +0 -0
  66. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/triggers/sql.py +0 -0
  67. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/triggers/sql.pyi +0 -0
  68. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/version_compat.py +0 -0
  69. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/conftest.py +0 -0
  70. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/__init__.py +0 -0
  71. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/__init__.py +0 -0
  72. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/__init__.py +0 -0
  73. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_generic_transfer.py +0 -0
  74. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_sql_bulk_load.py +0 -0
  75. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_sql_column_table_check.py +0 -0
  76. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_sql_execute_query.py +0 -0
  77. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_sql_insert_rows.py +0 -0
  78. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_sql_threshold_check.py +0 -0
  79. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_sql_value_check.py +0 -0
  80. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/__init__.py +0 -0
  81. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/__init__.py +0 -0
  82. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/__init__.py +0 -0
  83. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/datafusion/__init__.py +0 -0
  84. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/datafusion/test_base.py +0 -0
  85. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/datafusion/test_exceptions.py +0 -0
  86. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/datafusion/test_format_handlers.py +0 -0
  87. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/decorators/__init__.py +0 -0
  88. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/decorators/test_analytics.py +0 -0
  89. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/decorators/test_sql.py +0 -0
  90. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/dialects/__init__.py +0 -0
  91. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/dialects/test_dialect.py +0 -0
  92. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/hooks/__init__.py +0 -0
  93. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/hooks/test_dbapi.py +0 -0
  94. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/hooks/test_handlers.py +0 -0
  95. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/hooks/test_lineage.py +0 -0
  96. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/hooks/test_sql.py +0 -0
  97. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/hooks/test_sqlparse.py +0 -0
  98. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/operators/__init__.py +0 -0
  99. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/operators/test_generic_transfer.py +0 -0
  100. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/operators/test_sql.py +0 -0
  101. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/operators/test_sql_execute.py +0 -0
  102. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/sensors/__init__.py +0 -0
  103. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/sensors/test_sql.py +0 -0
  104. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/triggers/__init__.py +0 -0
  105. {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/triggers/test_sql.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: apache-airflow-providers-common-sql
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Provider package apache-airflow-providers-common-sql for Apache Airflow
5
5
  Keywords: airflow-provider,common.sql,airflow,integration
6
6
  Author-email: Apache Software Foundation <dev@airflow.apache.org>
@@ -23,7 +23,7 @@ Classifier: Topic :: System :: Monitoring
23
23
  License-File: LICENSE
24
24
  License-File: NOTICE
25
25
  Requires-Dist: apache-airflow>=2.11.0
26
- Requires-Dist: apache-airflow-providers-common-compat>=1.14.1
26
+ Requires-Dist: apache-airflow-providers-common-compat>=1.23.0
27
27
  Requires-Dist: sqlparse>=0.5.1
28
28
  Requires-Dist: more-itertools>=9.0.0
29
29
  Requires-Dist: methodtools>=0.4.7
@@ -38,8 +38,8 @@ Requires-Dist: polars>=1.26.0 ; extra == "polars"
38
38
  Requires-Dist: pyiceberg-core>=0.8.0 ; extra == "pyiceberg-core"
39
39
  Requires-Dist: sqlalchemy>=1.4.54,!=2.1.0 ; extra == "sqlalchemy"
40
40
  Project-URL: Bug Tracker, https://github.com/apache/airflow/issues
41
- Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.3.0/changelog.html
42
- Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.3.0
41
+ Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0/changelog.html
42
+ Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0
43
43
  Project-URL: Mastodon, https://fosstodon.org/@airflow
44
44
  Project-URL: Slack Chat, https://s.apache.org/airflow-slack
45
45
  Project-URL: Source Code, https://github.com/apache/airflow
@@ -82,7 +82,7 @@ Import-Namespace: airflow.providers.common
82
82
 
83
83
  Package ``apache-airflow-providers-common-sql``
84
84
 
85
- Release: ``2.3.0``
85
+ Release: ``2.4.0``
86
86
 
87
87
 
88
88
  `Common SQL Provider <https://en.wikipedia.org/wiki/SQL>`__
@@ -95,7 +95,7 @@ This is a provider package for ``common.sql`` provider. All classes for this pro
95
95
  are in ``airflow.providers.common.sql`` python package.
96
96
 
97
97
  You can find package information and changelog for the provider
98
- in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.3.0/>`_.
98
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0/>`_.
99
99
 
100
100
  Installation
101
101
  ------------
@@ -113,7 +113,7 @@ Requirements
113
113
  PIP package Version required
114
114
  ========================================== ==================
115
115
  ``apache-airflow`` ``>=2.11.0``
116
- ``apache-airflow-providers-common-compat`` ``>=1.14.1``
116
+ ``apache-airflow-providers-common-compat`` ``>=1.23.0``
117
117
  ``sqlparse`` ``>=0.5.1``
118
118
  ``more-itertools`` ``>=9.0.0``
119
119
  ``methodtools`` ``>=0.4.7``
@@ -157,5 +157,5 @@ Extra Dependencies
157
157
  ================== =======================================================================================================================================================================
158
158
 
159
159
  The changelog for the provider package can be found in the
160
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.3.0/changelog.html>`_.
160
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0/changelog.html>`_.
161
161
 
@@ -23,7 +23,7 @@
23
23
 
24
24
  Package ``apache-airflow-providers-common-sql``
25
25
 
26
- Release: ``2.3.0``
26
+ Release: ``2.4.0``
27
27
 
28
28
 
29
29
  `Common SQL Provider <https://en.wikipedia.org/wiki/SQL>`__
@@ -36,7 +36,7 @@ This is a provider package for ``common.sql`` provider. All classes for this pro
36
36
  are in ``airflow.providers.common.sql`` python package.
37
37
 
38
38
  You can find package information and changelog for the provider
39
- in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.3.0/>`_.
39
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0/>`_.
40
40
 
41
41
  Installation
42
42
  ------------
@@ -54,7 +54,7 @@ Requirements
54
54
  PIP package Version required
55
55
  ========================================== ==================
56
56
  ``apache-airflow`` ``>=2.11.0``
57
- ``apache-airflow-providers-common-compat`` ``>=1.14.1``
57
+ ``apache-airflow-providers-common-compat`` ``>=1.23.0``
58
58
  ``sqlparse`` ``>=0.5.1``
59
59
  ``more-itertools`` ``>=9.0.0``
60
60
  ``methodtools`` ``>=0.4.7``
@@ -98,4 +98,4 @@ Extra Dependencies
98
98
  ================== =======================================================================================================================================================================
99
99
 
100
100
  The changelog for the provider package can be found in the
101
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.3.0/changelog.html>`_.
101
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0/changelog.html>`_.
@@ -25,6 +25,18 @@
25
25
  Changelog
26
26
  ---------
27
27
 
28
+ 2.4.0
29
+ .....
30
+
31
+ Features
32
+ ~~~~~~~~
33
+
34
+ * ``Support 'abfs://' and 'abfss://' URIs in DataFusion Azure Blob Storage (#74228)``
35
+
36
+ .. Below changes are excluded from the changelog. Move them to
37
+ appropriate section above if needed. Do not delete the lines(!):
38
+ * ``Remove unused mypy suppressions in common providers (#75257)``
39
+
28
40
  2.3.0
29
41
  .....
30
42
 
@@ -79,7 +79,7 @@ apache-airflow-providers-common-sql package
79
79
  `Common SQL Provider <https://en.wikipedia.org/wiki/SQL>`__
80
80
 
81
81
 
82
- Release: 2.3.0
82
+ Release: 2.4.0
83
83
 
84
84
  Provider package
85
85
  ----------------
@@ -103,7 +103,7 @@ The minimum Apache Airflow version supported by this provider distribution is ``
103
103
  PIP package Version required
104
104
  ========================================== ==================
105
105
  ``apache-airflow`` ``>=2.11.0``
106
- ``apache-airflow-providers-common-compat`` ``>=1.14.1``
106
+ ``apache-airflow-providers-common-compat`` ``>=1.23.0``
107
107
  ``sqlparse`` ``>=0.5.1``
108
108
  ``more-itertools`` ``>=9.0.0``
109
109
  ``methodtools`` ``>=0.4.7``
@@ -160,5 +160,5 @@ Downloading official packages
160
160
  You can download officially released packages and verify their checksums and signatures from the
161
161
  `Official Apache Download site <https://downloads.apache.org/airflow/providers/>`_
162
162
 
163
- * `The apache-airflow-providers-common-sql 2.3.0 sdist package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.3.0.tar.gz>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.3.0.tar.gz.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.3.0.tar.gz.sha512>`__)
164
- * `The apache-airflow-providers-common-sql 2.3.0 wheel package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.3.0-py3-none-any.whl>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.3.0-py3-none-any.whl.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.3.0-py3-none-any.whl.sha512>`__)
163
+ * `The apache-airflow-providers-common-sql 2.4.0 sdist package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.4.0.tar.gz>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.4.0.tar.gz.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.4.0.tar.gz.sha512>`__)
164
+ * `The apache-airflow-providers-common-sql 2.4.0 wheel package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.4.0-py3-none-any.whl>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.4.0-py3-none-any.whl.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.4.0-py3-none-any.whl.sha512>`__)
@@ -365,10 +365,22 @@ resolved in this order:
365
365
 
366
366
  Azure Storage
367
367
  -------------
368
- Use an ``az://`` URI with a ``conn_id`` pointing to a ``wasb`` connection.
369
- ``abfs://`` and ``abfss://`` URIs are not recognized yet. The account name
368
+ Use an ``az://``, ``abfs://``, or ``abfss://`` URI with a ``conn_id`` pointing
369
+ to a ``wasb`` connection. ``az://container/path`` takes the storage account
370
+ from the connection; when the connection resolves none (no ``host``, and no ``login``
371
+ or a service principal), the binding falls back to ``AZURE_STORAGE_ACCOUNT_NAME`` on
372
+ the worker.
373
+ ``abfs://container@account.dfs.core.windows.net/path`` and ``abfss://...``
374
+ additionally name the account in the URI itself -- if it disagrees with the
375
+ account the connection resolves to, registering the datasource raises rather
376
+ than silently picking one, and when the connection names no account at all, the
377
+ URI's account is used instead of ``AZURE_STORAGE_ACCOUNT_NAME``. The URI host
378
+ follows the same cloud rule as the connection host below: a host outside
379
+ ``*.core.windows.net`` raises unless ``AZURE_STORAGE_ENDPOINT``/``AZURE_ENDPOINT``
380
+ is set. The account name
370
381
  comes from ``host`` (its first DNS label) when set, falling back to
371
- ``login`` only when ``host`` is empty; only the public
382
+ ``login`` only when ``host`` is empty and the connection is not a service principal
383
+ (``tenant_id`` set, where ``login`` is the client ID); only the public
372
384
  ``*.blob.core.windows.net`` cloud is supported, since DataFusion's binding
373
385
  otherwise has no endpoint override -- unless the worker sets
374
386
  ``AZURE_STORAGE_ENDPOINT``/``AZURE_ENDPOINT`` for a real sovereign-cloud
@@ -23,12 +23,13 @@ description: |
23
23
 
24
24
  state: ready
25
25
  lifecycle: production
26
- source-date-epoch: 1791328846
26
+ source-date-epoch: 1792489528
27
27
  # Note that those versions are maintained by release manager - do not update them manually
28
28
  # with the exception of case where other provider in sources has >= new provider version.
29
29
  # In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have
30
30
  # to be done in the same PR
31
31
  versions:
32
+ - 2.4.0
32
33
  - 2.3.0
33
34
  - 2.2.0
34
35
  - 2.1.1
@@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi"
25
25
 
26
26
  [project]
27
27
  name = "apache-airflow-providers-common-sql"
28
- version = "2.3.0"
28
+ version = "2.4.0"
29
29
  description = "Provider package apache-airflow-providers-common-sql for Apache Airflow"
30
30
  readme = "README.rst"
31
31
  license = "Apache-2.0"
@@ -59,7 +59,7 @@ requires-python = ">=3.11"
59
59
  # After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build``
60
60
  dependencies = [
61
61
  "apache-airflow>=2.11.0",
62
- "apache-airflow-providers-common-compat>=1.14.1",
62
+ "apache-airflow-providers-common-compat>=1.23.0",
63
63
  "sqlparse>=0.5.1",
64
64
  "more-itertools>=9.0.0",
65
65
  # The methodtools dependency is necessary since the introduction of dialects:
@@ -147,8 +147,8 @@ apache-airflow-providers-common-sql = {workspace = true}
147
147
  apache-airflow-providers-standard = {workspace = true}
148
148
 
149
149
  [project.urls]
150
- "Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.3.0"
151
- "Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.3.0/changelog.html"
150
+ "Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0"
151
+ "Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0/changelog.html"
152
152
  "Bug Tracker" = "https://github.com/apache/airflow/issues"
153
153
  "Source Code" = "https://github.com/apache/airflow"
154
154
  "Slack Chat" = "https://s.apache.org/airflow-slack"
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
29
29
 
30
30
  __all__ = ["__version__"]
31
31
 
32
- __version__ = "2.3.0"
32
+ __version__ = "2.4.0"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
35
  "2.11.0"
@@ -52,6 +52,14 @@ class StorageType(str, Enum):
52
52
  LOCAL = "local"
53
53
 
54
54
 
55
+ STORAGE_TYPE_SCHEMES: dict[StorageType, tuple[str, ...]] = {
56
+ StorageType.S3: ("s3://",),
57
+ StorageType.GCS: ("gs://",),
58
+ StorageType.AZURE: ("az://", "abfs://", "abfss://"),
59
+ StorageType.LOCAL: ("file://",),
60
+ }
61
+
62
+
55
63
  @dataclass
56
64
  class DataSourceConfig:
57
65
  """
@@ -114,12 +122,7 @@ class DataSourceConfig:
114
122
  @property
115
123
  def _extract_storage_type(self) -> StorageType | None:
116
124
  """Extract storage type."""
117
- if self.uri.startswith("s3://"):
118
- return StorageType.S3
119
- if self.uri.startswith("gs://"):
120
- return StorageType.GCS
121
- if self.uri.startswith("az://"):
122
- return StorageType.AZURE
123
- if self.uri.startswith("file://"):
124
- return StorageType.LOCAL
125
+ for storage_type, schemes in STORAGE_TYPE_SCHEMES.items():
126
+ if self.uri.startswith(schemes):
127
+ return storage_type
125
128
  raise ValueError(f"Unsupported storage type for URI: {self.uri}")
@@ -50,6 +50,15 @@ class ObjectStorageProvider(LoggingMixin, ABC):
50
50
  """Return URL scheme for this storage type (e.g., 's3://', 'gs://')."""
51
51
  raise NotImplementedError
52
52
 
53
+ def normalize_uri(self, uri: str) -> str:
54
+ """
55
+ Return the canonical ``uri`` to register under.
56
+
57
+ Default is identity; a provider overrides this when its URI shape differs from
58
+ DataFusion's (schema, host) registry key.
59
+ """
60
+ return uri
61
+
53
62
  def get_bucket(self, path: str) -> str | None:
54
63
  """Extract the bucket name from the given path."""
55
64
  if path and path.startswith(self.get_scheme()):
@@ -17,6 +17,7 @@
17
17
  from __future__ import annotations
18
18
 
19
19
  import os
20
+ from dataclasses import replace
20
21
  from typing import TYPE_CHECKING, Any
21
22
  from urllib.parse import parse_qsl, urlsplit
22
23
 
@@ -64,14 +65,15 @@ class DataFusionEngine(LoggingMixin):
64
65
  else:
65
66
  connection_config = self._get_connection_config(datasource_config.conn_id)
66
67
 
67
- self._register_object_store(datasource_config, connection_config)
68
+ canonical_uri = self._register_object_store(datasource_config, connection_config)
69
+ datasource_config = replace(datasource_config, uri=canonical_uri)
68
70
 
69
71
  self._register_data_source_format(datasource_config)
70
72
 
71
73
  def _register_object_store(
72
74
  self, datasource_config: DataSourceConfig, connection_config: ConnectionConfig | None
73
- ):
74
- """Register object stores."""
75
+ ) -> str:
76
+ """Register the object store and return the canonical URI to register the table under."""
75
77
  if TYPE_CHECKING:
76
78
  assert datasource_config.storage_type is not None
77
79
 
@@ -80,9 +82,14 @@ class DataFusionEngine(LoggingMixin):
80
82
  object_store = storage_provider.create_object_store(
81
83
  datasource_config.uri, connection_config=connection_config
82
84
  )
85
+ canonical_uri = storage_provider.normalize_uri(datasource_config.uri)
83
86
  schema = storage_provider.get_scheme()
84
- self.session_context.register_object_store(schema=schema, store=object_store)
85
- self.log.info("Registered object store for schema: %s", schema)
87
+ # abfs(s) folds account+container into one host token, so the container-name host
88
+ # DataFusion defaults to when host is omitted would miss the registered store.
89
+ host = urlsplit(canonical_uri).netloc
90
+ self.session_context.register_object_store(schema=schema, store=object_store, host=host)
91
+ self.log.info("Registered object store for schema: %s host: %s", schema, host)
92
+ return canonical_uri
86
93
  except Exception as e:
87
94
  raise ObjectStoreCreationException(
88
95
  f"Error while creating object store for {datasource_config.storage_type}: {e}"
@@ -240,6 +247,9 @@ class DataFusionEngine(LoggingMixin):
240
247
  credentials.update(
241
248
  {"client_id": conn.login, "client_secret": conn.password, "tenant_id": tenant_id}
242
249
  )
250
+ if not conn.host:
251
+ # login holds the client_id here, so it cannot name the storage account.
252
+ credentials["account"] = None
243
253
  credential_tier = "client_secret"
244
254
  elif shared_access_key := _get_wasb_extra_field(extra_dejson, "shared_access_key"):
245
255
  # Checked ahead of sas_token to match WasbHook.get_conn's precedence.
@@ -353,7 +363,8 @@ class DataFusionEngine(LoggingMixin):
353
363
 
354
364
  From ``host`` (its netloc's first label) when set, falling back to ``login`` only when
355
365
  ``host`` is empty -- login holds the service-principal client_id otherwise, not the
356
- account name. Returns ``None`` when neither is set, so the binding falls back to
366
+ account name (the caller drops the login-derived account for service principals).
367
+ Returns ``None`` when neither is set, so the binding falls back to
357
368
  ``AZURE_STORAGE_ACCOUNT_NAME`` rather than the literal string ``"None"``. Reimplemented
358
369
  locally instead of importing
359
370
  ``airflow.providers.microsoft.azure.utils.parse_blob_account_url``, to avoid pulling in
@@ -17,6 +17,8 @@
17
17
  from __future__ import annotations
18
18
 
19
19
  import json
20
+ import os
21
+ import re
20
22
  import tempfile
21
23
  from pathlib import Path
22
24
 
@@ -107,6 +109,15 @@ class GCSObjectStorageProvider(ObjectStorageProvider):
107
109
  return "gs://"
108
110
 
109
111
 
112
+ _ABFS_URI_RE = re.compile(
113
+ r"^abfss?://(?P<container>[^@/]+)@(?P<account>[^./]+)\.(?P<host>[^/]+)(?P<path>/.*)?$"
114
+ )
115
+ # MicrosoftAzure has no endpoint kwarg, so a non-public host only reaches the right cloud when
116
+ # AZURE_STORAGE_ENDPOINT/AZURE_ENDPOINT redirects the binding; same rule as _resolve_wasb_account.
117
+ _AZURE_PUBLIC_HOST_SUFFIX = ".core.windows.net"
118
+ _ABFS_SCHEMES = ("abfs://", "abfss://")
119
+
120
+
110
121
  class AzureObjectStorageProvider(ObjectStorageProvider):
111
122
  """Azure Object Storage Provider using DataFusion's MicrosoftAzure."""
112
123
 
@@ -115,6 +126,48 @@ class AzureObjectStorageProvider(ObjectStorageProvider):
115
126
  """Return the storage type."""
116
127
  return StorageType.AZURE
117
128
 
129
+ def get_bucket(self, path: str) -> str | None:
130
+ """Extract the container name, from either the ``az://`` or ``abfs(s)://`` URI shape."""
131
+ if match := _ABFS_URI_RE.match(path):
132
+ return match.group("container")
133
+ if path.startswith(_ABFS_SCHEMES):
134
+ raise ValueError(
135
+ f"{path!r} does not match the required abfs(s)://<container>@<account>.<host>/<path> shape"
136
+ )
137
+ return super().get_bucket(path)
138
+
139
+ def _validate_uri_host(self, path: str) -> None:
140
+ match = _ABFS_URI_RE.match(path)
141
+ if not match:
142
+ return
143
+ host = match.group("host").lower()
144
+ if host == _AZURE_PUBLIC_HOST_SUFFIX[1:] or host.endswith(_AZURE_PUBLIC_HOST_SUFFIX):
145
+ return
146
+ if os.environ.get("AZURE_STORAGE_ENDPOINT") or os.environ.get("AZURE_ENDPOINT"):
147
+ return
148
+ raise ValueError(
149
+ f"URI {path!r} host {match.group('host')!r} is not a public *{_AZURE_PUBLIC_HOST_SUFFIX} "
150
+ "endpoint, and DataFusion's Azure Blob Storage binding has no endpoint override, so the "
151
+ "request would go to the public cloud. A sovereign cloud is supported once the "
152
+ "AZURE_STORAGE_ENDPOINT environment variable is set."
153
+ )
154
+
155
+ def _get_uri_account(self, path: str) -> str | None:
156
+ match = _ABFS_URI_RE.match(path)
157
+ return match.group("account") if match else None
158
+
159
+ def normalize_uri(self, path: str) -> str:
160
+ """
161
+ Rewrite ``abfs(s)://<container>@<account>.<host>/<path>`` to ``az://<account>.<container>/<path>``.
162
+
163
+ DataFusion's registry keys on (schema, host) alone, so container-only would collide for
164
+ two different accounts sharing a container name. Account and container names never
165
+ contain a dot, so joining on one is unambiguous. ``az://`` URIs pass through unchanged.
166
+ """
167
+ if match := _ABFS_URI_RE.match(path):
168
+ return f"az://{match.group('account')}.{match.group('container')}{match.group('path') or ''}"
169
+ return path
170
+
118
171
  def create_object_store(self, path: str, connection_config: ConnectionConfig | None = None):
119
172
  """Create an Azure object store using DataFusion's MicrosoftAzure."""
120
173
  if connection_config is None:
@@ -124,6 +177,21 @@ class AzureObjectStorageProvider(ObjectStorageProvider):
124
177
  credentials = connection_config.credentials
125
178
  container = self.get_bucket(path)
126
179
 
180
+ self._validate_uri_host(path)
181
+ uri_account = self._get_uri_account(path)
182
+ resolved_account = credentials.get("account")
183
+ if uri_account and resolved_account and uri_account.lower() != resolved_account.lower():
184
+ raise ValueError(
185
+ f"URI {path!r} names storage account {uri_account!r}, but connection "
186
+ f"{connection_config.conn_id!r} resolves to account {resolved_account!r}. Point "
187
+ "the URI and the connection at the same account, or omit the account from one of "
188
+ "them."
189
+ )
190
+ if uri_account and not resolved_account:
191
+ # Pins the URI's account; MicrosoftAzure otherwise falls back to AZURE_STORAGE_ACCOUNT_NAME,
192
+ # which can name a different account.
193
+ credentials = {**credentials, "account": uri_account}
194
+
127
195
  azure_store = MicrosoftAzure(container_name=container, **credentials)
128
196
  self.log.info("Created Azure object store for container %s", container)
129
197
 
@@ -134,7 +202,7 @@ class AzureObjectStorageProvider(ObjectStorageProvider):
134
202
  raise ObjectStoreCreationException(f"Failed to create Azure object store: {e}")
135
203
 
136
204
  def get_scheme(self) -> str:
137
- """Return the scheme for Azure."""
205
+ """Return the scheme for Azure; ``normalize_uri`` rewrites ``abfs(s)://`` URIs onto it."""
138
206
  return "az://"
139
207
 
140
208
 
@@ -18,10 +18,12 @@ from __future__ import annotations
18
18
 
19
19
  import os
20
20
  import tempfile
21
+ from pathlib import Path
21
22
  from unittest.mock import MagicMock, patch
22
23
 
23
24
  import pytest
24
25
  from datafusion import SessionContext
26
+ from datafusion.object_store import LocalFileSystem
25
27
 
26
28
  from airflow.models import Connection
27
29
  from airflow.providers.common.sql.config import ConnectionConfig, DataSourceConfig
@@ -96,6 +98,7 @@ class TestDataFusionEngine:
96
98
  mock_store = MagicMock()
97
99
  mock_provider.create_object_store.return_value = mock_store
98
100
  mock_provider.get_scheme.return_value = scheme
101
+ mock_provider.normalize_uri.side_effect = lambda uri: uri
99
102
  mock_factory.return_value = mock_provider
100
103
 
101
104
  engine = DataFusionEngine()
@@ -112,7 +115,9 @@ class TestDataFusionEngine:
112
115
  mock_provider.create_object_store.assert_called_once_with(
113
116
  f"{scheme}://bucket/path", connection_config=mock_get_conn.return_value
114
117
  )
115
- engine.df_ctx.register_object_store.assert_called_once_with(schema=scheme, store=mock_store)
118
+ engine.df_ctx.register_object_store.assert_called_once_with(
119
+ schema=scheme, store=mock_store, host="bucket"
120
+ )
116
121
 
117
122
  if format == "parquet":
118
123
  engine.df_ctx.register_parquet.assert_called_once_with("test_table", f"{scheme}://bucket/path")
@@ -208,8 +213,10 @@ class TestDataFusionEngine:
208
213
  with pytest.raises(QueryExecutionException, match="Error while executing query"):
209
214
  engine.execute_query("SELECT * FROM test_table")
210
215
 
216
+ @pytest.mark.parametrize("uri_prefix", ["file://", ""], ids=["file-uri", "bare-path"])
211
217
  @patch.object(DataFusionEngine, "_get_connection_config")
212
- def test_execute_query_with_local_csv(self, mock_get_conn):
218
+ def test_execute_query_with_local_csv(self, mock_get_conn, uri_prefix):
219
+ """A bare path (no file:// prefix) is accepted with an explicit storage_type=StorageType.LOCAL."""
213
220
  mock_get_conn.return_value = None
214
221
 
215
222
  with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f:
@@ -220,7 +227,7 @@ class TestDataFusionEngine:
220
227
  engine = DataFusionEngine()
221
228
  datasource_config = DataSourceConfig(
222
229
  table_name="test_csv",
223
- uri=f"file://{csv_path}",
230
+ uri=f"{uri_prefix}{csv_path}",
224
231
  format="csv",
225
232
  storage_type="local",
226
233
  conn_id="",
@@ -230,11 +237,87 @@ class TestDataFusionEngine:
230
237
 
231
238
  result = engine.execute_query("SELECT * FROM test_csv ORDER BY name")
232
239
 
233
- expected = {"name": ["Alice", "Bob"], "age": [30, 25]}
234
- assert result == expected
240
+ assert result == {"name": ["Alice", "Bob"], "age": [30, 25]}
241
+ finally:
242
+ os.unlink(csv_path)
243
+
244
+ @patch.object(DataFusionEngine, "_get_connection_config")
245
+ def test_execute_query_with_abfs_uri_matches_real_registry(self, mock_get_conn):
246
+ """Only object-store construction is mocked; normalize_uri and DataFusion's real
247
+ registry run live, so a missing store surfaces as "No suitable object store found"."""
248
+ mock_get_conn.return_value = None
249
+
250
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f:
251
+ f.write("name,age\nAlice,30\nBob,25\n")
252
+ csv_path = f.name
253
+
254
+ try:
255
+ engine = DataFusionEngine()
256
+ datasource_config = DataSourceConfig(
257
+ table_name="abfs_csv",
258
+ uri=f"abfss://demo-container@myaccount.dfs.core.windows.net{csv_path}",
259
+ format="csv",
260
+ conn_id="wasb_default",
261
+ )
262
+
263
+ with patch(
264
+ "airflow.providers.common.sql.datafusion.object_storage_provider"
265
+ ".AzureObjectStorageProvider.create_object_store",
266
+ autospec=True,
267
+ return_value=LocalFileSystem(),
268
+ ):
269
+ engine.register_datasource(datasource_config)
270
+
271
+ result = engine.execute_query("SELECT * FROM abfs_csv ORDER BY name")
272
+
273
+ assert result == {"name": ["Alice", "Bob"], "age": [30, 25]}
274
+ assert datasource_config.uri == f"abfss://demo-container@myaccount.dfs.core.windows.net{csv_path}"
275
+ assert engine.registered_tables["abfs_csv"] == f"az://myaccount.demo-container{csv_path}"
235
276
  finally:
236
277
  os.unlink(csv_path)
237
278
 
279
+ @pytest.mark.parametrize(
280
+ ("uri_a", "uri_b"),
281
+ [
282
+ pytest.param(
283
+ "abfss://bronze@myaccount.dfs.core.windows.net/data.csv",
284
+ "abfss://silver@myaccount.dfs.core.windows.net/data.csv",
285
+ id="same-account-different-containers",
286
+ ),
287
+ pytest.param(
288
+ "abfss://data@devacct.dfs.core.windows.net/data.csv",
289
+ "abfss://data@prodacct.dfs.core.windows.net/data.csv",
290
+ id="same-container-different-accounts",
291
+ ),
292
+ ],
293
+ )
294
+ @patch.object(DataFusionEngine, "_get_connection_config")
295
+ def test_execute_query_with_shared_abfs_identifiers_reads_each_store(self, mock_get_conn, uri_a, uri_b):
296
+ mock_get_conn.return_value = None
297
+
298
+ with (
299
+ tempfile.TemporaryDirectory() as dir_a,
300
+ tempfile.TemporaryDirectory() as dir_b,
301
+ ):
302
+ (Path(dir_a) / "data.csv").write_text("name,age\nAlice,30\n")
303
+ (Path(dir_b) / "data.csv").write_text("name,age\nBob,25\n")
304
+
305
+ engine = DataFusionEngine()
306
+ config_a = DataSourceConfig(table_name="table_a", uri=uri_a, format="csv", conn_id="wasb_a")
307
+ config_b = DataSourceConfig(table_name="table_b", uri=uri_b, format="csv", conn_id="wasb_b")
308
+
309
+ with patch(
310
+ "airflow.providers.common.sql.datafusion.object_storage_provider"
311
+ ".AzureObjectStorageProvider.create_object_store",
312
+ autospec=True,
313
+ side_effect=[LocalFileSystem(prefix=dir_a), LocalFileSystem(prefix=dir_b)],
314
+ ):
315
+ engine.register_datasource(config_a)
316
+ engine.register_datasource(config_b)
317
+
318
+ assert engine.execute_query("SELECT * FROM table_a") == {"name": ["Alice"], "age": [30]}
319
+ assert engine.execute_query("SELECT * FROM table_b") == {"name": ["Bob"], "age": [25]}
320
+
238
321
  @patch("airflow.providers.common.sql.datafusion.engine.get_object_storage_provider", autospec=True)
239
322
  @patch.object(DataFusionEngine, "_get_connection_config")
240
323
  def test_register_datasource_with_options(self, mock_get_conn, mock_factory):
@@ -243,6 +326,7 @@ class TestDataFusionEngine:
243
326
  mock_store = MagicMock()
244
327
  mock_provider.create_object_store.return_value = mock_store
245
328
  mock_provider.get_scheme.return_value = "s3"
329
+ mock_provider.normalize_uri.side_effect = lambda uri: uri
246
330
  mock_factory.return_value = mock_provider
247
331
 
248
332
  engine = DataFusionEngine()
@@ -263,7 +347,9 @@ class TestDataFusionEngine:
263
347
  mock_provider.create_object_store.assert_called_once_with(
264
348
  "s3://bucket/path/", connection_config=mock_get_conn.return_value
265
349
  )
266
- engine.df_ctx.register_object_store.assert_called_once_with(schema="s3", store=mock_store)
350
+ engine.df_ctx.register_object_store.assert_called_once_with(
351
+ schema="s3", store=mock_store, host="bucket"
352
+ )
267
353
 
268
354
  engine.df_ctx.register_parquet.assert_called_once_with(
269
355
  "test_table",
@@ -430,7 +516,6 @@ class TestDataFusionEngine:
430
516
  credentials, extra_config = engine._get_credentials(mock_conn)
431
517
 
432
518
  assert credentials == {
433
- "account": "client-id",
434
519
  "client_id": "client-id",
435
520
  "client_secret": "client-secret",
436
521
  "tenant_id": "tenant-id",
@@ -748,7 +833,6 @@ class TestDataFusionEngine:
748
833
  else:
749
834
  credentials, extra_config = engine._get_credentials(mock_conn)
750
835
  assert credentials == {
751
- "account": "client-id",
752
836
  "client_id": "client-id",
753
837
  "client_secret": "client-secret",
754
838
  "tenant_id": "tenant-id",
@@ -159,6 +159,136 @@ class TestObjectStorageProvider:
159
159
  assert provider.get_storage_type == StorageType.AZURE
160
160
  assert provider.get_scheme() == "az://"
161
161
 
162
+ @pytest.mark.parametrize(
163
+ "uri",
164
+ [
165
+ "abfs://demo-container@myaccount.dfs.core.windows.net/path",
166
+ "abfss://demo-container@myaccount.dfs.core.windows.net/path",
167
+ "abfss://demo-container@myaccount.blob.core.windows.net/path",
168
+ "abfss://demo-container@myaccount.DFS.Core.Windows.Net/path",
169
+ ],
170
+ )
171
+ @patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
172
+ def test_azure_provider_success_with_abfs_uri(self, mock_azure, uri, monkeypatch):
173
+ # Without these, a public-cloud host check that is too strict would still pass.
174
+ monkeypatch.delenv("AZURE_STORAGE_ENDPOINT", raising=False)
175
+ monkeypatch.delenv("AZURE_ENDPOINT", raising=False)
176
+ provider = AzureObjectStorageProvider()
177
+ connection_config = ConnectionConfig(
178
+ conn_id="wasb_default",
179
+ credentials={"account": "myaccount", "access_key": "fake_key"},
180
+ )
181
+
182
+ store = provider.create_object_store(uri, connection_config)
183
+
184
+ mock_azure.assert_called_once_with(
185
+ container_name="demo-container", account="myaccount", access_key="fake_key"
186
+ )
187
+ assert store == mock_azure.return_value
188
+ assert provider.get_bucket(uri) == "demo-container"
189
+
190
+ @patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
191
+ def test_azure_provider_abfs_uri_account_mismatch_raises(self, mock_azure):
192
+ provider = AzureObjectStorageProvider()
193
+ connection_config = ConnectionConfig(
194
+ conn_id="wasb_default",
195
+ credentials={"account": "connection-account", "access_key": "fake_key"},
196
+ )
197
+
198
+ with pytest.raises(ObjectStoreCreationException, match="names storage account 'uri-account'"):
199
+ provider.create_object_store(
200
+ "abfss://demo-container@uri-account.dfs.core.windows.net/path", connection_config
201
+ )
202
+
203
+ mock_azure.assert_not_called()
204
+
205
+ @pytest.mark.parametrize(
206
+ "uri",
207
+ [
208
+ "abfs://demo-container/path", # missing @account entirely
209
+ "abfss://demo-container@/path", # empty account
210
+ ],
211
+ )
212
+ @patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
213
+ def test_azure_provider_malformed_abfs_uri_raises_clear_error(self, mock_azure, uri):
214
+ provider = AzureObjectStorageProvider()
215
+ connection_config = ConnectionConfig(
216
+ conn_id="wasb_default",
217
+ credentials={"account": "myaccount", "access_key": "fake_key"},
218
+ )
219
+
220
+ with pytest.raises(ObjectStoreCreationException, match="does not match the required"):
221
+ provider.create_object_store(uri, connection_config)
222
+
223
+ mock_azure.assert_not_called()
224
+
225
+ @patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
226
+ def test_azure_provider_abfs_uri_account_used_when_connection_resolves_none(self, mock_azure):
227
+ provider = AzureObjectStorageProvider()
228
+ connection_config = ConnectionConfig(
229
+ conn_id="wasb_default",
230
+ credentials={"access_key": "fake_key"},
231
+ )
232
+
233
+ provider.create_object_store(
234
+ "abfss://demo-container@uri-account.dfs.core.windows.net/path", connection_config
235
+ )
236
+
237
+ mock_azure.assert_called_once_with(
238
+ container_name="demo-container", account="uri-account", access_key="fake_key"
239
+ )
240
+
241
+ @pytest.mark.parametrize("account", [None, "usgovacct"])
242
+ @patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
243
+ def test_azure_provider_abfs_uri_sovereign_host_without_endpoint_raises(
244
+ self, mock_azure, account, monkeypatch
245
+ ):
246
+ monkeypatch.delenv("AZURE_STORAGE_ENDPOINT", raising=False)
247
+ monkeypatch.delenv("AZURE_ENDPOINT", raising=False)
248
+ credentials = {"access_key": "fake_key"}
249
+ if account:
250
+ credentials["account"] = account
251
+ connection_config = ConnectionConfig(conn_id="wasb_default", credentials=credentials)
252
+
253
+ with pytest.raises(ObjectStoreCreationException, match="no endpoint override"):
254
+ AzureObjectStorageProvider().create_object_store(
255
+ "abfss://demo-container@usgovacct.dfs.core.usgovcloudapi.net/path", connection_config
256
+ )
257
+
258
+ mock_azure.assert_not_called()
259
+
260
+ @pytest.mark.parametrize("env_var", ["AZURE_STORAGE_ENDPOINT", "AZURE_ENDPOINT"])
261
+ @patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
262
+ def test_azure_provider_abfs_uri_sovereign_host_with_endpoint_env_passes(
263
+ self, mock_azure, env_var, monkeypatch
264
+ ):
265
+ monkeypatch.setenv(env_var, "https://usgovacct.blob.core.usgovcloudapi.net")
266
+ connection_config = ConnectionConfig(conn_id="wasb_default", credentials={"access_key": "fake_key"})
267
+
268
+ AzureObjectStorageProvider().create_object_store(
269
+ "abfss://demo-container@usgovacct.dfs.core.usgovcloudapi.net/path", connection_config
270
+ )
271
+
272
+ mock_azure.assert_called_once_with(
273
+ container_name="demo-container", account="usgovacct", access_key="fake_key"
274
+ )
275
+
276
+ @patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
277
+ def test_azure_provider_abfs_uri_account_matches_case_insensitively(self, mock_azure):
278
+ provider = AzureObjectStorageProvider()
279
+ connection_config = ConnectionConfig(
280
+ conn_id="wasb_default",
281
+ credentials={"account": "MyAccount", "access_key": "fake_key"},
282
+ )
283
+
284
+ provider.create_object_store(
285
+ "abfss://demo-container@myaccount.dfs.core.windows.net/path", connection_config
286
+ )
287
+
288
+ mock_azure.assert_called_once_with(
289
+ container_name="demo-container", account="MyAccount", access_key="fake_key"
290
+ )
291
+
162
292
  def test_azure_provider_failure(self):
163
293
  provider = AzureObjectStorageProvider()
164
294
  connection_config = ConnectionConfig(conn_id="wasb_default")
@@ -176,6 +306,29 @@ class TestObjectStorageProvider:
176
306
  with pytest.raises(ValueError, match="connection_config must be provided for azure"):
177
307
  provider.create_object_store("az://demo-container/path")
178
308
 
309
+ @pytest.mark.parametrize(
310
+ ("uri", "expected"),
311
+ [
312
+ (
313
+ "abfs://demo-container@myaccount.dfs.core.windows.net/path/to/data.parquet",
314
+ "az://myaccount.demo-container/path/to/data.parquet",
315
+ ),
316
+ (
317
+ "abfss://demo-container@myaccount.dfs.core.windows.net/path/to/data.parquet",
318
+ "az://myaccount.demo-container/path/to/data.parquet",
319
+ ),
320
+ ("abfss://demo-container@myaccount.dfs.core.windows.net", "az://myaccount.demo-container"),
321
+ ("az://demo-container/path", "az://demo-container/path"),
322
+ # Same container name on different accounts maps to distinct hosts.
323
+ ("abfss://data@devacct.dfs.core.windows.net/data.csv", "az://devacct.data/data.csv"),
324
+ ("abfss://data@prodacct.dfs.core.windows.net/data.csv", "az://prodacct.data/data.csv"),
325
+ ],
326
+ )
327
+ def test_azure_provider_normalize_uri(self, uri, expected):
328
+ provider = AzureObjectStorageProvider()
329
+
330
+ assert provider.normalize_uri(uri) == expected
331
+
179
332
  def test_azure_provider_partial_service_principal_raises_clear_error(self):
180
333
  """Uses the real MicrosoftAzure binding, not a mock, since it's the one that panics
181
334
  on a partial client_id/client_secret/tenant_id combination."""
@@ -126,7 +126,7 @@ class TestAnalyticsOperator:
126
126
  datasource_configs=[datasource_config],
127
127
  queries=["SELECT * FROM users_data"],
128
128
  engine=mock_engine,
129
- result_output_format=["invalid"], # type: ignore
129
+ result_output_format=["invalid"],
130
130
  )
131
131
 
132
132
  with pytest.raises(ValueError, match="Unsupported output format"):
@@ -35,6 +35,8 @@ class TestDataSourceConfig:
35
35
  ("s3://bucket/path", StorageType.S3),
36
36
  ("gs://bucket/path", StorageType.GCS),
37
37
  ("az://container/path", StorageType.AZURE),
38
+ ("abfs://container@account.dfs.core.windows.net/path", StorageType.AZURE),
39
+ ("abfss://container@account.dfs.core.windows.net/path", StorageType.AZURE),
38
40
  ("file:///path/to/file", StorageType.LOCAL),
39
41
  ],
40
42
  )