Skip to main content

rudb_cli/
format.rs

1//! The output modes.
2//!
3//! Every mode here is one DuckDB has, spelled the way DuckDB spells it, and producing the bytes
4//! DuckDB produces. That is the whole point: a script that pipes `.mode csv` output into something
5//! else is a script that has to keep working when the binary name changes, and a mode that is
6//! nearly right is worse than one that is missing, because a missing one says so.
7//!
8//! `tests/shell.rs` holds the captured output of a real DuckDB binary for each of these and diffs
9//! against it, so none of this is a claim.
10
11use std::fmt::Write as _;
12
13use rudb::QueryResult;
14use rudb_common::{LogicalType, Value};
15
16/// How many rows `duckbox` prints before it starts leaving some out.
17const MAX_ROWS: usize = 40;
18
19/// The three dots that stand in for the rows `duckbox` left out.
20const ELIDED: usize = 3;
21
22/// The narrowest field `line` mode right aligns a column name in.
23const LINE_NAME_WIDTH: usize = 5;
24
25/// What a result is printed as.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum Format {
28    /// The default. A box with the column types under the names and a row count under the table.
29    #[default]
30    DuckBox,
31    /// A box without the type row and without the row count.
32    Box,
33    /// The same table drawn in `+`, `-` and `|`.
34    Table,
35    /// A GitHub flavoured markdown table.
36    Markdown,
37    /// One `name = value` per line, a blank line between rows.
38    Line,
39    /// Values joined by the separator, which defaults to a pipe.
40    List,
41    /// Comma separated, with the quoting rules of RFC 4180.
42    Csv,
43    /// Tab separated.
44    Tsv,
45    /// One JSON array of objects.
46    Json,
47    /// One JSON object per line.
48    JsonLines,
49    /// Single quoted values, comma separated, in the spelling SQL wants.
50    Quote,
51    /// One `INSERT` statement per row.
52    Insert,
53    /// Table rows and cells as HTML, without the surrounding table element, which is what DuckDB
54    /// emits.
55    Html,
56    /// One value per line, columns first, no decoration at all.
57    Ascii,
58    /// Space padded columns under a dashed rule.
59    Column,
60    /// Nothing at all, for timing a query without paying to print it.
61    Trash,
62}
63
64impl Format {
65    /// The mode of that name, or `None` if there is no such mode.
66    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    /// The name this mode answers to, which is what `.show` prints.
89    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    /// What `.mode` sets the column separator to, since setting the mode resets it.
111    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    /// What `.mode` sets the row separator to.
121    ///
122    /// CSV gets `\r\n` because RFC 4180 says so and because DuckDB does it, which surprises people
123    /// reading the file on a Unix machine and is nonetheless what a CSV is.
124    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/// Everything about how output is printed, which is what the dot commands change.
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct Settings {
136    /// The output mode.
137    pub format: Format,
138    /// Whether to print the column names.
139    pub header: bool,
140    /// What goes between two values in the separated modes.
141    pub separator: String,
142    /// What goes between two rows in the separated modes.
143    pub newline: String,
144    /// What a null prints as in the modes that do not have a spelling of their own for it.
145    pub nullvalue: String,
146    /// The table name `.mode insert` puts in the statements it writes.
147    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    /// Switches mode, resetting both separators to that mode's defaults.
165    ///
166    /// Resetting is DuckDB's behaviour and it surprises people, so it is worth saying why it is
167    /// right: `.mode csv` means "write me a CSV", and a pipe separator left over from an earlier
168    /// `.mode list` would produce a file that is not one. The header setting is deliberately left
169    /// alone, which is also DuckDB's behaviour and was checked against the binary rather than
170    /// guessed, because `.headers off` is a thing somebody says once and expects to stay said.
171    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
178/// How `.show` spells a separator, which is with the escapes rather than the bytes.
179pub 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
196/// Prints a result.
197pub 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
220/// Every value as the text it prints as, which the table modes then measure and pad.
221fn 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
231/// One value as text.
232fn cell(value: &Value, settings: &Settings) -> String {
233    match value {
234        Value::Null => settings.nullvalue.clone(),
235        other => other.to_string(),
236    }
237}
238
239/// The name `duckbox` puts under a column heading.
240///
241/// These are DuckDB's internal type names rather than the SQL spelling, which is why an `INTEGER`
242/// column says `int32`. Lives here rather than on [`LogicalType`] because the shell is the only
243/// thing that wants them; it moves down to `rudb-common` the day a second surface does.
244///
245/// The last arm is there because [`LogicalType`] is `non_exhaustive`, so a type added below this
246/// crate compiles rather than breaking the build. It prints the lowercased SQL name, which is right
247/// for most of them and is at worst a name a reader can still recognize.
248fn 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
296/// How wide a string is on a terminal.
297///
298/// Character count, not grapheme clusters and not East Asian width. That is wrong for a string
299/// holding a combining mark or a full width character and it is right for everything in the
300/// benchmarks and the corpus, and fixing it properly means a Unicode width table, which is a
301/// dependency and a decision of its own. Named here so it is a known gap rather than a surprise.
302fn width(text: &str) -> usize {
303    text.chars().count()
304}
305
306/// Which rows a `duckbox` table shows, and where the dots go.
307///
308/// Only elide when eliding actually saves lines. Replacing 41 rows with 20, three dots and 20 is
309/// longer than printing all 41, and DuckDB knows that, so the cut is at `MAX_ROWS + ELIDED`.
310fn shown(rows: usize) -> Option<(usize, usize)> {
311    if rows > MAX_ROWS + ELIDED { Some((MAX_ROWS / 2, MAX_ROWS / 2)) } else { None }
312}
313
314/// Pads `text` to `size`, to the right if `right` and to the left otherwise.
315fn 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
324/// Pads `text` to `size` with the extra space split, the odd one going right.
325fn 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
331/// The widest each column has to be, over the heading, the type and every value shown.
332fn 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
347/// The characters a box is drawn with.
348struct 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
369/// One horizontal rule of a box.
370fn 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
375/// One row of a box, each cell already padded.
376fn 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
384/// The default mode: a box, a type row, and a count under it.
385fn duckbox(result: &QueryResult, cells: &[Vec<String>]) -> String {
386    let mut sizes = widths(result, cells, true);
387    let counts = Counts::of(result);
388    // The table cannot be narrower than the count that has to appear under it, which is the only
389    // reason a one column table of `int32` holding no rows comes out eight wide rather than seven.
390    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
416/// How wide the box is across, the two border characters included.
417fn box_width(sizes: &[usize]) -> usize {
418    sizes.iter().map(|size| size + 2).sum::<usize>() + sizes.len() + 1
419}
420
421/// The hint DuckDB drops in the middle of the count line when a table is wide enough to hold it.
422const HINT: &str = "use .last to show entire result";
423
424/// What goes under a `duckbox` table, which is nothing at all for most results.
425///
426/// DuckDB prints a count only when the box does not already say what the count is. Three rows are
427/// three rows and get no commentary. Ten or more get a count because at that point nobody is
428/// counting the lines, none at all get one because an empty box says nothing on its own, and a
429/// result that had rows left out gets one because a table that is not all of the answer has to say
430/// so. The column count joins it only when there is more than one column and the box is wide
431/// enough to hold both.
432///
433/// The thresholds and the padding here were read off the duckdb binary rather than guessed, by
434/// sweeping the column count, the row count and the width of the widest heading and diffing every
435/// combination. That is also why the second line is one character longer than the first, which
436/// nobody would write on purpose and which a diff of the two shells would otherwise report forever.
437struct Counts {
438    rows: usize,
439    columns: usize,
440    /// How many rows the box actually shows, when that is fewer than there are.
441    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    /// `N rows`, and `N rows (M shown)` once the two have been put on one line.
452    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    /// How wide the box has to be for a count that has to appear to fit under it.
465    ///
466    /// Only a count that carries information the box does not widens the box. A result of ten rows
467    /// in a narrow box simply goes without its count, which looks like an oversight and is what
468    /// DuckDB does.
469    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    /// The lines to print under a box `total` wide.
481    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        // The two counts share a line as soon as they both fit on it, next to the column count if
490        // that is there as well. They can end up touching it, with no gap at all.
491        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
517/// A count on the left and a count on the right of one line, with the hint between them when rows
518/// were left out and the gap is wide enough to leave five spaces on either side of it.
519fn 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
534/// One count centred under a box `total` wide, on a line of `line` characters.
535///
536/// The centring is against the whole box and the trimming is against the line, which is not the
537/// same as centring in the line and is what puts the text one to the right of where centred would
538/// have it. Copied from DuckDB on purpose.
539fn 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
545/// The rows of a box, with the dots in the middle if some were left out.
546fn 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
576/// One cell per column of the row of dots that stands in for the rows a box left out.
577///
578/// The dot sits where the middle of a value in the column sat, measured from whichever edge that
579/// column is aligned against, and the value it lines up with is the shorter of the two either side
580/// of the gap. Only those two, not the shortest of everything printed, so a column can have one
581/// short value at the top and still have its dots way out to the right. So a column of small
582/// integers has its dots hard against the right edge and a column of `true` and `false` has them one
583/// in from the left, which looks like an accident and is what the duckdb binary prints. Centring
584/// them in the column is tidier and is a difference on every result over forty three rows.
585fn 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
610/// `.mode box` and `.mode table`, which are the same table without the types and without the count.
611fn boxed(result: &QueryResult, cells: &[Vec<String>], glyphs: Glyphs) -> String {
612    let sizes = widths(result, cells, false);
613    // Left for every column, numbers included. Only duckbox right aligns numbers, which reads oddly
614    // until you notice that box and table are the modes DuckDB inherited from SQLite and duckbox is
615    // the one it wrote.
616    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
628/// `.mode markdown`, where the alignment lives in the rule rather than in the padding.
629fn markdown(result: &QueryResult, cells: &[Vec<String>]) -> String {
630    let sizes = widths(result, cells, false);
631    // A numeric column says it is right aligned in the rule and is still padded on the right in the
632    // cells, which is markdown's whole trick: the renderer downstream does the aligning.
633    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
655/// `.mode line`, one `name = value` per line.
656///
657/// The names are right aligned in a field at least five wide, which is the width SQLite picked and
658/// DuckDB kept. A one letter column therefore gets four spaces in front of it and looks deliberate
659/// rather than broken, which is presumably the point.
660fn 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
675/// `.mode list`, `.mode csv` and `.mode tabs`, which differ only in the separator and the quoting.
676fn 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
699/// One CSV field, quoted when RFC 4180 says it has to be.
700fn 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
708/// `.mode json` and `.mode jsonlines`.
709fn json(result: &QueryResult, settings: &Settings, array: bool) -> String {
710    let mut out = String::new();
711    // row at a time: a printed row is a row, and the thing being fed is a terminal or a pipe, so
712    // the cost of the loop is nowhere near the cost of the bytes leaving the process.
713    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
745/// One JSON string, escaped.
746fn 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
766/// One JSON value, which is a number for a number and a string for everything that is not one.
767fn 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
787/// Whether a value prints as a JSON number rather than as a JSON string.
788fn 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
807/// `.mode quote`, which is the spelling a value has inside a SQL statement.
808fn 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    // row at a time: the output is one quoted row per line, so there is nothing to batch.
817    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
826/// `.mode insert`, one statement per row.
827fn insert(result: &QueryResult, settings: &Settings) -> String {
828    let mut out = String::new();
829    let columns = result.names().join(",");
830    // row at a time: the output is one INSERT statement per row, which is the shape of the mode.
831    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
845/// A value as it would be written in SQL.
846fn 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
855/// `.mode html`, the rows without the table around them, which is what DuckDB emits.
856fn 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
875/// The four characters that cannot appear raw in HTML text.
876fn escape(text: &str) -> String {
877    text.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")
878}
879
880/// `.mode ascii`, every value on its own line with no decoration.
881fn 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
897/// `.mode column`, space padded under a dashed rule.
898fn 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}