Skip to content

Commit

Permalink
Docs: Adapt migration guides to removal of old ser/deser APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
Lorak-mmk committed Jan 30, 2025
1 parent fa66d14 commit 30dc749
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 118 deletions.
31 changes: 5 additions & 26 deletions docs/source/migration-guides/0.11-serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ When executing a statement through the CQL protocol, values for the bind markers

Before 0.11, the driver couldn't do this kind of type checking. For example, in the case of non-batch queries, the only information about the user data it has is that it implements `ValueList` - defined as follows:

```rust
```rust,ignore
# extern crate scylla;
# extern crate bytes;
# use scylla::frame::value::{SerializedResult, SerializeValuesError};
Expand Down Expand Up @@ -76,29 +76,8 @@ If you send simple statements along with non-empty lists of values, the slowdown

In both cases, if the additional roundtrips are unacceptable, you should prepare the statements beforehand and reuse them - which aligns with our general recommendation against use of simple statements in performance sensitive scenarios.

### Migrating from old to new traits *gradually*

In some cases, migration will be as easy as changing occurrences of `IntoUserType` to `SerializeValue` and `ValueList` to `SerializeRow` and adding some atributes for procedural macros. However, if you have a large enough codebase or some custom, complicated implementations of the old traits then you might not want to migrate everything at once. To support gradual migration, the old traits were not removed but rather deprecated, and we introduced some additional utilities.

#### Converting an object implementing an old trait to a new trait

We provide a number of newtype wrappers:

- `ValueAdapter` - implements `SerializeValue` if the type wrapped over implements `Value`,
- `ValueListAdapter` - implements `SerializeRow` if the type wrapped over implements `ValueList`,
- `LegacyBatchValuesAdapter` - implements `BatchValues` if the type wrapped over implements `LegacyBatchValues`.

Note that these wrappers are not zero cost and incur some overhead: in case of `ValueAdapter` and `ValueListAdapter`, the data is first written into a newly allocated buffer and then rewritten to the final buffer. In case of `LegacyBatchValuesAdapter` there shouldn't be any additional allocations unless the implementation has an efficient, non-default `Self::LegacyBatchValuesIterator::write_next_to_request` implementation (which is not the case for the built-in `impl`s).

Naturally, the implementations provided by the wrappers are not type safe as they directly use methods from the old traits.

Conversion in the other direction is not possible.

#### Custom implementations of old traits

It is possible to directly generate an `impl` of `SerializeRow` and `SerializeValue` on a type which implements, respectively, `ValueList` or `Value`, without using the wrappers from the previous section. The following macros are provided:

- `impl_serialize_value_via_value` - implements `SerializeValue` if the type wrapped over implements `Value`,
- `impl_serialize_row_via_value_list` - implements `SerializeRow` if the type wrapped over implements `ValueList`,
### Migrating from old to new traits

The implementations are practically as those generated by the wrappers described in the previous section.
Up to 1.0 the driver offered a set of migration types and macros to allow adapting new API gradually.
Since 1.0 this is no longer the case and the old API is fully removed. In order to use 1.0 you have to
change your code to use new API.
113 changes: 21 additions & 92 deletions docs/source/migration-guides/0.15-deserialization.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# Adjusting code to changes in deserialization API introduced in 0.15

In 0.15, a new deserialization API has been introduced. The new API improves type safety and performance of the old one, so it is highly recommended to switch to it. However, deserialization is an area of the API that users frequently interact with: deserialization traits appear in generic code and custom implementations have been written. In order to make migration easier, the driver still offers the old API, which - while opt-in - can be very easily switched to after version upgrade. Furthermore, a number of facilities have been introduced which help migrate the user code to the new API piece-by-piece.

The old API and migration facilities will be removed in a future major release.
In 0.15, a new deserialization API has been introduced. The new API improves type safety and performance of the old one, so it is highly recommended to switch to it. However, deserialization is an area of the API that users frequently interact with: deserialization traits appear in generic code and custom implementations have been written.
In order to make migration easier, the driver 0.15 still offered the old API, which - while opt-in - can be very easily switched to after version upgrade.
Since 1.0 the old API (and thus the migration utilities too) have been fully removed.

