1use std::io::Write;
8
9use comfy_table::{Cell, CellAlignment, ContentArrangement, Table};
10
11use crate::colorizer::CqlColorizer;
12use crate::driver::{CqlColumn, CqlResult, CqlRow};
13
14pub fn print_tabular(result: &CqlResult, colorizer: &CqlColorizer, writer: &mut dyn Write) {
20 if result.columns.is_empty() {
21 return;
22 }
23
24 if result.rows.is_empty() {
25 writeln!(writer, "\n(0 rows)\n").ok();
26 return;
27 }
28
29 let mut table = Table::new();
30 table.set_content_arrangement(ContentArrangement::Disabled);
31 table.load_preset(CQLSH_PRESET);
32
33 let headers: Vec<Cell> = result
35 .columns
36 .iter()
37 .map(|c| Cell::new(colorizer.colorize_header(&c.name)))
38 .collect();
39 table.set_header(headers);
40
41 for row in &result.rows {
43 let cells: Vec<Cell> = row
44 .values
45 .iter()
46 .enumerate()
47 .map(|(i, val)| {
48 let display = colorizer.colorize_value(val);
49 let mut cell = Cell::new(display);
50 if is_numeric_type(&result.columns[i].type_name) {
52 cell = cell.set_alignment(CellAlignment::Right);
53 }
54 cell
55 })
56 .collect();
57 table.add_row(cells);
58 }
59
60 writeln!(writer).ok();
61 for line in format!("{table}").lines() {
62 writeln!(writer, "{}", line.trim_end()).ok();
63 }
64 writeln!(writer).ok();
65 let row_count = result.rows.len();
66 writeln!(
67 writer,
68 "({} row{})",
69 row_count,
70 if row_count == 1 { "" } else { "s" }
71 )
72 .ok();
73 writeln!(writer).ok();
74}
75
76pub fn print_expanded(result: &CqlResult, colorizer: &CqlColorizer, writer: &mut dyn Write) {
81 if result.columns.is_empty() {
82 return;
83 }
84
85 if result.rows.is_empty() {
86 writeln!(writer, "\n(0 rows)\n").ok();
87 return;
88 }
89
90 let max_col_width = result
91 .columns
92 .iter()
93 .map(|c| c.name.len())
94 .max()
95 .unwrap_or(0);
96
97 writeln!(writer).ok();
98
99 for (row_idx, row) in result.rows.iter().enumerate() {
100 writeln!(writer, "@ Row {}", row_idx + 1).ok();
101 writeln!(writer, "{}", "-".repeat(max_col_width + 10)).ok();
102 for (col_idx, col) in result.columns.iter().enumerate() {
103 let value = row
104 .get(col_idx)
105 .map(|v| colorizer.colorize_value(v))
106 .unwrap_or_else(|| colorizer.colorize_value(&crate::driver::types::CqlValue::Null));
107 writeln!(
108 writer,
109 " {:>width$} | {}",
110 colorizer.colorize_header(&col.name),
111 value,
112 width = max_col_width
113 )
114 .ok();
115 }
116 writeln!(writer).ok();
117 }
118
119 let row_count = result.rows.len();
120 writeln!(
121 writer,
122 "({} row{})",
123 row_count,
124 if row_count == 1 { "" } else { "s" }
125 )
126 .ok();
127 writeln!(writer).ok();
128}
129
130pub fn print_json(result: &CqlResult, writer: &mut dyn Write) {
137 use crate::driver::types::CqlValue;
138
139 if result.columns.is_empty() || result.rows.is_empty() {
140 writeln!(writer, "[]").ok();
141 return;
142 }
143
144 writeln!(writer, "[").ok();
145 let last_row = result.rows.len() - 1;
146 for (row_idx, row) in result.rows.iter().enumerate() {
147 write!(writer, " {{").ok();
148 for (col_idx, col) in result.columns.iter().enumerate() {
149 if col_idx > 0 {
150 write!(writer, ", ").ok();
151 }
152 let val = row.get(col_idx).unwrap_or(&CqlValue::Null);
153 write!(
154 writer,
155 "\"{}\": {}",
156 json_escape_string(&col.name),
157 cql_value_to_json(val)
158 )
159 .ok();
160 }
161 if row_idx < last_row {
162 writeln!(writer, "}},").ok();
163 } else {
164 writeln!(writer, "}}").ok();
165 }
166 }
167 writeln!(writer, "]").ok();
168}
169
170fn json_escape_string(s: &str) -> String {
172 let mut out = String::with_capacity(s.len());
173 for c in s.chars() {
174 match c {
175 '"' => out.push_str("\\\""),
176 '\\' => out.push_str("\\\\"),
177 '\n' => out.push_str("\\n"),
178 '\r' => out.push_str("\\r"),
179 '\t' => out.push_str("\\t"),
180 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
181 c => out.push(c),
182 }
183 }
184 out
185}
186
187fn cql_value_to_json(val: &crate::driver::types::CqlValue) -> String {
189 use crate::driver::types::CqlValue;
190 match val {
191 CqlValue::Null | CqlValue::Unset => "null".to_string(),
192 CqlValue::Boolean(b) => if *b { "true" } else { "false" }.to_string(),
193 CqlValue::Int(v) => v.to_string(),
194 CqlValue::BigInt(v) | CqlValue::Counter(v) => v.to_string(),
195 CqlValue::SmallInt(v) => v.to_string(),
196 CqlValue::TinyInt(v) => v.to_string(),
197 CqlValue::Float(v) => {
198 if v.is_finite() {
199 v.to_string()
200 } else {
201 format!("\"{}\"", val)
202 }
203 }
204 CqlValue::Double(v) => {
205 if v.is_finite() {
206 v.to_string()
207 } else {
208 format!("\"{}\"", val)
209 }
210 }
211 CqlValue::Decimal(v) => format!("\"{}\"", v),
212 CqlValue::Varint(v) => format!("\"{}\"", v),
213 CqlValue::Text(s) | CqlValue::Ascii(s) => {
214 format!("\"{}\"", json_escape_string(s))
215 }
216 CqlValue::Uuid(u) | CqlValue::TimeUuid(u) => format!("\"{}\"", u),
217 CqlValue::Inet(addr) => format!("\"{}\"", addr),
218 CqlValue::Blob(bytes) => {
219 let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
220 format!("\"0x{hex}\"")
221 }
222 CqlValue::Timestamp(_) | CqlValue::Date(_) | CqlValue::Time(_) => {
223 format!("\"{}\"", val)
224 }
225 CqlValue::Duration {
226 months,
227 days,
228 nanoseconds,
229 } => format!("\"{months}mo{days}d{nanoseconds}ns\""),
230 CqlValue::List(items) | CqlValue::Set(items) => {
231 let elems: Vec<String> = items.iter().map(cql_value_to_json).collect();
232 format!("[{}]", elems.join(", "))
233 }
234 CqlValue::Map(entries) => {
235 let pairs: Vec<String> = entries
236 .iter()
237 .map(|(k, v)| {
238 let key = match k {
239 CqlValue::Text(s) | CqlValue::Ascii(s) => {
240 format!("\"{}\"", json_escape_string(s))
241 }
242 other => format!("\"{}\"", json_escape_string(&other.to_string())),
243 };
244 format!("{key}: {}", cql_value_to_json(v))
245 })
246 .collect();
247 format!("{{{}}}", pairs.join(", "))
248 }
249 CqlValue::Tuple(items) => {
250 let elems: Vec<String> = items
251 .iter()
252 .map(|opt| opt.as_ref().map_or("null".to_string(), cql_value_to_json))
253 .collect();
254 format!("[{}]", elems.join(", "))
255 }
256 CqlValue::UserDefinedType { fields, .. } => {
257 let pairs: Vec<String> = fields
258 .iter()
259 .map(|(name, v)| {
260 let json_val = v.as_ref().map_or("null".to_string(), cql_value_to_json);
261 format!("\"{}\": {json_val}", json_escape_string(name))
262 })
263 .collect();
264 format!("{{{}}}", pairs.join(", "))
265 }
266 }
267}
268
269pub fn print_trace(
273 trace: &crate::driver::TracingSession,
274 colorizer: &CqlColorizer,
275 writer: &mut dyn Write,
276) {
277 writeln!(writer).ok();
278 writeln!(
279 writer,
280 "{} {}",
281 colorizer.colorize_trace_label("Tracing session:"),
282 trace.trace_id
283 )
284 .ok();
285 writeln!(writer).ok();
286
287 if let Some(ref request) = trace.request {
288 writeln!(writer, " Request: {request}").ok();
289 }
290 if let Some(ref coordinator) = trace.coordinator {
291 writeln!(writer, " Coordinator: {coordinator}").ok();
292 }
293 if let Some(duration) = trace.duration {
294 writeln!(writer, " Duration: {} microseconds", duration).ok();
295 }
296 if let Some(ref started_at) = trace.started_at {
297 writeln!(writer, " Started at: {started_at}").ok();
298 }
299
300 if !trace.events.is_empty() {
301 writeln!(writer).ok();
302
303 let mut table = Table::new();
304 table.set_content_arrangement(ContentArrangement::Disabled);
305 table.load_preset(CQLSH_PRESET);
306 table.set_header(vec![
307 Cell::new(colorizer.colorize_header("activity")),
308 Cell::new(colorizer.colorize_header("timestamp")),
309 Cell::new(colorizer.colorize_header("source")),
310 Cell::new(colorizer.colorize_header("source_elapsed")),
311 Cell::new(colorizer.colorize_header("thread")),
312 ]);
313
314 for event in &trace.events {
315 let elapsed_str = event
316 .source_elapsed
317 .map(|e| format!("{e}"))
318 .unwrap_or_default();
319 table.add_row(vec![
320 Cell::new(event.activity.as_deref().unwrap_or("")),
321 Cell::new(""),
322 Cell::new(event.source.as_deref().unwrap_or("")),
323 Cell::new(&elapsed_str).set_alignment(CellAlignment::Right),
324 Cell::new(event.thread.as_deref().unwrap_or("")),
325 ]);
326 }
327
328 writeln!(writer, "{table}").ok();
329 }
330 writeln!(writer).ok();
331}
332
333fn is_numeric_type(type_name: &str) -> bool {
335 let lower = type_name.to_lowercase();
336 matches!(
337 lower.as_str(),
338 "int"
339 | "bigint"
340 | "smallint"
341 | "tinyint"
342 | "float"
343 | "double"
344 | "decimal"
345 | "varint"
346 | "counter"
347 ) || lower.contains("int")
348 || lower.contains("float")
349 || lower.contains("double")
350 || lower.contains("decimal")
351 || lower.contains("counter")
352 || lower.contains("varint")
353}
354
355const CQLSH_PRESET: &str = " -+ | ";
375
376pub struct StreamingTableFormatter<'w> {
379 columns: Vec<CqlColumn>,
380 colorizer: &'w CqlColorizer,
381 writer: &'w mut dyn Write,
382 first_page_buffer: Vec<CqlRow>,
384 page_size: usize,
386 header_written: bool,
388 col_widths: Vec<usize>,
390 row_count: usize,
392 expanded: bool,
394}
395
396impl<'w> StreamingTableFormatter<'w> {
397 pub fn new(
399 columns: Vec<CqlColumn>,
400 colorizer: &'w CqlColorizer,
401 writer: &'w mut dyn Write,
402 page_size: usize,
403 ) -> Self {
404 Self {
405 columns,
406 colorizer,
407 writer,
408 first_page_buffer: Vec::with_capacity(page_size),
409 page_size,
410 header_written: false,
411 col_widths: Vec::new(),
412 row_count: 0,
413 expanded: false,
414 }
415 }
416
417 pub fn new_expanded(
419 columns: Vec<CqlColumn>,
420 colorizer: &'w CqlColorizer,
421 writer: &'w mut dyn Write,
422 ) -> Self {
423 Self {
424 columns,
425 colorizer,
426 writer,
427 first_page_buffer: Vec::new(),
428 page_size: 0,
429 header_written: true, col_widths: Vec::new(),
431 row_count: 0,
432 expanded: true,
433 }
434 }
435
436 pub fn add_row(&mut self, row: CqlRow) -> std::io::Result<()> {
438 self.row_count += 1;
439
440 if self.expanded {
441 return self.write_expanded_row(&row);
442 }
443
444 if !self.header_written {
445 self.first_page_buffer.push(row);
446 if self.first_page_buffer.len() >= self.page_size {
447 self.flush_first_page()?;
448 }
449 return Ok(());
450 }
451
452 self.write_row(&row)
453 }
454
455 pub fn flush_writer(&mut self) -> std::io::Result<()> {
456 self.writer.flush()
457 }
458
459 pub fn finish(mut self) -> std::io::Result<usize> {
462 if !self.header_written && !self.first_page_buffer.is_empty() {
463 self.flush_first_page()?;
464 }
465
466 if self.row_count == 0 {
467 writeln!(self.writer, "\n(0 rows)\n")?;
468 } else {
469 writeln!(self.writer)?;
470 writeln!(
471 self.writer,
472 "({} row{})",
473 self.row_count,
474 if self.row_count == 1 { "" } else { "s" }
475 )?;
476 writeln!(self.writer)?;
477 }
478
479 Ok(self.row_count)
480 }
481
482 fn flush_first_page(&mut self) -> std::io::Result<()> {
484 self.col_widths = self
486 .columns
487 .iter()
488 .enumerate()
489 .map(|(i, col)| {
490 let header_width = col.name.len();
491 let max_val_width = self
492 .first_page_buffer
493 .iter()
494 .map(|row| {
495 row.values
496 .get(i)
497 .map(|v| format_value_plain(v).len())
498 .unwrap_or(0)
499 })
500 .max()
501 .unwrap_or(0);
502 header_width.max(max_val_width)
503 })
504 .collect();
505
506 self.header_written = true;
507
508 writeln!(self.writer)?;
510 self.write_separator()?;
511 self.write_header_row()?;
512 self.write_separator()?;
513
514 let buffered: Vec<CqlRow> = std::mem::take(&mut self.first_page_buffer);
516 for row in &buffered {
517 self.write_row(row)?;
518 }
519
520 Ok(())
521 }
522
523 fn write_separator(&mut self) -> std::io::Result<()> {
524 let parts: Vec<String> = self.col_widths.iter().map(|w| "-".repeat(*w + 2)).collect();
525 writeln!(self.writer, "+{}+", parts.join("+"))
526 }
527
528 fn write_header_row(&mut self) -> std::io::Result<()> {
529 let cells: Vec<String> = self
530 .columns
531 .iter()
532 .enumerate()
533 .map(|(i, col)| {
534 let width = self.col_widths[i];
535 let name = self.colorizer.colorize_header(&col.name);
536 let padding = width.saturating_sub(col.name.len());
538 format!(" {}{} ", name, " ".repeat(padding))
539 })
540 .collect();
541 writeln!(self.writer, "|{}|", cells.join("|"))
542 }
543
544 fn write_row(&mut self, row: &CqlRow) -> std::io::Result<()> {
545 let cells: Vec<String> = row
546 .values
547 .iter()
548 .enumerate()
549 .map(|(i, val)| {
550 let width = self.col_widths.get(i).copied().unwrap_or(10);
551 let display = self.colorizer.colorize_value(val);
552 let plain_len = format_value_plain(val).len();
553 let padding = width.saturating_sub(plain_len);
554 if is_numeric_type(
555 self.columns
556 .get(i)
557 .map(|c| c.type_name.as_str())
558 .unwrap_or(""),
559 ) {
560 format!(" {}{} ", " ".repeat(padding), display)
562 } else {
563 format!(" {}{} ", display, " ".repeat(padding))
564 }
565 })
566 .collect();
567 writeln!(self.writer, "|{}|", cells.join("|"))
568 }
569
570 fn write_expanded_row(&mut self, row: &CqlRow) -> std::io::Result<()> {
571 let max_col_width = self.columns.iter().map(|c| c.name.len()).max().unwrap_or(0);
572
573 writeln!(self.writer, "@ Row {}", self.row_count)?;
574 writeln!(self.writer, "{}", "-".repeat(max_col_width + 10))?;
575 for (col_idx, col) in self.columns.iter().enumerate() {
576 let value = row
577 .values
578 .get(col_idx)
579 .map(|v| self.colorizer.colorize_value(v))
580 .unwrap_or_else(|| {
581 self.colorizer
582 .colorize_value(&crate::driver::types::CqlValue::Null)
583 });
584 writeln!(
585 self.writer,
586 " {:>width$} | {}",
587 self.colorizer.colorize_header(&col.name),
588 value,
589 width = max_col_width
590 )?;
591 }
592 writeln!(self.writer)?;
593 Ok(())
594 }
595}
596
597fn format_value_plain(val: &crate::driver::types::CqlValue) -> String {
599 use crate::driver::types::CqlValue;
600 match val {
601 CqlValue::Null => "null".to_string(),
602 CqlValue::Text(s) | CqlValue::Ascii(s) => s.clone(),
603 CqlValue::Boolean(b) => if *b { "True" } else { "False" }.to_string(),
604 CqlValue::Int(n) => n.to_string(),
605 CqlValue::BigInt(n) => n.to_string(),
606 CqlValue::SmallInt(n) => n.to_string(),
607 CqlValue::TinyInt(n) => n.to_string(),
608 CqlValue::Float(n) => format!("{}", n),
609 CqlValue::Double(n) => format!("{}", n),
610 CqlValue::Uuid(u) | CqlValue::TimeUuid(u) => u.to_string(),
611 _ => format!("{}", val),
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use crate::driver::types::{CqlColumn, CqlResult, CqlRow, CqlValue};
619
620 fn no_color() -> CqlColorizer {
621 CqlColorizer::new(false)
622 }
623
624 fn sample_result() -> CqlResult {
625 CqlResult {
626 columns: vec![
627 CqlColumn {
628 name: "name".to_string(),
629 type_name: "text".to_string(),
630 },
631 CqlColumn {
632 name: "age".to_string(),
633 type_name: "int".to_string(),
634 },
635 ],
636 rows: vec![
637 CqlRow {
638 values: vec![CqlValue::Text("Alice".to_string()), CqlValue::Int(30)],
639 },
640 CqlRow {
641 values: vec![CqlValue::Text("Bob".to_string()), CqlValue::Int(25)],
642 },
643 ],
644 has_rows: true,
645 tracing_id: None,
646 warnings: vec![],
647 }
648 }
649
650 #[test]
651 fn tabular_output_contains_headers_and_rows() {
652 let result = sample_result();
653 let mut buf = Vec::new();
654 print_tabular(&result, &no_color(), &mut buf);
655 let output = String::from_utf8(buf).unwrap();
656 assert!(output.contains("name"));
657 assert!(output.contains("age"));
658 assert!(output.contains("Alice"));
659 assert!(output.contains("Bob"));
660 assert!(output.contains("(2 rows)"));
661 }
662
663 #[test]
664 fn expanded_output_shows_row_headers() {
665 let result = sample_result();
666 let mut buf = Vec::new();
667 print_expanded(&result, &no_color(), &mut buf);
668 let output = String::from_utf8(buf).unwrap();
669 assert!(output.contains("@ Row 1"));
670 assert!(output.contains("@ Row 2"));
671 assert!(output.contains("Alice"));
672 assert!(output.contains("(2 rows)"));
673 }
674
675 #[test]
676 fn tabular_empty_result_produces_no_output() {
677 let result = CqlResult::empty();
678 let mut buf = Vec::new();
679 print_tabular(&result, &no_color(), &mut buf);
680 assert!(buf.is_empty());
681 }
682
683 #[test]
684 fn single_row_says_row_not_rows() {
685 let result = CqlResult {
686 columns: vec![CqlColumn {
687 name: "id".to_string(),
688 type_name: "int".to_string(),
689 }],
690 rows: vec![CqlRow {
691 values: vec![CqlValue::Int(1)],
692 }],
693 has_rows: true,
694 tracing_id: None,
695 warnings: vec![],
696 };
697 let mut buf = Vec::new();
698 print_tabular(&result, &no_color(), &mut buf);
699 let output = String::from_utf8(buf).unwrap();
700 assert!(output.contains("(1 row)"));
701 assert!(!output.contains("(1 rows)"));
702 }
703
704 #[test]
705 fn numeric_type_detection() {
706 assert!(is_numeric_type("int"));
707 assert!(is_numeric_type("bigint"));
708 assert!(is_numeric_type("float"));
709 assert!(is_numeric_type("double"));
710 assert!(is_numeric_type("decimal"));
711 assert!(!is_numeric_type("text"));
712 assert!(!is_numeric_type("uuid"));
713 assert!(!is_numeric_type("boolean"));
714 }
715
716 #[test]
717 fn tabular_row_separators_not_pipes() {
718 let result = sample_result();
719 let mut buf = Vec::new();
720 print_tabular(&result, &no_color(), &mut buf);
721 let output = String::from_utf8(buf).unwrap();
722 assert!(
723 !output.contains("||||"),
724 "row separators should not contain pipe characters"
725 );
726 assert!(
727 output.contains("-+-") || output.contains("---"),
728 "header separator should use dashes"
729 );
730 }
731
732 #[test]
733 fn tabular_columns_separated_by_pipes() {
734 let result = sample_result();
735 let mut buf = Vec::new();
736 print_tabular(&result, &no_color(), &mut buf);
737 let output = String::from_utf8(buf).unwrap();
738 assert!(
739 output.contains("| "),
740 "columns should be separated by pipes"
741 );
742 }
743
744 #[test]
745 fn trace_output_format() {
746 use crate::driver::{TracingEvent, TracingSession};
747 use std::collections::HashMap;
748
749 let trace = TracingSession {
750 trace_id: uuid::Uuid::nil(),
751 client: Some("127.0.0.1".to_string()),
752 command: Some("QUERY".to_string()),
753 coordinator: Some("127.0.0.1".to_string()),
754 duration: Some(1234),
755 parameters: HashMap::new(),
756 request: Some("SELECT * FROM test".to_string()),
757 started_at: Some("2024-01-01 00:00:00".to_string()),
758 events: vec![TracingEvent {
759 activity: Some("Parsing request".to_string()),
760 source: Some("127.0.0.1".to_string()),
761 source_elapsed: Some(100),
762 thread: Some("Native-Transport-1".to_string()),
763 }],
764 };
765
766 let mut buf = Vec::new();
767 print_trace(&trace, &no_color(), &mut buf);
768 let output = String::from_utf8(buf).unwrap();
769 assert!(output.contains("Tracing session:"));
770 assert!(output.contains("SELECT * FROM test"));
771 assert!(output.contains("1234 microseconds"));
772 assert!(output.contains("Parsing request"));
773 }
774
775 #[test]
776 fn json_escape_special_chars() {
777 assert_eq!(json_escape_string("hello"), "hello");
778 assert_eq!(json_escape_string("say \"hi\""), "say \\\"hi\\\"");
779 assert_eq!(json_escape_string("back\\slash"), "back\\\\slash");
780 assert_eq!(json_escape_string("new\nline"), "new\\nline");
781 assert_eq!(json_escape_string("tab\there"), "tab\\there");
782 assert_eq!(json_escape_string("cr\rhere"), "cr\\rhere");
783 assert_eq!(json_escape_string("\x01"), "\\u0001");
784 }
785
786 #[test]
787 fn cql_value_to_json_scalars() {
788 use crate::driver::types::CqlValue;
789 assert_eq!(cql_value_to_json(&CqlValue::Null), "null");
790 assert_eq!(cql_value_to_json(&CqlValue::Unset), "null");
791 assert_eq!(cql_value_to_json(&CqlValue::Boolean(true)), "true");
792 assert_eq!(cql_value_to_json(&CqlValue::Boolean(false)), "false");
793 assert_eq!(cql_value_to_json(&CqlValue::Int(42)), "42");
794 assert_eq!(cql_value_to_json(&CqlValue::BigInt(-100)), "-100");
795 assert_eq!(cql_value_to_json(&CqlValue::SmallInt(7)), "7");
796 assert_eq!(cql_value_to_json(&CqlValue::TinyInt(-1)), "-1");
797 assert_eq!(cql_value_to_json(&CqlValue::Counter(99)), "99");
798 }
799
800 #[test]
801 fn cql_value_to_json_strings() {
802 use crate::driver::types::CqlValue;
803 assert_eq!(
804 cql_value_to_json(&CqlValue::Text("hello".to_string())),
805 "\"hello\""
806 );
807 assert_eq!(
808 cql_value_to_json(&CqlValue::Ascii("world".to_string())),
809 "\"world\""
810 );
811 assert_eq!(
812 cql_value_to_json(&CqlValue::Text("say \"hi\"".to_string())),
813 "\"say \\\"hi\\\"\""
814 );
815 }
816
817 #[test]
818 fn cql_value_to_json_float_special() {
819 use crate::driver::types::CqlValue;
820 assert_eq!(cql_value_to_json(&CqlValue::Float(1.5)), "1.5");
821 assert_eq!(cql_value_to_json(&CqlValue::Double(2.5)), "2.5");
822
823 let nan_json = cql_value_to_json(&CqlValue::Float(f32::NAN));
824 assert!(nan_json.starts_with('"') && nan_json.ends_with('"'));
825 let inf_json = cql_value_to_json(&CqlValue::Double(f64::INFINITY));
826 assert!(inf_json.starts_with('"') && inf_json.ends_with('"'));
827 }
828
829 #[test]
830 fn cql_value_to_json_uuid_inet_blob() {
831 use crate::driver::types::CqlValue;
832 use std::net::IpAddr;
833 assert_eq!(
834 cql_value_to_json(&CqlValue::Uuid(uuid::Uuid::nil())),
835 "\"00000000-0000-0000-0000-000000000000\""
836 );
837 assert_eq!(
838 cql_value_to_json(&CqlValue::Inet("127.0.0.1".parse::<IpAddr>().unwrap())),
839 "\"127.0.0.1\""
840 );
841 assert_eq!(
842 cql_value_to_json(&CqlValue::Blob(vec![0xca, 0xfe])),
843 "\"0xcafe\""
844 );
845 }
846
847 #[test]
848 fn cql_value_to_json_collections() {
849 use crate::driver::types::CqlValue;
850 let list = CqlValue::List(vec![CqlValue::Int(1), CqlValue::Int(2)]);
851 assert_eq!(cql_value_to_json(&list), "[1, 2]");
852
853 let set = CqlValue::Set(vec![CqlValue::Text("a".to_string())]);
854 assert_eq!(cql_value_to_json(&set), "[\"a\"]");
855
856 let map = CqlValue::Map(vec![(CqlValue::Text("key".to_string()), CqlValue::Int(42))]);
857 assert_eq!(cql_value_to_json(&map), "{\"key\": 42}");
858
859 let map2 = CqlValue::Map(vec![(CqlValue::Int(1), CqlValue::Boolean(true))]);
860 assert_eq!(cql_value_to_json(&map2), "{\"1\": true}");
861 }
862
863 #[test]
864 fn cql_value_to_json_tuple_and_udt() {
865 use crate::driver::types::CqlValue;
866 let tuple = CqlValue::Tuple(vec![Some(CqlValue::Int(1)), None]);
867 assert_eq!(cql_value_to_json(&tuple), "[1, null]");
868
869 let udt = CqlValue::UserDefinedType {
870 keyspace: "ks".to_string(),
871 type_name: "t".to_string(),
872 fields: vec![
873 (
874 "name".to_string(),
875 Some(CqlValue::Text("Alice".to_string())),
876 ),
877 ("age".to_string(), None),
878 ],
879 };
880 assert_eq!(
881 cql_value_to_json(&udt),
882 "{\"name\": \"Alice\", \"age\": null}"
883 );
884 }
885
886 #[test]
887 fn cql_value_to_json_duration_decimal_varint() {
888 use crate::driver::types::CqlValue;
889 use bigdecimal::BigDecimal;
890 use num_bigint::BigInt;
891 use std::str::FromStr;
892
893 let dur = CqlValue::Duration {
894 months: 1,
895 days: 2,
896 nanoseconds: 3,
897 };
898 assert_eq!(cql_value_to_json(&dur), "\"1mo2d3ns\"");
899
900 let dec = CqlValue::Decimal(BigDecimal::from_str("3.14").unwrap());
901 assert_eq!(cql_value_to_json(&dec), "\"3.14\"");
902
903 let varint = CqlValue::Varint(BigInt::from(12345));
904 assert_eq!(cql_value_to_json(&varint), "\"12345\"");
905 }
906
907 #[test]
908 fn print_json_empty_result() {
909 let result = CqlResult::empty();
910 let mut buf = Vec::new();
911 print_json(&result, &mut buf);
912 let output = String::from_utf8(buf).unwrap();
913 assert_eq!(output.trim(), "[]");
914 }
915
916 #[test]
917 fn print_json_single_row() {
918 let result = CqlResult {
919 columns: vec![
920 CqlColumn {
921 name: "id".to_string(),
922 type_name: "int".to_string(),
923 },
924 CqlColumn {
925 name: "name".to_string(),
926 type_name: "text".to_string(),
927 },
928 ],
929 rows: vec![CqlRow {
930 values: vec![CqlValue::Int(1), CqlValue::Text("Alice".to_string())],
931 }],
932 has_rows: true,
933 tracing_id: None,
934 warnings: vec![],
935 };
936 let mut buf = Vec::new();
937 print_json(&result, &mut buf);
938 let output = String::from_utf8(buf).unwrap();
939 assert!(output.contains("\"id\": 1"));
940 assert!(output.contains("\"name\": \"Alice\""));
941 assert!(output.starts_with("[\n"));
942 assert!(output.trim().ends_with("]"));
943 }
944
945 #[test]
946 fn print_json_multiple_rows_has_commas() {
947 let result = CqlResult {
948 columns: vec![CqlColumn {
949 name: "v".to_string(),
950 type_name: "int".to_string(),
951 }],
952 rows: vec![
953 CqlRow {
954 values: vec![CqlValue::Int(1)],
955 },
956 CqlRow {
957 values: vec![CqlValue::Int(2)],
958 },
959 ],
960 has_rows: true,
961 tracing_id: None,
962 warnings: vec![],
963 };
964 let mut buf = Vec::new();
965 print_json(&result, &mut buf);
966 let output = String::from_utf8(buf).unwrap();
967 let lines: Vec<&str> = output.lines().collect();
968 assert!(lines[1].ends_with("},"));
969 assert!(lines[2].ends_with("}"));
970 }
971
972 #[test]
973 fn wide_table_not_truncated() {
974 let columns: Vec<CqlColumn> = (0..20)
975 .map(|i| CqlColumn {
976 name: format!("column_{i}"),
977 type_name: "text".to_string(),
978 })
979 .collect();
980 let rows = vec![CqlRow {
981 values: (0..20)
982 .map(|i| CqlValue::Text(format!("value_{i}_with_long_content")))
983 .collect(),
984 }];
985 let result = CqlResult {
986 columns,
987 rows,
988 has_rows: true,
989 tracing_id: None,
990 warnings: vec![],
991 };
992 let mut buf = Vec::new();
993 print_tabular(&result, &no_color(), &mut buf);
994 let output = String::from_utf8(buf).unwrap();
995 assert!(output.contains("column_0"));
997 assert!(output.contains("column_19"));
998 assert!(output.contains("value_19_with_long_content"));
1000 }
1001
1002 fn make_col(name: &str, type_name: &str) -> CqlColumn {
1003 CqlColumn {
1004 name: name.to_string(),
1005 type_name: type_name.to_string(),
1006 }
1007 }
1008
1009 fn make_row(values: Vec<CqlValue>) -> CqlRow {
1010 CqlRow { values }
1011 }
1012
1013 #[test]
1014 fn streaming_finish_zero_rows() {
1015 let cols = vec![make_col("id", "int")];
1016 let color = no_color();
1017 let mut buf: Vec<u8> = Vec::new();
1018 let fmt = StreamingTableFormatter::new(cols, &color, &mut buf, 100);
1019 fmt.finish().unwrap();
1020 let output = String::from_utf8(buf).unwrap();
1021 assert!(output.contains("(0 rows)"));
1022 }
1023
1024 #[test]
1025 fn streaming_finish_one_row_singular() {
1026 let cols = vec![make_col("id", "int")];
1027 let color = no_color();
1028 let mut buf: Vec<u8> = Vec::new();
1029 let mut fmt = StreamingTableFormatter::new(cols, &color, &mut buf, 100);
1030 fmt.add_row(make_row(vec![CqlValue::Int(1)])).unwrap();
1031 fmt.finish().unwrap();
1032 let output = String::from_utf8(buf).unwrap();
1033 assert!(output.contains("(1 row)"));
1034 assert!(!output.contains("(1 rows)"));
1035 }
1036
1037 #[test]
1038 fn streaming_finish_multiple_rows_plural() {
1039 let cols = vec![make_col("id", "int")];
1040 let color = no_color();
1041 let mut buf: Vec<u8> = Vec::new();
1042 let mut fmt = StreamingTableFormatter::new(cols, &color, &mut buf, 100);
1043 fmt.add_row(make_row(vec![CqlValue::Int(1)])).unwrap();
1044 fmt.add_row(make_row(vec![CqlValue::Int(2)])).unwrap();
1045 fmt.add_row(make_row(vec![CqlValue::Int(3)])).unwrap();
1046 fmt.finish().unwrap();
1047 let output = String::from_utf8(buf).unwrap();
1048 assert!(output.contains("(3 rows)"));
1049 }
1050
1051 #[test]
1052 fn streaming_tabular_buffered_rows_flushed_on_finish() {
1053 let cols = vec![make_col("name", "text"), make_col("age", "int")];
1054 let color = no_color();
1055 let mut buf: Vec<u8> = Vec::new();
1056 let mut fmt = StreamingTableFormatter::new(cols, &color, &mut buf, 100);
1057 fmt.add_row(make_row(vec![
1058 CqlValue::Text("Alice".to_string()),
1059 CqlValue::Int(30),
1060 ]))
1061 .unwrap();
1062 fmt.add_row(make_row(vec![
1063 CqlValue::Text("Bob".to_string()),
1064 CqlValue::Int(25),
1065 ]))
1066 .unwrap();
1067 fmt.finish().unwrap();
1068 let output = String::from_utf8(buf).unwrap();
1069 assert!(output.contains("name"));
1070 assert!(output.contains("age"));
1071 assert!(output.contains("Alice"));
1072 assert!(output.contains("Bob"));
1073 assert!(output.contains("(2 rows)"));
1074 }
1075
1076 #[test]
1077 fn streaming_flushes_first_page_at_page_size() {
1078 let cols = vec![make_col("id", "int")];
1079 let color = no_color();
1080 let mut buf: Vec<u8> = Vec::new();
1081 let page_size = 3;
1082 let mut fmt = StreamingTableFormatter::new(cols, &color, &mut buf, page_size);
1083 for i in 0..page_size {
1084 fmt.add_row(make_row(vec![CqlValue::Int(i as i32)]))
1085 .unwrap();
1086 }
1087 fmt.finish().unwrap();
1088 let output = String::from_utf8(buf).unwrap();
1089 assert!(output.contains("id"));
1090 assert!(output.contains("(3 rows)"));
1091 }
1092
1093 #[test]
1094 fn streaming_post_flush_rows_written_directly() {
1095 let cols = vec![make_col("id", "int")];
1096 let color = no_color();
1097 let mut buf: Vec<u8> = Vec::new();
1098 let page_size = 2;
1099 let mut fmt = StreamingTableFormatter::new(cols, &color, &mut buf, page_size);
1100 fmt.add_row(make_row(vec![CqlValue::Int(1)])).unwrap();
1101 fmt.add_row(make_row(vec![CqlValue::Int(2)])).unwrap();
1102 fmt.add_row(make_row(vec![CqlValue::Int(3)])).unwrap();
1103 fmt.finish().unwrap();
1104 let output = String::from_utf8(buf).unwrap();
1105 assert!(output.contains("(3 rows)"));
1106 }
1107
1108 #[test]
1109 fn streaming_flush_writer_succeeds() {
1110 let cols = vec![make_col("id", "int")];
1111 let color = no_color();
1112 let mut buf: Vec<u8> = Vec::new();
1113 let mut fmt = StreamingTableFormatter::new(cols, &color, &mut buf, 100);
1114 assert!(fmt.flush_writer().is_ok());
1115 }
1116
1117 #[test]
1118 fn streaming_expanded_mode_writes_row_headers() {
1119 let cols = vec![make_col("name", "text"), make_col("age", "int")];
1120 let color = no_color();
1121 let mut buf: Vec<u8> = Vec::new();
1122 let mut fmt = StreamingTableFormatter::new_expanded(cols, &color, &mut buf);
1123 fmt.add_row(make_row(vec![
1124 CqlValue::Text("Alice".to_string()),
1125 CqlValue::Int(30),
1126 ]))
1127 .unwrap();
1128 fmt.add_row(make_row(vec![
1129 CqlValue::Text("Bob".to_string()),
1130 CqlValue::Int(25),
1131 ]))
1132 .unwrap();
1133 fmt.finish().unwrap();
1134 let output = String::from_utf8(buf).unwrap();
1135 assert!(output.contains("@ Row 1"));
1136 assert!(output.contains("@ Row 2"));
1137 assert!(output.contains("Alice"));
1138 assert!(output.contains("Bob"));
1139 assert!(output.contains("(2 rows)"));
1140 }
1141
1142 #[test]
1143 fn streaming_expanded_zero_rows_footer() {
1144 let cols = vec![make_col("id", "int")];
1145 let color = no_color();
1146 let mut buf: Vec<u8> = Vec::new();
1147 let fmt = StreamingTableFormatter::new_expanded(cols, &color, &mut buf);
1148 fmt.finish().unwrap();
1149 let output = String::from_utf8(buf).unwrap();
1150 assert!(output.contains("(0 rows)"));
1151 }
1152
1153 #[test]
1154 fn streaming_numeric_right_aligned() {
1155 let cols = vec![make_col("name", "text"), make_col("score", "int")];
1156 let color = no_color();
1157 let mut buf: Vec<u8> = Vec::new();
1158 let page_size = 1;
1159 let mut fmt = StreamingTableFormatter::new(cols, &color, &mut buf, page_size);
1160 fmt.add_row(make_row(vec![
1161 CqlValue::Text("Alice".to_string()),
1162 CqlValue::Int(42),
1163 ]))
1164 .unwrap();
1165 fmt.finish().unwrap();
1166 let output = String::from_utf8(buf).unwrap();
1167 assert!(output.contains("42"));
1168 assert!(output.contains("Alice"));
1169 }
1170
1171 #[test]
1172 fn streaming_write_error_propagates() {
1173 struct FailWriter;
1174 impl Write for FailWriter {
1175 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
1176 Err(std::io::Error::new(
1177 std::io::ErrorKind::BrokenPipe,
1178 "broken",
1179 ))
1180 }
1181 fn flush(&mut self) -> std::io::Result<()> {
1182 Err(std::io::Error::new(
1183 std::io::ErrorKind::BrokenPipe,
1184 "broken",
1185 ))
1186 }
1187 }
1188
1189 let cols = vec![make_col("id", "int")];
1190 let color = no_color();
1191 let mut writer = FailWriter;
1192 let mut fmt = StreamingTableFormatter::new(cols, &color, &mut writer, 1);
1193 let result = fmt.add_row(make_row(vec![CqlValue::Int(1)]));
1194 assert!(result.is_err());
1195 }
1196}