Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
database: mysql
database-url: mysql+pymysql://sqlmodel:sqlmodel@127.0.0.1:13306/sqlmodel_test
- os: ubuntu-latest
python-version: "3.10"
python-version: "3.11"
uv-resolution: lowest-direct
database: mysql
database-url: mysql+pymysql://sqlmodel:sqlmodel@127.0.0.1:13306/sqlmodel_test
Expand Down
10 changes: 4 additions & 6 deletions docs/advanced/datetime.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,11 @@ Let's say that each event in our application has a `created_at` field.

We can declare it with `datetime` and use `default_factory` to generate the current time in UTC:

{* ./docs_src/advanced/datetime/tutorial001_py311.py ln[1:8] hl[1,8] *}
{* ./docs_src/advanced/datetime/tutorial001.py ln[1:8] hl[1,8] *}

/// tip

The [`UTC` constant](https://docs.python.org/3/library/datetime.html#datetime.UTC) is available on Python 3.11 and newer. It is an alias for `timezone.utc`.

For Python 3.10, expand **Other versions and variants** below the example. That version imports `timezone` and uses `datetime.now(timezone.utc)` instead.
The [`UTC` constant](https://docs.python.org/3/library/datetime.html#datetime.UTC) is an alias for `timezone.utc`.

///

Expand All @@ -39,15 +37,15 @@ Passing `datetime.now(UTC)` directly would call it immediately, when defining th

We can create an event without passing a value for `created_at`:

{* ./docs_src/advanced/datetime/tutorial001_py311.py ln[17:19] hl[18] *}
{* ./docs_src/advanced/datetime/tutorial001.py ln[17:19] hl[18] *}

For the database column, **SQLModel** uses `UTCDateTime`, a custom SQLAlchemy type based on `DateTime(timezone=True)`. It uses native timezone support when the database provides it.

### Write and Read Datetimes

Let's save the event to `database.db` and read it back using SQLite:

{* ./docs_src/advanced/datetime/tutorial001_py311.py ln[11:14,21:26] hl[23:25] *}
{* ./docs_src/advanced/datetime/tutorial001.py ln[11:14,21:26] hl[23:25] *}

The datetime goes through these steps:

Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial/fastapi/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ Now we can run the tests with `pytest` and see the results:
$ uv run pytest

============= test session starts ==============
platform linux -- Python 3.10.0, pytest-7.4.4, pluggy-1.5.0
platform linux -- Python 3.11.0, pytest-7.4.4, pluggy-1.5.0
rootdir: /home/user/code/sqlmodel-tutorial
<b>collected 7 items </b>

Expand Down
30 changes: 0 additions & 30 deletions docs_src/advanced/datetime/tutorial001_py310.py

This file was deleted.

33 changes: 16 additions & 17 deletions docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,43 @@
1. Import `Optional` from `typing` to declare fields that could be `None`.
2. Import the things we will need from `sqlmodel`: `Field`, `SQLModel`, `create_engine`.
3. Create the `Hero` model class, representing the `hero` table in the database.
1. Import the things we will need from `sqlmodel`: `Field`, `SQLModel`, `create_engine`.
2. Create the `Hero` model class, representing the `hero` table in the database.

And also mark this class as a **table model** with `table=True`.

4. Create the `id` field:
3. Create the `id` field:

It could be `None` until the database assigns a value to it, so we annotate it with `Optional` (`int | None` in Python 3.10+).
It could be `None` until the database assigns a value to it, so we annotate it with `int | None`.

It is a **primary key**, so we use `Field()` and the argument `primary_key=True`.

5. Create the `name` field.
4. Create the `name` field.

It is required, so there's no default value, and it's not `Optional`.
It is required, so there's no default value, and it cannot be `None`.

6. Create the `secret_name` field.
5. Create the `secret_name` field.

Also required.

7. Create the `age` field.
6. Create the `age` field.

It is not required, the default value is `None`.

In the database, the default value will be `NULL`, the SQL equivalent of `None`.

As this field could be `None` (and `NULL` in the database), we annotate it with `Optional` (`int | None` in Python 3.10+).
As this field could be `None` (and `NULL` in the database), we annotate it with `int | None`.

8. Write the name of the database file.
9. Use the name of the database file to create the database URL.
10. Create the engine using the URL.
7. Write the name of the database file.
8. Use the name of the database file to create the database URL.
9. Create the engine using the URL.

This doesn't create the database yet, no file or table is created at this point, only the **engine** object that will handle the connections with this specific database, and with specific support for SQLite (based on the URL).

11. Put the code that creates side effects in a function.
10. Put the code that creates side effects in a function.

In this case, only one line that creates the database file with the table.

12. Create all the tables that were automatically registered in `SQLModel.metadata`.
11. Create all the tables that were automatically registered in `SQLModel.metadata`.

13. Add a main block, or "Top-level script environment".
12. Add a main block, or "Top-level script environment".

And put some logic to be executed when this is called directly with Python, as in:

Expand All @@ -58,7 +57,7 @@
from app import Hero
```

14. In this main block, call the function that creates the database file and the table.
13. In this main block, call the function that creates the database file and the table.

This way when we call it with:

Expand Down
26 changes: 13 additions & 13 deletions docs_src/tutorial/create_db_and_table/tutorial003.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
from sqlmodel import Field, SQLModel, create_engine # (2)!
from sqlmodel import Field, SQLModel, create_engine # (1)!


class Hero(SQLModel, table=True): # (3)!
id: int | None = Field(default=None, primary_key=True) # (4)!
name: str # (5)!
secret_name: str # (6)!
age: int | None = None # (7)!
class Hero(SQLModel, table=True): # (2)!
id: int | None = Field(default=None, primary_key=True) # (3)!
name: str # (4)!
secret_name: str # (5)!
age: int | None = None # (6)!


sqlite_file_name = "database.db" # (8)!
sqlite_url = f"sqlite:///{sqlite_file_name}" # (9)!
sqlite_file_name = "database.db" # (7)!
sqlite_url = f"sqlite:///{sqlite_file_name}" # (8)!

engine = create_engine(sqlite_url, echo=True) # (10)!
engine = create_engine(sqlite_url, echo=True) # (9)!


def create_db_and_tables(): # (11)!
SQLModel.metadata.create_all(engine) # (12)!
def create_db_and_tables(): # (10)!
SQLModel.metadata.create_all(engine) # (11)!


if __name__ == "__main__": # (13)!
create_db_and_tables() # (14)!
if __name__ == "__main__": # (12)!
create_db_and_tables() # (13)!
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "sqlmodel"
dynamic = ["version"]
description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness."
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.11"
authors = [
{ name = "Sebastián Ramírez", email = "tiangolo@gmail.com" },
]
Expand All @@ -21,7 +21,6 @@ classifiers = [
"Intended Audience :: Science/Research",
"Intended Audience :: System Administrators",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
Expand Down Expand Up @@ -83,7 +82,8 @@ tests = [
"aiosqlite >=0.17.0",
"alembic >=1.12.0",
"black >=24.1.0",
"coverage[toml] >=6.2",
# Support Python 3.11+ and the configured C tracer.
"coverage[toml] >=7.9.0",
"dirty-equals >=0.11",
"fastapi >=0.128.0",
"httpx >=0.28.1",
Expand Down
19 changes: 10 additions & 9 deletions sqlmodel/ext/asyncio/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import (
Any,
TypeVar,
TypeVarTuple,
cast,
overload,
)
Expand All @@ -17,7 +18,7 @@
from sqlalchemy.sql.base import Executable as _Executable
from sqlalchemy.sql.dml import UpdateBase
from sqlalchemy.util.concurrency import greenlet_spawn
from typing_extensions import TypeVarTuple, Unpack, deprecated
from typing_extensions import deprecated

from ...orm.session import Session
from ...sql.base import Executable
Expand All @@ -34,14 +35,14 @@ class AsyncSession(_AsyncSession):
@overload
async def exec(
self,
statement: Select[Unpack[_Ts]],
statement: Select[*_Ts],
*,
params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = None,
execution_options: Mapping[str, Any] = util.EMPTY_DICT,
bind_arguments: dict[str, Any] | None = None,
_parent_execute_state: Any | None = None,
_add_event: Any | None = None,
) -> TupleResult[tuple[Unpack[_Ts]]]: ...
) -> TupleResult[tuple[*_Ts]]: ...

@overload
async def exec(
Expand All @@ -65,11 +66,11 @@ async def exec(
bind_arguments: dict[str, Any] | None = None,
_parent_execute_state: Any | None = None,
_add_event: Any | None = None,
) -> CursorResult[Unpack[tuple[Any, ...]]]: ...
) -> CursorResult[*tuple[Any, ...]]: ...

async def exec(
self,
statement: Select[Unpack[_Ts]]
statement: Select[*_Ts]
| SelectOfScalar[_TSelectParam]
| Executable[_TSelectParam]
| UpdateBase,
Expand All @@ -80,9 +81,9 @@ async def exec(
_parent_execute_state: Any | None = None,
_add_event: Any | None = None,
) -> (
TupleResult[tuple[Unpack[_Ts]]]
TupleResult[tuple[*_Ts]]
| ScalarResult[_TSelectParam]
| CursorResult[Unpack[tuple[Any, ...]]]
| CursorResult[*tuple[Any, ...]]
):
if execution_options:
execution_options = util.immutabledict(execution_options).union(
Expand All @@ -101,7 +102,7 @@ async def exec(
_add_event=_add_event,
)
result_value = await _ensure_sync_result(
cast(Result[Unpack[tuple[Any, ...]]], result), self.exec
cast(Result[*tuple[Any, ...]], result), self.exec
)
return result_value # type: ignore

Expand Down Expand Up @@ -136,7 +137,7 @@ async def execute(
bind_arguments: dict[str, Any] | None = None,
_parent_execute_state: Any | None = None,
_add_event: Any | None = None,
) -> Result[Unpack[tuple[Any, ...]]]:
) -> Result[*tuple[Any, ...]]:
"""
🚨 You probably want to use `session.exec()` instead of `session.execute()`.

Expand Down
3 changes: 2 additions & 1 deletion sqlmodel/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
TypeVar,
Union,
cast,
dataclass_transform,
get_origin,
overload,
)
Expand Down Expand Up @@ -52,7 +53,7 @@
from sqlalchemy.sql.schema import MetaData
from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid
from sqlalchemy.types import TypeEngine
from typing_extensions import dataclass_transform, deprecated
from typing_extensions import deprecated

from ._compat import (
PYDANTIC_MINOR_VERSION,
Expand Down
17 changes: 9 additions & 8 deletions sqlmodel/orm/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import (
Any,
TypeVar,
TypeVarTuple,
overload,
)

Expand All @@ -17,7 +18,7 @@
from sqlalchemy.sql.dml import UpdateBase
from sqlmodel.sql.base import Executable
from sqlmodel.sql.expression import Select, SelectOfScalar
from typing_extensions import TypeVarTuple, Unpack, deprecated
from typing_extensions import deprecated

_TSelectParam = TypeVar("_TSelectParam", bound=Any)
_Ts = TypeVarTuple("_Ts")
Expand All @@ -27,14 +28,14 @@ class Session(_Session):
@overload
def exec(
self,
statement: Select[Unpack[_Ts]],
statement: Select[*_Ts],
*,
params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = None,
execution_options: Mapping[str, Any] = util.EMPTY_DICT,
bind_arguments: dict[str, Any] | None = None,
_parent_execute_state: Any | None = None,
_add_event: Any | None = None,
) -> TupleResult[tuple[Unpack[_Ts]]]: ...
) -> TupleResult[tuple[*_Ts]]: ...

@overload
def exec(
Expand All @@ -58,11 +59,11 @@ def exec(
bind_arguments: dict[str, Any] | None = None,
_parent_execute_state: Any | None = None,
_add_event: Any | None = None,
) -> CursorResult[Unpack[tuple[Any, ...]]]: ...
) -> CursorResult[*tuple[Any, ...]]: ...

def exec(
self,
statement: Select[Unpack[_Ts]]
statement: Select[*_Ts]
| SelectOfScalar[_TSelectParam]
| Executable[_TSelectParam]
| UpdateBase,
Expand All @@ -73,9 +74,9 @@ def exec(
_parent_execute_state: Any | None = None,
_add_event: Any | None = None,
) -> (
TupleResult[tuple[Unpack[_Ts]]]
TupleResult[tuple[*_Ts]]
| ScalarResult[_TSelectParam]
| CursorResult[Unpack[tuple[Any, ...]]]
| CursorResult[*tuple[Any, ...]]
):
results = super().execute(
statement,
Expand Down Expand Up @@ -119,7 +120,7 @@ def execute(
bind_arguments: dict[str, Any] | None = None,
_parent_execute_state: Any | None = None,
_add_event: Any | None = None,
) -> Result[Unpack[tuple[Any, ...]]]:
) -> Result[*tuple[Any, ...]]:
"""
🚨 You probably want to use `session.exec()` instead of `session.execute()`.

Expand Down
Loading
Loading