Problem
AutoCRUD uses a single loading strategy for two different jobs:
- Read-for-serialization —
get / get_if_exist / get_multi feeding a response schema, which wants every relationship the schema touches eager-loaded.
- Existence checks inside mutations —
create / update / delete flows (and downstream apps' composed operations) that only need the row, and actively don't want relationship collections cached mid-mutation.
Because both go through the same loader, downstream apps end up fighting session-identity-map semantics. Concrete case: teban's crud_resources had to deliberately keep user.roles lazy (documented in its _resource_select docstring) because get_if_exist is also called mid-create before role assignment — which produced the recurring /api/v1/resources/ N+1 family (teban Sentry TEBAN-3/5/6/8/C/E) and a hand-rolled _refresh_resource_collections workaround.
The 1.11 line (perf/rbac-query-loading) mitigates the worst of it (select_options ClassVar applied on read paths, selectin RBAC relationships, expire-on-association-mutation), but the structural conflation remains.
Proposal (v2)
Split the two concerns in the AutoCRUD surface:
- Declared read loader — first-class per-CRUD (or per-operation) loader options for serialization reads, e.g. evolving
select_options into read_options with the ability to differ between get and get_multi.
- Lightweight existence check — an internal
_exists(entity_id, context) (or get_for_update) that base mutations and app-level composed mutations use instead of the full read loader: no eager loads, no caching of collections that the ongoing mutation is about to invalidate.
- Base mutations (
update, delete, create_if_not_exist) switch to the lightweight check; read endpoints keep the declared loader.
Relationship to feature/user-model-extensibility
That branch already reshapes the v2 CRUD surface (kernel-resolved user model, generic CRUDUsers with explicit model, router factories). The read/mutation split should ride the same major, and wants to be designed against that surface, not against 1.x:
read_options must be declarable for kernel-resolved models (e.g. a custom user model's relationships), so the option type likely needs to be lazy/callable rather than a class-level tuple evaluated at import time.
CRUDUsers' generic-model rework is the natural place to define how per-model loader declarations compose with subclassing.
Acceptance sketch
- Mutations never populate relationship collections as a side effect of their existence checks.
- Read paths load exactly what the declared read loader specifies; apps can assert this with
kwik.testing.assert_max_queries.
- teban can delete its
_refresh_resource_collections / populate_existing workarounds entirely.
Problem
AutoCRUDuses a single loading strategy for two different jobs:get/get_if_exist/get_multifeeding a response schema, which wants every relationship the schema touches eager-loaded.create/update/deleteflows (and downstream apps' composed operations) that only need the row, and actively don't want relationship collections cached mid-mutation.Because both go through the same loader, downstream apps end up fighting session-identity-map semantics. Concrete case: teban's
crud_resourceshad to deliberately keepuser.roleslazy (documented in its_resource_selectdocstring) becauseget_if_existis also called mid-createbefore role assignment — which produced the recurring/api/v1/resources/N+1 family (teban Sentry TEBAN-3/5/6/8/C/E) and a hand-rolled_refresh_resource_collectionsworkaround.The 1.11 line (
perf/rbac-query-loading) mitigates the worst of it (select_optionsClassVar applied on read paths, selectin RBAC relationships, expire-on-association-mutation), but the structural conflation remains.Proposal (v2)
Split the two concerns in the
AutoCRUDsurface:select_optionsintoread_optionswith the ability to differ betweengetandget_multi._exists(entity_id, context)(orget_for_update) that base mutations and app-level composed mutations use instead of the full read loader: no eager loads, no caching of collections that the ongoing mutation is about to invalidate.update,delete,create_if_not_exist) switch to the lightweight check; read endpoints keep the declared loader.Relationship to
feature/user-model-extensibilityThat branch already reshapes the v2 CRUD surface (kernel-resolved user model, generic
CRUDUserswith explicit model, router factories). The read/mutation split should ride the same major, and wants to be designed against that surface, not against 1.x:read_optionsmust be declarable for kernel-resolved models (e.g. a custom user model's relationships), so the option type likely needs to be lazy/callable rather than a class-level tuple evaluated at import time.CRUDUsers' generic-model rework is the natural place to define how per-model loader declarations compose with subclassing.Acceptance sketch
kwik.testing.assert_max_queries._refresh_resource_collections/ populate_existing workarounds entirely.