From 893bcf3d71c493077ad2306c184e9ccf4c8aa7c7 Mon Sep 17 00:00:00 2001
From: Robert Rosca <32569096+RobertRosca@users.noreply.github.com>
Date: Thu, 30 Jun 2022 10:51:03 +0200
Subject: [PATCH 1/5] Add example code for sa_column onupdate timestamps
---
docs/advanced/sa-column.md | 33 ++++++++++
docs_src/advanced/sa_column/__init__.py | 0
docs_src/advanced/sa_column/tutorial001.py | 74 ++++++++++++++++++++++
mkdocs.yml | 1 +
4 files changed, 108 insertions(+)
create mode 100644 docs/advanced/sa-column.md
create mode 100644 docs_src/advanced/sa_column/__init__.py
create mode 100644 docs_src/advanced/sa_column/tutorial001.py
diff --git a/docs/advanced/sa-column.md b/docs/advanced/sa-column.md
new file mode 100644
index 0000000000..1bda5e8c9f
--- /dev/null
+++ b/docs/advanced/sa-column.md
@@ -0,0 +1,33 @@
+# SQLAlchemy Columns
+
+In some cases you may need more control over the columns generated by SQLModel, this can be done by using the `sa_column`, `sa_column_args`, and `sa_column_kwargs` arguments when creating the `Field` object.
+
+There are many use cases for this, but ones where this is particularity useful is when you want more advanced defaults for values than what is easy to implement with Pydantic, such `created_at` or `update_at` timestamps for rows.().
+
+## Columns for Timestamps
+
+Two ways of implementing `created_at` timestamps with Pydantic are [default factories](https://pydantic-docs.helpmanual.io/usage/models/#field-with-dynamic-default-value) and [validators](https://pydantic-docs.helpmanual.io/usage/validators/#validate-always), however there's no straightforward way to have an `update_at` timestamp.
+
+The SQLAlchemy docs describe how `created_at` timestamps can be automatically set with either [default](https://docs.sqlalchemy.org/en/14/core/defaults.html#python-executed-functions) or [server-default](https://docs.sqlalchemy.org/en/14/core/defaults.html#server-invoked-ddl-explicit-default-expressions) functions, by using `sa_column=Column(...)` as described in the SQLAlchemy documentation we can achieve the same behaviour:
+
+```{.python .annotate hl_lines="8 12"}
+{!./docs_src/advanced/sa_column/tutorial001.py[ln:9-21]!}
+```
+
+Above we are saying that the `registered_at` column should have a `server_default` value of `func.now()` (see full code for imports), which means that if there is no provided value then the current time will be the recorded value for that row.
+
+As there is a value there now, then it will not be changed automatically in the future.
+
+The `updated_at` column has an `onupdate` value of `func.now()`, this means that each time an `UPDATE` is performed, the function will be executed, meaning that the timestamp changes whenever a change is made to the row.
+
+!!! warning
+ The difference between client-side python functions, server-side ddl expressions, and server-side implicit defaults is important in some situations but too in-depth to go into here. Check the SQL and SQLAlchemy docs for more information.
+
+
+👀 Full file preview
+
+```Python
+{!./docs_src/advanced/sa_column/tutorial001.py!}
+```
+
+
diff --git a/docs_src/advanced/sa_column/__init__.py b/docs_src/advanced/sa_column/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs_src/advanced/sa_column/tutorial001.py b/docs_src/advanced/sa_column/tutorial001.py
new file mode 100644
index 0000000000..cf6961a4d3
--- /dev/null
+++ b/docs_src/advanced/sa_column/tutorial001.py
@@ -0,0 +1,74 @@
+from datetime import datetime
+from time import sleep
+from typing import Optional
+
+from sqlmodel import Field, Session, SQLModel, create_engine, select
+from sqlalchemy import Column, DateTime, func
+
+
+class Hero(SQLModel, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ name: str
+ secret_name: str
+ age: Optional[int] = None
+
+ registered_at: datetime = Field(
+ sa_column=Column(DateTime(timezone=False), server_default=func.now())
+ )
+
+ updated_at: Optional[datetime] = Field(
+ sa_column=Column(DateTime(timezone=False), onupdate=func.now())
+ )
+
+
+sqlite_file_name = "database.db"
+sqlite_url = f"sqlite:///{sqlite_file_name}"
+
+engine = create_engine(sqlite_url, echo=True)
+
+
+def create_db_and_tables():
+ SQLModel.metadata.create_all(engine)
+
+
+def create_heroes():
+ hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
+ hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
+ hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
+
+ session = Session(engine)
+
+ session.add(hero_1)
+ session.add(hero_2)
+ session.add(hero_3)
+
+ session.commit()
+
+ session.close()
+
+
+def update_hero_age(new_secret_name):
+ with Session(engine) as session:
+ statement = select(Hero).where(Hero.name == "Spider-Boy")
+ results = session.exec(statement)
+ hero = results.one()
+ print("Hero:", hero)
+
+ hero.secret_name = new_secret_name
+ session.add(hero)
+ session.commit()
+ session.refresh(hero)
+ print("Updated hero:", hero)
+
+
+def main():
+ create_db_and_tables()
+ create_heroes()
+ sleep(1)
+ update_hero_age("Arachnid-Lad")
+ sleep(1)
+ update_hero_age("The Wallclimber")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/mkdocs.yml b/mkdocs.yml
index a27bbde8a1..eade4bca3c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -85,6 +85,7 @@ nav:
- Advanced User Guide:
- advanced/index.md
- advanced/decimal.md
+ - advanced/sa-column.md
- alternatives.md
- help.md
- contributing.md
From 1e5caaaf02b46f50f48ff5141f4f7980e36387e2 Mon Sep 17 00:00:00 2001
From: Robert Rosca <32569096+RobertRosca@users.noreply.github.com>
Date: Sun, 18 Sep 2022 09:36:50 +0200
Subject: [PATCH 2/5] Fix import order
---
docs_src/advanced/sa_column/tutorial001.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs_src/advanced/sa_column/tutorial001.py b/docs_src/advanced/sa_column/tutorial001.py
index cf6961a4d3..8477cd6462 100644
--- a/docs_src/advanced/sa_column/tutorial001.py
+++ b/docs_src/advanced/sa_column/tutorial001.py
@@ -2,8 +2,8 @@
from time import sleep
from typing import Optional
-from sqlmodel import Field, Session, SQLModel, create_engine, select
from sqlalchemy import Column, DateTime, func
+from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
From 01ca385558e50ea23c6fa3961e87cd1fdb92abc8 Mon Sep 17 00:00:00 2001
From: Robert Rosca <32569096+RobertRosca@users.noreply.github.com>
Date: Wed, 2 Nov 2022 10:38:31 +0100
Subject: [PATCH 3/5] Fix typo
---
docs/advanced/sa-column.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/advanced/sa-column.md b/docs/advanced/sa-column.md
index 1bda5e8c9f..41767ea07d 100644
--- a/docs/advanced/sa-column.md
+++ b/docs/advanced/sa-column.md
@@ -2,7 +2,7 @@
In some cases you may need more control over the columns generated by SQLModel, this can be done by using the `sa_column`, `sa_column_args`, and `sa_column_kwargs` arguments when creating the `Field` object.
-There are many use cases for this, but ones where this is particularity useful is when you want more advanced defaults for values than what is easy to implement with Pydantic, such `created_at` or `update_at` timestamps for rows.().
+There are many use cases for this, but ones where this is particularity useful is when you want more advanced defaults for values than what is easy to implement with Pydantic, such `created_at` or `update_at` timestamps for rows.
## Columns for Timestamps
From d78fc7116a241ef9aef41ab7d927192ff3e48df9 Mon Sep 17 00:00:00 2001
From: Robert Rosca <32569096+RobertRosca@users.noreply.github.com>
Date: Wed, 2 Nov 2022 11:18:03 +0100
Subject: [PATCH 4/5] Add notes on Pydantic implementation of updated_at
---
docs/advanced/sa-column.md | 43 ++++++++++++++++++++++++++++++++++++++
1 file changed, 43 insertions(+)
diff --git a/docs/advanced/sa-column.md b/docs/advanced/sa-column.md
index 41767ea07d..6f7f91cc28 100644
--- a/docs/advanced/sa-column.md
+++ b/docs/advanced/sa-column.md
@@ -31,3 +31,46 @@ The `updated_at` column has an `onupdate` value of `func.now()`, this means that
```
+
+### Pydantic Implementation
+
+Implementing these timestamps on the DB side with SQLAlchemy works very well as the database itself is what will create and update the fields whenever a relevant database interaction occurs.
+
+It's possible to achieve similar behaviour with Pydantic, for the `created_at` timestamp by using a Pydantic `Field` with a `default_factory`:
+
+```python
+from datetime import datetime
+
+from pydantic import BaseModel, Field
+
+
+class Model(BaseModel):
+ created_at: datetime = Field(default_factory=datetime.utcnow)
+
+
+m1 = Model()
+m2 = Model()
+print(f'{m1.created_at} != {m2.created_at}')
+#> 2022-05-19 10:49:22.053624 != 2022-05-19 10:49:22.053641
+```
+
+Another approach is to use a Pydantic `validator`:
+
+```python
+from datetime import datetime
+
+from pydantic import BaseModel, validator
+
+class Model(BaseModel):
+ created_at: datetime = None
+
+ @validator('ts', pre=True, always=True)
+ def set_created_at_now(cls, v):
+ return v or datetime.now()
+```
+
+Both of these approaches come with the major caveat that default fields are set during the **Pydantic model instantiation**, as opposed to during **interactions with the database**, instead of the SQLModel approach which sets it with `server_default` which means that the timestamp will be exactly when the row is created in the database.
+
+The real issue starts when looking at the `updated_at` timestamp - SQLAlchemy has the `onupdate` default which runs a function when the row is updated in the database, but there is no easy way to do this in Pydantic as it has no concept of 'about to be saved'.
+
+So the pure Pydantic approach would require some additional logic to always change the `updated_at` timestamp before doing a write to the database, which adds some more complexity to the code and does not have benefits over the SQLAlchemy approach.
From bf882bedb4f0110faa6ae54f33377055f1f2b9b0 Mon Sep 17 00:00:00 2001
From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com>
Date: Mon, 25 Aug 2025 12:22:13 +0200
Subject: [PATCH 5/5] Update formatting of code include block and warning block
---
docs/advanced/sa-column.md | 16 ++++------------
1 file changed, 4 insertions(+), 12 deletions(-)
diff --git a/docs/advanced/sa-column.md b/docs/advanced/sa-column.md
index 6f7f91cc28..c3ec7f12a0 100644
--- a/docs/advanced/sa-column.md
+++ b/docs/advanced/sa-column.md
@@ -10,9 +10,7 @@ Two ways of implementing `created_at` timestamps with Pydantic are [default fact
The SQLAlchemy docs describe how `created_at` timestamps can be automatically set with either [default](https://docs.sqlalchemy.org/en/14/core/defaults.html#python-executed-functions) or [server-default](https://docs.sqlalchemy.org/en/14/core/defaults.html#server-invoked-ddl-explicit-default-expressions) functions, by using `sa_column=Column(...)` as described in the SQLAlchemy documentation we can achieve the same behaviour:
-```{.python .annotate hl_lines="8 12"}
-{!./docs_src/advanced/sa_column/tutorial001.py[ln:9-21]!}
-```
+{* ./docs_src/advanced/sa_column/tutorial001.py ln[9:21] hl[16,20] *}
Above we are saying that the `registered_at` column should have a `server_default` value of `func.now()` (see full code for imports), which means that if there is no provided value then the current time will be the recorded value for that row.
@@ -20,17 +18,11 @@ As there is a value there now, then it will not be changed automatically in the
The `updated_at` column has an `onupdate` value of `func.now()`, this means that each time an `UPDATE` is performed, the function will be executed, meaning that the timestamp changes whenever a change is made to the row.
-!!! warning
- The difference between client-side python functions, server-side ddl expressions, and server-side implicit defaults is important in some situations but too in-depth to go into here. Check the SQL and SQLAlchemy docs for more information.
-
-
-👀 Full file preview
+/// warning
-```Python
-{!./docs_src/advanced/sa_column/tutorial001.py!}
-```
+The difference between client-side python functions, server-side ddl expressions, and server-side implicit defaults is important in some situations but too in-depth to go into here. Check the SQL and SQLAlchemy docs for more information.
-
+///
### Pydantic Implementation