22
33from datetime import UTC , datetime
44from typing import TYPE_CHECKING , cast
5- from uuid import UUID
5+ from uuid import UUID , uuid4
66
77import pytest
88from sqlalchemy .orm import Mapped , Session , mapped_column
99
1010from brussels .base import Base , DataclassBase
11- from brussels .mixins import PrimaryKeyMixin
11+ from brussels .mixins import PrimaryKeyMixin , UUIDv7PrimaryKeyMixin
1212
1313try :
1414 from obstore .store import MemoryStore
2020if TYPE_CHECKING :
2121 from obstore import GetOptions , PutMode
2222 from sqlalchemy import Engine
23+ from sqlalchemy .ext .asyncio import AsyncSession
2324
2425 from brussels .types .file ._types import RemoteMetadataField
2526
@@ -45,6 +46,12 @@ class OtherFileModel(DataclassBase, PrimaryKeyMixin):
4546 file : Mapped [RemoteMetadata | None ] = mapped_column (RemoteStorage (store = MemoryStore ()), nullable = True , default = None )
4647
4748
49+ class UUIDv7FileModel (DataclassBase , UUIDv7PrimaryKeyMixin ):
50+ __tablename__ = "uuidv7_file_model"
51+
52+ file : Mapped [RemoteMetadata | None ] = mapped_column (RemoteStorage (store = MemoryStore ()), nullable = True , default = None )
53+
54+
4855class TablelessPrimaryKeyModel (PrimaryKeyMixin ):
4956 pass
5057
@@ -54,10 +61,29 @@ def _configure_store(store_ops: FakeStoreOps) -> None:
5461 remote_storage .store = store_ops
5562
5663
64+ def _configure_uuidv7_store (store_ops : FakeStoreOps ) -> None :
65+ remote_storage = cast ("RemoteStorage" , UUIDv7FileModel .__table__ .c ["file" ].type )
66+ remote_storage .store = store_ops
67+
68+
5769def _file_handle (model : FileModel ) -> RemoteFile :
5870 return RemoteFile .from_metadata (model , FileModel .file )
5971
6072
73+ def _uuidv7_file_handle (model : UUIDv7FileModel ) -> RemoteFile :
74+ return RemoteFile .from_metadata (model , UUIDv7FileModel .file )
75+
76+
77+ class AsyncSessionShim :
78+ def __init__ (self , sync_session : Session , * , on_flush = None ) -> None :
79+ self .sync_session = sync_session
80+ self ._on_flush = on_flush
81+
82+ async def flush (self ) -> None :
83+ if self ._on_flush is not None :
84+ self ._on_flush ()
85+
86+
6187def test_from_metadata_rejects_models_without_primary_key_mixin () -> None :
6288 model = NoPrimaryKeyMixinModel (id = 1 )
6389
@@ -105,6 +131,14 @@ def test_from_metadata_accepts_models_with_primary_key_mixin() -> None:
105131 assert remote_file .field_name == "file"
106132
107133
134+ def test_from_metadata_accepts_uuidv7_primary_key_models () -> None :
135+ model = UUIDv7FileModel ()
136+
137+ remote_file = RemoteFile .from_metadata (model , UUIDv7FileModel .file )
138+
139+ assert remote_file .field_name == "file"
140+
141+
108142def test_put_sync_without_sqlalchemy_session_raises_and_does_not_call_store () -> None :
109143 store_ops = FakeStoreOps ()
110144 _configure_store (store_ops )
@@ -120,6 +154,62 @@ def test_put_sync_without_sqlalchemy_session_raises_and_does_not_call_store() ->
120154 assert model .file is None
121155
122156
157+ def test_put_sync_flush_populates_uuidv7_id_before_key_generation (
158+ engine : Engine ,
159+ monkeypatch : pytest .MonkeyPatch ,
160+ ) -> None :
161+ store_ops = FakeStoreOps ()
162+ _configure_uuidv7_store (store_ops )
163+
164+ with Session (engine ) as session :
165+ model = UUIDv7FileModel ()
166+ session .add (model )
167+ assigned_id = uuid4 ()
168+ flush_calls = 0
169+
170+ def fake_flush () -> None :
171+ nonlocal flush_calls
172+ flush_calls += 1
173+ if model .id is None :
174+ model .id = assigned_id
175+
176+ monkeypatch .setattr (session , "flush" , fake_flush )
177+
178+ put_result = _uuidv7_file_handle (model ).put (
179+ b"hello" ,
180+ content_type = "text/plain" ,
181+ session = session ,
182+ flush = True ,
183+ )
184+
185+ assert put_result == {"e_tag" : None , "version" : None }
186+ assert flush_calls == 2
187+ assert model .id == assigned_id
188+ assert model .file is not None
189+ assert model .file .key == f"{ assigned_id } /file"
190+ assert model .file .status == "pending"
191+ assert store_ops .calls == []
192+
193+
194+ def test_put_sync_rejects_uuidv7_model_without_flush (engine : Engine ) -> None :
195+ store_ops = FakeStoreOps ()
196+ _configure_uuidv7_store (store_ops )
197+
198+ with Session (engine ) as session :
199+ model = UUIDv7FileModel ()
200+ session .add (model )
201+
202+ with pytest .raises (ValueError , match = r"Pass flush=True or flush the model before calling put" ):
203+ _uuidv7_file_handle (model ).put (
204+ b"hello" ,
205+ content_type = "text/plain" ,
206+ session = session ,
207+ )
208+
209+ assert model .file is None
210+ assert store_ops .calls == []
211+
212+
123213def test_put_sync_defers_and_commits_when_sqlalchemy_session_is_attached (engine : Engine ) -> None :
124214 store_ops = FakeStoreOps ()
125215 _configure_store (store_ops )
@@ -256,6 +346,58 @@ async def test_put_rejects_invalid_model_id_type(engine: Engine) -> None:
256346 await _file_handle (model ).put_async (b"data" )
257347
258348
349+ @pytest .mark .asyncio
350+ async def test_put_async_flush_populates_uuidv7_id_before_key_generation (engine : Engine ) -> None :
351+ store_ops = FakeStoreOps ()
352+ _configure_uuidv7_store (store_ops )
353+
354+ with Session (engine ) as session :
355+ model = UUIDv7FileModel ()
356+ session .add (model )
357+ assigned_id = uuid4 ()
358+ flush_calls = 0
359+
360+ def on_flush () -> None :
361+ nonlocal flush_calls
362+ flush_calls += 1
363+ if model .id is None :
364+ model .id = assigned_id
365+
366+ await _uuidv7_file_handle (model ).put_async (
367+ b"data" ,
368+ content_type = "text/plain" ,
369+ session = cast ("AsyncSession" , AsyncSessionShim (session , on_flush = on_flush )),
370+ flush = True ,
371+ )
372+
373+ assert flush_calls == 2
374+ assert model .id == assigned_id
375+ assert model .file is not None
376+ assert model .file .key == f"{ assigned_id } /file"
377+ assert model .file .status == "pending"
378+ assert store_ops .calls == []
379+
380+
381+ @pytest .mark .asyncio
382+ async def test_put_async_rejects_uuidv7_model_without_flush (engine : Engine ) -> None :
383+ store_ops = FakeStoreOps ()
384+ _configure_uuidv7_store (store_ops )
385+
386+ with Session (engine ) as session :
387+ model = UUIDv7FileModel ()
388+ session .add (model )
389+
390+ with pytest .raises (ValueError , match = r"Pass flush=True or flush the model before calling put_async" ):
391+ await _uuidv7_file_handle (model ).put_async (
392+ b"data" ,
393+ content_type = "text/plain" ,
394+ session = session ,
395+ )
396+
397+ assert model .file is None
398+ assert store_ops .calls == []
399+
400+
259401@pytest .mark .asyncio
260402async def test_put_allows_uuid_model_id (engine : Engine ) -> None :
261403 store_ops = FakeStoreOps ()
0 commit comments