scylladb / scylladb/python-rs-driver

Preserve specific serialization exceptions across the Rust driver execution boundary

Open
#111 0 comments 0 reactions 1 assignee View on GitHub

@pasinskik is already working on this.

Since Jun 15, 2026.

  • #117 by @pasinskik — closed without merging
enhancement help wanted
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 parameter attribute;
  • 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:

  1. downcast err to DriverSerializationError;
  2. attach the parameter location directly;
  3. return the enriched error;
  4. fall back to ScyllaSerializeFailed when the underlying error is not a DriverSerializationError.

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() raises ValueOverflowSerializationError.
  • 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 parameter attribute 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 ExecuteError messages.
  • Error-handling documentation is updated after the public behavior is finalized.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.