Skip to content

Commit e529e30

Browse files
committed
stopping wrapping of driver exceptions as is maintenance issue
1 parent 349d486 commit e529e30

File tree

9 files changed

+135
-101
lines changed

9 files changed

+135
-101
lines changed

docs/api.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -514,28 +514,46 @@ except ConnectionError as e:
514514

515515
### QueryError
516516

517-
Raised when a query execution fails.
517+
Raised when a non-Cassandra exception occurs during query execution. Most Cassandra driver exceptions (like `InvalidRequest`, `Unauthorized`, `AlreadyExists`, etc.) are passed through directly without wrapping.
518518

519519
```python
520+
# Cassandra exceptions pass through directly
521+
from cassandra import InvalidRequest, Unauthorized
522+
520523
try:
521524
result = await session.execute("SELECT * FROM invalid_table")
525+
except InvalidRequest as e:
526+
print(f"Invalid query: {e}") # Cassandra exception passed through
522527
except QueryError as e:
523-
print(f"Query failed: {e}")
528+
print(f"Unexpected error: {e}") # Only non-Cassandra exceptions wrapped
524529
if e.cause:
525530
print(f"Caused by: {e.cause}")
526531
```
527532

533+
### Cassandra Driver Exceptions
534+
535+
The following Cassandra driver exceptions are passed through directly without wrapping:
536+
- `InvalidRequest` - Invalid query syntax or schema issues
537+
- `Unauthorized` - Permission/authorization failures
538+
- `AuthenticationFailed` - Authentication failures
539+
- `AlreadyExists` - Schema already exists errors
540+
- `NoHostAvailable` - No Cassandra hosts available
541+
- `Unavailable`, `ReadTimeout`, `WriteTimeout` - Consistency/timeout errors
542+
- `OperationTimedOut` - Query timeout
543+
- Protocol exceptions like `SyntaxException`, `ServerError`
544+
528545
### Other Exceptions
529546

530-
The library also defines TimeoutError, AuthenticationError, and ConfigurationError internally, but these are typically wrapped in the main exception types above. You should generally catch ConnectionError and QueryError in your code.
547+
The library defines `ConnectionError` for connection-related issues and `QueryError` for wrapping unexpected non-Cassandra exceptions. Most of the time, you should catch specific Cassandra exceptions for proper error handling.
531548

532549
## Complete Example
533550

