Skip to content

fix(base): remove unreachable msgspec.Struct annotation mapping - #775

Merged
hasansezertasan merged 1 commit into
litestar-org:mainfrom
hasansezertasan:feat/luxuriant-binder
Jul 22, 2026
Merged

fix(base): remove unreachable msgspec.Struct annotation mapping#775
hasansezertasan merged 1 commit into
litestar-org:mainfrom
hasansezertasan:feat/luxuriant-binder

Conversation

@hasansezertasan

@hasansezertasan hasansezertasan commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

Closes #772. Follow-up to #477 / #771.

create_registry() seeds type_annotation_map with Struct: JsonB (msgspec) so struct-typed columns are stored as JsonB. Like the DataclassProtocol entry removed in #771, this entry only ever matches an exact Mapped[Struct] annotation — never a concrete subclass, which is the only realistic usage.

Root cause — SQLAlchemy resolves type_annotation_map by walking the concrete type's __mro__, then rejecting supertype matches in TypeEngine._resolve_for_python_type:

if python_type is not matched_on_flattened:
    return None

MyStruct.__mro__ contains Struct, so the key is found, but because MyStruct is not Struct the match is rejected. So Struct: JsonB is effectively dead for the common case of user-defined structs.

MCVE (before this change)

from advanced_alchemy.base import orm_registry
import msgspec

class MyStruct(msgspec.Struct):
    a: int

orm_registry._resolve_type(msgspec.Struct)  # JSON  (exact base — rare)
orm_registry._resolve_type(MyStruct)         # None  (subclass — the real case)

Changes

  • Remove the default Struct: JsonB entry and its now-unused msgspec import from advanced_alchemy/base.py.
  • Add regression coverage: a guard against reintroducing the Struct key, and a test of the supported concrete-registration path.

Compatibility

This changes the narrow case where a model is annotated literally as Mapped[Struct] (the msgspec base class); that exact annotation previously selected JsonB. Concrete annotations such as Mapped[MyStruct] did not resolve through this entry before this change and continue to require an explicit concrete mapping.

Supported replacement

from advanced_alchemy.base import create_registry
from advanced_alchemy.types import JsonB
from msgspec import Struct


class MyStruct(Struct):
    a: int


registry = create_registry(custom_annotation_map={MyStruct: JsonB})

Notes

This mirrors the direction taken for DataclassProtocol in #771, whose PR body explicitly flagged the identical Struct limitation as out of scope. The regression test uses a direct Struct not in type_annotation_map assertion rather than #771's _is_protocol guard, since msgspec.Struct is a concrete base class, not a Protocol.

Testing

  • uv run pytest tests/unit/test_base.py — all pass
  • uv run ruff check — clean
  • uv run mypy advanced_alchemy/base.py — clean

📚 Documentation preview: https://litestar-org.github.io/advanced-alchemy-docs-preview/775

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.45%. Comparing base (c5ea0c9) to head (eab4246).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #775      +/-   ##
==========================================
+ Coverage   82.43%   82.45%   +0.02%     
==========================================
  Files         105      105              
  Lines        9039     9039              
  Branches     1219     1219              
==========================================
+ Hits         7451     7453       +2     
+ Misses       1262     1260       -2     
  Partials      326      326              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hasansezertasan
hasansezertasan enabled auto-merge (squash) July 22, 2026 12:58
@hasansezertasan
hasansezertasan requested a review from Copilot July 22, 2026 13:25
Follow-up to litestar-org#771 for the same class of dead configuration.

`create_registry()` seeded `type_annotation_map` with `Struct: JsonB`.
SQLAlchemy resolves `type_annotation_map` through an exact/`__mro__`
dictionary lookup that rejects supertype matches
(`_resolve_for_python_type` returns `None` unless
`python_type is matched_on_flattened`). A base `Struct` key is found in
a concrete struct's `__mro__` but then rejected, so the entry only ever
matched the literal `Mapped[Struct]` annotation and never a user-defined
subclass -- the only realistic usage. Like the `DataclassProtocol` entry
removed in litestar-org#771, it was misleading dead configuration.

Remove the entry (and its now-unused msgspec import). Users who want a
struct column register the concrete type via
`custom_annotation_map={MyStruct: JsonB}`, which lands as an exact key
and resolves.

Adds regression tests guarding against reintroducing the `Struct` key
and documenting the supported concrete-type registration path.

Closes litestar-org#772

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR removes an ineffective default msgspec.Struct -> JsonB mapping from create_registry() because SQLAlchemy’s type_annotation_map resolution rejects supertype matches, making the base Struct key effectively unusable for real-world Struct subclasses. It also adds regression tests to prevent the mapping from being reintroduced and to validate the supported explicit concrete-type registration path.

Changes:

  • Remove the unreachable msgspec.Struct entry (and optional import) from advanced_alchemy.base.create_registry().
  • Add a regression test asserting Struct is not present in the default registry’s type_annotation_map.
  • Add a test confirming Struct subclasses resolve when explicitly registered via custom_annotation_map.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
advanced_alchemy/base.py Removes the dead optional msgspec.Struct default mapping from create_registry() to avoid misleading configuration.
tests/unit/test_base.py Adds regression coverage to prevent reintroducing the Struct key and verifies the supported explicit concrete mapping behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@hasansezertasan
hasansezertasan disabled auto-merge July 22, 2026 13:54
@hasansezertasan
hasansezertasan enabled auto-merge (squash) July 22, 2026 13:58

@cofin cofin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM - while this is technically breaking, this and the DictProtocol were never actually working as intended. I think it's safe to release now.

@hasansezertasan
hasansezertasan merged commit 2accd6e into litestar-org:main Jul 22, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: msgspec.Struct in type_annotation_map never matches subclasses

4 participants