## Introduction

### Old traits

The legacy API works by deserializing rows in the query response to a sequence of `Row`s. The `Row` is just a `Vec<Option<CqlValue>>`, where `CqlValue` is an enum that is able to represent any CQL value.
The legacy API worked by deserializing rows in the query response to a sequence of `Row`s. The `Row` is just a `Vec<Option<CqlValue>>`, where `CqlValue` is an enum that is able to represent any CQL value.

The user can request this type-erased representation to be converted into something useful. There are two traits that power this:
The user could request this type-erased representation to be converted into something useful. There were two traits that powered this:

__`FromRow`__

```rust
```rust,ignore
# extern crate scylla;
# use scylla::frame::response::cql_to_rust::FromRowError;
# use scylla::frame::response::result::Row;
Expand All @@ -25,7 +25,7 @@ pub trait FromRow: Sized {

__`FromCqlVal`__

```rust
```rust,ignore
# extern crate scylla;
# use scylla::frame::response::cql_to_rust::FromCqlValError;
// The `T` parameter is supposed to be either `CqlValue` or `Option<CqlValue>`
Expand All @@ -34,21 +34,21 @@ pub trait FromCqlVal<T>: Sized {
}
```

These traits are implemented for some common types:
These traits were implemented for some common types:

- `FromRow` is implemented for tuples up to 16 elements,
- `FromCqlVal` is implemented for a bunch of types, and each CQL type can be converted to one of them.
- `FromRow` was implemented for tuples up to 16 elements,
- `FromCqlVal` was implemented for a bunch of types, and each CQL type could be converted to one of them.

While it's possible to implement those manually, the driver provides procedural macros for automatic derivation in some cases:
While it was possible to implement those manually, the driver provided procedural macros for automatic derivation in some cases:

- `FromRow` - implements `FromRow` for a struct.
- `FromRow` - implemented `FromRow` for a struct.
- `FromUserType` - generated an implementation of `FromCqlVal` for the struct, trying to parse the CQL value as a UDT.

Note: the macros above have a default behavior that is different than what `FromRow` and `FromUserType` do.
Note: the macros above had a default behavior that is different than what `FromRow` and `FromUserType` do.

### New traits

The new API introduce two analogous traits that, instead of consuming pre-parsed `Vec<Option<CqlValue>>`, are given raw, serialized data with full information about its type. This leads to better performance and allows for better type safety.
The new API introduces two analogous traits that, instead of consuming pre-parsed `Vec<Option<CqlValue>>`, are given raw, serialized data with full information about its type. This leads to better performance and allows for better type safety.

The new traits are:

Expand Down Expand Up @@ -106,7 +106,7 @@ Note that the `QueryResult::rows` field is not available anymore. If you used to

Before:

```rust
```rust,ignore
# extern crate scylla;
# use scylla::client::session::LegacySession;
# use std::error::Error;
Expand Down Expand Up @@ -155,7 +155,7 @@ The `Session::query_iter` and `Session::execute_iter` have been adjusted, too. T

Before:

```rust
```rust,ignore
# extern crate scylla;
# extern crate futures;
# use scylla::client::session::LegacySession;
Expand Down Expand Up @@ -209,89 +209,18 @@ As mentioned in the Introduction section, the driver provides new procedural mac

__`FromRow` vs. `DeserializeRow`__

The impl generated by `FromRow` expects columns to be in the same order as the struct fields. The `FromRow` trait does not have information about column names, so it cannot match them with the struct field names. You can use `enforce_order` and `skip_name_checks` attributes to achieve such behavior via `DeserializeRow` trait.
The impl generated by `FromRow` expected columns to be in the same order as the struct fields. The `FromRow` trait did not have information about column names, so it could not match them with the struct field names. You can use `enforce_order` and `skip_name_checks` attributes to achieve such behavior via `DeserializeRow` trait.

__`FromUserType` vs. `DeserializeValue`__

The impl generated by `FromUserType` expects UDT fields to be in the same order as the struct fields. Field names should be the same both in the UDT and in the struct. You can use the `enforce_order` attribute to achieve such behavior via the `DeserializeValue` trait.
The impl generated by `FromUserType` expected UDT fields to be in the same order as the struct fields. Field names should be the same both in the UDT and in the struct. You can use the `enforce_order` attribute to achieve such behavior via the `DeserializeValue` trait.

### Adjusting custom impls of deserialization traits

If you have a custom type with a hand-written `impl FromRow` or `impl FromCqlVal`, the best thing to do is to just write a new impl for `DeserializeRow` or `DeserializeValue` manually. Although it's technically possible to implement the new traits by using the existing implementation of the old ones, rolling out a new implementation will avoid performance problems related to the inefficient `CqlValue` representation.

## Accessing the old API

Most important types related to deserialization of the old API have been renamed and contain a `Legacy` prefix in their names:

- `Session` -> `LegacySession`
- `CachingSession` -> `LegacyCachingSession`
- `RowIterator` -> `LegacyRowIterator`
- `TypedRowIterator` -> `LegacyTypedRowIterator`
- `QueryResult` -> `LegacyQueryResult`

If you intend to quickly migrate your application by using the old API, you can just import the legacy stuff and alias it as the new one, e.g.:

```rust
# extern crate scylla;
use scylla::client::session::LegacySession as Session;
```

In order to create the `LegacySession` instead of the new `Session`, you need to use `SessionBuilder`'s `build_legacy()` method instead of `build()`:

```rust
# extern crate scylla;
# use scylla::client::session::LegacySession;
# use scylla::client::session_builder::SessionBuilder;
# use std::error::Error;
# async fn check_only_compiles() -> Result<(), Box<dyn Error>> {
let session: LegacySession = SessionBuilder::new()
.known_node("127.0.0.1")
.build_legacy()
.await?;
# Ok(())
# }
```

## Mixing the old and the new API

It is possible to use different APIs in different parts of the program. The `Session` allows to create a `LegacySession` object that has the old API but shares all resources with the session that has the new API (and vice versa - you can create a new API session from the old API session).

```rust
# extern crate scylla;
# use scylla::client::session::{LegacySession, Session};
# use std::error::Error;
# async fn check_only_compiles(new_api_session: &Session) -> Result<(), Box<dyn Error>> {
// All of the session objects below will use the same resources: connections,
// metadata, current keyspace, etc.
let old_api_session: LegacySession = new_api_session.make_shared_session_with_legacy_api();
let another_new_api_session: Session = old_api_session.make_shared_session_with_new_api();
# Ok(())
# }
```

In addition to that, it is possible to convert a `QueryResult` to `LegacyQueryResult`:

```rust
# extern crate scylla;
# use scylla::response::query_result::QueryResult;
# use scylla::response::legacy_query_result::LegacyQueryResult;
# use std::error::Error;
# async fn check_only_compiles(result: QueryResult) -> Result<(), Box<dyn Error>> {
let result: QueryResult = result;
let legacy_result: LegacyQueryResult = result.into_legacy_result()?;
# Ok(())
# }
```

... and `QueryPager` into `LegacyRowIterator`:

```rust
# extern crate scylla;
# use scylla::client::pager::{QueryPager, LegacyRowIterator};
# use std::error::Error;
# async fn check_only_compiles(pager: QueryPager) -> Result<(), Box<dyn Error>> {
let pager: QueryPager = pager;
let legacy_result: LegacyRowIterator = pager.into_legacy();
# Ok(())
# }
```
In 0.15 version of the driver it was possible to access the old API, and to mix usages of the old and new APIs in order
to allow gradual migration.
Since 1.0 this is no longer the case. The application must migrate to the new API in order to use driver 1.0.

0 comments on commit 30dc749

Please sign in to comment.