Skip to main content

reifydb_core/value/frame/
render.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::fmt::{self, Write};
5
6use reifydb_value::{
7	reifydb_assertions,
8	util::unicode::UnicodeWidthStr,
9	value::frame::{column::FrameColumn, frame::Frame},
10};
11
12pub struct FrameRenderer;
13
14impl FrameRenderer {
15	pub fn render_full(frame: &Frame) -> Result<String, fmt::Error> {
16		let mut output = String::new();
17		Self::render_full_to(frame, &mut output)?;
18		Ok(output)
19	}
20
21	pub fn render_without_row_numbers(frame: &Frame) -> Result<String, fmt::Error> {
22		let mut output = String::new();
23		Self::render_without_row_numbers_to(frame, &mut output)?;
24		Ok(output)
25	}
26
27	pub fn render_full_to(frame: &Frame, f: &mut dyn Write) -> fmt::Result {
28		Self::render_internal(frame, f, true)
29	}
30
31	pub fn render_without_row_numbers_to(frame: &Frame, f: &mut dyn Write) -> fmt::Result {
32		Self::render_internal(frame, f, false)
33	}
34
35	fn render_internal(frame: &Frame, f: &mut dyn Write, include_row_numbers: bool) -> fmt::Result {
36		let row_count = frame.first().map_or(0, |c| c.data.len());
37		let has_row_numbers = include_row_numbers && !frame.row_numbers.is_empty();
38		let has_created_at = !frame.created_at.is_empty();
39		let has_updated_at = !frame.updated_at.is_empty();
40		let column_order = Self::get_column_display_order(frame);
41
42		let col_widths = Self::compute_column_widths(
43			frame,
44			&column_order,
45			row_count,
46			has_row_numbers,
47			has_created_at,
48			has_updated_at,
49		);
50		let sep = Self::separator_line(&col_widths);
51
52		writeln!(f, "{}", sep)?;
53		Self::emit_header(
54			frame,
55			f,
56			&column_order,
57			&col_widths,
58			has_row_numbers,
59			has_created_at,
60			has_updated_at,
61		)?;
62		writeln!(f, "{}", sep)?;
63		Self::emit_data_rows(
64			frame,
65			f,
66			&column_order,
67			&col_widths,
68			row_count,
69			has_row_numbers,
70			has_created_at,
71			has_updated_at,
72		)?;
73		writeln!(f, "{}", sep)
74	}
75
76	#[inline]
77	#[allow(clippy::too_many_arguments)]
78	fn compute_column_widths(
79		frame: &Frame,
80		column_order: &[usize],
81		row_count: usize,
82		has_row_numbers: bool,
83		has_created_at: bool,
84		has_updated_at: bool,
85	) -> Vec<usize> {
86		let col_count = frame.len()
87			+ if has_row_numbers {
88				1
89			} else {
90				0
91			} + if has_created_at {
92			1
93		} else {
94			0
95		} + if has_updated_at {
96			1
97		} else {
98			0
99		};
100
101		let mut col_widths = vec![0; col_count];
102
103		let mut sys_col_idx = 0;
104		if has_row_numbers {
105			col_widths[sys_col_idx] = Self::display_width("#rownum");
106			for row_num in &frame.row_numbers {
107				col_widths[sys_col_idx] =
108					col_widths[sys_col_idx].max(Self::display_width(&row_num.to_string()));
109			}
110			sys_col_idx += 1;
111		}
112		if has_created_at {
113			col_widths[sys_col_idx] = Self::display_width("#created_at");
114			for ts in &frame.created_at {
115				col_widths[sys_col_idx] =
116					col_widths[sys_col_idx].max(Self::display_width(&ts.to_string()));
117			}
118			sys_col_idx += 1;
119		}
120		if has_updated_at {
121			col_widths[sys_col_idx] = Self::display_width("#updated_at");
122			for ts in &frame.updated_at {
123				col_widths[sys_col_idx] =
124					col_widths[sys_col_idx].max(Self::display_width(&ts.to_string()));
125			}
126			sys_col_idx += 1;
127		}
128		let row_num_col_idx = sys_col_idx;
129
130		for (display_idx, &col_idx) in column_order.iter().enumerate() {
131			let col = &frame[col_idx];
132			let display_name = Self::escape_control_chars(&col.name);
133			col_widths[row_num_col_idx + display_idx] = Self::display_width(&display_name);
134		}
135
136		for row_numberx in 0..row_count {
137			for (display_idx, &col_idx) in column_order.iter().enumerate() {
138				let col = &frame[col_idx];
139				let s = Self::extract_string_value(col, row_numberx);
140				col_widths[row_num_col_idx + display_idx] =
141					col_widths[row_num_col_idx + display_idx].max(Self::display_width(&s));
142			}
143		}
144
145		for w in &mut col_widths {
146			*w += 2;
147		}
148
149		col_widths
150	}
151
152	#[inline]
153	fn separator_line(col_widths: &[usize]) -> String {
154		format!("+{}+", col_widths.iter().map(|w| "-".repeat(*w + 2)).collect::<Vec<_>>().join("+"))
155	}
156
157	#[inline]
158	#[allow(clippy::too_many_arguments)]
159	fn emit_header(
160		frame: &Frame,
161		f: &mut dyn Write,
162		column_order: &[usize],
163		col_widths: &[usize],
164		has_row_numbers: bool,
165		has_created_at: bool,
166		has_updated_at: bool,
167	) -> fmt::Result {
168		let mut header = Vec::new();
169
170		let mut sys_idx = 0;
171		if has_row_numbers {
172			header.push(Self::format_cell(col_widths[sys_idx], "#rownum"));
173			sys_idx += 1;
174		}
175		if has_created_at {
176			header.push(Self::format_cell(col_widths[sys_idx], "#created_at"));
177			sys_idx += 1;
178		}
179		if has_updated_at {
180			header.push(Self::format_cell(col_widths[sys_idx], "#updated_at"));
181			sys_idx += 1;
182		}
183		let row_num_col_idx = sys_idx;
184
185		reifydb_assertions! {
186			let needed = row_num_col_idx + column_order.len();
187			assert!(
188				col_widths.len() >= needed,
189				"header system-column count diverged from compute_column_widths, so a data \
190				 column would index col_widths out of bounds and panic mid-render \
191				 (system cols={row_num_col_idx}, data cols={}, col_widths.len()={})",
192				column_order.len(),
193				col_widths.len()
194			);
195		}
196
197		for (display_idx, &col_idx) in column_order.iter().enumerate() {
198			let col = &frame[col_idx];
199			let name = Self::escape_control_chars(&col.name);
200			header.push(Self::format_cell(col_widths[row_num_col_idx + display_idx], &name));
201		}
202
203		writeln!(f, "|{}|", header.join("|"))
204	}
205
206	#[inline]
207	#[allow(clippy::too_many_arguments)]
208	fn emit_data_rows(
209		frame: &Frame,
210		f: &mut dyn Write,
211		column_order: &[usize],
212		col_widths: &[usize],
213		row_count: usize,
214		has_row_numbers: bool,
215		has_created_at: bool,
216		has_updated_at: bool,
217	) -> fmt::Result {
218		for row_numberx in 0..row_count {
219			let mut row = Vec::new();
220
221			let mut sys_idx = 0;
222			if has_row_numbers {
223				let s = frame.row_numbers[row_numberx].to_string();
224				row.push(Self::format_cell(col_widths[sys_idx], &s));
225				sys_idx += 1;
226			}
227			if has_created_at {
228				let s = frame.created_at[row_numberx].to_string();
229				row.push(Self::format_cell(col_widths[sys_idx], &s));
230				sys_idx += 1;
231			}
232			if has_updated_at {
233				let s = frame.updated_at[row_numberx].to_string();
234				row.push(Self::format_cell(col_widths[sys_idx], &s));
235				sys_idx += 1;
236			}
237			let row_num_col_idx = sys_idx;
238
239			reifydb_assertions! {
240				let needed = row_num_col_idx + column_order.len();
241				assert!(
242					col_widths.len() >= needed,
243					"data-row system-column count diverged from compute_column_widths, so a data \
244					 column would index col_widths out of bounds and panic mid-render \
245					 (system cols={row_num_col_idx}, data cols={}, col_widths.len()={})",
246					column_order.len(),
247					col_widths.len()
248				);
249			}
250
251			for (display_idx, &col_idx) in column_order.iter().enumerate() {
252				let col = &frame[col_idx];
253				let s = Self::extract_string_value(col, row_numberx);
254				row.push(Self::format_cell(col_widths[row_num_col_idx + display_idx], &s));
255			}
256
257			writeln!(f, "|{}|", row.join("|"))?;
258		}
259
260		Ok(())
261	}
262
263	fn format_cell(width: usize, content: &str) -> String {
264		let pad = width - Self::display_width(content);
265		let l = pad / 2;
266		let r = pad - l;
267		format!(" {:left$}{}{:right$} ", "", content, "", left = l, right = r)
268	}
269
270	fn display_width(s: &str) -> usize {
271		if s.contains('\n') {
272			s.lines().map(|line| line.width()).max().unwrap_or(0)
273		} else {
274			s.width()
275		}
276	}
277
278	fn escape_control_chars(s: &str) -> String {
279		s.replace('\n', "\\n").replace('\t', "\\t")
280	}
281
282	fn get_column_display_order(frame: &Frame) -> Vec<usize> {
283		(0..frame.len()).collect()
284	}
285
286	fn extract_string_value(col: &FrameColumn, row_numberx: usize) -> String {
287		let s = col.data.as_string(row_numberx);
288		Self::escape_control_chars(&s)
289	}
290}