Skip to content

Commit a44e5ec

Browse files
committed
Replace auto_now + default=Now/GenRandomUUID with type-scoped kwargs
DB-default behavior was overloaded onto `default=` via a generic `DatabaseDefaultExpression` marker. Users wrote `default=Now()` on DateTimeField and `default=GenRandomUUID()` on UUIDField, and nothing stopped `default=Now()` from being typed on an IntegerField. This change narrows the surface to each field type's actual use case: - DateTimeField(create_now=True) — DB STATEMENT_TIMESTAMP() on INSERT - DateTimeField(update_now=True) — Python pre_save, fires on every save - UUIDField(generate=True) — DB gen_random_uuid() on INSERT `default=` is removed entirely from DateTimeField and UUIDField. DateField/TimeField keep `default=` and drop the rarely-useful `auto_now`. `Now()` and `GenRandomUUID()` stay exported for use in annotations and filters; they're no longer valid as `default=` values. Internal cleanup: - `DatabaseDefaultExpression` marker class deleted; `Now`/`GenRandomUUID` are plain `Func` subclasses. - `_db_default_expression` moved off `self.default` onto a per-field `cached_property`; expression construction is derived from the boolean kwarg. - `has_db_default()` routes through a new `get_db_default_expression()` method on Field; `db_returning` is now a property that delegates to it. - `auto_fills_on_save` property on Field lets full_clean and the autodetector skip validation/backfill for pre_save-filled fields. - `has_any_default()` consolidates the three-way predicate used by autodetector guards and the schema editor's four-way backfill gate. - DateTimeField no longer inherits from DateField; temporal lookups (ExtractYear/Month/Day/etc.) are registered on DateTimeField explicitly, and TruncBase/CombinedExpression isinstance checks now include DateTimeField. - UUIDField inherits from ColumnField (not DefaultableField). All shipped model fields and migrations are converted to the new API. The plainmigrations recorder uses `update_now=True` (not `create_now=True`) because ensure_schema() skips tables that already exist — existing installations wouldn't gain the DB default.
1 parent 90142f5 commit a44e5ec

56 files changed

Lines changed: 635 additions & 496 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ Models are Postgres-only:
3131
# app/users/models.py
3232
from plain import postgres
3333
from plain.postgres import types
34-
from plain.postgres.functions import Now
3534
from plain.passwords.models import PasswordField
3635

3736
@postgres.register_model
@@ -40,7 +39,7 @@ class User(postgres.Model):
4039
password: str = PasswordField()
4140
display_name: str = types.TextField(max_length=100)
4241
is_admin: bool = types.BooleanField(default=False)
43-
created_at: datetime = types.DateTimeField(default=Now())
42+
created_at: datetime = types.DateTimeField(create_now=True)
4443

4544
query: postgres.QuerySet[User] = postgres.QuerySet()
4645

example/app/notes/migrations/0001_initial.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import plain.postgres.deletion
44
from plain import postgres
55
from plain.postgres import migrations
6-
from plain.postgres.functions import Now
76

87

