-
Notifications
You must be signed in to change notification settings - Fork 126
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
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") | ||
|
||
### 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would a user care about implementation details? |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⛏ Missing newline |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's nice that you added a new example for the displayer. |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
.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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(()) | ||
} |
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) | ||
} | ||
Michu1596 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? (@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). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It was easier to be written as a single test, just that. |
||
} | ||
|
||
#[test] | ||
|
Large diffs are not rendered by default.
There was a problem hiding this comment.
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.