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::{LogicalType, QueryResult, Value};
14
15/// How many rows `duckbox` prints before it starts leaving some out.
16const MAX_ROWS: usize = 40;
17
18/// The three dots that stand in for the rows `duckbox` left out.
19const ELIDED: usize = 3;
20
21/// The narrowest field `line` mode right aligns a column name in.
22const LINE_NAME_WIDTH: usize = 5;
23
24/// What a result is printed as.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum Format {
27    /// The default. A box with the column types under the names and a row count under the table.
28    #[default]
29    DuckBox,
30    /// A box without the type row and without the row count.
31    Box,
32    /// The same table drawn in `+`, `-` and `|`.
33    Table,
34    /// A GitHub flavoured markdown table.
35    Markdown,
36    /// One `name = value` per line, a blank line between rows.
37    Line,
38    /// Values joined by the separator, which defaults to a pipe.
39    List,
40    /// Comma separated, with the quoting rules of RFC 4180.
41    Csv,
42    /// Tab separated.
43    Tsv,
44    /// One JSON array of objects.
45    Json,
46    /// One JSON object per line.
47    JsonLines,
48    /// Single quoted values, comma separated, in the spelling SQL wants.
49    Quote,
50    /// One `INSERT` statement per row.
51    Insert,
52    /// Table rows and cells as HTML, without the surrounding table element, which is what DuckDB
53    /// emits.
54    Html,
55    /// One value per line, columns first, no decoration at all.
56    Ascii,
57    /// Space padded columns under a dashed rule.
58    Column,
59    /// Nothing at all, for timing a query without paying to print it.
60    Trash,
61}
62
63impl Format {
64    /// The mode of that name, or `None` if there is no such mode.
65    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    /// The name this mode answers to, which is what `.show` prints.
88    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    /// What `.mode` sets the column separator to, since setting the mode resets it.
110    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    /// What `.mode` sets the row separator to.
120    ///
121    /// CSV gets `\r\n` because RFC 4180 says so and because DuckDB does it, which surprises people
122    /// reading the file on a Unix machine and is nonetheless what a CSV is.
123    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/// Everything about how output is printed, which is what the dot commands change.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct Settings {
135    /// The output mode.
136    pub format: Format,
137    /// Whether to print the column names.
138    pub header: bool,
139    /// What goes between two values in the separated modes.
140    pub separator: String,
141    /// What goes between two rows in the separated modes.
142    pub newline: String,
143    /// What a null prints as in the modes that do not have a spelling of their own for it.
144    pub nullvalue: String,
145    /// The table name `.mode insert` puts in the statements it writes.
146    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    /// Switches mode, resetting both separators to that mode's defaults.
164    ///
165    /// Resetting is DuckDB's behaviour and it surprises people, so it is worth saying why it is
166    /// right: `.mode csv` means "write me a CSV", and a pipe separator left over from an earlier
167    /// `.mode list` would produce a file that is not one. The header setting is deliberately left
168    /// alone, which is also DuckDB's behaviour and was checked against the binary rather than
169    /// guessed, because `.headers off` is a thing somebody says once and expects to stay said.
170    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    /// Switches mode the way a command line flag does, which is not the way `.mode` does.
177    ///
178    /// A flag sets the column separator and leaves the row separator alone. The difference shows up
179    /// in exactly one place and it is the one people pipe into other programs: `duckdb -csv` ends a
180    /// row with `\n` and `duckdb -cmd ".mode csv"` ends it with `\r\n`, on the same build, in the
181    /// same run. It looks like an oversight upstream and it is not ours to correct, because a script
182    /// written against `duckdb -csv` is a script whose next stage is counting bytes.
183    ///
184    /// Checked against `duckdb v2.0.0-dev84237` for `-list`, `-csv` and `-ascii`, which are the
185    /// flags that name a mode and reach this.
186    pub fn set_format_flag(&mut self, format: Format) {
187        self.format = format;
188        self.separator = format.separator().to_string();
189    }
190}
191
192/// How `.show` spells a separator, which is with the escapes rather than the bytes.
193pub 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
210/// Prints a result.
211pub 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
234/// Every value as the text it prints as, which the table modes then measure and pad.
235fn 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
245/// One value as text.
246fn cell(value: &Value, settings: &Settings) -> String {
247    match value {
248        Value::Null => settings.nullvalue.clone(),
249        other => other.to_string(),
250    }
251}
252
253/// The name `duckbox` puts under a column heading.
254///
255/// These are DuckDB's internal type names rather than the SQL spelling, which is why an `INTEGER`
256/// column says `int32`. Lives here rather than on [`LogicalType`] because the shell is the only
257/// thing that wants them; it moves down to `rudb-common` the day a second surface does.
258///
259/// The last arm is there because [`LogicalType`] is `non_exhaustive`, so a type added below this
260/// crate compiles rather than breaking the build. It prints the lowercased SQL name, which is right
261/// for most of them and is at worst a name a reader can still recognize.
262fn 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
310/// How wide a string is on a terminal.
311///
312/// Character count, not grapheme clusters and not East Asian width. That is wrong for a string
313/// holding a combining mark or a full width character and it is right for everything in the
314/// benchmarks and the corpus, and fixing it properly means a Unicode width table, which is a
315/// dependency and a decision of its own. Named here so it is a known gap rather than a surprise.
316fn width(text: &str) -> usize {
317    text.chars().count()
318}
319
320/// Which rows a `duckbox` table shows, and where the dots go.
321///
322/// Only elide when eliding actually saves lines. Replacing 41 rows with 20, three dots and 20 is
323/// longer than printing all 41, and DuckDB knows that, so the cut is at `MAX_ROWS + ELIDED`.
324fn shown(rows: usize) -> Option<(usize, usize)> {
325    if rows > MAX_ROWS + ELIDED { Some((MAX_ROWS / 2, MAX_ROWS / 2)) } else { None }
326}
327
328/// Pads `text` to `size`, to the right if `right` and to the left otherwise.
329fn 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
338/// Pads `text` to `size` with the extra space split, the odd one going right.
339fn 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
345/// The widest each column has to be, over the heading, the type and every value shown.
346fn 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
361/// The characters a box is drawn with.
362struct 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
383/// One horizontal rule of a box.
384fn 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
389/// One row of a box, each cell already padded.
390fn 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
398/// The default mode: a box, a type row, and a count under it.
399fn duckbox(result: &QueryResult, cells: &[Vec<String>]) -> String {
400    let mut sizes = widths(result, cells, true);
401    let counts = Counts::of(result);
402    // The table cannot be narrower than the count that has to appear under it, which is the only
403    // reason a one column table of `int32` holding no rows comes out eight wide rather than seven.
404    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
430/// How wide the box is across, the two border characters included.
431fn box_width(sizes: &[usize]) -> usize {
432    sizes.iter().map(|size| size + 2).sum::<usize>() + sizes.len() + 1
433}
434
435/// The hint DuckDB drops in the middle of the count line when a table is wide enough to hold it.
436const HINT: &str = "use .last to show entire result";
437
438/// What goes under a `duckbox` table, which is nothing at all for most results.
439///
440/// DuckDB prints a count only when the box does not already say what the count is. Three rows are
441/// three rows and get no commentary. Ten or more get a count because at that point nobody is
442/// counting the lines, none at all get one because an empty box says nothing on its own, and a
443/// result that had rows left out gets one because a table that is not all of the answer has to say
444/// so. The column count joins it only when there is more than one column and the box is wide
445/// enough to hold both.
446///
447/// The thresholds and the padding here were read off the duckdb binary rather than guessed, by
448/// sweeping the column count, the row count and the width of the widest heading and diffing every
449/// combination. That is also why the second line is one character longer than the first, which
450/// nobody would write on purpose and which a diff of the two shells would otherwise report forever.
451struct Counts {
452    rows: usize,
453    columns: usize,
454    /// How many rows the box actually shows, when that is fewer than there are.
455    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    /// `N rows`, and `N rows (M shown)` once the two have been put on one line.
466    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    /// How wide the box has to be for a count that has to appear to fit under it.
479    ///
480    /// Only a count that carries information the box does not widens the box. A result of ten rows
481    /// in a narrow box simply goes without its count, which looks like an oversight and is what
482    /// DuckDB does.
483    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    /// The lines to print under a box `total` wide.
495    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        // The two counts share a line as soon as they both fit on it, next to the column count if
504        // that is there as well. They can end up touching it, with no gap at all.
505        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
531/// A count on the left and a count on the right of one line, with the hint between them when rows
532/// were left out and the gap is wide enough to leave five spaces on either side of it.
533fn 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
548/// One count centred under a box `total` wide, on a line of `line` characters.
549///
550/// The centring is against the whole box and the trimming is against the line, which is not the
551/// same as centring in the line and is what puts the text one to the right of where centred would
552/// have it. Copied from DuckDB on purpose.
553fn 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
559/// The rows of a box, with the dots in the middle if some were left out.
560fn 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
590/// One cell per column of the row of dots that stands in for the rows a box left out.
591///
592/// The dot sits where the middle of a value in the column sat, measured from whichever edge that
593/// column is aligned against, and the value it lines up with is the shorter of the two either side
594/// of the gap. Only those two, not the shortest of everything printed, so a column can have one
595/// short value at the top and still have its dots way out to the right. So a column of small
596/// integers has its dots hard against the right edge and a column of `true` and `false` has them one
597/// in from the left, which looks like an accident and is what the duckdb binary prints. Centring
598/// them in the column is tidier and is a difference on every result over forty three rows.
599fn 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
624/// `.mode box` and `.mode table`, which are the same table without the types and without the count.
625fn boxed(result: &QueryResult, cells: &[Vec<String>], glyphs: Glyphs) -> String {
626    let sizes = widths(result, cells, false);
627    // Left for every column, numbers included. Only duckbox right aligns numbers, which reads oddly
628    // until you notice that box and table are the modes DuckDB inherited from SQLite and duckbox is
629    // the one it wrote.
630    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
642/// `.mode markdown`, where the alignment lives in the rule rather than in the padding.
643fn markdown(result: &QueryResult, cells: &[Vec<String>]) -> String {
644    let sizes = widths(result, cells, false);
645    // A numeric column says it is right aligned in the rule and is still padded on the right in the
646    // cells, which is markdown's whole trick: the renderer downstream does the aligning.
647    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
669/// `.mode line`, one `name = value` per line.
670///
671/// The names are right aligned in a field at least five wide, which is the width SQLite picked and
672/// DuckDB kept. A one letter column therefore gets four spaces in front of it and looks deliberate
673/// rather than broken, which is presumably the point.
674fn 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
689/// `.mode list`, `.mode csv` and `.mode tabs`, which differ only in the separator and the quoting.
690fn 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
713/// One CSV field, quoted when the shell would quote it.
714///
715/// Not RFC 4180, which asks for a quote around a field holding the separator, a quote or a line
716/// break and nothing else. The shell quotes a good deal more than that, and it has to be matched
717/// rather than improved on, because a diff of two CSV files is a byte comparison and a field that
718/// is correct under the standard and different from the reference is still a difference.
719///
720/// What it quotes on top of the separator is in [`AWKWARD`]. The one that matters is the top half
721/// of the byte range, which means every non-ASCII value in the output comes out quoted. Thirty two
722/// of the forty three ClickBench queries return Russian text and every one of them differs from the
723/// reference without this.
724fn 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
730/// The bytes that put quotes around a CSV field on their own, whatever the separator is.
731///
732/// Every byte under a space, both quote characters, delete, and the whole top half. This is the
733/// table the shell carries, copied because the rule is a table and writing it as a condition is
734/// how the delete and the apostrophe get left out.
735static 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
747/// `.mode json` and `.mode jsonlines`.
748fn json(result: &QueryResult, settings: &Settings, array: bool) -> String {
749    let mut out = String::new();
750    // row at a time: a printed row is a row, and the thing being fed is a terminal or a pipe, so
751    // the cost of the loop is nowhere near the cost of the bytes leaving the process.
752    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
784/// One JSON string, escaped.
785fn 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
805/// One JSON value, which is a number for a number and a string for everything that is not one.
806fn 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
826/// Whether a value prints as a JSON number rather than as a JSON string.
827fn 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
846/// `.mode quote`, which is the spelling a value has inside a SQL statement.
847fn 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    // row at a time: the output is one quoted row per line, so there is nothing to batch.
856    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
865/// `.mode insert`, one statement per row.
866fn insert(result: &QueryResult, settings: &Settings) -> String {
867    let mut out = String::new();
868    let columns = result.names().join(",");
869    // row at a time: the output is one INSERT statement per row, which is the shape of the mode.
870    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
884/// A value as it would be written in SQL.
885fn 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
894/// `.mode html`, the rows without the table around them, which is what DuckDB emits.
895fn 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
914/// The four characters that cannot appear raw in HTML text.
915fn escape(text: &str) -> String {
916    text.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")
917}
918
919/// `.mode ascii`, every value on its own line with no decoration.
920fn 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
936/// `.mode column`, space padded under a dashed rule.
937fn 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}