Skip to content

Commit 1adb815

Browse files
committed
Merge branch 'main' into feature/mapped_column
2 parents 66644fc + dd51746 commit 1adb815

14 files changed

Lines changed: 418 additions & 124 deletions

File tree

.github/labeler.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ internal:
1818
- .pre-commit-config.yaml
1919
- pdm_build.py
2020
- requirements*.txt
21+
- uv.lock
2122
- all-globs-to-all-files:
2223
- '!docs/**'
2324
- '!sqlmodel/**'

.github/workflows/issue-manager.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@ jobs:
4141
"message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR.",
4242
"reminder": {
4343
"before": "P3D",
44-
"message": "Heads-up: this will be closed in 3 days unless theres new activity."
44+
"message": "Heads-up: this will be closed in 3 days unless there's new activity."
4545
}
4646
},
4747
"invalid": {
4848
"delay": 0,
4949
"message": "This was marked as invalid and will be closed now. If this is an error, please provide additional details."
50+
},
51+
"maybe-ai": {
52+
"delay": 0,
53+
"message": "This was marked as potentially AI generated and will be closed now. If this is an error, please provide additional details, make sure to read the docs about contributing and AI."
5054
}
5155
}

docs/contributing.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,39 @@ This helps to make sure that:
158158
* The documentation is up-to-date.
159159
* The documentation examples can be run as is.
160160
* Most of the features are covered by the documentation, ensured by test coverage.
161+
162+
## Automated Code and AI
163+
164+
You are encouraged to use all the tools you want to do your work and contribute as efficiently as possible, this includes AI (LLM) tools, etc. Nevertheless, contributions should have meaningful human intervention, judgement, context, etc.
165+
166+
If the **human effort** put in a PR, e.g. writing LLM prompts, is **less** than the **effort we would need to put** to **review it**, please **don't** submit the PR.
167+
168+
Think of it this way: we can already write LLM prompts or run automated tools ourselves, and that would be faster than reviewing external PRs.
169+
170+
### Closing Automated and AI PRs
171+
172+
If we see PRs that seem AI generated or automated in similar ways, we'll flag them and close them.
173+
174+
The same applies to comments and descriptions, please don't copy paste the content generated by an LLM.
175+
176+
### Human Effort Denial of Service
177+
178+
Using automated tools and AI to submit PRs or comments that we have to carefully review and handle would be the equivalent of a <a href="https://en.wikipedia.org/wiki/Denial-of-service_attack" class="external-link" target="_blank">Denial-of-service attack</a> on our human effort.
179+
180+
It would be very little effort from the person submitting the PR (an LLM prompt) that generates a large amount of effort on our side (carefully reviewing code).
181+
182+
Please don't do that.
183+
184+
We'll need to block accounts that spam us with repeated automated PRs or comments.
185+
186+
### Use Tools Wisely
187+
188+
As Uncle Ben said:
189+
190+
<blockquote>
191+
With great <strike>power</strike> <strong>tools</strong> comes great responsibility.
192+
</blockquote>
193+
194+
Avoid inadvertently doing harm.
195+
196+
You have amazing tools at hand, use them wisely to help effectively.

