Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove legacy serialization and deserialization APIs #1184

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9d70c3e
Remove '#[allow(deprecated)]' from `NextRowError` import
Lorak-mmk Jan 30, 2025
4a5dca6
hygiene.rs: Remove `#![allow(deprecated)]`
Lorak-mmk Jan 30, 2025
cbeb687
NewSessionError: `#[allow(deprecated)]`
Lorak-mmk Jan 30, 2025
13e4819
scylla_cql: Migrate value_tests to new framework
Lorak-mmk Jan 30, 2025
feed4fc
conf.py: Allow `rust,ignore` lexer
Lorak-mmk Jan 30, 2025
5606f1e
Docs: Adapt migration guides to removal of old ser/deser APIs
Lorak-mmk Jan 30, 2025
737fcfc
Remove LegacyCachingSession
Lorak-mmk Jan 28, 2025
32236ad
De-genericize CachingSession
Lorak-mmk Jan 28, 2025
fe3384d
Remove LegacySession
Lorak-mmk Jan 28, 2025
41f352d
De-genericize Session
Lorak-mmk Jan 28, 2025
808abf7
Remove legacy pager
Lorak-mmk Jan 30, 2025
408d2bb
Remove LegacyQueryResult
Lorak-mmk Jan 30, 2025
8d077ef
Remove legacy deserialization derive macros
Lorak-mmk Jan 28, 2025
552dd18
scylla_cql: Remove cql_to_rust module
Lorak-mmk Jan 28, 2025
98160e8
Remove legacy serialization derive macros
Lorak-mmk Jan 29, 2025
df42d08
scylla: Remove `SerializeValuesError` re-export and usage
Lorak-mmk Jan 30, 2025
d2e69e9
scylla_cql: Migrate a remaining test in result.rs to new API
Lorak-mmk Jan 30, 2025
d1fdfb5
Remove adapters between new and old serialization frameworks
Lorak-mmk Jan 30, 2025
6f80ac7
scylla_cql: Remove old serialization API
Lorak-mmk Jan 30, 2025
e9fe2ea
macro_internal: Remove CqlValue and Row
Lorak-mmk Jan 29, 2025
6422f76
Make Cluster not clonable
Lorak-mmk Jan 30, 2025
33fbda7
value_tests.rs: Remove comments that mention legacy traits.
Lorak-mmk Jan 30, 2025
294ff98
session_builder: remove mentions of old deser API
wprzytula Jan 30, 2025
e894b24
docs: batches: ValueList -> SerializeRow
wprzytula Jan 30, 2025
3d03639
scylla_cql/serialize: Extract value tests to a separate file
Lorak-mmk Jan 31, 2025
3f6fffb
scylla_cql/serialize/value: Move doctests to test file
Lorak-mmk Jan 31, 2025
53b1952
scylla_cql/serialize: Extract row tests to a separate file
Lorak-mmk Jan 31, 2025
5a4b655
scylla_cql/serialize/row: Move doctests to test file
Lorak-mmk Jan 31, 2025
13dcfed
frame/value_tests: Use `do_serialize` from serialization tests
Lorak-mmk Jan 31, 2025
6e1a0cc
frame/value_tests: Use other utils from serialization tests
Lorak-mmk Jan 31, 2025
1ab4b60
Move relevant tests tests from frame/value_tests to serialize/value_t…
Lorak-mmk Jan 31, 2025
7fae60a
row_tests.rs: Remove `col_spec` helper
Lorak-mmk Jan 31, 2025
3dbcf73
row_tests.rs: Move `test_dyn_serialize_row` after util functions
Lorak-mmk Jan 31, 2025
1872316
frame/value_tests.rs: Rename col_spec to col
Lorak-mmk Jan 31, 2025
47196d9
Move relevant tests tests from frame/value_tests to serialize/row_tests
Lorak-mmk Jan 31, 2025
9fc9069
Move batch tests from frame/value_tests.rs to serialize/batch_tests.rs
Lorak-mmk Jan 31, 2025
62fa3a8
Move scylla-cql/frame/value.rs to scylla-cql/value.rs
Lorak-mmk Jan 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
# Setup Sphinx
def setup(sphinx):
lexers['rust'] = RustLexer()
lexers['rust,ignore'] = RustLexer()
lexers['toml'] = TOMLLexer()
Comment on lines 64 to 68
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 I wasn't aware that there is such a place where we can configure how ```<lang> is parsed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me neither. In general I don't know much abut how Scylla documentation building works.


# -- Options for not found extension
Expand Down
2 changes: 1 addition & 1 deletion docs/source/data-types/counter.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use futures::TryStreamExt;
use scylla::frame::value::Counter;
use scylla::value::Counter;

