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 mode a command line flag names, or `None` when there is no flag of that name.
88    ///
89    /// Twelve of the sixteen modes are also flags and four of them are not, and the four are an
90    /// error rather than a mode. This is upstream's list rather than a rule, so it is written out
91    /// rather than derived: `duckdb -help` has a line for each of these twelve and no line for
92    /// `-duckbox`, `-insert`, `-tabs` or `-trash`, and asking v2.0.0-dev84237 for any of those four
93    /// gets `Unrecognized option` and a non zero exit. The aliases are not flags either, so
94    /// `-lines`, `-tsv` and `-ndjson` are refused while `.mode lines` is still accepted. Per #238.
95    pub fn from_flag(name: &str) -> Option<Self> {
96        match name {
97            "ascii" | "box" | "column" | "csv" | "html" | "json" | "jsonlines" | "line"
98            | "list" | "markdown" | "quote" | "table" => Self::from_name(name),
99            _ => None,
100        }
101    }
102
103    /// The name this mode answers to, which is what `.show` prints.
104    pub fn name(self) -> &'static str {
105        match self {
106            Self::DuckBox => "duckbox",
107            Self::Box => "box",
108            Self::Table => "table",
109            Self::Markdown => "markdown",
110            Self::Line => "line",
111            Self::List => "list",
112            Self::Csv => "csv",
113            Self::Tsv => "tabs",
114            Self::Json => "json",
115            Self::JsonLines => "jsonlines",
116            Self::Quote => "quote",
117            Self::Insert => "insert",
118            Self::Html => "html",
119            Self::Ascii => "ascii",
120            Self::Column => "column",
121            Self::Trash => "trash",
122        }
123    }
124
125    /// What `.mode` sets the column separator to, since setting the mode resets it.
126    fn separator(self) -> &'static str {
127        match self {
128            Self::Csv | Self::Quote => ",",
129            Self::Tsv => "\t",
130            Self::Ascii => "\u{1f}",
131            _ => "|",
132        }
133    }
134
135    /// What `.mode` sets the row separator to.
136    ///
137    /// CSV gets `\r\n` because RFC 4180 says so and because DuckDB does it, which surprises people
138    /// reading the file on a Unix machine and is nonetheless what a CSV is.
139    fn newline(self) -> &'static str {
140        match self {
141            Self::Csv => "\r\n",
142            Self::Ascii => "\u{1e}",
143            _ => "\n",
144        }
145    }
146}
147
148/// Everything about how output is printed, which is what the dot commands change.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct Settings {
151    /// The output mode.
152    pub format: Format,
153    /// Whether to print the column names.
154    pub header: bool,
155    /// What goes between two values in the separated modes.
156    pub separator: String,
157    /// What goes between two rows in the separated modes.
158    pub newline: String,
159    /// What a null prints as in the modes that do not have a spelling of their own for it.
160    pub nullvalue: String,
161    /// The table name `.mode insert` puts in the statements it writes.
162    pub table: String,
163}
164
165impl Default for Settings {
166    fn default() -> Self {
167        Self {
168            format: Format::DuckBox,
169            header: true,
170            separator: "|".to_string(),
171            newline: "\n".to_string(),
172            nullvalue: "NULL".to_string(),
173            table: "table".to_string(),
174        }
175    }
176}
177
178impl Settings {
179    /// Switches mode, resetting both separators to that mode's defaults.
180    ///
181    /// Resetting is DuckDB's behaviour and it surprises people, so it is worth saying why it is
182    /// right: `.mode csv` means "write me a CSV", and a pipe separator left over from an earlier
183    /// `.mode list` would produce a file that is not one. The header setting is deliberately left
184    /// alone, which is also DuckDB's behaviour and was checked against the binary rather than
185    /// guessed, because `.headers off` is a thing somebody says once and expects to stay said.
186    pub fn set_format(&mut self, format: Format) {
187        self.format = format;
188        self.separator = format.separator().to_string();
189        self.newline = format.newline().to_string();
190    }
191
192    /// Switches mode the way a command line flag does, which is not the way `.mode` does.
193    ///
194    /// There is no rule here, only a table, and the table is upstream's. `-ascii` sets both
195    /// separators, `-csv` sets the column separator and leaves the row separator, and the other ten
196    /// flags set neither. That is three behaviours across three flags on one build, which is why
197    /// this is a match rather than a line of code.
198    ///
199    /// It matters because these are the modes people pipe into other programs. `duckdb -csv` ends a
200    /// row with `\n` and `duckdb -cmd ".mode csv"` ends it with `\r\n`, and `duckdb -quote` writes
201    /// `'a'|'b'` where `duckdb -cmd ".mode quote"` writes `'a','b'`. A script written against the
202    /// flag is a script whose next stage counts bytes or splits on a character, so copying the
203    /// oversight is the whole point.
204    ///
205    /// Read out of `.show` on `duckdb v2.0.0-dev84237` for all twelve flags, once on its own and
206    /// once after `-separator ';' -newline '@'` so that leaving a separator alone can be told apart
207    /// from setting it to the same thing it already was. Per #239.
208    pub fn set_format_flag(&mut self, format: Format) {
209        self.format = format;
210        match format {
211            Format::Ascii => {
212                self.separator = format.separator().to_string();
213                self.newline = format.newline().to_string();
214            }
215            Format::Csv => self.separator = format.separator().to_string(),
216            _ => {}
217        }
218    }
219}
220
221/// How `.show` spells a separator, which is with the escapes rather than the bytes.
222pub fn escaped(text: &str) -> String {
223    let mut out = String::new();
224    for character in text.chars() {
225        match character {
226            '\n' => out.push_str("\\n"),
227            '\r' => out.push_str("\\r"),
228            '\t' => out.push_str("\\t"),
229            '\\' => out.push_str("\\\\"),
230            other if (other as u32) < 0x20 => {
231                let _ = write!(out, "\\{:03o}", other as u32);
232            }
233            other => out.push(other),
234        }
235    }
236    out
237}
238
239/// Prints a result.
240pub fn render(result: &QueryResult, settings: &Settings) -> String {
241    if result.width() == 0 {
242        return String::new();
243    }
244    let cells = cells(result, settings);
245    match settings.format {
246        Format::DuckBox => duckbox(result, &cells),
247        Format::Box => boxed(result, &cells, BOX_GLYPHS),
248        Format::Table => boxed(result, &cells, TABLE_GLYPHS),
249        Format::Markdown => markdown(result, &cells),
250        Format::Line => line(result, &cells),
251        Format::List | Format::Csv | Format::Tsv => separated(result, &cells, settings),
252        Format::Json => json(result, settings, true),
253        Format::JsonLines => json(result, settings, false),
254        Format::Quote => quote(result, settings),
255        Format::Insert => insert(result, settings),
256        Format::Html => html(result, &cells, settings),
257        Format::Ascii => ascii(result, &cells, settings),
258        Format::Column => column(result, &cells),
259        Format::Trash => String::new(),
260    }
261}
262
263/// Every value as the text it prints as, which the table modes then measure and pad.
264fn cells(result: &QueryResult, settings: &Settings) -> Vec<Vec<String>> {
265    (0..result.len())
266        .map(|row| {
267            (0..result.width())
268                .map(|column| cell(&result.value_at(row, column), settings))
269                .collect()
270        })
271        .collect()
272}
273
274/// One value as text.
275fn cell(value: &Value, settings: &Settings) -> String {
276    match value {
277        Value::Null => settings.nullvalue.clone(),
278        other => other.to_string(),
279    }
280}
281
282/// The name `duckbox` puts under a column heading.
283///
284/// These are DuckDB's internal type names rather than the SQL spelling, which is why an `INTEGER`
285/// column says `int32`. Lives here rather than on [`LogicalType`] because the shell is the only
286/// thing that wants them; it moves down to `rudb-common` the day a second surface does.
287///
288/// The last arm is there because [`LogicalType`] is `non_exhaustive`, so a type added below this
289/// crate compiles rather than breaking the build. It prints the lowercased SQL name, which is right
290/// for most of them and is at worst a name a reader can still recognize.
291fn type_name(ty: &LogicalType) -> String {
292    match ty {
293        LogicalType::Null => "\"NULL\"".to_string(),
294        LogicalType::Boolean => "boolean".to_string(),
295        LogicalType::TinyInt => "int8".to_string(),
296        LogicalType::SmallInt => "int16".to_string(),
297        LogicalType::Integer => "int32".to_string(),
298        LogicalType::BigInt => "int64".to_string(),
299        LogicalType::HugeInt => "int128".to_string(),
300        LogicalType::UTinyInt => "uint8".to_string(),
301        LogicalType::USmallInt => "uint16".to_string(),
302        LogicalType::UInteger => "uint32".to_string(),
303        LogicalType::UBigInt => "uint64".to_string(),
304        LogicalType::UHugeInt => "uint128".to_string(),
305        LogicalType::Float => "float".to_string(),
306        LogicalType::Double => "double".to_string(),
307        LogicalType::Decimal { width, scale } => format!("decimal({width},{scale})"),
308        LogicalType::Varchar => "varchar".to_string(),
309        LogicalType::Blob => "blob".to_string(),
310        LogicalType::Bit => "bit".to_string(),
311        LogicalType::Uuid => "uuid".to_string(),
312        LogicalType::Date => "date".to_string(),
313        LogicalType::Time => "time".to_string(),
314        LogicalType::TimeTz => "time with time zone".to_string(),
315        LogicalType::Timestamp => "timestamp".to_string(),
316        LogicalType::TimestampS => "timestamp_s".to_string(),
317        LogicalType::TimestampMs => "timestamp_ms".to_string(),
318        LogicalType::TimestampNs => "timestamp_ns".to_string(),
319        LogicalType::TimestampTz => "timestamp with time zone".to_string(),
320        LogicalType::Interval => "interval".to_string(),
321        LogicalType::List(inner) | LogicalType::Array(inner, _) => {
322            format!("{}[]", type_name(inner))
323        }
324        LogicalType::Map(key, value) => format!("map({}, {})", type_name(key), type_name(value)),
325        LogicalType::Struct(fields) => {
326            let inner: Vec<String> =
327                fields.iter().map(|field| format!("{} {}", field.name, field.ty)).collect();
328            format!("struct({})", inner.join(", ")).to_lowercase()
329        }
330        LogicalType::Union(fields) => {
331            let inner: Vec<String> =
332                fields.iter().map(|field| format!("{} {}", field.name, field.ty)).collect();
333            format!("union({})", inner.join(", ")).to_lowercase()
334        }
335        other => other.to_string().to_lowercase(),
336    }
337}
338
339/// How wide a string is on a terminal.
340///
341/// Character count, not grapheme clusters and not East Asian width. That is wrong for a string
342/// holding a combining mark or a full width character and it is right for everything in the
343/// benchmarks and the corpus, and fixing it properly means a Unicode width table, which is a
344/// dependency and a decision of its own. Named here so it is a known gap rather than a surprise.
345fn width(text: &str) -> usize {
346    text.chars().count()
347}
348
349/// Which rows a `duckbox` table shows, and where the dots go.
350///
351/// Only elide when eliding actually saves lines. Replacing 41 rows with 20, three dots and 20 is
352/// longer than printing all 41, and DuckDB knows that, so the cut is at `MAX_ROWS + ELIDED`.
353fn shown(rows: usize) -> Option<(usize, usize)> {
354    if rows > MAX_ROWS + ELIDED { Some((MAX_ROWS / 2, MAX_ROWS / 2)) } else { None }
355}
356
357/// Pads `text` to `size`, to the right if `right` and to the left otherwise.
358fn pad(text: &str, size: usize, right: bool) -> String {
359    let missing = size.saturating_sub(width(text));
360    if right {
361        format!("{}{}", " ".repeat(missing), text)
362    } else {
363        format!("{}{}", text, " ".repeat(missing))
364    }
365}
366
367/// Pads `text` to `size` with the extra space split, the odd one going right.
368fn centre(text: &str, size: usize) -> String {
369    let missing = size.saturating_sub(width(text));
370    let left = missing / 2;
371    format!("{}{}{}", " ".repeat(left), text, " ".repeat(missing - left))
372}
373
374/// The widest each column has to be, over the heading, the type and every value shown.
375fn widths(result: &QueryResult, cells: &[Vec<String>], types: bool) -> Vec<usize> {
376    (0..result.width())
377        .map(|column| {
378            let mut size = width(&result.names()[column]);
379            if types {
380                size = size.max(width(&type_name(&result.types()[column])));
381            }
382            for row in cells {
383                size = size.max(width(&row[column]));
384            }
385            size
386        })
387        .collect()
388}
389
390/// The characters a box is drawn with.
391struct Glyphs {
392    top: [&'static str; 4],
393    middle: [&'static str; 4],
394    bottom: [&'static str; 4],
395    vertical: &'static str,
396}
397
398const BOX_GLYPHS: Glyphs = Glyphs {
399    top: ["┌", "─", "┬", "┐"],
400    middle: ["├", "─", "┼", "┤"],
401    bottom: ["└", "─", "┴", "┘"],
402    vertical: "│",
403};
404
405const TABLE_GLYPHS: Glyphs = Glyphs {
406    top: ["+", "-", "+", "+"],
407    middle: ["+", "-", "+", "+"],
408    bottom: ["+", "-", "+", "+"],
409    vertical: "|",
410};
411
412/// One horizontal rule of a box.
413fn rule(widths: &[usize], glyphs: &[&str; 4]) -> String {
414    let parts: Vec<String> = widths.iter().map(|size| glyphs[1].repeat(size + 2)).collect();
415    format!("{}{}{}", glyphs[0], parts.join(glyphs[2]), glyphs[3])
416}
417
418/// One row of a box, each cell already padded.
419fn row(parts: &[String], vertical: &str) -> String {
420    let mut out = String::from(vertical);
421    for part in parts {
422        let _ = write!(out, " {part} {vertical}");
423    }
424    out
425}
426
427/// The default mode: a box, a type row, and a count under it.
428fn duckbox(result: &QueryResult, cells: &[Vec<String>]) -> String {
429    let mut sizes = widths(result, cells, true);
430    let counts = Counts::of(result);
431    // The table cannot be narrower than the count that has to appear under it, which is the only
432    // reason a one column table of `int32` holding no rows comes out eight wide rather than seven.
433    if let Some(needed) = counts.minimum_width() {
434        let total = box_width(&sizes);
435        if let Some(last) = sizes.last_mut() {
436            *last += needed.saturating_sub(total);
437        }
438    }
439    let right: Vec<bool> = result.types().iter().map(LogicalType::is_numeric).collect();
440    let mut out = String::new();
441    let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.top));
442    let heads: Vec<String> =
443        result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
444    let _ = writeln!(out, "{}", row(&heads, BOX_GLYPHS.vertical));
445    let types: Vec<String> =
446        result.types().iter().zip(&sizes).map(|(ty, size)| centre(&type_name(ty), *size)).collect();
447    let _ = writeln!(out, "{}", row(&types, BOX_GLYPHS.vertical));
448    if !cells.is_empty() {
449        let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.middle));
450        write_rows(&mut out, cells, &sizes, &right, BOX_GLYPHS.vertical);
451    }
452    let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.bottom));
453    for text in counts.footer(box_width(&sizes)) {
454        let _ = writeln!(out, "{text}");
455    }
456    out
457}
458
459/// How wide the box is across, the two border characters included.
460fn box_width(sizes: &[usize]) -> usize {
461    sizes.iter().map(|size| size + 2).sum::<usize>() + sizes.len() + 1
462}
463
464/// The hint DuckDB drops in the middle of the count line when a table is wide enough to hold it.
465const HINT: &str = "use .last to show entire result";
466
467/// What goes under a `duckbox` table, which is nothing at all for most results.
468///
469/// DuckDB prints a count only when the box does not already say what the count is. Three rows are
470/// three rows and get no commentary. Ten or more get a count because at that point nobody is
471/// counting the lines, none at all get one because an empty box says nothing on its own, and a
472/// result that had rows left out gets one because a table that is not all of the answer has to say
473/// so. The column count joins it only when there is more than one column and the box is wide
474/// enough to hold both.
475///
476/// The thresholds and the padding here were read off the duckdb binary rather than guessed, by
477/// sweeping the column count, the row count and the width of the widest heading and diffing every
478/// combination. That is also why the second line is one character longer than the first, which
479/// nobody would write on purpose and which a diff of the two shells would otherwise report forever.
480struct Counts {
481    rows: usize,
482    columns: usize,
483    /// How many rows the box actually shows, when that is fewer than there are.
484    shown: Option<usize>,
485}
486
487impl Counts {
488    fn of(result: &QueryResult) -> Self {
489        let rows = result.len();
490        let shown = shown(rows).map(|(head, tail)| head + tail);
491        Self { rows, columns: result.width(), shown }
492    }
493
494    /// `N rows`, and `N rows (M shown)` once the two have been put on one line.
495    fn row_text(&self) -> String {
496        format!("{} rows", self.rows)
497    }
498
499    fn shown_text(&self) -> Option<String> {
500        self.shown.map(|shown| format!("({shown} shown)"))
501    }
502
503    fn column_text(&self) -> String {
504        format!("{} columns", self.columns)
505    }
506
507    /// How wide the box has to be for a count that has to appear to fit under it.
508    ///
509    /// Only a count that carries information the box does not widens the box. A result of ten rows
510    /// in a narrow box simply goes without its count, which looks like an oversight and is what
511    /// DuckDB does.
512    fn minimum_width(&self) -> Option<usize> {
513        if self.rows != 0 && self.shown.is_none() {
514            return None;
515        }
516        let widest = match self.shown_text() {
517            Some(text) => width(&text).max(width(&self.row_text())),
518            None => width(&self.row_text()),
519        };
520        Some(widest + 4)
521    }
522
523    /// The lines to print under a box `total` wide.
524    fn footer(&self, total: usize) -> Vec<String> {
525        if self.rows != 0 && self.rows < 10 {
526            return Vec::new();
527        }
528        let mut rows = self.row_text();
529        let columns = self.column_text();
530        let with_columns =
531            self.rows >= 10 && self.columns > 1 && total >= width(&rows) + width(&columns) + 6;
532        // The two counts share a line as soon as they both fit on it, next to the column count if
533        // that is there as well. They can end up touching it, with no gap at all.
534        let mut separate = self.shown_text();
535        if let Some(shown) = &separate {
536            let taken = if with_columns { width(&columns) } else { 0 };
537            if total.saturating_sub(taken) >= width(&rows) + width(shown) + 5 {
538                rows = format!("{rows} {shown}");
539                separate = None;
540            }
541        }
542        if with_columns {
543            let mut lines = vec![spread(&rows, &columns, total, self.shown.is_some())];
544            if let Some(shown) = separate {
545                lines.push(pad(&format!("  {shown}"), total - 1, false));
546            }
547            return lines;
548        }
549        if total < width(&rows) + 4 {
550            return Vec::new();
551        }
552        let mut lines = vec![middle(&rows, total, total - 2)];
553        if let Some(shown) = separate {
554            lines.push(middle(&shown, total, total - 1));
555        }
556        lines
557    }
558}
559
560/// A count on the left and a count on the right of one line, with the hint between them when rows
561/// were left out and the gap is wide enough to leave five spaces on either side of it.
562fn spread(left: &str, right: &str, total: usize, hint: bool) -> String {
563    let line = total - 2;
564    let gap = line.saturating_sub(2 + width(left) + width(right));
565    if hint && gap >= width(HINT) + 10 {
566        let spare = gap - width(HINT);
567        let before = spare / 2;
568        return format!(
569            "  {left}{}{HINT}{}{right}",
570            " ".repeat(before),
571            " ".repeat(spare - before)
572        );
573    }
574    format!("  {left}{}{right}", " ".repeat(gap))
575}
576
577/// One count centred under a box `total` wide, on a line of `line` characters.
578///
579/// The centring is against the whole box and the trimming is against the line, which is not the
580/// same as centring in the line and is what puts the text one to the right of where centred would
581/// have it. Copied from DuckDB on purpose.
582fn middle(text: &str, total: usize, line: usize) -> String {
583    let left = total.saturating_sub(width(text)) / 2;
584    let right = line.saturating_sub(left + width(text));
585    format!("{}{text}{}", " ".repeat(left), " ".repeat(right))
586}
587
588/// The rows of a box, with the dots in the middle if some were left out.
589fn write_rows(
590    out: &mut String,
591    cells: &[Vec<String>],
592    sizes: &[usize],
593    right: &[bool],
594    vertical: &str,
595) {
596    let dots = shown(cells.len());
597    for (at, values) in cells.iter().enumerate() {
598        if let Some((head, tail)) = dots {
599            if at == head {
600                let parts = dot_row(cells, sizes, right, head, tail);
601                for _ in 0..ELIDED {
602                    let _ = writeln!(out, "{}", row(&parts, vertical));
603                }
604            }
605            if at >= head && at < cells.len() - tail {
606                continue;
607            }
608        }
609        let parts: Vec<String> = values
610            .iter()
611            .zip(sizes)
612            .zip(right)
613            .map(|((value, size), right)| pad(value, *size, *right))
614            .collect();
615        let _ = writeln!(out, "{}", row(&parts, vertical));
616    }
617}
618
619/// One cell per column of the row of dots that stands in for the rows a box left out.
620///
621/// The dot sits where the middle of a value in the column sat, measured from whichever edge that
622/// column is aligned against, and the value it lines up with is the shorter of the two either side
623/// of the gap. Only those two, not the shortest of everything printed, so a column can have one
624/// short value at the top and still have its dots way out to the right. So a column of small
625/// integers has its dots hard against the right edge and a column of `true` and `false` has them one
626/// in from the left, which looks like an accident and is what the duckdb binary prints. Centring
627/// them in the column is tidier and is a difference on every result over forty three rows.
628fn dot_row(
629    cells: &[Vec<String>],
630    sizes: &[usize],
631    right: &[bool],
632    head: usize,
633    tail: usize,
634) -> Vec<String> {
635    let above = cells.get(head.wrapping_sub(1));
636    let below = cells.get(cells.len() - tail);
637    sizes
638        .iter()
639        .zip(right)
640        .enumerate()
641        .map(|(column, (size, right))| {
642            let edge = |row: Option<&Vec<String>>| {
643                row.and_then(|values| values.get(column)).map_or(usize::MAX, |value| width(value))
644            };
645            let shortest = edge(above).min(edge(below));
646            let inset = (shortest.saturating_sub(1) / 2).min(size.saturating_sub(1));
647            let before = if *right { size.saturating_sub(1 + inset) } else { inset };
648            pad(&format!("{}·", " ".repeat(before)), *size, false)
649        })
650        .collect()
651}
652
653/// `.mode box` and `.mode table`, which are the same table without the types and without the count.
654fn boxed(result: &QueryResult, cells: &[Vec<String>], glyphs: Glyphs) -> String {
655    let sizes = widths(result, cells, false);
656    // Left for every column, numbers included. Only duckbox right aligns numbers, which reads oddly
657    // until you notice that box and table are the modes DuckDB inherited from SQLite and duckbox is
658    // the one it wrote.
659    let right = vec![false; result.width()];
660    let mut out = String::new();
661    let _ = writeln!(out, "{}", rule(&sizes, &glyphs.top));
662    let heads: Vec<String> =
663        result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
664    let _ = writeln!(out, "{}", row(&heads, glyphs.vertical));
665    let _ = writeln!(out, "{}", rule(&sizes, &glyphs.middle));
666    write_rows(&mut out, cells, &sizes, &right, glyphs.vertical);
667    let _ = writeln!(out, "{}", rule(&sizes, &glyphs.bottom));
668    out
669}
670
671/// `.mode markdown`, where the alignment lives in the rule rather than in the padding.
672fn markdown(result: &QueryResult, cells: &[Vec<String>]) -> String {
673    let sizes = widths(result, cells, false);
674    // A numeric column says it is right aligned in the rule and is still padded on the right in the
675    // cells, which is markdown's whole trick: the renderer downstream does the aligning.
676    let numeric: Vec<bool> = result.types().iter().map(LogicalType::is_numeric).collect();
677    let right = vec![false; result.width()];
678    let mut out = String::new();
679    let heads: Vec<String> =
680        result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
681    let _ = writeln!(out, "{}", row(&heads, "|"));
682    let rules: Vec<String> = sizes
683        .iter()
684        .zip(&numeric)
685        .map(
686            |(size, right)| {
687                if *right { format!("{}:", "-".repeat(size + 1)) } else { "-".repeat(size + 2) }
688            },
689        )
690        .collect();
691    let _ = writeln!(out, "|{}|", rules.join("|"));
692    let mut rows = String::new();
693    write_rows(&mut rows, cells, &sizes, &right, "|");
694    out.push_str(&rows);
695    out
696}
697
698/// `.mode line`, one `name = value` per line.
699///
700/// The names are right aligned in a field at least five wide, which is the width SQLite picked and
701/// DuckDB kept. A one letter column therefore gets four spaces in front of it and looks deliberate
702/// rather than broken, which is presumably the point.
703fn line(result: &QueryResult, cells: &[Vec<String>]) -> String {
704    let widest =
705        result.names().iter().map(|name| width(name)).max().unwrap_or(0).max(LINE_NAME_WIDTH);
706    let mut out = String::new();
707    for (at, values) in cells.iter().enumerate() {
708        if at > 0 {
709            out.push('\n');
710        }
711        for (name, value) in result.names().iter().zip(values) {
712            let _ = writeln!(out, "{} = {}", pad(name, widest, true), value);
713        }
714    }
715    out
716}
717
718/// `.mode list`, `.mode csv` and `.mode tabs`, which differ only in the separator and the quoting.
719fn separated(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
720    let quoted = settings.format == Format::Csv;
721    let mut out = String::new();
722    if settings.header {
723        let heads: Vec<String> = result
724            .names()
725            .iter()
726            .map(|name| if quoted { csv(name, settings) } else { name.clone() })
727            .collect();
728        out.push_str(&heads.join(&settings.separator));
729        out.push_str(&settings.newline);
730    }
731    for values in cells {
732        let parts: Vec<String> = values
733            .iter()
734            .map(|value| if quoted { csv(value, settings) } else { value.clone() })
735            .collect();
736        out.push_str(&parts.join(&settings.separator));
737        out.push_str(&settings.newline);
738    }
739    out
740}
741
742/// One CSV field, quoted when the shell would quote it.
743///
744/// Not RFC 4180, which asks for a quote around a field holding the separator, a quote or a line
745/// break and nothing else. The shell quotes a good deal more than that, and it has to be matched
746/// rather than improved on, because a diff of two CSV files is a byte comparison and a field that
747/// is correct under the standard and different from the reference is still a difference.
748///
749/// What it quotes on top of the separator is in [`AWKWARD`]. The one that matters is the top half
750/// of the byte range, which means every non-ASCII value in the output comes out quoted. Thirty two
751/// of the forty three ClickBench queries return Russian text and every one of them differs from the
752/// reference without this.
753fn csv(text: &str, settings: &Settings) -> String {
754    let awkward =
755        text.contains(&settings.separator) || text.bytes().any(|byte| AWKWARD[byte as usize]);
756    if awkward { format!("\"{}\"", text.replace('"', "\"\"")) } else { text.to_string() }
757}
758
759/// The bytes that put quotes around a CSV field on their own, whatever the separator is.
760///
761/// Every byte under a space, both quote characters, delete, and the whole top half. This is the
762/// table the shell carries, copied because the rule is a table and writing it as a condition is
763/// how the delete and the apostrophe get left out.
764static AWKWARD: [bool; 256] = {
765    let mut table = [false; 256];
766    let mut byte = 0;
767    while byte < 256 {
768        table[byte] = byte < 0x20 || byte == 0x7f || byte >= 0x80;
769        byte += 1;
770    }
771    table[b'"' as usize] = true;
772    table[b'\'' as usize] = true;
773    table
774};
775
776/// `.mode json` and `.mode jsonlines`.
777fn json(result: &QueryResult, settings: &Settings, array: bool) -> String {
778    let mut out = String::new();
779    // row at a time: a printed row is a row, and the thing being fed is a terminal or a pipe, so
780    // the cost of the loop is nowhere near the cost of the bytes leaving the process.
781    for row in 0..result.len() {
782        let parts: Vec<String> = (0..result.width())
783            .map(|column| {
784                format!(
785                    "{}:{}",
786                    json_string(&result.names()[column]),
787                    json_value(&result.value_at(row, column))
788                )
789            })
790            .collect();
791        let object = format!("{{{}}}", parts.join(","));
792        if array {
793            if row == 0 {
794                out.push('[');
795            }
796            out.push_str(&object);
797            if row + 1 < result.len() {
798                out.push_str(",\n");
799            } else {
800                out.push_str("]\n");
801            }
802        } else {
803            let _ = writeln!(out, "{object}");
804        }
805    }
806    if array && result.is_empty() {
807        out.push_str("[]\n");
808    }
809    let _ = settings;
810    out
811}
812
813/// One JSON string, escaped.
814fn json_string(text: &str) -> String {
815    let mut out = String::with_capacity(text.len() + 2);
816    out.push('"');
817    for character in text.chars() {
818        match character {
819            '"' => out.push_str("\\\""),
820            '\\' => out.push_str("\\\\"),
821            '\n' => out.push_str("\\n"),
822            '\r' => out.push_str("\\r"),
823            '\t' => out.push_str("\\t"),
824            other if (other as u32) < 0x20 => {
825                let _ = write!(out, "\\u{:04x}", other as u32);
826            }
827            other => out.push(other),
828        }
829    }
830    out.push('"');
831    out
832}
833
834/// One JSON value, which is a number for a number and a string for everything that is not one.
835fn json_value(value: &Value) -> String {
836    match value {
837        Value::Null => "null".to_string(),
838        Value::Boolean(flag) => flag.to_string(),
839        Value::List { values, .. } => {
840            let parts: Vec<String> = values.iter().map(json_value).collect();
841            format!("[{}]", parts.join(","))
842        }
843        Value::Struct(fields) => {
844            let parts: Vec<String> = fields
845                .iter()
846                .map(|(name, value)| format!("{}:{}", json_string(name), json_value(value)))
847                .collect();
848            format!("{{{}}}", parts.join(","))
849        }
850        other if is_number(other) => other.to_string(),
851        other => json_string(&other.to_string()),
852    }
853}
854
855/// Whether a value prints as a JSON number rather than as a JSON string.
856fn is_number(value: &Value) -> bool {
857    matches!(
858        value,
859        Value::TinyInt(_)
860            | Value::SmallInt(_)
861            | Value::Integer(_)
862            | Value::BigInt(_)
863            | Value::HugeInt(_)
864            | Value::UTinyInt(_)
865            | Value::USmallInt(_)
866            | Value::UInteger(_)
867            | Value::UBigInt(_)
868            | Value::UHugeInt(_)
869            | Value::Float(_)
870            | Value::Double(_)
871            | Value::Decimal { .. }
872    )
873}
874
875/// `.mode quote`, which is the spelling a value has inside a SQL statement.
876fn quote(result: &QueryResult, settings: &Settings) -> String {
877    let mut out = String::new();
878    if settings.header {
879        let heads: Vec<String> =
880            result.names().iter().map(|name| format!("'{}'", name.replace('\'', "''"))).collect();
881        out.push_str(&heads.join(&settings.separator));
882        out.push_str(&settings.newline);
883    }
884    // row at a time: the output is one quoted row per line, so there is nothing to batch.
885    for row in 0..result.len() {
886        let parts: Vec<String> =
887            (0..result.width()).map(|column| sql_literal(&result.value_at(row, column))).collect();
888        out.push_str(&parts.join(&settings.separator));
889        out.push_str(&settings.newline);
890    }
891    out
892}
893
894/// `.mode insert`, one statement per row.
895fn insert(result: &QueryResult, settings: &Settings) -> String {
896    let mut out = String::new();
897    let columns = result.names().join(",");
898    // row at a time: the output is one INSERT statement per row, which is the shape of the mode.
899    for row in 0..result.len() {
900        let parts: Vec<String> =
901            (0..result.width()).map(|column| sql_literal(&result.value_at(row, column))).collect();
902        let _ = writeln!(
903            out,
904            "INSERT INTO \"{}\"({}) VALUES({});",
905            settings.table,
906            columns,
907            parts.join(",")
908        );
909    }
910    out
911}
912
913/// A value as it would be written in SQL.
914fn sql_literal(value: &Value) -> String {
915    match value {
916        Value::Null => "NULL".to_string(),
917        other if is_number(other) => other.to_string(),
918        Value::Boolean(flag) => flag.to_string(),
919        other => format!("'{}'", other.to_string().replace('\'', "''")),
920    }
921}
922
923/// `.mode html`, the rows without the table around them, which is what DuckDB emits.
924fn html(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
925    let mut out = String::new();
926    if settings.header {
927        out.push_str("<tr>");
928        for name in result.names() {
929            let _ = writeln!(out, "<th>{}</th>", escape(name));
930        }
931        out.push_str("</tr>\n");
932    }
933    for values in cells {
934        out.push_str("<tr>");
935        for value in values {
936            let _ = writeln!(out, "<td>{}</td>", escape(value));
937        }
938        out.push_str("</tr>\n");
939    }
940    out
941}
942
943/// The four characters that cannot appear raw in HTML text.
944fn escape(text: &str) -> String {
945    text.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")
946}
947
948/// `.mode ascii`, every value on its own line with no decoration.
949fn ascii(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
950    let mut out = String::new();
951    if settings.header {
952        for name in result.names() {
953            let _ = writeln!(out, "{name}");
954        }
955    }
956    for values in cells {
957        for value in values {
958            let _ = writeln!(out, "{value}");
959        }
960    }
961    let _ = settings;
962    out
963}
964
965/// `.mode column`, space padded under a dashed rule.
966fn column(result: &QueryResult, cells: &[Vec<String>]) -> String {
967    let sizes = widths(result, cells, false);
968    let mut out = String::new();
969    let heads: Vec<String> =
970        result.names().iter().zip(&sizes).map(|(name, size)| pad(name, *size, false)).collect();
971    let _ = writeln!(out, "{}", heads.join("  "));
972    let rules: Vec<String> = sizes.iter().map(|size| "-".repeat(*size)).collect();
973    let _ = writeln!(out, "{}", rules.join("  "));
974    for values in cells {
975        let parts: Vec<String> =
976            values.iter().zip(&sizes).map(|(value, size)| pad(value, *size, false)).collect();
977        let _ = writeln!(out, "{}", parts.join("  "));
978    }
979    out
980}