sqlmodel-mypy-plugin 0.19.1__tar.gz → 0.20.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 (118) hide show
  1. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/.pre-commit-config.yaml +1 -1
  2. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/PKG-INFO +34 -2
  3. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/README.md +31 -0
  4. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/pyproject.toml +2 -2
  5. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/plugin.py +1 -1
  6. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/transform.py +191 -60
  7. sqlmodel_mypy_plugin-0.20.0/tests/mypy/modules/nullable_defaults.py +89 -0
  8. sqlmodel_mypy_plugin-0.20.0/tests/mypy/modules/sequence_name_collision.py +30 -0
  9. sqlmodel_mypy_plugin-0.20.0/tests/mypy/modules/sqlmodel_reexported_sequence.py +24 -0
  10. sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +93 -0
  11. sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin-strict_ini/sequence_name_collision.py +31 -0
  12. sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin-strict_ini/sqlmodel_reexported_sequence.py +25 -0
  13. sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +93 -0
  14. sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin_ini/sequence_name_collision.py +31 -0
  15. sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin_ini/sqlmodel_reexported_sequence.py +25 -0
  16. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/test_mypy.py +4 -0
  17. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/test_transform_unit.py +290 -0
  18. sqlmodel_mypy_plugin-0.19.1/tests/mypy/modules/nullable_defaults.py +0 -26
  19. sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +0 -27
  20. sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +0 -27
  21. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/.gitignore +0 -0
  22. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/AGENTS.md +0 -0
  23. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/LICENSE +0 -0
  24. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/__init__.py +0 -0
  25. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/persisted.py +0 -0
  26. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/py.typed +0 -0
  27. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/conftest.py +0 -0
  28. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
  29. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
  30. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
  31. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +0 -0
  32. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-strict-typed-execute.ini +0 -0
  33. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
  34. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin.ini +0 -0
  35. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/pyproject-plugin.toml +0 -0
  36. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/aliased_joins.py +0 -0
  37. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/aliases.py +0 -0
  38. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/annotated_field_metadata.py +0 -0
  39. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/constructor.py +0 -0
  40. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/docs_parity_many_to_many_create_data.py +0 -0
  41. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/docs_parity_read_connected_data.py +0 -0
  42. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/docs_parity_relationship_attributes.py +0 -0
  43. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/docs_parity_update_patterns.py +0 -0
  44. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/execution_options.py +0 -0
  45. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/expression_typing_strict.py +0 -0
  46. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/expressions.py +0 -0
  47. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/field_sa_type.py +0 -0
  48. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/field_validation_alias.py +0 -0
  49. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/generics.py +0 -0
  50. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/getattr_column_property.py +0 -0
  51. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/inheritance.py +0 -0
  52. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/joins.py +0 -0
  53. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/label.py +0 -0
  54. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/many_to_many_relationships.py +0 -0
  55. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/mapped_fields.py +0 -0
  56. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/model_config.py +0 -0
  57. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/model_construct.py +0 -0
  58. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/persisted_helpers.py +0 -0
  59. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/relationship_comparators.py +0 -0
  60. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/relationship_expressions.py +0 -0
  61. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/relationships.py +0 -0
  62. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/select_varargs.py +0 -0
  63. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/selectinload.py +0 -0
  64. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/session_exec.py +0 -0
  65. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/session_execute_typed.py +0 -0
  66. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/session_get.py +0 -0
  67. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/table_dunders.py +0 -0
  68. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/table_model_awareness.py +0 -0
  69. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/table_via_model_config.py +0 -0
  70. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/tuple_expression.py +0 -0
  71. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/untyped_fields.py +0 -0
  72. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-pydantic-first_ini/relationships.py +0 -0
  73. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-pydantic-last_ini/relationships.py +0 -0
  74. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict-typed-execute_ini/session_execute_typed.py +0 -0
  75. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/aliased_joins.py +0 -0
  76. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +0 -0
  77. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/annotated_field_metadata.py +0 -0
  78. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +0 -0
  79. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_many_to_many_create_data.py +0 -0
  80. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_read_connected_data.py +0 -0
  81. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_relationship_attributes.py +0 -0
  82. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_update_patterns.py +0 -0
  83. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/execution_options.py +0 -0
  84. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
  85. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -0
  86. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
  87. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/many_to_many_relationships.py +0 -0
  88. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +0 -0
  89. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
  90. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/persisted_helpers.py +0 -0
  91. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
  92. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +0 -0
  93. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/select_varargs.py +0 -0
  94. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/session_exec.py +0 -0
  95. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
  96. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/table_via_model_config.py +0 -0
  97. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/tuple_expression.py +0 -0
  98. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/aliased_joins.py +0 -0
  99. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
  100. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +0 -0
  101. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/execution_options.py +0 -0
  102. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
  103. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +0 -0
  104. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/joins.py +0 -0
  105. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
  106. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
  107. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
  108. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
  109. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
  110. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +0 -0
  111. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/select_varargs.py +0 -0
  112. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/session_exec.py +0 -0
  113. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
  114. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
  115. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +0 -0
  116. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/smoke_test.py +0 -0
  117. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/test_persisted_helpers.py +0 -0
  118. {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/test_plugin_unit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  repos:
2
2
  - repo: https://github.com/astral-sh/ruff-pre-commit
3
- rev: v0.6.9
3
+ rev: v0.16.4
4
4
  hooks:
5
5
  - id: ruff
6
6
  args: [--fix]
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: sqlmodel-mypy-plugin
3
- Version: 0.19.1
3
+ Version: 0.20.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
@@ -38,6 +38,7 @@ Classifier: Programming Language :: Python :: 3.10
38
38
  Classifier: Programming Language :: Python :: 3.11
39
39
  Classifier: Programming Language :: Python :: 3.12
40
40
  Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Programming Language :: Python :: 3.14
41
42
  Classifier: Typing :: Typed
42
43
  Requires-Python: >=3.10
43
44
  Requires-Dist: mypy>=1.19.1
@@ -167,10 +168,12 @@ expressions, so you can write queries without `col()`:
167
168
  ```py
168
169
  from sqlmodel import Field, SQLModel, select
169
170
 
171
+
170
172
  class User(SQLModel, table=True):
171
173
  id: int = Field(primary_key=True)
172
174
  name: str = Field()
173
175
 
176
+
174
177
  stmt = select(User).where(User.name.like("%x%"))
175
178
  ```
176
179
 
@@ -228,6 +231,35 @@ stmt = select(Team).where(Team.heroes.contains(hero_obj))
228
231
  - **SQLAlchemy typing**: relied upon for core SQL/ORM typing wherever possible.
229
232
  - **Stub overlays**: only for upstream gaps that can’t be addressed cleanly via hooks (prefer upstream fixes first).
230
233
 
234
+ ## Database-generated columns are optional kwargs
235
+
236
+ A field whose value the database (or SQLAlchemy) supplies on INSERT is not required in the generated
237
+ `__init__` / `model_construct` signature. The plugin reads this off `Field(...)` syntactically, from
238
+ `nullable=True`, `sa_column=Column(...)` and `sa_column_kwargs={...}`:
239
+
240
+ | Form | Why it counts |
241
+ | --- | --- |
242
+ | `nullable=True` | column accepts `NULL` |
243
+ | `default=` / `insert_default=` / `server_default=` (incl. `server_default=FetchedValue()`) | an explicit default |
244
+ | positional `Computed(...)` | `GENERATED ALWAYS AS`, computed by the database |
245
+ | positional `Sequence(...)` | becomes `Column.default`, identical to `default=Sequence(...)` |
246
+ | positional `Identity(...)` | becomes `Column.server_default` (`GENERATED ... AS IDENTITY`) |
247
+ | positional `FetchedValue()` | becomes `Column.server_default` |
248
+ | `primary_key=True` **and** `autoincrement=True` | renders as `SERIAL`/`IDENTITY` |
249
+
250
+ Markers are matched by their resolved module, so `Sequence` imported from `typing` or
251
+ `collections.abc` is never mistaken for `sqlalchemy.Sequence`; `sqlmodel` re-exports of these
252
+ constructs are recognized. A name the plugin cannot trace back to SQLAlchemy is left alone.
253
+
254
+ Deliberately **not** covered:
255
+
256
+ - a bare `primary_key=True`. SQLAlchemy's default `autoincrement="auto"` only generates a value for
257
+ a *single-column integer* primary key, and neither the column type nor the rest of the table's
258
+ primary key is knowable from one `Column(...)` call — composite or non-integer primary keys would
259
+ silently become optional. Use SQLModel's usual `Field(default=None, primary_key=True)`.
260
+ - `autoincrement=True` **without** `primary_key=True`. SQLAlchemy ignores it there (the column
261
+ compiles to a plain `INTEGER`), so the value really is caller-supplied.
262
+
231
263
  ## Field aliases in constructor kwargs
232
264
 
233
265
  If you use `Field(alias=...)` (or `validation_alias=...` / `schema_extra={"validation_alias": ...}`), the plugin
@@ -121,10 +121,12 @@ expressions, so you can write queries without `col()`:
121
121
  ```py
122
122
  from sqlmodel import Field, SQLModel, select
123
123
 
124
+
124
125
  class User(SQLModel, table=True):
125
126
  id: int = Field(primary_key=True)
126
127
  name: str = Field()
127
128
 
129
+
128
130
  stmt = select(User).where(User.name.like("%x%"))
129
131
  ```
130
132
 
@@ -182,6 +184,35 @@ stmt = select(Team).where(Team.heroes.contains(hero_obj))
182
184
  - **SQLAlchemy typing**: relied upon for core SQL/ORM typing wherever possible.
183
185
  - **Stub overlays**: only for upstream gaps that can’t be addressed cleanly via hooks (prefer upstream fixes first).
184
186
 
187
+ ## Database-generated columns are optional kwargs
188
+
189
+ A field whose value the database (or SQLAlchemy) supplies on INSERT is not required in the generated
190
+ `__init__` / `model_construct` signature. The plugin reads this off `Field(...)` syntactically, from
191
+ `nullable=True`, `sa_column=Column(...)` and `sa_column_kwargs={...}`:
192
+
193
+ | Form | Why it counts |
194
+ | --- | --- |
195
+ | `nullable=True` | column accepts `NULL` |
196
+ | `default=` / `insert_default=` / `server_default=` (incl. `server_default=FetchedValue()`) | an explicit default |
197
+ | positional `Computed(...)` | `GENERATED ALWAYS AS`, computed by the database |
198
+ | positional `Sequence(...)` | becomes `Column.default`, identical to `default=Sequence(...)` |
199
+ | positional `Identity(...)` | becomes `Column.server_default` (`GENERATED ... AS IDENTITY`) |
200
+ | positional `FetchedValue()` | becomes `Column.server_default` |
201
+ | `primary_key=True` **and** `autoincrement=True` | renders as `SERIAL`/`IDENTITY` |
202
+
203
+ Markers are matched by their resolved module, so `Sequence` imported from `typing` or
204
+ `collections.abc` is never mistaken for `sqlalchemy.Sequence`; `sqlmodel` re-exports of these
205
+ constructs are recognized. A name the plugin cannot trace back to SQLAlchemy is left alone.
206
+
207
+ Deliberately **not** covered:
208
+
209
+ - a bare `primary_key=True`. SQLAlchemy's default `autoincrement="auto"` only generates a value for
210
+ a *single-column integer* primary key, and neither the column type nor the rest of the table's
211
+ primary key is knowable from one `Column(...)` call — composite or non-integer primary keys would
212
+ silently become optional. Use SQLModel's usual `Field(default=None, primary_key=True)`.
213
+ - `autoincrement=True` **without** `primary_key=True`. SQLAlchemy ignores it there (the column
214
+ compiles to a plain `INTEGER`), so the value really is caller-supplied.
215
+
185
216
  ## Field aliases in constructor kwargs
186
217
 
187
218
  If you use `Field(alias=...)` (or `validation_alias=...` / `schema_extra={"validation_alias": ...}`), the plugin
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlmodel-mypy-plugin"
7
- version = "0.19.1"
7
+ version = "0.20.0"
8
8
  description = "Mypy plugin for SQLModel (better constructor/Field typing)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -20,6 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.11",
21
21
  "Programming Language :: Python :: 3.12",
22
22
  "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
23
24
  "Typing :: Typed",
24
25
  ]
