pretty_sqlite/
pretties.rs

1use crate::options::PrettyOptions;
2use crate::{Error, Result};
3use rusqlite::types::ValueRef;
4use rusqlite::{Connection, Params, Rows};
5use std::str::from_utf8;
6use std::sync::OnceLock;
7use tabled::settings::Style;
8
9/// Default PRETTY OPTIONS
10/// Note: Given the simplicy of the current PrettyOptions, this is probably not needed.
11static DEFAULT_PRETTY_OPTIONS: OnceLock<PrettyOptions> = OnceLock::new();
12
13pub fn print_table(conn: &Connection, table: &str) -> Result<()> {
14	let table_content = pretty_table(conn, table)?;
15	println!("{table_content}");
16
17	Ok(())
18}
19
20pub fn pretty_table(conn: &Connection, table: &str) -> Result<String> {
21	let pretty_options = DEFAULT_PRETTY_OPTIONS.get_or_init(PrettyOptions::default);
22
23	let mut content = format!(" TABLE: {table}\n");
24
25	let select_content = pretty_select_with_options(
26		conn,
27		&format!("SELECT * FROM {table} limit {}", pretty_options.rows_limit),
28		[],
29		pretty_options,
30	)?;
31
32	content.push_str(&select_content);
33
34	Ok(content)
35}
36
37pub fn print_select(conn: &Connection, sql: &str, params: impl Params) -> Result<()> {
38	let select_content = pretty_select(conn, sql, params)?;
39	println!("{select_content}");
40	Ok(())
41}
42
43pub fn pretty_select(conn: &Connection, sql: &str, params: impl Params) -> Result<String> {
44	let pretty_options = DEFAULT_PRETTY_OPTIONS.get_or_init(PrettyOptions::default);
45
46	let mut stmt = conn.prepare(sql)?;
47	let rows = stmt.query(params)?;
48
49	pretty_rows(rows, pretty_options)
50}
51
52pub fn pretty_select_with_options(
53	conn: &Connection,
54	sql: &str,
55	params: impl Params,
56	options: &PrettyOptions,
57) -> Result<String> {
58	let mut stmt = conn.prepare(sql)?;
59	let rows = stmt.query(params)?;
60
61	pretty_rows(rows, options)
62}
63
64pub fn print_rows(rows: Rows<'_>) -> Result<()> {
65	let pretty_options = DEFAULT_PRETTY_OPTIONS.get_or_init(PrettyOptions::default);
66	let rows_content = pretty_rows(rows, pretty_options)?;
67	println!("{rows_content}");
68	Ok(())
69}
70
71fn pretty_rows(mut rows: Rows<'_>, options: &PrettyOptions) -> Result<String> {
72	enum SubType {
73		Time,
74		None,
75	}
76
77	let stmt = rows.as_ref().ok_or(Error::CantPrintRowsHasNoStatement)?;
78	let names: Vec<String> = stmt.column_names().into_iter().map(|s| s.to_string()).collect();
79	let sub_types: Vec<SubType> = names
80		.iter()
81		.map(|n| {
82			if n.ends_with("time") {
83				SubType::Time
84			} else {
85				SubType::None
86			}
87		})
88		.collect();
89
90	let mut table_builder = tabled::builder::Builder::new();
91	table_builder.push_record(names.clone());
92
93	let mut count = 0;
94
95	while let Some(row) = rows.next()? {
96		count += 1;
97		if count > options.rows_limit {
98			break;
99		}
100		// -- Extract row cells
101		let mut cells: Vec<String> = Vec::new();
102		for (i, _k) in names.iter().enumerate() {
103			let v = row.get_ref(i)?;
104			let v = match v {
105				ValueRef::Null => "NULL".to_string(),
106				ValueRef::Integer(num) => match sub_types[i] {
107					SubType::Time => format!("{num}"), //epoch_us_to_rfc3339(num),
108					SubType::None => format!("{num}"),
109				},
110				ValueRef::Real(num) => format!("{num}"),
111				ValueRef::Text(bytes) => {
112					let txt = format!("\"{}\"", from_utf8(bytes).map_err(|_| Error::SQLiteTextCellIsNotUtf8)?);
113					truncate_string(txt, options)
114				}
115				ValueRef::Blob(blob) => format!("BLOB (length: {})", blob.len()),
116			};
117
118			cells.push(v);
119		}
120
121		// -- Add the row celles to the table builder
122		table_builder.push_record(cells);
123	}
124
125	let table_content = table_builder.build().with(Style::modern()).to_string();
126
127	Ok(table_content)
128}
129
130fn truncate_string(s: String, options: &PrettyOptions) -> String {
131	let Some(truncate_cell_max) = options.cell_truncate_max else {
132		return s;
133	};
134
135	let max_cell_len = truncate_cell_max as usize;
136
137	let char_indices: Vec<(usize, char)> = s.char_indices().collect();
138	if char_indices.len() > max_cell_len {
139		let first_part_len = max_cell_len / 6; // TODO: Need to use the Truncate data
140		let remaining_len = max_cell_len - first_part_len;
141		let first_part = char_indices[..first_part_len].iter().map(|&(_, c)| c).collect::<String>();
142		let second_part = char_indices[char_indices.len() - remaining_len..]
143			.iter()
144			.map(|&(_, c)| c)
145			.collect::<String>();
146		format!("{first_part}...{second_part}")
147	} else {
148		s.to_string()
149	}
150}