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) diff --git a/src/flask_sqlalchemy/model.py b/src/flask_sqlalchemy/model.py index 6468a734..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) @@ -109,6 +115,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__" ): @@ -136,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__) @@ -206,6 +221,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__)