Skip to content

fix: migrate to SQLModel session.exec() and gate regressions (#890)#914

Merged
PythonFZ merged 24 commits intomainfrom
worktree-fix+sqlmodel-session-exec-migration
Apr 9, 2026
Merged

fix: migrate to SQLModel session.exec() and gate regressions (#890)#914
PythonFZ merged 24 commits intomainfrom
worktree-fix+sqlmodel-session-exec-migration

Conversation

@PythonFZ
Copy link
Copy Markdown
Member

@PythonFZ PythonFZ commented Apr 9, 2026

Summary

  • Migrate all 77 session.execute() call sites across 17 files in src/ to SQLModel's session.exec() API — the SOTA recommendation per sqlmodel.tiangolo.com
  • Eliminates 15,000+ DeprecationWarning instances per test run that were drowning out real warnings in CI
  • Adds a pyproject.toml filterwarnings gate that escalates any future session.execute() reintroduction (on a SQLModel session) to a hard CI failure
  • Updates AsyncSession imports from sqlalchemy.ext.asyncio to sqlmodel.ext.asyncio.session in files that use .exec(), so type annotations match the runtime session type
  • Updates test fixture session factories to create SQLModel sessions (matching production), and migrates test-level session.execute() calls accordingly

Closes #890.

Approach

  • Per-file atomic commits so bisection is trivial if a regression slips through
  • No behavioral changes. session.exec() returns the same data via .one() / .one_or_none() / .all() instead of .scalar_one() / .scalar_one_or_none() / .scalars().all()
  • The one bulk update(Task).where(...).values(...) call in router.py (optimistic-locking task-claim path) uses SQLModel's UpdateBase overload on exec(), which returns a fully-typed CursorResult with .rowcount
  • test_resilience.py is intentionally untouched — it creates its own sqlalchemy sessions for raw SQL manipulation, which don't go through SQLModel's wrapper

Test plan

  • uv run pytest tests/zndraw_joblib/ tests/zndraw_auth/ — 358 passed
  • uv run pytest tests/zndraw/ -m "not integration" — 1072 passed, 1 skipped
  • grep -rn "session\.execute(" src/ --include="*.py" — returns empty
  • grep -rn "\.scalars(" src/ --include="*.py" — returns empty
  • The filterwarnings gate is in effect (any session.execute() on a SQLModel session now fails the suite)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Documentation

    • Added migration plan and design specification for SQLModel query API updates across the codebase.
  • Chores

    • Migrated database query execution methods across the application to resolve deprecation warnings and improve ORM compatibility.
    • Updated test configuration to enforce compliance with updated database query patterns.

PythonFZ and others added 24 commits April 8, 2026 18:40
Migration plan for all 77 session.execute() call sites across src/
(zndraw, zndraw_auth, zndraw_joblib) to the SOTA session.exec() API,
plus a pytest filterwarnings gate to prevent regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add the 25 test call sites (4 files) to the spec — with filterwarnings
set to error, unmigrated test files would fail the suite too. No
@pytest.mark.protected tests in the affected files, so no carve-outs
needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Empirical verification shows test fixtures use pure sqlalchemy
AsyncSession, so direct session.execute() calls in tests don't emit the
SQLModel deprecation warning and aren't blocked by the filterwarnings
gate. The 15k warnings observed come entirely from integration tests
hitting src/ code via the production uvicorn server (which configures
class_=SQLModelAsyncSession). Migrating src/ alone eliminates all of
them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-file atomic task breakdown for the 77 src/ call sites, plus the
pyproject.toml filterwarnings gate. Each task is self-contained with
exact line numbers, before/after code, test commands, and commit
command.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test session factories now create SQLModel sessions (matching production)
so the migrated src/ code's session.exec() calls work in tests. Also
migrates direct session.execute() calls in test_sweeper and
test_registry to session.exec() since those sessions are now SQLModel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

A repository-wide migration replaces SQLModel async session execute() calls with exec() across 17 source files and test files. Result API calls are updated correspondingly (e.g., .scalar_one*().one*()), import sources for select and AsyncSession are changed to sqlmodel, and a pytest filterwarnings regression gate is added to prevent future deprecation usage.

Changes

Cohort / File(s) Summary
Documentation & Specifications
docs/superpowers/plans/2026-04-08-sqlmodel-session-exec-migration.md, docs/superpowers/specs/2026-04-08-sqlmodel-session-exec-migration-design.md
New migration plan and design documents detailing deterministic rewrite rules (77 call sites across 17 files), result API mappings, import adjustments, and regression-gate CI configuration via pytest filterwarnings.
Configuration
pyproject.toml
Added filterwarnings under [tool.pytest.ini_options] to convert .session.execute() deprecation warnings to test errors and ignore unrelated fastapi_users warnings.
Core Database
src/zndraw/database.py
Migrated ensure_internal_worker to use session.exec(), changed scalar_one_or_none() to one_or_none(), and updated AsyncSession import source to sqlmodel.ext.asyncio.session.
Route Handlers: Query Execution
src/zndraw/routes/admin.py, src/zndraw/routes/bookmarks.py, src/zndraw/routes/chat.py, src/zndraw/routes/figures.py, src/zndraw/routes/geometries.py, src/zndraw/routes/presets.py, src/zndraw/routes/rooms.py, src/zndraw/routes/screenshots.py, src/zndraw/routes/selection_groups.py
Replaced session.execute() with session.exec(), changed .scalars().all() to .all(), and updated scalar result extraction methods (.scalar_one*().one*()).
Route Handlers: Select Import Migration
src/zndraw/routes/frames.py
Migrated from sqlalchemy.select to sqlmodel.select, updated session.execute() to session.exec(), changed result handling from scalar_one_or_none() to one_or_none(), and updated AsyncSession import.
Auth Modules
src/zndraw_auth/cli_login.py, src/zndraw_auth/db.py
Updated session execution and result extraction methods across three CLI login endpoints and ensure_default_admin. Adjusted imports to use sqlmodel.select and sqlmodel.ext.asyncio.session.AsyncSession.
Joblib Components
src/zndraw_joblib/dependencies.py, src/zndraw_joblib/registry.py, src/zndraw_joblib/router.py, src/zndraw_joblib/sweeper.py
Systematically migrated all async session queries from execute() to exec(), updated result API calls throughout job/task/worker/provider lookups, and adjusted imports to use sqlmodel equivalents. router.py affected most (82 lines).
Test Fixtures
tests/zndraw/conftest.py, tests/zndraw_auth/conftest.py, tests/zndraw_joblib/conftest.py
Updated AsyncSession imports to source from sqlmodel.ext.asyncio.session, and adjusted session factory class specifications. One test route changed to use session.connection() instead of session.execute().
Test Implementations
tests/zndraw_joblib/test_registry.py, tests/zndraw_joblib/test_sweeper.py
Updated test query execution and result extraction to match migration pattern: session.exec() and .one()/.one_or_none()/.all() for assertion verification across job, task, and worker cleanup checks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hop, hop! The queries now exec() with pride,
No more .scalars() chains left far and wide,
.one() and .all() march in graceful line,
SQLModel's warnings fade—migration divine! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: migrate to SQLModel session.exec() and gate regressions (#890)' accurately and specifically describes the main change: migrating from session.execute() to session.exec() and adding a regression gate.
Linked Issues check ✅ Passed The PR fully addresses issue #890 by replacing 77 occurrences of session.execute() with session.exec() across 17 files, updating AsyncSession imports, adding a filterwarnings regression gate, and updating test fixtures—comprehensively eliminating the 15,000+ DeprecationWarning instances reported.
Out of Scope Changes check ✅ Passed All changes directly support the stated objectives: migration from session.execute() to session.exec(), AsyncSession import updates, filterwarnings regression gate, and test fixture alignment. No unrelated or extraneous changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 96.55% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix+sqlmodel-session-exec-migration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
docs/superpowers/plans/2026-04-08-sqlmodel-session-exec-migration.md (1)

182-204: Minor: Add language specifier to fenced code block.

The expected output block at line 184 is missing a language specifier, which triggers a markdownlint warning.

📝 Suggested fix
 Expected output (17 files, summing to 77):
-```
+```text
 src/zndraw/database.py:1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-08-sqlmodel-session-exec-migration.md` around
lines 182 - 204, The fenced code block showing the "Expected output (17 files,
summing to 77):" is missing a language specifier which causes a markdownlint
warning; update the opening fence for that block to use a language (e.g., change
``` to ```text) so the block becomes ```text and leave the contents
unchanged—locate the fenced block labeled "Expected output (17 files, summing to
77):" and modify its opening fence accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/superpowers/specs/2026-04-08-sqlmodel-session-exec-migration-design.md`:
- Line 152: The regex in the pattern string 'error:[\s\S]*You probably want to
use .session\.exec:DeprecationWarning' uses an unescaped dot before "session"
which matches any character; update that literal pattern to escape the period so
it reads 'error:[\s\S]*You probably want to use
\.session\.exec:DeprecationWarning' (modify the string containing the regex in
the docs/spec where the pattern is defined).