// Add to counter value
let to_add: Counter = Counter(100);
Expand Down
4 changes: 2 additions & 2 deletions docs/source/data-types/date.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Internally [date](https://docs.scylladb.com/stable/cql/types.html#dates) is repr

## CqlDate

Without any extra features enabled, only `frame::value::CqlDate` is available. It's an
Without any extra features enabled, only `value::CqlDate` is available. It's an
[`u32`](https://doc.rust-lang.org/std/primitive.u32.html) wrapper and it matches the internal date representation.

However, for most use cases other types are more practical. See following sections for `chrono` and `time`.
Expand All @@ -18,7 +18,7 @@ However, for most use cases other types are more practical. See following sectio
# use scylla::client::session::Session;
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use scylla::frame::value::CqlDate;
use scylla::value::CqlDate;
use futures::TryStreamExt;

// 1970-01-08
Expand Down
2 changes: 1 addition & 1 deletion docs/source/data-types/decimal.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Without any feature flags, the user can interact with `decimal` type by making u
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use futures::TryStreamExt;
use scylla::frame::value::CqlDecimal;
use scylla::value::CqlDecimal;
use std::str::FromStr;

// Insert a decimal (123.456) into the table
Expand Down
2 changes: 1 addition & 1 deletion docs/source/data-types/duration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use futures::TryStreamExt;
use scylla::frame::value::CqlDuration;
use scylla::value::CqlDuration;

// Insert some duration into the table
let to_insert: CqlDuration = CqlDuration { months: 1, days: 2, nanoseconds: 3 };
Expand Down
4 changes: 2 additions & 2 deletions docs/source/data-types/time.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ midnight. It can't be negative or exceed `86399999999999` (23:59:59.999999999).

## CqlTime

Without any extra features enabled, only `frame::value::CqlTime` is available. It's an
Without any extra features enabled, only `value::CqlTime` is available. It's an
[`i64`](https://doc.rust-lang.org/std/primitive.i64.html) wrapper and it matches the internal time representation.

However, for most use cases other types are more practical. See following sections for `chrono` and `time`.
Expand All @@ -18,7 +18,7 @@ However, for most use cases other types are more practical. See following sectio
# use scylla::client::session::Session;
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use scylla::frame::value::CqlTime;
use scylla::value::CqlTime;
use futures::TryStreamExt;

// 64 seconds since midnight
Expand Down
4 changes: 2 additions & 2 deletions docs/source/data-types/timestamp.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Internally [timestamp](https://docs.scylladb.com/stable/cql/types.html#timestamp

## CqlTimestamp

Without any extra features enabled, only `frame::value::CqlTimestamp` is available. It's an
Without any extra features enabled, only `value::CqlTimestamp` is available. It's an
[`i64`](https://doc.rust-lang.org/std/primitive.i64.html) wrapper and it matches the internal time representation. It's
the only type that supports full range of values that database accepts.

Expand All @@ -19,7 +19,7 @@ However, for most use cases other types are more practical. See following sectio
# use scylla::client::session::Session;
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use scylla::frame::value::CqlTimestamp;
use scylla::value::CqlTimestamp;
use futures::TryStreamExt;

// 64 seconds since unix epoch, 1970-01-01 00:01:04
Expand Down
4 changes: 2 additions & 2 deletions docs/source/data-types/timeuuid.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Also, `value::CqlTimeuuid` is a wrapper for `uuid::Uuid` with custom ordering lo
# use std::str::FromStr;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use futures::TryStreamExt;
use scylla::frame::value::CqlTimeuuid;
use scylla::value::CqlTimeuuid;

// Insert some timeuuid into the table
let to_insert: CqlTimeuuid = CqlTimeuuid::from_str("8e14e760-7fa8-11eb-bc66-000000000001")?;
Expand Down Expand Up @@ -51,7 +51,7 @@ and now you're gonna be able to use the `uuid::v1` features:
# use std::error::Error;
# use std::str::FromStr;
use futures::TryStreamExt;
use scylla::frame::value::CqlTimeuuid;
use scylla::value::CqlTimeuuid;
use uuid::Uuid;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {

Expand Down
1 change: 0 additions & 1 deletion docs/source/data-types/udt.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ Now it can be sent and received just like any other CQL value:
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use futures::TryStreamExt;
use scylla::macros::{DeserializeValue, SerializeValue};
use scylla::cql_to_rust::FromCqlVal;

#[derive(Debug, DeserializeValue, SerializeValue)]
struct MyType {
Expand Down
33 changes: 6 additions & 27 deletions docs/source/migration-guides/0.11-serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ 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
Lorak-mmk marked this conversation as resolved.
Show resolved Hide resolved
# extern crate scylla;
# extern crate bytes;
# use scylla::frame::value::{SerializedResult, SerializeValuesError};
# use scylla::value::{SerializedResult, SerializeValuesError};
# use bytes::BufMut;

pub trait ValueList {
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.
114 changes: 21 additions & 93 deletions docs/source/migration-guides/0.15-deserialization.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
# 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. 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 +24,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 +33,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 +105,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 +154,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 +208,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.
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.

## 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.
2 changes: 1 addition & 1 deletion docs/source/queries/batch.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Length of batch values must be equal to the number of statements in a batch.\
Each statement must have its values specified, even if they are empty.

Values passed to `Session::batch` must implement the trait `BatchValues`.\
By default this includes tuples `()` and slices `&[]` of tuples and slices which implement `ValueList`.
By default this includes tuples `()` and slices `&[]` of tuples and slices which implement `SerializeRow`.

Example:
```rust
Expand Down
2 changes: 1 addition & 1 deletion docs/source/queries/values.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Using `Unset` results in better performance:
# use scylla::client::session::Session;
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use scylla::frame::value::{MaybeUnset, Unset};
use scylla::value::{MaybeUnset, Unset};

// Inserting a null results in suboptimal performance
let null_i32: Option<i32> = None;
Expand Down
2 changes: 1 addition & 1 deletion examples/cql-time-types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use futures::{StreamExt as _, TryStreamExt as _};
use scylla::client::session::Session;
use scylla::client::session_builder::SessionBuilder;
use scylla::frame::response::result::CqlValue;
use scylla::frame::value::{CqlDate, CqlTime, CqlTimestamp};
use scylla::value::{CqlDate, CqlTime, CqlTimestamp};
use std::env;

#[tokio::main]
Expand Down
Loading
Loading