BetterCRUD Changelog¶
Introduction¶
The Changelog documents all notable changes made to BetterCRUD. This includes new features, bug fixes, and improvements. It's organized by version and date, providing a clear history of the library's development.
[0.2.6] - Oct 11, 2026¶
improvements
- Fix the license field:
_version.pydeclared "Apache License 2.0" while the LICENSE file, the classifiers and the repository were MIT, so PyPI published an incorrect license - Improve the PyPI summary to lead with "FastAPI CRUD" and expand keywords with crudrouter, fastapi-crud, sqlmodel and rest so the package is discoverable from the terms people actually search for
- Add classifiers for Application Frameworks, WWW/HTTP and AsyncIO, plus Changelog / Issue Tracker project URLs
- Add the missing
py.typedmarker: theTyping :: Typedclassifier was declared but inline types were not visible to mypy or pyright - Configure
site_urlandsite_descriptionfor the docs: the published sitemap.xml was empty and pages had no canonical URL - Expand the test matrix from Python 3.9 only to 3.9-3.12 so CI covers the Python versions the package declares
[0.2.5] - Aug 18, 2026¶
feature
- Add query.filters for declaring typed, OpenAPI-visible query parameters on get_many that automatically participate in WHERE (supports base fields and relationship fields such as company.name)
- Validate query.filters at route registration: reject names colliding with reserved get_many parameters, and require relationship fields to be declared in query.joins with join=True
[0.2.4] - Aug 18, 2026¶
fixed issues
- Use an explicit crud_action response_model as the final response shape as-is (passing ResponseModel[CurrentUser] no longer gets wrapped into ResponseModel[ResponseModel[CurrentUser]])
[0.2.3] - Aug 17, 2026¶
feature
- Infer crud_action response model from the return type annotation when response_schema is configured (instead of always falling back to serialize.base)
- Skip response schema wrapping when the endpoint already returns a response schema instance (no more double-wrapped payloads)
- Preserve Optional[X] / X | None return annotations so None responses stay valid
- Fix route registration crashing with NameError/TypeError when a crud_action return annotation cannot be resolved at import time (e.g.
from __future__ import annotationsreferencing a non-global model); falls back to the bare response_schema (data unconstrained) and emits a warning
[0.2.2] - Aug 09, 2026¶
feature
- Add crud_action decorator for custom endpoints
- Make bulk create/update operations atomic (single transaction with rollback)
- Add soft-delete recover route (PATCH /{id}/recover, opt-in via allow_recover)
[0.2.1] - Aug 09, 2026¶
feature
- Support optional pagination mode for get_many (pagination_mode: always/optional/disabled)
[0.2.0] - Nov 10, 2025¶
fixed issues
- $notany operator support empty
[0.1.8] - Jun 16, 2025¶
fixed issues
- Add parms parameters only for crud routes
[0.1.7] - Jun 12, 2025¶
feature
- Expose registered crud routes
[0.1.6] - Jun 8, 2025¶
fixed issues
- BetterCrudGlobalConfig DEFAULT_ACTION_MAP value should be str,should not be an enumeration value
[0.1.5] - May 31, 2025¶
- Support many-to-many relationships in object mode
You can post zone data by passing zone_id client.post("/user", json={**user_data,"zone":zone_id})
class Zone(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: Optional[str] = None description: Optional[str] = Field(default=None) is_active: Optional[bool] = Field(default=True) class UserZoneLink(SQLModel, table=True): __tablename__ = "user_zone" user_id: Optional[int] = Field( default=None, foreign_key="user.id", primary_key=True ) zone_id: Optional[int] = Field( default=None, foreign_key="zone.id", primary_key=True ) class User(UserBase, table=True): ... zone: Optional[Zone] = Relationship( sa_relationship_kwargs={ "lazy": "noload", "uselist": False}, link_model=UserZoneLink ) ... class UserCreate(UserBase): ... zone:Optional[int] = None
[0.1.4] - Apr 21, 2025¶
- RouteOptions add custom action
[0.1.3] - Apr 6, 2025¶
- Add more filter operators
- $startsL (starts with,not case sensitive )
- $endsL (ends with,not case sensitive )
- $contL (contains,not case sensitive )
- $exclL (not contains,not case sensitive )
- $eqL (=,not case sensitive )
- $neL (!=,not case sensitive )
- $inL (IN, in range, accepts multiple values separated by commas,not case sensitive )
- $notinL (NOT IN, not in range, accepts multiple values separated by commas,not case sensitive )
[0.1.2] - Mar 30, 2025¶
- Some design refactoring
- Remove the crud_get_many parameters criteria, options, joins
[0.1.1] - Mar 26, 2025¶
Features¶
- Add some parameters to crud_get_many method
- criterions embedded query criteria
- options load options
- joins relationship joins
joins = [(UserProfile, UserProfile.id == User.profile_id)] options = [joinedload(User.profile), joinedload(User.roles)] criterions = [UserProfile.gender == "female"] fetched_records = await user_service.crud_get_many( test_request, joins=joins, criterions=criterions, options=options, db_session=async_session )
[0.1.0] - Mar 25, 2025¶
Added¶
- Keep the s and filter query conditions not mutually exclusive and can be run in parallel
[0.0.9] - Mar 25, 2025¶
Added¶
- query filter support lambda function,it can realize the purpose of query condition transformation
[0.0.8] - Mar 24, 2025¶
Fixed¶
- routes options only set to empty list does not take effect
[0.0.7] - Mar 11, 2025¶
Fixed¶
- Support model alias in nested relationship
from sqlalchemy.orm import aliased
ModifierUser = aliased(User)
query={
"joins": {
"user": {
"join": True,
"select": True
},
"user.modifier": {
"join": True,
"alias": ModifierUser
}
}
}
[0.0.6] - Feb 16, 2025¶
Fixed¶
- Sorting can't use the Related Table alias field
- Under certain conditions, queries using alias tables are invalid
[0.0.5] - Feb 12, 2025¶
Added¶
- Add model alias to query,avoid Not unique table/alias errors
Usage Example¶
from sqlalchemy.orm import aliased
ModifierUser = aliased(User)
class Post(PostBase, table=True):
__tablename__ = "post"
id: Optional[int] = Field(default=None, primary_key=True)
creater_user_id: Optional[int] = Field(
default=None, foreign_key="user.id"
)
modifier_user_id: Optional[int] = Field(
default=None, foreign_key="user.id"
)
creater_user: User = Relationship(
sa_relationship_kwargs={"uselist": False, "lazy": "noload", "foreign_keys": "[Post.creater_user_id]"})
modifier_user: User = Relationship(
sa_relationship_kwargs={"uselist": False, "lazy": "noload", "foreign_keys": "[Post.modifier_user_id]"})
@crud(
router,
dto={"create": PostCreate, "update": PostUpdate},
serialize={"base": PostPublic},
query={
"joins": {
"creater_user": {
"select": True,
"join": True
},
"modifier_user": {
"select": True,
"join": True,
"alias": ModifierUser
}
},
}
)
class PostController():
service: PostService = Depends(PostService)