Skip to main content

cqlite_cli/output/
json.rs

1//! JSON output writer for QueryResult
2//!
3//! Emits deterministic JSON with keys in column order (as defined in metadata.columns).
4//! This ensures that JSON object keys appear in the same order as columns, NOT in
5//! arbitrary HashMap iteration order.
6
7use crate::config::OutputConfig;
8use crate::output::{OutputError, StreamingWriter};
9use cqlite_core::query::{QueryMetadata, QueryResult, QueryRow};
10use cqlite_core::Value;
11use serde_json::{json, Map, Value as JsonValue};
12use std::error::Error as StdError;
13use std::io::Write;
14
15use super::value_fmt::ValueFormatter;
16
17/// JSON writer for QueryResult
18#[allow(dead_code)]
19pub struct JSONWriter;
20
21impl JSONWriter {
22    /// Write QueryResult to JSON string with deterministic key ordering
23    ///
24    /// # Key Ordering Guarantee
25    ///
26    /// JSON object keys will appear in the SAME order as columns in `metadata.columns`.
27    /// This is critical for testing and ensures deterministic output regardless of
28    /// HashMap iteration order.
29    ///
30    /// # Example
31    ///
32    /// If metadata.columns = [c, b, a], the JSON will be:
33    /// ```json
34    /// [
35    ///   {"c": 1, "b": 2, "a": 3}
36    /// ]
37    /// ```
38    /// NOT {"a": 3, "b": 2, "c": 1}
39    ///
40    /// # Arguments
41    ///
42    /// * `result` - The query result to convert to JSON
43    /// * `config` - Output configuration for row limits
44    ///
45    /// # Returns
46    ///
47    /// Pretty-printed JSON string or error
48    #[allow(dead_code)]
49    pub fn write(result: &QueryResult, config: &OutputConfig) -> Result<String, Box<dyn StdError>> {
50        let mut rows_json = Vec::new();
51
52        // Apply row limit if specified in config
53        let rows_to_display = if let Some(limit) = config.limit {
54            &result.rows[..result.rows.len().min(limit)]
55        } else {
56            &result.rows
57        };
58
59        for row in rows_to_display {
60            // CRITICAL: Use LinkedHashMap-like pattern by iterating columns in order
61            let mut row_obj = Map::new();
62
63            // Iterate columns in metadata order, NOT HashMap order!
64            for col in &result.metadata.columns {
65                let value_opt = row.values.get(&col.name);
66                let json_value = match value_opt {
67                    Some(value) => Self::value_to_json(value),
68                    None => JsonValue::Null,
69                };
70                row_obj.insert(col.name.clone(), json_value);
71            }
72
73            rows_json.push(JsonValue::Object(row_obj));
74        }
75
76        // Pretty-print for readability
77        serde_json::to_string_pretty(&rows_json).map_err(|e| e.into())
78    }
79
80    /// Convert a CQLite Value to a serde_json::Value
81    ///
82    /// Uses string representations for complex types to ensure human readability.
83    #[allow(dead_code)]
84    fn value_to_json(value: &Value) -> JsonValue {
85        match value {
86            Value::Null => JsonValue::Null,
87            Value::Boolean(b) => JsonValue::Bool(*b),
88            Value::Integer(i) => JsonValue::Number((*i).into()),
89            Value::BigInt(i) => JsonValue::Number((*i).into()),
90            Value::Counter(c) => JsonValue::Number((*c).into()),
91            Value::TinyInt(i) => JsonValue::Number((*i as i64).into()),
92            Value::SmallInt(i) => JsonValue::Number((*i as i64).into()),
93            Value::Float(f) => serde_json::Number::from_f64(*f)
94                .map(JsonValue::Number)
95                .unwrap_or(JsonValue::Null),
96            Value::Float32(f) => serde_json::Number::from_f64(*f as f64)
97                .map(JsonValue::Number)
98                .unwrap_or(JsonValue::Null),
99            Value::Text(s) => JsonValue::String(s.clone()),
100            // Use ValueFormatter for human-readable Blob formatting (0x... hex)
101            Value::Blob(_) => JsonValue::String(ValueFormatter::format_value(value)),
102            // Use ValueFormatter for human-readable Timestamp (YYYY-MM-DD HH:MM:SS.fff+0000)
103            Value::Timestamp(_) => JsonValue::String(ValueFormatter::format_value(value)),
104            // Use ValueFormatter for human-readable Date (YYYY-MM-DD)
105            Value::Date(_) => JsonValue::String(ValueFormatter::format_value(value)),
106            // Use ValueFormatter for human-readable Time (HH:MM:SS.nnnnnnnnn)
107            Value::Time(_) => JsonValue::String(ValueFormatter::format_value(value)),
108            Value::Uuid(uuid) => {
109                // Format UUID as standard hyphenated string
110                let uuid_str = format!(
111                    "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
112                    uuid[0], uuid[1], uuid[2], uuid[3],
113                    uuid[4], uuid[5],
114                    uuid[6], uuid[7],
115                    uuid[8], uuid[9],
116                    uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]
117                );
118                JsonValue::String(uuid_str)
119            }
120            // Use ValueFormatter for human-readable Varint (decimal string)
121            Value::Varint(_) => JsonValue::String(ValueFormatter::format_value(value)),
122            // Use ValueFormatter for human-readable Decimal (e.g., "69799.73")
123            Value::Decimal { .. } => JsonValue::String(ValueFormatter::format_value(value)),
124            // Use ValueFormatter for human-readable Duration (XmoYdZns format)
125            Value::Duration { .. } => JsonValue::String(ValueFormatter::format_value(value)),
126            Value::Json(j) => j.clone(),
127            Value::List(list) => {
128                let json_list: Vec<JsonValue> = list.iter().map(Self::value_to_json).collect();
129                JsonValue::Array(json_list)
130            }
131            Value::Set(set) => {
132                let json_list: Vec<JsonValue> = set.iter().map(Self::value_to_json).collect();
133                JsonValue::Array(json_list)
134            }
135            Value::Map(map) => {
136                // Maps are Vec<(Value, Value)> in CQLite
137                // Represent as array of {"key": k, "value": v} objects for clarity
138                let entries: Vec<JsonValue> = map
139                    .iter()
140                    .map(|(k, v)| {
141                        json!({
142                            "key": Self::value_to_json(k),
143                            "value": Self::value_to_json(v)
144                        })
145                    })
146                    .collect();
147                JsonValue::Array(entries)
148            }
149            Value::Tuple(tuple) => {
150                let json_list: Vec<JsonValue> = tuple.iter().map(Self::value_to_json).collect();
151                JsonValue::Array(json_list)
152            }
153            Value::Udt(udt) => {
154                let mut udt_obj = Map::new();
155                udt_obj.insert(
156                    "_type".to_string(),
157                    JsonValue::String(udt.type_name.clone()),
158                );
159
160                // Preserve field order from UDT definition
161                for field in &udt.fields {
162                    let field_json = match &field.value {
163                        Some(value) => Self::value_to_json(value),
164                        None => JsonValue::Null,
165                    };
166                    udt_obj.insert(field.name.clone(), field_json);
167                }
168
169                JsonValue::Object(udt_obj)
170            }
171            Value::Frozen(boxed_value) => Self::value_to_json(boxed_value),
172            Value::Tombstone(info) => {
173                json!({
174                    "type": "tombstone",
175                    "deletion_time": info.deletion_time,
176                    "tombstone_type": format!("{:?}", info.tombstone_type),
177                    "ttl": info.ttl
178                })
179            }
180            Value::Inet(bytes) => {
181                // Format as IP address string if possible
182                if bytes.len() == 4 {
183                    JsonValue::String(format!(
184                        "{}.{}.{}.{}",
185                        bytes[0], bytes[1], bytes[2], bytes[3]
186                    ))
187                } else if bytes.len() == 16 {
188                    // IPv6 - use std::net::Ipv6Addr for canonical formatting
189                    use std::net::Ipv6Addr;
190                    let mut octets = [0u8; 16];
191                    octets.copy_from_slice(bytes);
192                    let addr = Ipv6Addr::from(octets);
193                    JsonValue::String(addr.to_string())
194                } else {
195                    // Invalid length, encode as base64
196                    use base64::Engine;
197                    let engine = base64::engine::general_purpose::STANDARD;
198                    JsonValue::String(engine.encode(bytes))
199                }
200            }
201        }
202    }
203}
204
205// ============================================================================
206// Streaming JSON Writer (Issue #280)
207// ============================================================================
208
209/// Streaming JSON writer for memory-efficient export of large datasets
210///
211/// Outputs a JSON array with one object per row. Unlike the batch `JSONWriter`,
212/// this writer processes data incrementally, allowing export of arbitrarily
213/// large result sets within memory constraints.
214///
215/// # Output Format
216///
217/// ```json
218/// [
219///   {"col1": "value1", "col2": 123},
220///   {"col1": "value2", "col2": 456}
221/// ]
222/// ```
223///
224/// # Example
225///
226/// ```ignore
227/// let file = File::create("output.json")?;
228/// let mut writer = StreamingJSONWriter::new(file);
229///
230/// writer.write_header(&metadata)?;
231///
232/// for chunk in result_iterator.chunks(10_000) {
233///     writer.write_chunk(&chunk)?;
234/// }
235///
236/// writer.finalize()?;
237/// ```
238pub struct StreamingJSONWriter<W: Write> {
239    /// Inner writer
240    writer: W,
241    /// Column names in order
242    columns: Vec<String>,
243    /// Count of rows written
244    rows_written: u64,
245    /// Whether we've written any rows (for comma handling)
246    first_row: bool,
247    /// Whether to pretty-print
248    pretty: bool,
249}
250
251impl<W: Write> StreamingJSONWriter<W> {
252    /// Create a new streaming JSON writer with pretty-printing
253    pub fn new(output: W) -> Self {
254        Self {
255            writer: output,
256            columns: Vec::new(),
257            rows_written: 0,
258            first_row: true,
259            pretty: true,
260        }
261    }
262
263    /// Create with compact (non-pretty) output
264    #[allow(dead_code)]
265    pub fn compact(output: W) -> Self {
266        Self {
267            writer: output,
268            columns: Vec::new(),
269            rows_written: 0,
270            first_row: true,
271            pretty: false,
272        }
273    }
274
275    /// Convert a single row to JSON object with deterministic key ordering
276    #[allow(dead_code)]
277    fn row_to_json(&self, row: &QueryRow) -> JsonValue {
278        let mut row_obj = Map::new();
279
280        // Iterate columns in metadata order, NOT HashMap order
281        for col in &self.columns {
282            let value_opt = row.values.get(col);
283            let json_value = match value_opt {
284                Some(value) => JSONWriter::value_to_json(value),
285                None => JsonValue::Null,
286            };
287            row_obj.insert(col.clone(), json_value);
288        }
289
290        JsonValue::Object(row_obj)
291    }
292}
293
294impl<W: Write + Send> StreamingWriter for StreamingJSONWriter<W> {
295    fn write_header(&mut self, metadata: &QueryMetadata) -> Result<(), OutputError> {
296        // Store column names for row writing
297        self.columns = metadata.columns.iter().map(|c| c.name.clone()).collect();
298
299        // Write opening bracket
300        if self.pretty {
301            writeln!(self.writer, "[").map_err(OutputError::Io)?;
302        } else {
303            write!(self.writer, "[").map_err(OutputError::Io)?;
304        }
305
306        Ok(())
307    }
308
309    fn write_chunk(&mut self, rows: &[QueryRow]) -> Result<usize, OutputError> {
310        for row in rows {
311            let json_obj = self.row_to_json(row);
312
313            // Handle comma separator between rows
314            if !self.first_row {
315                if self.pretty {
316                    writeln!(self.writer, ",").map_err(OutputError::Io)?;
317                } else {
318                    write!(self.writer, ",").map_err(OutputError::Io)?;
319                }
320            }
321            self.first_row = false;
322
323            // Write JSON object
324            if self.pretty {
325                let json_str = serde_json::to_string_pretty(&json_obj).map_err(|e| {
326                    OutputError::Io(std::io::Error::new(std::io::ErrorKind::Other, e))
327                })?;
328                // Indent each line
329                for line in json_str.lines() {
330                    write!(self.writer, "  {}", line).map_err(OutputError::Io)?;
331                    writeln!(self.writer).map_err(OutputError::Io)?;
332                }
333            } else {
334                let json_str = serde_json::to_string(&json_obj).map_err(|e| {
335                    OutputError::Io(std::io::Error::new(std::io::ErrorKind::Other, e))
336                })?;
337                write!(self.writer, "{}", json_str).map_err(OutputError::Io)?;
338            }
339
340            self.rows_written += 1;
341        }
342
343        Ok(rows.len())
344    }
345
346    fn finalize(&mut self) -> Result<(), OutputError> {
347        // Write closing bracket
348        if self.pretty {
349            writeln!(self.writer, "]").map_err(OutputError::Io)?;
350        } else {
351            write!(self.writer, "]").map_err(OutputError::Io)?;
352        }
353
354        self.writer.flush().map_err(OutputError::Io)
355    }
356
357    fn rows_written(&self) -> u64 {
358        self.rows_written
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use cqlite_core::query::ColumnInfo;
366    use cqlite_core::{RowKey, Value};
367    use std::collections::HashMap;
368
369    fn default_config() -> OutputConfig {
370        OutputConfig::default()
371    }
372
373    #[test]
374    fn test_deterministic_key_ordering() {
375        // Create QueryResult with columns in reverse alphabetical order: [c, b, a]
376        let mut result = QueryResult::new();
377
378        // Set metadata with columns in specific order
379        result.metadata.columns = vec![
380            ColumnInfo::new(
381                "c".to_string(),
382                cqlite_core::types::DataType::Integer,
383                false,
384                0,
385            ),
386            ColumnInfo::new(
387                "b".to_string(),
388                cqlite_core::types::DataType::Integer,
389                false,
390                1,
391            ),
392            ColumnInfo::new(
393                "a".to_string(),
394                cqlite_core::types::DataType::Integer,
395                false,
396                2,
397            ),
398        ];
399
400        // Add a row
401        let mut values = HashMap::new();
402        values.insert("a".to_string(), Value::Integer(1));
403        values.insert("b".to_string(), Value::Integer(2));
404        values.insert("c".to_string(), Value::Integer(3));
405
406        let row = QueryRow::with_values(RowKey::new(vec![1]), values);
407        result.rows.push(row);
408
409        // Write to JSON
410        let json_str = JSONWriter::write(&result, &default_config()).unwrap();
411
412        // Parse to verify structure
413        let parsed: Vec<serde_json::Value> = serde_json::from_str(&json_str).unwrap();
414        assert_eq!(parsed.len(), 1);
415
416        let row_obj = parsed[0].as_object().unwrap();
417
418        // CRITICAL: Verify key order matches column order [c, b, a], NOT [a, b, c]
419        let keys: Vec<&String> = row_obj.keys().collect();
420        assert_eq!(keys, vec!["c", "b", "a"], "Keys must be in column order");
421
422        // Verify JSON string representation has keys in correct order
423        assert!(
424            json_str.find("\"c\"").unwrap() < json_str.find("\"b\"").unwrap(),
425            "Key 'c' must appear before 'b' in JSON string"
426        );
427        assert!(
428            json_str.find("\"b\"").unwrap() < json_str.find("\"a\"").unwrap(),
429            "Key 'b' must appear before 'a' in JSON string"
430        );
431    }
432
433    #[test]
434    fn test_null_values() {
435        let mut result = QueryResult::new();
436        result.metadata.columns = vec![ColumnInfo::new(
437            "nullable_col".to_string(),
438            cqlite_core::types::DataType::Text,
439            true,
440            0,
441        )];
442
443        // Row with missing value (should be null)
444        let values = HashMap::new(); // Empty - no value for nullable_col
445        let row = QueryRow::with_values(RowKey::new(vec![1]), values);
446        result.rows.push(row);
447
448        let json_str = JSONWriter::write(&result, &default_config()).unwrap();
449        assert!(
450            json_str.contains("null"),
451            "Missing values should be JSON null"
452        );
453    }
454
455    #[test]
456    fn test_value_types() {
457        let mut result = QueryResult::new();
458        result.metadata.columns = vec![
459            ColumnInfo::new(
460                "int_col".to_string(),
461                cqlite_core::types::DataType::Integer,
462                false,
463                0,
464            ),
465            ColumnInfo::new(
466                "text_col".to_string(),
467                cqlite_core::types::DataType::Text,
468                false,
469                1,
470            ),
471            ColumnInfo::new(
472                "bool_col".to_string(),
473                cqlite_core::types::DataType::Boolean,
474                false,
475                2,
476            ),
477        ];
478
479        let mut values = HashMap::new();
480        values.insert("int_col".to_string(), Value::Integer(42));
481        values.insert("text_col".to_string(), Value::Text("hello".to_string()));
482        values.insert("bool_col".to_string(), Value::Boolean(true));
483
484        let row = QueryRow::with_values(RowKey::new(vec![1]), values);
485        result.rows.push(row);
486
487        let json_str = JSONWriter::write(&result, &default_config()).unwrap();
488
489        // Verify values are correctly represented
490        assert!(json_str.contains("42"));
491        assert!(json_str.contains("\"hello\""));
492        assert!(json_str.contains("true"));
493    }
494
495    #[test]
496    fn test_empty_result() {
497        let result = QueryResult::new();
498        let json_str = JSONWriter::write(&result, &default_config()).unwrap();
499
500        // Empty result should be empty array
501        let parsed: Vec<serde_json::Value> = serde_json::from_str(&json_str).unwrap();
502        assert_eq!(parsed.len(), 0);
503    }
504
505    #[test]
506    fn test_multiple_rows() {
507        let mut result = QueryResult::new();
508        result.metadata.columns = vec![ColumnInfo::new(
509            "id".to_string(),
510            cqlite_core::types::DataType::Integer,
511            false,
512            0,
513        )];
514
515        // Add multiple rows
516        for i in 1..=3 {
517            let mut values = HashMap::new();
518            values.insert("id".to_string(), Value::Integer(i));
519            let row = QueryRow::with_values(RowKey::new(vec![i as u8]), values);
520            result.rows.push(row);
521        }
522
523        let json_str = JSONWriter::write(&result, &default_config()).unwrap();
524        let parsed: Vec<serde_json::Value> = serde_json::from_str(&json_str).unwrap();
525        assert_eq!(parsed.len(), 3);
526    }
527
528    #[test]
529    fn test_config_limit() {
530        let mut result = QueryResult::new();
531        result.metadata.columns = vec![ColumnInfo::new(
532            "id".to_string(),
533            cqlite_core::types::DataType::Integer,
534            false,
535            0,
536        )];
537
538        // Add 10 rows
539        for i in 1..=10 {
540            let mut values = HashMap::new();
541            values.insert("id".to_string(), Value::Integer(i));
542            let row = QueryRow::with_values(RowKey::new(vec![i as u8]), values);
543            result.rows.push(row);
544        }
545
546        // Apply limit of 3 rows
547        let config = OutputConfig {
548            color_enabled: true,
549            limit: Some(3),
550            page_size: None,
551            target: crate::output::OutputTarget::Stdout,
552            overwrite: false,
553        };
554        let json_str = JSONWriter::write(&result, &config).unwrap();
555        let parsed: Vec<serde_json::Value> = serde_json::from_str(&json_str).unwrap();
556
557        // Should only have 3 rows, not 10
558        assert_eq!(parsed.len(), 3, "Limit should restrict output to 3 rows");
559    }
560
561    #[test]
562    fn test_uuid_formatting() {
563        let uuid_bytes = [
564            0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
565            0x77, 0x88,
566        ];
567
568        let json_val = JSONWriter::value_to_json(&Value::Uuid(uuid_bytes));
569        let uuid_str = json_val.as_str().unwrap();
570
571        // Should be formatted as hyphenated UUID
572        assert_eq!(uuid_str, "12345678-9abc-def0-1122-334455667788");
573    }
574
575    #[test]
576    fn test_list_value() {
577        let list_value = Value::List(vec![
578            Value::Integer(1),
579            Value::Integer(2),
580            Value::Integer(3),
581        ]);
582
583        let json_val = JSONWriter::value_to_json(&list_value);
584        assert!(json_val.is_array());
585
586        let array = json_val.as_array().unwrap();
587        assert_eq!(array.len(), 3);
588        assert_eq!(array[0], serde_json::json!(1));
589        assert_eq!(array[1], serde_json::json!(2));
590        assert_eq!(array[2], serde_json::json!(3));
591    }
592
593    #[test]
594    fn test_map_value() {
595        let map_value = Value::Map(vec![
596            (Value::Text("key1".to_string()), Value::Integer(1)),
597            (Value::Text("key2".to_string()), Value::Integer(2)),
598        ]);
599
600        let json_val = JSONWriter::value_to_json(&map_value);
601        assert!(json_val.is_array());
602
603        let array = json_val.as_array().unwrap();
604        assert_eq!(array.len(), 2);
605
606        // Each entry should have "key" and "value" fields
607        let entry1 = array[0].as_object().unwrap();
608        assert_eq!(entry1.get("key").unwrap().as_str().unwrap(), "key1");
609        assert_eq!(entry1.get("value").unwrap().as_i64().unwrap(), 1);
610    }
611
612    // Issue #227: Tests for human-readable formatting of complex types
613
614    #[test]
615    fn test_blob_formatting() {
616        let blob = Value::Blob(vec![0xDE, 0xAD, 0xBE, 0xEF]);
617        let json_val = JSONWriter::value_to_json(&blob);
618        // Should be 0x hex format, not base64
619        assert_eq!(json_val.as_str().unwrap(), "0xdeadbeef");
620    }
621
622    #[test]
623    fn test_timestamp_formatting() {
624        // 2023-01-15 10:30:45.123 UTC = 1673778645123 milliseconds
625        let timestamp = Value::Timestamp(1673778645123);
626        let json_val = JSONWriter::value_to_json(&timestamp);
627        let formatted = json_val.as_str().unwrap();
628        // Should be human-readable format, not raw milliseconds
629        assert!(formatted.starts_with("2023-01-15"));
630        assert!(formatted.contains("10:30:45"));
631        assert!(formatted.ends_with("+0000"));
632    }
633
634    #[test]
635    fn test_date_formatting() {
636        // 2023-01-01 = 19358 days since 1970-01-01
637        let date = Value::Date(19358);
638        let json_val = JSONWriter::value_to_json(&date);
639        // Should be YYYY-MM-DD format, not raw days number
640        assert_eq!(json_val.as_str().unwrap(), "2023-01-01");
641    }
642
643    #[test]
644    fn test_time_formatting() {
645        // 14:30:45.123456789 in nanoseconds
646        let nanos =
647            14 * 3600 * 1_000_000_000 + 30 * 60 * 1_000_000_000 + 45 * 1_000_000_000 + 123_456_789;
648        let time = Value::Time(nanos);
649        let json_val = JSONWriter::value_to_json(&time);
650        // Should be HH:MM:SS.nnnnnnnnn format, not raw nanoseconds
651        assert_eq!(json_val.as_str().unwrap(), "14:30:45.123456789");
652    }
653
654    #[test]
655    fn test_varint_formatting() {
656        let varint = Value::Varint(vec![0x01, 0x00]); // 256
657        let json_val = JSONWriter::value_to_json(&varint);
658        // Should be decimal string, not base64
659        assert_eq!(json_val.as_str().unwrap(), "256");
660    }
661
662    #[test]
663    fn test_decimal_formatting() {
664        // 123.45 with scale=2, unscaled=12345 (big-endian: 0x30, 0x39)
665        let decimal = Value::Decimal {
666            scale: 2,
667            unscaled: vec![0x30, 0x39],
668        };
669        let json_val = JSONWriter::value_to_json(&decimal);
670        // Should be human-readable decimal string, not {scale, unscaled} object
671        let formatted = json_val.as_str().unwrap();
672        assert!(formatted.contains('.'));
673    }
674
675    #[test]
676    fn test_duration_formatting() {
677        let duration = Value::Duration {
678            months: 2,
679            days: 15,
680            nanos: 123456789,
681        };
682        let json_val = JSONWriter::value_to_json(&duration);
683        // Should be "XmoYdZns" format, not {months, days, nanos} object
684        assert_eq!(json_val.as_str().unwrap(), "2mo15d123456789ns");
685    }
686}