apache-airflow-providers-postgres 7.0.2__tar.gz → 7.1.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 (43) hide show
  1. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/PKG-INFO +6 -6
  2. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/README.rst +3 -3
  3. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/changelog.rst +19 -0
  4. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/connections/postgres.rst +9 -0
  5. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/dialects.rst +1 -1
  6. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/index.rst +3 -3
  7. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/provider.yaml +2 -2
  8. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/pyproject.toml +3 -3
  9. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/src/airflow/providers/postgres/__init__.py +1 -1
  10. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/src/airflow/providers/postgres/hooks/postgres.py +28 -2
  11. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/unit/postgres/hooks/test_postgres.py +87 -0
  12. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/.gitignore +0 -0
  13. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/LICENSE +0 -0
  14. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/NOTICE +0 -0
  15. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/.latest-doc-only-change.txt +0 -0
  16. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/commits.rst +0 -0
  17. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/conf.py +0 -0
  18. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/configurations-ref.rst +0 -0
  19. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/installing-providers-from-sources.rst +0 -0
  20. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/integration-logos/Postgres.png +0 -0
  21. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/operators.rst +0 -0
  22. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/redirects.txt +0 -0
  23. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/docs/security.rst +0 -0
  24. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/src/airflow/__init__.py +0 -0
  25. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/src/airflow/providers/__init__.py +0 -0
  26. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/src/airflow/providers/postgres/assets/__init__.py +0 -0
  27. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/src/airflow/providers/postgres/assets/postgres.py +0 -0
  28. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/src/airflow/providers/postgres/dialects/__init__.py +0 -0
  29. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/src/airflow/providers/postgres/dialects/postgres.py +0 -0
  30. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/src/airflow/providers/postgres/get_provider_info.py +0 -0
  31. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/src/airflow/providers/postgres/hooks/__init__.py +0 -0
  32. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/conftest.py +0 -0
  33. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/system/__init__.py +0 -0
  34. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/system/postgres/__init__.py +0 -0
  35. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/system/postgres/example_postgres.py +0 -0
  36. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/unit/__init__.py +0 -0
  37. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/unit/postgres/__init__.py +0 -0
  38. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/unit/postgres/assets/__init__.py +0 -0
  39. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/unit/postgres/assets/test_postgres.py +0 -0
  40. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/unit/postgres/dialects/__init__.py +0 -0
  41. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/unit/postgres/dialects/test_postgres.py +0 -0
  42. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/unit/postgres/hooks/__init__.py +0 -0
  43. {apache_airflow_providers_postgres-7.0.2 → apache_airflow_providers_postgres-7.1.0}/tests/unit/postgres/test_provider_dependencies.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: apache-airflow-providers-postgres
3
- Version: 7.0.2
3
+ Version: 7.1.0
4
4
  Summary: Provider package apache-airflow-providers-postgres for Apache Airflow
5
5
  Keywords: airflow-provider,postgres,airflow,integration
6
6
  Author-email: Apache Software Foundation <dev@airflow.apache.org>
