sqlarec
sqlarec adds a context-aware Active Record API to synchronous SQLAlchemy 2.
It gives models concise query and persistence methods without requiring every
service function to receive and forward a Session.
user = User.query.where(User.email == "hamza@example.com").one_or_none()
users = User.query.order_by(User.name).all()
user = User.create(name="Hamza", email="hamza@example.com")
Your application still creates the session and controls commits, rollbacks, and
cleanup. sqlarec does not depend on a web framework and never commits inside a
model method.
Install
Install the published package from PyPI:
uv add sqlarec
You can also install it with pip:
python -m pip install sqlarec
sqlarec requires Python 3.11 or later, SQLAlchemy 2, and a synchronous
SQLAlchemy Session.
Quickstart
Define models with standard SQLAlchemy mapped columns:
from sqlalchemy import Boolean, String
from sqlalchemy.orm import Mapped, mapped_column
from sqlarec import BaseModel, init_engine, new_session
class User(BaseModel):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
email: Mapped[str] = mapped_column(String(255), unique=True)
active: Mapped[bool] = mapped_column(Boolean, default=True)
engine = init_engine("sqlite:///:memory:")
BaseModel.metadata.create_all(engine)
session = new_session()
BaseModel.register_session_provider(lambda: session)
User.create(name="Hamza", email="hamza@example.com")
session.commit()
user = User.query.one()
print(user.email)
Expected output:
hamza@example.com
BaseModel inherits from SQLAlchemy's DeclarativeBase, so relationships,
constraints, indexes, and mapper configuration continue to use normal SQLAlchemy
APIs.
Keep session handling at the application boundary
Regular SQLAlchemy often requires passing a session through each application layer:
from sqlalchemy import select
from sqlalchemy.orm import Session
def find_user(session: Session, email: str) -> User | None:
return session.scalars(select(User).where(User.email == email)).one_or_none()
With sqlarec, your application registers a zero-argument provider once. Models
resolve the current session when an operation executes:
from contextvars import ContextVar
from sqlalchemy.orm import Session
from sqlarec import BaseModel
current_session = ContextVar[Session]("current_session")
BaseModel.register_session_provider(current_session.get)
def find_user(email: str) -> User | None:
return User.query.where(User.email == email).one_or_none()
The provider can read from application state, a dependency scope, or a
ContextVar. This allows middleware to bind one session to the current request:
from sqlarec import new_session
def database_middleware(request, handler):
session = new_session()
token = current_session.set(session)
try:
response = handler(request)
session.commit()
return response
except Exception:
session.rollback()
raise
finally:
current_session.reset(token)
session.close()
Handlers and services inside that middleware can use User.query,
User.create(), or User.session without receiving a session argument.
Transaction ownership remains explicit at the middleware boundary.
Query models and rows
Model.query and Model.select() return immutable ModelQuery wrappers:
users = User.query.all()
user = User.query.where(User.email == "hamza@example.com").one_or_none()
active = User.query.filter_by(active=True).order_by(User.name).limit(20).all()
Pass individual columns to Model.select() to receive SQLAlchemy rows:
rows = User.select(User.id, User.email).order_by(User.id).all()
mappings = User.select(User.id, User.email).mappings().all()
User.query.all() -> Sequence[User]
User.select().all() -> Sequence[User]
User.select(User.id, User.email).all() -> Sequence[Row]
Query builders include where(), filter_by(), order_by(), group_by(),
having(), join(), outerjoin(), limit(), offset(), distinct(),
options(), union(), and union_all().
Write without hidden commits
Create, update, and delete operations flush the current session but do not commit:
user = User.create(name="Hamza", email="hamza@example.com")
user.name = "Hamza S."
user.save()
User.update().where(User.active.is_(False)).values(active=True).execute()
user.delete()
session.commit()
Keeping commits outside model methods lets your application commit or roll back the complete unit of work atomically.
Single primary keys support direct lookup:
user = User.get_by_pk(42)
exists = User.exists(42)
String primary keys without a Python or database default receive a generated UUID hex value. Composite primary-key lookup accepts a tuple in mapper-defined key order.
Use SQLAlchemy directly when needed
Wrappers expose their underlying SQLAlchemy statement:
query = User.query.where(User.active.is_(True))
statement = query.statement
The resolved session is also available on the model:
result = User.session.execute(custom_statement)
sqlarec is an ergonomic layer, not a replacement for SQLAlchemy.
Develop the library
Clone the repository, then install the package and development tools:
uv sync
| Command | Purpose |
|---|---|
make install |
Install runtime and development dependencies. |
make install-prod |
Install runtime dependencies only. |
make test |
Run pytest. |
make lint |
Check source and tests with Ruff. |
make typecheck |
Check package types with mypy. |
make format |
Format source and tests with Ruff. |
make clean |
Remove Python, pytest, and Ruff caches. |
Current limitations
- You must register a session provider before model operations.
- Only synchronous SQLAlchemy sessions are supported.
- Query wrappers cover common operations; use the underlying statement for advanced SQLAlchemy features.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sqlarec-0.1.2.tar.gz.
File metadata
- Download URL: sqlarec-0.1.2.tar.gz
- Upload date:
- Size: 39.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9dc98009e353bc8911585dce6ee50ca61c7db0433852594a1755519fa14ed45e
|
|
| MD5 |
36279e149365ad2c211971f819f28066
|
|
| BLAKE2b-256 |
9d35a97a8a5ac0fb1d06205956683f8b13b566674ae50770df6e87ea7af80f34
|
File details
Details for the file sqlarec-0.1.2-py3-none-any.whl.
File metadata
- Download URL: sqlarec-0.1.2-py3-none-any.whl
- Upload date:
- Size: 11.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
03dbe7f86bbb53d641265f7beec28887e11fdfef16db19d0a853a88247e526b1
|
|
| MD5 |
899351a8f0f311e17660425814127c55
|
|
| BLAKE2b-256 |
2b2aeeb8b69327a56fda5ffd3248ad1a216eeb2caf6eda4dae15024279848e7b
|