fastapi icon indicating copy to clipboard operation
fastapi copied to clipboard

Upgrade databases and SQLAlchemy

Open musicinmybrain opened this issue 1 year ago • 29 comments

Incompatibilities with recent SQLAlchemy versions are fixed in databases 0.7.0. By requiring databases[sqlite] >=0.7.0,<0.8.0 for testing, we can stop pinning an old SQLAlchemy version, instead requiring sqlalchemy >=1.4.42,<1.5.

Fixes https://github.com/tiangolo/fastapi/issues/5556.

musicinmybrain avatar Dec 18 '22 16:12 musicinmybrain

📝 Docs preview for commit 574b0a20b8161e5367b3316d44cc058ef4019e02 at: https://639f3fe2d8175d240d2d6490--fastapi.netlify.app

github-actions[bot] avatar Dec 18 '22 16:12 github-actions[bot]

📝 Docs preview for commit bd2295f1a26b0be28f58444c53008b72a54797d2 at: https://63b9f42d42bbfe7af491a2cf--fastapi.netlify.app

github-actions[bot] avatar Jan 07 '23 22:01 github-actions[bot]

Thanks for looking at it. Those warnings were added in sqlalchemy 1.4.46, which was released after I opened this PR. I see three possible courses of action:

  1. Since the version specification in the PR does not permit SQLAlchemy 2.0, ignore these forward-compatibility warnings (by filtering them in pyproject.toml or by setting the environment variable SQLALCHEMY_SILENCE_UBER_WARNING=1 to suppress them).
  2. Pin SQLAlchemy to <1.4.46; but this also keeps users from benefiting from the bugfixes in an otherwise-compatible release.
  3. Find the places where deprecated API features are used, and adjust them to be forward-compatible with SQLAlchemy 2.0.

I’ll take a closer look at this and see if there is an obvious way to do option 3.

musicinmybrain avatar Jan 15 '23 16:01 musicinmybrain

Here’s the full output:

====================================================== FAILURES ======================================================
__________________________________________________ test_create_read __________________________________________________

    def test_create_read():
        with TestClient(app) as client:
            note = {"text": "Foo bar", "completed": False}
            response = client.post("/notes/", json=note)
            assert response.status_code == 200, response.text
            data = response.json()
            assert data["text"] == note["text"]
            assert data["completed"] == note["completed"]
            assert "id" in data
>           response = client.get("/notes/")

