Typorithm · medium

Set-based updates and the identity map

pythonsqlalchemyalembicdatabasesindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Set-based updates and the identity map

0%

Concept and contract

Keep loaded ORM objects consistent with an explicit bulk statement and verify the database separately.

A Session identity map retains mapped instances that have already been loaded. A set-based UPDATE can change several database rows at once, so the program must decide how those existing Python objects are synchronized with that change. Correct SQL alone does not guarantee that a later read from an already loaded object reflects the same state.

The fixture loads all Role objects, increments scores below a threshold, then reports values from both those objects and a scalar-column query against the database. Comparing these two observations makes stale in-memory state visible instead of hiding it behind another entity lookup.

Mechanism and worked example

The worked UPDATE uses a column predicate and the expression Role.score + increment.

The worked UPDATE uses a column predicate and the expression Role.score + increment. Session.execute receives synchronize_session set to fetch so affected identities can be synchronized. For scores [1, 5, 9], threshold 5 and increment 10, both report paths must yield [11, 5, 9].

The database observation selects score columns rather than simply returning the same loaded objects again. This matters because a second entity query can resolve through existing identities. The statement and its transaction still have clear owners; synchronization does not independently commit the database change or replace the need to handle an exception.

Complete worked example

python

from sqlalchemy import create_engine, select, text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Role(Base):
    __tablename__ = "roles"
    id: Mapped[int] = mapped_column(primary_key=True)
    score: Mapped[int]

from sqlalchemy import update

def inspect_example(scores, threshold, increment):
    engine = create_engine("sqlite+pysqlite://", connect_args={"autocommit": False})
    try:
        Base.metadata.create_all(engine)
        with Session(engine) as session, session.begin():
            session.add_all(Role(id=i, score=value) for i, value in enumerate(scores, 1))
        with Session(engine) as session, session.begin():
            loaded = session.scalars(select(Role).order_by(Role.id)).all()
            statement = update(Role).where(Role.score < threshold).values(score=Role.score + increment)
            session.execute(statement, execution_options={"synchronize_session": "fetch"})
            objects = [role.score for role in loaded]
            database = list(session.scalars(select(Role.score).order_by(Role.id)))
        return {"objects": objects, "database": database}
    finally:
        engine.dispose()
Reasoning
The worked UPDATE uses a column predicate and the expression Role.score + increment. Session.execute receives synchronize_session set to fetch so affected identities can be synchronized. For scores [1, 5, 9], threshold 5 and increment 10, both report paths must yield [11, 5, 9]. The database observation selects score columns rather than simply returning the same loaded objects again. This matters because a second entity query can resolve through existing identities. The statement and its transaction still have clear owners; synchronization does not independently commit the database change or replace the need to handle an exception.
Download the SQLAlchemy and Alembic computer lab ↗

Focused decision: context is in the complete example

python

statement = update(Role).where(Role.score < threshold).values(score=Role.score + increment)
session.execute(statement, execution_options={"synchronize_session": "fetch"})
Database decision
fetch synchronization keeps already loaded instances aligned with this UPDATE. The separate scalar query checks storage rather than trusting the same identity-map objects.
Download the SQLAlchemy and Alembic computer lab ↗

Counterexamples and common mistakes

The controlled mistake disables synchronization while keeping the same bulk UPDATE.

The controlled mistake disables synchronization while keeping the same bulk UPDATE. The scalar database values change, but loaded objects may still contain the old scores. A caller that builds its response from those objects can therefore report something different from the committed data.

Do not assume every backend supports the same synchronization details or RETURNING behavior. Also distinguish arbitrary-WHERE DML from per-instance ORM flushes. A mapper’s version counter is not automatically a guard for every bulk UPDATE, so a separate concurrency predicate may be required for a service that uses that style of write.

Controlled bug: observe the rejected database contract

python

from sqlalchemy import create_engine, select, text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Role(Base):
    __tablename__ = "roles"
    id: Mapped[int] = mapped_column(primary_key=True)
    score: Mapped[int]

from sqlalchemy import update

def inspect_example(scores, threshold, increment):
    engine = create_engine("sqlite+pysqlite://", connect_args={"autocommit": False})
    try:
        Base.metadata.create_all(engine)
        with Session(engine) as session, session.begin():
            session.add_all(Role(id=i, score=value) for i, value in enumerate(scores, 1))
        with Session(engine) as session, session.begin():
            loaded = session.scalars(select(Role).order_by(Role.id)).all()
            statement = update(Role).where(Role.score < threshold).values(score=Role.score + increment)
            session.execute(statement, execution_options={"synchronize_session": False})
            objects = [role.score for role in loaded]
            database = list(session.scalars(select(Role.score).order_by(Role.id)))
        return {"objects": objects, "database": database}
    finally:
        engine.dispose()
Reasoning
The controlled mistake disables synchronization while keeping the same bulk UPDATE. The scalar database values change, but loaded objects may still contain the old scores. A caller that builds its response from those objects can therefore report something different from the committed data. Do not assume every backend supports the same synchronization details or RETURNING behavior. Also distinguish arbitrary-WHERE DML from per-instance ORM flushes. A mapper’s version counter is not automatically a guard for every bulk UPDATE, so a separate concurrency predicate may be required for a service that uses that style of write.
Download the SQLAlchemy and Alembic computer lab ↗

Independent practice

Type the statement and its execution option, then predict both observations for a value exactly equal to the threshold.

