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

Displayer for QueryRowsResult #1138

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
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,102 changes: 721 additions & 381 deletions Cargo.lock.msrv

Large diffs are not rendered by default.

124 changes: 124 additions & 0 deletions docs/source/statements/displaying-results.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Displaying Query Results

This guide explains how to display query results from the database using the `RowsDisplayer` utility. The `RowsDisplayer` provides a flexible way to format and visualize query results as tables with various customization options, it tries to copy the behavior of the `cqlsh` utility.

## Basic Usage

To display query results, create a `RowsDisplayer` instance and configure its display settings:

```rust
let result: QueryRowsResult = session
.query_unpaged("SELECT * FROM examples_ks.basic1", &[])
.await?
.into_rows_result()?;
let displayer = result.rows_displayer();
println!("{}", displayer);
```

## Display Settings

### Terminal Width

Control the width of the output table:

```rust
displayer.set_terminal_width(80);
```

- Setting width to 0 (default) disables wrapping
- Table will attempt to wrap at specified width while maintaining readability
- Columns are adjusted proportionally when wrapping

### Color Output

Enable or disable colored output:

```rust
displayer.use_color(true); // Enable colors (default)
displayer.use_color(false); // Disable colors
```

When enabled, different data types are displayed in distinct colors:
- Numbers (integers, decimals, floats): Green
- Text and strings: Yellow
- Collections (lists, sets, maps): Blue
- Errors: Red
- Binary data: Magenta

### Binary Data Display

Configure how BLOB data is displayed using `ByteDisplaying`:

```rust
displayer.set_blob_displaying(ByteDisplaying::Hex); // Default
displayer.set_blob_displaying(ByteDisplaying::Ascii);
displayer.set_blob_displaying(ByteDisplaying::Dec);
```

Options:
- `Hex`: Display as hexadecimal values (e.g., "0A FF 42")
- `Ascii`: Display as ASCII characters where possible
- `Dec`: Display as decimal values (e.g., "213 7 66")

Comment on lines +58 to +62
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is that correct? In the implementation I did not see spaces between values. In your screenshot it also looked differently than here.

### Number Formatting

#### Integer Display

Control scientific notation for integers:

```rust
displayer.set_exponent_displaying_integers(true); // Enable scientific notation
displayer.set_exponent_displaying_integers(false); // Disable (default)
```

#### Floating Point Precision

Set the number of decimal places for floating point numbers:

```rust
displayer.set_floating_point_precision(6); // Show 6 decimal places (default)
```

## Example Output

Here's an example of how the output might look with default settings:

```
+----------+-------------+----------------+-------------+
| id | name | values | created_at |
+----------+-------------+----------------+-------------+
| 1 | Example | [1, 2, 3] | 2024-01-06 |
| 2 | Test Data | [4, 5, 6] | 2024-01-06 |
+----------+-------------+----------------+-------------+
```

## Best Practices

1. **Terminal Width**
- Set appropriate terminal width for better readability
- Consider using terminal width detection if available
- Use 0 width for untruncated output

2. **Color Usage**
- Enable colors for better type distinction
- Disable colors when outputting to files or non-terminal destinations
- Consider user accessibility settings

3. **Binary Data**
- Choose appropriate blob display format based on data content
- Use Hex for general binary data
- Use ASCII when data is known to be text
- Use Dec for byte-oriented analysis

4. **Number Formatting**
- Adjust floating point precision based on data requirements
- Enable scientific notation for very large/small numbers
- Consider locale-specific formatting needs

Comment on lines +95 to +117
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this section LLM-generated? It contains a lot of text, yet not a single useful information.

## Implementation Details

The displayer uses the following traits internally:
- `Display` for converting values to strings
- Custom formatting traits for specific types

Output is generated using Rust's formatting system (`fmt::Display`), ensuring efficient memory usage and proper error handling.
Comment on lines +118 to +124
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why would a user care about implementation details?
Why are we talking about efficient memory usage if the formatter uses Row and CqlValue, allocated all the rows at once, and boxes all the values again?