534551
```python
535552
import asyncio
536553
import uuid
537554
from async_cassandra import AsyncCluster, AsyncCassandraSession
538-
from async_cassandra.exceptions import QueryError, ConnectionError
555+
from async_cassandra.exceptions import ConnectionError
556+
from cassandra import InvalidRequest, AlreadyExists
539557

540558
async def main():
541559
# Create cluster with authentication
@@ -591,8 +609,10 @@ async def main():
591609

592610
except ConnectionError as e:
593611
print(f"Connection failed: {e}")
594-
except QueryError as e:
595-
print(f"Query failed: {e}")
612+
except InvalidRequest as e:
613+
print(f"Invalid query: {e}")
614+
except AlreadyExists as e:
615+
print(f"Schema already exists: {e.keyspace}.{e.table}")
596616
finally:
597617
await cluster.shutdown()
598618

docs/why-async-wrapper.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,16 @@ future.add_errback(err_callback)
194194

195195
### async-cassandra Solution
196196
```python
197+
from cassandra import InvalidRequest
198+
197199
# Clean async/await pattern
198200
try:
199201
result = await session.execute(query)
200202
processed = await process(result)
201203
await save(processed)
202-
except QueryError as e:
203-
# Natural error handling with full stack trace
204-
logger.error(f"Query failed: {e}")
204+
except InvalidRequest as e:
205+
# Natural error handling with Cassandra exceptions
206+
logger.error(f"Invalid query: {e}")
205207
```
206208

207209
## 7. Resource Management in Async Contexts

src/async_cassandra/session.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
import time
1111
from typing import Any, Dict, Optional
1212

13-
from cassandra import InvalidRequest, OperationTimedOut, ReadTimeout, Unavailable, WriteTimeout
13+
from cassandra import DriverException
1414
from cassandra.cluster import _NOT_SET, EXEC_PROFILE_DEFAULT, Cluster, NoHostAvailable, Session
15+
from cassandra.protocol import ErrorMessage
1516
from cassandra.query import BatchStatement, PreparedStatement, SimpleStatement
1617

1718
from .base import AsyncContextManageable
@@ -176,12 +177,9 @@ async def execute(
176177
return result
177178

178179
except (
179-
InvalidRequest,
180-
Unavailable,
181-
ReadTimeout,
182-
WriteTimeout,
183-
OperationTimedOut,
184-
NoHostAvailable,
180+
DriverException, # Base class for all Cassandra driver exceptions
181+
ErrorMessage, # Base class for protocol-level errors like SyntaxException
182+
NoHostAvailable, # Not a DriverException but should pass through
185183
) as e:
186184
# Re-raise Cassandra exceptions without wrapping
187185
error_type = type(e).__name__
@@ -292,12 +290,9 @@ async def execute_stream(
292290
return result
293291

294292
except (
295-
InvalidRequest,
296-
Unavailable,
297-
ReadTimeout,
298-
WriteTimeout,
299-
OperationTimedOut,
300-
NoHostAvailable,
293+
DriverException, # Base class for all Cassandra driver exceptions
294+
ErrorMessage, # Base class for protocol-level errors like SyntaxException
295+
NoHostAvailable, # Not a DriverException but should pass through
301296
) as e:
302297
# Re-raise Cassandra exceptions without wrapping
303298
error_type = type(e).__name__
@@ -400,8 +395,9 @@ async def prepare(
400395
return prepared
401396
except asyncio.TimeoutError:
402397
raise
403-
except (InvalidRequest, OperationTimedOut) as e:
404-
raise QueryError(f"Statement preparation failed: {str(e)}") from e
398+
except (DriverException, ErrorMessage, NoHostAvailable):
399+
# Re-raise Cassandra exceptions without wrapping
400+
raise
405401
except Exception as e:
406402
raise QueryError(f"Statement preparation failed: {str(e)}") from e
407403

tests/integration/test_error_propagation.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111

1212
import pytest
1313
from cassandra import AlreadyExists, ConfigurationException, InvalidRequest
14+
from cassandra.protocol import SyntaxException
1415
from cassandra.query import SimpleStatement
1516

17+
from async_cassandra.exceptions import QueryError
18+
1619

1720
class TestErrorPropagation:
1821
"""Test that various Cassandra errors are properly propagated through the async wrapper."""
@@ -52,12 +55,17 @@ async def test_invalid_query_syntax_error(self, cassandra_cluster):
5255
]
5356

5457
for query in invalid_queries:
55-
with pytest.raises(InvalidRequest) as exc_info:
58+
# The driver raises SyntaxException for syntax errors, not InvalidRequest
59+
# We might get either SyntaxException directly or QueryError wrapping it
60+
with pytest.raises((SyntaxException, QueryError)) as exc_info:
5661
await session.execute(query)
5762

5863
# Verify error details are preserved
5964
assert str(exc_info.value) # Has error message
60-
assert exc_info.type == InvalidRequest # Correct exception type
65+
66+
# If it's wrapped in QueryError, check the cause
67+
if isinstance(exc_info.value, QueryError):
68+
assert isinstance(exc_info.value.__cause__, SyntaxException)
6169

6270
await session.close()
6371

@@ -96,13 +104,18 @@ async def test_table_not_found_error(self, cassandra_cluster):
96104
await session.set_keyspace("test_errors")
97105

98106
# Try to query non-existent table
99-
with pytest.raises(InvalidRequest) as exc_info:
107+
# This should raise InvalidRequest or be wrapped in QueryError
108+
with pytest.raises((InvalidRequest, QueryError)) as exc_info:
100109
await session.execute("SELECT * FROM non_existent_table")
101110

102111
# Error should mention the table
103112
error_msg = str(exc_info.value).lower()
104113
assert "non_existent_table" in error_msg or "table" in error_msg
105114

115+
# If wrapped, check the cause
116+
if isinstance(exc_info.value, QueryError):
117+
assert exc_info.value.__cause__ is not None
118+
106119
# Cleanup
107120
await session.execute("DROP KEYSPACE IF EXISTS test_errors")
108121
await session.close()
@@ -178,10 +191,16 @@ async def test_prepared_statement_invalidation_error(self, cassandra_cluster):
178191
await session.execute("DROP TABLE prepare_test")
179192

180193
# Trying to prepare for non-existent table should fail
181-
with pytest.raises(InvalidRequest) as exc_info:
194+
# This might raise InvalidRequest or be wrapped in QueryError
195+
with pytest.raises((InvalidRequest, QueryError)) as exc_info:
182196
await session.prepare("SELECT * FROM prepare_test WHERE id = ?")
183197

184-
assert "prepare_test" in str(exc_info.value) or "table" in str(exc_info.value).lower()
198+
error_msg = str(exc_info.value).lower()
199+
assert "prepare_test" in error_msg or "table" in error_msg
200+
201+
# If wrapped, check the cause
202+
if isinstance(exc_info.value, QueryError):
203+
assert exc_info.value.__cause__ is not None
185204

186205
# Cleanup
187206
await session.execute("DROP KEYSPACE IF EXISTS test_prepare_errors")
@@ -449,22 +468,26 @@ async def test_timeout_errors(self, cassandra_cluster):
449468
[uuid.uuid4(), f"data_{i}" * 100], # Make data reasonably large
450469
)
451470

452-
# Create a query with very short timeout
453-
# Note: This might not always timeout in fast local environments
454-
stmt = SimpleStatement(
455-
"SELECT * FROM timeout_test", timeout=0.001 # 1ms timeout - very aggressive
456-
)
471+
# Create a simple query
472+
stmt = SimpleStatement("SELECT * FROM timeout_test")
457473

458-
# Even if timeout doesn't trigger, session should handle it gracefully
474+
# Execute with very short timeout
475+
# Note: This might not always timeout in fast local environments
459476
try:
460-
result = await session.execute(stmt)
477+
result = await session.execute(stmt, timeout=0.001) # 1ms timeout - very aggressive
461478
# If it succeeds, that's fine - timeout is environment dependent
462479
rows = list(result)
463480
assert len(rows) > 0
464481
except Exception as e:
465482
# If it times out, verify we get a timeout-related error
483+
# TimeoutError might have empty string representation, check type name too
466484
error_msg = str(e).lower()
467-
assert "timeout" in error_msg or "timed out" in error_msg
485+
error_type = type(e).__name__.lower()
486+
assert (
487+
"timeout" in error_msg
488+
or "timeout" in error_type
489+
or isinstance(e, asyncio.TimeoutError)
490+
)
468491

469492
# Session should still be usable after timeout
470493
result = await session.execute("SELECT count(*) FROM timeout_test")
@@ -598,7 +621,8 @@ async def test_concurrent_schema_modification_errors(self, cassandra_cluster):
598621
)
599622

600623
# Try to create the same table again (without IF NOT EXISTS)
601-
with pytest.raises(AlreadyExists) as exc_info:
624+
# This might raise AlreadyExists or be wrapped in QueryError
625+
with pytest.raises((AlreadyExists, QueryError)) as exc_info:
602626
await session.execute(
603627
"""
604628
CREATE TABLE schema_test (
@@ -608,19 +632,27 @@ async def test_concurrent_schema_modification_errors(self, cassandra_cluster):
608632
"""
609633
)
610634

611-
assert (
612-
"schema_test" in str(exc_info.value) or "already exists" in str(exc_info.value).lower()
613-
)
635+
error_msg = str(exc_info.value).lower()
636+
assert "schema_test" in error_msg or "already exists" in error_msg
637+
638+
# If wrapped, check the cause
639+
if isinstance(exc_info.value, QueryError):
640+
assert exc_info.value.__cause__ is not None
614641

615642
# Try to create duplicate index
616643
await session.execute("CREATE INDEX IF NOT EXISTS idx_data ON schema_test (data)")
617644

618-
with pytest.raises(InvalidRequest) as exc_info:
645+
# This might raise InvalidRequest or be wrapped in QueryError
646+
with pytest.raises((InvalidRequest, QueryError)) as exc_info:
619647
await session.execute("CREATE INDEX idx_data ON schema_test (data)")
620648

621649
error_msg = str(exc_info.value).lower()
622650
assert "index" in error_msg or "already exists" in error_msg
623651

652+
# If wrapped, check the cause
653+
if isinstance(exc_info.value, QueryError):
654+
assert exc_info.value.__cause__ is not None
655+
624656
# Simulate concurrent modifications by trying operations that might conflict
625657
async def create_column(col_name):
626658
try:
@@ -860,7 +892,8 @@ async def test_large_query_handling(self, cassandra_cluster):
860892
await session.execute(insert_many_stmt, [row_id, f"row_{i}", medium_text])
861893

862894
# Select all of them at once
863-
placeholders = ",".join(["?"] * len(row_ids))
895+
# For simple statements, use %s placeholders
896+
placeholders = ",".join(["%s"] * len(row_ids))
864897
select_many = f"SELECT * FROM large_data_test WHERE id IN ({placeholders})"
865898
result = await session.execute(select_many, row_ids)
866899
rows = list(result)

tests/unit/test_auth_failures.py

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from cassandra.cluster import NoHostAvailable
3333

3434
from async_cassandra import AsyncCluster
35-
from async_cassandra.exceptions import ConnectionError, QueryError
35+
from async_cassandra.exceptions import ConnectionError
3636

3737

3838
class TestAuthenticationFailures:
@@ -154,12 +154,11 @@ async def test_auth_failure_during_operation(self):
154154
Unauthorized("User has no SELECT permission on <table test.users>")
155155
)
156156

157-
# Unauthorized is wrapped in QueryError
158-
with pytest.raises(QueryError) as exc_info:
157+
# Unauthorized is passed through directly (not wrapped)
158+
with pytest.raises(Unauthorized) as exc_info:
159159
await session.execute("SELECT * FROM test.users")
160160

161-
assert "Query execution failed: User has no SELECT permission" in str(exc_info.value)
162-
assert isinstance(exc_info.value.cause, Unauthorized)
161+
assert "User has no SELECT permission" in str(exc_info.value)
163162

164163
await session.close()
165164
await async_cluster.shutdown()
@@ -214,12 +213,11 @@ async def test_credential_rotation_reconnect(self):
214213
AuthenticationFailed("Password verification failed")
215214
)
216215

217-
# AuthenticationFailed is wrapped in QueryError
218-
with pytest.raises(QueryError) as exc_info:
216+
# AuthenticationFailed is passed through directly
217+
with pytest.raises(AuthenticationFailed) as exc_info:
219218
await session.execute("SELECT * FROM test")
220219

221220
assert "Password verification failed" in str(exc_info.value)
222-
assert isinstance(exc_info.value.cause, AuthenticationFailed)
223221

224222
await session.close()
225223
await async_cluster.shutdown()
@@ -274,12 +272,11 @@ async def test_authorization_failure_different_operations(self):
274272
Unauthorized(error_msg)
275273
)
276274

277-
# Unauthorized is wrapped in QueryError
278-
with pytest.raises(QueryError) as exc_info:
275+
# Unauthorized is passed through directly
276+
with pytest.raises(Unauthorized) as exc_info:
279277
await session.execute(query)
280278

281279
assert error_msg in str(exc_info.value)
282-
assert isinstance(exc_info.value.cause, Unauthorized)
283280

284281
await session.close()
285282
await async_cluster.shutdown()
@@ -328,12 +325,11 @@ async def test_session_invalidation_on_auth_change(self):
328325
AuthenticationFailed("Session expired")
329326
)
330327

331-
# AuthenticationFailed is wrapped in QueryError
332-
with pytest.raises(QueryError) as exc_info:
328+
# AuthenticationFailed is passed through directly
329+
with pytest.raises(AuthenticationFailed) as exc_info:
333330
await session.execute("SELECT * FROM test")
334331

335332
assert "Session expired" in str(exc_info.value)
336-
assert isinstance(exc_info.value.cause, AuthenticationFailed)
337333

338334
await session.close()
339335
await async_cluster.shutdown()
@@ -382,10 +378,9 @@ async def test_concurrent_auth_failures(self):
382378
# Execute multiple concurrent queries
383379
tasks = [session.execute(f"SELECT * FROM table{i}") for i in range(5)]
384380

385-
# All should fail with QueryError wrapping Unauthorized
381+
# All should fail with Unauthorized directly
386382
results = await asyncio.gather(*tasks, return_exceptions=True)
387-
assert all(isinstance(r, QueryError) for r in results)
388-
assert all(isinstance(r.cause, Unauthorized) for r in results)
383+
assert all(isinstance(r, Unauthorized) for r in results)
389384

390385
await session.close()
391386
await async_cluster.shutdown()
@@ -445,12 +440,11 @@ async def test_auth_error_in_prepared_statement(self):
445440
Unauthorized("User has no MODIFY permission on <table test.users>")
446441
)
447442

448-
# Unauthorized is wrapped in QueryError
449-
with pytest.raises(QueryError) as exc_info:
443+
# Unauthorized is passed through directly
444+
with pytest.raises(Unauthorized) as exc_info:
450445
await session.execute(stmt, [1, "test"])
451446

452447
assert "no MODIFY permission" in str(exc_info.value)
453-
assert isinstance(exc_info.value.cause, Unauthorized)
454448

455449
await session.close()
456450
await async_cluster.shutdown()

0 commit comments

Comments
 (0)