Skip to main content

cqlsh_rs/driver/
types.rs

1//! CQL value types and result set representations.
2//!
3//! Provides an intermediate type layer between the scylla driver's native types
4//! and the cqlsh-rs formatting/display layer. This decouples the driver
5//! implementation from the rest of the application.
6
7use std::fmt;
8use std::net::IpAddr;
9
10use bigdecimal::BigDecimal;
11use chrono::{NaiveDate, NaiveTime};
12use num_bigint::BigInt;
13use uuid::Uuid;
14
15/// A single CQL value, mirroring all CQL data types.
16#[derive(Debug, Clone, PartialEq)]
17pub enum CqlValue {
18    /// ASCII string.
19    Ascii(String),
20    /// Boolean value.
21    Boolean(bool),
22    /// Arbitrary-precision integer.
23    BigInt(i64),
24    /// Arbitrary blob of bytes.
25    Blob(Vec<u8>),
26    /// Counter value.
27    Counter(i64),
28    /// Arbitrary-precision decimal.
29    Decimal(BigDecimal),
30    /// Double-precision floating point.
31    Double(f64),
32    /// Duration (months, days, nanoseconds).
33    Duration {
34        months: i32,
35        days: i32,
36        nanoseconds: i64,
37    },
38    /// Single-precision floating point.
39    Float(f32),
40    /// 32-bit integer.
41    Int(i32),
42    /// 16-bit integer (smallint).
43    SmallInt(i16),
44    /// 8-bit integer (tinyint).
45    TinyInt(i8),
46    /// Timestamp (milliseconds since Unix epoch).
47    Timestamp(i64),
48    /// UUID.
49    Uuid(Uuid),
50    /// TimeUUID (v1).
51    TimeUuid(Uuid),
52    /// IP address (inet).
53    Inet(IpAddr),
54    /// Date (days since epoch: 2^31).
55    Date(NaiveDate),
56    /// Time of day (nanoseconds since midnight).
57    Time(NaiveTime),
58    /// UTF-8 string.
59    Text(String),
60    /// Arbitrary-precision integer.
61    Varint(BigInt),
62    /// Ordered list of values.
63    List(Vec<CqlValue>),
64    /// Set of values.
65    Set(Vec<CqlValue>),
66    /// Map of key-value pairs.
67    Map(Vec<(CqlValue, CqlValue)>),
68    /// Tuple of values.
69    Tuple(Vec<Option<CqlValue>>),
70    /// User-defined type.
71    UserDefinedType {
72        keyspace: String,
73        type_name: String,
74        fields: Vec<(String, Option<CqlValue>)>,
75    },
76    /// Null/empty value.
77    Null,
78    /// Unset value (for prepared statement bindings).
79    Unset,
80}
81
82impl fmt::Display for CqlValue {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            CqlValue::Ascii(s) | CqlValue::Text(s) => write!(f, "{s}"),
86            CqlValue::Boolean(b) => {
87                if *b {
88                    write!(f, "True")
89                } else {
90                    write!(f, "False")
91                }
92            }
93            CqlValue::BigInt(v) => write!(f, "{v}"),
94            CqlValue::Blob(bytes) => {
95                write!(f, "0x")?;
96                for b in bytes {
97                    write!(f, "{b:02x}")?;
98                }
99                Ok(())
100            }
101            CqlValue::Counter(v) => write!(f, "{v}"),
102            CqlValue::Decimal(v) => write!(f, "{v}"),
103            CqlValue::Double(v) => format_float64(f, *v),
104            CqlValue::Duration {
105                months,
106                days,
107                nanoseconds,
108            } => write!(f, "{months}mo{days}d{nanoseconds}ns"),
109            CqlValue::Float(v) => format_float32(f, *v),
110            CqlValue::Int(v) => write!(f, "{v}"),
111            CqlValue::SmallInt(v) => write!(f, "{v}"),
112            CqlValue::TinyInt(v) => write!(f, "{v}"),
113            CqlValue::Timestamp(millis) => format_timestamp(f, *millis),
114            CqlValue::Uuid(u) | CqlValue::TimeUuid(u) => write!(f, "{u}"),
115            CqlValue::Inet(addr) => write!(f, "{addr}"),
116            CqlValue::Date(d) => write!(f, "{d}"),
117            CqlValue::Time(t) => write!(f, "{t}"),
118            CqlValue::Varint(v) => write!(f, "{v}"),
119            CqlValue::List(items) | CqlValue::Set(items) => {
120                let is_set = matches!(self, CqlValue::Set(_));
121                let (open, close) = if is_set { ('{', '}') } else { ('[', ']') };
122                write!(f, "{open}")?;
123                for (i, item) in items.iter().enumerate() {
124                    if i > 0 {
125                        write!(f, ", ")?;
126                    }
127                    write_cql_literal(f, item)?;
128                }
129                write!(f, "{close}")
130            }
131            CqlValue::Map(entries) => {
132                write!(f, "{{")?;
133                for (i, (k, v)) in entries.iter().enumerate() {
134                    if i > 0 {
135                        write!(f, ", ")?;
136                    }
137                    write_cql_literal(f, k)?;
138                    write!(f, ": ")?;
139                    write_cql_literal(f, v)?;
140                }
141                write!(f, "}}")
142            }
143            CqlValue::Tuple(items) => {
144                write!(f, "(")?;
145                for (i, item) in items.iter().enumerate() {
146                    if i > 0 {
147                        write!(f, ", ")?;
148                    }
149                    match item {
150                        Some(v) => write_cql_literal(f, v)?,
151                        None => write!(f, "null")?,
152                    }
153                }
154                write!(f, ")")
155            }
156            CqlValue::UserDefinedType { fields, .. } => {
157                write!(f, "{{")?;
158                for (i, (name, value)) in fields.iter().enumerate() {
159                    if i > 0 {
160                        write!(f, ", ")?;
161                    }
162                    write!(f, "{name}: ")?;
163                    match value {
164                        Some(v) => write_cql_literal(f, v)?,
165                        None => write!(f, "null")?,
166                    }
167                }
168                write!(f, "}}")
169            }
170            CqlValue::Null => Ok(()),
171            CqlValue::Unset => write!(f, "<unset>"),
172        }
173    }
174}
175
176/// Write a CQL literal value, quoting strings.
177fn write_cql_literal(f: &mut fmt::Formatter<'_>, value: &CqlValue) -> fmt::Result {
178    match value {
179        CqlValue::Ascii(s) | CqlValue::Text(s) => {
180            write!(f, "'{}'", s.replace('\'', "''"))
181        }
182        other => write!(f, "{other}"),
183    }
184}
185
186/// Format a float64 matching Python cqlsh output style.
187fn format_float64(f: &mut fmt::Formatter<'_>, v: f64) -> fmt::Result {
188    if v.is_nan() {
189        write!(f, "NaN")
190    } else if v.is_infinite() {
191        if v.is_sign_positive() {
192            write!(f, "Infinity")
193        } else {
194            write!(f, "-Infinity")
195        }
196    } else if v == v.trunc() && v.abs() < 1e15 {
197        // Show as integer-like when possible, matching Python behavior
198        write!(f, "{v}")
199    } else {
200        write!(f, "{v}")
201    }
202}
203
204/// Format a float32 matching Python cqlsh output style.
205fn format_float32(f: &mut fmt::Formatter<'_>, v: f32) -> fmt::Result {
206    if v.is_nan() {
207        write!(f, "NaN")
208    } else if v.is_infinite() {
209        if v.is_sign_positive() {
210            write!(f, "Infinity")
211        } else {
212            write!(f, "-Infinity")
213        }
214    } else {
215        write!(f, "{v}")
216    }
217}
218
219/// Format a CQL timestamp (milliseconds since Unix epoch).
220fn format_timestamp(f: &mut fmt::Formatter<'_>, millis: i64) -> fmt::Result {
221    use chrono::{DateTime, Utc};
222    let dt = DateTime::from_timestamp_millis(millis);
223    match dt {
224        Some(dt) => {
225            let utc: DateTime<Utc> = dt;
226            write!(f, "{}", utc.format("%Y-%m-%d %H:%M:%S%.6f%z"))
227        }
228        None => write!(f, "<invalid timestamp: {millis}>"),
229    }
230}
231
232/// A column descriptor in a result set.
233#[derive(Debug, Clone)]
234pub struct CqlColumn {
235    /// Column name.
236    pub name: String,
237    /// CQL type name (e.g., "text", "int", "uuid").
238    pub type_name: String,
239}
240
241/// A single row in a result set.
242#[derive(Debug, Clone)]
243pub struct CqlRow {
244    /// The values in this row, one per column.
245    pub values: Vec<CqlValue>,
246}
247
248impl CqlRow {
249    /// Get a value by column index.
250    pub fn get(&self, index: usize) -> Option<&CqlValue> {
251        self.values.get(index)
252    }
253
254    /// Get a value by column name (requires column metadata).
255    pub fn get_by_name<'a>(&'a self, name: &str, columns: &[CqlColumn]) -> Option<&'a CqlValue> {
256        columns
257            .iter()
258            .position(|c| c.name == name)
259            .and_then(|idx| self.values.get(idx))
260    }
261}
262
263use std::pin::Pin;
264
265use futures::Stream;
266
267/// A streaming result set that yields rows lazily from the database.
268///
269/// Rows are fetched page-by-page from the server as they are consumed,
270/// keeping memory usage bounded for large result sets.
271pub struct CqlRowStream {
272    /// Column metadata (available immediately after query starts).
273    pub columns: Vec<CqlColumn>,
274    /// The underlying async stream of rows.
275    pub rows: Pin<Box<dyn Stream<Item = anyhow::Result<CqlRow>> + Send>>,
276}
277
278/// The result of executing a CQL query.
279#[derive(Debug, Clone)]
280pub struct CqlResult {
281    /// Column metadata for the result set.
282    pub columns: Vec<CqlColumn>,
283    /// The rows returned by the query.
284    pub rows: Vec<CqlRow>,
285    /// Whether the query returned rows (SELECT) vs. was a schema/DML change.
286    pub has_rows: bool,
287    /// Tracing ID if tracing was enabled.
288    pub tracing_id: Option<uuid::Uuid>,
289    /// Warnings from the database.
290    pub warnings: Vec<String>,
291}
292
293impl CqlResult {
294    /// Create an empty result (for DML/DDL statements).
295    pub fn empty() -> Self {
296        Self {
297            columns: Vec::new(),
298            rows: Vec::new(),
299            has_rows: false,
300            tracing_id: None,
301            warnings: Vec::new(),
302        }
303    }
304
305    /// Number of rows in the result.
306    pub fn row_count(&self) -> usize {
307        self.rows.len()
308    }
309
310    /// Number of columns in the result.
311    pub fn column_count(&self) -> usize {
312        self.columns.len()
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn cql_value_display_text() {
322        assert_eq!(CqlValue::Text("hello".to_string()).to_string(), "hello");
323    }
324
325    #[test]
326    fn cql_value_display_boolean() {
327        assert_eq!(CqlValue::Boolean(true).to_string(), "True");
328        assert_eq!(CqlValue::Boolean(false).to_string(), "False");
329    }
330
331    #[test]
332    fn cql_value_display_int() {
333        assert_eq!(CqlValue::Int(42).to_string(), "42");
334        assert_eq!(CqlValue::BigInt(-100).to_string(), "-100");
335        assert_eq!(CqlValue::SmallInt(7).to_string(), "7");
336        assert_eq!(CqlValue::TinyInt(-1).to_string(), "-1");
337    }
338
339    #[test]
340    fn cql_value_display_blob() {
341        assert_eq!(
342            CqlValue::Blob(vec![0xde, 0xad, 0xbe, 0xef]).to_string(),
343            "0xdeadbeef"
344        );
345    }
346
347    #[test]
348    fn cql_value_display_uuid() {
349        let id = Uuid::nil();
350        assert_eq!(
351            CqlValue::Uuid(id).to_string(),
352            "00000000-0000-0000-0000-000000000000"
353        );
354    }
355
356    #[test]
357    fn cql_value_display_null() {
358        assert_eq!(CqlValue::Null.to_string(), "");
359    }
360
361    #[test]
362    fn cql_value_display_list() {
363        let list = CqlValue::List(vec![CqlValue::Int(1), CqlValue::Int(2), CqlValue::Int(3)]);
364        assert_eq!(list.to_string(), "[1, 2, 3]");
365    }
366
367    #[test]
368    fn cql_value_display_set() {
369        let set = CqlValue::Set(vec![
370            CqlValue::Text("a".to_string()),
371            CqlValue::Text("b".to_string()),
372        ]);
373        assert_eq!(set.to_string(), "{'a', 'b'}");
374    }
375
376    #[test]
377    fn cql_value_display_map() {
378        let map = CqlValue::Map(vec![(CqlValue::Text("key".to_string()), CqlValue::Int(42))]);
379        assert_eq!(map.to_string(), "{'key': 42}");
380    }
381
382    #[test]
383    fn cql_value_display_tuple() {
384        let tuple = CqlValue::Tuple(vec![
385            Some(CqlValue::Int(1)),
386            None,
387            Some(CqlValue::Text("x".to_string())),
388        ]);
389        assert_eq!(tuple.to_string(), "(1, null, 'x')");
390    }
391
392    #[test]
393    fn cql_value_display_udt() {
394        let udt = CqlValue::UserDefinedType {
395            keyspace: "ks".to_string(),
396            type_name: "my_type".to_string(),
397            fields: vec![
398                (
399                    "name".to_string(),
400                    Some(CqlValue::Text("Alice".to_string())),
401                ),
402                ("age".to_string(), Some(CqlValue::Int(30))),
403            ],
404        };
405        assert_eq!(udt.to_string(), "{name: 'Alice', age: 30}");
406    }
407
408    #[test]
409    fn cql_value_display_float_special() {
410        assert_eq!(CqlValue::Float(f32::NAN).to_string(), "NaN");
411        assert_eq!(CqlValue::Float(f32::INFINITY).to_string(), "Infinity");
412        assert_eq!(CqlValue::Float(f32::NEG_INFINITY).to_string(), "-Infinity");
413        assert_eq!(CqlValue::Double(f64::NAN).to_string(), "NaN");
414    }
415
416    #[test]
417    fn cql_result_empty() {
418        let result = CqlResult::empty();
419        assert!(!result.has_rows);
420        assert_eq!(result.row_count(), 0);
421        assert_eq!(result.column_count(), 0);
422    }
423
424    #[test]
425    fn cql_row_get_by_name() {
426        let columns = vec![
427            CqlColumn {
428                name: "id".to_string(),
429                type_name: "int".to_string(),
430            },
431            CqlColumn {
432                name: "name".to_string(),
433                type_name: "text".to_string(),
434            },
435        ];
436        let row = CqlRow {
437            values: vec![CqlValue::Int(1), CqlValue::Text("Alice".to_string())],
438        };
439        assert_eq!(
440            row.get_by_name("name", &columns),
441            Some(&CqlValue::Text("Alice".to_string()))
442        );
443        assert_eq!(row.get_by_name("missing", &columns), None);
444    }
445}