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 => write!(f, "null"),
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%.3f%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
263/// The result of executing a CQL query.
264#[derive(Debug, Clone)]
265pub struct CqlResult {
266    /// Column metadata for the result set.
267    pub columns: Vec<CqlColumn>,
268    /// The rows returned by the query.
269    pub rows: Vec<CqlRow>,
270    /// Whether the query returned rows (SELECT) vs. was a schema/DML change.
271    pub has_rows: bool,
272    /// Tracing ID if tracing was enabled.
273    pub tracing_id: Option<uuid::Uuid>,
274    /// Warnings from the database.
275    pub warnings: Vec<String>,
276}
277
278impl CqlResult {
279    /// Create an empty result (for DML/DDL statements).
280    pub fn empty() -> Self {
281        Self {
282            columns: Vec::new(),
283            rows: Vec::new(),
284            has_rows: false,
285            tracing_id: None,
286            warnings: Vec::new(),
287        }
288    }
289
290    /// Number of rows in the result.
291    pub fn row_count(&self) -> usize {
292        self.rows.len()
293    }
294
295    /// Number of columns in the result.
296    pub fn column_count(&self) -> usize {
297        self.columns.len()
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn cql_value_display_text() {
307        assert_eq!(CqlValue::Text("hello".to_string()).to_string(), "hello");
308    }
309
310    #[test]
311    fn cql_value_display_boolean() {
312        assert_eq!(CqlValue::Boolean(true).to_string(), "True");
313        assert_eq!(CqlValue::Boolean(false).to_string(), "False");
314    }
315
316    #[test]
317    fn cql_value_display_int() {
318        assert_eq!(CqlValue::Int(42).to_string(), "42");
319        assert_eq!(CqlValue::BigInt(-100).to_string(), "-100");
320        assert_eq!(CqlValue::SmallInt(7).to_string(), "7");
321        assert_eq!(CqlValue::TinyInt(-1).to_string(), "-1");
322    }
323
324    #[test]
325    fn cql_value_display_blob() {
326        assert_eq!(
327            CqlValue::Blob(vec![0xde, 0xad, 0xbe, 0xef]).to_string(),
328            "0xdeadbeef"
329        );
330    }
331
332    #[test]
333    fn cql_value_display_uuid() {
334        let id = Uuid::nil();
335        assert_eq!(
336            CqlValue::Uuid(id).to_string(),
337            "00000000-0000-0000-0000-000000000000"
338        );
339    }
340
341    #[test]
342    fn cql_value_display_null() {
343        assert_eq!(CqlValue::Null.to_string(), "null");
344    }
345
346    #[test]
347    fn cql_value_display_list() {
348        let list = CqlValue::List(vec![CqlValue::Int(1), CqlValue::Int(2), CqlValue::Int(3)]);
349        assert_eq!(list.to_string(), "[1, 2, 3]");
350    }
351
352    #[test]
353    fn cql_value_display_set() {
354        let set = CqlValue::Set(vec![
355            CqlValue::Text("a".to_string()),
356            CqlValue::Text("b".to_string()),
357        ]);
358        assert_eq!(set.to_string(), "{'a', 'b'}");
359    }
360
361    #[test]
362    fn cql_value_display_map() {
363        let map = CqlValue::Map(vec![(CqlValue::Text("key".to_string()), CqlValue::Int(42))]);
364        assert_eq!(map.to_string(), "{'key': 42}");
365    }
366
367    #[test]
368    fn cql_value_display_tuple() {
369        let tuple = CqlValue::Tuple(vec![
370            Some(CqlValue::Int(1)),
371            None,
372            Some(CqlValue::Text("x".to_string())),
373        ]);
374        assert_eq!(tuple.to_string(), "(1, null, 'x')");
375    }
376
377    #[test]
378    fn cql_value_display_udt() {
379        let udt = CqlValue::UserDefinedType {
380            keyspace: "ks".to_string(),
381            type_name: "my_type".to_string(),
382            fields: vec![
383                (
384                    "name".to_string(),
385                    Some(CqlValue::Text("Alice".to_string())),
386                ),
387                ("age".to_string(), Some(CqlValue::Int(30))),
388            ],
389        };
390        assert_eq!(udt.to_string(), "{name: 'Alice', age: 30}");
391    }
392
393    #[test]
394    fn cql_value_display_float_special() {
395        assert_eq!(CqlValue::Float(f32::NAN).to_string(), "NaN");
396        assert_eq!(CqlValue::Float(f32::INFINITY).to_string(), "Infinity");
397        assert_eq!(CqlValue::Float(f32::NEG_INFINITY).to_string(), "-Infinity");
398        assert_eq!(CqlValue::Double(f64::NAN).to_string(), "NaN");
399    }
400
401    #[test]
402    fn cql_result_empty() {
403        let result = CqlResult::empty();
404        assert!(!result.has_rows);
405        assert_eq!(result.row_count(), 0);
406        assert_eq!(result.column_count(), 0);
407    }
408
409    #[test]
410    fn cql_row_get_by_name() {
411        let columns = vec![
412            CqlColumn {
413                name: "id".to_string(),
414                type_name: "int".to_string(),
415            },
416            CqlColumn {
417                name: "name".to_string(),
418                type_name: "text".to_string(),
419            },
420        ];
421        let row = CqlRow {
422            values: vec![CqlValue::Int(1), CqlValue::Text("Alice".to_string())],
423        };
424        assert_eq!(
425            row.get_by_name("name", &columns),
426            Some(&CqlValue::Text("Alice".to_string()))
427        );
428        assert_eq!(row.get_by_name("missing", &columns), None);
429    }
430}