Skip to content

Commit 58d4eec

Browse files
committed
Merge branch 'main' into fk-attrs
2 parents 5b3ff03 + dd51746 commit 58d4eec

File tree

14 files changed

+418
-124
lines changed

14 files changed

+418
-124
lines changed

.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
@@ -6,6 +6,7 @@
66
import weakref
77
from collections.abc import Mapping, Sequence, Set
88
from copy import copy
9+
from dataclasses import dataclass
910
from datetime import date, datetime, time, timedelta
1011
from decimal import Decimal
1112
from enum import Enum
@@ -15,6 +16,7 @@
1516
Any,
1617
Callable,
1718
ClassVar,
19+
Literal,
1820
Optional,
1921
TypeVar,
2022
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,
@@ -206,6 +208,38 @@ def __init__(
206208
self.sa_relationship_kwargs = sa_relationship_kwargs
207209

208210

211+
@dataclass
212+
class FieldInfoMetadata:
213+
primary_key: Union[bool, UndefinedType] = Undefined
214+
nullable: Union[bool, UndefinedType] = Undefined
215+
foreign_key: Any = Undefined
216+
ondelete: Union[OnDeleteType, UndefinedType] = Undefined
217+
unique: Union[bool, UndefinedType] = Undefined
218+
index: Union[bool, UndefinedType] = Undefined
219+
sa_type: Union[type[Any], UndefinedType] = Undefined
220+
sa_column: Union[Column[Any], UndefinedType] = Undefined
221+
sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined
222+
sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined
223+
224+
225+
def _get_sqlmodel_field_metadata(field_info: Any) -> Optional[FieldInfoMetadata]:
226+
metadata_items = getattr(field_info, "metadata", None)
227+
if metadata_items:
228+
for meta in metadata_items:
229+
if isinstance(meta, FieldInfoMetadata):
230+
return meta
231+
return None
232+
233+
234+
def _get_sqlmodel_field_value(
235+
field_info: Any, attribute: str, default: Any = Undefined
236+
) -> Any:
237+
metadata = _get_sqlmodel_field_metadata(field_info)
238+
if metadata is not None and hasattr(metadata, attribute):
239+
return getattr(metadata, attribute)
240+
return getattr(field_info, attribute, default)
241+
242+
209243
# include sa_type, sa_column_args, sa_column_kwargs
210244
@overload
211245
def Field(
@@ -429,6 +463,20 @@ def Field(
429463
default_factory=default_factory,
430464
**field_info_kwargs,
431465
)
466+
field_metadata = FieldInfoMetadata(
467+
primary_key=primary_key,
468+
nullable=nullable,
469+
foreign_key=foreign_key,
470+
ondelete=ondelete,
471+
unique=unique,
472+
index=index,
473+
sa_type=sa_type,
474+
sa_column=sa_column,
475+
sa_column_args=sa_column_args,
476+
sa_column_kwargs=sa_column_kwargs,
477+
)
478+
if hasattr(field_info, "metadata"):
479+
field_info.metadata.append(field_metadata)
432480
return field_info
433481

434482

@@ -643,7 +691,7 @@ def __init__(
643691

644692
def get_sqlalchemy_type(field: Any) -> Any:
645693
field_info = field
646-
sa_type = getattr(field_info, "sa_type", Undefined) # noqa: B009
694+
sa_type = _get_sqlmodel_field_value(field_info, "sa_type", Undefined) # noqa: B009
647695
if sa_type is not Undefined:
648696
return sa_type
649697

@@ -697,40 +745,40 @@ def get_sqlalchemy_type(field: Any) -> Any:
697745

698746
def get_column_from_field(field: Any) -> Column: # type: ignore
699747
field_info = field
700-
sa_column = getattr(field_info, "sa_column", Undefined)
748+
sa_column = _get_sqlmodel_field_value(field_info, "sa_column", Undefined)
701749
if isinstance(sa_column, Column):
702750
return sa_column
703751
sa_type = get_sqlalchemy_type(field)
704-
primary_key = getattr(field_info, "primary_key", Undefined)
752+
primary_key = _get_sqlmodel_field_value(field_info, "primary_key", Undefined)
705753
if primary_key is Undefined:
706754
primary_key = False
707-
index = getattr(field_info, "index", Undefined)
755+
index = _get_sqlmodel_field_value(field_info, "index", Undefined)
708756
if index is Undefined:
709757
index = False
710758
nullable = not primary_key and is_field_noneable(field)
711759
# Override derived nullability if the nullable property is set explicitly
712760
# on the field
713-
field_nullable = getattr(field_info, "nullable", Undefined) # noqa: B009
761+
field_nullable = _get_sqlmodel_field_value(field_info, "nullable", Undefined)
714762
if field_nullable is not Undefined:
715763
assert not isinstance(field_nullable, UndefinedType)
716764
nullable = field_nullable
717765
args = []
718-
foreign_key = getattr(field_info, "foreign_key", Undefined)
766+
foreign_key = _get_sqlmodel_field_value(field_info, "foreign_key", Undefined)
719767
if foreign_key is Undefined:
720768
foreign_key = None
721-
unique = getattr(field_info, "unique", Undefined)
769+
unique = _get_sqlmodel_field_value(field_info, "unique", Undefined)
722770
if unique is Undefined:
723771
unique = False
724772
if foreign_key:
725773
if isinstance(foreign_key, str):
726-
if field_info.ondelete == "SET NULL" and not nullable:
774+
ondelete_value = _get_sqlmodel_field_value(field_info, "ondelete", Undefined)
775+
if ondelete_value is Undefined:
776+
ondelete_value = None
777+
if ondelete_value == "SET NULL" and not nullable:
727778
raise RuntimeError('ondelete="SET NULL" requires nullable=True')
728779
assert isinstance(foreign_key, str)
729-
ondelete = getattr(field_info, "ondelete", Undefined)
730-
if ondelete is Undefined:
731-
ondelete = None
732-
assert isinstance(ondelete, (str, type(None))) # for typing
733-
args.append(ForeignKey(foreign_key, ondelete=ondelete))
780+
assert isinstance(ondelete_value, (str, type(None))) # for typing
781+
args.append(ForeignKey(foreign_key, ondelete=ondelete_value))
734782
else:
735783
assert isinstance(foreign_key, ForeignKey)
736784
args.append(copy(foreign_key))
@@ -747,14 +795,16 @@ def get_column_from_field(field: Any) -> Column: # type: ignore
747795
sa_default = field_info.default
748796
if sa_default is not Undefined:
749797
kwargs["default"] = sa_default
750-
sa_column_args = getattr(field_info, "sa_column_args", Undefined)
798+
sa_column_args = _get_sqlmodel_field_value(field_info, "sa_column_args", Undefined)
751799
if sa_column_args is not Undefined:
752800
for arg_v in list(cast(Sequence[Any], sa_column_args)):
753801
if isinstance(arg_v, ForeignKey):
754802
args.append(copy(arg_v))
755803
else:
756804
args.append(arg_v)
757-
sa_column_kwargs = getattr(field_info, "sa_column_kwargs", Undefined)
805+
sa_column_kwargs = _get_sqlmodel_field_value(
806+
field_info, "sa_column_kwargs", Undefined
807+
)
758808
if sa_column_kwargs is not Undefined:
759809
kwargs.update(cast(dict[Any, Any], sa_column_kwargs))
760810
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
@@ -17,6 +17,17 @@ class Item(SQLModel, table=True):
1717
assert isinstance(Item.id.type, String) # type: ignore
1818

1919

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

0 commit comments

Comments
 (0)