Skip to content

No typed path to .scalars() when exec() gets a compound select from .union() #2040

Description

@Pedrexus

Description

Combining two selects with .union() and running them through session.exec() works at runtime, but the result's .scalars() doesn't survive type checking:

from sqlmodel import Field, Session, SQLModel, create_engine, select


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str


class Villain(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str


engine = create_engine("sqlite://")
SQLModel.metadata.create_all(engine)

with Session(engine) as session:
    session.add(Hero(name="Deadpond"))
    session.add(Villain(name="Chaos"))
    session.commit()
    statement = select(Hero.name).union(select(Villain.name))
    result = session.exec(statement)
    names = result.scalars().all()  # runs fine, rejected by type checkers
    print(names)  # ['Chaos', 'Deadpond']

At runtime .union() returns a SQLAlchemy CompoundSelect, and exec() hands back the plain SQLAlchemy Result (a ChunkedIteratorResult here), which has .scalars(). On the typed surface though:

  • pyright 1.1.407 rejects the exec() call itself: No overloads for "exec" match the provided argumentsCompoundSelect[tuple[str]] is not SQLModel's Select/SelectOfScalar, so only the UpdateBase overload is left and it doesn't fit either.
  • ty 0.0.59 resolves exec() to TupleResult[Unknown] and then rejects the attribute: Object of type TupleResult[Unknown] has no attribute scalars. TupleResult is SQLAlchemy's typing-only class that whitelists a subset of Result methods, and scalars() isn't among them.

So any compound select (union, intersect, except) currently has no way to reach .scalars() without a cast or an ignore.

Versions: sqlmodel 0.0.39, SQLAlchemy 2.1.0b3, Python 3.14.

A fix could be an exec() overload accepting CompoundSelect (or Executable) that returns Result[Any], similar to what #909 did for update/delete statements, or a tuple-result annotation that declares scalars().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions