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 name(self) -> &'static str {
89 match self {
90 Self::DuckBox => "duckbox",
91 Self::Box => "box",
92 Self::Table => "table",
93 Self::Markdown => "markdown",
94 Self::Line => "line",
95 Self::List => "list",
96 Self::Csv => "csv",
97 Self::Tsv => "tabs",
98 Self::Json => "json",
99 Self::JsonLines => "jsonlines",
100 Self::Quote => "quote",
101 Self::Insert => "insert",
102 Self::Html => "html",
103 Self::Ascii => "ascii",
104 Self::Column => "column",
105 Self::Trash => "trash",
106 }
107 }
108
109 fn separator(self) -> &'static str {
111 match self {
112 Self::Csv | Self::Quote => ",",
113 Self::Tsv => "\t",
114 Self::Ascii => "\u{1f}",
115 _ => "|",
116 }
117 }
118
119 fn newline(self) -> &'static str {
124 match self {
125 Self::Csv => "\r\n",
126 Self::Ascii => "\u{1e}",
127 _ => "\n",
128 }
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct Settings {
135 pub format: Format,
137 pub header: bool,
139 pub separator: String,
141 pub newline: String,
143 pub nullvalue: String,
145 pub table: String,
147}
148
149impl Default for Settings {
150 fn default() -> Self {
151 Self {
152 format: Format::DuckBox,
153 header: true,
154 separator: "|".to_string(),
155 newline: "\n".to_string(),
156 nullvalue: "NULL".to_string(),
157 table: "table".to_string(),
158 }
159 }
160}
161
162impl Settings {
163 pub fn set_format(&mut self, format: Format) {
171 self.format = format;
172 self.separator = format.separator().to_string();
173 self.newline = format.newline().to_string();
174 }
175
176 pub fn set_format_flag(&mut self, format: Format) {
187 self.format = format;
188 self.separator = format.separator().to_string();
189 }
190}
191
192pub fn escaped(text: &str) -> String {
194 let mut out = String::new();
195 for character in text.chars() {
196 match character {
197 '\n' => out.push_str("\\n"),
198 '\r' => out.push_str("\\r"),
199 '\t' => out.push_str("\\t"),
200 '\\' => out.push_str("\\\\"),
201 other if (other as u32) < 0x20 => {
202 let _ = write!(out, "\\{:03o}", other as u32);
203 }
204 other => out.push(other),
205 }
206 }
207 out
208}
209
210pub fn render(result: &QueryResult, settings: &Settings) -> String {
212 if result.width() == 0 {
213 return String::new();
214 }
215 let cells = cells(result, settings);
216 match settings.format {
217 Format::DuckBox => duckbox(result, &cells),
218 Format::Box => boxed(result, &cells, BOX_GLYPHS),
219 Format::Table => boxed(result, &cells, TABLE_GLYPHS),
220 Format::Markdown => markdown(result, &cells),
221 Format::Line => line(result, &cells),
222 Format::List | Format::Csv | Format::Tsv => separated(result, &cells, settings),
223 Format::Json => json(result, settings, true),
224 Format::JsonLines => json(result, settings, false),
225 Format::Quote => quote(result, settings),
226 Format::Insert => insert(result, settings),
227 Format::Html => html(result, &cells, settings),
228 Format::Ascii => ascii(result, &cells, settings),
229 Format::Column => column(result, &cells),
230 Format::Trash => String::new(),
231 }
232}
233
234fn cells(result: &QueryResult, settings: &Settings) -> Vec<Vec<String>> {
236 (0..result.len())
237 .map(|row| {
238 (0..result.width())
239 .map(|column| cell(&result.value_at(row, column), settings))
240 .collect()
241 })
242 .collect()
243}
244
245fn cell(value: &Value, settings: &Settings) -> String {
247 match value {
248 Value::Null => settings.nullvalue.clone(),
249 other => other.to_string(),
250 }
251}
252
253fn type_name(ty: &LogicalType) -> String {
263 match ty {
264 LogicalType::Null => "\"NULL\"".to_string(),
265 LogicalType::Boolean => "boolean".to_string(),
266 LogicalType::TinyInt => "int8".to_string(),
267 LogicalType::SmallInt => "int16".to_string(),
268 LogicalType::Integer => "int32".to_string(),
269 LogicalType::BigInt => "int64".to_string(),
270 LogicalType::HugeInt => "int128".to_string(),
271 LogicalType::UTinyInt => "uint8".to_string(),
272 LogicalType::USmallInt => "uint16".to_string(),
273 LogicalType::UInteger => "uint32".to_string(),
274 LogicalType::UBigInt => "uint64".to_string(),
275 LogicalType::UHugeInt => "uint128".to_string(),
276 LogicalType::Float => "float".to_string(),
277 LogicalType::Double => "double".to_string(),
278 LogicalType::Decimal { width, scale } => format!("decimal({width},{scale})"),
279 LogicalType::Varchar => "varchar".to_string(),
280 LogicalType::Blob => "blob".to_string(),
281 LogicalType::Bit => "bit".to_string(),
282 LogicalType::Uuid => "uuid".to_string(),
283 LogicalType::Date => "date".to_string(),
284 LogicalType::Time => "time".to_string(),
285 LogicalType::TimeTz => "time with time zone".to_string(),
286 LogicalType::Timestamp => "timestamp".to_string(),
287 LogicalType::TimestampS => "timestamp_s".to_string(),
288 LogicalType::TimestampMs => "timestamp_ms".to_string(),
289 LogicalType::TimestampNs => "timestamp_ns".to_string(),
290 LogicalType::TimestampTz => "timestamp with time zone".to_string(),
291 LogicalType::Interval => "interval".to_string(),
292 LogicalType::List(inner) | LogicalType::Array(inner, _) => {
293 format!("{}[]", type_name(inner))
294 }
295 LogicalType::Map(key, value) => format!("map({}, {})", type_name(key), type_name(value)),
296 LogicalType::Struct(fields) => {
297 let inner: Vec<String> =
298 fields.iter().map(|field| format!("{} {}", field.name, field.ty)).collect();
299 format!("struct({})", inner.join(", ")).to_lowercase()
300 }
301 LogicalType::Union(fields) => {
302 let inner: Vec<String> =
303 fields.iter().map(|field| format!("{} {}", field.name, field.ty)).collect();
304 format!("union({})", inner.join(", ")).to_lowercase()
305 }
306 other => other.to_string().to_lowercase(),
307 }
308}
309
310fn width(text: &str) -> usize {
317 text.chars().count()
318}
319
320fn shown(rows: usize) -> Option<(usize, usize)> {
325 if rows > MAX_ROWS + ELIDED { Some((MAX_ROWS / 2, MAX_ROWS / 2)) } else { None }
326}
327
328fn pad(text: &str, size: usize, right: bool) -> String {
330 let missing = size.saturating_sub(width(text));
331 if right {
332 format!("{}{}", " ".repeat(missing), text)
333 } else {
334 format!("{}{}", text, " ".repeat(missing))
335 }
336}
337
338fn centre(text: &str, size: usize) -> String {
340 let missing = size.saturating_sub(width(text));
341 let left = missing / 2;
342 format!("{}{}{}", " ".repeat(left), text, " ".repeat(missing - left))
343}
344
345fn widths(result: &QueryResult, cells: &[Vec<String>], types: bool) -> Vec<usize> {
347 (0..result.width())
348 .map(|column| {
349 let mut size = width(&result.names()[column]);
350 if types {
351 size = size.max(width(&type_name(&result.types()[column])));
352 }
353 for row in cells {
354 size = size.max(width(&row[column]));
355 }
356 size
357 })
358 .collect()
359}
360
361struct Glyphs {
363 top: [&'static str; 4],
364 middle: [&'static str; 4],
365 bottom: [&'static str; 4],
366 vertical: &'static str,
367}
368
369const BOX_GLYPHS: Glyphs = Glyphs {
370 top: ["┌", "─", "┬", "┐"],
371 middle: ["├", "─", "┼", "┤"],
372 bottom: ["└", "─", "┴", "┘"],
373 vertical: "│",
374};
375
376const TABLE_GLYPHS: Glyphs = Glyphs {
377 top: ["+", "-", "+", "+"],
378 middle: ["+", "-", "+", "+"],
379 bottom: ["+", "-", "+", "+"],
380 vertical: "|",
381};
382
383fn rule(widths: &[usize], glyphs: &[&str; 4]) -> String {
385 let parts: Vec<String> = widths.iter().map(|size| glyphs[1].repeat(size + 2)).collect();
386 format!("{}{}{}", glyphs[0], parts.join(glyphs[2]), glyphs[3])
387}
388
389fn row(parts: &[String], vertical: &str) -> String {
391 let mut out = String::from(vertical);
392 for part in parts {
393 let _ = write!(out, " {part} {vertical}");
394 }
395 out
396}
397
398fn duckbox(result: &QueryResult, cells: &[Vec<String>]) -> String {
400 let mut sizes = widths(result, cells, true);
401 let counts = Counts::of(result);
402 if let Some(needed) = counts.minimum_width() {
405 let total = box_width(&sizes);
406 if let Some(last) = sizes.last_mut() {
407 *last += needed.saturating_sub(total);
408 }
409 }
410 let right: Vec<bool> = result.types().iter().map(LogicalType::is_numeric).collect();
411 let mut out = String::new();
412 let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.top));
413 let heads: Vec<String> =
414 result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
415 let _ = writeln!(out, "{}", row(&heads, BOX_GLYPHS.vertical));
416 let types: Vec<String> =
417 result.types().iter().zip(&sizes).map(|(ty, size)| centre(&type_name(ty), *size)).collect();
418 let _ = writeln!(out, "{}", row(&types, BOX_GLYPHS.vertical));
419 if !cells.is_empty() {
420 let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.middle));
421 write_rows(&mut out, cells, &sizes, &right, BOX_GLYPHS.vertical);
422 }
423 let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.bottom));
424 for text in counts.footer(box_width(&sizes)) {
425 let _ = writeln!(out, "{text}");
426 }
427 out
428}
429
430fn box_width(sizes: &[usize]) -> usize {
432 sizes.iter().map(|size| size + 2).sum::<usize>() + sizes.len() + 1
433}
434
435const HINT: &str = "use .last to show entire result";
437
438struct Counts {
452 rows: usize,
453 columns: usize,
454 shown: Option<usize>,
456}
457
458impl Counts {
459 fn of(result: &QueryResult) -> Self {
460 let rows = result.len();
461 let shown = shown(rows).map(|(head, tail)| head + tail);
462 Self { rows, columns: result.width(), shown }
463 }
464
465 fn row_text(&self) -> String {
467 format!("{} rows", self.rows)
468 }
469
470 fn shown_text(&self) -> Option<String> {
471 self.shown.map(|shown| format!("({shown} shown)"))
472 }
473
474 fn column_text(&self) -> String {
475 format!("{} columns", self.columns)
476 }
477
478 fn minimum_width(&self) -> Option<usize> {
484 if self.rows != 0 && self.shown.is_none() {
485 return None;
486 }
487 let widest = match self.shown_text() {
488 Some(text) => width(&text).max(width(&self.row_text())),
489 None => width(&self.row_text()),
490 };
491 Some(widest + 4)
492 }
493
494 fn footer(&self, total: usize) -> Vec<String> {
496 if self.rows != 0 && self.rows < 10 {
497 return Vec::new();
498 }
499 let mut rows = self.row_text();
500 let columns = self.column_text();
501 let with_columns =
502 self.rows >= 10 && self.columns > 1 && total >= width(&rows) + width(&columns) + 6;
503 let mut separate = self.shown_text();
506 if let Some(shown) = &separate {
507 let taken = if with_columns { width(&columns) } else { 0 };
508 if total.saturating_sub(taken) >= width(&rows) + width(shown) + 5 {
509 rows = format!("{rows} {shown}");
510 separate = None;
511 }
512 }
513 if with_columns {
514 let mut lines = vec![spread(&rows, &columns, total, self.shown.is_some())];
515 if let Some(shown) = separate {
516 lines.push(pad(&format!(" {shown}"), total - 1, false));
517 }
518 return lines;
519 }
520 if total < width(&rows) + 4 {
521 return Vec::new();
522 }
523 let mut lines = vec![middle(&rows, total, total - 2)];
524 if let Some(shown) = separate {
525 lines.push(middle(&shown, total, total - 1));
526 }
527 lines
528 }
529}
530
531fn spread(left: &str, right: &str, total: usize, hint: bool) -> String {
534 let line = total - 2;
535 let gap = line.saturating_sub(2 + width(left) + width(right));
536 if hint && gap >= width(HINT) + 10 {
537 let spare = gap - width(HINT);
538 let before = spare / 2;
539 return format!(
540 " {left}{}{HINT}{}{right}",
541 " ".repeat(before),
542 " ".repeat(spare - before)
543 );
544 }
545 format!(" {left}{}{right}", " ".repeat(gap))
546}
547
548fn middle(text: &str, total: usize, line: usize) -> String {
554 let left = total.saturating_sub(width(text)) / 2;
555 let right = line.saturating_sub(left + width(text));
556 format!("{}{text}{}", " ".repeat(left), " ".repeat(right))
557}
558
559fn write_rows(
561 out: &mut String,
562 cells: &[Vec<String>],
563 sizes: &[usize],
564 right: &[bool],
565 vertical: &str,
566) {
567 let dots = shown(cells.len());
568 for (at, values) in cells.iter().enumerate() {
569 if let Some((head, tail)) = dots {
570 if at == head {
571 let parts = dot_row(cells, sizes, right, head, tail);
572 for _ in 0..ELIDED {
573 let _ = writeln!(out, "{}", row(&parts, vertical));
574 }
575 }
576 if at >= head && at < cells.len() - tail {
577 continue;
578 }
579 }
580 let parts: Vec<String> = values
581 .iter()
582 .zip(sizes)
583 .zip(right)
584 .map(|((value, size), right)| pad(value, *size, *right))
585 .collect();
586 let _ = writeln!(out, "{}", row(&parts, vertical));
587 }
588}
589
590fn dot_row(
600 cells: &[Vec<String>],
601 sizes: &[usize],
602 right: &[bool],
603 head: usize,
604 tail: usize,
605) -> Vec<String> {
606 let above = cells.get(head.wrapping_sub(1));
607 let below = cells.get(cells.len() - tail);
608 sizes
609 .iter()
610 .zip(right)
611 .enumerate()
612 .map(|(column, (size, right))| {
613 let edge = |row: Option<&Vec<String>>| {
614 row.and_then(|values| values.get(column)).map_or(usize::MAX, |value| width(value))
615 };
616 let shortest = edge(above).min(edge(below));
617 let inset = (shortest.saturating_sub(1) / 2).min(size.saturating_sub(1));
618 let before = if *right { size.saturating_sub(1 + inset) } else { inset };
619 pad(&format!("{}·", " ".repeat(before)), *size, false)
620 })
621 .collect()
622}
623
624fn boxed(result: &QueryResult, cells: &[Vec<String>], glyphs: Glyphs) -> String {
626 let sizes = widths(result, cells, false);
627 let right = vec![false; result.width()];
631 let mut out = String::new();
632 let _ = writeln!(out, "{}", rule(&sizes, &glyphs.top));
633 let heads: Vec<String> =
634 result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
635 let _ = writeln!(out, "{}", row(&heads, glyphs.vertical));
636 let _ = writeln!(out, "{}", rule(&sizes, &glyphs.middle));
637 write_rows(&mut out, cells, &sizes, &right, glyphs.vertical);
638 let _ = writeln!(out, "{}", rule(&sizes, &glyphs.bottom));
639 out
640}
641
642fn markdown(result: &QueryResult, cells: &[Vec<String>]) -> String {
644 let sizes = widths(result, cells, false);
645 let numeric: Vec<bool> = result.types().iter().map(LogicalType::is_numeric).collect();
648 let right = vec![false; result.width()];
649 let mut out = String::new();
650 let heads: Vec<String> =
651 result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
652 let _ = writeln!(out, "{}", row(&heads, "|"));
653 let rules: Vec<String> = sizes
654 .iter()
655 .zip(&numeric)
656 .map(
657 |(size, right)| {
658 if *right { format!("{}:", "-".repeat(size + 1)) } else { "-".repeat(size + 2) }
659 },
660 )
661 .collect();
662 let _ = writeln!(out, "|{}|", rules.join("|"));
663 let mut rows = String::new();
664 write_rows(&mut rows, cells, &sizes, &right, "|");
665 out.push_str(&rows);
666 out
667}
668
669fn line(result: &QueryResult, cells: &[Vec<String>]) -> String {
675 let widest =
676 result.names().iter().map(|name| width(name)).max().unwrap_or(0).max(LINE_NAME_WIDTH);
677 let mut out = String::new();
678 for (at, values) in cells.iter().enumerate() {
679 if at > 0 {
680 out.push('\n');
681 }
682 for (name, value) in result.names().iter().zip(values) {
683 let _ = writeln!(out, "{} = {}", pad(name, widest, true), value);
684 }
685 }
686 out
687}
688
689fn separated(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
691 let quoted = settings.format == Format::Csv;
692 let mut out = String::new();
693 if settings.header {
694 let heads: Vec<String> = result
695 .names()
696 .iter()
697 .map(|name| if quoted { csv(name, settings) } else { name.clone() })
698 .collect();
699 out.push_str(&heads.join(&settings.separator));
700 out.push_str(&settings.newline);
701 }
702 for values in cells {
703 let parts: Vec<String> = values
704 .iter()
705 .map(|value| if quoted { csv(value, settings) } else { value.clone() })
706 .collect();
707 out.push_str(&parts.join(&settings.separator));
708 out.push_str(&settings.newline);
709 }
710 out
711}
712
713fn csv(text: &str, settings: &Settings) -> String {
725 let awkward =
726 text.contains(&settings.separator) || text.bytes().any(|byte| AWKWARD[byte as usize]);
727 if awkward { format!("\"{}\"", text.replace('"', "\"\"")) } else { text.to_string() }
728}
729
730static AWKWARD: [bool; 256] = {
736 let mut table = [false; 256];
737 let mut byte = 0;
738 while byte < 256 {
739 table[byte] = byte < 0x20 || byte == 0x7f || byte >= 0x80;
740 byte += 1;
741 }
742 table[b'"' as usize] = true;
743 table[b'\'' as usize] = true;
744 table
745};
746
747fn json(result: &QueryResult, settings: &Settings, array: bool) -> String {
749 let mut out = String::new();
750 for row in 0..result.len() {
753 let parts: Vec<String> = (0..result.width())
754 .map(|column| {
755 format!(
756 "{}:{}",
757 json_string(&result.names()[column]),
758 json_value(&result.value_at(row, column))
759 )
760 })
761 .collect();
762 let object = format!("{{{}}}", parts.join(","));
763 if array {
764 if row == 0 {
765 out.push('[');
766 }
767 out.push_str(&object);
768 if row + 1 < result.len() {
769 out.push_str(",\n");
770 } else {
771 out.push_str("]\n");
772 }
773 } else {
774 let _ = writeln!(out, "{object}");
775 }
776 }
777 if array && result.is_empty() {
778 out.push_str("[]\n");
779 }
780 let _ = settings;
781 out
782}
783
784fn json_string(text: &str) -> String {
786 let mut out = String::with_capacity(text.len() + 2);
787 out.push('"');
788 for character in text.chars() {
789 match character {
790 '"' => out.push_str("\\\""),
791 '\\' => out.push_str("\\\\"),
792 '\n' => out.push_str("\\n"),
793 '\r' => out.push_str("\\r"),
794 '\t' => out.push_str("\\t"),
795 other if (other as u32) < 0x20 => {
796 let _ = write!(out, "\\u{:04x}", other as u32);
797 }
798 other => out.push(other),
799 }
800 }
801 out.push('"');
802 out
803}
804
805fn json_value(value: &Value) -> String {
807 match value {
808 Value::Null => "null".to_string(),
809 Value::Boolean(flag) => flag.to_string(),
810 Value::List { values, .. } => {
811 let parts: Vec<String> = values.iter().map(json_value).collect();
812 format!("[{}]", parts.join(","))
813 }
814 Value::Struct(fields) => {
815 let parts: Vec<String> = fields
816 .iter()
817 .map(|(name, value)| format!("{}:{}", json_string(name), json_value(value)))
818 .collect();
819 format!("{{{}}}", parts.join(","))
820 }
821 other if is_number(other) => other.to_string(),
822 other => json_string(&other.to_string()),
823 }
824}
825
826fn is_number(value: &Value) -> bool {
828 matches!(
829 value,
830 Value::TinyInt(_)
831 | Value::SmallInt(_)
832 | Value::Integer(_)
833 | Value::BigInt(_)
834 | Value::HugeInt(_)
835 | Value::UTinyInt(_)
836 | Value::USmallInt(_)
837 | Value::UInteger(_)
838 | Value::UBigInt(_)
839 | Value::UHugeInt(_)
840 | Value::Float(_)
841 | Value::Double(_)
842 | Value::Decimal { .. }
843 )
844}
845
846fn quote(result: &QueryResult, settings: &Settings) -> String {
848 let mut out = String::new();
849 if settings.header {
850 let heads: Vec<String> =
851 result.names().iter().map(|name| format!("'{}'", name.replace('\'', "''"))).collect();
852 out.push_str(&heads.join(&settings.separator));
853 out.push_str(&settings.newline);
854 }
855 for row in 0..result.len() {
857 let parts: Vec<String> =
858 (0..result.width()).map(|column| sql_literal(&result.value_at(row, column))).collect();
859 out.push_str(&parts.join(&settings.separator));
860 out.push_str(&settings.newline);
861 }
862 out
863}
864
865fn insert(result: &QueryResult, settings: &Settings) -> String {
867 let mut out = String::new();
868 let columns = result.names().join(",");
869 for row in 0..result.len() {
871 let parts: Vec<String> =
872 (0..result.width()).map(|column| sql_literal(&result.value_at(row, column))).collect();
873 let _ = writeln!(
874 out,
875 "INSERT INTO \"{}\"({}) VALUES({});",
876 settings.table,
877 columns,
878 parts.join(",")
879 );
880 }
881 out
882}
883
884fn sql_literal(value: &Value) -> String {
886 match value {
887 Value::Null => "NULL".to_string(),
888 other if is_number(other) => other.to_string(),
889 Value::Boolean(flag) => flag.to_string(),
890 other => format!("'{}'", other.to_string().replace('\'', "''")),
891 }
892}
893
894fn html(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
896 let mut out = String::new();
897 if settings.header {
898 out.push_str("<tr>");
899 for name in result.names() {
900 let _ = writeln!(out, "<th>{}</th>", escape(name));
901 }
902 out.push_str("</tr>\n");
903 }
904 for values in cells {
905 out.push_str("<tr>");
906 for value in values {
907 let _ = writeln!(out, "<td>{}</td>", escape(value));
908 }
909 out.push_str("</tr>\n");
910 }
911 out
912}
913
914fn escape(text: &str) -> String {
916 text.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)
917}
918
919fn ascii(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
921 let mut out = String::new();
922 if settings.header {
923 for name in result.names() {
924 let _ = writeln!(out, "{name}");
925 }
926 }
927 for values in cells {
928 for value in values {
929 let _ = writeln!(out, "{value}");
930 }
931 }
932 let _ = settings;
933 out
934}
935
936fn column(result: &QueryResult, cells: &[Vec<String>]) -> String {
938 let sizes = widths(result, cells, false);
939 let mut out = String::new();
940 let heads: Vec<String> =
941 result.names().iter().zip(&sizes).map(|(name, size)| pad(name, *size, false)).collect();
942 let _ = writeln!(out, "{}", heads.join(" "));
943 let rules: Vec<String> = sizes.iter().map(|size| "-".repeat(*size)).collect();
944 let _ = writeln!(out, "{}", rules.join(" "));
945 for values in cells {
946 let parts: Vec<String> =
947 values.iter().zip(&sizes).map(|(value, size)| pad(value, *size, false)).collect();
948 let _ = writeln!(out, "{}", parts.join(" "));
949 }
950 out
951}