1+ import datetime
12import math
3+ import tomllib
24from pathlib import Path
35from unittest import mock
46
57import pytest
8+ from packaging .requirements import Requirement
69from reflex_base .constants .state import FIELD_MARKER
710from reflex_base .event import Event
811
2326)
2427
2528pytest .importorskip ("alembic" )
26- pytest .importorskip ("sqlalchemy" )
27- pytest .importorskip ("sqlmodel" )
29+ sa = pytest .importorskip ("sqlalchemy" )
30+ sqlmodel = pytest .importorskip ("sqlmodel" )
31+
32+
33+ @pytest .mark .parametrize ("version" , ["0.0.45" , "0.0.47" ])
34+ def test_db_extra_accepts_utc_sqlmodel (version : str ):
35+ """The database extra must accept versions used to generate UTC migrations.
36+
37+ Args:
38+ version: A SQLModel version supporting UTCDateTime.
39+ """
40+ project = tomllib .loads ((Path (__file__ ).parents [2 ] / "pyproject.toml" ).read_text ())
41+ requirement = next (
42+ parsed
43+ for dep in project ["project" ]["optional-dependencies" ]["db" ]
44+ if (parsed := Requirement (dep )).name == "sqlmodel"
45+ )
46+ assert requirement .specifier .contains (version )
47+
48+
49+ @pytest .mark .skipif (
50+ not hasattr (sqlmodel , "UTCDateTime" ), reason = "SQLModel before 0.0.45"
51+ )
52+ def test_utc_datetime_migration_and_roundtrip (
53+ tmp_working_dir : Path ,
54+ monkeypatch : pytest .MonkeyPatch ,
55+ model_registry : type [ModelRegistry ],
56+ ):
57+ """Generated UTC migrations work on fresh databases and retain aware datetimes.
58+
59+ Args:
60+ tmp_working_dir: The database and migration directory.
61+ monkeypatch: The configuration patch fixture.
62+ model_registry: The isolated model registry.
63+ """
64+ monkeypatch .setattr (
65+ reflex .constants , "ALEMBIC_CONFIG" , str (tmp_working_dir / "alembic.ini" )
66+ )
67+ config = mock .Mock (db_url = f"sqlite:///{ tmp_working_dir } /reflex.db" )
68+ monkeypatch .setattr (reflex .model , "get_config" , lambda : config )
69+ alembic_init ()
70+
71+ class UTCPost (Model , table = True ):
72+ created_at : datetime .datetime
73+
74+ with get_engine ().connect () as connection :
75+ assert alembic_autogenerate (connection = connection , message = "UTC datetime" )
76+ migration = next ((tmp_working_dir / "alembic" / "versions" ).glob ("*.py" ))
77+ assert "sqlmodel.sql.sqltypes.UTCDateTime()" in migration .read_text ()
78+ assert migrate ()
79+
80+ value = datetime .datetime (
81+ 2026 , 9 , 21 , 12 , tzinfo = datetime .timezone (datetime .timedelta (hours = 5 ))
82+ )
83+ with reflex .model .session () as session :
84+ session .add (UTCPost (created_at = value ))
85+ session .commit ()
86+ stored = session .exec (sqlmodel .select (UTCPost )).one ()
87+ assert stored .created_at == value
88+ assert stored .created_at .utcoffset () == datetime .timedelta (0 )
89+
90+ get_engine ().dispose ()
91+ (tmp_working_dir / "reflex.db" ).unlink ()
92+ assert migrate ()
93+ with reflex .model .session () as session :
94+ assert session .exec (sqlmodel .select (UTCPost )).all () == []
95+
96+ model_registry .get_metadata ().clear ()
2897
2998
3099@pytest .fixture
@@ -115,6 +184,10 @@ class AlembicThing(Model, table=True): # pyright: ignore [reportRedeclaration]
115184 version_scripts = list (versions .glob ("*.py" ))
116185 assert len (version_scripts ) == 1
117186 assert version_scripts [0 ].name .endswith ("initial_revision.py" )
187+ assert set (sa .inspect (get_engine ()).get_table_names ()) == {
188+ "alembic_version" ,
189+ "alembicthing" ,
190+ }
118191
119192 with reflex .model .session () as session :
120193 session .add (AlembicThing (id = None , t1 = "foo" ))
@@ -263,7 +336,8 @@ class AlembicCallable(Model, table=True): # pyright: ignore [reportRedeclaratio
263336 class AlembicCallable (Model , table = True ): # pyright: ignore [reportRedeclaration]
264337 t1 : str
265338 created : datetime .datetime = sqlmodel .Field (
266- default_factory = datetime .datetime .now
339+ default_factory = datetime .datetime .now ,
340+ sa_type = sa .DateTime (timezone = False ),
267341 )
268342 count : int = 5
269343
@@ -290,7 +364,8 @@ class AlembicCallable(Model, table=True): # pyright: ignore [reportRedeclaratio
290364 class AlembicCallable (Model , table = True ): # pyright: ignore [reportRedeclaration]
291365 t1 : str
292366 created : datetime .datetime = sqlmodel .Field (
293- default_factory = datetime .datetime .now
367+ default_factory = datetime .datetime .now ,
368+ sa_type = sa .DateTime (timezone = False ),
294369 )
295370 count : int = 5
296371 note : str | None = sqlmodel .Field (default_factory = lambda : "generated" )
0 commit comments