docs/js/custom.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,11 @@ function setupTermynal() {
8181
}
8282
}
8383
saveBuffer();
84+
const inputCommands = useLines.filter(line => line.type === "input").map(line => line.value).join("\n");
85+
node.textContent = inputCommands;
8486
const div = document.createElement("div");
85-
node.replaceWith(div);
87+
node.style.display = "none";
88+
node.after(div);
8689
const termynal = new Termynal(div, {
8790
lineData: useLines,
8891
noInit: true,

docs/release-notes.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,31 @@
22

33
## Latest Changes
44

5+
## 0.0.32
6+
7+
### Fixes
8+
9+
* 🐛 Fix support for `Annotated` fields with Pydantic 2.12+. PR [#1607](https://github.com/fastapi/sqlmodel/pull/1607) by [@vimota](https://github.com/vimota).
10+
11+
### Refactors
12+
13+
* ♻️ Import `Literal` from the `typing` module directly. PR [#1699](https://github.com/fastapi/sqlmodel/pull/1699) by [@svlandeg](https://github.com/svlandeg).
14+
515
### Docs
616

17+
* 📝 Add contribution instructions about LLM generated code and comments and automated tools for PRs. PR [#1712](https://github.com/fastapi/sqlmodel/pull/1712) by [@alejsdev](https://github.com/alejsdev).
18+
* 🐛 Fix copy button in `custom.js`. PR [#1711](https://github.com/fastapi/sqlmodel/pull/1711) by [@alejsdev](https://github.com/alejsdev).
719
* 📝 Remove duplicated word in `read-relationships.md`. PR [#1705](https://github.com/fastapi/sqlmodel/pull/1705) by [@stefmolin](https://github.com/stefmolin).
820

921
### Internal
1022

23+
* ⬆ Bump ruff from 0.14.13 to 0.14.14. PR [#1721](https://github.com/fastapi/sqlmodel/pull/1721) by [@dependabot[bot]](https://github.com/apps/dependabot).
24+
* ⬆ Bump prek from 0.2.30 to 0.3.0. PR [#1720](https://github.com/fastapi/sqlmodel/pull/1720) by [@dependabot[bot]](https://github.com/apps/dependabot).
25+
* 🔧 Ensure that an edit to `uv.lock` gets the `internal` label. PR [#1719](https://github.com/fastapi/sqlmodel/pull/1719) by [@svlandeg](https://github.com/svlandeg).
26+
* ⬆ Bump sqlalchemy from 2.0.45 to 2.0.46. PR [#1717](https://github.com/fastapi/sqlmodel/pull/1717) by [@dependabot[bot]](https://github.com/apps/dependabot).
27+
* ⬆ Bump typer from 0.21.0 to 0.21.1. PR [#1715](https://github.com/fastapi/sqlmodel/pull/1715) by [@dependabot[bot]](https://github.com/apps/dependabot).
28+
* ⬆ Bump ruff from 0.14.10 to 0.14.13. PR [#1714](https://github.com/fastapi/sqlmodel/pull/1714) by [@dependabot[bot]](https://github.com/apps/dependabot).
29+
* ⬆ Bump prek from 0.2.25 to 0.2.30. PR [#1716](https://github.com/fastapi/sqlmodel/pull/1716) by [@dependabot[bot]](https://github.com/apps/dependabot).
1130
* ⬆️ Update FastAPI version pin to `>=0.103.2` in tests. PR [#1709](https://github.com/fastapi/sqlmodel/pull/1709) by [@YuriiMotov](https://github.com/YuriiMotov).
1231
* 📌 Pin development Python version to 3.10, for `deploy_docs_status.py`. PR [#1707](https://github.com/fastapi/sqlmodel/pull/1707) by [@tiangolo](https://github.com/tiangolo).
1332
* ⬆️ Migrate to uv. PR [#1688](https://github.com/fastapi/sqlmodel/pull/1688) by [@DoctorJohn](https://github.com/DoctorJohn).

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ docs = [
6666
"mkdocstrings[python]==0.30.1",
6767
"pillow==11.3.0",
6868
"pyyaml>=5.3.1,<7.0.0",
69-
"typer==0.21.0",
69+
"typer==0.21.1",
7070
]
7171
github-actions = [
7272
"httpx>=0.27.0,<0.29.0",
@@ -85,7 +85,7 @@ tests = [
8585
"mypy==1.19.1",
8686
"pre-commit>=2.17.0,<5.0.0",
8787
"pytest>=7.0.1,<9.0.0",
88-
"ruff==0.14.10",
88+
"ruff==0.14.14",
8989
"typing-extensions==4.15.0",
9090
]
9191

sqlmodel/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.0.31"
1+
__version__ = "0.0.32"
22

33
# Re-export from SQLAlchemy
44
from sqlalchemy.engine import create_engine as create_engine

sqlmodel/main.py

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import uuid
66
import weakref
77
from collections.abc import Mapping, Sequence, Set
8+
from dataclasses import dataclass
89
from datetime import date, datetime, time, timedelta
910
from decimal import Decimal
1011
from enum import Enum
@@ -14,6 +15,7 @@
1415
Any,
1516
Callable,
1617
ClassVar,
18+
Literal,
1719
Optional,
1820
TypeVar,
1921
Union,
@@ -49,7 +51,7 @@
4951
from sqlalchemy.orm.instrumentation import is_instrumented
5052
from sqlalchemy.sql.schema import MetaData
5153
from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid
52-
from typing_extensions import Literal, TypeAlias, deprecated, get_origin
54+
from typing_extensions import TypeAlias, deprecated, get_origin
5355

5456
from ._compat import ( # type: ignore[attr-defined]
5557
PYDANTIC_MINOR_VERSION,
@@ -200,6 +202,38 @@ def __init__(
200202
self.sa_relationship_kwargs = sa_relationship_kwargs
201203

202204

205+
@dataclass
206+
class FieldInfoMetadata:
207+
primary_key: Union[bool, UndefinedType] = Undefined
208+
nullable: Union[bool, UndefinedType] = Undefined
209+
foreign_key: Any = Undefined
210+
ondelete: Union[OnDeleteType, UndefinedType] = Undefined
211+
unique: Union[bool, UndefinedType] = Undefined
212+
index: Union[bool, UndefinedType] = Undefined
213+
sa_type: Union[type[Any], UndefinedType] = Undefined
214+
sa_column: Union[Column[Any], UndefinedType] = Undefined
215+
sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined
216+
sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined
217+
218+
219+
def _get_sqlmodel_field_metadata(field_info: Any) -> Optional[FieldInfoMetadata]:
220+
metadata_items = getattr(field_info, "metadata", None)
221+
if metadata_items:
222+
for meta in metadata_items:
223+
if isinstance(meta, FieldInfoMetadata):
224+
return meta
225+
return None
226+
227+
228+
def _get_sqlmodel_field_value(
229+
field_info: Any, attribute: str, default: Any = Undefined
230+
) -> Any:
231+
metadata = _get_sqlmodel_field_metadata(field_info)
232+
if metadata is not None and hasattr(metadata, attribute):
233+
return getattr(metadata, attribute)
234+
return getattr(field_info, attribute, default)
235+
236+
203237
# include sa_type, sa_column_args, sa_column_kwargs
204238
@overload
205239
def Field(
@@ -423,6 +457,20 @@ def Field(
423457
default_factory=default_factory,
424458
**field_info_kwargs,
425459
)
460+
field_metadata = FieldInfoMetadata(
461+
primary_key=primary_key,
462+
nullable=nullable,
463+
foreign_key=foreign_key,
464+
ondelete=ondelete,
465+
unique=unique,
466+
index=index,
467+
sa_type=sa_type,
468+
sa_column=sa_column,
469+
sa_column_args=sa_column_args,
470+
sa_column_kwargs=sa_column_kwargs,
471+
)
472+
if hasattr(field_info, "metadata"):
473+
field_info.metadata.append(field_metadata)
426474
return field_info
427475

428476

@@ -637,7 +685,7 @@ def __init__(
637685

638686
def get_sqlalchemy_type(field: Any) -> Any:
639687
field_info = field
640-
sa_type = getattr(field_info, "sa_type", Undefined) # noqa: B009
688+
sa_type = _get_sqlmodel_field_value(field_info, "sa_type", Undefined) # noqa: B009
641689
if sa_type is not Undefined:
642690
return sa_type
643691

@@ -691,39 +739,39 @@ def get_sqlalchemy_type(field: Any) -> Any:
691739

692740
def get_column_from_field(field: Any) -> Union[Column, MappedColumn]: # type: ignore
693741
field_info = field
694-
sa_column = getattr(field_info, "sa_column", Undefined)
742+
sa_column = _get_sqlmodel_field_value(field_info, "sa_column", Undefined)
695743
if isinstance(sa_column, (Column, MappedColumn)):
696744
return sa_column
697745
sa_type = get_sqlalchemy_type(field)
698-
primary_key = getattr(field_info, "primary_key", Undefined)
746+
primary_key = _get_sqlmodel_field_value(field_info, "primary_key", Undefined)
699747
if primary_key is Undefined:
700748
primary_key = False
701-
index = getattr(field_info, "index", Undefined)
749+
index = _get_sqlmodel_field_value(field_info, "index", Undefined)
702750
if index is Undefined:
703751
index = False
704752
nullable = not primary_key and is_field_noneable(field)
705753
# Override derived nullability if the nullable property is set explicitly
706754
# on the field
707-
field_nullable = getattr(field_info, "nullable", Undefined) # noqa: B009
755+
field_nullable = _get_sqlmodel_field_value(field_info, "nullable", Undefined)
708756
if field_nullable is not Undefined:
709757
assert not isinstance(field_nullable, UndefinedType)
710758
nullable = field_nullable
711759
args = []
712-
foreign_key = getattr(field_info, "foreign_key", Undefined)
760+
foreign_key = _get_sqlmodel_field_value(field_info, "foreign_key", Undefined)
713761
if foreign_key is Undefined:
714762
foreign_key = None
715-
unique = getattr(field_info, "unique", Undefined)
763+
unique = _get_sqlmodel_field_value(field_info, "unique", Undefined)
716764
if unique is Undefined:
717765
unique = False
718766
if foreign_key:
719-
if field_info.ondelete == "SET NULL" and not nullable:
767+
ondelete_value = _get_sqlmodel_field_value(field_info, "ondelete", Undefined)
768+
if ondelete_value is Undefined:
769+
ondelete_value = None
770+
if ondelete_value == "SET NULL" and not nullable:
720771
raise RuntimeError('ondelete="SET NULL" requires nullable=True')
721772
assert isinstance(foreign_key, str)
722-
ondelete = getattr(field_info, "ondelete", Undefined)
723-
if ondelete is Undefined:
724-
ondelete = None
725-
assert isinstance(ondelete, (str, type(None))) # for typing
726-
args.append(ForeignKey(foreign_key, ondelete=ondelete))
773+
assert isinstance(ondelete_value, (str, type(None))) # for typing
774+
args.append(ForeignKey(foreign_key, ondelete=ondelete_value))
727775
kwargs = {
728776
"primary_key": primary_key,
729777
"nullable": nullable,
@@ -737,10 +785,12 @@ def get_column_from_field(field: Any) -> Union[Column, MappedColumn]: # type: i
737785
sa_default = field_info.default
738786
if sa_default is not Undefined:
739787
kwargs["default"] = sa_default
740-
sa_column_args = getattr(field_info, "sa_column_args", Undefined)
788+
sa_column_args = _get_sqlmodel_field_value(field_info, "sa_column_args", Undefined)
741789
if sa_column_args is not Undefined:
742790
args.extend(list(cast(Sequence[Any], sa_column_args)))
743-
sa_column_kwargs = getattr(field_info, "sa_column_kwargs", Undefined)
791+
sa_column_kwargs = _get_sqlmodel_field_value(
792+
field_info, "sa_column_kwargs", Undefined
793+
)
744794
if sa_column_kwargs is not Undefined:
745795
kwargs.update(cast(dict[Any, Any], sa_column_kwargs))
746796
return Column(sa_type, *args, **kwargs) # type: ignore

sqlmodel/sql/expression.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from collections.abc import Iterable, Mapping, Sequence
22
from typing import (
33
Any,
4+
Literal,
45
Optional,
56
TypeVar,
67
Union,
@@ -34,7 +35,6 @@
3435
UnaryExpression,
3536
)
3637
from sqlalchemy.sql.type_api import TypeEngine
37-
from typing_extensions import Literal
3838

3939
from ._expression_select_cls import Select as Select
4040
from ._expression_select_cls import SelectOfScalar as SelectOfScalar

tests/test_field_sa_column.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Annotated, Optional
22

33
import pytest
44
from sqlalchemy import Column, Integer, String
@@ -19,6 +19,17 @@ class Item(SQLModel, table=True):
1919
assert isinstance(Item.id.type, String) # type: ignore
2020

2121

22+
def test_sa_column_with_annotated_metadata() -> None:
23+
class Item(SQLModel, table=True):
24+
id: Annotated[Optional[int], "meta"] = Field(
25+
default=None,
26+
sa_column=Column(String, primary_key=True, nullable=False),
27+
)
28+
29+
assert Item.id.nullable is False # type: ignore
30+
assert isinstance(Item.id.type, String) # type: ignore
31+
32+
2233
@pytest.mark.parametrize("column_class", [Column, mapped_column])
2334
def test_sa_column_no_sa_args(column_class) -> None:
2435
with pytest.raises(RuntimeError):

0 commit comments

Comments
 (0)