Skip to main content

cqlite_core/util/
value_fmt.rs

1//! Value formatting shared by output writers
2//! Implements the Value → String mapping per QUERY_RESULT_CONTRACT.md
3//!
4//! This module provides stable, cqlsh-compatible formatting for all CQL value
5//! types.  It originally lived in `cqlite-cli/src/output/value_fmt.rs` and was
6//! moved into core (Issue #683) so that the Parquet export writer — which uses
7//! it for textual fallbacks such as inet and duration — can live in
8//! `cqlite-core` together with its formatting helpers.  The CLI re-exports it
9//! unchanged from `cqlite_cli::output::value_fmt`.
10
11use crate::types::Value;
12use chrono::DateTime;
13use std::net::{Ipv4Addr, Ipv6Addr};
14
15/// ValueFormatter provides cqlsh-compatible string formatting for CQL values
16pub struct ValueFormatter;
17
18impl ValueFormatter {
19    /// Format a Value to its string representation according to the contract specification
20    ///
21    /// # Contract Guarantees
22    /// - UUID/TimeUUID: lowercase hyphenated (e.g., "a8f167f0-ebe7-4f20-a386-31ff138bec3b")
23    /// - Timestamps: `YYYY-MM-DD HH:MM:SS[.fff][+0000]`, default UTC
24    /// - Collections: list `[a, b]`, set `{a, b}`, map `{k: v}`
25    /// - Blob: `0x`-prefixed lowercase hex
26    /// - Boolean: `true`/`false`
27    /// - Numbers: standard Rust formatting, avoid scientific notation unless necessary
28    pub fn format_value(value: &Value) -> String {
29        match value {
30            Value::Null => "null".to_string(),
31
32            // Boolean: lowercase true/false
33            Value::Boolean(b) => b.to_string(),
34
35            // Integer types: standard decimal formatting
36            Value::TinyInt(i) => i.to_string(),
37            Value::SmallInt(i) => i.to_string(),
38            Value::Integer(i) => i.to_string(),
39            Value::BigInt(i) => i.to_string(),
40            Value::Counter(i) => i.to_string(),
41
42            // Floating point: avoid scientific notation for reasonable ranges
43            Value::Float32(f) => Self::format_float32(*f),
44            Value::Float(f) => Self::format_float64(*f),
45
46            // Text: output as-is (no quotes for CLI display)
47            Value::Text(s) => s.clone(),
48
49            // Blob: 0x-prefixed lowercase hex
50            Value::Blob(bytes) => format!("0x{}", hex::encode(bytes)),
51
52            // Timestamp: milliseconds since epoch → YYYY-MM-DD HH:MM:SS.fff+0000
53            Value::Timestamp(millis) => Self::format_timestamp(*millis),
54
55            // Date: days since epoch → YYYY-MM-DD
56            Value::Date(days) => Self::format_date(*days),
57
58            // Time: nanoseconds since midnight → HH:MM:SS.nnnnnnnnn
59            Value::Time(nanos) => Self::format_time(*nanos),
60
61            // UUID: lowercase hyphenated format
62            Value::Uuid(bytes) => Self::format_uuid(bytes),
63
64            // Varint: arbitrary precision integer as decimal string
65            Value::Varint(bytes) => Self::format_varint(bytes),
66
67            // Decimal: scale + unscaled value → decimal string
68            Value::Decimal { scale, unscaled } => Self::format_decimal(*scale, unscaled),
69
70            // Duration: months, days, nanoseconds → "XmoYdZns" format
71            Value::Duration {
72                months,
73                days,
74                nanos,
75            } => Self::format_duration(*months, *days, *nanos),
76
77            // JSON: serialize to JSON string
78            Value::Json(json_value) => json_value.to_string(),
79
80            // Collections
81            Value::List(elements) => Self::format_list(elements),
82            Value::Set(elements) => Self::format_set(elements),
83            Value::Map(pairs) => Self::format_map(pairs),
84            Value::Tuple(fields) => Self::format_tuple(fields),
85
86            // User Defined Type
87            Value::Udt(udt) => Self::format_udt(udt),
88
89            // Frozen: unwrap and format inner value
90            Value::Frozen(inner) => Self::format_value(inner),
91
92            // Tombstone: special marker (should rarely appear in query results)
93            Value::Tombstone(info) => format!("<deleted@{}>", info.deletion_time),
94
95            // Inet: IPv4 or IPv6 address
96            Value::Inet(bytes) => Self::format_inet(bytes),
97        }
98    }
99
100    // ==================== Helper Methods ====================
101
102    /// Format float32 avoiding scientific notation for reasonable ranges
103    fn format_float32(f: f32) -> String {
104        if f.is_nan() {
105            "NaN".to_string()
106        } else if f.is_infinite() {
107            if f.is_sign_positive() {
108                "Infinity".to_string()
109            } else {
110                "-Infinity".to_string()
111            }
112        } else if f.abs() < 1e-6 || f.abs() > 1e10 {
113            format!("{:e}", f)
114        } else {
115            format!("{}", f)
116        }
117    }
118
119    /// Format float64 avoiding scientific notation for reasonable ranges
120    fn format_float64(f: f64) -> String {
121        if f.is_nan() {
122            "NaN".to_string()
123        } else if f.is_infinite() {
124            if f.is_sign_positive() {
125                "Infinity".to_string()
126            } else {
127                "-Infinity".to_string()
128            }
129        } else if f.abs() < 1e-6 || f.abs() > 1e10 {
130            format!("{:e}", f)
131        } else {
132            format!("{}", f)
133        }
134    }
135
136    /// Format timestamp (milliseconds since epoch) as YYYY-MM-DD HH:MM:SS.fff+0000
137    fn format_timestamp(millis: i64) -> String {
138        // Use from_timestamp_millis to correctly handle pre-epoch timestamps
139        // (truncating division was incorrect for negative values)
140        if let Some(datetime) = DateTime::from_timestamp_millis(millis) {
141            // Format with milliseconds: YYYY-MM-DD HH:MM:SS.fff+0000
142            datetime.format("%Y-%m-%d %H:%M:%S%.3f+0000").to_string()
143        } else {
144            format!("<invalid-timestamp:{}>", millis)
145        }
146    }
147
148    /// Format date (days since Unix epoch) as YYYY-MM-DD
149    fn format_date(days: i32) -> String {
150        // Unix epoch: 1970-01-01
151        let epoch = DateTime::from_timestamp(0, 0)
152            .map(|dt| dt.date_naive())
153            .unwrap_or_else(|| {
154                // Fallback: construct epoch date directly if timestamp fails
155                // Ultimate fallback for Date value formatting
156                chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap_or(chrono::NaiveDate::MIN)
157            });
158
159        if let Some(date) = epoch.checked_add_signed(chrono::Duration::days(days as i64)) {
160            date.format("%Y-%m-%d").to_string()
161        } else {
162            format!("<invalid-date:{}>", days)
163        }
164    }
165
166    /// Format time (nanoseconds since midnight) as HH:MM:SS.nnnnnnnnn
167    fn format_time(nanos: i64) -> String {
168        if nanos < 0 {
169            return format!("<invalid-time:{}>", nanos);
170        }
171
172        let total_secs = nanos / 1_000_000_000;
173        let hours = total_secs / 3600;
174        let minutes = (total_secs % 3600) / 60;
175        let seconds = total_secs % 60;
176        let remaining_nanos = nanos % 1_000_000_000;
177
178        if hours >= 24 {
179            return format!("<invalid-time:{}>", nanos);
180        }
181
182        format!(
183            "{:02}:{:02}:{:02}.{:09}",
184            hours, minutes, seconds, remaining_nanos
185        )
186    }
187
188    /// Format UUID as lowercase hyphenated format
189    fn format_uuid(bytes: &[u8; 16]) -> String {
190        format!(
191            "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
192            bytes[0], bytes[1], bytes[2], bytes[3],
193            bytes[4], bytes[5],
194            bytes[6], bytes[7],
195            bytes[8], bytes[9],
196            bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]
197        )
198    }
199
200    /// Format varint as decimal string
201    fn format_varint(bytes: &[u8]) -> String {
202        if bytes.is_empty() {
203            return "0".to_string();
204        }
205
206        // Use from_signed_bytes_be to handle both positive and negative values correctly
207        let result = num_bigint::BigInt::from_signed_bytes_be(bytes);
208        result.to_string()
209    }
210
211    /// Format decimal (scale + unscaled value) as decimal string
212    fn format_decimal(scale: i32, unscaled: &[u8]) -> String {
213        if unscaled.is_empty() {
214            return "0".to_string();
215        }
216
217        // Convert unscaled bytes to bigint
218        let is_negative = (unscaled[0] & 0x80) != 0;
219        let bigint = if is_negative {
220            // Two's complement for negative
221            num_bigint::BigInt::from_signed_bytes_be(unscaled)
222        } else {
223            num_bigint::BigInt::from_bytes_be(num_bigint::Sign::Plus, unscaled)
224        };
225
226        let mut decimal_str = bigint.to_string();
227        let is_neg = decimal_str.starts_with('-');
228        if is_neg {
229            decimal_str = decimal_str[1..].to_string();
230        }
231
232        // Insert decimal point based on scale
233        if scale <= 0 {
234            // Scale <= 0: multiply by 10^(-scale)
235            decimal_str.push_str(&"0".repeat((-scale) as usize));
236        } else if scale as usize >= decimal_str.len() {
237            // Need leading zeros
238            let leading_zeros = scale as usize - decimal_str.len() + 1;
239            decimal_str = format!("0.{}{}", "0".repeat(leading_zeros - 1), decimal_str);
240        } else {
241            // Insert decimal point
242            let pos = decimal_str.len() - scale as usize;
243            decimal_str.insert(pos, '.');
244        }
245
246        if is_neg {
247            format!("-{}", decimal_str)
248        } else {
249            decimal_str
250        }
251    }
252
253    /// Format duration as "XmoYdZns" (cqlsh format)
254    fn format_duration(months: i32, days: i32, nanos: i64) -> String {
255        let mut parts = Vec::new();
256
257        if months != 0 {
258            parts.push(format!("{}mo", months));
259        }
260        if days != 0 {
261            parts.push(format!("{}d", days));
262        }
263        if nanos != 0 {
264            parts.push(format!("{}ns", nanos));
265        }
266
267        if parts.is_empty() {
268            "0ns".to_string()
269        } else {
270            parts.join("")
271        }
272    }
273
274    /// Format list as [a, b, c]
275    fn format_list(elements: &[Value]) -> String {
276        let formatted_elements: Vec<String> = elements.iter().map(Self::format_value).collect();
277        format!("[{}]", formatted_elements.join(", "))
278    }
279
280    /// Format set as {a, b, c}
281    fn format_set(elements: &[Value]) -> String {
282        let formatted_elements: Vec<String> = elements.iter().map(Self::format_value).collect();
283        format!("{{{}}}", formatted_elements.join(", "))
284    }
285
286    /// Format map as {k1: v1, k2: v2}
287    fn format_map(pairs: &[(Value, Value)]) -> String {
288        let formatted_pairs: Vec<String> = pairs
289            .iter()
290            .map(|(k, v)| format!("{}: {}", Self::format_value(k), Self::format_value(v)))
291            .collect();
292        format!("{{{}}}", formatted_pairs.join(", "))
293    }
294
295    /// Format tuple as (a, b, c)
296    fn format_tuple(fields: &[Value]) -> String {
297        let formatted_fields: Vec<String> = fields.iter().map(Self::format_value).collect();
298        format!("({})", formatted_fields.join(", "))
299    }
300
301    /// Format UDT as {field1: value1, field2: value2}
302    fn format_udt(udt: &crate::types::UdtValue) -> String {
303        let formatted_fields: Vec<String> = udt
304            .fields
305            .iter()
306            .map(|field| {
307                let value_str = field
308                    .value
309                    .as_ref()
310                    .map(Self::format_value)
311                    .unwrap_or_else(|| "null".to_string());
312                format!("{}: {}", field.name, value_str)
313            })
314            .collect();
315        format!("{{{}}}", formatted_fields.join(", "))
316    }
317
318    /// Format inet address (IPv4 or IPv6)
319    fn format_inet(bytes: &[u8]) -> String {
320        if bytes.len() == 4 {
321            // IPv4
322            let addr = Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3]);
323            addr.to_string()
324        } else if bytes.len() == 16 {
325            // IPv6
326            let mut octets = [0u8; 16];
327            octets.copy_from_slice(bytes);
328            let addr = Ipv6Addr::from(octets);
329            addr.to_string()
330        } else {
331            format!("<invalid-inet:{}-bytes>", bytes.len())
332        }
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use crate::types::{UdtField, UdtValue};
340
341    #[test]
342    fn test_null() {
343        assert_eq!(ValueFormatter::format_value(&Value::Null), "null");
344    }
345
346    #[test]
347    fn test_boolean() {
348        assert_eq!(ValueFormatter::format_value(&Value::Boolean(true)), "true");
349        assert_eq!(
350            ValueFormatter::format_value(&Value::Boolean(false)),
351            "false"
352        );
353    }
354
355    #[test]
356    fn test_integers() {
357        assert_eq!(ValueFormatter::format_value(&Value::TinyInt(127)), "127");
358        assert_eq!(ValueFormatter::format_value(&Value::TinyInt(-128)), "-128");
359        assert_eq!(
360            ValueFormatter::format_value(&Value::SmallInt(32767)),
361            "32767"
362        );
363        assert_eq!(
364            ValueFormatter::format_value(&Value::Integer(2147483647)),
365            "2147483647"
366        );
367        assert_eq!(
368            ValueFormatter::format_value(&Value::BigInt(9223372036854775807)),
369            "9223372036854775807"
370        );
371        assert_eq!(
372            ValueFormatter::format_value(&Value::Counter(1000000)),
373            "1000000"
374        );
375    }
376
377    #[test]
378    fn test_floats() {
379        assert_eq!(ValueFormatter::format_value(&Value::Float32(3.25)), "3.25");
380        assert_eq!(ValueFormatter::format_value(&Value::Float(2.75)), "2.75");
381
382        // Special values
383        assert_eq!(
384            ValueFormatter::format_value(&Value::Float32(f32::NAN)),
385            "NaN"
386        );
387        assert_eq!(
388            ValueFormatter::format_value(&Value::Float32(f32::INFINITY)),
389            "Infinity"
390        );
391        assert_eq!(
392            ValueFormatter::format_value(&Value::Float32(f32::NEG_INFINITY)),
393            "-Infinity"
394        );
395
396        // Scientific notation for very small/large numbers
397        let small = Value::Float(1e-7);
398        let formatted = ValueFormatter::format_value(&small);
399        assert!(formatted.contains('e') || formatted.contains('E'));
400    }
401
402    #[test]
403    fn test_text() {
404        assert_eq!(
405            ValueFormatter::format_value(&Value::Text("hello world".to_string())),
406            "hello world"
407        );
408        assert_eq!(
409            ValueFormatter::format_value(&Value::Text("".to_string())),
410            ""
411        );
412    }
413
414    #[test]
415    fn test_blob() {
416        let blob = Value::Blob(vec![0xDE, 0xAD, 0xBE, 0xEF]);
417        assert_eq!(ValueFormatter::format_value(&blob), "0xdeadbeef");
418
419        let empty_blob = Value::Blob(vec![]);
420        assert_eq!(ValueFormatter::format_value(&empty_blob), "0x");
421    }
422
423    #[test]
424    fn test_uuid() {
425        // UUID: a8f167f0-ebe7-4f20-a386-31ff138bec3b
426        let uuid = Value::Uuid([
427            0xa8, 0xf1, 0x67, 0xf0, 0xeb, 0xe7, 0x4f, 0x20, 0xa3, 0x86, 0x31, 0xff, 0x13, 0x8b,
428            0xec, 0x3b,
429        ]);
430        assert_eq!(
431            ValueFormatter::format_value(&uuid),
432            "a8f167f0-ebe7-4f20-a386-31ff138bec3b"
433        );
434    }
435
436    #[test]
437    fn test_timestamp() {
438        // 2023-01-15 10:30:45.123 UTC = 1673778645123 milliseconds
439        let timestamp = Value::Timestamp(1673778645123);
440        let formatted = ValueFormatter::format_value(&timestamp);
441        assert!(formatted.starts_with("2023-01-15"));
442        assert!(formatted.contains("10:30:45"));
443        assert!(formatted.ends_with("+0000"));
444    }
445
446    #[test]
447    fn test_date() {
448        // 2023-01-01 = 19358 days since 1970-01-01
449        let date = Value::Date(19358);
450        assert_eq!(ValueFormatter::format_value(&date), "2023-01-01");
451
452        // Unix epoch
453        let epoch = Value::Date(0);
454        assert_eq!(ValueFormatter::format_value(&epoch), "1970-01-01");
455    }
456
457    #[test]
458    fn test_time() {
459        // 14:30:45.123456789
460        let nanos =
461            14 * 3600 * 1_000_000_000 + 30 * 60 * 1_000_000_000 + 45 * 1_000_000_000 + 123_456_789;
462        let time = Value::Time(nanos);
463        assert_eq!(ValueFormatter::format_value(&time), "14:30:45.123456789");
464
465        // Midnight
466        let midnight = Value::Time(0);
467        assert_eq!(
468            ValueFormatter::format_value(&midnight),
469            "00:00:00.000000000"
470        );
471    }
472
473    #[test]
474    fn test_duration() {
475        let duration = Value::Duration {
476            months: 2,
477            days: 15,
478            nanos: 123456789,
479        };
480        assert_eq!(ValueFormatter::format_value(&duration), "2mo15d123456789ns");
481
482        let zero_duration = Value::Duration {
483            months: 0,
484            days: 0,
485            nanos: 0,
486        };
487        assert_eq!(ValueFormatter::format_value(&zero_duration), "0ns");
488
489        let partial_duration = Value::Duration {
490            months: 0,
491            days: 5,
492            nanos: 0,
493        };
494        assert_eq!(ValueFormatter::format_value(&partial_duration), "5d");
495    }
496
497    #[test]
498    fn test_list() {
499        let list = Value::List(vec![
500            Value::Integer(1),
501            Value::Integer(2),
502            Value::Integer(3),
503        ]);
504        assert_eq!(ValueFormatter::format_value(&list), "[1, 2, 3]");
505
506        let empty_list = Value::List(vec![]);
507        assert_eq!(ValueFormatter::format_value(&empty_list), "[]");
508    }
509
510    #[test]
511    fn test_set() {
512        let set = Value::Set(vec![
513            Value::Text("apple".to_string()),
514            Value::Text("banana".to_string()),
515        ]);
516        assert_eq!(ValueFormatter::format_value(&set), "{apple, banana}");
517
518        let empty_set = Value::Set(vec![]);
519        assert_eq!(ValueFormatter::format_value(&empty_set), "{}");
520    }
521
522    #[test]
523    fn test_map() {
524        let map = Value::Map(vec![
525            (Value::Text("key1".to_string()), Value::Integer(100)),
526            (Value::Text("key2".to_string()), Value::Integer(200)),
527        ]);
528        assert_eq!(ValueFormatter::format_value(&map), "{key1: 100, key2: 200}");
529
530        let empty_map = Value::Map(vec![]);
531        assert_eq!(ValueFormatter::format_value(&empty_map), "{}");
532    }
533
534    #[test]
535    fn test_tuple() {
536        let tuple = Value::Tuple(vec![
537            Value::Integer(42),
538            Value::Text("hello".to_string()),
539            Value::Boolean(true),
540        ]);
541        assert_eq!(ValueFormatter::format_value(&tuple), "(42, hello, true)");
542    }
543
544    #[test]
545    fn test_udt() {
546        let udt = Value::Udt(UdtValue {
547            type_name: "person".to_string(),
548            keyspace: "test_ks".to_string(),
549            fields: vec![
550                UdtField {
551                    name: "name".to_string(),
552                    value: Some(Value::Text("Alice".to_string())),
553                },
554                UdtField {
555                    name: "age".to_string(),
556                    value: Some(Value::Integer(30)),
557                },
558                UdtField {
559                    name: "email".to_string(),
560                    value: None,
561                },
562            ],
563        });
564        assert_eq!(
565            ValueFormatter::format_value(&udt),
566            "{name: Alice, age: 30, email: null}"
567        );
568    }
569
570    #[test]
571    fn test_frozen() {
572        let frozen = Value::Frozen(Box::new(Value::List(vec![
573            Value::Integer(1),
574            Value::Integer(2),
575        ])));
576        assert_eq!(ValueFormatter::format_value(&frozen), "[1, 2]");
577    }
578
579    #[test]
580    fn test_inet() {
581        // IPv4
582        let ipv4 = Value::Inet(vec![192, 168, 1, 1]);
583        assert_eq!(ValueFormatter::format_value(&ipv4), "192.168.1.1");
584
585        // IPv6
586        let ipv6 = Value::Inet(vec![
587            0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
588            0x00, 0x01,
589        ]);
590        let formatted = ValueFormatter::format_value(&ipv6);
591        assert!(formatted.contains("2001:db8"));
592    }
593
594    #[test]
595    fn test_nested_collections() {
596        // List of lists
597        let nested = Value::List(vec![
598            Value::List(vec![Value::Integer(1), Value::Integer(2)]),
599            Value::List(vec![Value::Integer(3), Value::Integer(4)]),
600        ]);
601        assert_eq!(ValueFormatter::format_value(&nested), "[[1, 2], [3, 4]]");
602
603        // Map with complex values
604        let complex_map = Value::Map(vec![(
605            Value::Text("data".to_string()),
606            Value::Set(vec![Value::Integer(1), Value::Integer(2)]),
607        )]);
608        assert_eq!(ValueFormatter::format_value(&complex_map), "{data: {1, 2}}");
609    }
610
611    #[test]
612    fn test_json() {
613        let json = Value::Json(serde_json::json!({
614            "name": "Alice",
615            "age": 30
616        }));
617        let formatted = ValueFormatter::format_value(&json);
618        assert!(formatted.contains("Alice"));
619        assert!(formatted.contains("30"));
620    }
621
622    #[test]
623    fn test_varint() {
624        // Positive varint
625        let varint = Value::Varint(vec![0x01, 0x00]);
626        let formatted = ValueFormatter::format_value(&varint);
627        assert_eq!(formatted, "256");
628
629        // Zero
630        let zero = Value::Varint(vec![]);
631        assert_eq!(ValueFormatter::format_value(&zero), "0");
632    }
633
634    #[test]
635    fn test_decimal() {
636        // 123.45 (scale=2, unscaled=12345)
637        let decimal = Value::Decimal {
638            scale: 2,
639            unscaled: vec![0x30, 0x39], // 12345 in big-endian
640        };
641        let formatted = ValueFormatter::format_value(&decimal);
642        // Should contain decimal point
643        assert!(formatted.contains('.'));
644    }
645
646    #[test]
647    fn test_format_varint_negative() {
648        // Test negative varint: -1 in big-endian two's complement
649        let negative_bytes = vec![0xFF];
650        let formatted = ValueFormatter::format_value(&Value::Varint(negative_bytes));
651        assert_eq!(
652            formatted, "-1",
653            "Negative varint -1 should format correctly"
654        );
655
656        // Test larger negative number: -256
657        let negative_256 = vec![0xFF, 0x00];
658        let formatted_256 = ValueFormatter::format_value(&Value::Varint(negative_256));
659        assert_eq!(
660            formatted_256, "-256",
661            "Negative varint -256 should format correctly"
662        );
663
664        // Ensure no debug markers like '<' or '>' in output
665        assert!(!formatted.contains('<'), "Should not contain debug markers");
666        assert!(!formatted.contains('>'), "Should not contain debug markers");
667    }
668}