Skip to content

Commit 55d3a98

Browse files
committed
utils/pretty: add CqlValueDisplayer
A pretty printer for CqlValues is added. The intent is to use it in tracing, notably for including information about requests' partition keys.
1 parent b845c63 commit 55d3a98

File tree

1 file changed

+315
-1
lines changed

1 file changed

+315
-1
lines changed

scylla/src/utils/pretty.rs

Lines changed: 315 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use std::fmt::{LowerHex, UpperHex};
1+
use chrono::{LocalResult, TimeZone, Utc};
2+
use scylla_cql::frame::response::result::CqlValue;
3+
4+
use std::borrow::Borrow;
5+
use std::fmt::{Display, LowerHex, UpperHex};
26

37
pub(crate) struct HexBytes<'a>(pub(crate) &'a [u8]);
48

@@ -19,3 +23,313 @@ impl<'a> UpperHex for HexBytes<'a> {
1923
Ok(())
2024
}
2125
}
26+
27+
// Displays a CqlValue. The syntax should resemble the CQL literal syntax
28+
// (but no guarantee is given that it's always the same).
29+
pub(crate) struct CqlValueDisplayer<C>(pub C);
30+
31+
impl<C> Display for CqlValueDisplayer<C>
32+
where
33+
C: Borrow<CqlValue>,
34+
{
35+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36+
match self.0.borrow() {
37+
// Scalar types
38+
CqlValue::Ascii(a) => write!(f, "{}", CqlStringLiteralDisplayer(a))?,
39+
CqlValue::Text(t) => write!(f, "{}", CqlStringLiteralDisplayer(t))?,
40+
CqlValue::Blob(b) => write!(f, "0x{:x}", HexBytes(b))?,
41+
CqlValue::Empty => write!(f, "0x")?,
42+
CqlValue::Decimal(d) => write!(f, "{}", d)?,
43+
CqlValue::Float(fl) => write!(f, "{}", fl)?,
44+
CqlValue::Double(d) => write!(f, "{}", d)?,
45+
CqlValue::Boolean(b) => write!(f, "{}", b)?,
46+
CqlValue::Int(i) => write!(f, "{}", i)?,
47+
CqlValue::BigInt(bi) => write!(f, "{}", bi)?,
48+
CqlValue::Inet(i) => write!(f, "'{}'", i)?,
49+
CqlValue::SmallInt(si) => write!(f, "{}", si)?,
50+
CqlValue::TinyInt(ti) => write!(f, "{}", ti)?,
51+
CqlValue::Varint(vi) => write!(f, "{}", vi)?,
52+
CqlValue::Counter(c) => write!(f, "{}", c.0)?,
53+
CqlValue::Date(d) => {
54+
let days_since_epoch = chrono::Duration::days(*d as i64 - (1 << 31));
55+
56+
// TODO: chrono::NaiveDate does not handle the whole range
57+
// supported by the `date` datatype
58+
match chrono::NaiveDate::from_ymd_opt(1970, 1, 1)
59+
.unwrap()
60+
.checked_add_signed(days_since_epoch)
61+
{
62+
Some(d) => write!(f, "'{}'", d)?,
63+
None => f.write_str("<date out of representable range>")?,
64+
}
65+
}
66+
CqlValue::Duration(d) => write!(f, "{}mo{}d{}ns", d.months, d.days, d.nanoseconds)?,
67+
CqlValue::Time(t) => {
68+
write!(
69+
f,
70+
"'{:02}:{:02}:{:02}.{:09}'",
71+
t.num_hours() % 24,
72+
t.num_minutes() % 60,
73+
t.num_seconds() % 60,
74+
t.num_nanoseconds().unwrap_or(0) % 1_000_000_000,
75+
)?;
76+
}
77+
CqlValue::Timestamp(t) => {
78+
match Utc.timestamp_millis_opt(t.num_milliseconds()) {
79+
LocalResult::Ambiguous(_, _) => unreachable!(), // not supposed to happen with timestamp_millis_opt
80+
LocalResult::Single(d) => {
81+
write!(f, "{}", d.format("'%Y-%m-%d %H:%M:%S%.3f%z'"))?
82+
}
83+
LocalResult::None => f.write_str("<timestamp out of representable range>")?,
84+
}
85+
}
86+
CqlValue::Timeuuid(t) => write!(f, "{}", t)?,
87+
CqlValue::Uuid(u) => write!(f, "{}", u)?,
88+
89+
// Compound types
90+
CqlValue::Tuple(t) => {
91+
f.write_str("(")?;
92+
CommaSeparatedDisplayer(
93+
t.iter()
94+
.map(|x| MaybeNullDisplayer(x.as_ref().map(CqlValueDisplayer))),
95+
)
96+
.fmt(f)?;
97+
f.write_str(")")?;
98+
}
99+
CqlValue::List(v) => {
100+
f.write_str("[")?;
101+
CommaSeparatedDisplayer(v.iter().map(CqlValueDisplayer)).fmt(f)?;
102+
f.write_str("]")?;
103+
}
104+
CqlValue::Set(v) => {
105+
f.write_str("{")?;
106+
CommaSeparatedDisplayer(v.iter().map(CqlValueDisplayer)).fmt(f)?;
107+
f.write_str("}")?;
108+
}
109+
CqlValue::Map(m) => {
110+
f.write_str("{")?;
111+
CommaSeparatedDisplayer(
112+
m.iter()
113+
.map(|(k, v)| PairDisplayer(CqlValueDisplayer(k), CqlValueDisplayer(v))),
114+
)
115+
.fmt(f)?;
116+
f.write_str("}")?;
117+
}
118+
CqlValue::UserDefinedType {
119+
keyspace: _,
120+
type_name: _,
121+
fields,
122+
} => {
123+
f.write_str("{")?;
124+
CommaSeparatedDisplayer(fields.iter().map(|(k, v)| {
125+
PairDisplayer(k, MaybeNullDisplayer(v.as_ref().map(CqlValueDisplayer)))
126+
}))
127+
.fmt(f)?;
128+
f.write_str("}")?;
129+
}
130+
}
131+
Ok(())
132+
}
133+
}
134+
135+
pub(crate) struct CqlStringLiteralDisplayer<'a>(&'a str);
136+
137+
impl<'a> Display for CqlStringLiteralDisplayer<'a> {
138+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139+
// CQL string literals use single quotes. The only character that
140+
// needs escaping is singular quote, and escaping is done by repeating
141+
// the quote character.
142+
f.write_str("'")?;
143+
let mut first = true;
144+
for part in self.0.split('\'') {
145+
if first {
146+
first = false;
147+
} else {
148+
f.write_str("''")?;
149+
}
150+
f.write_str(part)?;
151+
}
152+
f.write_str("'")?;
153+
Ok(())
154+
}
155+
}
156+
157+
pub(crate) struct CommaSeparatedDisplayer<I>(pub I);
158+
159+
impl<I, T> Display for CommaSeparatedDisplayer<I>
160+
where
161+
I: Iterator<Item = T> + Clone,
162+
T: Display,
163+
{
164+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165+
let mut first = true;
166+
for t in self.0.clone() {
167+
if first {
168+
first = false;
169+
} else {
170+
f.write_str(",")?;
171+
}
172+
write!(f, "{}", t)?;
173+
}
174+
Ok(())
175+
}
176+
}
177+
178+
pub(crate) struct PairDisplayer<K, V>(K, V);
179+
180+
impl<K, V> Display for PairDisplayer<K, V>
181+
where
182+
K: Display,
183+
V: Display,
184+
{
185+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186+
write!(f, "{}:{}", self.0, self.1)
187+
}
188+
}
189+
190+
pub(crate) struct MaybeNullDisplayer<T>(Option<T>);
191+
192+
impl<T> Display for MaybeNullDisplayer<T>
193+
where
194+
T: Display,
195+
{
196+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197+
match &self.0 {
198+
None => write!(f, "null")?,
199+
Some(v) => write!(f, "{}", v)?,
200+
}
201+
Ok(())
202+
}
203+
}
204+
205+
#[cfg(test)]
206+
mod tests {
207+
use std::str::FromStr;
208+
use std::time::Duration;
209+
210+
use bigdecimal::BigDecimal;
211+
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
212+
213+
use scylla_cql::frame::response::result::CqlValue;
214+
use scylla_cql::frame::value::CqlDuration;
215+
216+
use crate::utils::pretty::CqlValueDisplayer;
217+
218+
#[test]
219+
fn test_cql_value_displayer() {
220+
assert_eq!(
221+
format!("{}", CqlValueDisplayer(CqlValue::Boolean(true))),
222+
"true"
223+
);
224+
assert_eq!(format!("{}", CqlValueDisplayer(CqlValue::Int(123))), "123");
225+
assert_eq!(
226+
format!(
227+
"{}",
228+
CqlValueDisplayer(CqlValue::Decimal(BigDecimal::from_str("123.456").unwrap()))
229+
),
230+
"123.456"
231+
);
232+
assert_eq!(
233+
format!("{}", CqlValueDisplayer(CqlValue::Float(12.75))),
234+
"12.75"
235+
);
236+
assert_eq!(
237+
format!(
238+
"{}",
239+
CqlValueDisplayer(CqlValue::Text("Ala ma kota".to_owned()))
240+
),
241+
"'Ala ma kota'"
242+
);
243+
assert_eq!(
244+
format!("{}", CqlValueDisplayer(CqlValue::Text("Foo's".to_owned()))),
245+
"'Foo''s'"
246+
);
247+
248+
// Time types are the most tricky
249+
assert_eq!(
250+
format!("{}", CqlValueDisplayer(CqlValue::Date(40 + (1 << 31)))),
251+
"'1970-02-10'"
252+
);
253+
assert_eq!(
254+
format!(
255+
"{}",
256+
CqlValueDisplayer(CqlValue::Duration(CqlDuration {
257+
months: 1,
258+
days: 2,
259+
nanoseconds: 3,
260+
}))
261+
),
262+
"1mo2d3ns"
263+
);
264+
let t = Duration::from_nanos(123)
265+
+ Duration::from_secs(4)
266+
+ Duration::from_secs(5 * 60)
267+
+ Duration::from_secs(6 * 60 * 60);
268+
assert_eq!(
269+
format!(
270+
"{}",
271+
CqlValueDisplayer(CqlValue::Time(chrono::Duration::from_std(t).unwrap()))
272+
),
273+
"'06:05:04.000000123'"
274+
);
275+
276+
let t = NaiveDate::from_ymd_opt(2005, 4, 2)
277+
.unwrap()
278+
.and_time(NaiveTime::from_hms_opt(19, 37, 42).unwrap());
279+
assert_eq!(
280+
format!(
281+
"{}",
282+
CqlValueDisplayer(CqlValue::Timestamp(
283+
t.signed_duration_since(NaiveDateTime::default())
284+
))
285+
),
286+
"'2005-04-02 19:37:42.000+0000'"
287+
);
288+
289+
// Compound types
290+
let list_or_set = vec![CqlValue::Int(1), CqlValue::Int(3), CqlValue::Int(2)];
291+
assert_eq!(
292+
format!("{}", CqlValueDisplayer(CqlValue::List(list_or_set.clone()))),
293+
"[1,3,2]"
294+
);
295+
assert_eq!(
296+
format!("{}", CqlValueDisplayer(CqlValue::Set(list_or_set.clone()))),
297+
"{1,3,2}"
298+
);
299+
300+
let tuple: Vec<_> = list_or_set
301+
.into_iter()
302+
.map(Some)
303+
.chain(std::iter::once(None))
304+
.collect();
305+
assert_eq!(
306+
format!("{}", CqlValueDisplayer(CqlValue::Tuple(tuple))),
307+
"(1,3,2,null)"
308+
);
309+
310+
let map = vec![
311+
(CqlValue::Text("foo".to_owned()), CqlValue::Int(123)),
312+
(CqlValue::Text("bar".to_owned()), CqlValue::Int(321)),
313+
];
314+
assert_eq!(
315+
format!("{}", CqlValueDisplayer(CqlValue::Map(map))),
316+
"{'foo':123,'bar':321}"
317+
);
318+
319+
let fields = vec![
320+
("foo".to_owned(), Some(CqlValue::Int(123))),
321+
("bar".to_owned(), Some(CqlValue::Int(321))),
322+
];
323+
assert_eq!(
324+
format!(
325+
"{}",
326+
CqlValueDisplayer(CqlValue::UserDefinedType {
327+
keyspace: "ks".to_owned(),
328+
type_name: "typ".to_owned(),
329+
fields,
330+
})
331+
),
332+
"{foo:123,bar:321}"
333+
);
334+
}
335+
}

0 commit comments

Comments
 (0)