---

Nitpick comments:
In `@docs/superpowers/plans/2026-04-08-sqlmodel-session-exec-migration.md`:
- Around line 182-204: The fenced code block showing the "Expected output (17
files, summing to 77):" is missing a language specifier which causes a
markdownlint warning; update the opening fence for that block to use a language
(e.g., change ``` to ```text) so the block becomes ```text and leave the
contents unchanged—locate the fenced block labeled "Expected output (17 files,
summing to 77):" and modify its opening fence accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 14492ab8-a13a-471b-82e9-79ca2dbb8318

📥 Commits

Reviewing files that changed from the base of the PR and between 6e34a37 and 574666d.

📒 Files selected for processing (25)
  • docs/superpowers/plans/2026-04-08-sqlmodel-session-exec-migration.md
  • docs/superpowers/specs/2026-04-08-sqlmodel-session-exec-migration-design.md
  • pyproject.toml
  • src/zndraw/database.py
  • src/zndraw/routes/admin.py
  • src/zndraw/routes/bookmarks.py
  • src/zndraw/routes/chat.py
  • src/zndraw/routes/figures.py
  • src/zndraw/routes/frames.py
  • src/zndraw/routes/geometries.py
  • src/zndraw/routes/presets.py
  • src/zndraw/routes/rooms.py
  • src/zndraw/routes/screenshots.py
  • src/zndraw/routes/selection_groups.py
  • src/zndraw_auth/cli_login.py
  • src/zndraw_auth/db.py
  • src/zndraw_joblib/dependencies.py
  • src/zndraw_joblib/registry.py
  • src/zndraw_joblib/router.py
  • src/zndraw_joblib/sweeper.py
  • tests/zndraw/conftest.py
  • tests/zndraw_auth/conftest.py
  • tests/zndraw_joblib/conftest.py
  • tests/zndraw_joblib/test_registry.py
  • tests/zndraw_joblib/test_sweeper.py


```toml
filterwarnings = [
'error:[\s\S]*You probably want to use .session\.exec:DeprecationWarning',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the exact deprecation message from SQLModel source

# Find the deprecation message in the SQLModel package
python -c "
import sqlmodel.ext.asyncio.session as sess
import inspect
import ast

# Get the source code of the AsyncSession class
source = inspect.getsource(sess.AsyncSession)
print('Searching for deprecated decorator and message...')
print(source[:2000])  # Print first part to see the decorator
" | grep -A 5 -B 5 "deprecated\|session.exec"

Repository: zincware/ZnDraw

Length of output: 180


🏁 Script executed:

cat -n docs/superpowers/specs/2026-04-08-sqlmodel-session-exec-migration-design.md | sed -n '145,165p'

Repository: zincware/ZnDraw

Length of output: 1356


🌐 Web query:

SQLModel AsyncSession session.exec deprecation warning message

💡 Result:

session.exec(...) is not deprecated in SQLModel. The SQLModel deprecation warning is for session.execute(...), and it tells you to switch to session.exec(...). In the SQLModel source, the deprecation text starts with: “You probably want to use session.exec() instead of session.execute() …” and explains that execute() returns Row objects and you’d typically need .scalars() to get model objects. [1][2]

What to do instead

  • Sync:

    heroes = session.exec(select(Hero)).all()

    (instead of session.execute(...).scalars().all()) [1]

  • Async:

    result = await session.exec(select(Hero))
    heroes = result.all()

    (instead of await session.execute(...) + .scalars()) [2]

If you’re seeing a warning that specifically mentions exec being deprecated, it’s likely coming from a different library or from importing the wrong AsyncSession (e.g., SQLAlchemy’s instead of SQLModel’s). The SQLModel release notes also mention adjustments to the session.execute() docstring/deprecation messaging. [3]

Sources:
[1] https://github.com/fastapi/sqlmodel/blob/530c2684/sqlmodel/orm/session.py
[2] https://github.com/fastapi/sqlmodel/blob/530c2684/sqlmodel/ext/asyncio/session.py
[3] https://sqlmodel.tiangolo.com/release-notes/


Escape the period before "session" in the regex pattern.

Line 152 has an unescaped period before session:

'error:[\s\S]*You probably want to use .session\.exec:DeprecationWarning',
                                          ^
                                     unescaped

In regex, an unescaped . matches any character, not a literal period. The SQLModel deprecation message contains "You probably want to use session.exec(...)", so the pattern should be:

'error:[\s\S]*You probably want to use \.session\.exec:DeprecationWarning',

This ensures the filter matches only the intended warning and prevents false positives from strings like "Xsession" or "asession".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-08-sqlmodel-session-exec-migration-design.md`
at line 152, The regex in the pattern string 'error:[\s\S]*You probably want to
use .session\.exec:DeprecationWarning' uses an unescaped dot before "session"
which matches any character; update that literal pattern to escape the period so
it reads 'error:[\s\S]*You probably want to use
\.session\.exec:DeprecationWarning' (modify the string containing the regex in
the docs/spec where the pattern is defined).

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 58.73786% with 85 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.77%. Comparing base (06cab0d) to head (574666d).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/zndraw_joblib/router.py 31.25% 55 Missing ⚠️
src/zndraw/routes/rooms.py 18.18% 9 Missing ⚠️
src/zndraw/routes/chat.py 20.00% 4 Missing ⚠️
src/zndraw/routes/presets.py 20.00% 4 Missing ⚠️
src/zndraw/routes/admin.py 25.00% 3 Missing ⚠️
src/zndraw_auth/cli_login.py 57.14% 3 Missing ⚠️
src/zndraw/routes/screenshots.py 33.33% 2 Missing ⚠️
src/zndraw/routes/bookmarks.py 50.00% 1 Missing ⚠️
src/zndraw/routes/figures.py 50.00% 1 Missing ⚠️
src/zndraw/routes/frames.py 75.00% 1 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #914      +/-   ##
==========================================
- Coverage   91.78%   91.77%   -0.01%     
==========================================
  Files         246      246              
  Lines       22881    22905      +24     
==========================================
+ Hits        21001    21022      +21     
- Misses       1880     1883       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@PythonFZ PythonFZ merged commit 48625eb into main Apr 9, 2026
6 checks passed
@PythonFZ PythonFZ deleted the worktree-fix+sqlmodel-session-exec-migration branch April 9, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

zndraw-joblib registry.py:108 uses session.execute() instead of session.exec() — 15,000+ deprecation warnings

2 participants