Composite columns and None

256 views
Skip to first unread message

jens.t...@gmail.com

unread,
Nov 14, 2023, 6:02:16 PM11/14/23
to sqlalchemy
Hello everyone,

I wanted to follow up on the examples onĀ Composite Column TypesĀ and in particular setting the mapped composite value in Python-land to None.

For instance, this class
@dataclasses.dataclass
class Point:
Ā  Ā  x: int
Ā  Ā  y: int
is used in the following composite mapping and I’d like the two mapped properties to be optional (for the sake of the argument, whether it makes sense or not):
class Vertex(Base):
Ā  Ā  start: Mapped[Point] | None = composite(mapped_column("x1"), mapped_column("y1"))
Ā  Ā Ā 
end: Mapped[Point] | None = composite(mapped_column("x2"), mapped_column("y2"))
The optional would then have to map to nullable=TrueĀ for both mapped columns, which in turn would need to be checked to ensure integrity — either both or neither of the two mapped columns for a Point can be NULL at the same time.

I wasn’t able to find a recipeĀ for this use case. What’s the recommendation to implement this? Will I have to roll most of this manually, or does SQLA provide support?

Much thanks!
Jens

jens.t...@gmail.com

unread,
Nov 14, 2023, 11:04:04 PM11/14/23
to sqlalchemy
Come to think of it, mapping to NoneĀ doesn’t make much sense because then there’s no mapper.

However, it probably would make sense to define an ā€œinvalidā€ mapped Point if all mapped columns are NULL. In that case, I’d still consider adding check constraints to mildly improve integrity, although if any one mapped properties is NULL then by definition to Point would be invalid.

Hmm šŸ¤”



Mike Bayer

unread,
Nov 15, 2023, 8:42:05 AM11/15/23
to noreply-spamdigest via sqlalchemy
you can create this using a custom callable for the actual composite type, where you would be bypassing the new-style annotations and dataclass detection part of things and relying on the older style of declaration.Ā Ā  You'd add a composite_values to your dataclass as well and just use the old style

import dataclasses

from sqlalchemy import create_engine
from sqlalchemy import Integer
from sqlalchemy import select
from sqlalchemy.orm import composite
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import Session


@dataclasses.dataclass
class Point:
Ā Ā Ā  x: int | None
Ā Ā Ā  y: int | None

Ā Ā Ā  def __composite_values__(self):
Ā Ā Ā Ā Ā Ā Ā  return (self.x, self.y)


class Base(DeclarativeBase):
Ā Ā Ā  pass


def nullable_point(x, y):
Ā Ā Ā  if x is None and y is None:
Ā Ā Ā Ā Ā Ā Ā  return None
Ā Ā Ā  else:
Ā Ā Ā Ā Ā Ā Ā  return Point(x, y)


class Vertex(Base):
Ā Ā Ā  __tablename__ = "vertices"

Ā Ā Ā  id: Mapped[int] = mapped_column(primary_key=True)

Ā Ā Ā  start: Mapped[Point | None] = composite(
Ā Ā Ā Ā Ā Ā Ā  nullable_point,
Ā Ā Ā Ā Ā Ā Ā  mapped_column("x1", Integer, nullable=True),
Ā Ā Ā Ā Ā Ā Ā  mapped_column("y1", Integer, nullable=True),
Ā Ā Ā  )
Ā Ā Ā  end: Mapped[Point | None] = composite(
Ā Ā Ā Ā Ā Ā Ā  nullable_point,
Ā Ā Ā Ā Ā Ā Ā  mapped_column("x2", Integer, nullable=True),
Ā Ā Ā Ā Ā Ā Ā  mapped_column("y2", Integer, nullable=True),
Ā Ā Ā  )

Ā Ā Ā  def __repr__(self):
Ā Ā Ā Ā Ā Ā Ā  return f"Vertex(start={self.start}, end={self.end})"


e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

s = Session(e)

s.add_all(
Ā Ā Ā  [
Ā Ā Ā Ā Ā Ā Ā  Vertex(start=Point(5, 10)),
Ā Ā Ā Ā Ā Ā Ā  Vertex(start=Point(10, None), end=Point(25, 17)),
Ā Ā Ā  ]
)
s.commit()

for v in s.scalars(select(Vertex)):
Ā Ā Ā  print(f"{v.start} {v.end}")






Much thanks!
Jens


--
SQLAlchemy -
The Python SQL Toolkit and Object Relational Mapper
Ā 
Ā 
To post example code, please provide an MCVE: Minimal, Complete, and Verifiable Example. See http://stackoverflow.com/help/mcve for a full description.
---
You received this message because you are subscribed to the Google Groups "sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email to sqlalchemy+...@googlegroups.com.

jens.t...@gmail.com

unread,
Nov 26, 2023, 11:57:33 PM11/26/23
to sqlalchemy
Thank you, Mike, for the suggestion!

Another thought I mulled over was toĀ create a new pointĀ table and then use a nullable FK from the vertexĀ table. That way, the PointĀ class can map to its own dedicated table, and a VertexĀ class would then use a relationship that’s able to map optional Points to a Vertex via that FK relationship šŸ¤”

Once we get to implementing this, I’ll let you know what we did…

jens.t...@gmail.com

unread,
Feb 16, 2024, 2:53:07 AM2/16/24
to sqlalchemy
I wanted to follow up on this thread.

Mike’s suggestion above worked, and considering the related discussionĀ Dataclasses and Composites with init-only variablesĀ I adjusted the code somewhat: instead of using the nullable_point()Ā function (from above) I extended theĀ as_composite() class method (from the other thread) as suggested:

@classmethod
def as_composite(cls, x: int | None, y: int | None) -> Point | None:
Ā  Ā  if x is None and y is None:
Ā  Ā  Ā  Ā  return None
Ā  Ā  return Point(x, y)

Then adjust the mapper types accordingly, and that’s it šŸ‘šŸ¼

Thanks!
Jens

Reply all
Reply to author
Forward
0 new messages