Skip to content

unexpected-keyword false positives from the SQLAlchemy update().values() check on SQLModel models with Mapped[] relationships #4902

Description

@leonqadirie

Since 1.3.0, the update(Model).values(field=...) check from #4047 reports every plain column of a SQLModel table model as "Unexpected SQLAlchemy update field" if the model has at least one relationship annotated with sqlalchemy.orm.Mapped[...].

The check takes the model's columns to be the attributes annotated Mapped[...], and skips the model if there are none. That works for DeclarativeBase classes, where every column is Mapped[...]. In SQLModel, columns are Pydantic fields (name: str = Field(...)) and Mapped[...] only appears on Relationship() attributes. So the check sees only the relationships as columns and reports every real column as unknown.

SQLModel accepts Mapped[...] on relationships, though its docs do not use it. SQLModelMetaclass.__init__ (sqlmodel/main.py, since fastapi/sqlmodel#700) unwraps a Mapped[...] annotation if present, otherwise it rewrites the annotation to Mapped[ann] itself; either way it builds the relationship() from the inner type, so both spellings produce the same mapper. selectinload(Model.rel) only type-checks with the explicit form: Mapped.__get__ returns InstrumentedAttribute[_T] for class-level access, while with a plain annotation Model.rel has the instance type and selectinload rejects it.

Reproduction

pyrefly==1.3.0, sqlmodel==0.0.42, sqlalchemy==2.0.52, Python 3.13.

from typing import Optional
from uuid import UUID

from sqlalchemy import update
from sqlalchemy.orm import Mapped
from sqlmodel import Field, Relationship, SQLModel


class Node(SQLModel, table=True):
    id: UUID = Field(primary_key=True)
    name: str = Field(description="x")
    parent_id: UUID | None = Field(default=None, foreign_key="node.id")
    parent: Mapped[Optional["Node"]] = Relationship()


update(Node).values(name="a")        # real column, false positive
update(Node).values(parent_id=None)  # real column, false positive
update(Node).values(nope="a")        # wrong, the check catches it
ERROR Unexpected SQLAlchemy update field `name` [unexpected-keyword]
  --> repro.py:16:21
ERROR Unexpected SQLAlchemy update field `parent_id` [unexpected-keyword]
  --> repro.py:17:21
ERROR Unexpected SQLAlchemy update field `nope` [unexpected-keyword]
  --> repro.py:18:21

Removing the parent relationship, or its Mapped[...] wrapper, makes all three lines pass, including the wrong one, because the model then has no Mapped[...] attributes and the check skips it.

1.2.0 reports nothing for this file.

Expected behavior

Either

  • skip classes whose MRO contains sqlmodel.SQLModel, since the check cannot read their columns from Mapped[...] annotations, or
  • use the Pydantic fields as the columns for SQLModel classes, which keeps the nope error and removes the false positives.

There is no config switch for this check and it uses the generic unexpected-keyword kind, so staying on 1.3.0 means a suppression at every update().values() call on such a model (31 sites in our codebase).

Sandbox Link

Link here

Activity

  1. hrolfurgylfa commented on Sep 13, 2026

    @hrolfurgylfa
    Contributor

    I'm taking a look at this #claim

    So far, I'm going with collecting all fields with an annotation as Mapped fields if SQLModel shows up in the mro.

  2. github-actions commented on Sep 13, 2026

    @github-actions

    @hrolfurgylfa you've claimed this issue — it's now assigned to you. Thanks for picking it up! 🎉

  3. hrolfurgylfa commented on Sep 13, 2026

    @hrolfurgylfa
    Contributor

    Actually, since this modifies the exact same code as #4911, I'll wait with creating the PR until that one is merged.

    I think this should be ready-ish, just need to do some more testing tomorrow. Fix commit is here: hrolfurgylfa@bd50e1f

    It still feels a bit weird to be adding this additional logic for other libraries though, maybe we want to just ignore this check entirely for SQLModel classes instead? I'm not sure

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

Metadata

Metadata

Assignees

Labels

pydanticIssues related to support for PydanticsqlalchemyIssues related to support for SQLAlchemy 2.0typechecking

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions