Skip to main content

systemprompt_database/services/
display.rs

1//! CLI display traits for printing query results, table descriptors, and
2//! database info to stdout.
3//!
4//! A display write is best-effort: the command's outcome does not depend on
5//! whether the terminal accepted the bytes, so a failed write is reported
6//! through `tracing` via [`report_write_failure`] and the caller continues.
7//! A closed downstream pipe is not reported at all — with SIGPIPE ignored
8//! every later write would fail the same way and flood the log for a reader
9//! that has already gone away. Every stdio display sink in the infra layer
10//! routes its failures through that one helper.
11//!
12//! Copyright (c) systemprompt.io — Business Source License 1.1.
13//! See <https://systemprompt.io> for licensing details.
14
15use std::io::Write;
16
17use crate::models::{ColumnInfo, DatabaseInfo, QueryResult, TableInfo};
18
19pub trait DatabaseCliDisplay {
20    fn display_with_cli(&self);
21}
22
23pub fn report_write_failure(sink: &'static str, error: &std::io::Error) {
24    if error.kind() == std::io::ErrorKind::BrokenPipe {
25        return;
26    }
27    tracing::warn!(sink, error = %error, "Display sink write failed");
28}
29
30fn stdout_writeln(args: std::fmt::Arguments<'_>) {
31    if let Err(error) = writeln!(std::io::stdout(), "{args}") {
32        report_write_failure("stdout", &error);
33    }
34}
35
36impl DatabaseCliDisplay for Vec<TableInfo> {
37    fn display_with_cli(&self) {
38        if self.is_empty() {
39            stdout_writeln(format_args!("No tables found"));
40        } else {
41            stdout_writeln(format_args!("Tables:"));
42            for table in self {
43                stdout_writeln(format_args!("  {} (rows: {})", table.name, table.row_count));
44            }
45        }
46    }
47}
48
49impl DatabaseCliDisplay for (Vec<ColumnInfo>, i64) {
50    fn display_with_cli(&self) {
51        let (columns, _) = self;
52        stdout_writeln(format_args!("Columns:"));
53        for col in columns {
54            let default_display = col
55                .default
56                .as_deref()
57                .map_or_else(String::new, |d| format!("DEFAULT {d}"));
58
59            stdout_writeln(format_args!(
60                "  {} {} {} {} {}",
61                col.name,
62                col.data_type,
63                if col.nullable { "NULL" } else { "NOT NULL" },
64                if col.primary_key { "PK" } else { "" },
65                default_display
66            ));
67        }
68    }
69}
70
71impl DatabaseCliDisplay for DatabaseInfo {
72    fn display_with_cli(&self) {
73        stdout_writeln(format_args!("Database Info:"));
74        stdout_writeln(format_args!("  Path: {}", self.path));
75        stdout_writeln(format_args!("  Version: {}", self.version));
76        stdout_writeln(format_args!("  Tables: {}", self.tables.len()));
77    }
78}
79
80impl DatabaseCliDisplay for QueryResult {
81    fn display_with_cli(&self) {
82        if self.columns.is_empty() {
83            stdout_writeln(format_args!("No data returned"));
84            return;
85        }
86
87        stdout_writeln(format_args!("{}", self.columns.join(" | ")));
88        stdout_writeln(format_args!("{}", "-".repeat(80)));
89
90        for row in &self.rows {
91            let values: Vec<String> = self
92                .columns
93                .iter()
94                .map(|col| {
95                    row.get(col).map_or_else(
96                        || "NULL".to_owned(),
97                        |v| match v {
98                            serde_json::Value::String(s) => s.clone(),
99                            serde_json::Value::Null => "NULL".to_owned(),
100                            serde_json::Value::Bool(_)
101                            | serde_json::Value::Number(_)
102                            | serde_json::Value::Array(_)
103                            | serde_json::Value::Object(_) => v.to_string(),
104                        },
105                    )
106                })
107                .collect();
108            stdout_writeln(format_args!("{}", values.join(" | ")));
109        }
110
111        stdout_writeln(format_args!(
112            "\n{} rows returned in {}ms",
113            self.row_count, self.execution_time_ms
114        ));
115    }
116}