From 3111f89362417f1835b3146161a89e68ccb982c7 Mon Sep 17 00:00:00 2001 From: mreid-tt <943378+mreid-tt@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:51:50 -0400 Subject: [PATCH 1/3] Fix dataclass mapping error for models defining __table__ --- src/flask_sqlalchemy/model.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/flask_sqlalchemy/model.py b/src/flask_sqlalchemy/model.py index 6468a734..0e6941da 100644 --- a/src/flask_sqlalchemy/model.py +++ b/src/flask_sqlalchemy/model.py @@ -109,6 +109,10 @@ class BindMixin: @classmethod def __init_subclass__(cls: type[BindMixin], **kwargs: dict[str, t.Any]) -> None: + # See note in NameMixin: explicit __table__ + mapped-as-dataclass are incompatible. + if "__table__" in cls.__dict__ and getattr(cls, "__sa_dataclass__", None) is not False: + cls.__sa_dataclass__ = False + if not ("metadata" in cls.__dict__ or "__table__" in cls.__dict__) and hasattr( cls, "__bind_key__" ): @@ -206,6 +210,12 @@ class NameMixin: @classmethod def __init_subclass__(cls: type[NameMixin], **kwargs: dict[str, t.Any]) -> None: + # If mapped-as-dataclass is globally enabled, models that declare an + # explicit __table__ must opt out, otherwise SQLAlchemy raises: + # "ORM Annotated Dataclasses do not support a pre-existing '__table__' element". + if "__table__" in cls.__dict__ and getattr(cls, "__sa_dataclass__", None) is not False: + cls.__sa_dataclass__ = False + if should_set_tablename(cls): cls.__tablename__ = camel_to_snake_case(cls.__name__) From 51a6665a2461a61295f8cddf1913e895cee6b142 Mon Sep 17 00:00:00 2001 From: mreid-tt <943378+mreid-tt@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:58:52 -0400 Subject: [PATCH 2/3] Also opt out in metaclass path for explicit __table__ --- src/flask_sqlalchemy/model.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/flask_sqlalchemy/model.py b/src/flask_sqlalchemy/model.py index 0e6941da..cbe0ccf1 100644 --- a/src/flask_sqlalchemy/model.py +++ b/src/flask_sqlalchemy/model.py @@ -79,6 +79,12 @@ class BindMetaMixin(type): def __init__( cls, name: str, bases: tuple[type, ...], d: dict[str, t.Any], **kwargs: t.Any ) -> None: + # If mapped-as-dataclass is globally enabled, classes that declare an + # explicit __table__ must opt out; otherwise SQLAlchemy 2.x raises: + # "ORM Annotated Dataclasses do not support a pre-existing '__table__' element". + if "__table__" in cls.__dict__ and getattr(cls, "__sa_dataclass__", None) is not False: + cls.__sa_dataclass__ = False + if not ("metadata" in cls.__dict__ or "__table__" in cls.__dict__): bind_key = getattr(cls, "__bind_key__", None) parent_metadata = getattr(cls, "metadata", None) @@ -140,6 +146,11 @@ class NameMetaMixin(type): def __init__( cls, name: str, bases: tuple[type, ...], d: dict[str, t.Any], **kwargs: t.Any ) -> None: + # See note above: explicit __table__ + dataclass transform are incompatible. + # Opt out early during metaclass initialization. + if "__table__" in cls.__dict__ and getattr(cls, "__sa_dataclass__", None) is not False: + cls.__sa_dataclass__ = False + if should_set_tablename(cls): cls.__tablename__ = camel_to_snake_case(cls.__name__) From cce6fa5b4288bc1b6b903857bd66f2ea19db21d6 Mon Sep 17 00:00:00 2001 From: mreid-tt <943378+mreid-tt@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:09:21 -0400 Subject: [PATCH 3/3] SQLA 2.x: disable dataclass transform on base to fix explicit __table__ models --- src/flask_sqlalchemy/extension.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/flask_sqlalchemy/extension.py b/src/flask_sqlalchemy/extension.py index ccae54b4..a6758016 100644 --- a/src/flask_sqlalchemy/extension.py +++ b/src/flask_sqlalchemy/extension.py @@ -544,6 +544,11 @@ def _make_declarative_base( elif len(declarative_bases) == 1: body = dict(model_class.__dict__) body["__fsa__"] = self + # Default to *not* using SQLAlchemy's dataclass transform for db.Model. + # This avoids "ORM Annotated Dataclasses do not support a pre-existing '__table__' element" + # when a model sets __table__ explicitly (as in test_explicit_table). + # Individual models can opt back in with __sa_dataclass__ = True if desired. + body.setdefault("__sa_dataclass__", False) mixin_classes = [BindMixin, NameMixin, Model] if disable_autonaming: mixin_classes.remove(NameMixin)