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 footer = footer_text(result, cells.len());
388    // The table cannot be narrower than the count printed under it, which is the only reason a
389    // one column table of `int32` comes out eight wide rather than seven.
390    if let Some(first) = footer.first() {
391        let total: usize = sizes.iter().map(|size| size + 2).sum::<usize>() + sizes.len() - 1;
392        let needed = width(first) + 2;
393        if let Some(last) = sizes.last_mut() {
394            *last += needed.saturating_sub(total);
395        }
396    }
397    let right: Vec<bool> = result.types().iter().map(LogicalType::is_numeric).collect();
398    let mut out = String::new();
399    let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.top));
400    let heads: Vec<String> =
401        result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
402    let _ = writeln!(out, "{}", row(&heads, BOX_GLYPHS.vertical));
403    let types: Vec<String> =
404        result.types().iter().zip(&sizes).map(|(ty, size)| centre(&type_name(ty), *size)).collect();
405    let _ = writeln!(out, "{}", row(&types, BOX_GLYPHS.vertical));
406    if !cells.is_empty() {
407        let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.middle));
408        write_rows(&mut out, cells, &sizes, &right, BOX_GLYPHS.vertical);
409    }
410    let _ = writeln!(out, "{}", rule(&sizes, &BOX_GLYPHS.bottom));
411    let total: usize = sizes.iter().map(|size| size + 2).sum::<usize>() + sizes.len() - 1;
412    for text in footer {
413        let left = (total.saturating_sub(width(&text))) / 2 + 1;
414        let _ = writeln!(out, "{}{}", " ".repeat(left), text);
415    }
416    out
417}
418
419/// The lines printed under a `duckbox` table, which is nothing at all for most results.
420///
421/// A count appears when the result is empty, because an empty box says nothing on its own, and
422/// when rows were left out, because a table that is not all of the answer has to say so. A result
423/// of three rows prints three rows and no commentary.
424fn footer_text(result: &QueryResult, rows: usize) -> Vec<String> {
425    if rows == 0 {
426        return vec!["0 rows".to_string()];
427    }
428    let Some((head, tail)) = shown(result.len()) else {
429        return Vec::new();
430    };
431    let counted = format!("{} rows", result.len());
432    let elided = format!("({} shown)", head + tail);
433    vec![counted, elided]
434}
435
436/// The rows of a box, with the dots in the middle if some were left out.
437fn write_rows(
438    out: &mut String,
439    cells: &[Vec<String>],
440    sizes: &[usize],
441    right: &[bool],
442    vertical: &str,
443) {
444    let dots = shown(cells.len());
445    for (at, values) in cells.iter().enumerate() {
446        if let Some((head, tail)) = dots {
447            if at == head {
448                for _ in 0..ELIDED {
449                    let parts: Vec<String> = sizes.iter().map(|size| centre("·", *size)).collect();
450                    let _ = writeln!(out, "{}", row(&parts, vertical));
451                }
452            }
453            if at >= head && at < cells.len() - tail {
454                continue;
455            }
456        }
457        let parts: Vec<String> = values
458            .iter()
459            .zip(sizes)
460            .zip(right)
461            .map(|((value, size), right)| pad(value, *size, *right))
462            .collect();
463        let _ = writeln!(out, "{}", row(&parts, vertical));
464    }
465}
466
467/// `.mode box` and `.mode table`, which are the same table without the types and without the count.
468fn boxed(result: &QueryResult, cells: &[Vec<String>], glyphs: Glyphs) -> String {
469    let sizes = widths(result, cells, false);
470    // Left for every column, numbers included. Only duckbox right aligns numbers, which reads oddly
471    // until you notice that box and table are the modes DuckDB inherited from SQLite and duckbox is
472    // the one it wrote.
473    let right = vec![false; result.width()];
474    let mut out = String::new();
475    let _ = writeln!(out, "{}", rule(&sizes, &glyphs.top));
476    let heads: Vec<String> =
477        result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
478    let _ = writeln!(out, "{}", row(&heads, glyphs.vertical));
479    let _ = writeln!(out, "{}", rule(&sizes, &glyphs.middle));
480    write_rows(&mut out, cells, &sizes, &right, glyphs.vertical);
481    let _ = writeln!(out, "{}", rule(&sizes, &glyphs.bottom));
482    out
483}
484
485/// `.mode markdown`, where the alignment lives in the rule rather than in the padding.
486fn markdown(result: &QueryResult, cells: &[Vec<String>]) -> String {
487    let sizes = widths(result, cells, false);
488    // A numeric column says it is right aligned in the rule and is still padded on the right in the
489    // cells, which is markdown's whole trick: the renderer downstream does the aligning.
490    let numeric: Vec<bool> = result.types().iter().map(LogicalType::is_numeric).collect();
491    let right = vec![false; result.width()];
492    let mut out = String::new();
493    let heads: Vec<String> =
494        result.names().iter().zip(&sizes).map(|(name, size)| centre(name, *size)).collect();
495    let _ = writeln!(out, "{}", row(&heads, "|"));
496    let rules: Vec<String> = sizes
497        .iter()
498        .zip(&numeric)
499        .map(
500            |(size, right)| {
501                if *right { format!("{}:", "-".repeat(size + 1)) } else { "-".repeat(size + 2) }
502            },
503        )
504        .collect();
505    let _ = writeln!(out, "|{}|", rules.join("|"));
506    let mut rows = String::new();
507    write_rows(&mut rows, cells, &sizes, &right, "|");
508    out.push_str(&rows);
509    out
510}
511
512/// `.mode line`, one `name = value` per line.
513///
514/// The names are right aligned in a field at least five wide, which is the width SQLite picked and
515/// DuckDB kept. A one letter column therefore gets four spaces in front of it and looks deliberate
516/// rather than broken, which is presumably the point.
517fn line(result: &QueryResult, cells: &[Vec<String>]) -> String {
518    let widest =
519        result.names().iter().map(|name| width(name)).max().unwrap_or(0).max(LINE_NAME_WIDTH);
520    let mut out = String::new();
521    for (at, values) in cells.iter().enumerate() {
522        if at > 0 {
523            out.push('\n');
524        }
525        for (name, value) in result.names().iter().zip(values) {
526            let _ = writeln!(out, "{} = {}", pad(name, widest, true), value);
527        }
528    }
529    out
530}
531
532/// `.mode list`, `.mode csv` and `.mode tabs`, which differ only in the separator and the quoting.
533fn separated(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
534    let quoted = settings.format == Format::Csv;
535    let mut out = String::new();
536    if settings.header {
537        let heads: Vec<String> = result
538            .names()
539            .iter()
540            .map(|name| if quoted { csv(name, settings) } else { name.clone() })
541            .collect();
542        out.push_str(&heads.join(&settings.separator));
543        out.push_str(&settings.newline);
544    }
545    for values in cells {
546        let parts: Vec<String> = values
547            .iter()
548            .map(|value| if quoted { csv(value, settings) } else { value.clone() })
549            .collect();
550        out.push_str(&parts.join(&settings.separator));
551        out.push_str(&settings.newline);
552    }
553    out
554}
555
556/// One CSV field, quoted when RFC 4180 says it has to be.
557fn csv(text: &str, settings: &Settings) -> String {
558    let awkward = text.contains(&settings.separator)
559        || text.contains('"')
560        || text.contains('\n')
561        || text.contains('\r');
562    if awkward { format!("\"{}\"", text.replace('"', "\"\"")) } else { text.to_string() }
563}
564
565/// `.mode json` and `.mode jsonlines`.
566fn json(result: &QueryResult, settings: &Settings, array: bool) -> String {
567    let mut out = String::new();
568    // row at a time: a printed row is a row, and the thing being fed is a terminal or a pipe, so
569    // the cost of the loop is nowhere near the cost of the bytes leaving the process.
570    for row in 0..result.len() {
571        let parts: Vec<String> = (0..result.width())
572            .map(|column| {
573                format!(
574                    "{}:{}",
575                    json_string(&result.names()[column]),
576                    json_value(&result.value_at(row, column))
577                )
578            })
579            .collect();
580        let object = format!("{{{}}}", parts.join(","));
581        if array {
582            if row == 0 {
583                out.push('[');
584            }
585            out.push_str(&object);
586            if row + 1 < result.len() {
587                out.push_str(",\n");
588            } else {
589                out.push_str("]\n");
590            }
591        } else {
592            let _ = writeln!(out, "{object}");
593        }
594    }
595    if array && result.is_empty() {
596        out.push_str("[]\n");
597    }
598    let _ = settings;
599    out
600}
601
602/// One JSON string, escaped.
603fn json_string(text: &str) -> String {
604    let mut out = String::with_capacity(text.len() + 2);
605    out.push('"');
606    for character in text.chars() {
607        match character {
608            '"' => out.push_str("\\\""),
609            '\\' => out.push_str("\\\\"),
610            '\n' => out.push_str("\\n"),
611            '\r' => out.push_str("\\r"),
612            '\t' => out.push_str("\\t"),
613            other if (other as u32) < 0x20 => {
614                let _ = write!(out, "\\u{:04x}", other as u32);
615            }
616            other => out.push(other),
617        }
618    }
619    out.push('"');
620    out
621}
622
623/// One JSON value, which is a number for a number and a string for everything that is not one.
624fn json_value(value: &Value) -> String {
625    match value {
626        Value::Null => "null".to_string(),
627        Value::Boolean(flag) => flag.to_string(),
628        Value::List { values, .. } => {
629            let parts: Vec<String> = values.iter().map(json_value).collect();
630            format!("[{}]", parts.join(","))
631        }
632        Value::Struct(fields) => {
633            let parts: Vec<String> = fields
634                .iter()
635                .map(|(name, value)| format!("{}:{}", json_string(name), json_value(value)))
636                .collect();
637            format!("{{{}}}", parts.join(","))
638        }
639        other if is_number(other) => other.to_string(),
640        other => json_string(&other.to_string()),
641    }
642}
643
644/// Whether a value prints as a JSON number rather than as a JSON string.
645fn is_number(value: &Value) -> bool {
646    matches!(
647        value,
648        Value::TinyInt(_)
649            | Value::SmallInt(_)
650            | Value::Integer(_)
651            | Value::BigInt(_)
652            | Value::HugeInt(_)
653            | Value::UTinyInt(_)
654            | Value::USmallInt(_)
655            | Value::UInteger(_)
656            | Value::UBigInt(_)
657            | Value::UHugeInt(_)
658            | Value::Float(_)
659            | Value::Double(_)
660            | Value::Decimal { .. }
661    )
662}
663
664/// `.mode quote`, which is the spelling a value has inside a SQL statement.
665fn quote(result: &QueryResult, settings: &Settings) -> String {
666    let mut out = String::new();
667    if settings.header {
668        let heads: Vec<String> =
669            result.names().iter().map(|name| format!("'{}'", name.replace('\'', "''"))).collect();
670        out.push_str(&heads.join(&settings.separator));
671        out.push_str(&settings.newline);
672    }
673    // row at a time: the output is one quoted row per line, so there is nothing to batch.
674    for row in 0..result.len() {
675        let parts: Vec<String> =
676            (0..result.width()).map(|column| sql_literal(&result.value_at(row, column))).collect();
677        out.push_str(&parts.join(&settings.separator));
678        out.push_str(&settings.newline);
679    }
680    out
681}
682
683/// `.mode insert`, one statement per row.
684fn insert(result: &QueryResult, settings: &Settings) -> String {
685    let mut out = String::new();
686    let columns = result.names().join(",");
687    // row at a time: the output is one INSERT statement per row, which is the shape of the mode.
688    for row in 0..result.len() {
689        let parts: Vec<String> =
690            (0..result.width()).map(|column| sql_literal(&result.value_at(row, column))).collect();
691        let _ = writeln!(
692            out,
693            "INSERT INTO \"{}\"({}) VALUES({});",
694            settings.table,
695            columns,
696            parts.join(",")
697        );
698    }
699    out
700}
701
702/// A value as it would be written in SQL.
703fn sql_literal(value: &Value) -> String {
704    match value {
705        Value::Null => "NULL".to_string(),
706        other if is_number(other) => other.to_string(),
707        Value::Boolean(flag) => flag.to_string(),
708        other => format!("'{}'", other.to_string().replace('\'', "''")),
709    }
710}
711
712/// `.mode html`, the rows without the table around them, which is what DuckDB emits.
713fn html(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
714    let mut out = String::new();
715    if settings.header {
716        out.push_str("<tr>");
717        for name in result.names() {
718            let _ = writeln!(out, "<th>{}</th>", escape(name));
719        }
720        out.push_str("</tr>\n");
721    }
722    for values in cells {
723        out.push_str("<tr>");
724        for value in values {
725            let _ = writeln!(out, "<td>{}</td>", escape(value));
726        }
727        out.push_str("</tr>\n");
728    }
729    out
730}
731
732/// The four characters that cannot appear raw in HTML text.
733fn escape(text: &str) -> String {
734    text.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")
735}
736
737/// `.mode ascii`, every value on its own line with no decoration.
738fn ascii(result: &QueryResult, cells: &[Vec<String>], settings: &Settings) -> String {
739    let mut out = String::new();
740    if settings.header {
741        for name in result.names() {
742            let _ = writeln!(out, "{name}");
743        }
744    }
745    for values in cells {
746        for value in values {
747            let _ = writeln!(out, "{value}");
748        }
749    }
750    let _ = settings;
751    out
752}
753
754/// `.mode column`, space padded under a dashed rule.
755fn column(result: &QueryResult, cells: &[Vec<String>]) -> String {
756    let sizes = widths(result, cells, false);
757    let mut out = String::new();
758    let heads: Vec<String> =
759        result.names().iter().zip(&sizes).map(|(name, size)| pad(name, *size, false)).collect();
760    let _ = writeln!(out, "{}", heads.join("  "));
761    let rules: Vec<String> = sizes.iter().map(|size| "-".repeat(*size)).collect();
762    let _ = writeln!(out, "{}", rules.join("  "));
763    for values in cells {
764        let parts: Vec<String> =
765            values.iter().zip(&sizes).map(|(value, size)| pad(value, *size, false)).collect();
766        let _ = writeln!(out, "{}", parts.join("  "));
767    }
768    out
769}