98
class Migration(migrations.Migration):
@@ -19,9 +18,9 @@ class Migration(migrations.Migration):
1918
fields=[
2019
("id", postgres.PrimaryKeyField()),
2120
("body", postgres.TextField(default="", required=False)),
22-
("created_at", postgres.DateTimeField(default=Now())),
21+
("created_at", postgres.DateTimeField(create_now=True)),
2322
("title", postgres.TextField(max_length=200)),
24-
("updated_at", postgres.DateTimeField(auto_now=True)),
23+
("updated_at", postgres.DateTimeField(update_now=True)),
2524
(
2625
"author",
2726
postgres.ForeignKeyField(

example/app/notes/models.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from app.users.models import User
66
from plain import postgres
77
from plain.postgres import types
8-
from plain.postgres.functions import Now
98
from plain.urls import reverse
109

1110

@@ -18,8 +17,8 @@ class Note(postgres.Model):
1817
)
1918
title: str = types.TextField(max_length=200)
2019
body: str = types.TextField(default="", required=False)
21-
created_at: datetime.datetime = types.DateTimeField(default=Now())
22-
updated_at: datetime.datetime = types.DateTimeField(auto_now=True)
20+
created_at: datetime.datetime = types.DateTimeField(create_now=True)
21+
updated_at: datetime.datetime = types.DateTimeField(update_now=True)
2322

2423
query: postgres.QuerySet[Note] = postgres.QuerySet()
2524

example/app/users/migrations/0002_user_created_at.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from plain import postgres
44
from plain.postgres import migrations
5-
from plain.postgres.functions import Now
65

76

87
class Migration(migrations.Migration):
@@ -14,6 +13,6 @@ class Migration(migrations.Migration):
1413
migrations.AddField(
1514
model_name="user",
1615
name="created_at",
17-
field=postgres.DateTimeField(default=Now()),
16+
field=postgres.DateTimeField(create_now=True),
1817
),
1918
]

example/app/users/models.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55
from plain import postgres
66
from plain.passwords.types import PasswordField
77
from plain.postgres import types
8-
from plain.postgres.functions import Now
98

109

1110
@postgres.register_model
1211
class User(postgres.Model):
1312
email: str = types.EmailField()
1413
password: str = PasswordField()
1514
is_admin: bool = types.BooleanField(default=False)
16-
created_at: datetime.datetime = types.DateTimeField(default=Now())
15+
created_at: datetime.datetime = types.DateTimeField(create_now=True)
1716

1817
query: postgres.QuerySet[User] = postgres.QuerySet()

plain-admin/plain/admin/migrations/0001_initial.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import plain.postgres.deletion
44
from plain import postgres
55
from plain.postgres import migrations
6-
from plain.postgres.functions import Now
76

87

98
class Migration(migrations.Migration):
@@ -18,7 +17,7 @@ class Migration(migrations.Migration):
1817
name="PinnedNavItem",
1918
fields=[
2019
("id", postgres.PrimaryKeyField()),
21-
("created_at", postgres.DateTimeField(default=Now())),
20+
("created_at", postgres.DateTimeField(create_now=True)),
2221
("order", postgres.SmallIntegerField(default=0)),
2322
("view_slug", postgres.TextField(max_length=255)),
2423
(

plain-admin/plain/admin/models.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from plain import postgres
66
from plain.postgres import types
7-
from plain.postgres.functions import Now
87

98

109
@postgres.register_model
@@ -17,7 +16,7 @@ class PinnedNavItem(postgres.Model):
1716
)
1817
view_slug: str = types.TextField(max_length=255)
1918
order: int = types.SmallIntegerField(default=0)
20-
created_at: datetime = types.DateTimeField(default=Now())
19+
created_at: datetime = types.DateTimeField(create_now=True)
2120

2221
query: postgres.QuerySet[PinnedNavItem] = postgres.QuerySet()
2322

plain-api/plain/api/migrations/0001_initial.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
# Generated by Plain 0.52.2 on 2025-07-08 01:17
22

3-
import uuid
43

54
import plain.api.models
65
from plain import postgres
76
from plain.postgres import migrations
8-
from plain.postgres.functions import Now
97

108

119
class Migration(migrations.Migration):
@@ -18,9 +16,9 @@ class Migration(migrations.Migration):
1816
name="APIKey",
1917
fields=[
2018
("id", postgres.PrimaryKeyField()),
21-
("uuid", postgres.UUIDField(default=uuid.uuid4)),
22-
("created_at", postgres.DateTimeField(default=Now())),
23-
("updated_at", postgres.DateTimeField(auto_now=True)),
19+
("uuid", postgres.UUIDField(generate=True)),
20+
("created_at", postgres.DateTimeField(create_now=True)),
21+
("updated_at", postgres.DateTimeField(update_now=True)),
2422
("expires_at", postgres.DateTimeField(allow_null=True, required=False)),
2523
(
2624
"last_used_at",

plain-api/plain/api/models.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22

33
import binascii
44
import os
5-
import uuid
65
from datetime import datetime
76
from uuid import UUID
87

98
from plain import postgres
109
from plain.postgres import types
11-
from plain.postgres.functions import Now
1210

1311
__all__ = ["APIKey"]
1412

@@ -19,9 +17,9 @@ def generate_token() -> str:
1917

2018
@postgres.register_model
2119
class APIKey(postgres.Model):
22-
uuid: UUID = types.UUIDField(default=uuid.uuid4)
23-
created_at: datetime = types.DateTimeField(default=Now())
24-
updated_at: datetime = types.DateTimeField(auto_now=True)
20+
uuid: UUID = types.UUIDField(generate=True)
21+
created_at: datetime = types.DateTimeField(create_now=True)
22+
updated_at: datetime = types.DateTimeField(update_now=True)
2523
expires_at: datetime | None = types.DateTimeField(required=False, allow_null=True)
2624
last_used_at: datetime | None = types.DateTimeField(required=False, allow_null=True)
2725

plain-auth/plain/auth/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ from datetime import datetime
7676

7777
from plain import postgres
7878
from plain.postgres import types
79-
from plain.postgres.functions import Now
8079
from plain.passwords.models import PasswordField
8180

8281

@@ -85,7 +84,7 @@ class User(postgres.Model):
8584
email: str = types.EmailField()
8685
password = PasswordField()
8786
is_admin: bool = types.BooleanField(default=False)
88-
created_at: datetime = types.DateTimeField(default=Now())
87+
created_at: datetime = types.DateTimeField(create_now=True)
8988

9089
def __str__(self):
9190
return self.email

0 commit comments

Comments
 (0)