feat: add .exclude() method for exclusion projection on find queries - #1352
Open
smahn9123 wants to merge 1 commit into
Open
feat: add .exclude() method for exclusion projection on find queries#1352smahn9123 wants to merge 1 commit into
smahn9123 wants to merge 1 commit into
Conversation
Add .exclude(*fields) to FindMany and FindOne, so a query can drop a few fields without defining a projection model listing every other one. - Accepts Python names, MongoDB aliases, ExpressionField refs and dotted paths into embedded models; repeated calls accumulate. - Results are parsed with a cached subclass where excluded fields become Optional=None. Every inherited field is re-declared explicitly, since init_beanie installs ExpressionField class attributes that Pydantic would otherwise adopt as field defaults. - The exclusion is applied inside parse_obj, after union / inheritance dispatch, so with_children and UnionDoc queries relax the concrete class each document belongs to. - Uses $unset in aggregation pipelines, a plain projection otherwise. - Rejects combinations that cannot work: project() with a different model, aggregate(), and nested paths that cannot be rebuilt. - Whole-document writes raise DocumentWasPartiallyLoaded instead of overwriting excluded values with None; save_changes() and set() still work, as they only touch named fields.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Beanie's
projection_modelonly supports inclusion — you define a model listing every field you want. Sometimes you just want to skip a couple of fields (a large blob, sensitive data) without spelling out all the others. MongoDB supports this natively with{field: 0}:Inheritance (
is_root = True) andUnionDocare supported: the exclusion is applied to the concrete class each document turns out to belong to, sowith_children=Truestill returns properly typed children.Related: #163
Supersedes #1322
Write protection
Excluded fields are
Noneon the returned instance, so writing the whole document back would overwrite the stored values.save(),replace()andinsert()therefore raiseDocumentWasPartiallyLoaded(new inbeanie.exceptions), whilesave_changes()andset()still work since they only touch named fields. The guard fires only on instances produced by an exclusion projection, so no existing behaviour changes.Restrictions
Combinations that cannot work raise rather than silently misbehaving:
.project()with a model other than the document model,.aggregate()(and thesum/avg/min/maxhelpers), and nested paths that cannot be rebuilt — the last from.exclude()itself, before the query runs.Implementation notes
Results are parsed with a cached subclass where the excluded fields become
Optional = None; it still passesisinstancechecks. Two details are worth flagging for review:get_exclusion_model()re-declares every inherited field, not just the excluded ones. This looks redundant but is load-bearing:init_beanieinstallsExpressionFieldclass attributes on document models, and Pydantic adopts those as field defaults in a subclass unless the field is re-declared. Without it every field defaults to the string of its own name —revision_idbecomes"revision_id", which lands on every excluded document and is written back on the next save.parse_obj, after union / inheritance dispatch. Applying it earlier would relax the class the query was issued on, and the_class_iddispatch would then hand the document to the original child class, which still requires the excluded field.fetch_links=Trueruns through an aggregation, where exclusion uses$unsetrather than$project, which is unreliable for non-_idfields on some MongoDB versions. Field names are normalised to Python paths at.exclude()call time and converted to MongoDB names only when the query is built.Tests
18 tests covering query building, parsing, inheritance/
UnionDoc, nested paths, caching,lazy_parse, the restrictions and the write guard. The cache-key andlazy_parsetests were checked against deliberately reintroduced regressions to confirm they fail when the behaviour breaks.Full suite: 596 passed, verified against MongoDB 8.0 with a replica set.
ruff,ruff-formatandmypyreport nothing new versusmain.