@@ -152,6 +152,172 @@ def test_query_insert(self, request, cli_runner):
152152 assert "David" in result .stdout
153153
154154
155+ def test_cancellable_query_select (self , request ):
156+ """Test CancellableQuery execution (used by TUI).
157+
158+ This tests the async query path that the TUI uses, which is different
159+ from the CLI path tested by other tests.
160+ """
161+ from sqlit .config import load_connections
162+ from sqlit .db .adapters import get_adapter
163+ from sqlit .services .cancellable import CancellableQuery
164+ from sqlit .services .query import QueryResult
165+
166+ # Get the connection fixture name and load the config
167+ connection_name = request .getfixturevalue (self .config .connection_fixture )
168+ connections = load_connections ()
169+ config = next ((c for c in connections if c .name == connection_name ), None )
170+ assert config is not None , f"Connection { connection_name } not found"
171+
172+ adapter = get_adapter (self .config .db_type )
173+
174+ # Test SELECT query through CancellableQuery
175+ query = CancellableQuery (
176+ sql = "SELECT * FROM test_users ORDER BY id" ,
177+ config = config ,
178+ adapter = adapter ,
179+ )
180+ result = query .execute (max_rows = 100 )
181+
182+ assert isinstance (result , QueryResult )
183+ assert len (result .columns ) >= 2 # At least id and name
184+ assert len (result .rows ) == 3
185+ # Check that Alice is in the first row
186+ row_values = [str (v ) for v in result .rows [0 ]]
187+ assert "Alice" in row_values
188+
189+ def test_cancellable_query_insert (self , request ):
190+ """Test CancellableQuery non-SELECT execution (used by TUI)."""
191+ from sqlit .config import load_connections
192+ from sqlit .db .adapters import get_adapter
193+ from sqlit .services .cancellable import CancellableQuery
194+ from sqlit .services .query import NonQueryResult
195+
196+ connection_name = request .getfixturevalue (self .config .connection_fixture )
197+ connections = load_connections ()
198+ config = next ((c for c in connections if c .name == connection_name ), None )
199+ assert config is not None
200+
201+ adapter = get_adapter (self .config .db_type )
202+
203+ # Test INSERT through CancellableQuery
204+ query = CancellableQuery (
205+ sql = "INSERT INTO test_users (id, name, email) VALUES (99, 'CancellableTest', 'cancel@test.com')" ,
206+ config = config ,
207+ adapter = adapter ,
208+ )
209+ result = query .execute ()
210+
211+ assert isinstance (result , NonQueryResult )
212+ # Some DBs return 1, others return -1 (unknown), both are acceptable
213+ assert result .rows_affected >= - 1
214+
215+ def test_streaming_csv_output (self , request , cli_runner ):
216+ """Test CSV output works for all adapters including those without cursor support.
217+
218+ This tests the streaming path in commands.py which needs special handling
219+ for adapters that don't support cursor-based access.
220+ """
221+ connection = request .getfixturevalue (self .config .connection_fixture )
222+ # No --max-rows to trigger the streaming path for cursor-based adapters
223+ result = cli_runner (
224+ "query" ,
225+ "-c" , connection ,
226+ "-q" , "SELECT id, name FROM test_users ORDER BY id" ,
227+ "--format" , "csv" ,
228+ )
229+ assert result .returncode == 0
230+ assert "id,name" in result .stdout
231+ assert "Alice" in result .stdout
232+
233+ def test_streaming_json_output (self , request , cli_runner ):
234+ """Test JSON output works for all adapters including those without cursor support."""
235+ connection = request .getfixturevalue (self .config .connection_fixture )
236+ result = cli_runner (
237+ "query" ,
238+ "-c" , connection ,
239+ "-q" , "SELECT id, name FROM test_users ORDER BY id" ,
240+ "--format" , "json" ,
241+ )
242+ assert result .returncode == 0
243+ data = json .loads (result .stdout )
244+ assert len (data ) == 3
245+ assert data [0 ]["name" ] == "Alice"
246+
247+ def test_adapter_interface_compliance (self , request ):
248+ """Verify adapter implements required interface without relying on cursor.
249+
250+ This ensures all database operations go through the adapter abstraction
251+ rather than directly accessing the connection object.
252+ """
253+ from sqlit .db .adapters import get_adapter
254+
255+ adapter = get_adapter (self .config .db_type )
256+
257+ # Required methods that should work without cursor
258+ required_methods = [
259+ 'connect' ,
260+ 'execute_query' ,
261+ 'execute_non_query' ,
262+ 'get_tables' ,
263+ 'get_views' ,
264+ 'get_columns' ,
265+ 'get_databases' ,
266+ 'get_procedures' ,
267+ 'quote_identifier' ,
268+ 'build_select_query' ,
269+ ]
270+
271+ for method_name in required_methods :
272+ method = getattr (adapter , method_name , None )
273+ assert method is not None , f"Adapter missing required method: { method_name } "
274+ assert callable (method ), f"Adapter method { method_name } is not callable"
275+
276+ # Required properties
277+ required_properties = [
278+ 'name' ,
279+ 'supports_multiple_databases' ,
280+ 'supports_stored_procedures' ,
281+ ]
282+
283+ for prop_name in required_properties :
284+ assert hasattr (adapter , prop_name ), f"Adapter missing required property: { prop_name } "
285+
286+ def test_query_service_execution (self , request ):
287+ """Test QueryService execution path (used by CLI with row limits).
288+
289+ This tests the standard query execution path through QueryService
290+ which should use adapter methods.
291+ """
292+ from sqlit .config import load_connections
293+ from sqlit .db .adapters import get_adapter
294+ from sqlit .services .query import QueryResult , QueryService
295+ from sqlit .services .session import ConnectionSession
296+
297+ connection_name = request .getfixturevalue (self .config .connection_fixture )
298+ connections = load_connections ()
299+ config = next ((c for c in connections if c .name == connection_name ), None )
300+ assert config is not None
301+
302+ service = QueryService ()
303+
304+ # Create a session and execute through QueryService
305+ with ConnectionSession .create (config , get_adapter ) as session :
306+ result = service .execute (
307+ connection = session .connection ,
308+ adapter = session .adapter ,
309+ query = "SELECT * FROM test_users ORDER BY id" ,
310+ config = config ,
311+ max_rows = 100 ,
312+ save_to_history = False ,
313+ )
314+
315+ assert isinstance (result , QueryResult )
316+ assert len (result .rows ) == 3
317+ row_values = [str (v ) for v in result .rows [0 ]]
318+ assert "Alice" in row_values
319+
320+
155321class BaseDatabaseTestsWithLimit (BaseDatabaseTests ):
156322 """Base tests for databases that support LIMIT syntax."""
157323
0 commit comments