Skip to content

Instantly share code, notes, and snippets.

@zzzeek
Last active June 7, 2023 20:42
Show Gist options
  • Save zzzeek/3c027db82a4222d01f71fe503228dfc5 to your computer and use it in GitHub Desktop.
Save zzzeek/3c027db82a4222d01f71fe503228dfc5 to your computer and use it in GitHub Desktop.
issues with pydantic dataclasses / sqlalchemy
from __future__ import annotations
from typing import TYPE_CHECKING
import pydantic.dataclasses
from sqlalchemy import Column
from sqlalchemy import create_engine
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
class Base(
MappedAsDataclass,
DeclarativeBase,
dataclass_callable=pydantic.dataclasses.dataclass,
):
if not TYPE_CHECKING:
# workaround for problem 1 below
id = Column(Integer, primary_key=True)
class B(Base):
__tablename__ = "b"
# problem 1 - there is no way to use a pydantic dataclass field
# with init=False that keeps the value totally unset. this field
# fails validation. if we add default=None to pass validation, SQLAlchemy
# does not use the server side primary key generator.
# SQLAlchemy will likely need to add a new default value DONT_SET to
# work around this
# id: Mapped[int | None] = mapped_column(primary_key=True, init=False)
a_id: Mapped[int | None] = mapped_column(
ForeignKey("a.id"), init=False, default=None
)
class A(Base):
__tablename__ = "a"
# also problem 1 here
# id: Mapped[int | None] = mapped_column(primary_key=True, init=False)
data: Mapped[str]
bs: Mapped[list[B]] = relationship("B")
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
with Session(e) as s:
# problem 2. Pydantic lets the class init, then validators
# come in and *rewrite* the collections, then assign them on __dict__.
# patch that fixes this specific case here, however there can be many
# more since pydantic writes into `__dict__` quite a lot
a1 = A(data="a1", bs=[B(), B()])
s.add(a1)
s.commit()
@zzzeek
Copy link
Author

zzzeek commented Jun 7, 2023

how does the int id with init=False work? does it only validate arguments that were actually passed ? (because hooray if so?)

@CaselIT
Copy link

CaselIT commented Jun 7, 2023

how does the int id with init=False work? does it only validate arguments that were actually passed ? (because hooray if so?)

no clue. I guess it does?

@CaselIT
Copy link

CaselIT commented Jun 7, 2023

I think the best option at the moment is to wait for v2 to be released and at that point look if anything is still needed. At the moment is seems no, but it's a beta the current v2 version

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment