Skip to content

fix(#493, #494, #518)!: decide type reuse in a shared identity cache and keep only naming in the registry - #520

Open
gazorby wants to merge 15 commits into
feat/scope-schema-onlyfrom
refactor/type-identity-cache
Open

gazorby wants to merge 15 commits into
feat/scope-schema-onlyfrom
refactor/type-identity-cache

Conversation

@gazorby

@gazorby gazorby commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Problem

Two layers decided when two generated types are "the same", on different keys:

  • the DTO factory cache, per factory, keyed on model, purpose/partial/alias/type overrides and (roots only) the field selection, blind to the GraphQL name and to field arguments (paginate, order, distinct_on);
  • the registry, shared, keyed on GraphQL name, kind and field arguments, blind to the field selection, deduplicating every registration by that key.

Each covered the other's blind spot by accident. The registry folded same-named types whatever their fields (#487, #493: the first distinct_on list or the first narrowed aggregate filter decided the enum for everyone), the DTO cache handed nested types built for one root to every other root (#518: nested pagination/ordering arguments depended on declaration order), and schema scope was handled twice. It also hid #494: nested create payloads under update inputs took the update shape, or the create shape, depending on which root input was declared first.

Change

  • One identity cache (dto/identity.py, TypeIdentityCache), owned by the mapper and shared by every factory. It decides reuse:
    • by request key: factory kind, model, purpose/partial/alias/type overrides, defaults/sentinels, field selection, the declared-root marker, the requested name of generated roots, and for object types the field arguments of the root they are reached from;
    • by shape: a generated type with the same name and shape (fields, annotations, defaults, field options, description/directives) as an earlier one is that type. This is what lets differently configured requests that produce the same GraphQL type share it, and it covers the inputs built outside a factory (aggregate order-by and filter function inputs), which now register through GraphQLFactory._register_built.
    • It alone holds the scope="schema" stand-ins and raises ScopeConflictError.
  • The registry only names: a declaration owns its name, a generated type whose shape differs from the one holding a name takes its path name or a numbered one, references are retargeted when a declaration takes over or a schema scope is declared, and it calls strawberry.type. Its dedup map (_type_map), scoped-type store and per-info option fields are gone, and so are the DTO backends' copy.
  • fix: nested create inputs of update inputs take the update shape, and leak it into create inputs #494: the related input of a relation is the payload of create, so it is built in create mode under every root mode, and its back references to the root model point at a create input instead of the update root.
  • fix: nested relation field arguments depend on the order root types are declared #518: object types are keyed on the field arguments of their root, so each root's nested relations carry its own paginate/order/distinct_on arguments.
  • fix: generated DistinctOnFields and aggregate argument enums take the shape of the first field or filter that builds them #493: fields with different distinct_on lists get their own enums, and the shared {Model}CountFields and argument enums are built from the mapper-wide selection rather than the declaring aggregate filter's include.

Breaking changes and migration

  • Generated type names can change. Identical generated types are merged (FruitOrderBy2 becomes FruitOrderBy), generated types whose shape differs from the one holding a name get their own name (ColorDistinctOnFields1, ContainerType3FruitType), and the numbering of the remaining …N names shifts. Clients or persisted queries that spell generated names need updating; declare the type (or use scope="schema") when a client depends on its name.
  • Nested create payloads under pk_update_input / filter_update_input now have the create shape: non-nullable columns are required, generated keys optional, description "Create input", and their own relations use create wrappers (ToManyCreateInput, no remove). Payloads that omitted required columns must now send them.
  • Nested relations of a root declared with paginate/order/distinct_on carry those arguments, which adds root-prefixed nested types next to the plain ones.
  • Internals: StrawberryRegistry takes the mapper's TypeIdentityCache; register_type no longer takes paginate, order, distinct_on or default_pagination; RegistryTypeInfo loses the matching fields; DTOBackend.copy and its overrides are removed; no_cache only skips the schema-scope stand-in.

Tests

Closes #494
Closes #518
Closes #493

Stacked on #517

@gazorby gazorby changed the title refactor!: decide type reuse in a shared identity cache and keep only naming in the registry fix(#493, #494, #518)!: decide type reuse in a shared identity cache and keep only naming in the registry Oct 13, 2026
@gazorby gazorby added the fix label Oct 13, 2026
@greptile-apps

greptile-apps Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[High impact] Refactors type identity and caching across schema factories.

Fix the cached declaration path before merging so schema-scoped types remain reliable.

Findings

  1. P1 Schema scope is not saved ▶
  2. P2 Generated settings are ignored ▶
Fix with agent prompt
### Issue 1
src/strawchemy/dto/base.py:833
When an unscoped declaration already has the same fields, a later `scope="schema"` declaration takes this cached-build path and returns before `declare_scope` runs. The registry no longer stores scoped types, so relations declared afterward do not use that replacement. Another scoped declaration can also claim the same model and kind without raising `ScopeConflictError`.

Save the scoped declaration on the cached-build path too.

### Issue 2
src/strawchemy/dto/base.py:555
The request key omits `registration`, although shape matching includes it. Two generated `make_input` requests with the same model, name and config but different descriptions or directives therefore return the first registered type. This also happens with `no_cache=True`, which previously built a fresh type. The registry then skips the second request's settings.

Include `registration` in the request key so cached requests follow the same matching rules as shape reuse.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Moves generated-type reuse into the mapper-owned TypeIdentityCache and leaves naming and Strawberry registration in StrawberryRegistry.

  • Generated types share one identity cache, while the registry assigns their names.
  • Nested create payloads use create-shaped inputs under update roots.
  • Nested relation arguments and generated enums follow the settings that request them.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Type request] --> B{Schema-scoped replacement?}
    B -->|Yes| C[Return declaration]
    B -->|No| D{Request cache hit?}
    D -->|Generated| E[Return cached type]
    D -->|Declared| F[Build declaration from cached fields]
    D -->|No| G[Build type]
    G --> H[Match generated name and shape]
    H --> I[Save type and schema scope]
    E --> J[Registry assigns names and decorates]
    F --> J
    I --> J
Loading

Reviews (1) · Last reviewed commit: "test: cover annotations a shape key leav..." · Reviewed by Greptile

Comment thread src/strawchemy/dto/base.py Outdated
if base is None:
return self.backend.copy(dto, name)
return dto
return self._build_from_cached(dto, name, base, dto_config, tags=tags, backend_kwargs=backend_kwargs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Schema scope is not saved

When an unscoped declaration already has the same fields, a later scope="schema" declaration takes this cached-build path and returns before declare_scope runs. The registry no longer stores scoped types, so relations declared afterward do not use that replacement. Another scoped declaration can also claim the same model and kind without raising ScopeConflictError.

Save the scoped declaration on the cached-build path too.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/strawchemy/dto/base.py
Line: 833

Comment:
**Schema scope is not saved**

When an unscoped declaration already has the same fields, a later `scope="schema"` declaration takes this cached-build path and returns before `declare_scope` runs. The registry no longer stores scoped types, so relations declared afterward do not use that replacement. Another scoped declaration can also claim the same model and kind without raising `ScopeConflictError`.

Save the scoped declaration on the cached-build path too.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 06c7010: a declaration built from a cached one now declares its schema scope too, so relations use it and a second scoped declaration raises ScopeConflictError.

Comment thread src/strawchemy/dto/base.py Outdated
dto_config.unset_sentinel,
dto_config.exclude_defaults,
)
return (type(self), self.backend, model, base_key, node_key, config_key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Generated settings are ignored

The request key omits registration, although shape matching includes it. Two generated make_input requests with the same model, name and config but different descriptions or directives therefore return the first registered type. This also happens with no_cache=True, which previously built a fresh type. The registry then skips the second request's settings.

Include registration in the request key so cached requests follow the same matching rules as shape reuse.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/strawchemy/dto/base.py
Line: 555

Comment:
**Generated settings are ignored**

The request key omits `registration`, although shape matching includes it. Two generated `make_input` requests with the same model, name and config but different descriptions or directives therefore return the first registered type. This also happens with `no_cache=True`, which previously built a fresh type. The registry then skips the second request's settings.

Include `registration` in the request key so cached requests follow the same matching rules as shape reuse.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 42cede7: the request key includes the registration (description and directives), so a request with other settings gets its own type, with or without no_cache.

@gazorby
gazorby force-pushed the feat/scope-schema-only branch from 994c468 to 23b1a56 Compare October 13, 2026 14:10
@gazorby
gazorby force-pushed the refactor/type-identity-cache branch from dbd4862 to 42cede7 Compare October 13, 2026 14:10
@gazorby
gazorby force-pushed the feat/scope-schema-only branch from 23b1a56 to dd887c8 Compare October 13, 2026 14:20
@gazorby
gazorby force-pushed the refactor/type-identity-cache branch 2 times, most recently from 9070250 to b72d128 Compare October 13, 2026 14:53
…oot input

BREAKING CHANGE: the nested `{Root}{Relation}Input` reached from a `pk_update_input` or `filter_update_input` now has the create shape (non-nullable columns required, generated keys optional, description "Create input"), and its own relations use create wrappers (`ToManyCreateInput`, no `remove`). Clients that sent partial nested `create` payloads under update mutations must send every required column.
… naming in the registry

BREAKING CHANGE: generated types that expose the same shape under the same name are now one type, and a generated type whose shape differs from the one holding its name gets a path or numbered name instead of silently reusing it. Generated names such as `FruitOrderBy2` or `ColorDistinctOnFields` can therefore change; declare the type, or use `scope="schema"`, when a client depends on its name. `StrawberryRegistry` takes the mapper's `TypeIdentityCache`, `register_type` no longer takes `paginate`, `order`, `distinct_on` or `default_pagination`, `RegistryTypeInfo` loses the matching fields, and DTO backends no longer have `copy`.
@gazorby
gazorby force-pushed the refactor/type-identity-cache branch from 94b36b7 to 8c2deb6 Compare October 13, 2026 15:33

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant