Skip to content

Commit

Permalink
scylla-cql: Improve support of common date and time types
Browse files Browse the repository at this point in the history
Adds support of 'time' and 'chrono' types for CQL queries.

Changelist:
- Rework scylla-cql 'Time', 'Date' and 'Timestamp' types to align with
  CQL data representation;
- Add 'Cql'- prefix to builtin date and time types;
- Implement conversion traits between chrono, time and Cql- types;
- Implement CQL 'Value' trait for 'chrono' and 'time' types;
- Implement 'FromCqlVal' trait for 'chrono' and 'time' types.
- Encapsulate CQL types in a wrapper

Closes #745
  • Loading branch information
Anfid authored and piodul committed Nov 10, 2023
1 parent a5d5345 commit 270e3dc
Show file tree
Hide file tree
Showing 18 changed files with 2,171 additions and 371 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/cassandra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ jobs:
docker compose -f test/cluster/cassandra/docker-compose.yml up -d --wait
# A separate step for building to separate measuring time of compilation and testing
- name: Build the project
run: cargo build --verbose --tests
run: cargo build --verbose --tests --features "full-serialization"
- name: Run tests on cassandra
run: |
CDC='disabled' SCYLLA_URI=172.42.0.2:9042 SCYLLA_URI2=172.42.0.3:9042 SCYLLA_URI3=172.42.0.4:9042 cargo test --verbose -- --skip test_views_in_schema_info --skip test_large_batch_statements
CDC='disabled' SCYLLA_URI=172.42.0.2:9042 SCYLLA_URI2=172.42.0.3:9042 SCYLLA_URI3=172.42.0.4:9042 cargo test --verbose --features "full-serialization" -- --skip test_views_in_schema_info --skip test_large_batch_statements
- name: Stop the cluster
if: ${{ always() }}
run: docker compose -f test/cluster/cassandra/docker-compose.yml stop
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ jobs:
run: cargo clippy --verbose --all-targets -- -Aclippy::uninlined_format_args
- name: Cargo check without features
run: cargo check --all-targets --manifest-path "scylla/Cargo.toml" --features ""
- name: Cargo check with secrecy feature
run: cargo check --all-targets --manifest-path "scylla/Cargo.toml" --features "secret"
- name: Cargo check with all serialization features
run: cargo check --all-targets --manifest-path "scylla/Cargo.toml" --features "full-serialization"
- name: Build scylla-cql
run: cargo build --verbose --all-targets --manifest-path "scylla-cql/Cargo.toml"
run: cargo build --verbose --all-targets --manifest-path "scylla-cql/Cargo.toml" --features "full-serialization"
- name: Build
run: cargo build --verbose --all-targets
run: cargo build --verbose --all-targets --features "full-serialization"
- name: Run tests
run: SCYLLA_URI=172.42.0.2:9042 SCYLLA_URI2=172.42.0.3:9042 SCYLLA_URI3=172.42.0.4:9042 cargo test --verbose
run: SCYLLA_URI=172.42.0.2:9042 SCYLLA_URI2=172.42.0.3:9042 SCYLLA_URI3=172.42.0.4:9042 cargo test --verbose --features "full-serialization"
- name: Stop the cluster
if: ${{ always() }}
run: docker compose -f test/cluster/docker-compose.yml stop
Expand All @@ -63,7 +63,7 @@ jobs:
- name: Use MSRV Cargo.lock
run: mv Cargo.lock.msrv Cargo.lock
- name: MSRV cargo check with features
run: cargo check --verbose --all-targets --locked
run: cargo check --verbose --all-targets --all-features --locked
- name: MSRV cargo check without features
run: cargo check --verbose --all-targets --locked --manifest-path "scylla/Cargo.toml"
- name: MSRV cargo check scylla-cql
Expand Down
29 changes: 29 additions & 0 deletions Cargo.lock.msrv

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions docs/source/data-types/data-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ Database types and their Rust equivalents:
* `Blob` <----> `Vec<u8>`
* `Inet` <----> `std::net::IpAddr`
* `Uuid`, `Timeuuid` <----> `uuid::Uuid`
* `Date` <----> `chrono::NaiveDate`, `u32`
* `Time` <----> `chrono::Duration`
* `Timestamp` <----> `chrono::Duration`
* `Date` <----> `value::CqlDate`, `chrono::NaiveDate`, `time::Date`
* `Time` <----> `value::CqlTime`, `chrono::NaiveTime`, `time::Time`
* `Timestamp` <----> `value::CqlTimestamp`, `chrono::DateTime<Utc>`, `time::OffsetDateTime`
* `Duration` <----> `value::CqlDuration`
* `Decimal` <----> `bigdecimal::Decimal`
* `Varint` <----> `num_bigint::BigInt`
Expand Down Expand Up @@ -55,4 +55,4 @@ Database types and their Rust equivalents:
tuple
udt
```
```
102 changes: 79 additions & 23 deletions docs/source/data-types/date.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,77 @@
# Date

For most use cases `Date` can be represented as
[`chrono::NaiveDate`](https://docs.rs/chrono/0.4.19/chrono/naive/struct.NaiveDate.html).\
`NaiveDate` supports dates from -262145-1-1 to 262143-12-31.
Depending on feature flags, three different types can be used to interact with date.

For dates outside of this range you can use the raw `u32` representation.
Internally [date](https://docs.scylladb.com/stable/cql/types.html#dates) is represented as number of days since
-5877641-06-23 i.e. 2^31 days before unix epoch.

## CqlDate

Without any extra features enabled, only `frame::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`.

### Using `chrono::NaiveDate`:
```rust
# extern crate scylla;
# use scylla::Session;
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use scylla::frame::value::CqlDate;
use scylla::IntoTypedRows;

// 1970-01-08
let to_insert = CqlDate((1 << 31) + 7);

// Insert date into the table
session
.query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,))
.await?;

// Read raw Date from the table
if let Some(rows) = session
.query("SELECT a FROM keyspace.table", &[])
.await?
.rows
{
for row in rows.into_typed::<(CqlDate,)>() {
let (date_value,): (CqlDate,) = row?;
}
}
# Ok(())
# }
```

## chrono::NaiveDate

If full range is not required and `chrono` feature is enabled,
[`chrono::NaiveDate`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html) can be used.
[`chrono::NaiveDate`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html) supports dates from
-262145-01-01 to 262143-12-31.

```rust
# extern crate chrono;
# extern crate scylla;
# use scylla::Session;
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use chrono::NaiveDate;
use scylla::IntoTypedRows;
use chrono::naive::NaiveDate;

// Insert some date into the table
let to_insert: NaiveDate = NaiveDate::from_ymd_opt(2021, 3, 24).unwrap();
// 2021-03-24
let to_insert = NaiveDate::from_ymd_opt(2021, 3, 24).unwrap();

// Insert date into the table
session
.query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,))
.await?;

// Read NaiveDate from the table
if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.rows {
if let Some(rows) = session
.query("SELECT a FROM keyspace.table", &[])
.await?
.rows
{
for row in rows.into_typed::<(NaiveDate,)>() {
let (date_value,): (NaiveDate,) = row?;
}
Expand All @@ -32,32 +80,40 @@ if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.ro
# }
```

### Using raw `u32` representation
Internally `Date` is represented as number of days since -5877641-06-23 i.e. 2^31 days before unix epoch.
## time::Date

Alternatively, `time` feature can be used to enable support of
[`time::Date`](https://docs.rs/time/0.3/time/struct.Date.html).
[`time::Date`](https://docs.rs/time/0.3/time/struct.Date.html)'s value range depends on feature flags, see its
documentation to get more info.

```rust
# extern crate scylla;
# extern crate time;
# use scylla::Session;
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use scylla::frame::value::Date;
use scylla::frame::response::result::CqlValue;
use scylla::IntoTypedRows;
use time::{Date, Month};

// Insert date using raw u32 representation
let to_insert: Date = Date(2_u32.pow(31)); // 1970-01-01
// 2021-03-24
let to_insert = Date::from_calendar_date(2021, Month::March, 24).unwrap();

// Insert date into the table
session
.query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,))
.await?;

// Read raw Date from the table
if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.rows {
for row in rows {
let date_value: u32 = match row.columns[0] {
Some(CqlValue::Date(date_value)) => date_value,
_ => panic!("Should be a date!")
};
// Read Date from the table
if let Some(rows) = session
.query("SELECT a FROM keyspace.table", &[])
.await?
.rows
{
for row in rows.into_typed::<(Date,)>() {
let (date_value,): (Date,) = row?;
}
}
# Ok(())
# }
```
```
112 changes: 98 additions & 14 deletions docs/source/data-types/time.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,117 @@
# Time
`Time` is represented as [`chrono::Duration`](https://docs.rs/chrono/0.4.19/chrono/struct.Duration.html)

Internally `Time` is represented as number of nanoseconds since midnight.
It can't be negative or exceed `86399999999999` (24 hours).
Depending on feature flags used, three different types can be used to interact with time.

When sending in a query it needs to be wrapped in `value::Time` to differentiate from [`Timestamp`](timestamp.md)
Internally [time](https://docs.scylladb.com/stable/cql/types.html#times) is represented as number of nanoseconds since
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
[`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`.

```rust
# extern crate scylla;
# use scylla::Session;
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use scylla::frame::value::CqlTime;
use scylla::IntoTypedRows;

// 64 seconds since midnight
let to_insert = CqlTime(64 * 1_000_000_000);

// Insert time into the table
session
.query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,))
.await?;

// Read time from the table
if let Some(rows) = session
.query("SELECT a FROM keyspace.table", &[])
.await?
.rows
{
for row in rows.into_typed::<(CqlTime,)>() {
let (time_value,): (CqlTime,) = row?;
}
}
# Ok(())
# }
```

## chrono::NaiveTime

If `chrono` feature is enabled, [`chrono::NaiveTime`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html)
can be used to interact with the database. Although chrono can represent leap seconds, they are not supported.
Attempts to convert [`chrono::NaiveTime`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html) with leap
second to `CqlTime` or write it to the database will return an error.

```rust
# extern crate chrono;
# extern crate scylla;
# use scylla::Session;
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use chrono::NaiveTime;
use scylla::IntoTypedRows;

// 01:02:03.456,789,012
let to_insert = NaiveTime::from_hms_nano_opt(1, 2, 3, 456_789_012);

// Insert time into the table
session
.query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,))
.await?;

// Read time from the table
if let Some(rows) = session
.query("SELECT a FROM keyspace.table", &[])
.await?
.rows
{
for row in rows.into_typed::<(NaiveTime,)>() {
let (time_value,): (NaiveTime,) = row?;
}
}
# Ok(())
# }
```

## time::Time

If `time` feature is enabled, [`time::Time`](https://docs.rs/time/0.3/time/struct.Time.html) can be used to interact
with the database.

```rust
# extern crate scylla;
# extern crate time;
# use scylla::Session;
# use std::error::Error;
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
use scylla::IntoTypedRows;
use scylla::frame::value::Time;
use chrono::Duration;
use time::Time;

// 01:02:03.456,789,012
let to_insert = Time::from_hms_nano(1, 2, 3, 456_789_012).unwrap();

// Insert some time into the table
let to_insert: Duration = Duration::seconds(64);
// Insert time into the table
session
.query("INSERT INTO keyspace.table (a) VALUES(?)", (Time(to_insert),))
.query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,))
.await?;

// Read time from the table, no need for a wrapper here
if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.rows {
for row in rows.into_typed::<(Duration,)>() {
let (time_value,): (Duration,) = row?;
// Read time from the table
if let Some(rows) = session
.query("SELECT a FROM keyspace.table", &[])
.await?
.rows
{
for row in rows.into_typed::<(Time,)>() {
let (time_value,): (Time,) = row?;
}
}
# Ok(())
# }
```
```
Loading

0 comments on commit 270e3dc

Please sign in to comment.