Skip to content

Sessionmakers with multiple binds only use engines from first registered app #41

@brihall

Description

@brihall

In an application that uses Werkzeug's Application Dispatcher middleware and vertical partitioning, SQLAlchemy.test_isolation was behaving inconsistently during testing with pytest. Tests would pass when run alone but fail when other tests were included, because rows inserted in earlier tests weren't being rolled back.

_make_sessionmaker uses a shallow copy of SQLAlchemy._session_options, so options["binds"] is the same object as ._session_options["binds"]. After the first app is registered, the sessionmakers for subsequent apps registered on the same instance will use the binds created for the first app, because they've been assigned to ._session_options.

Here's a script demonstrating this:

# /// script
# requires-python = ">=3.14"
# dependencies = [
#     "flask-sqlalchemy-lite",
# ]
# ///
import sqlalchemy.orm as orm
from flask import Flask
from flask_sqlalchemy_lite import SQLAlchemy


class DefaultBase(orm.DeclarativeBase):
    pass


class AltBase(orm.DeclarativeBase):
    pass


db = SQLAlchemy(session_options={"binds": {DefaultBase: "default", AltBase: "alt"}})


def create_app() -> Flask:
    app = Flask(__name__)
    app.config.update({
        "SQLALCHEMY_ENGINES": {
            "default": {"url": "sqlite://", "connect_args": {"autocommit": False}},
            "alt": {"url": "sqlite://", "connect_args": {"autocommit": False}},
        }
    })
    db.init_app(app)
    return app


if __name__ == "__main__":
    assert db._session_options["binds"] == {DefaultBase: "default", AltBase: "alt"}
    app1 = create_app()
    with app1.app_context():
        assert db._session_options["binds"] == {
            DefaultBase: db.engines["default"],
            AltBase: db.engines["alt"],
        }
    app2 = create_app()

    state1 = db._app_state[app1]
    state2 = db._app_state[app2]

    assert state2.sessionmaker.kw["binds"][DefaultBase] is state1.engines["default"]
    assert state2.sessionmaker.kw["binds"][AltBase] is state1.engines["alt"]
    assert (
        set(state2.sessionmaker.kw["binds"].values()) & set(state2.engines.values())
        == set()
    )

This only became an issue because of some niche (some would say "poor") design in my own project, but it still seems like unintended behavior.

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