Type the statement and its execution option, then predict both observations for a value exactly equal to the threshold. The independent task makes that boundary inclusive: increment scores equal to the threshold as well as those below it. Keep synchronization enabled and preserve the unchanged higher scores.

Change the WHERE comparison rather than modifying the returned dictionary or iterating over every object to fake the result. Retain the zero, negative-score and empty-population cases. These boundaries ensure the task checks a precise set-based rule rather than merely reproducing one positive example by hand.

Step 1: Include scores exactly equal to the threshold while keeping loaded objects synchronized. Step 2: Keep fetch synchronization and the database observation unchanged.

Practice starter: solve before reading the solution

python

from sqlalchemy import create_engine, select, text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Role(Base):
    __tablename__ = "roles"
    id: Mapped[int] = mapped_column(primary_key=True)
    score: Mapped[int]

from sqlalchemy import update

def inspect_example(scores, threshold, increment):
    engine = create_engine("sqlite+pysqlite://", connect_args={"autocommit": False})
    try:
        Base.metadata.create_all(engine)
        with Session(engine) as session, session.begin():
            session.add_all(Role(id=i, score=value) for i, value in enumerate(scores, 1))
        with Session(engine) as session, session.begin():
            loaded = session.scalars(select(Role).order_by(Role.id)).all()
            statement = update(Role).where(Role.score < threshold).values(score=Role.score + increment)
            session.execute(statement, execution_options={"synchronize_session": "fetch"})
            objects = [role.score for role in loaded]
            database = list(session.scalars(select(Role.score).order_by(Role.id)))
        return {"objects": objects, "database": database}
    finally:
        engine.dispose()

# Task: Include scores exactly equal to the threshold while keeping loaded objects synchronized.
Reasoning
Include scores exactly equal to the threshold while keeping loaded objects synchronized.
Download the SQLAlchemy and Alembic computer lab ↗
  • Hint 1: Change only the strict threshold comparison.
  • Hint 2: Include a score equal to the threshold in the acceptance case.

Solution, tests and trade-offs

The solution replaces the strict comparison with an inclusive comparison.

The solution replaces the strict comparison with an inclusive comparison. Its cases require the exact updated list from both the loaded objects and the scalar database query. The empty fixture remains empty, and every caller-owned input list remains unchanged. An implementation that updates storage but reports stale instances fails the contract.

This lesson uses a small owned database and one transaction. Real batching also needs an explicit admission predicate, update size, concurrency policy and failure behavior. The fixture demonstrates an observable synchronization decision; it does not establish that a large production UPDATE is safe simply because a similar statement passed on three rows.

Practice solution and acceptance checks

python

from sqlalchemy import create_engine, select, text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Role(Base):
    __tablename__ = "roles"
    id: Mapped[int] = mapped_column(primary_key=True)
    score: Mapped[int]

from sqlalchemy import update

def inspect_example(scores, threshold, increment):
    engine = create_engine("sqlite+pysqlite://", connect_args={"autocommit": False})
    try:
        Base.metadata.create_all(engine)
        with Session(engine) as session, session.begin():
            session.add_all(Role(id=i, score=value) for i, value in enumerate(scores, 1))
        with Session(engine) as session, session.begin():
            loaded = session.scalars(select(Role).order_by(Role.id)).all()
            statement = update(Role).where(Role.score <= threshold).values(score=Role.score + increment)
            session.execute(statement, execution_options={"synchronize_session": "fetch"})
            objects = [role.score for role in loaded]
            database = list(session.scalars(select(Role.score).order_by(Role.id)))
        return {"objects": objects, "database": database}
    finally:
        engine.dispose()
Reasoning
The solution replaces the strict comparison with an inclusive comparison. Its cases require the exact updated list from both the loaded objects and the scalar database query. The empty fixture remains empty, and every caller-owned input list remains unchanged. An implementation that updates storage but reports stale instances fails the contract. This lesson uses a small owned database and one transaction. Real batching also needs an explicit admission predicate, update size, concurrency policy and failure behavior. The fixture demonstrates an observable synchronization decision; it does not establish that a large production UPDATE is safe simply because a similar statement passed on three rows.
Download the SQLAlchemy and Alembic computer lab ↗

Interview reasoning and next step

Explain the difference between changing a row, synchronizing an identity map and committing a transaction.

Explain the difference between changing a row, synchronizing an identity map and committing a transaction. Give an example in which the database is correct but the response built from loaded objects is stale. Describe why selecting scalar columns is useful as a separate observation in this fixture.

Next, handle a database constraint failure without discarding unrelated valid work. A failed flush changes Session transaction state, so catching an exception alone is not recovery. Savepoints let the program define a smaller failure boundary when the domain deliberately wants to keep the surrounding transaction.

Optional stretch — after the core task works: Write an explicit version predicate for an owned bulk update and check its affected-row result.

Computer practice: https://applydjinn.com/typorithm/sqlalchemy-lab-20261107.zip. Install the pinned requirements in an isolated Python 3.13 environment and run verify.py. Edit the working exercise baseline, then run --practice sa-dml. The lab uses disposable SQLite databases and actual SQLAlchemy/Alembic calls; it does not connect to application accounts, production databases or paid services. The app includes prepared results and explained questions without an editor or Python runtime.

Keep loaded ORM objects consistent with an explicit bulk statement and verify the database separately.