scylladb / scylladb/python-rs-driver
Preserve specific serialization exceptions across the Rust driver execution boundary
@pasinskik is already working on this.
Since Jun 15, 2026.
- #117 by @pasinskik — closed without merging
- Dominant language
- Rust
- Stars
- 6
- Forks
- 7
- Avg merge
- 5d 13h
- Merged PRs (30d)
- 9
Description
TL;DR
The driver exposes specific Python serialization exceptions such as ValueOverflowSerializationError and TypeMismatchSerializationError, but they are not currently observable when serialization fails during Session.execute().
Serialization errors are wrapped multiple times while passing through the serialization and execution layers:
DriverSerializationError
→ scylla::serialize::SerializationError
→ DriverSerializationError::ScyllaSerializeFailed
→ scylla::serialize::SerializationError
→ ExecutionError
→ DriverExecuteError
→ Python ExecuteError
Because the final Python conversion only handles the outer ExecutionError, users currently receive a generic ExecuteError with a long nested message.
For example, an integer overflow currently has to be tested by catching ExecuteError and inspecting its message:
with pytest.raises(ExecuteError) as exc_info:
await session.execute(
"INSERT INTO values_table (id, value) VALUES (?, ?)",
[1, 999999999999999999999],
)
assert "value overflow during serialization" in str(exc_info.value).lower()
Similarly, passing a value with the wrong collection shape also raises ExecuteError:
with pytest.raises(ExecuteError) as exc_info:
await session.execute(
"INSERT INTO values_table (id, tags) VALUES (?, ?)",
[1, {"unexpected": "mapping"}],
)
assert "type mismatch" in str(exc_info.value).lower()
The expected behavior would be to preserve the original exception type and structured metadata:
with pytest.raises(ValueOverflowSerializationError) as exc_info:
await session.execute(statement, [1, very_large_integer])
assert exc_info.value.parameter == 1
with pytest.raises(TypeMismatchSerializationError) as exc_info:
await session.execute(statement, [1, invalid_collection])
assert exc_info.value.parameter == 1
This issue tracks investigation of the wrapping and propagation path, with the goal of exposing the existing specific serialization exceptions instead of collapsing all serialization failures into ExecuteError.
Problem
The Python RS driver exposes a detailed serialization exception hierarchy:
SerializationError
├── UnsupportedTypeSerializationError
├── TypeMismatchSerializationError
├── ValueOverflowSerializationError
├── SerializeFailedError
└── PySerializationFailedError
DriverSerializationError also stores structured information about the failure, including:
- the serialization error kind;
- the affected parameter index or name;
- the original Python exception, where applicable.
However, serialization failures that occur during statement execution are currently exposed to Python only as ExecuteError.
For example, an integer overflow currently has to be handled as follows:
with pytest.raises(ExecuteError) as exc_info:
await session.execute(statement, [1, very_large_integer])
assert "value overflow during serialization" in str(exc_info.value).lower()
Ideally, users should be able to catch the corresponding serialization exception directly:
with pytest.raises(ValueOverflowSerializationError) as exc_info:
await session.execute(statement, [1, very_large_integer])
assert exc_info.value.parameter == 1
As a result, the exported serialization subclasses are not currently observable for serialization failures raised through Session.execute().
The same problem may also affect batch execution.
Current error propagation
A serialization failure currently passes through several layers.
1. Python value serialization
The serializers in the Python RS driver initially create a structured error such as:
DriverSerializationError::value_overflow()
DriverSerializationError::type_mismatch(...)
DriverSerializationError::unsupported_type(...)
DriverSerializationError::python_interop_failed(...)
The error is then type-erased so that it can be returned through the Rust driver's serialization API:
impl From<DriverSerializationError> for scylla::serialize::SerializationError {
fn from(err: DriverSerializationError) -> Self {
scylla::serialize::SerializationError::new(err)
}
}
The Rust driver's SerializationError retains the original error internally and supports downcasting, so the original DriverSerializationError may still be recoverable.
2. Parameter location wrapping
Back to the Python RS driver. PyValueList receives the type-erased error from SerializeValue and wraps it again to attach a parameter index or name:
serialize_element(col, &val, row_writer).map_err(|err| {
DriverSerializationError::scylla_serialize_failed(err)
.at_parameter_index(index)
})?;
The mapping-based implementation follows the same pattern using at_parameter_name().
The resulting structure is approximately:
scylla::serialize::SerializationError
└── DriverSerializationError::ScyllaSerializeFailed
├── location: parameter index or name
└── scylla::serialize::SerializationError
└── original DriverSerializationError
This additional wrapping also contributes to repeated SerializationError: fragments in the final exception message.
3. Rust driver execution errors
For prepared statements, the Rust driver serializes values before executing the request.
A serialization failure is represented approximately as:
ExecutionError::BadQuery
└── BadQuery::SerializationError
└── scylla::serialize::SerializationError
For unprepared statements with bound values, the driver may first prepare the statement internally and serialize the values during a request attempt.
In that case, the failure may follow this path:
ExecutionError::LastAttemptError
└── RequestAttemptError::SerializationError
└── scylla::serialize::SerializationError
Other paths, especially batch execution, should also be investigated.
4. Python RS execution error conversion
All Rust driver ExecutionError values are currently converted into:
DriverExecuteError::RustDriverExecutionError
The Python RS conversion then formats the complete nested Rust error as a string and creates an ExecuteError:
DriverExecuteError::RustDriverExecutionError { source } => {
let message = format!("Failed to execute statement: {source}");
ExecuteError::new_err(message)
}
The conversion does not inspect the nested scylla::serialize::SerializationError.
Consequently, the existing conversion:
impl From<DriverSerializationError> for PyErr
is bypassed during normal statement execution. Its mappings to exceptions such as ValueOverflowSerializationError and TypeMismatchSerializationError are therefore not used.
Desired behavior
Client-side serialization failures should be distinguishable from transport, request, and server failures.
When the original error is a DriverSerializationError, Session.execute() should expose the corresponding Python exception.
| Rust serialization error | Python exception |
|---|---|
UnsupportedType |
UnsupportedTypeSerializationError |
TypeMismatch |
TypeMismatchSerializationError |
ValueOverflow |
ValueOverflowSerializationError |
PythonInteropFailed |
PySerializationFailedError |
| Unknown Rust-driver serialization failure | SerializeFailedError |
The Python exception should preserve:
- the parameter index or name in the
parameterattribute; - the original Python exception as
__cause__, where applicable; - a concise message without duplicated serialization wrapper text.
ExecuteError should continue to represent actual execution failures, such as:
- transport failures;
- request timeouts;
- unavailable nodes;
- server-side errors;
- invalid execution configuration.
Suggested investigation and implementation direction
The exact implementation should be determined during investigation, but a possible approach is described below.
1. Detect serialization errors inside ExecutionError
At minimum, inspect the following variants:
ExecutionError::BadQuery(
BadQuery::SerializationError(error)
)
and:
ExecutionError::LastAttemptError(
RequestAttemptError::SerializationError(error)
)
Batch-related execution paths should be checked as well.
2. Downcast the Rust serialization error
Use the Rust driver's downcasting support:
error.downcast_ref::<DriverSerializationError>()
This may allow the original structured error to be recovered from the type-erased scylla::serialize::SerializationError.
3. Handle nested serialization wrappers
When the recovered error is:
DriverSerializationError {
kind: SerializationErrorKind::ScyllaSerializeFailed { source },
location,
}
and source contains another DriverSerializationError, recursively inspect the inner error.
The outer parameter location should be combined with the inner error kind.
For example:
outer error:
parameter = 1
kind = ScyllaSerializeFailed
inner error:
kind = ValueOverflow
should produce:
ValueOverflowSerializationError(parameter=1)
rather than a generic SerializeFailedError or ExecuteError.
4. Consider avoiding the second wrapper
The parameter-location logic in value_list.rs could potentially enrich an existing DriverSerializationError instead of wrapping it in another ScyllaSerializeFailed variant.
Conceptually, instead of:
DriverSerializationError::scylla_serialize_failed(err)
.at_parameter_index(index)
the implementation could:
- downcast
errtoDriverSerializationError; - attach the parameter location directly;
- return the enriched error;
- fall back to
ScyllaSerializeFailedwhen the underlying error is not aDriverSerializationError.
This may simplify both the error structure and the resulting messages.
5. Refactor Python exception construction if necessary
scylla::serialize::SerializationError::downcast_ref() returns a reference to the original error.
The current implementation:
impl From<DriverSerializationError> for PyErr
requires ownership of the error.
It may therefore be necessary to extract the Python exception construction into a helper that accepts a borrowed DriverSerializationError, or to introduce a separate intermediate representation.
6. Define a fallback
When no known DriverSerializationError can be recovered, the failure should fall back to one of:
SerializeFailedError, when it is known to be a serialization failure;ExecuteError, when the execution error cannot be safely classified.
The chosen fallback should be documented as part of the public API.
Acceptance criteria
- Integer overflow during
Session.execute()raisesValueOverflowSerializationError. - Collection shape mismatch raises
TypeMismatchSerializationError. - Unsupported CQL serialization raises
UnsupportedTypeSerializationError. - Python interoperability failure raises
PySerializationFailedError. - The original Python exception is available through
__cause__, where applicable. - Unknown serialization failures raise
SerializeFailedError. - The
parameterattribute contains the correct positional index or parameter name. - Prepared and unprepared statements expose consistent exception types.
- Paged and unpaged execution expose consistent exception types.
- Sequence-based and mapping-based bound values preserve parameter information.
- Serialization failures during batch execution are investigated and handled consistently.
- Transport, timeout, and server failures continue to raise
ExecuteError. - Exception messages do not contain unnecessary repeated serialization wrappers.
- Tests catch specific serialization exceptions instead of parsing
ExecuteErrormessages. - Error-handling documentation is updated after the public behavior is finalized.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.