1use std::fmt::Write as _;
12
13use rudb::QueryResult;
14use rudb_common::{LogicalType, Value};
15
16const MAX_ROWS: usize = 40;
18
19const ELIDED: usize = 3;
21
22const LINE_NAME_WIDTH: usize = 5;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum Format {
28 #[default]
30 DuckBox,
31 Box,
33 Table,
35 Markdown,
37 Line,
39 List,
41 Csv,
43 Tsv,
45 Json,
47 JsonLines,
49 Quote,
51 Insert,
53 Html,
56 Ascii,
58 Column,
60 Trash,
62}
63
64impl Format {
65 pub fn from_name(name: &str) -> Option<Self> {
67 Some(match name {
68 "duckbox" => Self::DuckBox,
69 "box" => Self::Box,
70 "table" => Self::Table,
71 "markdown" => Self::Markdown,
72 "line" | "lines" => Self::Line,
73 "list" => Self::List,
74 "csv" => Self::Csv,
75 "tabs" | "tsv" => Self::Tsv,
76 "json" => Self::Json,
77 "jsonlines" | "ndjson" => Self::JsonLines,
78 "quote" => Self::Quote,
79 "insert" => Self::Insert,
80 "html" => Self::Html,
81 "ascii" => Self::Ascii,
82 "column" => Self::Column,
83 "trash" => Self::Trash,
84 _ => return None,
85 })
86 }
87
88 pub fn name(self) -> &'static str {
90 match self {
91 Self::DuckBox => "duckbox",
92 Self::Box => "box",
93 Self::Table => "table",
94 Self::Markdown => "markdown",
95 Self::Line => "line",
96 Self::List => "list",
97 Self::Csv => "csv",
98 Self::Tsv => "tabs",
99 Self::Json => "json",
100 Self::JsonLines => "jsonlines",
101 Self::Quote => "quote",
102 Self::Insert => "insert",
103 Self::Html => "html",
104 Self::Ascii => "ascii",
105 Self::Column => "column",
106 Self::Trash => "trash",
107 }
108 }
109
110 fn separator(self) -> &'static str {
112 match self {
113 Self::Csv | Self::Quote => ",",
114 Self::Tsv => "\t",
115 Self::Ascii => "\u{1f}",
116 _ => "|",
117 }
118 }
119
120 fn newline(self) -> &'static str {
125 match self {
126 Self::Csv => "\r\n",
127 Self::Ascii => "\u{1e}",
128 _ => "\n",
129 }
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct Settings {
136 pub format: Format,
138 pub header: bool,
140 pub separator: String,
142 pub newline: String,
144 pub nullvalue: String,
146 pub table: String,
148}
149
150impl Default for Settings {
151 fn default() -> Self {
152 Self {
153 format: Format::DuckBox,
154 header: true,
155 separator: "|".to_string(),
156 newline: "\n".to_string(),
157 nullvalue: "NULL".to_string(),
158 table: "table".to_string(),
159 }
160 }
161}
162
163impl Settings {
164 pub fn set_format(&mut self, format: Format) {
172 self.format = format;
173 self.separator = format.separator().to_string();
174 self.newline = format.newline().to_string();
175 }
176}
177
178pub fn escaped(text: &str) -> String {
180 let mut out = String::new();
181 for character in text.chars() {
182 match character {
183 '\n' => out.push_str("\\n"),
184 '\r' => out.push_str("\\r"),
185 '\t' => out.push_str("\\t"),
186 '\\' => out.push_str("\\\\"),
187 other if (other as u32) < 0x20 => {
188 let _ = write!(out, "\\{:03o}", other as u32);
189 }
190 other => out.push(other),
191 }
192 }
193 out
194}
195
196pub fn render(result: &QueryResult, settings: &Settings) -> String {
198 if result.width() == 0 {
199 return String::new();
200 }
201 let cells = cells(result, settings);
202 match settings.format {
203 Format::DuckBox => duckbox(result, &cells),
204 Format::Box => boxed(result, &cells, BOX_GLYPHS),
205 Format::Table => boxed(result, &cells, TABLE_GLYPHS),
206 Format::Markdown => markdown(result, &cells),
207 Format::Line => line(result, &cells),
208 Format::List | Format::Csv | Format::Tsv => separated(result, &cells, settings),
209 Format::Json => json(result, settings, true),
210 Format::JsonLines => json(result, settings, false),
211 Format::Quote => quote(result, settings),
212 Format::Insert => insert(result, settings),
213 Format::Html => html(result, &cells, settings),
214 Format::Ascii => ascii(result, &cells, settings),
215 Format::Column => column(result, &cells),
216 Format::Trash => String::new(),
217 }
218}
219
220fn cells(result: &QueryResult, settings: &Settings) -> Vec<Vec<String>> {
222 (0..result.len())
223 .map(|row| {
224 (0..result.width())
225 .map(|column| cell(&result.value_at(row, column), settings))
226 .collect()
227 })
228 .collect()
229}
230
231fn cell(value: &Value, settings: &Settings) -> String {
233 match value {
234 Value::Null => settings.nullvalue.clone(),
235 other => other.to_string(),
236 }
237}
238
239fn type_name(ty: &LogicalType) -> String {
249 match ty {
250 LogicalType::Null => "\"NULL\"".to_string(),
251 LogicalType::Boolean => "boolean".to_string(),
252 LogicalType::TinyInt => "int8".to_string(),
253 LogicalType::SmallInt => "int16".to_string(),
254 LogicalType::Integer => "int32".to_string(),
255 LogicalType::BigInt => "int64".to_string(),
256 LogicalType::HugeInt => "int128".to_string(),
257 LogicalType::UTinyInt => "uint8".to_string(),
258 LogicalType::USmallInt => "uint16".to_string(),
259 LogicalType::UInteger => "uint32".to_string(),
260 LogicalType::UBigInt => "uint64".to_string(),
261 LogicalType::UHugeInt => "uint128".to_string(),
262 LogicalType::Float => "float".to_string(),
263 LogicalType::Double => "double".to_string(),
264 LogicalType::Decimal { width, scale } => format!("decimal({width},{scale})"),
265 LogicalType::Varchar => "varchar".to_string(),
266 LogicalType::Blob => "blob".to_string(),
267 LogicalType::Bit => "bit".to_string(),
268 LogicalType::Uuid => "uuid".to_string(),
269 LogicalType::Date => "date".to_string(),
270 LogicalType::Time => "time".to_string(),
271 LogicalType::TimeTz => "time with time zone".to_string(),
272 LogicalType::Timestamp => "timestamp".to_string(),
273 LogicalType::TimestampS => "timestamp_s".to_string(),
274 LogicalType::TimestampMs => "timestamp_ms".to_string(),
275 LogicalType::TimestampNs => "timestamp_ns".to_string(),
276 LogicalType::TimestampTz => "timestamp with time zone".to_string(),
277 LogicalType::Interval => "interval".to_string(),
278 LogicalType::List(inner) | LogicalType::Array(inner, _) => {
279 format!("{}[]", type_name(inner))
280 }
281 LogicalType::Map(key, value) => format!("map({}, {})", type_name(key), type_name(value)),
282 LogicalType::Struct(fields) => {
283 let inner: Vec<String> =
284 fields.iter().map(|field| format!("{} {}", field.name, field.ty)).collect();
285 format!("struct({})", inner.join(", ")).to_lowercase()
286 }
287 LogicalType::Union(fields) => {
288 let inner: Vec<String> =
289 fields.iter().map(|field| format!("{} {}", field.name, field.ty)).collect();
290 format!("union({})", inner.join(", ")).to_lowercase()
291 }
292 other => other.to_string().to_lowercase(),
293 }
294}
295
296fn width(text: &str) -> usize {
303 text.chars().count()
304}
305
306fn shown(rows: usize) -> Option<(usize, usize)> {
311 if rows > MAX_ROWS + ELIDED { Some((MAX_ROWS / 2, MAX_ROWS / 2)) } else { None }
312}
313
314fn pad(text: &str, size: usize, right: bool) -> String {
316 let missing = size.saturating_sub(width(text));
317 if right {
318 format!("{}{}", " ".repeat(missing), text)
319 } else {
320 format!("{}{}", text, " ".repeat(missing))
321 }
322}
323
324fn centre(text: &str, size: usize) -> String {
326 let missing = size.saturating_sub(width(text));
327 let left = missing / 2;
328 format!("{}{}{}", " ".repeat(left), text, " ".repeat(missing - left))
329}
330
331fn widths(result: &QueryResult, cells: &[Vec<String>], types: bool) -> Vec<usize> {
333 (0..result.width())
334 .map(|column| {
335 let mut size = width(&result.names()[column]);
336 if types {
337 size = size.max(width(&type_name(&result.types()[column])));
338 }
339 for row in cells {
340 size = size.max(width(&row[column]));
341 }
342 size
343 })
344 .collect()
345}
346
347struct Glyphs {
349 top: [&'static str; 4],
350 middle: [&'static str; 4],
351 bottom: [&'static str; 4],
352 vertical: &'static str,
353}
354
355const BOX_GLYPHS: Glyphs = Glyphs {
356 top: ["┌", "─", "┬", "┐"],
357 middle: ["├", "─", "┼", "┤"],
358 bottom: ["└", "─", "┴", "┘"],
359 vertical: "│",
360};
361
362const TABLE_GLYPHS: Glyphs = Glyphs {
363 top: ["+", "-", "+", "+"],
364 middle: ["+", "-", "+", "+"],
365 bottom: ["+", "-", "+", "+"],
366 vertical: "|",
367};
368
369fn rule(widths: &[usize], glyphs: &[&str; 4]) -> String {
371 let parts: Vec<String> = widths.iter().map(|size| glyphs[1].repeat(size + 2)).collect();
372 format!("{}{}{}", glyphs[0], parts.join(glyphs[2]), glyphs[3])
373}
374
375fn row(parts: &[String], vertical: &str) -> String {
377 let mut out = String::from(vertical);
378 for part in parts {
379 let _ = write!(out, " {part} {vertical}");
380 }
381 out
382}
383
384fn duckbox(result: &QueryResult, cells: &[Vec<String>]) -> String {
386 let mut sizes = widths(result, cells, true);
387 let counts = Counts::of(result);
388 if let Some(needed) = counts.minimum_width() {
391 let total = box_width(&sizes);
392 if let Some(last) = sizes.last_mut() {
393 *last += needed.saturating_sub(total);
394 }
395 }
396 let right: Vec<bool> = result.types().iter().map(LogicalType::is_numeric).collect();
397 let mut out = String::new();
398 let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.top));
399 let heads: Vec<String> =
400 result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
401 let _ = writeln!(out, "{}", row(&heads, BOX_GLYPHS.vertical));
402 let types: Vec<String> =
403 result.types().iter().zip(&sizes).map(|(ty, size)| centre(&type_name(ty), *size)).collect();
404 let _ = writeln!(out, "{}", row(&types, BOX_GLYPHS.vertical));
405 if !cells.is_empty() {
406 let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.middle));
407 write_rows(&mut out, cells, &sizes, &right, BOX_GLYPHS.vertical);
408 }
409 let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.bottom));
410 for text in counts.footer(box_width(&sizes)) {
411 let _ = writeln!(out, "{text}");
412 }
413 out
414}
415
416fn box_width(sizes: &[usize]) -> usize {
418 sizes.iter().map(|size| size + 2).sum::<usize>() + sizes.len() + 1
419}
420
421const HINT: &str = "use .last to show entire result";
423
424struct Counts {
438 rows: usize,
439 columns: usize,
440 shown: Option<usize>,
442}
443
444impl Counts {
445 fn of(result: &QueryResult) -> Self {
446 let rows = result.len();
447 let shown = shown(rows).map(|(head, tail)| head + tail);
448 Self { rows, columns: result.width(), shown }
449 }
450
451 fn row_text(&self) -> String {
453 format!("{} rows", self.rows)
454 }
455
456 fn shown_text(&self) -> Option<String> {
457 self.shown.map(|shown| format!("({shown} shown)"))
458 }
459
460 fn column_text(&self) -> String {
461 format!("{} columns", self.columns)
462 }
463
464 fn minimum_width(&self) -> Option<usize> {
470 if self.rows != 0 && self.shown.is_none() {
471 return None;
472 }
473 let widest = match self.shown_text() {
474 Some(text) => width(&text).max(width(&self.row_text())),
475 None => width(&self.row_text()),
476 };
477 Some(widest + 4)
478 }
479
480 fn footer(&self, total: usize) -> Vec<String> {
482 if self.rows != 0 && self.rows < 10 {
483 return Vec::new();
484 }
485 let mut rows = self.row_text();
486 let columns = self.column_text();
487 let with_columns =
488 self.rows >= 10 && self.columns > 1 && total >= width(&rows) + width(&columns) + 6;
489 let mut separate = self.shown_text();
492 if let Some(shown) = &separate {
493 let taken = if with_columns { width(&columns) } else { 0 };
494 if total.saturating_sub(taken) >= width(&rows) + width(shown) + 5 {
495 rows = format!("{rows} {shown}");
496 separate = None;
497 }
498 }
499 if with_columns {
500 let mut lines = vec![spread(&rows, &columns, total, self.shown.is_some())];
501 if let Some(shown) = separate {
502 lines.push(pad(&format!(" {shown}"), total - 1, false));
503 }
504 return lines;
505 }
506 if total < width(&rows) + 4 {
507 return Vec::new();
508 }
509 let mut lines = vec![middle(&rows, total, total - 2)];
510 if let Some(shown) = separate {
511 lines.push(middle(&shown, total, total - 1));
512 }
513 lines
514 }
515}
516
517fn spread(left: &str, right: &str, total: usize, hint: bool) -> String {
520 let line = total - 2;
521 let gap = line.saturating_sub(2 + width(left) + width(right));
522 if hint && gap >= width(HINT) + 10 {
523 let spare = gap - width(HINT);
524 let before = spare / 2;
525 return format!(
526 " {left}{}{HINT}{}{right}",
527 " ".repeat(before),
528 " ".repeat(spare - before)
529 );
530 }
531 format!(" {left}{}{right}", " ".repeat(gap))
532}
533
534fn middle(text: &str, total: usize, line: usize) -> String {
540 let left = total.saturating_sub(width(text)) / 2;
541 let right = line.saturating_sub(left + width(text));
542 format!("{}{text}{}", " ".repeat(left), " ".repeat(right))
543}
544
545fn write_rows(
547 out: &mut String,
548 cells: &[Vec<String>],
549 sizes: &[usize],
550 right: &[bool],
551 vertical: &str,
552) {
553 let dots = shown(cells.len());
554 for (at, values) in cells.iter().enumerate() {
555 if let Some((head, tail)) = dots {
556 if at == head {
557 let parts = dot_row(cells, sizes, right, head, tail);
558 for _ in 0..ELIDED {
559 let _ = writeln!(out, "{}", row(&parts, vertical));
560 }
561 }
562 if at >= head && at < cells.len() - tail {
563 continue;
564 }
565 }
566 let parts: Vec<String> = values
567 .iter()
568 .zip(sizes)
569 .zip(right)
570 .map(|((value, size), right)| pad(value, *size, *right))
571 .collect();
572 let _ = writeln!(out, "{}", row(&parts, vertical));
573 }
574}
575
576fn dot_row(
586 cells: &[Vec<String>],
587 sizes: &[usize],
588 right: &[bool],
589 head: usize,
590 tail: usize,
591) -> Vec<String> {
592 let above = cells.get(head.wrapping_sub(1));
593 let below = cells.get(cells.len() - tail);
594 sizes
595 .iter()
596 .zip(right)
597 .enumerate()
598 .map(|(column, (size, right))| {
599 let edge = |row: Option<&Vec<String>>| {
600 row.and_then(|values| values.get(column)).map_or(usize::MAX, |value| width(value))
601 };
602 let shortest = edge(above).min(edge(below));
603 let inset = (shortest.saturating_sub(1) / 2).min(size.saturating_sub(1));
604 let before = if *right { size.saturating_sub(1 + inset) } else { inset };
605 pad(&format!("{}·", " ".repeat(before)), *size, false)
606 })
607 .collect()
608}
609
610fn boxed(result: &QueryResult, cells: &[Vec<String>], glyphs: Glyphs) -> String {
612 let sizes = widths(result, cells, false);
613 let right = vec![false; result.width()];
617 let mut out = String::new();
618 let _ = writeln!(out, "{}", rule(&sizes, &glyphs.top));
619 let heads: Vec<String> =
620 result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
621 let _ = writeln!(out, "{}", row(&heads, glyphs.vertical));
622 let _ = writeln!(out, "{}", rule(&sizes, &glyphs.middle));
623 write_rows(&mut out, cells, &sizes, &right, glyphs.vertical);
624 let _ = writeln!(out, "{}", rule(&sizes, &glyphs.bottom));
625 out
626}
627
628fn markdown(result: &QueryResult, cells: &[Vec<String>]) -> String {
630 let sizes = widths(result, cells, false);
631 let numeric: Vec<bool> = result.types().iter().map(LogicalType::is_numeric).collect();
634 let right = vec![false; result.width()];
635 let mut out = String::new();
636 let heads: Vec<String> =
637 result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
638 let _ = writeln!(out, "{}", row(&heads, "|"));
639 let rules: Vec<String> = sizes
640 .iter()
641 .zip(&numeric)
642 .map(
643 |(size, right)| {
644 if *right { format!("{}:", "-".repeat(size + 1)) } else { "-".repeat(size + 2) }
645 },
646 )
647 .collect();
648 let _ = writeln!(out, "|{}|", rules.join("|"));
649 let mut rows = String::new();
650 write_rows(&mut rows, cells, &sizes, &right, "|");
651 out.push_str(&rows);
652 out
653}
654
655fn line(result: &QueryResult, cells: &[Vec<String>]) -> String {
661 let widest =
662 result.names().iter().map(|name| width(name)).max().unwrap_or(0).max(LINE_NAME_WIDTH);
663 let mut out = String::new();
664 for (at, values) in cells.iter().enumerate() {
665 if at > 0 {
666 out.push('\n');
667 }
668 for (name, value) in result.names().iter().zip(values) {
669 let _ = writeln!(out, "{} = {}", pad(name, widest, true), value);
670 }
671 }
672 out
673}
674
675fn separated(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
677 let quoted = settings.format == Format::Csv;
678 let mut out = String::new();
679 if settings.header {
680 let heads: Vec<String> = result
681 .names()
682 .iter()
683 .map(|name| if quoted { csv(name, settings) } else { name.clone() })
684 .collect();
685 out.push_str(&heads.join(&settings.separator));
686 out.push_str(&settings.newline);
687 }
688 for values in cells {
689 let parts: Vec<String> = values
690 .iter()
691 .map(|value| if quoted { csv(value, settings) } else { value.clone() })
692 .collect();
693 out.push_str(&parts.join(&settings.separator));
694 out.push_str(&settings.newline);
695 }
696 out
697}
698
699fn csv(text: &str, settings: &Settings) -> String {
701 let awkward = text.contains(&settings.separator)
702 || text.contains('"')
703 || text.contains('\n')
704 || text.contains('\r');
705 if awkward { format!("\"{}\"", text.replace('"', "\"\"")) } else { text.to_string() }
706}
707
708fn json(result: &QueryResult, settings: &Settings, array: bool) -> String {
710 let mut out = String::new();
711 for row in 0..result.len() {
714 let parts: Vec<String> = (0..result.width())
715 .map(|column| {
716 format!(
717 "{}:{}",
718 json_string(&result.names()[column]),
719 json_value(&result.value_at(row, column))
720 )
721 })
722 .collect();
723 let object = format!("{{{}}}", parts.join(","));
724 if array {
725 if row == 0 {
726 out.push('[');
727 }
728 out.push_str(&object);
729 if row + 1 < result.len() {
730 out.push_str(",\n");
731 } else {
732 out.push_str("]\n");
733 }
734 } else {
735 let _ = writeln!(out, "{object}");
736 }
737 }
738 if array && result.is_empty() {
739 out.push_str("[]\n");
740 }
741 let _ = settings;
742 out
743}
744
745fn json_string(text: &str) -> String {
747 let mut out = String::with_capacity(text.len() + 2);
748 out.push('"');
749 for character in text.chars() {
750 match character {
751 '"' => out.push_str("\\\""),
752 '\\' => out.push_str("\\\\"),
753 '\n' => out.push_str("\\n"),
754 '\r' => out.push_str("\\r"),
755 '\t' => out.push_str("\\t"),
756 other if (other as u32) < 0x20 => {
757 let _ = write!(out, "\\u{:04x}", other as u32);
758 }
759 other => out.push(other),
760 }
761 }
762 out.push('"');
763 out
764}
765
766fn json_value(value: &Value) -> String {
768 match value {
769 Value::Null => "null".to_string(),
770 Value::Boolean(flag) => flag.to_string(),
771 Value::List { values, .. } => {
772 let parts: Vec<String> = values.iter().map(json_value).collect();
773 format!("[{}]", parts.join(","))
774 }
775 Value::Struct(fields) => {
776 let parts: Vec<String> = fields
777 .iter()
778 .map(|(name, value)| format!("{}:{}", json_string(name), json_value(value)))
779 .collect();
780 format!("{{{}}}", parts.join(","))
781 }
782 other if is_number(other) => other.to_string(),
783 other => json_string(&other.to_string()),
784 }
785}
786
787fn is_number(value: &Value) -> bool {
789 matches!(
790 value,
791 Value::TinyInt(_)
792 | Value::SmallInt(_)
793 | Value::Integer(_)
794 | Value::BigInt(_)
795 | Value::HugeInt(_)
796 | Value::UTinyInt(_)
797 | Value::USmallInt(_)
798 | Value::UInteger(_)
799 | Value::UBigInt(_)
800 | Value::UHugeInt(_)
801 | Value::Float(_)
802 | Value::Double(_)
803 | Value::Decimal { .. }
804 )
805}
806
807fn quote(result: &QueryResult, settings: &Settings) -> String {
809 let mut out = String::new();
810 if settings.header {
811 let heads: Vec<String> =
812 result.names().iter().map(|name| format!("'{}'", name.replace('\'', "''"))).collect();
813 out.push_str(&heads.join(&settings.separator));
814 out.push_str(&settings.newline);
815 }
816 for row in 0..result.len() {
818 let parts: Vec<String> =
819 (0..result.width()).map(|column| sql_literal(&result.value_at(row, column))).collect();
820 out.push_str(&parts.join(&settings.separator));
821 out.push_str(&settings.newline);
822 }
823 out
824}
825
826fn insert(result: &QueryResult, settings: &Settings) -> String {
828 let mut out = String::new();
829 let columns = result.names().join(",");
830 for row in 0..result.len() {
832 let parts: Vec<String> =
833 (0..result.width()).map(|column| sql_literal(&result.value_at(row, column))).collect();
834 let _ = writeln!(
835 out,
836 "INSERT INTO \"{}\"({}) VALUES({});",
837 settings.table,
838 columns,
839 parts.join(",")
840 );
841 }
842 out
843}
844
845fn sql_literal(value: &Value) -> String {
847 match value {
848 Value::Null => "NULL".to_string(),
849 other if is_number(other) => other.to_string(),
850 Value::Boolean(flag) => flag.to_string(),
851 other => format!("'{}'", other.to_string().replace('\'', "''")),
852 }
853}
854
855fn html(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
857 let mut out = String::new();
858 if settings.header {
859 out.push_str("<tr>");
860 for name in result.names() {
861 let _ = writeln!(out, "<th>{}</th>", escape(name));
862 }
863 out.push_str("</tr>\n");
864 }
865 for values in cells {
866 out.push_str("<tr>");
867 for value in values {
868 let _ = writeln!(out, "<td>{}</td>", escape(value));
869 }
870 out.push_str("</tr>\n");
871 }
872 out
873}
874
875fn escape(text: &str) -> String {
877 text.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)
878}
879
880fn ascii(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
882 let mut out = String::new();
883 if settings.header {
884 for name in result.names() {
885 let _ = writeln!(out, "{name}");
886 }
887 }
888 for values in cells {
889 for value in values {
890 let _ = writeln!(out, "{value}");
891 }
892 }
893 let _ = settings;
894 out
895}
896
897fn column(result: &QueryResult, cells: &[Vec<String>]) -> String {
899 let sizes = widths(result, cells, false);
900 let mut out = String::new();
901 let heads: Vec<String> =
902 result.names().iter().zip(&sizes).map(|(name, size)| pad(name, *size, false)).collect();
903 let _ = writeln!(out, "{}", heads.join(" "));
904 let rules: Vec<String> = sizes.iter().map(|size| "-".repeat(*size)).collect();
905 let _ = writeln!(out, "{}", rules.join(" "));
906 for values in cells {
907 let parts: Vec<String> =
908 values.iter().zip(&sizes).map(|(value, size)| pad(value, *size, false)).collect();
909 let _ = writeln!(out, "{}", parts.join(" "));
910 }
911 out
912}