1 change: 1 addition & 0 deletions docs/source/statements/statements.md
Original file line number Diff line number Diff line change
@@ -107,5 +107,6 @@ There is a special functionality to enable [USE keyspace](usekeyspace.md).
schema-agreement
lwt
timeouts
displaying-results
timestamp-generators
```
6 changes: 6 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ scylla = { path = "../scylla", features = [
"num-bigint-04",
"bigdecimal-04",
"metrics",
"result-displayer",
] }
tokio = { version = "1.34", features = ["full"] }
tracing = { version = "0.1.25", features = ["log"] }
@@ -137,3 +138,8 @@ path = "tls-rustls.rs"
[[example]]
name = "execution_profile"
path = "execution_profile.rs"


[[example]]
name = "displayer"
path = "displayer.rs"
Comment on lines +141 to +145
Copy link
Collaborator

Choose a reason for hiding this comment

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

⛏ Missing newline

341 changes: 341 additions & 0 deletions examples/displayer.rs
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's nice that you added a new example for the displayer.
Do you think you could modify our cqlsh-rs example to utilize the new displayer, instead of using its simplistic print_result function?

Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
use anyhow::Result;
use scylla::{
client::{session::Session, session_builder::SessionBuilder},
response::rows_displayer::ByteDisplaying,
};
use std::env;

#[tokio::main]
async fn main() -> Result<()> {
let uri = env::var("SCYLLA_URI").unwrap_or_else(|_| "127.0.0.1:9042".to_string());

println!("Connecting to {} ...", uri);

// prepare the session
let session: Session = SessionBuilder::new().known_node(uri).build().await?;

session.query_unpaged("CREATE KEYSPACE IF NOT EXISTS examples_ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?;

session
.query_unpaged(
"CREATE TABLE IF NOT EXISTS examples_ks.basic (a int, b int, c text, primary key (a, b))",
&[],
)
Comment on lines +19 to +23
Copy link
Collaborator

Choose a reason for hiding this comment

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

Each example should use its own table. Here you are conflicting with the basci example.

.await?;

session
.query_unpaged(
"INSERT INTO examples_ks.basic (a, b, c) VALUES (?, ?, ?)",
(3, 4, "lorem Ipsum jest tekstem stosowanym jako przykładowy wypełniacz w przemyśle poligraficznym. Został po raz pierwszy użyty w XV w. przez nieznanego drukarza do wypełnienia tekstem próbnej książki. Pięć wieków później zaczął być używany przemyśle elektronicznym,"),
)
.await?;

session
.query_unpaged(
"INSERT INTO examples_ks.basic (a, b, c) VALUES (1, 2, 'abc')",
&[],
)
.await?;

let prepared = session
.prepare("INSERT INTO examples_ks.basic (a, b, c) VALUES (?, 7, ?)")
.await?;
session
.execute_unpaged(&prepared, (42_i32, "I'm prepared!"))
.await?;
session
.execute_unpaged(&prepared, (43_i32, "I'm prepared 2!"))
.await?;
session
.execute_unpaged(&prepared, (44_i32, "I'm prepared 3!"))
.await?;

// example 1 - basic table

// fetch the data

// in real life scenario you should always fetch data in pages for maximum performance
let result = session
.query_unpaged("SELECT a, b, c FROM examples_ks.basic", &[])
.await?
.into_rows_result()?;

let displayer = result.rows_displayer();
println!("\nlong text and special characters:");
println!("{}", displayer);

// example 2 - blob, double, float, time, timestamp
session
.query_unpaged(
"CREATE TABLE IF NOT EXISTS examples_ks.basic4 (a int, b int, c text, d int, timest timestamp, bytes blob, fl float, db double, time1 time, primary key (a, c))",
&[],
)
.await?;

session
.query_unpaged(
"INSERT INTO examples_ks.basic4
(a, b, c, d, timest, bytes, fl, db, time1)
VALUES
(1, 10, 'example text', 3, toTimestamp(now()), textAsBlob('sample bytes'), 3.14, 2.718281, '14:30:00');",
&[],
)
.await?;

// fetch the data

// in real life scenario you should always fetch data in pages for maximum performance
let result = session
.query_unpaged("SELECT * FROM examples_ks.basic4", &[])
.await?
.into_rows_result()?;

let mut displayer = result.rows_displayer();
displayer.set_blob_displaying(ByteDisplaying::Ascii);
println!("\nblob, double, float, time, timestamp:");
println!("{}", displayer);

// example 3 - date, duration, ip address, timeuuid

session
.query_unpaged(
"CREATE TABLE IF NOT EXISTS examples_ks.basic6 (a int, timud timeuuid, date1 date, ipaddr inet, dur duration, primary key (a))",
&[],
)
.await?;

// insert some data
session
.query_unpaged(
"INSERT INTO examples_ks.basic6
(a, timud, date1, ipaddr, dur)
VALUES
(1, now(), '2021-01-01', '3.14.15.9', 1h);",
&[],
)
.await?;

session
.query_unpaged(
"INSERT INTO examples_ks.basic6
(a, timud, date1, ipaddr, dur)
VALUES
(3, NOW(), '2024-01-15', '128.0.0.1', 89h4m48s137ms);", // cqlsh prints this as 89.08003805555556h4.8022833333333335m48.137s137.0ms
&[],
)
.await?;

session
.query_unpaged(
"INSERT INTO examples_ks.basic6
(a, timud, date1, ipaddr, dur)
VALUES
(4, NOW(), '2024-01-15', '192.168.0.14', 13y2w89h4m48s137ms);",
&[],
)
.await?;

session
.query_unpaged(
"INSERT INTO examples_ks.basic6 (a, timud, date1, ipaddr, dur)
VALUES (2, NOW(), '2024-02-20', '2001:0db8:0:0::1428:57ab', 5d2h);",
&[],
)
.await?;

session
.query_unpaged(
"INSERT INTO examples_ks.basic6 (a, timud, date1, ipaddr, dur)
VALUES (5, NOW(), '-250000-02-20', '2001:db8::1428:57ab', 1y1mo1w1d1h1m1s700ms);",
&[],
)
.await?;

// fetch the data

// in real life scenario you should always fetch data in pages for maximum performance
let result = session
.query_unpaged("SELECT * FROM examples_ks.basic6", &[])
.await?
.into_rows_result()?;

let mut displayer = result.rows_displayer();
println!("\ndate, duration, ip address, timeuuid:");
println!("{}", displayer);

displayer.set_terminal_width(80);
displayer.use_color(false);
println!("\nno color, width = 80:");
println!("{}", displayer);

// example 4 - List
// Create a table with a list of text
session
.query_unpaged(
"CREATE TABLE IF NOT EXISTS examples_ks.upcoming_calendar ( year int, month int, events list<text>, PRIMARY KEY ( year, month) )",
&[],
)
.await?;

// Insert some data
session
.query_unpaged(
"INSERT INTO examples_ks.upcoming_calendar(year, month, events) VALUES (2015, 6, ['e1', 'e2', 'e3'])",
&[],
)
.await?;

// fetch the data

// in real life scenario you should always fetch data in pages for maximum performance
let result = session
.query_unpaged("SELECT * FROM examples_ks.upcoming_calendar", &[])
.await?
.into_rows_result()?;

let displayer = result.rows_displayer();
println!("\nList:");
println!("{}", displayer);

// example 5 - map
// Create a table with a list of text
session
.query_unpaged(
"CREATE TABLE IF NOT EXISTS examples_ks.cyclist_teams ( id UUID PRIMARY KEY, lastname text, firstname text, teams map<int,text> )",
&[],
)
.await?;

// Insert some data
session
.query_unpaged(
"INSERT INTO examples_ks.cyclist_teams (id, lastname, firstname, teams)
VALUES (
5b6962dd-3f90-4c93-8f61-eabfa4a803e2,
'VOS',
'Marianne',
{2015 : 'Rabobank-Liv Woman Cycling Team', 2014 : 'Rabobank-Liv Woman Cycling Team', 2013 : 'Rabobank-Liv Giant',
2012 : 'Rabobank Women Team', 2011 : 'Nederland bloeit' })",
&[],
)
.await?;

// fetch the data

// in real life scenario you should always fetch data in pages for maximum performance
let result = session
.query_unpaged("SELECT * FROM examples_ks.cyclist_teams", &[])
.await?
.into_rows_result()?;

let displayer = result.rows_displayer();
println!("\nMap:");
println!("{}", displayer);

// example 6 - set
session
.query_unpaged(
"CREATE TABLE IF NOT EXISTS examples_ks.cyclist_career_teams ( id UUID PRIMARY KEY, lastname text, teams set<text> );",
&[],
)
.await?;

// Insert some data
session
.query_unpaged(
"INSERT INTO examples_ks.cyclist_career_teams (id,lastname,teams)
VALUES (5b6962dd-3f90-4c93-8f61-eabfa4a803e2, 'VOS',
{ 'Rabobank-Liv Woman Cycling Team','Rabobank-Liv Giant','Rabobank Women Team','Nederland bloeit' } )",
&[],
)
.await?;

// fetch the data

// in real life scenario you should always fetch data in pages for maximum performance
let result = session
.query_unpaged("SELECT * FROM examples_ks.cyclist_career_teams", &[])
.await?
.into_rows_result()?;

let displayer = result.rows_displayer();
println!("\nSet:");
println!("{}", displayer);

// example 7 - user defined type
session
.query_unpaged(
"CREATE TYPE IF NOT EXISTS examples_ks.basic_info (
birthday timestamp,
nationality text,
weight text,
height text
)",
&[],
)
.await?;

// make table
session
.query_unpaged(
"CREATE TABLE IF NOT EXISTS examples_ks.cyclist_stats ( id uuid PRIMARY KEY, lastname text, basics FROZEN<basic_info>)",
&[],
)
.await?;

// Insert some data
session
.query_unpaged(
"INSERT INTO examples_ks.cyclist_stats (id, lastname, basics) VALUES (
e7ae5cf3-d358-4d99-b900-85902fda9bb0,
'FRAME',
{ birthday : '1993-06-18', nationality : 'New Zealand', weight : null, height : null }
)",
&[],
)
.await?;

// fetch the data

// in real life scenario you should always fetch data in pages for maximum performance
let result = session
.query_unpaged("SELECT * FROM examples_ks.cyclist_stats", &[])
.await?
.into_rows_result()?;

Comment on lines +300 to +305
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do as I say, not as I do?

let displayer = result.rows_displayer();
println!("\nUser defined type:");
println!("{}", displayer);

// example 8 - tuples

// make table
session
.query_unpaged(
"CREATE TABLE IF NOT EXISTS examples_ks.route (race_id int, race_name text, point_id int, lat_long tuple<text, tuple<float,float>>, PRIMARY KEY (race_id, point_id))",
&[],
)
.await?;

// Insert some data
session
.query_unpaged(
"INSERT INTO examples_ks.route (race_id, race_name, point_id, lat_long) VALUES (500, '47th Tour du Pays de Vaud', 2, ('Champagne', (46.833, 6.65)))",
&[],
)
.await?;

// fetch the data

// in real life scenario you should always fetch data in pages for maximum performance
let result = session
.query_unpaged("SELECT * FROM examples_ks.route", &[])
.await?
.into_rows_result()?;

let displayer = result.rows_displayer();
println!("\nTuples:");
println!("{}", displayer);

Ok(())
}
5 changes: 5 additions & 0 deletions scylla/Cargo.toml
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ full-serialization = [
]
metrics = ["dep:histogram"]
unstable-testing = []
result-displayer = ["dep:tabled", "dep:inline_colorization", "dep:num-bigint-04", "dep:bigdecimal-04"]

[dependencies]
scylla-cql = { version = "1.0.0", path = "../scylla-cql" }
@@ -80,6 +81,10 @@ base64 = { version = "0.22.1", optional = true }
rand_pcg = "0.9.0"
socket2 = { version = "0.5.3", features = ["all"] }
lazy_static = "1"
tabled = { version = "0.17.0", features = ["std", "ansi"], optional = true }
inline_colorization = { version = "0.1.6", optional = true }
num-bigint-04 = { package = "num-bigint", version = "0.4", optional = true }
bigdecimal-04 = { package = "bigdecimal", version = "0.4", optional = true }

[dev-dependencies]
num-bigint-03 = { package = "num-bigint", version = "0.3" }
2 changes: 2 additions & 0 deletions scylla/src/response/mod.rs
Original file line number Diff line number Diff line change
@@ -9,6 +9,8 @@
pub mod query_result;
mod request_response;
#[cfg(feature = "result-displayer")]
pub mod rows_displayer;

pub(crate) use request_response::{
NonErrorAuthResponse, NonErrorQueryResponse, NonErrorStartupResponse, QueryResponse,
32 changes: 32 additions & 0 deletions scylla/src/response/query_result.rs
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@ use std::fmt::Debug;
use thiserror::Error;
use uuid::Uuid;

#[cfg(feature = "result-displayer")]
use crate::response::rows_displayer::RowsDisplayer;
use scylla_cql::deserialize::result::TypedRowIterator;
use scylla_cql::deserialize::row::DeserializeRow;
use scylla_cql::deserialize::{DeserializationError, TypeCheckError};
@@ -340,6 +342,14 @@ impl QueryRowsResult {

(raw_rows_with_metadata, tracing_id, warnings)
}

/// Returns a displayer for the rows.
///
/// This method is only available when the `result-displayer` feature is enabled.
#[cfg(feature = "result-displayer")]
pub fn rows_displayer(&self) -> RowsDisplayer<'_> {
RowsDisplayer::new(self)
}
}

/// An error returned by [`QueryResult::into_rows_result`]
@@ -426,6 +436,9 @@ mod tests {
use scylla_cql::frame::response::result::{NativeType, TableSpec};
use scylla_cql::frame::types;

#[cfg(feature = "result-displayer")]
use crate::response::rows_displayer::ByteDisplaying;

use super::*;

const TABLE_SPEC: TableSpec<'static> = TableSpec::borrowed("ks", "tbl");
@@ -709,6 +722,25 @@ mod tests {
}
}
}

// use of QueryRowsResult after use of displayer
#[cfg(feature = "result-displayer")]
{
let rr = sample_raw_rows(2, 1);
let rqr = QueryResult::new(Some(rr), None, Vec::new());
let qr: QueryRowsResult = rqr.into_rows_result().unwrap();
let mut displayer = qr.rows_displayer();
displayer.set_terminal_width(80);
displayer.set_blob_displaying(ByteDisplaying::Hex);
displayer.use_color(true);
let _ = format!("{}", displayer);
let rows = qr.rows::<(&str, bool)>();

let mut rows_data = rows.unwrap();
let row = rows_data.next().unwrap().unwrap();

assert_eq!(row, ("MOCK", true));
}
Comment on lines +726 to +743
Copy link
Collaborator

@Lorak-mmk Lorak-mmk Mar 13, 2025

Choose a reason for hiding this comment

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

This test is already gigantic, can you put it in a separate test?
If you put the tests in the rows_displayer module, you won't need to guard it with cfg.

(@wprzytula is there a reason for this being a 300 lines test instead of few smaller ones? test cases in it seem independent at the first glance).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm.... is this the only test for this feature? We definitely need more.
Check out history tests (history.rs file) - it is also a module that produces text output, and has tests for its correctness.

Copy link
Collaborator

Choose a reason for hiding this comment

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

(btw @wprzytula is there a reason for this being a 300 lines test instead of few smaller ones? test cases in it seem independent at the first glance).

It was easier to be written as a single test, just that.

}

#[test]
1,191 changes: 1,191 additions & 0 deletions scylla/src/response/rows_displayer.rs

Large diffs are not rendered by default.

Loading