1use std::fmt::Write as _;
12
13use rudb::QueryResult;
14use rudb_common::{LogicalType, Value};
15
16const MAX_ROWS: usize = 40;
18
19const ELIDED: usize = 3;
21
22const LINE_NAME_WIDTH: usize = 5;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum Format {
28 #[default]
30 DuckBox,
31 Box,
33 Table,
35 Markdown,
37 Line,
39 List,
41 Csv,
43 Tsv,
45 Json,
47 JsonLines,
49 Quote,
51 Insert,
53 Html,
56 Ascii,
58 Column,
60 Trash,
62}
63
64impl Format {
65 pub fn from_name(name: &str) -> Option<Self> {
67 Some(match name {
68 "duckbox" => Self::DuckBox,
69 "box" => Self::Box,
70 "table" => Self::Table,
71 "markdown" => Self::Markdown,
72 "line" | "lines" => Self::Line,
73 "list" => Self::List,
74 "csv" => Self::Csv,
75 "tabs" | "tsv" => Self::Tsv,
76 "json" => Self::Json,
77 "jsonlines" | "ndjson" => Self::JsonLines,
78 "quote" => Self::Quote,
79 "insert" => Self::Insert,
80 "html" => Self::Html,
81 "ascii" => Self::Ascii,
82 "column" => Self::Column,
83 "trash" => Self::Trash,
84 _ => return None,
85 })
86 }
87
88 pub fn name(self) -> &'static str {
90 match self {
91 Self::DuckBox => "duckbox",
92 Self::Box => "box",
93 Self::Table => "table",
94 Self::Markdown => "markdown",
95 Self::Line => "line",
96 Self::List => "list",
97 Self::Csv => "csv",
98 Self::Tsv => "tabs",
99 Self::Json => "json",
100 Self::JsonLines => "jsonlines",
101 Self::Quote => "quote",
102 Self::Insert => "insert",
103 Self::Html => "html",
104 Self::Ascii => "ascii",
105 Self::Column => "column",
106 Self::Trash => "trash",
107 }
108 }
109
110 fn separator(self) -> &'static str {
112 match self {
113 Self::Csv | Self::Quote => ",",
114 Self::Tsv => "\t",
115 Self::Ascii => "\u{1f}",
116 _ => "|",
117 }
118 }
119
120 fn newline(self) -> &'static str {
125 match self {
126 Self::Csv => "\r\n",
127 Self::Ascii => "\u{1e}",
128 _ => "\n",
129 }
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct Settings {
136 pub format: Format,
138 pub header: bool,
140 pub separator: String,
142 pub newline: String,
144 pub nullvalue: String,
146 pub table: String,
148}
149
150impl Default for Settings {
151 fn default() -> Self {
152 Self {
153 format: Format::DuckBox,
154 header: true,
155 separator: "|".to_string(),
156 newline: "\n".to_string(),
157 nullvalue: "NULL".to_string(),
158 table: "table".to_string(),
159 }
160 }
161}
162
163impl Settings {
164 pub fn set_format(&mut self, format: Format) {
172 self.format = format;
173 self.separator = format.separator().to_string();
174 self.newline = format.newline().to_string();
175 }
176}
177
178pub fn escaped(text: &str) -> String {
180 let mut out = String::new();
181 for character in text.chars() {
182 match character {
183 '\n' => out.push_str("\\n"),
184 '\r' => out.push_str("\\r"),
185 '\t' => out.push_str("\\t"),
186 '\\' => out.push_str("\\\\"),
187 other if (other as u32) < 0x20 => {
188 let _ = write!(out, "\\{:03o}", other as u32);
189 }
190 other => out.push(other),
191 }
192 }
193 out
194}
195
196pub fn render(result: &QueryResult, settings: &Settings) -> String {
198 if result.width() == 0 {
199 return String::new();
200 }
201 let cells = cells(result, settings);
202 match settings.format {
203 Format::DuckBox => duckbox(result, &cells),
204 Format::Box => boxed(result, &cells, BOX_GLYPHS),
205 Format::Table => boxed(result, &cells, TABLE_GLYPHS),
206 Format::Markdown => markdown(result, &cells),
207 Format::Line => line(result, &cells),
208 Format::List | Format::Csv | Format::Tsv => separated(result, &cells, settings),
209 Format::Json => json(result, settings, true),
210 Format::JsonLines => json(result, settings, false),
211 Format::Quote => quote(result, settings),
212 Format::Insert => insert(result, settings),
213 Format::Html => html(result, &cells, settings),
214 Format::Ascii => ascii(result, &cells, settings),
215 Format::Column => column(result, &cells),
216 Format::Trash => String::new(),
217 }
218}
219
220fn cells(result: &QueryResult, settings: &Settings) -> Vec<Vec<String>> {
222 (0..result.len())
223 .map(|row| {
224 (0..result.width())
225 .map(|column| cell(&result.value_at(row, column), settings))
226 .collect()
227 })
228 .collect()
229}
230
231fn cell(value: &Value, settings: &Settings) -> String {
233 match value {
234 Value::Null => settings.nullvalue.clone(),
235 other => other.to_string(),
236 }
237}
238
239fn type_name(ty: &LogicalType) -> String {
249 match ty {
250 LogicalType::Null => "\"NULL\"".to_string(),
251 LogicalType::Boolean => "boolean".to_string(),
252 LogicalType::TinyInt => "int8".to_string(),
253 LogicalType::SmallInt => "int16".to_string(),
254 LogicalType::Integer => "int32".to_string(),
255 LogicalType::BigInt => "int64".to_string(),
256 LogicalType::HugeInt => "int128".to_string(),
257 LogicalType::UTinyInt => "uint8".to_string(),
258 LogicalType::USmallInt => "uint16".to_string(),
259 LogicalType::UInteger => "uint32".to_string(),
260 LogicalType::UBigInt => "uint64".to_string(),
261 LogicalType::UHugeInt => "uint128".to_string(),
262 LogicalType::Float => "float".to_string(),
263 LogicalType::Double => "double".to_string(),
264 LogicalType::Decimal { width, scale } => format!("decimal({width},{scale})"),
265 LogicalType::Varchar => "varchar".to_string(),
266 LogicalType::Blob => "blob".to_string(),
267 LogicalType::Bit => "bit".to_string(),
268 LogicalType::Uuid => "uuid".to_string(),
269 LogicalType::Date => "date".to_string(),
270 LogicalType::Time => "time".to_string(),
271 LogicalType::TimeTz => "time with time zone".to_string(),
272 LogicalType::Timestamp => "timestamp".to_string(),
273 LogicalType::TimestampS => "timestamp_s".to_string(),
274 LogicalType::TimestampMs => "timestamp_ms".to_string(),
275 LogicalType::TimestampNs => "timestamp_ns".to_string(),
276 LogicalType::TimestampTz => "timestamp with time zone".to_string(),
277 LogicalType::Interval => "interval".to_string(),
278 LogicalType::List(inner) | LogicalType::Array(inner, _) => {
279 format!("{}[]", type_name(inner))
280 }
281 LogicalType::Map(key, value) => format!("map({}, {})", type_name(key), type_name(value)),
282 LogicalType::Struct(fields) => {
283 let inner: Vec<String> =
284 fields.iter().map(|field| format!("{} {}", field.name, field.ty)).collect();
285 format!("struct({})", inner.join(", ")).to_lowercase()
286 }
287 LogicalType::Union(fields) => {
288 let inner: Vec<String> =
289 fields.iter().map(|field| format!("{} {}", field.name, field.ty)).collect();
290 format!("union({})", inner.join(", ")).to_lowercase()
291 }
292 other => other.to_string().to_lowercase(),
293 }
294}
295
296fn width(text: &str) -> usize {
303 text.chars().count()
304}
305
306fn shown(rows: usize) -> Option<(usize, usize)> {
311 if rows > MAX_ROWS + ELIDED { Some((MAX_ROWS / 2, MAX_ROWS / 2)) } else { None }
312}
313
314fn pad(text: &str, size: usize, right: bool) -> String {
316 let missing = size.saturating_sub(width(text));
317 if right {
318 format!("{}{}", " ".repeat(missing), text)
319 } else {
320 format!("{}{}", text, " ".repeat(missing))
321 }
322}
323
324fn centre(text: &str, size: usize) -> String {
326 let missing = size.saturating_sub(width(text));
327 let left = missing / 2;
328 format!("{}{}{}", " ".repeat(left), text, " ".repeat(missing - left))
329}
330
331fn widths(result: &QueryResult, cells: &[Vec<String>], types: bool) -> Vec<usize> {
333 (0..result.width())
334 .map(|column| {
335 let mut size = width(&result.names()[column]);
336 if types {
337 size = size.max(width(&type_name(&result.types()[column])));
338 }
339 for row in cells {
340 size = size.max(width(&row[column]));
341 }
342 size
343 })
344 .collect()
345}
346
347struct Glyphs {
349 top: [&'static str; 4],
350 middle: [&'static str; 4],
351 bottom: [&'static str; 4],
352 vertical: &'static str,
353}
354
355const BOX_GLYPHS: Glyphs = Glyphs {
356 top: ["┌", "─", "┬", "┐"],
357 middle: ["├", "─", "┼", "┤"],
358 bottom: ["└", "─", "┴", "┘"],
359 vertical: "│",
360};
361
362const TABLE_GLYPHS: Glyphs = Glyphs {
363 top: ["+", "-", "+", "+"],
364 middle: ["+", "-", "+", "+"],
365 bottom: ["+", "-", "+", "+"],
366 vertical: "|",
367};
368
369fn rule(widths: &[usize], glyphs: &[&str; 4]) -> String {
371 let parts: Vec<String> = widths.iter().map(|size| glyphs[1].repeat(size + 2)).collect();
372 format!("{}{}{}", glyphs[0], parts.join(glyphs[2]), glyphs[3])
373}
374
375fn row(parts: &[String], vertical: &str) -> String {
377 let mut out = String::from(vertical);
378 for part in parts {
379 let _ = write!(out, " {part} {vertical}");
380 }
381 out
382}
383
384fn duckbox(result: &QueryResult, cells: &[Vec<String>]) -> String {
386 let mut sizes = widths(result, cells, true);
387 let footer = footer_text(result, cells.len());
388 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
419fn 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
436fn 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
467fn boxed(result: &QueryResult, cells: &[Vec<String>], glyphs: Glyphs) -> String {
469 let sizes = widths(result, cells, false);
470 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
485fn markdown(result: &QueryResult, cells: &[Vec<String>]) -> String {
487 let sizes = widths(result, cells, false);
488 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
512fn 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
532fn 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
556fn 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
565fn json(result: &QueryResult, settings: &Settings, array: bool) -> String {
567 let mut out = String::new();
568 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
602fn 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
623fn 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
644fn 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
664fn 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 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
683fn insert(result: &QueryResult, settings: &Settings) -> String {
685 let mut out = String::new();
686 let columns = result.names().join(",");
687 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
702fn 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
712fn 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
732fn escape(text: &str) -> String {
734 text.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)
735}
736
737fn 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
754fn 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}