1use std::fmt::Write as _;
12
13use rudb::{LogicalType, QueryResult, Value};
14
15const MAX_ROWS: usize = 40;
17
18const ELIDED: usize = 3;
20
21const LINE_NAME_WIDTH: usize = 5;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum Format {
27 #[default]
29 DuckBox,
30 Box,
32 Table,
34 Markdown,
36 Line,
38 List,
40 Csv,
42 Tsv,
44 Json,
46 JsonLines,
48 Quote,
50 Insert,
52 Html,
55 Ascii,
57 Column,
59 Trash,
61}
62
63impl Format {
64 pub fn from_name(name: &str) -> Option<Self> {
66 Some(match name {
67 "duckbox" => Self::DuckBox,
68 "box" => Self::Box,
69 "table" => Self::Table,
70 "markdown" => Self::Markdown,
71 "line" | "lines" => Self::Line,
72 "list" => Self::List,
73 "csv" => Self::Csv,
74 "tabs" | "tsv" => Self::Tsv,
75 "json" => Self::Json,
76 "jsonlines" | "ndjson" => Self::JsonLines,
77 "quote" => Self::Quote,
78 "insert" => Self::Insert,
79 "html" => Self::Html,
80 "ascii" => Self::Ascii,
81 "column" => Self::Column,
82 "trash" => Self::Trash,
83 _ => return None,
84 })
85 }
86
87 pub fn from_flag(name: &str) -> Option<Self> {
96 match name {
97 "ascii" | "box" | "column" | "csv" | "html" | "json" | "jsonlines" | "line"
98 | "list" | "markdown" | "quote" | "table" => Self::from_name(name),
99 _ => None,
100 }
101 }
102
103 pub fn name(self) -> &'static str {
105 match self {
106 Self::DuckBox => "duckbox",
107 Self::Box => "box",
108 Self::Table => "table",
109 Self::Markdown => "markdown",
110 Self::Line => "line",
111 Self::List => "list",
112 Self::Csv => "csv",
113 Self::Tsv => "tabs",
114 Self::Json => "json",
115 Self::JsonLines => "jsonlines",
116 Self::Quote => "quote",
117 Self::Insert => "insert",
118 Self::Html => "html",
119 Self::Ascii => "ascii",
120 Self::Column => "column",
121 Self::Trash => "trash",
122 }
123 }
124
125 fn separator(self) -> &'static str {
127 match self {
128 Self::Csv | Self::Quote => ",",
129 Self::Tsv => "\t",
130 Self::Ascii => "\u{1f}",
131 _ => "|",
132 }
133 }
134
135 fn newline(self) -> &'static str {
140 match self {
141 Self::Csv => "\r\n",
142 Self::Ascii => "\u{1e}",
143 _ => "\n",
144 }
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct Settings {
151 pub format: Format,
153 pub header: bool,
155 pub separator: String,
157 pub newline: String,
159 pub nullvalue: String,
161 pub table: String,
163}
164
165impl Default for Settings {
166 fn default() -> Self {
167 Self {
168 format: Format::DuckBox,
169 header: true,
170 separator: "|".to_string(),
171 newline: "\n".to_string(),
172 nullvalue: "NULL".to_string(),
173 table: "table".to_string(),
174 }
175 }
176}
177
178impl Settings {
179 pub fn set_format(&mut self, format: Format) {
187 self.format = format;
188 self.separator = format.separator().to_string();
189 self.newline = format.newline().to_string();
190 }
191
192 pub fn set_format_flag(&mut self, format: Format) {
209 self.format = format;
210 match format {
211 Format::Ascii => {
212 self.separator = format.separator().to_string();
213 self.newline = format.newline().to_string();
214 }
215 Format::Csv => self.separator = format.separator().to_string(),
216 _ => {}
217 }
218 }
219}
220
221pub fn escaped(text: &str) -> String {
223 let mut out = String::new();
224 for character in text.chars() {
225 match character {
226 '\n' => out.push_str("\\n"),
227 '\r' => out.push_str("\\r"),
228 '\t' => out.push_str("\\t"),
229 '\\' => out.push_str("\\\\"),
230 other if (other as u32) < 0x20 => {
231 let _ = write!(out, "\\{:03o}", other as u32);
232 }
233 other => out.push(other),
234 }
235 }
236 out
237}
238
239pub fn render(result: &QueryResult, settings: &Settings) -> String {
241 if result.width() == 0 {
242 return String::new();
243 }
244 let cells = cells(result, settings);
245 match settings.format {
246 Format::DuckBox => duckbox(result, &cells),
247 Format::Box => boxed(result, &cells, BOX_GLYPHS),
248 Format::Table => boxed(result, &cells, TABLE_GLYPHS),
249 Format::Markdown => markdown(result, &cells),
250 Format::Line => line(result, &cells),
251 Format::List | Format::Csv | Format::Tsv => separated(result, &cells, settings),
252 Format::Json => json(result, settings, true),
253 Format::JsonLines => json(result, settings, false),
254 Format::Quote => quote(result, settings),
255 Format::Insert => insert(result, settings),
256 Format::Html => html(result, &cells, settings),
257 Format::Ascii => ascii(result, &cells, settings),
258 Format::Column => column(result, &cells),
259 Format::Trash => String::new(),
260 }
261}
262
263fn cells(result: &QueryResult, settings: &Settings) -> Vec<Vec<String>> {
265 (0..result.len())
266 .map(|row| {
267 (0..result.width())
268 .map(|column| cell(&result.value_at(row, column), settings))
269 .collect()
270 })
271 .collect()
272}
273
274fn cell(value: &Value, settings: &Settings) -> String {
276 match value {
277 Value::Null => settings.nullvalue.clone(),
278 other => other.to_string(),
279 }
280}
281
282fn type_name(ty: &LogicalType) -> String {
292 match ty {
293 LogicalType::Null => "\"NULL\"".to_string(),
294 LogicalType::Boolean => "boolean".to_string(),
295 LogicalType::TinyInt => "int8".to_string(),
296 LogicalType::SmallInt => "int16".to_string(),
297 LogicalType::Integer => "int32".to_string(),
298 LogicalType::BigInt => "int64".to_string(),
299 LogicalType::HugeInt => "int128".to_string(),
300 LogicalType::UTinyInt => "uint8".to_string(),
301 LogicalType::USmallInt => "uint16".to_string(),
302 LogicalType::UInteger => "uint32".to_string(),
303 LogicalType::UBigInt => "uint64".to_string(),
304 LogicalType::UHugeInt => "uint128".to_string(),
305 LogicalType::Float => "float".to_string(),
306 LogicalType::Double => "double".to_string(),
307 LogicalType::Decimal { width, scale } => format!("decimal({width},{scale})"),
308 LogicalType::Varchar => "varchar".to_string(),
309 LogicalType::Blob => "blob".to_string(),
310 LogicalType::Bit => "bit".to_string(),
311 LogicalType::Uuid => "uuid".to_string(),
312 LogicalType::Date => "date".to_string(),
313 LogicalType::Time => "time".to_string(),
314 LogicalType::TimeTz => "time with time zone".to_string(),
315 LogicalType::Timestamp => "timestamp".to_string(),
316 LogicalType::TimestampS => "timestamp_s".to_string(),
317 LogicalType::TimestampMs => "timestamp_ms".to_string(),
318 LogicalType::TimestampNs => "timestamp_ns".to_string(),
319 LogicalType::TimestampTz => "timestamp with time zone".to_string(),
320 LogicalType::Interval => "interval".to_string(),
321 LogicalType::List(inner) | LogicalType::Array(inner, _) => {
322 format!("{}[]", type_name(inner))
323 }
324 LogicalType::Map(key, value) => format!("map({}, {})", type_name(key), type_name(value)),
325 LogicalType::Struct(fields) => {
326 let inner: Vec<String> =
327 fields.iter().map(|field| format!("{} {}", field.name, field.ty)).collect();
328 format!("struct({})", inner.join(", ")).to_lowercase()
329 }
330 LogicalType::Union(fields) => {
331 let inner: Vec<String> =
332 fields.iter().map(|field| format!("{} {}", field.name, field.ty)).collect();
333 format!("union({})", inner.join(", ")).to_lowercase()
334 }
335 other => other.to_string().to_lowercase(),
336 }
337}
338
339fn width(text: &str) -> usize {
346 text.chars().count()
347}
348
349fn shown(rows: usize) -> Option<(usize, usize)> {
354 if rows > MAX_ROWS + ELIDED { Some((MAX_ROWS / 2, MAX_ROWS / 2)) } else { None }
355}
356
357fn pad(text: &str, size: usize, right: bool) -> String {
359 let missing = size.saturating_sub(width(text));
360 if right {
361 format!("{}{}", " ".repeat(missing), text)
362 } else {
363 format!("{}{}", text, " ".repeat(missing))
364 }
365}
366
367fn centre(text: &str, size: usize) -> String {
369 let missing = size.saturating_sub(width(text));
370 let left = missing / 2;
371 format!("{}{}{}", " ".repeat(left), text, " ".repeat(missing - left))
372}
373
374fn widths(result: &QueryResult, cells: &[Vec<String>], types: bool) -> Vec<usize> {
376 (0..result.width())
377 .map(|column| {
378 let mut size = width(&result.names()[column]);
379 if types {
380 size = size.max(width(&type_name(&result.types()[column])));
381 }
382 for row in cells {
383 size = size.max(width(&row[column]));
384 }
385 size
386 })
387 .collect()
388}
389
390struct Glyphs {
392 top: [&'static str; 4],
393 middle: [&'static str; 4],
394 bottom: [&'static str; 4],
395 vertical: &'static str,
396}
397
398const BOX_GLYPHS: Glyphs = Glyphs {
399 top: ["┌", "─", "┬", "┐"],
400 middle: ["├", "─", "┼", "┤"],
401 bottom: ["└", "─", "┴", "┘"],
402 vertical: "│",
403};
404
405const TABLE_GLYPHS: Glyphs = Glyphs {
406 top: ["+", "-", "+", "+"],
407 middle: ["+", "-", "+", "+"],
408 bottom: ["+", "-", "+", "+"],
409 vertical: "|",
410};
411
412fn rule(widths: &[usize], glyphs: &[&str; 4]) -> String {
414 let parts: Vec<String> = widths.iter().map(|size| glyphs[1].repeat(size + 2)).collect();
415 format!("{}{}{}", glyphs[0], parts.join(glyphs[2]), glyphs[3])
416}
417
418fn row(parts: &[String], vertical: &str) -> String {
420 let mut out = String::from(vertical);
421 for part in parts {
422 let _ = write!(out, " {part} {vertical}");
423 }
424 out
425}
426
427fn duckbox(result: &QueryResult, cells: &[Vec<String>]) -> String {
429 let mut sizes = widths(result, cells, true);
430 let counts = Counts::of(result);
431 if let Some(needed) = counts.minimum_width() {
434 let total = box_width(&sizes);
435 if let Some(last) = sizes.last_mut() {
436 *last += needed.saturating_sub(total);
437 }
438 }
439 let right: Vec<bool> = result.types().iter().map(LogicalType::is_numeric).collect();
440 let mut out = String::new();
441 let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.top));
442 let heads: Vec<String> =
443 result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
444 let _ = writeln!(out, "{}", row(&heads, BOX_GLYPHS.vertical));
445 let types: Vec<String> =
446 result.types().iter().zip(&sizes).map(|(ty, size)| centre(&type_name(ty), *size)).collect();
447 let _ = writeln!(out, "{}", row(&types, BOX_GLYPHS.vertical));
448 if !cells.is_empty() {
449 let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.middle));
450 write_rows(&mut out, cells, &sizes, &right, BOX_GLYPHS.vertical);
451 }
452 let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.bottom));
453 for text in counts.footer(box_width(&sizes)) {
454 let _ = writeln!(out, "{text}");
455 }
456 out
457}
458
459fn box_width(sizes: &[usize]) -> usize {
461 sizes.iter().map(|size| size + 2).sum::<usize>() + sizes.len() + 1
462}
463
464const HINT: &str = "use .last to show entire result";
466
467struct Counts {
481 rows: usize,
482 columns: usize,
483 shown: Option<usize>,
485}
486
487impl Counts {
488 fn of(result: &QueryResult) -> Self {
489 let rows = result.len();
490 let shown = shown(rows).map(|(head, tail)| head + tail);
491 Self { rows, columns: result.width(), shown }
492 }
493
494 fn row_text(&self) -> String {
496 format!("{} rows", self.rows)
497 }
498
499 fn shown_text(&self) -> Option<String> {
500 self.shown.map(|shown| format!("({shown} shown)"))
501 }
502
503 fn column_text(&self) -> String {
504 format!("{} columns", self.columns)
505 }
506
507 fn minimum_width(&self) -> Option<usize> {
513 if self.rows != 0 && self.shown.is_none() {
514 return None;
515 }
516 let widest = match self.shown_text() {
517 Some(text) => width(&text).max(width(&self.row_text())),
518 None => width(&self.row_text()),
519 };
520 Some(widest + 4)
521 }
522
523 fn footer(&self, total: usize) -> Vec<String> {
525 if self.rows != 0 && self.rows < 10 {
526 return Vec::new();
527 }
528 let mut rows = self.row_text();
529 let columns = self.column_text();
530 let with_columns =
531 self.rows >= 10 && self.columns > 1 && total >= width(&rows) + width(&columns) + 6;
532 let mut separate = self.shown_text();
535 if let Some(shown) = &separate {
536 let taken = if with_columns { width(&columns) } else { 0 };
537 if total.saturating_sub(taken) >= width(&rows) + width(shown) + 5 {
538 rows = format!("{rows} {shown}");
539 separate = None;
540 }
541 }
542 if with_columns {
543 let mut lines = vec![spread(&rows, &columns, total, self.shown.is_some())];
544 if let Some(shown) = separate {
545 lines.push(pad(&format!(" {shown}"), total - 1, false));
546 }
547 return lines;
548 }
549 if total < width(&rows) + 4 {
550 return Vec::new();
551 }
552 let mut lines = vec![middle(&rows, total, total - 2)];
553 if let Some(shown) = separate {
554 lines.push(middle(&shown, total, total - 1));
555 }
556 lines
557 }
558}
559
560fn spread(left: &str, right: &str, total: usize, hint: bool) -> String {
563 let line = total - 2;
564 let gap = line.saturating_sub(2 + width(left) + width(right));
565 if hint && gap >= width(HINT) + 10 {
566 let spare = gap - width(HINT);
567 let before = spare / 2;
568 return format!(
569 " {left}{}{HINT}{}{right}",
570 " ".repeat(before),
571 " ".repeat(spare - before)
572 );
573 }
574 format!(" {left}{}{right}", " ".repeat(gap))
575}
576
577fn middle(text: &str, total: usize, line: usize) -> String {
583 let left = total.saturating_sub(width(text)) / 2;
584 let right = line.saturating_sub(left + width(text));
585 format!("{}{text}{}", " ".repeat(left), " ".repeat(right))
586}
587
588fn write_rows(
590 out: &mut String,
591 cells: &[Vec<String>],
592 sizes: &[usize],
593 right: &[bool],
594 vertical: &str,
595) {
596 let dots = shown(cells.len());
597 for (at, values) in cells.iter().enumerate() {
598 if let Some((head, tail)) = dots {
599 if at == head {
600 let parts = dot_row(cells, sizes, right, head, tail);
601 for _ in 0..ELIDED {
602 let _ = writeln!(out, "{}", row(&parts, vertical));
603 }
604 }
605 if at >= head && at < cells.len() - tail {
606 continue;
607 }
608 }
609 let parts: Vec<String> = values
610 .iter()
611 .zip(sizes)
612 .zip(right)
613 .map(|((value, size), right)| pad(value, *size, *right))
614 .collect();
615 let _ = writeln!(out, "{}", row(&parts, vertical));
616 }
617}
618
619fn dot_row(
629 cells: &[Vec<String>],
630 sizes: &[usize],
631 right: &[bool],
632 head: usize,
633 tail: usize,
634) -> Vec<String> {
635 let above = cells.get(head.wrapping_sub(1));
636 let below = cells.get(cells.len() - tail);
637 sizes
638 .iter()
639 .zip(right)
640 .enumerate()
641 .map(|(column, (size, right))| {
642 let edge = |row: Option<&Vec<String>>| {
643 row.and_then(|values| values.get(column)).map_or(usize::MAX, |value| width(value))
644 };
645 let shortest = edge(above).min(edge(below));
646 let inset = (shortest.saturating_sub(1) / 2).min(size.saturating_sub(1));
647 let before = if *right { size.saturating_sub(1 + inset) } else { inset };
648 pad(&format!("{}·", " ".repeat(before)), *size, false)
649 })
650 .collect()
651}
652
653fn boxed(result: &QueryResult, cells: &[Vec<String>], glyphs: Glyphs) -> String {
655 let sizes = widths(result, cells, false);
656 let right = vec![false; result.width()];
660 let mut out = String::new();
661 let _ = writeln!(out, "{}", rule(&sizes, &glyphs.top));
662 let heads: Vec<String> =
663 result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
664 let _ = writeln!(out, "{}", row(&heads, glyphs.vertical));
665 let _ = writeln!(out, "{}", rule(&sizes, &glyphs.middle));
666 write_rows(&mut out, cells, &sizes, &right, glyphs.vertical);
667 let _ = writeln!(out, "{}", rule(&sizes, &glyphs.bottom));
668 out
669}
670
671fn markdown(result: &QueryResult, cells: &[Vec<String>]) -> String {
673 let sizes = widths(result, cells, false);
674 let numeric: Vec<bool> = result.types().iter().map(LogicalType::is_numeric).collect();
677 let right = vec![false; result.width()];
678 let mut out = String::new();
679 let heads: Vec<String> =
680 result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
681 let _ = writeln!(out, "{}", row(&heads, "|"));
682 let rules: Vec<String> = sizes
683 .iter()
684 .zip(&numeric)
685 .map(
686 |(size, right)| {
687 if *right { format!("{}:", "-".repeat(size + 1)) } else { "-".repeat(size + 2) }
688 },
689 )
690 .collect();
691 let _ = writeln!(out, "|{}|", rules.join("|"));
692 let mut rows = String::new();
693 write_rows(&mut rows, cells, &sizes, &right, "|");
694 out.push_str(&rows);
695 out
696}
697
698fn line(result: &QueryResult, cells: &[Vec<String>]) -> String {
704 let widest =
705 result.names().iter().map(|name| width(name)).max().unwrap_or(0).max(LINE_NAME_WIDTH);
706 let mut out = String::new();
707 for (at, values) in cells.iter().enumerate() {
708 if at > 0 {
709 out.push('\n');
710 }
711 for (name, value) in result.names().iter().zip(values) {
712 let _ = writeln!(out, "{} = {}", pad(name, widest, true), value);
713 }
714 }
715 out
716}
717
718fn separated(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
720 let quoted = settings.format == Format::Csv;
721 let mut out = String::new();
722 if settings.header {
723 let heads: Vec<String> = result
724 .names()
725 .iter()
726 .map(|name| if quoted { csv(name, settings) } else { name.clone() })
727 .collect();
728 out.push_str(&heads.join(&settings.separator));
729 out.push_str(&settings.newline);
730 }
731 for values in cells {
732 let parts: Vec<String> = values
733 .iter()
734 .map(|value| if quoted { csv(value, settings) } else { value.clone() })
735 .collect();
736 out.push_str(&parts.join(&settings.separator));
737 out.push_str(&settings.newline);
738 }
739 out
740}
741
742fn csv(text: &str, settings: &Settings) -> String {
754 let awkward =
755 text.contains(&settings.separator) || text.bytes().any(|byte| AWKWARD[byte as usize]);
756 if awkward { format!("\"{}\"", text.replace('"', "\"\"")) } else { text.to_string() }
757}
758
759static AWKWARD: [bool; 256] = {
765 let mut table = [false; 256];
766 let mut byte = 0;
767 while byte < 256 {
768 table[byte] = byte < 0x20 || byte == 0x7f || byte >= 0x80;
769 byte += 1;
770 }
771 table[b'"' as usize] = true;
772 table[b'\'' as usize] = true;
773 table
774};
775
776fn json(result: &QueryResult, settings: &Settings, array: bool) -> String {
778 let mut out = String::new();
779 for row in 0..result.len() {
782 let parts: Vec<String> = (0..result.width())
783 .map(|column| {
784 format!(
785 "{}:{}",
786 json_string(&result.names()[column]),
787 json_value(&result.value_at(row, column))
788 )
789 })
790 .collect();
791 let object = format!("{{{}}}", parts.join(","));
792 if array {
793 if row == 0 {
794 out.push('[');
795 }
796 out.push_str(&object);
797 if row + 1 < result.len() {
798 out.push_str(",\n");
799 } else {
800 out.push_str("]\n");
801 }
802 } else {
803 let _ = writeln!(out, "{object}");
804 }
805 }
806 if array && result.is_empty() {
807 out.push_str("[]\n");
808 }
809 let _ = settings;
810 out
811}
812
813fn json_string(text: &str) -> String {
815 let mut out = String::with_capacity(text.len() + 2);
816 out.push('"');
817 for character in text.chars() {
818 match character {
819 '"' => out.push_str("\\\""),
820 '\\' => out.push_str("\\\\"),
821 '\n' => out.push_str("\\n"),
822 '\r' => out.push_str("\\r"),
823 '\t' => out.push_str("\\t"),
824 other if (other as u32) < 0x20 => {
825 let _ = write!(out, "\\u{:04x}", other as u32);
826 }
827 other => out.push(other),
828 }
829 }
830 out.push('"');
831 out
832}
833
834fn json_value(value: &Value) -> String {
836 match value {
837 Value::Null => "null".to_string(),
838 Value::Boolean(flag) => flag.to_string(),
839 Value::List { values, .. } => {
840 let parts: Vec<String> = values.iter().map(json_value).collect();
841 format!("[{}]", parts.join(","))
842 }
843 Value::Struct(fields) => {
844 let parts: Vec<String> = fields
845 .iter()
846 .map(|(name, value)| format!("{}:{}", json_string(name), json_value(value)))
847 .collect();
848 format!("{{{}}}", parts.join(","))
849 }
850 other if is_number(other) => other.to_string(),
851 other => json_string(&other.to_string()),
852 }
853}
854
855fn is_number(value: &Value) -> bool {
857 matches!(
858 value,
859 Value::TinyInt(_)
860 | Value::SmallInt(_)
861 | Value::Integer(_)
862 | Value::BigInt(_)
863 | Value::HugeInt(_)
864 | Value::UTinyInt(_)
865 | Value::USmallInt(_)
866 | Value::UInteger(_)
867 | Value::UBigInt(_)
868 | Value::UHugeInt(_)
869 | Value::Float(_)
870 | Value::Double(_)
871 | Value::Decimal { .. }
872 )
873}
874
875fn quote(result: &QueryResult, settings: &Settings) -> String {
877 let mut out = String::new();
878 if settings.header {
879 let heads: Vec<String> =
880 result.names().iter().map(|name| format!("'{}'", name.replace('\'', "''"))).collect();
881 out.push_str(&heads.join(&settings.separator));
882 out.push_str(&settings.newline);
883 }
884 for row in 0..result.len() {
886 let parts: Vec<String> =
887 (0..result.width()).map(|column| sql_literal(&result.value_at(row, column))).collect();
888 out.push_str(&parts.join(&settings.separator));
889 out.push_str(&settings.newline);
890 }
891 out
892}
893
894fn insert(result: &QueryResult, settings: &Settings) -> String {
896 let mut out = String::new();
897 let columns = result.names().join(",");
898 for row in 0..result.len() {
900 let parts: Vec<String> =
901 (0..result.width()).map(|column| sql_literal(&result.value_at(row, column))).collect();
902 let _ = writeln!(
903 out,
904 "INSERT INTO \"{}\"({}) VALUES({});",
905 settings.table,
906 columns,
907 parts.join(",")
908 );
909 }
910 out
911}
912
913fn sql_literal(value: &Value) -> String {
915 match value {
916 Value::Null => "NULL".to_string(),
917 other if is_number(other) => other.to_string(),
918 Value::Boolean(flag) => flag.to_string(),
919 other => format!("'{}'", other.to_string().replace('\'', "''")),
920 }
921}
922
923fn html(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
925 let mut out = String::new();
926 if settings.header {
927 out.push_str("<tr>");
928 for name in result.names() {
929 let _ = writeln!(out, "<th>{}</th>", escape(name));
930 }
931 out.push_str("</tr>\n");
932 }
933 for values in cells {
934 out.push_str("<tr>");
935 for value in values {
936 let _ = writeln!(out, "<td>{}</td>", escape(value));
937 }
938 out.push_str("</tr>\n");
939 }
940 out
941}
942
943fn escape(text: &str) -> String {
945 text.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)
946}
947
948fn ascii(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
950 let mut out = String::new();
951 if settings.header {
952 for name in result.names() {
953 let _ = writeln!(out, "{name}");
954 }
955 }
956 for values in cells {
957 for value in values {
958 let _ = writeln!(out, "{value}");
959 }
960 }
961 let _ = settings;
962 out
963}
964
965fn column(result: &QueryResult, cells: &[Vec<String>]) -> String {
967 let sizes = widths(result, cells, false);
968 let mut out = String::new();
969 let heads: Vec<String> =
970 result.names().iter().zip(&sizes).map(|(name, size)| pad(name, *size, false)).collect();
971 let _ = writeln!(out, "{}", heads.join(" "));
972 let rules: Vec<String> = sizes.iter().map(|size| "-".repeat(*size)).collect();
973 let _ = writeln!(out, "{}", rules.join(" "));
974 for values in cells {
975 let parts: Vec<String> =
976 values.iter().zip(&sizes).map(|(value, size)| pad(value, *size, false)).collect();
977 let _ = writeln!(out, "{}", parts.join(" "));
978 }
979 out
980}