Skip to content

Commit b7e8366

Browse files
committed
Add comprehensive database integration tests
- Add test_cancellable_query_select for TUI async SELECT path - Add test_cancellable_query_insert for TUI async INSERT path - Add test_streaming_csv_output for CLI streaming path - Add test_streaming_json_output for CLI streaming path - Add test_adapter_interface_compliance to verify adapter methods - Add test_query_service_execution for QueryService path - These tests ensure all code paths are covered across all adapters
1 parent 7118f2d commit b7e8366

1 file changed

Lines changed: 166 additions & 0 deletions

File tree

tests/test_database_base.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
155321
class BaseDatabaseTestsWithLimit(BaseDatabaseTests):
156322
"""Base tests for databases that support LIMIT syntax."""
157323

0 commit comments

Comments
 (0)