@@ -43,8 +43,8 @@ Requires-Dist: psycopg2-binary>=2.9.9 ; extra == "psycopg2" and ( python_version
43
43
  Requires-Dist: psycopg2-binary>=2.9.10 ; extra == "psycopg2" and ( python_version >= '3.13')
44
44
  Requires-Dist: sqlalchemy>=1.4.54 ; extra == "sqlalchemy"
45
45
  Project-URL: Bug Tracker, https://github.com/apache/airflow/issues
46
- Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.0.2/changelog.html
47
- Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.0.2
46
+ Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.1.0/changelog.html
47
+ Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.1.0
48
48
  Project-URL: Mastodon, https://fosstodon.org/@airflow
49
49
  Project-URL: Slack Chat, https://s.apache.org/airflow-slack
50
50
  Project-URL: Source Code, https://github.com/apache/airflow
@@ -86,7 +86,7 @@ Import-Namespace: airflow.providers
86
86
 
87
87
  Package ``apache-airflow-providers-postgres``
88
88
 
89
- Release: ``7.0.2``
89
+ Release: ``7.1.0``
90
90
 
91
91
 
92
92
  `PostgreSQL <https://www.postgresql.org/>`__
@@ -99,7 +99,7 @@ This is a provider package for ``postgres`` provider. All classes for this provi
99
99
  are in ``airflow.providers.postgres`` python package.
100
100
 
101
101
  You can find package information and changelog for the provider
102
- in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.0.2/>`_.
102
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.1.0/>`_.
103
103
 
104
104
  Installation
105
105
  ------------
@@ -164,5 +164,5 @@ Extra Dependencies
164
164
  =================== ============================================================================================================================================================
165
165
 
166
166
  The changelog for the provider package can be found in the
167
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.0.2/changelog.html>`_.
167
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.1.0/changelog.html>`_.
168
168
 
@@ -23,7 +23,7 @@
23
23
 
24
24
  Package ``apache-airflow-providers-postgres``
25
25
 
26
- Release: ``7.0.2``
26
+ Release: ``7.1.0``
27
27
 
28
28
 
29
29
  `PostgreSQL <https://www.postgresql.org/>`__
@@ -36,7 +36,7 @@ This is a provider package for ``postgres`` provider. All classes for this provi
36
36
  are in ``airflow.providers.postgres`` 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-postgres/7.0.2/>`_.
39
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.1.0/>`_.
40
40
 
41
41
  Installation
42
42
  ------------
@@ -101,4 +101,4 @@ Extra Dependencies
101
101
  =================== ============================================================================================================================================================
102
102
 
103
103
  The changelog for the provider package can be found in the
104
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.0.2/changelog.html>`_.
104
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.1.0/changelog.html>`_.
@@ -27,6 +27,25 @@
27
27
  Changelog
28
28
  ---------
29
29
 
30
+ 7.1.0
31
+ .....
32
+
33
+ Features
34
+ ~~~~~~~~
35
+
36
+ * ``Allow keeping PostgresHook SQLAlchemy engines on psycopg2 (#72000)``
37
+
38
+ Doc-only
39
+ ~~~~~~~~
40
+
41
+ * ``Fix grammar in the dialects documentation (#73145)``
42
+
43
+ .. Below changes are excluded from the changelog. Move them to
44
+ appropriate section above if needed. Do not delete the lines(!):
45
+ * ``Revert "[main] Upgrade important CI environment (#73308)" (#73621)``
46
+ * ``[main] Upgrade important CI environment (#73308)``
47
+ * ``Prepare providers release 2026-09-22``
48
+
30
49
  7.0.2
31
50
  .....
32
51
 
@@ -108,6 +108,15 @@ Extra (optional)
108
108
  If not specified than hostname from **Connection Host** is used.
109
109
  * ``azure_conn_id`` - Azure Connection ID to be used for authentication via Azure Entra ID. Azure Oauth token
110
110
  is retrieved from the azure connection which is used as password for PostgreSQL connection. Scope for the Azure OAuth token can be set in the config option ``azure_oauth_scope`` under the section ``[postgres]``. Requires `apache-airflow-providers-microsoft-azure>=12.8.0`.
111
+ * ``sqlalchemy_scheme`` - The SQLAlchemy ``drivername`` used for the URLs the hook builds
112
+ (``get_uri``, ``get_sqlalchemy_engine``). Must be ``postgresql`` or ``postgresql+<driver>``.
113
+ Since provider 7.0.0 the hook selects psycopg (v3) whenever SQLAlchemy 2.x is installed;
114
+ set this to ``postgresql+psycopg2`` to keep SQLAlchemy engines created from this connection
115
+ on psycopg2. This matters if your Dags rely on psycopg2-specific behaviour — for example,
116
+ psycopg2 sends string parameters without a type so PostgreSQL implicitly coerces them, while
117
+ the psycopg (v3) SQLAlchemy dialect renders typed casts, so inserting string values into
118
+ e.g. ``uuid`` columns (as ``pandas.DataFrame.to_sql`` does) fails with
119
+ *"column is of type uuid but expression is of type character varying"*.
111
120
 
112
121
  Example "extras" field (Amazon RDS PostgreSQL or Amazon Aurora PostgreSQL):
113
122
 
@@ -25,7 +25,7 @@ and the :class:`~airflow.providers.jdbc.hooks.jdbc.JdbcHook` are generic hooks w
25
25
  database that has a driver for it, it needed an abstraction layer which allows us to run specialized queries
26
26
  depending of the database to which we connect and that's why dialects where introduced.
27
27
 
28
- The default :class:`~airflow.providers.common.sql.dialects.dialect.Dialect` class has following operations
28
+ The default :class:`~airflow.providers.common.sql.dialects.dialect.Dialect` class has the following operations
29
29
  available which underneath use SQLAlchemy to execute, but can be overloaded with specialized implementations
30
30
  per database:
31
31
 
@@ -78,7 +78,7 @@ apache-airflow-providers-postgres package
78
78
  `PostgreSQL <https://www.postgresql.org/>`__
79
79
 
80
80
 
81
- Release: 7.0.2
81
+ Release: 7.1.0
82
82
 
83
83
  Provider package
84
84
  ----------------
@@ -162,5 +162,5 @@ Downloading official packages
162
162
  You can download officially released packages and verify their checksums and signatures from the
163
163
  `Official Apache Download site <https://downloads.apache.org/airflow/providers/>`_
164
164
 
165
- * `The apache-airflow-providers-postgres 7.0.2 sdist package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_postgres-7.0.2.tar.gz>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_postgres-7.0.2.tar.gz.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_postgres-7.0.2.tar.gz.sha512>`__)
166
- * `The apache-airflow-providers-postgres 7.0.2 wheel package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_postgres-7.0.2-py3-none-any.whl>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_postgres-7.0.2-py3-none-any.whl.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_postgres-7.0.2-py3-none-any.whl.sha512>`__)
165
+ * `The apache-airflow-providers-postgres 7.1.0 sdist package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_postgres-7.1.0.tar.gz>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_postgres-7.1.0.tar.gz.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_postgres-7.1.0.tar.gz.sha512>`__)
166
+ * `The apache-airflow-providers-postgres 7.1.0 wheel package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_postgres-7.1.0-py3-none-any.whl>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_postgres-7.1.0-py3-none-any.whl.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_postgres-7.1.0-py3-none-any.whl.sha512>`__)
@@ -23,13 +23,13 @@ description: |
23
23
 
24
24
  state: ready
25
25
  lifecycle: production
26
- source-date-epoch: 1787011200
26
+ source-date-epoch: 1790023613
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
- - 7.0.2
32
+ - 7.1.0
33
33
  - 7.0.1
34
34
  - 7.0.0
35
35
  - 6.8.0
@@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi"
25
25
 
26
26
  [project]
27
27
  name = "apache-airflow-providers-postgres"
28
- version = "7.0.2"
28
+ version = "7.1.0"
29
29
  description = "Provider package apache-airflow-providers-postgres for Apache Airflow"
30
30
  readme = "README.rst"
31
31
  license = "Apache-2.0"
@@ -151,8 +151,8 @@ apache-airflow-providers-common-sql = {workspace = true}
151
151
  apache-airflow-providers-standard = {workspace = true}
152
152
 
153
153
  [project.urls]
154
- "Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.0.2"
155
- "Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.0.2/changelog.html"
154
+ "Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.1.0"
155
+ "Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-postgres/7.1.0/changelog.html"
156
156
  "Bug Tracker" = "https://github.com/apache/airflow/issues"
157
157
  "Source Code" = "https://github.com/apache/airflow"
158
158
  "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__ = "7.0.2"
32
+ __version__ = "7.1.0"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
35
  "2.11.0"
@@ -135,6 +135,11 @@ class PostgresHook(DbApiHook):
135
135
  :param options: Optional. Specifies command-line options to send to the server
136
136
  at connection start. For example, setting this to ``-c search_path=myschema``
137
137
  sets the session's value of the ``search_path`` to ``myschema``.
138
+ :param sqlalchemy_scheme: Optional. The SQLAlchemy ``drivername`` used for the URLs the hook
139
+ builds (``get_uri``, ``get_sqlalchemy_engine``), e.g. ``postgresql+psycopg2``. Must be
140
+ ``postgresql`` or ``postgresql+<driver>``. Defaults to ``postgresql+psycopg`` when
141
+ psycopg (v3) serves SQLAlchemy 2.x and to ``postgresql`` otherwise. Can also be set via
142
+ the connection extra ``sqlalchemy_scheme``; this parameter takes precedence.
138
143
  :param enable_log_db_messages: Optional. If enabled logs database messages sent to the client
139
144
  during the session. To avoid a memory leak psycopg2 only saves the last 50 messages.
140
145
  For details, see: `PostgreSQL logging configuration parameters
@@ -164,18 +169,39 @@ class PostgresHook(DbApiHook):
164
169
  default_azure_oauth_scope = "https://ossrdbms-aad.database.windows.net/.default"
165
170
 
166
171
  def __init__(
167
- self, *args, options: str | None = None, enable_log_db_messages: bool = False, **kwargs
172
+ self,
173
+ *args,
174
+ options: str | None = None,
175
+ enable_log_db_messages: bool = False,
176
+ sqlalchemy_scheme: str | None = None,
177
+ **kwargs,
168
178
  ) -> None:
169
179
  super().__init__(*args, **kwargs)
170
180
  self.conn: CompatConnection | None = None
171
181
  self.database: str | None = kwargs.pop("database", None)
172
182
  self.options = options
173
183
  self.enable_log_db_messages = enable_log_db_messages
184
+ self._sqlalchemy_scheme = sqlalchemy_scheme
174
185
 
175
186
  @staticmethod
176
187
  def __cast_nullable(value, dst_type: type) -> Any:
177
188
  return dst_type(value) if value is not None else None
178
189
 
190
+ @property
191
+ def sqlalchemy_scheme(self) -> str:
192
+ """SQLAlchemy ``drivername`` used for the URLs built by this hook."""
193
+ scheme = self._sqlalchemy_scheme or self.connection.extra_dejson.get("sqlalchemy_scheme")
194
+ if not scheme:
195
+ return "postgresql+psycopg" if USE_PSYCOPG3 else "postgresql"
196
+ if ":" in scheme or "/" in scheme:
197
+ raise ValueError("The parameter 'sqlalchemy_scheme' must not contain ':' or '/' characters!")
198
+ if scheme != "postgresql" and not scheme.startswith("postgresql+"):
199
+ raise ValueError(
200
+ f"The parameter 'sqlalchemy_scheme' must be 'postgresql' or 'postgresql+<driver>', "
201
+ f"got: {scheme!r}"
202
+ )
203
+ return scheme
204
+
179
205
  @property
180
206
  def sqlalchemy_url(self) -> URL:
181
207
  try:
@@ -192,7 +218,7 @@ class PostgresHook(DbApiHook):
192
218
  if conn.extra_dejson.get("iam", False):
193
219
  conn.login, conn.password, conn.port = self.get_iam_token(conn)
194
220
  return URL.create(
195
- drivername="postgresql+psycopg" if USE_PSYCOPG3 else "postgresql",
221
+ drivername=self.sqlalchemy_scheme,
196
222
  username=self.__cast_nullable(conn.login, str),
197
223
  password=self.__cast_nullable(conn.password, str),
198
224
  host=self.__cast_nullable(conn.host, str),
@@ -633,6 +633,93 @@ class TestPostgresHookConnPPG3:
633
633
  )
634
634
 
635
635
 
636
+ class TestPostgresHookSqlalchemyScheme:
637
+ """Tests for overriding the SQLAlchemy ``drivername`` via the sqlalchemy_scheme extra/parameter."""
638
+
639
+ @staticmethod
640
+ def get_hook(extra: dict | None = None, **hook_kwargs) -> PostgresHook:
641
+ conn = Connection(
642
+ login="login-conn", password="password-conn", host="host", schema="database", extra=extra
643
+ )
644
+ return PostgresHook(connection=conn, **hook_kwargs)
645
+
646
+ @pytest.mark.parametrize("scheme", ["postgresql", "postgresql+psycopg2", "postgresql+psycopg"])
647
+ def test_sqlalchemy_scheme_from_extra(self, scheme):
648
+ hook = self.get_hook(extra=dict(sqlalchemy_scheme=scheme))
649
+ expected = f"{scheme}://login-conn:password-conn@host/database"
650
+ assert hook.sqlalchemy_url.render_as_string(hide_password=False) == expected
651
+
652
+ def test_sqlalchemy_scheme_parameter_takes_precedence_over_extra(self):
653
+ hook = self.get_hook(
654
+ extra=dict(sqlalchemy_scheme="postgresql"), sqlalchemy_scheme="postgresql+psycopg2"
655
+ )
656
+ expected = "postgresql+psycopg2://login-conn:password-conn@host/database"
657
+ assert hook.sqlalchemy_url.render_as_string(hide_password=False) == expected
658
+
659
+ def test_get_uri_with_sqlalchemy_scheme(self):
660
+ hook = self.get_hook(extra=dict(sqlalchemy_scheme="postgresql+psycopg2"))
661
+ assert hook.get_uri() == "postgresql+psycopg2://login-conn:password-conn@host/database"
662
+
663
+ @pytest.mark.parametrize("scheme", ["mysql", "mysql+pymysql", "postgres+psycopg2"])
664
+ def test_sqlalchemy_scheme_with_wrong_dialect(self, scheme):
665
+ hook = self.get_hook(extra=dict(sqlalchemy_scheme=scheme))
666
+ with pytest.raises(
667
+ ValueError, match="'sqlalchemy_scheme' must be 'postgresql' or 'postgresql\\+<driver>'"
668
+ ):
669
+ hook.sqlalchemy_url
670
+
671
+ @pytest.mark.parametrize("scheme", ["postgresql+psycopg2://malicious", "postgresql+psycopg2/malicious"])
672
+ def test_sqlalchemy_scheme_with_forbidden_characters(self, scheme):
673
+ hook = self.get_hook(extra=dict(sqlalchemy_scheme=scheme))
674
+ with pytest.raises(ValueError, match="must not contain ':' or '/' characters"):
675
+ hook.sqlalchemy_url
676
+
677
+
678
+ @pytest.mark.backend("postgres")
679
+ class TestPostgresHookPandasToSqlUuid:
680
+ """DataFrame.to_sql with string values into a uuid column: fails on psycopg3 because SQLAlchemy
681
+ renders typed bind casts, works when the connection opts back into psycopg2 via sqlalchemy_scheme."""
682
+
683
+ table = "test_pandas_to_sql_uuid_table"
684
+
685
+ PSYCOPG3_XFAIL_REASON = (
686
+ "The psycopg3 SQLAlchemy dialect renders typed bind casts, so string params fail with "
687
+ "'column is of type uuid but expression is of type character varying' instead of being "
688
+ "implicitly coerced as under psycopg2. Tracked upstream in "
689
+ "https://github.com/pandas-dev/pandas/issues/63511, "
690
+ "https://github.com/apache/arrow/pull/50325, "
691
+ "https://github.com/sqlalchemy/sqlalchemy/discussions/10839 and "
692
+ "https://github.com/sqlalchemy/sqlalchemy/issues/12060"
693
+ )
694
+
695
+ def teardown_method(self):
696
+ with PostgresHook().get_conn() as conn:
697
+ with conn.cursor() as cur:
698
+ cur.execute(f"DROP TABLE IF EXISTS {self.table}")
699
+
700
+ def insert_string_uuid_df(self, hook: PostgresHook) -> None:
701
+ engine = hook.get_sqlalchemy_engine()
702
+ with engine.begin() as conn:
703
+ conn.execute(sqlalchemy.text(f"CREATE TABLE {self.table} (id UUID PRIMARY KEY, item TEXT)"))
704
+ df = pd.DataFrame({"id": ["b29d6cda-04ca-4073-8ef8-4f70d35e41e2"], "item": ["laptop"]})
705
+ df.to_sql(self.table, engine, if_exists="append", index=False)
706
+
707
+ @pytest.mark.skipif(not USE_PSYCOPG3, reason="psycopg v3 or sqlalchemy v2 not available")
708
+ @pytest.mark.xfail(
709
+ raises=(sqlalchemy.exc.ProgrammingError, pd.errors.DatabaseError),
710
+ strict=True,
711
+ reason=PSYCOPG3_XFAIL_REASON,
712
+ )
713
+ def test_to_sql_string_uuid_fails_on_psycopg3(self):
714
+ self.insert_string_uuid_df(PostgresHook())
715
+
716
+ def test_to_sql_string_uuid_works_with_psycopg2_scheme(self):
717
+ hook = PostgresHook(sqlalchemy_scheme="postgresql+psycopg2")
718
+ self.insert_string_uuid_df(hook)
719
+ with hook.get_sqlalchemy_engine().connect() as conn:
720
+ assert conn.execute(sqlalchemy.text(f"SELECT COUNT(*) FROM {self.table}")).scalar() == 1
721
+
722
+
636
723
  @pytest.mark.backend("postgres")
637
724
  class TestPostgresHook:
638
725
  """Tests that are identical between psycopg2 and psycopg3."""