tests/test_tutorial/test_async_sql_databases/test_tutorial001.py:129: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
_e/lib64/python3.11/site-packages/starlette/testclient.py:488: in get
    return super().get(
_e/lib64/python3.11/site-packages/httpx/_client.py:1045: in get
    return self.request(
_e/lib64/python3.11/site-packages/starlette/testclient.py:454: in request
    return super().request(
_e/lib64/python3.11/site-packages/httpx/_client.py:821: in request
    return self.send(request, auth=auth, follow_redirects=follow_redirects)
_e/lib64/python3.11/site-packages/httpx/_client.py:908: in send
    response = self._send_handling_auth(
_e/lib64/python3.11/site-packages/httpx/_client.py:936: in _send_handling_auth
    response = self._send_handling_redirects(
_e/lib64/python3.11/site-packages/httpx/_client.py:973: in _send_handling_redirects
    response = self._send_single_request(request)
_e/lib64/python3.11/site-packages/httpx/_client.py:1009: in _send_single_request
    response = transport.handle_request(request)
_e/lib64/python3.11/site-packages/starlette/testclient.py:337: in handle_request
    raise exc
_e/lib64/python3.11/site-packages/starlette/testclient.py:334: in handle_request
    portal.call(self.app, scope, receive, send)
_e/lib64/python3.11/site-packages/anyio/from_thread.py:283: in call
    return cast(T_Retval, self.start_task_soon(func, *args).result())
/usr/lib64/python3.11/concurrent/futures/_base.py:456: in result
    return self.__get_result()
/usr/lib64/python3.11/concurrent/futures/_base.py:401: in __get_result
    raise self._exception
_e/lib64/python3.11/site-packages/anyio/from_thread.py:219: in _call_func
    retval = await retval
fastapi/applications.py:270: in __call__
    await super().__call__(scope, receive, send)
_e/lib64/python3.11/site-packages/starlette/applications.py:124: in __call__
    await self.middleware_stack(scope, receive, send)
_e/lib64/python3.11/site-packages/starlette/middleware/errors.py:184: in __call__
    raise exc
_e/lib64/python3.11/site-packages/starlette/middleware/errors.py:162: in __call__
    await self.app(scope, receive, _send)
_e/lib64/python3.11/site-packages/starlette/middleware/exceptions.py:79: in __call__
    raise exc
_e/lib64/python3.11/site-packages/starlette/middleware/exceptions.py:68: in __call__
    await self.app(scope, receive, sender)
fastapi/middleware/asyncexitstack.py:21: in __call__
    raise e
fastapi/middleware/asyncexitstack.py:18: in __call__
    await self.app(scope, receive, send)
_e/lib64/python3.11/site-packages/starlette/routing.py:706: in __call__
    await route.handle(scope, receive, send)
_e/lib64/python3.11/site-packages/starlette/routing.py:276: in handle
    await self.app(scope, receive, send)
_e/lib64/python3.11/site-packages/starlette/routing.py:66: in app
    response = await func(request)
fastapi/routing.py:255: in app
    content = await serialize_response(
fastapi/routing.py:131: in serialize_response
    value, errors_ = field.validate(response_content, {}, loc=("response",))
pydantic/fields.py:895: in pydantic.fields.ModelField.validate
    ???
pydantic/fields.py:928: in pydantic.fields.ModelField._validate_sequence_like
    ???
pydantic/fields.py:1094: in pydantic.fields.ModelField._validate_singleton
    ???
pydantic/fields.py:884: in pydantic.fields.ModelField.validate
    ???
pydantic/fields.py:1101: in pydantic.fields.ModelField._validate_singleton
    ???
pydantic/fields.py:1148: in pydantic.fields.ModelField._apply_validators
    ???
pydantic/class_validators.py:318: in pydantic.class_validators._generic_validator_basic.lambda13
    ???
pydantic/main.py:717: in pydantic.main.BaseModel.validate
    ???
<string>:2: in keys
    ???
_e/lib64/python3.11/site-packages/sqlalchemy/util/deprecations.py:467: in warned
    _warn_with_version(message, version, wtype, stacklevel=3)
_e/lib64/python3.11/site-packages/sqlalchemy/util/deprecations.py:47: in _warn_with_version
    _emit_uber_warning(type_, stacklevel)
_e/lib64/python3.11/site-packages/sqlalchemy/util/deprecations.py:105: in _emit_uber_warning
    _warnings_warn(warn, stacklevel=stacklevel + 1)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

message = RemovedIn20Warning('Deprecated API features detected! These feature(s) are not compatible with SQLAlchemy 2.0. To prev...o show all deprecation warnings.  Set environment variable SQLALCHEMY_SILENCE_UBER_WARNING=1 to silence this message.')
category = None, stacklevel = 5

    def _warnings_warn(message, category=None, stacklevel=2):
    
        # adjust the given stacklevel to be outside of SQLAlchemy
        try:
            frame = sys._getframe(stacklevel)
        except ValueError:
            # being called from less than 3 (or given) stacklevels, weird,
            # but don't crash
            stacklevel = 0
        except:
            # _getframe() doesn't work, weird interpreter issue, weird,
            # ok, but don't crash
            stacklevel = 0
        else:
            # using __name__ here requires that we have __name__ in the
            # __globals__ of the decorated string functions we make also.
            # we generate this using {"__name__": fn.__module__}
            while frame is not None and re.match(
                r"^(?:sqlalchemy\.|alembic\.)", frame.f_globals.get("__name__", "")
            ):
                frame = frame.f_back
                stacklevel += 1
    
        if category is not None:
            warnings.warn(message, category, stacklevel=stacklevel + 1)
        else:
>           warnings.warn(message, stacklevel=stacklevel + 1)
E           sqlalchemy.exc.RemovedIn20Warning: Deprecated API features detected! These feature(s) are not compatible with SQLAlchemy 2.0. To prevent incompatible upgrades prior to updating applications, ensure requirements files are pinned to "sqlalchemy<2.0". Set environment variable SQLALCHEMY_WARN_20=1 to show all deprecation warnings.  Set environment variable SQLALCHEMY_SILENCE_UBER_WARNING=1 to silence this message. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)

_e/lib64/python3.11/site-packages/sqlalchemy/util/langhelpers.py:1679: RemovedIn20Warning
============================================== short test summary info ===============================================
FAILED tests/test_tutorial/test_async_sql_databases/test_tutorial001.py::test_create_read - sqlalchemy.exc.RemovedIn20Warning: Deprecated API features detected! These feature(s) are not compatible with SQL...
===================================== 1 failed, 1400 passed in 90.00s (0:01:30) ======================================

Unfortunately, this doesn’t carry any information about which deprecated API feature was used, and it’s hard to even be sure where the deprecated usage is happening.

I tried setting 'always::DeprecationWarning', in tool.pytest.ini_options.filterwarnings in pyproject.toml, and got this, which is much briefer but more helpful:

tests/test_tutorial/test_async_sql_databases/test_tutorial001.py::test_create_read
  /home/ben/src/forks/fastapi/fastapi/routing.py:131: RemovedIn20Warning: Deprecated API features detected! These feature(s) are not compatible with SQLAlchemy 2.0. To prevent incompatible upgrades prior to updating applications, ensure requirements files are pinned to "sqlalchemy<2.0". Set environment variable SQLALCHEMY_WARN_20=1 to show all deprecation warnings.  Set environment variable SQLALCHEMY_SILENCE_UBER_WARNING=1 to silence this message. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
    value, errors_ = field.validate(response_content, {}, loc=("response",))

That points at an incompatibility in FastAPI, and it is something specific enough I can reasonably filter it out.

musicinmybrain avatar Jan 17 '23 00:01 musicinmybrain

Added 'ignore::sqlalchemy.exc.RemovedIn20Warning:fastapi', to tool.pytest.ini_options.filterwarnings in pyproject.toml, along with a comment justifying the filtering.

musicinmybrain avatar Jan 17 '23 00:01 musicinmybrain

📝 Docs preview for commit 2deb233dc88c137b2d7343c2dc7a742ffbcfd841 at: https://63c5eb1e4a9d912fcb4f5299--fastapi.netlify.app

github-actions[bot] avatar Jan 17 '23 00:01 github-actions[bot]

📝 Docs preview for commit 656ce503fd90a880346e7bf1a168db10b178385b at: https://64038cb82bd72c60f5c3e0c2--fastapi.netlify.app

github-actions[bot] avatar Mar 04 '23 18:03 github-actions[bot]

Rebased on master again.

musicinmybrain avatar Mar 11 '23 14:03 musicinmybrain

📝 Docs preview for commit 262fa517a92859ee0b068a05b5a7df3872f2a313 at: https://640c9640611f6123320901d1--fastapi.netlify.app

github-actions[bot] avatar Mar 11 '23 14:03 github-actions[bot]

Rebased on master again.

musicinmybrain avatar May 27 '23 16:05 musicinmybrain

📝 Docs preview for commit c4a4592edb74c89ea0b12a92e33a2394ef3130f5 at: https://64722e9cae27a515156fe6e0--fastapi.netlify.app

github-actions[bot] avatar May 27 '23 16:05 github-actions[bot]

Rebased on master again.

musicinmybrain avatar Jun 21 '23 14:06 musicinmybrain

📝 Docs preview for commit 301746390d385ba18d74768a60ec4e098e39600c at: https://64930f371649720e5435aa03--fastapi.netlify.app

tiangolo avatar Jun 21 '23 14:06 tiangolo

Rebased again.

musicinmybrain avatar Jul 12 '23 20:07 musicinmybrain

@tiangolo, any interest in merging this? I’ve been carrying it as a downstream patch in Fedora Linux for quite a while, and it seems like a straighforward improvement that would allow CI testing with current SQLAlchemy releases in the 1.4.x series.

musicinmybrain avatar Aug 09 '23 18:08 musicinmybrain

Rebased again, and adjusted to allow databases 0.8.x too (databases[sqlite] >=0.7.0,<0.9.0).

musicinmybrain avatar Sep 12 '23 11:09 musicinmybrain

Can you clarify. Is SqlAlchemy 2.0 not supported yet? Or is that just a limitation of testing?

jonocodes avatar Oct 15 '23 01:10 jonocodes

Can you clarify. Is SqlAlchemy 2.0 not supported yet? Or is that just a limitation of testing?

These are only test dependencies.

The actual FastAPI package doesn’t import sqlalchemy, but some of the tests (and examples in the documentation, which can be run as tests) do. The databases usage is only in docs_src/async_sql_databases/tutorial001.py, but that means databases constrains the version of sqlalchemy used for testing.

This PR predates the SQLAlchemy 2.0.0 release, so updating to databases 0.7.0 allowed tests to run on the then-latest version of sqlalchemy. Now databases is at 0.8.0, which needs sqlalchemy>=1.4.42,<1.5. So databases is again blocking testing with the latest version of SQLAlchemy, and I should probably edit this PR to restore the original comment about removing the databases dependency in order to upgrade SQLAlchemy.

Still, even if it wouldn’t allow testing with SQLAlchemy 2.0, this PR would at least improve the status quo.

musicinmybrain avatar Oct 15 '23 03:10 musicinmybrain

[…] I should probably edit this PR to restore the original comment about removing the databases dependency in order to upgrade SQLAlchemy.

Rebased on master, and the PR no longer removes the mentioned comment.

musicinmybrain avatar Oct 15 '23 12:10 musicinmybrain

Added 'ignore::sqlalchemy.exc.RemovedIn20Warning:fastapi', to tool.pytest.ini_options.filterwarnings in pyproject.toml, along with a comment justifying the filtering.

It looks like this is no longer necessary in the current version of FastAPI, so I dropped the second commit.

For posterity, since I’m force-pushing again, that commit looked like:

From 6f11a91c4c60914075a5ca36594be499c524f71a Mon Sep 17 00:00:00 2001
From: "Benjamin A. Beasley" <[email protected]>
Date: Mon, 16 Jan 2023 19:19:56 -0500
Subject: [PATCH] Ignore SQLAlchemy RemovedIn20Warning from FastAPI

We have SQLAlchemy pinned anyway; we are not yet concerned about
forward-compatibility with 2.0, especially in dependencies.
---
 pyproject.toml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/pyproject.toml b/pyproject.toml
index 2870b31a..4aa9af44 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -97,6 +97,9 @@ filterwarnings = [
     "error",
     # TODO: needed by asyncio in Python 3.9.7 https://bugs.python.org/issue45097, try to remove on 3.9.8
     'ignore:The loop argument is deprecated since Python 3\.8, and scheduled for removal in Python 3\.10:DeprecationWarning:asyncio',
+    # We have SQLAlchemy pinned anyway; we are not yet concerned about
+    # forward-compatibility with 2.0, especially in dependencies.
+    'ignore::sqlalchemy.exc.RemovedIn20Warning:fastapi',
     'ignore:starlette.middleware.wsgi is deprecated and will be removed in a future release\..*:DeprecationWarning:starlette',
     # TODO: remove after upgrading HTTPX to a version newer than 0.23.0
     # Including PR: https://github.com/encode/httpx/pull/2309
-- 
2.41.0

musicinmybrain avatar Oct 15 '23 13:10 musicinmybrain

It looks like this is no longer necessary in the current version of FastAPI, so I dropped the second commit.

Hmm, the warning doesn’t show up in local testing, but still appears in the pydantic-v1 CI environments. I’ll restore the second commit to suppress the warning.

musicinmybrain avatar Oct 15 '23 13:10 musicinmybrain

Rebased on master again; added 'ignore::sqlalchemy.exc.RemovedIn20Warning:pydantic' since these warnings now arise from Pydantic as well.

musicinmybrain avatar Feb 08 '24 12:02 musicinmybrain

Rebased on master again.

musicinmybrain avatar Apr 30 '24 12:04 musicinmybrain

📝 Docs preview for commit dca742ea8cc1a3ea50de4912209ef3e616b60969 at: https://e0ca5caf.fastapitiangolo.pages.dev

tiangolo avatar Apr 30 '24 13:04 tiangolo

Now upgraded to allow current releases of databases and SQLAlchemy (0.9.x and 2.x, respectively).

Tests are still passing for me locally; we will see if the CI agrees.

musicinmybrain avatar Apr 30 '24 13:04 musicinmybrain

Trying to adjust the warning configuration so that this still works with Pydantic v1…

musicinmybrain avatar Apr 30 '24 14:04 musicinmybrain

📝 Docs preview for commit 229748dc0fad8d9a9fb66cd03751b4ccf3a81033 at: https://b684d8ee.fastapitiangolo.pages.dev

tiangolo avatar Apr 30 '24 14:04 tiangolo

📝 Docs preview for commit 25fe108a78e707b2dfadc2acce3f5b6f44db112b at: https://63296ba9.fastapitiangolo.pages.dev

tiangolo avatar Apr 30 '24 15:04 tiangolo

Now I’m seeing issues like:

E       AssertionError: {"detail":"Email already registered"}
E       assert 400 == 200
E        +  where 400 = <Response [400 Bad Request]>.status_code

and

sqlite3.OperationalError: attempt to write a readonly database

with Pydantic v1. I am not sure what to do about that. Suggestions welcome.

This is still useful as a downstream patch in Fedora, where we have Pydantic v2 only and would like to test with current versions of everything.

musicinmybrain avatar Apr 30 '24 15:04 musicinmybrain

📝 Docs preview for commit 5c88daca1e538785017012431aafd5a741dd76d2 at: https://71d46b8f.fastapitiangolo.pages.dev

tiangolo avatar Jul 16 '24 15:07 tiangolo