11"""Integration tests for ORM operations."""
22
33import datetime
4+ from collections .abc import Generator
45
56import pytest
67from sqlalchemy import (
1617 create_engine ,
1718 text ,
1819)
20+ from sqlalchemy .engine import Engine
1921from sqlalchemy .orm import Session , declarative_base
2022
2123Base = declarative_base ()
@@ -63,6 +65,14 @@ class DateTimeTest(Base): # type: ignore[valid-type,misc]
6365
6466@pytest .mark .integration
6567class TestORMOperations :
68+ @pytest .fixture
69+ def engine (self , engine_url : str ) -> Generator [Engine ]:
70+ engine = create_engine (engine_url )
71+ Base .metadata .create_all (engine )
72+ yield engine
73+ Base .metadata .drop_all (engine )
74+ engine .dispose ()
75+
6676 def test_create_engine (self , engine_url : str ) -> None :
6777 engine = create_engine (engine_url )
6878 assert engine is not None
@@ -79,108 +89,65 @@ def test_raw_sql(self, engine_url: str) -> None:
7989
8090 engine .dispose ()
8191
82- def test_create_table_and_insert (self , engine_url : str ) -> None :
83- engine = create_engine (engine_url )
84-
85- # Create tables
86- Base .metadata .create_all (engine )
87-
88- # Insert data
92+ def test_create_table_and_insert (self , engine : Engine ) -> None :
8993 with Session (engine ) as session :
9094 user = User (name = "Alice" , email = "alice@example.com" )
9195 session .add (user )
9296 session .commit ()
9397
94- # Query data
9598 users = session .query (User ).filter_by (name = "Alice" ).all ()
9699 assert len (users ) == 1
97100 assert users [0 ].email == "alice@example.com"
98101
99- # Cleanup
100- Base .metadata .drop_all (engine )
101- engine .dispose ()
102-
103- def test_unicode_text (self , engine_url : str ) -> None :
102+ def test_unicode_text (self , engine : Engine ) -> None :
104103 """Test Unicode text handling including emojis, CJK, RTL."""
105- engine = create_engine (engine_url )
106- Base .metadata .create_all (engine )
107-
108104 unicode_values = [
109- # Emojis (4-byte UTF-8)
110- "Hello 🎉 World" ,
111- "🎉🎊🎁🎂" ,
112- # CJK characters
113- "中文测试" ,
114- "日本語テスト" ,
115- "한국어 테스트" ,
116- # RTL languages
117- "العربية" ,
118- "עברית" ,
119- # Mixed scripts
120- "Hello 世界 🌍" ,
121- # Combining characters
122- "café résumé naïve" ,
105+ "Hello \U0001f389 World" ,
106+ "\U0001f389 \U0001f38a \U0001f381 \U0001f382 " ,
107+ "\u4e2d \u6587 \u6d4b \u8bd5 " ,
108+ "\u65e5 \u672c \u8a9e \u30c6 \u30b9 \u30c8 " ,
109+ "\ud55c \uad6d \uc5b4 \ud14c \uc2a4 \ud2b8 " ,
110+ "\u0627 \u0644 \u0639 \u0631 \u0628 \u064a \u0629 " ,
111+ "\u05e2 \u05d1 \u05e8 \u05d9 \u05ea " ,
112+ "Hello \u4e16 \u754c \U0001f30d " ,
113+ "caf\u00e9 r\u00e9 sum\u00e9 na\u00ef ve" ,
123114 ]
124115
125116 with Session (engine ) as session :
126117 for val in unicode_values :
127- # Insert
128118 record = UnicodeTest (content = val )
129119 session .add (record )
130120 session .commit ()
131121
132- # Query and verify
133122 result = session .query (UnicodeTest ).filter_by (content = val ).first ()
134123 assert result is not None , f"Failed to find: { repr (val )} "
135124 assert result .content == val , f"Mismatch for: { repr (val )} "
136125
137- # Cleanup
138126 session .delete (result )
139127 session .commit ()
140128
141- Base .metadata .drop_all (engine )
142- engine .dispose ()
143-
144- def test_binary_blob (self , engine_url : str ) -> None :
129+ def test_binary_blob (self , engine : Engine ) -> None :
145130 """Test binary blob handling including null bytes."""
146- engine = create_engine (engine_url )
147- Base .metadata .create_all (engine )
148-
149131 blob_values = [
150132 b"simple" ,
151- b"\x00 \x01 \x02 \x03 " , # Null bytes
152- b"\xff \xfe \xfd " , # High bytes
153- bytes (range (256 )), # All byte values
133+ b"\x00 \x01 \x02 \x03 " ,
134+ b"\xff \xfe \xfd " ,
135+ bytes (range (256 )),
154136 ]
155137
156138 with Session (engine ) as session :
157139 for val in blob_values :
158- # Insert
159140 record = BlobTest (data = val )
160141 session .add (record )
161142 session .commit ()
162143
163- # Query and verify
164144 result = session .query (BlobTest ).order_by (BlobTest .id .desc ()).first ()
165145 assert result is not None
166146 assert result .data == val , f"Mismatch for blob: { repr (val )} "
167147
168- Base .metadata .drop_all (engine )
169- engine .dispose ()
170-
171- def test_numeric_types (self , engine_url : str ) -> None :
172- """Test integer, bigint, float, and boolean types.
173-
174- Note: dqlite has a known limitation where BOOLEAN NULL values are
175- returned as False (type BOOLEAN with value 0) instead of NULL.
176- This is because dqlite returns the column's declared type even for
177- NULL values, and 0 is indistinguishable from NULL for BOOLEAN columns.
178- """
179- engine = create_engine (engine_url )
180- Base .metadata .create_all (engine )
181-
148+ def test_numeric_types (self , engine : Engine ) -> None :
149+ """Test integer, bigint, float, and boolean types."""
182150 test_cases = [
183- # (int, bigint, float, bool)
184151 (0 , 0 , 0.0 , False ),
185152 (1 , 1 , 1.0 , True ),
186153 (- 1 , - 1 , - 1.0 , False ),
@@ -199,7 +166,6 @@ def test_numeric_types(self, engine_url: str) -> None:
199166 session .add (record )
200167 session .commit ()
201168
202- # Query and verify
203169 result = session .query (NumericTest ).order_by (NumericTest .id .desc ()).first ()
204170 assert result is not None
205171 assert result .int_val == int_val
@@ -208,56 +174,34 @@ def test_numeric_types(self, engine_url: str) -> None:
208174 assert abs (result .float_val - float_val ) < 1e-9
209175 assert result .bool_val == bool_val
210176
211- Base .metadata .drop_all (engine )
212- engine .dispose ()
213-
214- def test_datetime_types (self , engine_url : str ) -> None :
215- """Test DateTime column type.
216-
217- Note: dqlite has a known limitation where DATETIME NULL values are
218- returned as empty string instead of NULL. This causes SQLAlchemy's
219- DateTime processor to fail when parsing. Avoid using NULL datetime
220- values with dqlite - use a sentinel value if needed.
221- """
222- engine = create_engine (engine_url )
223- Base .metadata .create_all (engine )
224-
177+ def test_datetime_types (self , engine : Engine ) -> None :
178+ """Test DateTime column type."""
225179 test_dates = [
226180 datetime .datetime (2024 , 1 , 15 , 10 , 30 , 45 ),
227- datetime .datetime (1970 , 1 , 1 , 0 , 0 , 0 ), # Unix epoch
228- datetime .datetime (2038 , 1 , 19 , 3 , 14 , 7 ), # Near Y2038
181+ datetime .datetime (1970 , 1 , 1 , 0 , 0 , 0 ),
182+ datetime .datetime (2038 , 1 , 19 , 3 , 14 , 7 ),
229183 datetime .datetime .now ().replace (microsecond = 0 ),
230184 ]
231185
232186 with Session (engine ) as session :
233187 for dt in test_dates :
234- # Note: We set updated_at to a value, not NULL, due to dqlite limitation
235188 record = DateTimeTest (created_at = dt , updated_at = dt )
236189 session .add (record )
237190 session .commit ()
238191
239- # Query and verify
240192 result = session .query (DateTimeTest ).order_by (DateTimeTest .id .desc ()).first ()
241193 assert result is not None
242194
243- # SQLite stores datetime as text, compare with second precision
244195 assert result .created_at .year == dt .year
245196 assert result .created_at .month == dt .month
246197 assert result .created_at .day == dt .day
247198 assert result .created_at .hour == dt .hour
248199 assert result .created_at .minute == dt .minute
249200 assert result .created_at .second == dt .second
250201
251- Base .metadata .drop_all (engine )
252- engine .dispose ()
253-
254- def test_null_handling (self , engine_url : str ) -> None :
202+ def test_null_handling (self , engine : Engine ) -> None :
255203 """Test NULL values across different column types."""
256- engine = create_engine (engine_url )
257- Base .metadata .create_all (engine )
258-
259204 with Session (engine ) as session :
260- # Insert record with all nullable fields as NULL
261205 record = NumericTest (
262206 int_val = None ,
263207 bigint_val = None ,
@@ -273,6 +217,3 @@ def test_null_handling(self, engine_url: str) -> None:
273217 assert result .bigint_val is None
274218 assert result .float_val is None
275219 assert result .bool_val is None
276-
277- Base .metadata .drop_all (engine )
278- engine .dispose ()
0 commit comments