sqlmodel-mypy-plugin 0.16.0__tar.gz → 0.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/AGENTS.md +5 -2
  2. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/PKG-INFO +26 -3
  3. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/README.md +25 -2
  4. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/ROADMAP.md +86 -48
  5. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/pyproject.toml +1 -1
  6. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/src/sqlmodel_mypy/__init__.py +1 -1
  7. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/src/sqlmodel_mypy/plugin.py +530 -49
  8. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/src/sqlmodel_mypy/transform.py +72 -0
  9. sqlmodel_mypy_plugin-0.18.0/tests/mypy/configs/mypy-plugin-strict-typed-execute.ini +20 -0
  10. sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/aliased_joins.py +32 -0
  11. sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/field_validation_alias.py +10 -0
  12. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/getattr_column_property.py +16 -0
  13. sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/select_varargs.py +36 -0
  14. sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/session_execute_typed.py +32 -0
  15. sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/table_via_model_config.py +35 -0
  16. sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/tuple_expression.py +14 -0
  17. sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin-strict-typed-execute_ini/session_execute_typed.py +35 -0
  18. sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin-strict_ini/aliased_joins.py +33 -0
  19. sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin-strict_ini/select_varargs.py +37 -0
  20. sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin-strict_ini/table_via_model_config.py +37 -0
  21. sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin-strict_ini/tuple_expression.py +14 -0
  22. sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin_ini/aliased_joins.py +33 -0
  23. sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin_ini/select_varargs.py +37 -0
  24. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/test_mypy.py +7 -0
  25. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/test_plugin_unit.py +915 -0
  26. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/test_transform_unit.py +42 -0
  27. sqlmodel_mypy_plugin-0.16.0/tests/mypy/modules/select_varargs.py +0 -18
  28. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/.gitignore +0 -0
  29. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/.pre-commit-config.yaml +0 -0
  30. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/LICENSE +0 -0
  31. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/src/sqlmodel_mypy/persisted.py +0 -0
  32. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/src/sqlmodel_mypy/py.typed +0 -0
  33. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/conftest.py +0 -0
  34. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
  35. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
  36. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
  37. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +0 -0
  38. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
  39. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/mypy-plugin.ini +0 -0
  40. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/pyproject-plugin.toml +0 -0
  41. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/aliases.py +0 -0
  42. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/annotated_field_metadata.py +0 -0
  43. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/constructor.py +0 -0
  44. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/docs_parity_many_to_many_create_data.py +0 -0
  45. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/docs_parity_read_connected_data.py +0 -0
  46. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/docs_parity_relationship_attributes.py +0 -0
  47. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/docs_parity_update_patterns.py +0 -0
  48. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/execution_options.py +0 -0
  49. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/expression_typing_strict.py +0 -0
  50. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/expressions.py +0 -0
  51. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/field_sa_type.py +0 -0
  52. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/generics.py +0 -0
  53. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/inheritance.py +0 -0
  54. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/joins.py +0 -0
  55. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/label.py +0 -0
  56. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/many_to_many_relationships.py +0 -0
  57. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/mapped_fields.py +0 -0
  58. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/model_config.py +0 -0
  59. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/model_construct.py +0 -0
  60. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/nullable_defaults.py +0 -0
  61. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/persisted_helpers.py +0 -0
  62. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/relationship_comparators.py +0 -0
  63. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/relationship_expressions.py +0 -0
  64. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/relationships.py +0 -0
  65. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/selectinload.py +0 -0
  66. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/session_exec.py +0 -0
  67. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/session_get.py +0 -0
  68. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/table_dunders.py +0 -0
  69. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/table_model_awareness.py +0 -0
  70. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/untyped_fields.py +0 -0
  71. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-pydantic-first_ini/relationships.py +0 -0
  72. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-pydantic-last_ini/relationships.py +0 -0
  73. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +0 -0
  74. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/annotated_field_metadata.py +0 -0
  75. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +0 -0
  76. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_many_to_many_create_data.py +0 -0
  77. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_read_connected_data.py +0 -0
  78. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_relationship_attributes.py +0 -0
  79. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_update_patterns.py +0 -0
  80. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/execution_options.py +0 -0
  81. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
  82. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -0
  83. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
  84. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/many_to_many_relationships.py +0 -0
  85. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +0 -0
  86. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
  87. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +0 -0
  88. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/persisted_helpers.py +0 -0
  89. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
  90. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +0 -0
  91. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
  92. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
  93. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +0 -0
  94. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/execution_options.py +0 -0
  95. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
  96. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +0 -0
  97. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/joins.py +0 -0
  98. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
  99. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
  100. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
  101. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +0 -0
  102. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
  103. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
  104. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +0 -0
  105. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
  106. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
  107. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +0 -0
  108. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/smoke_test.py +0 -0
  109. {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/test_persisted_helpers.py +0 -0
@@ -47,8 +47,11 @@ make update-mypy
47
47
  ## What the plugin does (scope)
48
48
 
49
49
  - Collects SQLModel fields and determines required/optional kwargs based on `sqlmodel.Field(...)`.
50
- - **Ignores** `sqlmodel.Relationship(...)` when generating constructor kwargs.
50
+ - For `table=True` models, includes `sqlmodel.Relationship(...)` members as **optional** constructor kwargs.
51
51
  - Synthesizes `__init__` and `model_construct` signatures for SQLModel subclasses during mypy semantic analysis.
52
+ - Adjusts `table=True` SQLModel class attribute types to behave like SQLAlchemy expressions (e.g. `User.id` and relationship expression/comparator patterns).
53
+ - Exposes SQLAlchemy table metadata on `table=True` models (e.g. `Model.__table__`).
54
+ - Adds targeted typing fixes for common query patterns/helpers (`select(...)` 5+ entities fallback, `sqlmodel.tuple_(...)` as an expression, outer-join `None` propagation, broadened `Session.exec(...)`, optional typed `Session.execute(...)`, and best-effort `column_property(...)` return typing).
52
55
 
53
56
  ## Repo layout (where things live)
54
57
 
@@ -83,4 +86,4 @@ make update-mypy
83
86
  - To release:
84
87
  - Bump the version.
85
88
  - Tag `vX.Y.Z` and push the tag.
86
- - GitHub Actions publishes to PyPI via **trusted publishing** (requires configuring the PyPI project + GitHub environment `pypi`).
89
+ - GitHub Actions publishes to PyPI via **trusted publishing**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlmodel-mypy-plugin
3
- Version: 0.16.0
3
+ Version: 0.18.0
4
4
  Summary: Mypy plugin for SQLModel (better constructor/Field typing).
5
5
  Project-URL: Homepage, https://github.com/sertione/sqlmodel-mypy-plugin
6
6
  Project-URL: Repository, https://github.com/sertione/sqlmodel-mypy-plugin
@@ -68,11 +68,18 @@ Planned work is tracked in [`ROADMAP.md`](ROADMAP.md).
68
68
  - Outer-join `None` propagation in `select(A, B).join(B, isouter=True)` result tuples.
69
69
  - Broaden `Session.exec()` / `AsyncSession.exec()` typing to accept SQLAlchemy `Executable` statements (e.g.
70
70
  `text(...)`), not just `select(...)`.
71
+ - Optional typed `Session.execute()` / `AsyncSession.execute()` for `select(...)` (opt-in; see `typed_execute`).
71
72
  - Accept SQLAlchemy TypeEngine instances in `Field(sa_type=...)` (e.g. `DateTime(timezone=True)`, `String(50)`)
72
73
  without `# type: ignore` in strict mode.
73
74
  - Accept `model_config = ConfigDict(...)` overrides on SQLModel subclasses in strict mode.
74
75
  - Compatible with `pydantic.mypy` **when `sqlmodel_mypy.plugin` is listed first**.
75
76
 
77
+ ## Install
78
+
79
+ ```bash
80
+ uv add sqlmodel-mypy-plugin
81
+ ```
82
+
76
83
  ## Install (dev)
77
84
 
78
85
  Prereqs: **Python 3.10+** and [`uv`](https://docs.astral.sh/uv/).
@@ -119,6 +126,9 @@ Supported options:
119
126
  `__init__` / `model_construct` / constructor signatures, so mypy reports unexpected keyword arguments.
120
127
  - **`warn_untyped_fields`** (default: `true`): When `true`, emit error code `sqlmodel-field` for untyped
121
128
  declarations like `x = Field(...)` / `x = Relationship(...)` (use `x: T = ...` instead).
129
+ - **`typed_execute`** (default: `false`): When `true`, type SQLModel’s deprecated `Session.execute()` /
130
+ `AsyncSession.execute()` for common `select(...)` statements by recovering `Result[...]` generics. This is meant to
131
+ reduce “type clutter” during migrations; prefer `Session.exec()` for new code.
122
132
  - **`debug_dataclass_transform`** (default: `false`): **Advanced/debug-only**. When `true`, keep SQLModel’s
123
133
  `__dataclass_transform__` handling enabled in mypy (useful for debugging plugin interactions; not recommended
124
134
  for normal use).
@@ -130,6 +140,7 @@ Supported options:
130
140
  init_typed = false
131
141
  init_forbid_extra = false
132
142
  warn_untyped_fields = true
143
+ typed_execute = false
133
144
  debug_dataclass_transform = false
134
145
  ```
135
146
 
@@ -140,12 +151,15 @@ debug_dataclass_transform = false
140
151
  init_typed = false
141
152
  init_forbid_extra = false
142
153
  warn_untyped_fields = true
154
+ typed_execute = false
143
155
  debug_dataclass_transform = false
144
156
  ```
145
157
 
146
158
  ## Error codes
147
159
 
148
160
  - `sqlmodel-field`: field-related plugin errors (e.g. untyped `x = Field(...)`).
161
+ - `sqlmodel-plugin-order`: plugin ordering errors (e.g. `pydantic.mypy` enabled before `sqlmodel_mypy.plugin`).
162
+ Fix: list `sqlmodel_mypy.plugin` first (see [Enable in mypy](#enable-in-mypy)).
149
163
 
150
164
  ## SQL expression typing
151
165
 
@@ -170,13 +184,22 @@ tbl = User.__table__
170
184
 
171
185
  ### `getattr(Model, "field")` support
172
186
 
173
- If you build query filters dynamically, `getattr(Model, "field")` with a **string literal** is typed the same as
174
- direct `Model.field` access on `table=True` models:
187
+ If you build query filters dynamically, `getattr(Model, "field")` with a **string literal** (or a `Final` / string
188
+ `Literal[...]` name) is typed the same as direct `Model.field` access on `table=True` models:
175
189
 
176
190
  ```py
177
191
  stmt = select(User).where(getattr(User, "name").like("%x%"))
178
192
  ```
179
193
 
194
+ Example using a `Final` constant:
195
+
196
+ ```py
197
+ from typing import Final
198
+
199
+ FIELD: Final = "name"
200
+ stmt = select(User).where(getattr(User, FIELD).like("%x%"))
201
+ ```
202
+
180
203
  Non-literal names (runtime strings) are intentionally left as `Any` (you’ll still need a cast or a different
181
204
  pattern).
182
205
 
@@ -22,11 +22,18 @@ Planned work is tracked in [`ROADMAP.md`](ROADMAP.md).
22
22
  - Outer-join `None` propagation in `select(A, B).join(B, isouter=True)` result tuples.
23
23
  - Broaden `Session.exec()` / `AsyncSession.exec()` typing to accept SQLAlchemy `Executable` statements (e.g.
24
24
  `text(...)`), not just `select(...)`.
25
+ - Optional typed `Session.execute()` / `AsyncSession.execute()` for `select(...)` (opt-in; see `typed_execute`).
25
26
  - Accept SQLAlchemy TypeEngine instances in `Field(sa_type=...)` (e.g. `DateTime(timezone=True)`, `String(50)`)
26
27
  without `# type: ignore` in strict mode.
27
28
  - Accept `model_config = ConfigDict(...)` overrides on SQLModel subclasses in strict mode.
28
29
  - Compatible with `pydantic.mypy` **when `sqlmodel_mypy.plugin` is listed first**.
29
30
 
31
+ ## Install
32
+
33
+ ```bash
34
+ uv add sqlmodel-mypy-plugin
35
+ ```
36
+
30
37
  ## Install (dev)
31
38
 
32
39
  Prereqs: **Python 3.10+** and [`uv`](https://docs.astral.sh/uv/).
@@ -73,6 +80,9 @@ Supported options:
73
80
  `__init__` / `model_construct` / constructor signatures, so mypy reports unexpected keyword arguments.
74
81
  - **`warn_untyped_fields`** (default: `true`): When `true`, emit error code `sqlmodel-field` for untyped
75
82
  declarations like `x = Field(...)` / `x = Relationship(...)` (use `x: T = ...` instead).
83
+ - **`typed_execute`** (default: `false`): When `true`, type SQLModel’s deprecated `Session.execute()` /
84
+ `AsyncSession.execute()` for common `select(...)` statements by recovering `Result[...]` generics. This is meant to
85
+ reduce “type clutter” during migrations; prefer `Session.exec()` for new code.
76
86
  - **`debug_dataclass_transform`** (default: `false`): **Advanced/debug-only**. When `true`, keep SQLModel’s
77
87
  `__dataclass_transform__` handling enabled in mypy (useful for debugging plugin interactions; not recommended
78
88
  for normal use).
@@ -84,6 +94,7 @@ Supported options:
84
94
  init_typed = false
85
95
  init_forbid_extra = false
86
96
  warn_untyped_fields = true
97
+ typed_execute = false
87
98
  debug_dataclass_transform = false
88
99
  ```
89
100
 
@@ -94,12 +105,15 @@ debug_dataclass_transform = false
94
105
  init_typed = false
95
106
  init_forbid_extra = false
96
107
  warn_untyped_fields = true
108
+ typed_execute = false
97
109
  debug_dataclass_transform = false
98
110
  ```
99
111
 
100
112
  ## Error codes
101
113
 
102
114
  - `sqlmodel-field`: field-related plugin errors (e.g. untyped `x = Field(...)`).
115
+ - `sqlmodel-plugin-order`: plugin ordering errors (e.g. `pydantic.mypy` enabled before `sqlmodel_mypy.plugin`).
116
+ Fix: list `sqlmodel_mypy.plugin` first (see [Enable in mypy](#enable-in-mypy)).
103
117
 
104
118
  ## SQL expression typing
105
119
 
@@ -124,13 +138,22 @@ tbl = User.__table__
124
138
 
125
139
  ### `getattr(Model, "field")` support
126
140
 
127
- If you build query filters dynamically, `getattr(Model, "field")` with a **string literal** is typed the same as
128
- direct `Model.field` access on `table=True` models:
141
+ If you build query filters dynamically, `getattr(Model, "field")` with a **string literal** (or a `Final` / string
142
+ `Literal[...]` name) is typed the same as direct `Model.field` access on `table=True` models:
129
143
 
130
144
  ```py
131
145
  stmt = select(User).where(getattr(User, "name").like("%x%"))
132
146
  ```
133
147
 
148
+ Example using a `Final` constant:
149
+
150
+ ```py
151
+ from typing import Final
152
+
153
+ FIELD: Final = "name"
154
+ stmt = select(User).where(getattr(User, FIELD).like("%x%"))
155
+ ```
156
+
134
157
  Non-literal names (runtime strings) are intentionally left as `Any` (you’ll still need a cast or a different
135
158
  pattern).
136
159
 
@@ -23,7 +23,7 @@
23
23
  - Decide approach: plugin hooks vs stub overlays vs relying on SQLAlchemy typing (documented trade-offs).
24
24
  - Compatibility matrix CI across mypy/SQLModel versions (and pin policy).
25
25
  - **Plugin ordering guardrails (mypy limitation)**
26
- - **Reality check**: mypy generally uses the **first plugin that claims a hook**, so true “order independence”
26
+ - **Reality check**: mypy generally uses the **first plugin that claims a hook**, so true "order independence"
27
27
  between `sqlmodel_mypy.plugin` and `pydantic.mypy` is not realistically achievable today.
28
28
  - Enforce and clearly explain that `sqlmodel_mypy.plugin` must be listed **before** `pydantic.mypy`:
29
29
  - Source: `src/sqlmodel_mypy/plugin.py` (`SQLModelMypyPlugin.__init__`, `_sqlmodel_metaclass_callback()`).
@@ -31,9 +31,9 @@
31
31
 
32
32
  ## v0.4 (Docs-aligned relationship constructors) - DONE
33
33
 
34
- **Motivation**: SQLModel’s official docs pass relationship attributes in constructors (e.g. `Hero(team=team)`,
34
+ **Motivation**: SQLModel's official docs pass relationship attributes in constructors (e.g. `Hero(team=team)`,
35
35
  `Team(heroes=[...])`). v0.4 accepts relationship kwargs in generated `__init__` / `model_construct` for
36
- `table=True` models, so `init_forbid_extra=true` no longer causes false-positive “unexpected keyword argument”
36
+ `table=True` models, so `init_forbid_extra=true` no longer causes false-positive "unexpected keyword argument"
37
37
  errors for documented patterns.
38
38
 
39
39
  - **Accept relationship kwargs in generated `__init__` / `model_construct` (table models)**
@@ -63,17 +63,17 @@ errors for documented patterns.
63
63
  pattern (and keep types conservative).
64
64
  - Update/add mypy integration tests to match the chosen strategy.
65
65
 
66
- ## v0.5 (Model-kind awareness + reduce “surprising” typing) - DONE
66
+ ## v0.5 (Model-kind awareness + reduce "surprising" typing) - DONE
67
67
 
68
68
  - **Table-model awareness for SQLAlchemy expression typing**
69
69
  - Only treat class attributes as SQLAlchemy expressions (e.g. wrap to `InstrumentedAttribute[T]`) for `table=True`
70
70
  SQLModel models.
71
- - Avoid changing attribute types for “data models” (non-table SQLModel subclasses) used for FastAPI schemas, e.g.
71
+ - Avoid changing attribute types for "data models" (non-table SQLModel subclasses) used for FastAPI schemas, e.g.
72
72
  `TeamBase`, `HeroCreate`, `HeroPublic`.
73
73
  - Docs: `https://sqlmodel.tiangolo.com/tutorial/fastapi/relationships/`
74
74
 
75
75
  - **Pydantic/Field alias & config parity checkpoints**
76
- - Verify our generated signatures stay compatible with SQLModel’s `Field(...)` alias behavior (alias propagation
76
+ - Verify our generated signatures stay compatible with SQLModel's `Field(...)` alias behavior (alias propagation
77
77
  and precedence in `validation_alias` / `serialization_alias`).
78
78
  - Source: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py` (`Field()` implementation).
79
79
  - Document what is supported vs intentionally out of scope when users rely on aliases in constructor kwargs.
@@ -94,20 +94,20 @@ errors for documented patterns.
94
94
  - **More query-shape coverage**
95
95
  - Multi-entity selects, joins/outer-joins, and relationship-based filters across common SQLModel tutorial patterns.
96
96
  - **Document trade-offs (and pick a default strategy)**
97
- - Plugin hooks vs stub overlays vs relying purely on SQLAlchemy typing (with “when to use what” guidance).
97
+ - Plugin hooks vs stub overlays vs relying purely on SQLAlchemy typing (with "when to use what" guidance).
98
98
 
99
99
  ## v0.8 (Field `sa_type=` supports SQLAlchemy TypeEngine instances) - DONE
100
100
 
101
- **Motivation**: SQLModel’s `Field(sa_type=...)` is a supported API, but real-world usage commonly passes
101
+ **Motivation**: SQLModel's `Field(sa_type=...)` is a supported API, but real-world usage commonly passes
102
102
  **instantiated SQLAlchemy types** (e.g. `DateTime(timezone=True)`, `String(50)`), which triggers mypy
103
103
  `call-overload` errors under `--strict`. Users currently work around this with `sa_column=Column(...)` or
104
- `# type: ignore`, which is exactly the “types clutter” we want to remove.
104
+ `# type: ignore`, which is exactly the "types clutter" we want to remove.
105
105
 
106
106
  - **Fix mypy `call-overload` for `Field(sa_type=<TypeEngine instance>)`**
107
107
  - Upstream issue reports:
108
108
  - `https://github.com/fastapi/sqlmodel/discussions/955`
109
109
  - `https://github.com/fastapi/sqlmodel/discussions/1228`
110
- - Upstream pending fix (adjust `sa_type` annotation to match SQLAlchemy’s `Column` type argument rules):
110
+ - Upstream pending fix (adjust `sa_type` annotation to match SQLAlchemy's `Column` type argument rules):
111
111
  - `https://github.com/fastapi/sqlmodel/pull/1345`
112
112
  - Source references:
113
113
  - SQLModel `Field()` signature: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py`
@@ -122,7 +122,7 @@ errors for documented patterns.
122
122
  touching runtime behavior.
123
123
 
124
124
  - **Tests**
125
- - Add a mypy integration module that reproduces the failure in strict mode and asserts it’s fixed without
125
+ - Add a mypy integration module that reproduces the failure in strict mode and asserts it's fixed without
126
126
  `# type: ignore`, e.g.:
127
127
  - `Field(sa_type=DateTime(timezone=True))`
128
128
  - `Field(sa_type=String(50))`
@@ -133,7 +133,7 @@ errors for documented patterns.
133
133
 
134
134
  **Motivation**: SQLModel is built on Pydantic v2, and users often want to set things like `extra="forbid"` on
135
135
  schema-like models (e.g. `HeroCreate`, `HeroUpdate`) via `model_config = ConfigDict(...)`. Today this can fail mypy
136
- strict with an “incompatible types in assignment” error (`ConfigDict` vs `SQLModelConfig`), forcing users into
136
+ strict with an "incompatible types in assignment" error (`ConfigDict` vs `SQLModelConfig`), forcing users into
137
137
  `# type: ignore` or `cast(...)`.
138
138
 
139
139
  - **Fix mypy incompatibility for `model_config` overrides**
@@ -148,7 +148,7 @@ strict with an “incompatible types in assignment” error (`ConfigDict` vs `SQ
148
148
  so assignment from `ConfigDict(...)` is always accepted.
149
149
  - **Plugin adjustment**: during semantic analysis, rewrite the inferred type of `model_config` on SQLModel subclasses
150
150
  to a compatible supertype (while keeping runtime untouched).
151
- - Ensure we don’t regress table-model behavior: SQLModel uses `model_config["table"]` and `model_config["registry"]`
151
+ - Ensure we don't regress table-model behavior: SQLModel uses `model_config["table"]` and `model_config["registry"]`
152
152
  at runtime (see `SQLModelMetaclass.__new__`).
153
153
 
154
154
  - **Tests**
@@ -160,8 +160,8 @@ strict with an “incompatible types in assignment” error (`ConfigDict` vs `SQ
160
160
  ## v0.10 (`select()` varargs: avoid overload ceiling) - DONE
161
161
 
162
162
  **Motivation**: SQLModel provides its own typed `select()` overloads, but they are generated only up to **4**
163
- entities. In real code it’s common to select more than 4 columns/entities (especially with joins/aggregates), which
164
- can trigger “No overload variant of `select` matches argument types …” even though runtime works.
163
+ entities. In real code it's common to select more than 4 columns/entities (especially with joins/aggregates), which
164
+ can trigger "No overload variant of `select` matches argument types …" even though runtime works.
165
165
 
166
166
  - **Fix overload ceiling for `sqlmodel.select(...)`**
167
167
  - Source reference (generated overloads; note the 4-entity cap):
@@ -171,26 +171,26 @@ can trigger “No overload variant of `select` matches argument types …” eve
171
171
  - `https://github.com/fastapi/sqlmodel/issues/271`
172
172
 
173
173
  - **Implementation options (pick default; document trade-offs)**
174
- - **Stub overlay (likely simplest)**: add a final “varargs fallback” overload, e.g.:
174
+ - **Stub overlay (likely simplest)**: add a final "varargs fallback" overload, e.g.:
175
175
  - `@overload`
176
176
  - `def select(__ent0: Any, __ent1: Any, __ent2: Any, __ent3: Any, __ent4: Any, *entities: Any) -> Select[tuple[Any, ...]]: ...`
177
177
  so mypy always has a matching overload beyond 4 args.
178
178
  - **Plugin signature hook**: provide a mypy hook for `sqlmodel.sql.expression.select` that adds the same fallback
179
179
  overload, keeping the existing 1–4 entity overload precision.
180
- - Optionally, generate more precise overloads (e.g. up to 8) if we’re willing to carry the maintenance cost.
180
+ - Optionally, generate more precise overloads (e.g. up to 8) if we're willing to carry the maintenance cost.
181
181
 
182
182
  - **Tests**
183
183
  - Add a mypy integration module that selects 5+ entities and asserts no `call-overload` errors under strict mode.
184
184
  - Decide and lock down the fallback return type (likely `Select[tuple[Any, ...]]`) to keep behavior predictable.
185
185
 
186
- ## v0.11 (SQLAlchemy “extras” coverage + SQLModel `Select` join parity) - DONE
186
+ ## v0.11 (SQLAlchemy "extras" coverage + SQLModel `Select` join parity) - DONE
187
187
 
188
188
  - **Lock down common SQLAlchemy helpers used with SQLModel**
189
189
  - `.label(...)` on `table=True` model fields typed as SQLAlchemy expressions.
190
190
  - ORM loader options like `selectinload(Model.relationship)` on `table=True` relationships.
191
191
  - `Select.execution_options(...)` chaining without losing the `Select[...]` generic (including outer-join `None` propagation).
192
- - **Fix outer-join `None` propagation for SQLModel’s own `Select` wrapper**
193
- - Apply the join/outerjoin return-type adjustment to both SQLAlchemy `Select` and SQLModel’s `sqlmodel.sql._expression_select_cls.Select`.
192
+ - **Fix outer-join `None` propagation for SQLModel's own `Select` wrapper**
193
+ - Apply the join/outerjoin return-type adjustment to both SQLAlchemy `Select` and SQLModel's `sqlmodel.sql._expression_select_cls.Select`.
194
194
  - **Tests**
195
195
  - Add/extend mypy integration coverage and commit golden outputs for the new cases.
196
196
 
@@ -201,7 +201,7 @@ models do provide this at runtime for `table=True`, but type checkers can miss i
201
201
 
202
202
  - **Expose and type `__table__` on `table=True` SQLModel models**
203
203
  - Provide a plugin-generated `__table__` attribute typed as `sqlalchemy.sql.schema.Table` (with safe fallbacks
204
- when stubs don’t expose `Table`).
204
+ when stubs don't expose `Table`).
205
205
  - **Tests**
206
206
  - Add mypy integration coverage for `Model.__table__` and `Session.get` / `AsyncSession.get` with SQLModel
207
207
  table models.
@@ -230,8 +230,8 @@ signatures (required/optional + alias kwargs) when `Field(...)` metadata lives i
230
230
  (`_collect_member_from_stmt`) so class-call signatures stay consistent with generated `__init__`.
231
231
  - Source references:
232
232
  - SQLModel `Field()` overloads + alias precedence: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py`
233
- - `Annotated` handling in SQLModel’s type parsing: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py`
234
- - Release note: “Fix support for types with `Optional[Annotated[x, f()]]`…” (PR #1093):
233
+ - `Annotated` handling in SQLModel's type parsing: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py`
234
+ - Release note: "Fix support for types with `Optional[Annotated[x, f()]]`…" (PR #1093):
235
235
  `https://github.com/fastapi/sqlmodel/pull/1093`
236
236
 
237
237
  ## v0.15 (Outer-join `None` propagation for relationship-attribute joins) - DONE
@@ -253,20 +253,20 @@ signatures (required/optional + alias kwargs) when `Field(...)` metadata lives i
253
253
 
254
254
  ## v0.16 (Strict typing ergonomics add-ons, opt-in) - DONE
255
255
 
256
- **Motivation**: strict typing + SQLModel often means the database populates values “later” (after flush/commit/
257
- refresh), especially primary keys (`id: int | None`). Mypy can’t infer those runtime effects, and users end up with
256
+ **Motivation**: strict typing + SQLModel often means the database populates values "later" (after flush/commit/
257
+ refresh), especially primary keys (`id: int | None`). Mypy can't infer those runtime effects, and users end up with
258
258
  repeated `assert obj.id is not None` (type clutter).
259
259
 
260
260
  - **Provide a tiny opt-in narrowing helper**
261
261
  - Docs context (why `id` starts as `None` and is populated after commit/refresh):
262
262
  - `https://sqlmodel.tiangolo.com/tutorial/automatic-id-none-refresh/`
263
- - Ship a small helper in this package (runtime-free or near-zero runtime) that helps users narrow “persisted” models
263
+ - Ship a small helper in this package (runtime-free or near-zero runtime) that helps users narrow "persisted" models
264
264
  without `cast(...)`/`# type: ignore`, e.g.:
265
265
  - `assert_persisted(obj)` / `require_id(obj)` / `ensure_persisted(obj)` returning a `TypeGuard[...]` or using
266
266
  overloads to narrow `id` to `int`.
267
267
  - Keep it optional and well-documented; default plugin behavior should remain conservative.
268
268
 
269
- ## v0.17 (`tuple_()` should return a SQL expression, not a Python tuple)
269
+ ## v0.17 (`tuple_()` should return a SQL expression, not a Python tuple) - DONE
270
270
 
271
271
  **Motivation**: SQLModel re-exports `tuple_` from `sqlmodel.sql.expression`, but its typing currently says it returns a
272
272
  built-in `tuple[Any, ...]`. In SQLAlchemy, `tuple_()` returns a SQL expression object (`sqlalchemy.sql.elements.Tuple`)
@@ -286,26 +286,64 @@ for otherwise normal SQLAlchemy patterns.
286
286
  - Add a mypy integration module demonstrating composite IN typing without ignores, e.g.:
287
287
  - `tuple_(User.id, User.team_id).in_([(1, 2)])`
288
288
 
289
- ## v0.18 (Recognize `table=True` via `model_config["table"]`, not just class kwargs)
289
+ ## v0.18 (Comprehensive typing polish) - DONE
290
290
 
291
- **Motivation**: SQLModel’s runtime `is_table_model_class()` checks `model_config.get("table")`, so a class can be a
292
- table model even when it isn’t declared as `class Model(SQLModel, table=True)`. Today our plugin/transformer only
293
- looks at the class keyword, so “table-only” behaviors may not activate (expression typing, relationship kwargs in
294
- constructor signatures, `__table__`, etc.).
291
+ This release consolidates several enhancements to round out SQLModel/mypy strict-mode ergonomics.
295
292
 
296
- - **Detect table models via `model_config` (metadata-driven)**
297
- - Extend semantic analysis in `src/sqlmodel_mypy/transform.py` to detect a statically-known class-body assignment like:
298
- - `model_config = ConfigDict(table=True)` or equivalent SQLModel config helpers
299
- - Persist the result in `TypeInfo.metadata["sqlmodel-mypy-metadata"]`, then update `_is_table_model(...)` in:
300
- - `src/sqlmodel_mypy/plugin.py`
301
- - `src/sqlmodel_mypy/transform.py`
302
- to consult metadata first (then fall back to `table=True` in the class definition).
303
- - Source references:
304
- - SQLModel metaclass reads config: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py` (`SQLModelMetaclass.__new__`)
305
- - SQLModel runtime table detection: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py` (`is_table_model_class`)
306
- - **Tests**
307
- - Add mypy integration coverage for a table model enabled via `model_config` and assert the same behavior as
308
- `table=True`:
309
- - class attribute expression typing (e.g. `User.id.in_([1])`)
310
- - constructor accepts relationship kwargs when configured (`init_forbid_extra=true`)
311
- - `User.__table__` is typed and present
293
+ ### Recognize `table=True` via `model_config["table"]`
294
+
295
+ SQLModel's runtime `is_table_model_class()` checks `model_config.get("table")`, so a class can be a table model even
296
+ without `class Model(SQLModel, table=True)`.
297
+
298
+ - Extend semantic analysis in `src/sqlmodel_mypy/transform.py` to detect `model_config = ConfigDict(table=True)`.
299
+ - Persist the result in `TypeInfo.metadata["sqlmodel-mypy-metadata"]`, then update `_is_table_model(...)` in both
300
+ `plugin.py` and `transform.py` to consult metadata first.
301
+ - Source references:
302
+ - SQLModel metaclass: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py` (`SQLModelMetaclass.__new__`)
303
+ - SQLModel runtime table detection: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py`
304
+
305
+ ### `select(5+)`: recover tuple element types
306
+
307
+ v0.10 fixed the overload ceiling by accepting `select(5+)`, but the fallback return type was `Select[tuple[Any, ...]]`.
308
+
309
+ - Add a best-effort return-type hook that recovers element types from inferred argument expression types.
310
+ - Code: `_sqlmodel_select_signature_callback()`, `_sqlmodel_select_return_type_callback()`, `_sqlmodel_select_entity_result_type()`
311
+ - Tests: `tests/mypy/modules/select_varargs.py`
312
+
313
+ ### Outer-join `None` propagation for aliased/variable join targets
314
+
315
+ Outer-join `None` propagation previously only worked when the join target was a direct model ref or relationship
316
+ attribute, not when users join via an alias variable (e.g. `team_alias = aliased(Team)`).
317
+
318
+ - Extend join target type inference to handle typed variables (including `aliased(Model)` results).
319
+ - Code: `_typeinfo_from_join_target_expr()`, `_typeinfo_from_typed_join_target_expr()`, `_typeinfo_from_typed_join_target_type()`
320
+ - Tests: `tests/mypy/modules/aliased_joins.py`
321
+
322
+ ### `getattr(Model, NAME)`: support `Final`/`Literal[...]` names
323
+
324
+ Dynamic query builders often use constants for attribute names; without this, `getattr(...)` falls back to `Any`.
325
+
326
+ - Extend `getattr(Model, NAME)` typing beyond string literals by accepting name args typed as string `Literal[...]`
327
+ (and best-effort `Final` constants where mypy retains the literal).
328
+ - Code: `_sqlmodel_getattr_return_type_callback()`
329
+ - Tests: `tests/mypy/modules/getattr_column_property.py`
330
+
331
+ ### `Field(validation_alias=...)`: Pydantic v2 alias parity
332
+
333
+ Pydantic v2 supports `validation_alias=AliasPath(...)` / `AliasChoices(...)`, but SQLModel's `Field(...)` typing is
334
+ narrower; strict mypy users otherwise need `cast(...)`/`# type: ignore`.
335
+
336
+ - Widen `Field(validation_alias=...)` to accept `pydantic.aliases.AliasPath | AliasChoices` (idempotent if upstream
337
+ is already widened).
338
+ - Code: `_sqlmodel_field_signature_callback()`
339
+ - Tests: `tests/mypy/modules/field_validation_alias.py`
340
+
341
+ ### Optional typed `Session.execute()` / `AsyncSession.execute()`
342
+
343
+ SQLModel overrides `execute()` and types it as `Result[Any]`, shadowing SQLAlchemy's typed overloads. Even though
344
+ `execute()` is deprecated in SQLModel docs, users still rely on it during migrations.
345
+
346
+ - Add an opt-in mypy plugin behavior that types `execute(select(...))` as `Result[...]` (and preserves async wrapping).
347
+ - Config: `typed_execute = true` under `[sqlmodel-mypy]`
348
+ - Code: `SQLModelPluginConfig.typed_execute`, `_sqlmodel_session_execute_return_type_callback()`, `_row_tuple_type_from_typed_statement()`
349
+ - Tests: `tests/mypy/configs/mypy-plugin-strict-typed-execute.ini`, `tests/mypy/modules/session_execute_typed.py`
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlmodel-mypy-plugin"
7
- version = "0.16.0"
7
+ version = "0.18.0"
8
8
  description = "Mypy plugin for SQLModel (better constructor/Field typing)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,4 +1,4 @@
1
- __version__ = "0.16.0"
1
+ __version__ = "0.18.0"
2
2
 
3
3
  from .persisted import HasId, HasOptionalId, has_id, require_id
4
4