25
26
  dependencies = [
@@ -52,7 +53,6 @@ include = [
52
53
  "/src",
53
54
  "/tests",
54
55
  "/README.md",
55
- "/ROADMAP.md",
56
56
  "/AGENTS.md",
57
57
  "/LICENSE",
58
58
  "/pyproject.toml",
@@ -171,7 +171,7 @@ SQLALCHEMY_INSPECT_FULLNAMES = {
171
171
  }
172
172
 
173
173
  # Increment when plugin changes should invalidate mypy cache.
174
- __version__ = 26
174
+ __version__ = 27
175
175
 
176
176
 
177
177
  class _CollectedField(NamedTuple):
@@ -11,8 +11,8 @@ from __future__ import annotations
11
11
 
12
12
  import ast
13
13
  import keyword
14
- from collections.abc import Iterator
15
- from typing import Any, Protocol
14
+ from collections.abc import Callable, Iterable, Iterator
15
+ from typing import Any, Protocol, TypeVar
16
16
 
17
17
  from mypy.errorcodes import ErrorCode
18
18
  from mypy.expandtype import expand_type
@@ -85,6 +85,19 @@ SQLMODEL_RELATIONSHIP_FULLNAME = "sqlmodel.main.Relationship"
85
85
 
86
86
  SQLALCHEMY_MAPPED_FULLNAMES = {"sqlalchemy.orm.Mapped", "sqlalchemy.orm.base.Mapped"}
87
87
 
88
+ # `Column(...)` keywords whose presence means the value does not have to be supplied by the caller.
89
+ # All of them end up as `Column.default` / `Column.server_default`, i.e. the value is produced by
90
+ # SQLAlchemy or by the database on INSERT.
91
+ SQLALCHEMY_DEFAULTISH_COLUMN_KWARGS = frozenset({"server_default", "default", "insert_default"})
92
+
93
+ # SQLAlchemy schema constructs that are passed to `Column(...)` *positionally* and install a
94
+ # default/server-default on the column via `SchemaEventTarget._set_parent`:
95
+ # - `Computed(...)` -> GENERATED ALWAYS AS, value is computed by the database
96
+ # - `Sequence(...)` -> becomes `Column.default` (identical to `default=Sequence(...)`)
97
+ # - `Identity(...)` -> becomes `Column.server_default` (GENERATED ... AS IDENTITY)
98
+ # - `FetchedValue()` -> becomes `Column.server_default` (value fetched after INSERT)
99
+ SQLALCHEMY_COLUMN_DEFAULT_MARKERS = frozenset({"Computed", "Sequence", "Identity", "FetchedValue"})
100
+
88
101
 
89
102
  def _plugin_any() -> AnyType:
90
103
  """Return an Any that should not trigger `disallow_any_explicit`."""
@@ -94,6 +107,109 @@ def _plugin_any() -> AnyType:
94
107
  ERROR_FIELD = ErrorCode("sqlmodel-field", "SQLModel field error", "SQLModel")
95
108
 
96
109
 
110
+ _ExprT = TypeVar("_ExprT")
111
+
112
+
113
+ def _is_sqlalchemy_module(module: str | None) -> bool:
114
+ """True for `sqlalchemy` and any of its submodules."""
115
+ if not module:
116
+ return False
117
+ return module.split(".", 1)[0] == "sqlalchemy"
118
+
119
+
120
+ def _is_sqlalchemy_marker_source_module(module: str | None) -> bool:
121
+ """Modules an import may legitimately pull a SQLAlchemy default marker out of.
122
+
123
+ `sqlmodel/__init__.py` re-exports `Computed` / `Sequence` / `Identity` / `FetchedValue`
124
+ straight from `sqlalchemy.schema` (`from sqlalchemy.schema import X as X`), so
125
+ `from sqlmodel import Sequence` binds the very same class.
126
+ """
127
+ if not module:
128
+ return False
129
+ return module.split(".", 1)[0] in {"sqlalchemy", "sqlmodel"}
130
+
131
+
132
+ def _column_args_are_defaultish(
133
+ args: Iterable[tuple[str | None, _ExprT]],
134
+ *,
135
+ is_marker_call: Callable[[_ExprT], bool],
136
+ is_true: Callable[[_ExprT], bool],
137
+ is_none: Callable[[_ExprT], bool],
138
+ ) -> bool:
139
+ """Shared `Column(...)` rule: does this column get its value without the caller supplying one?
140
+
141
+ `args` is the argument list normalised to `(keyword_name_or_None, value)` pairs, so the same
142
+ rule can be driven both from raw `ast` nodes (`Annotated[...]` metadata) and from mypy
143
+ `Expression` nodes (`x: T = Field(...)`), and from `sa_column_kwargs={...}` dicts (which only
144
+ ever produce keyword entries).
145
+
146
+ Deliberately *not* covered: a bare `primary_key=True`. SQLAlchemy's default
147
+ `autoincrement="auto"` only generates a value for a *single-column integer* primary key, and
148
+ neither the column type nor the rest of the table's primary key is knowable from one
149
+ `Column(...)` call, so composite or non-integer primary keys would silently become optional.
150
+ An explicit `autoincrement=True` *on a primary key* is different: it is the author stating that
151
+ the database generates the value, it renders as SERIAL/IDENTITY, and SQLAlchemy rejects it at
152
+ DDL time on an incompatible column type.
153
+ """
154
+ saw_autoincrement_true = False
155
+ saw_primary_key_true = False
156
+
157
+ for arg_name, arg in args:
158
+ if arg_name is None:
159
+ # Positional schema constructs, e.g. `Column(BigInteger, Sequence("s"))`.
160
+ if is_marker_call(arg):
161
+ return True
162
+ continue
163
+
164
+ if arg_name == "nullable" and is_true(arg):
165
+ return True
166
+ if arg_name in SQLALCHEMY_DEFAULTISH_COLUMN_KWARGS and not is_none(arg):
167
+ return True
168
+ if arg_name == "autoincrement" and is_true(arg):
169
+ saw_autoincrement_true = True
170
+ elif arg_name == "primary_key" and is_true(arg):
171
+ saw_primary_key_true = True
172
+
173
+ return saw_autoincrement_true and saw_primary_key_true
174
+
175
+
176
+ def _ast_dotted_name(expr: ast.expr) -> str | None:
177
+ """Flatten `a.b.C` into `"a.b.C"`; return None for anything that is not a plain dotted name."""
178
+ if isinstance(expr, ast.Name):
179
+ return expr.id
180
+ if isinstance(expr, ast.Attribute):
181
+ base = _ast_dotted_name(expr.value)
182
+ return None if base is None else f"{base}.{expr.attr}"
183
+ return None
184
+
185
+
186
+ def _collect_sqlalchemy_marker_bindings(tree: ast.Module) -> tuple[set[str], set[str]]:
187
+ """Return `(marker names, module aliases)` bound by the module's imports.
188
+
189
+ Import-aware on purpose: `Sequence` is also `typing.Sequence` / `collections.abc.Sequence`, so
190
+ matching the bare name would misread `x: Sequence[int]`-style code as a SQLAlchemy sequence.
191
+ Names we cannot trace back to a `sqlalchemy` / `sqlmodel` import are not treated as markers.
192
+ """
193
+ marker_names: set[str] = set()
194
+ module_aliases: set[str] = set()
195
+
196
+ for node in ast.walk(tree):
197
+ if isinstance(node, ast.Import):
198
+ for alias in node.names:
199
+ if not _is_sqlalchemy_marker_source_module(alias.name):
200
+ continue
201
+ # `import sqlalchemy.orm` binds the top-level package name.
202
+ module_aliases.add(alias.asname or alias.name.split(".", 1)[0])
203
+ elif isinstance(node, ast.ImportFrom):
204
+ if node.level or not _is_sqlalchemy_marker_source_module(node.module):
205
+ continue
206
+ for alias in node.names:
207
+ if alias.name in SQLALCHEMY_COLUMN_DEFAULT_MARKERS:
208
+ marker_names.add(alias.asname or alias.name)
209
+
210
+ return marker_names, module_aliases
211
+
212
+
97
213
  def _parse_annotated_field_metadata_by_line(source: str) -> dict[int, tuple[bool, list[str]]]:
98
214
  """Return mapping of line -> (has_default, aliases) for Annotated Field metadata.
99
215
 
@@ -103,6 +219,13 @@ def _parse_annotated_field_metadata_by_line(source: str) -> dict[int, tuple[bool
103
219
 
104
220
  Only handles class-body `AnnAssign` nodes with **no assignment value** (i.e. `x: ...`).
105
221
  """
222
+ out: dict[int, tuple[bool, list[str]]] = {}
223
+ try:
224
+ tree = ast.parse(source)
225
+ except SyntaxError:
226
+ return out
227
+
228
+ marker_names, sqlalchemy_module_aliases = _collect_sqlalchemy_marker_bindings(tree)
106
229
 
107
230
  def _call_func_name(expr: ast.expr) -> str | None:
108
231
  if isinstance(expr, ast.Name):
@@ -175,6 +298,20 @@ def _parse_annotated_field_metadata_by_line(source: str) -> dict[int, tuple[bool
175
298
  aliases.append(candidate)
176
299
  return aliases
177
300
 
301
+ def _is_marker_call(expr: ast.expr) -> bool:
302
+ if not isinstance(expr, ast.Call):
303
+ return False
304
+ name = _ast_dotted_name(expr.func)
305
+ if name is None:
306
+ return False
307
+ head, _, attr = name.rpartition(".")
308
+ if not head:
309
+ return name in marker_names
310
+ return (
311
+ attr in SQLALCHEMY_COLUMN_DEFAULT_MARKERS
312
+ and head.split(".", 1)[0] in sqlalchemy_module_aliases
313
+ )
314
+
178
315
  def _column_expr_is_defaultish(expr: ast.expr) -> bool:
179
316
  if not isinstance(expr, ast.Call):
180
317
  return False
@@ -183,32 +320,29 @@ def _parse_annotated_field_metadata_by_line(source: str) -> dict[int, tuple[bool
183
320
  if callee_name is not None and not callee_name.endswith("Column"):
184
321
  return False
185
322
 
186
- for arg in expr.args:
187
- if isinstance(arg, ast.Call):
188
- computed_name = _call_func_name(arg.func)
189
- if computed_name is not None and computed_name.endswith("Computed"):
190
- return True
191
-
192
- for kw in expr.keywords:
193
- if kw.arg == "nullable" and _is_true(kw.value):
194
- return True
195
- if kw.arg in {"server_default", "default", "insert_default"} and not _is_none(kw.value):
196
- return True
197
- return False
323
+ args: list[tuple[str | None, ast.expr]] = [(None, arg) for arg in expr.args]
324
+ args += [(kw.arg, kw.value) for kw in expr.keywords if kw.arg is not None]
325
+ return _column_args_are_defaultish(
326
+ args,
327
+ is_marker_call=_is_marker_call,
328
+ is_true=_is_true,
329
+ is_none=_is_none,
330
+ )
198
331
 
199
332
  def _sa_column_kwargs_are_defaultish(expr: ast.expr) -> bool:
200
333
  if not isinstance(expr, ast.Dict):
201
334
  return False
202
- for key_expr, val_expr in zip(expr.keys, expr.values, strict=True):
203
- if not (isinstance(key_expr, ast.Constant) and isinstance(key_expr.value, str)):
204
- continue
205
- if key_expr.value == "nullable" and _is_true(val_expr):
206
- return True
207
- if key_expr.value in {"server_default", "default", "insert_default"} and not _is_none(
208
- val_expr
209
- ):
210
- return True
211
- return False
335
+ items: list[tuple[str | None, ast.expr]] = [
336
+ (key_expr.value, val_expr)
337
+ for key_expr, val_expr in zip(expr.keys, expr.values, strict=True)
338
+ if isinstance(key_expr, ast.Constant) and isinstance(key_expr.value, str)
339
+ ]
340
+ return _column_args_are_defaultish(
341
+ items,
342
+ is_marker_call=_is_marker_call,
343
+ is_true=_is_true,
344
+ is_none=_is_none,
345
+ )
212
346
 
213
347
  def _field_has_default_from_call(call: ast.Call) -> bool:
214
348
  # Explicit default always wins.
@@ -263,12 +397,6 @@ def _parse_annotated_field_metadata_by_line(source: str) -> dict[int, tuple[bool
263
397
 
264
398
  return None
265
399
 
266
- out: dict[int, tuple[bool, list[str]]] = {}
267
- try:
268
- tree = ast.parse(source)
269
- except SyntaxError:
270
- return out
271
-
272
400
  class _Visitor(ast.NodeVisitor):
273
401
  def __init__(self) -> None:
274
402
  self.class_depth = 0
@@ -717,10 +845,8 @@ class SQLModelTransformer:
717
845
  def _column_expr_is_defaultish(value: Expression) -> bool:
718
846
  """Best-effort detection of SQLAlchemy-generated/defaultable columns.
719
847
 
720
- We treat these as "optional" constructor kwargs:
721
- - nullable=True
722
- - server_default / default / insert_default
723
- - Computed(...) columns
848
+ Shares `_column_args_are_defaultish` with the `Annotated[...]` `ast` reader; see
849
+ that function for what counts as "the caller need not supply this value".
724
850
  """
725
851
  if not isinstance(value, CallExpr):
726
852
  return False
@@ -730,38 +856,28 @@ class SQLModelTransformer:
730
856
  # Be conservative: avoid interpreting non-Column calls.
731
857
  return False
732
858
 
733
- for c_arg, c_arg_name in zip(value.args, value.arg_names, strict=True):
734
- # Positional Computed(...) (common in declarative).
735
- if c_arg_name is None and isinstance(c_arg, CallExpr):
736
- computed_fullname = _callee_fullname(c_arg)
737
- if computed_fullname is not None and computed_fullname.endswith(
738
- ".Computed"
739
- ):
740
- return True
741
-
742
- # Keyword hints.
743
- if c_arg_name == "nullable" and _is_bool_nameexpr(c_arg, True):
744
- return True
745
- if c_arg_name in {"server_default", "default", "insert_default"}:
746
- if not _is_none_expr(c_arg):
747
- return True
748
-
749
- return False
859
+ return _column_args_are_defaultish(
860
+ zip(value.arg_names, value.args, strict=True),
861
+ is_marker_call=_is_sqlalchemy_marker_call,
862
+ is_true=lambda e: _is_bool_nameexpr(e, True),
863
+ is_none=_is_none_expr,
864
+ )
750
865
 
751
866
  def _sa_column_kwargs_are_defaultish(value: Expression) -> bool:
752
867
  """Best-effort for `Field(sa_column_kwargs={...})`."""
753
868
  if not isinstance(value, DictExpr):
754
869
  return False
755
870
 
756
- for key_expr, val_expr in value.items:
757
- if not isinstance(key_expr, StrExpr):
758
- continue
759
- if key_expr.value == "nullable" and _is_bool_nameexpr(val_expr, True):
760
- return True
761
- if key_expr.value in {"server_default", "default", "insert_default"}:
762
- if not _is_none_expr(val_expr):
763
- return True
764
- return False
871
+ return _column_args_are_defaultish(
872
+ [
873
+ (key_expr.value, val_expr)
874
+ for key_expr, val_expr in value.items
875
+ if isinstance(key_expr, StrExpr)
876
+ ],
877
+ is_marker_call=_is_sqlalchemy_marker_call,
878
+ is_true=lambda e: _is_bool_nameexpr(e, True),
879
+ is_none=_is_none_expr,
880
+ )
765
881
 
766
882
  saw_nullable_true = False
767
883
  sa_column_expr: Expression | None = None
@@ -1057,6 +1173,21 @@ def _callee_fullname(call: CallExpr) -> str | None:
1057
1173
  return None
1058
1174
 
1059
1175
 
1176
+ def _is_sqlalchemy_marker_call(value: Expression) -> bool:
1177
+ """True for a call to a SQLAlchemy schema construct that installs a column default.
1178
+
1179
+ Matching is by resolved fullname, so `Sequence` imported from `typing` /
1180
+ `collections.abc` does not qualify. An unresolved callee is not treated as a marker.
1181
+ """
1182
+ if not isinstance(value, CallExpr):
1183
+ return False
1184
+ fullname = _callee_fullname(value)
1185
+ if fullname is None:
1186
+ return False
1187
+ module, _, attr = fullname.rpartition(".")
1188
+ return attr in SQLALCHEMY_COLUMN_DEFAULT_MARKERS and _is_sqlalchemy_module(module)
1189
+
1190
+
1060
1191
  def _unwrap_mapped_type(typ: Type) -> Type:
1061
1192
  """Convert SQLAlchemy `Mapped[T]` annotations to `T` for constructor signatures.
1062
1193
 
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy import (
4
+ BigInteger,
5
+ Column,
6
+ Computed,
7
+ FetchedValue,
8
+ Identity,
9
+ Integer,
10
+ Sequence,
11
+ String,
12
+ text,
13
+ )
14
+ from sqlmodel import Field, SQLModel
15
+
16
+
17
+ class Model(SQLModel, table=True):
18
+ id: int | None = Field(default=None, primary_key=True)
19
+
20
+ # Required field (no default, non-nullable).
21
+ required: str = Field()
22
+
23
+ # Nullable fields without explicit defaults should be optional constructor kwargs.
24
+ note: str = Field(nullable=True)
25
+
26
+ optional_sa: str = Field(sa_column=Column("optional_sa", String(), nullable=True))
27
+
28
+ search_vector: str | None = Field(
29
+ sa_column=Column("search_vector", String(), Computed("1", persisted=True))
30
+ )
31
+
32
+ created_by: str = Field(sa_column=Column("created_by", String(), server_default=text("'x'")))
33
+
34
+
35
+ Model()
36
+ Model(required="x")
37
+
38
+
39
+ class GeneratedDefaults(SQLModel, table=True):
40
+ id: int | None = Field(default=None, primary_key=True)
41
+
42
+ required: str = Field()
43
+
44
+ # Positional `Sequence(...)` becomes `Column.default`, exactly like `default=Sequence(...)`.
45
+ seq_number: int = Field(
46
+ sa_column=Column(
47
+ "seq_number",
48
+ BigInteger,
49
+ Sequence("generated_defaults_seq_number_seq", start=1, increment=1),
50
+ index=True,
51
+ nullable=False,
52
+ )
53
+ )
54
+
55
+ # Positional `Identity(...)` becomes `Column.server_default` (GENERATED ... AS IDENTITY).
56
+ identity_number: int = Field(
57
+ sa_column=Column("identity_number", Integer, Identity(always=True))
58
+ )
59
+
60
+ # Positional `FetchedValue()` becomes `Column.server_default`.
61
+ fetched: str = Field(sa_column=Column("fetched", String(), FetchedValue()))
62
+
63
+ fetched_kw: str = Field(sa_column=Column("fetched_kw", String(), server_default=FetchedValue()))
64
+
65
+ inserted: str = Field(sa_column=Column("inserted", String(), insert_default="x"))
66
+
67
+
68
+ GeneratedDefaults()
69
+ GeneratedDefaults(required="x")
70
+
71
+
72
+ class AutoIncrementPk(SQLModel, table=True):
73
+ # `autoincrement=True` on a primary key renders as SERIAL/IDENTITY: value comes from the DB.
74
+ auto_pk: int = Field(sa_column=Column("auto_pk", Integer, primary_key=True, autoincrement=True))
75
+
76
+
77
+ AutoIncrementPk()
78
+
79
+
80
+ class ManualPk(SQLModel, table=True):
81
+ # A bare `primary_key=True` says nothing about who supplies the value.
82
+ manual_pk: int = Field(sa_column=Column("manual_pk", Integer, primary_key=True))
83
+
84
+ # `autoincrement=True` off a primary key is ignored by SQLAlchemy (plain INTEGER column).
85
+ counter: int = Field(sa_column=Column("counter", Integer, autoincrement=True))
86
+
87
+
88
+ ManualPk()
89
+ ManualPk(manual_pk=1, counter=0)
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+
5
+ import sqlalchemy as sa
6
+ from sqlalchemy import JSON, BigInteger, Column
7
+ from sqlmodel import Field, SQLModel
8
+
9
+
10
+ class Order(SQLModel, table=True):
11
+ id: int | None = Field(default=None, primary_key=True)
12
+
13
+ # SQLAlchemy's `Sequence`, reached through a module alias: database-generated -> optional.
14
+ number: int = Field(
15
+ sa_column=Column(
16
+ "number",
17
+ BigInteger,
18
+ sa.Sequence("example_number_seq", start=1, increment=1),
19
+ index=True,
20
+ nullable=False,
21
+ )
22
+ )
23
+
24
+ # The bare name `Sequence` in this module is `collections.abc.Sequence`, not SQLAlchemy's.
25
+ # It must not turn the field into an optional constructor kwarg.
26
+ tags: Sequence[str] = Field(sa_column=Column("tags", JSON, nullable=False))
27
+
28
+
29
+ Order()
30
+ Order(tags=["a"])
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlmodel import BigInteger, Column, Field, Sequence, SQLModel
4
+
5
+
6
+ class Order(SQLModel, table=True):
7
+ id: int | None = Field(default=None, primary_key=True)
8
+
9
+ # `sqlmodel.Sequence` is `sqlalchemy.schema.Sequence` re-exported; passed positionally it
10
+ # becomes `Column.default`, so the database assigns the value.
11
+ number: int = Field(
12
+ sa_column=Column(
13
+ BigInteger,
14
+ Sequence("example_number_seq", start=1, increment=1),
15
+ index=True,
16
+ nullable=False,
17
+ )
18
+ )
19
+
20
+ title: str = Field()
21
+
22
+
23
+ Order()
24
+ Order(title="x")