Skip to content

Complete migration away from legacy SQLAlchemy Query API#156

Merged
winwinashwin merged 3 commits intomainfrom
ashwin/migrate-query
Jan 7, 2026
Merged

Complete migration away from legacy SQLAlchemy Query API#156
winwinashwin merged 3 commits intomainfrom
ashwin/migrate-query

Conversation

@winwinashwin
Copy link
Contributor

@winwinashwin winwinashwin commented Dec 24, 2025

This PR removes all remaining references to the legacy SQLAlchemy Query API and standardizes on 2.0-style select()-based querying across code, tests, and documentation.

Majority of the query patterns are based off of SQLAlchemy migration guide.

This will also help us long term if we need to add support for async sessions as async extension cannot work with the Query object and related constructs.

Summary of changes

  • Replaced legacy session.query(...) usage in tests and documentation with select() + Session helpers
  • Refactored internal code to consistently use SQLAlchemy 2.0 ORM querying patterns
  • Added support for dynamic (backward compatibility) and write_only loading strategies during query construction

Fixes: #155

See: https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#orm-query-unified-with-core-select

@winwinashwin
Copy link
Contributor Author

winwinashwin commented Dec 25, 2025

@ashish16052 The versions attribute on the versioned models are using the dynamic loading strategy which is now legacy. If we change this to write_only as sqlalchemy recommends it will change our public API.

How should we go about this?

Related thread in sqlalchemy-continuum - sqlalchemy-continuum/sqlalchemy-continuum#332

@ashish16052
Copy link
Collaborator

@ashish16052 The versions attribute on the versioned models are using the dynamic loading strategy which is now legacy. If we change this to write_only as sqlalchemy recommends it will change our public API.

How should we go about this?

Related thread in sqlalchemy-continuum - sqlalchemy-continuum/sqlalchemy-continuum#332

one way to go about this is making lazy strategy configurable.
(maybe adding a lazy_versions_strategy option to VersioningManager?)

and add a deprecation warning.

this will:
give enough time to current users to migrate.
new users can start using write_only immediately.

Move away from the query API (which is deemed legacy in 2.x) and use the
newer calling form. This will also help us in the long run to support
async sessions as async sessions do not work with the Query API.

See: https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-orm-usage
See: sqlalchemy/sqlalchemy#7971 (comment)
In sqla 2.0, lazy='dynamic' is superseded by lazy='write_only' strategy.
The relationship builder did not handle write_only strategy correctly
and these relationships in the version objects were eagerly loaded from
the DB.

1. Implement a minimal adapter that quacks like
   sa.orm.writeonly.WriteOnlyCollection and expose a select(...) method.
2. Refactor the builder to use the sqla select clauses instead of legacy
   Query API
3. Maintain backward compatibility for lazy=dynamic as it still depends
   on the Query object internally in sqlalchemy. Add a deprecation
   warning for users to migrate to write_only from dynamic
4. Add test cases for write_only relationships
code changes by cline, manually reviewed.
@winwinashwin
Copy link
Contributor Author

Let's keep the lazy strategy for versions to be dynamic for now - I'll tackle it in a separate PR.
This PR already has a refactor + feat.

if self.property.uselist is False:
return query.first()
return query.all()
return session.scalars(query.limit(1)).first()
Copy link
Collaborator

Choose a reason for hiding this comment

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

not clear on why was this limit required?

Copy link
Collaborator

Choose a reason for hiding this comment

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

session.scalars(query).first() would not work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I referred to the migration guide.

The query object used to apply a limit on the statement internally which the new form doesn't do.
There will be a performance regression in the newer form if it is not explicitly applied as all the rows will be returned from the DB but will be discarded in ORM layer.

Refer to the note for more details

@winwinashwin winwinashwin merged commit 2487d39 into main Jan 7, 2026
12 checks passed
@winwinashwin winwinashwin deleted the ashwin/migrate-query branch January 7, 2026 07:01
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.

Support write_only loader strategy in SQLAlchemy 2.0

2 participants