Skip to content

Commit ea5e93d

Browse files
authored
refactor(cli): render sqb plan output natively (#1099)
* refactor: render text and JSON plans natively * chore: update the Python boundary ratchet
1 parent 39562f8 commit ea5e93d

49 files changed

Lines changed: 3530 additions & 2732 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//! Bounded sections, aligned name/value rows and ANSI stripping, as `sqlbuild.presentation` does.
2+
3+
use crate::cli_text::constants::MIN_NAME_COLUMN_WIDTH;
4+
use crate::cli_text::models::{CliStyle, DisplayLimit};
5+
6+
/// The text without ANSI SGR sequences (`ESC [ digits-and-semicolons m`).
7+
pub(crate) fn strip_ansi(text: &str) -> String {
8+
let mut stripped: String = String::with_capacity(text.len());
9+
let mut rest: &str = text;
10+
while let Some(start) = rest.find("\x1b[") {
11+
stripped.push_str(&rest[..start]);
12+
let after: &str = &rest[start + 2..];
13+
let parameters: usize = after
14+
.find(|character: char| !(character.is_ascii_digit() || character == ';'))
15+
.unwrap_or(after.len());
16+
if after[parameters..].starts_with('m') {
17+
rest = &after[parameters + 1..];
18+
} else {
19+
stripped.push('\x1b');
20+
rest = &rest[start + 1..];
21+
}
22+
}
23+
stripped.push_str(rest);
24+
stripped
25+
}
26+
27+
/// `count_header_style` over `plan_section`: `Title (count)` as a bold title and dim count.
28+
pub(crate) fn section_header(style: CliStyle, text: &str) -> String {
29+
if let Some(split) = text.rfind(" (")
30+
&& text.ends_with(')')
31+
{
32+
return format!(
33+
"{} {}",
34+
style.plan_section(&text[..split]),
35+
style.muted(&text[split + 1..])
36+
);
37+
}
38+
style.plan_section(text)
39+
}
40+
41+
/// How many of `total` entries a section shows.
42+
pub(crate) fn visible_count(total: usize, display: &DisplayLimit) -> usize {
43+
display.max_entries.map_or(total, |limit| total.min(limit))
44+
}
45+
46+
/// The dim `... and N more` line when a section hides entries.
47+
pub(crate) fn overflow_line(
48+
(total, visible): (usize, usize),
49+
indent: &str,
50+
display: &DisplayLimit,
51+
) -> Option<String> {
52+
(total > visible).then(|| {
53+
CliStyle { use_color: true }.muted(&format!(
54+
"{indent}... and {} more (use {} to show all)",
55+
total - visible,
56+
display.overflow_flag
57+
))
58+
})
59+
}
60+
61+
/// The widest name, but at least the minimum name column.
62+
pub(crate) fn name_column_width<'a>(names: impl Iterator<Item = &'a str>) -> usize {
63+
names
64+
.map(|name| name.chars().count())
65+
.max()
66+
.unwrap_or(0)
67+
.max(MIN_NAME_COLUMN_WIDTH)
68+
}
69+
70+
/// ` name<padding> value`, with the value dim unless `dim_value` is off.
71+
pub(crate) fn name_value_line(name: &str, value: &str, width: usize, dim_value: bool) -> String {
72+
let style: CliStyle = CliStyle { use_color: true };
73+
let rendered: String = if dim_value {
74+
style.muted(value)
75+
} else {
76+
value.to_owned()
77+
};
78+
let padding: String = " ".repeat(width.saturating_sub(name.chars().count()));
79+
format!(" {}{padding} {rendered}", style.object_name(name))
80+
}
81+
82+
/// A comma-separated name list cut to the section limit with a `+N more` note.
83+
pub(crate) fn capped_names(
84+
names: &[String],
85+
display: &DisplayLimit,
86+
name_style: impl Fn(&str) -> String,
87+
) -> String {
88+
let visible: usize = visible_count(names.len(), display);
89+
let base: String = names[..visible]
90+
.iter()
91+
.map(|name| name_style(name))
92+
.collect::<Vec<String>>()
93+
.join(", ");
94+
if visible >= names.len() {
95+
return base;
96+
}
97+
format!(
98+
"{base}, ... (+{} more; use {} to show all)",
99+
names.len() - visible,
100+
display.overflow_flag
101+
)
102+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
1+
pub(crate) mod bounded_sections;
12
pub(crate) mod diagnostics;
23
pub(crate) mod layout;
4+
pub(crate) mod plan_document;
5+
pub(crate) mod plan_sections;
6+
pub(crate) mod tree_rows;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//! The full-refresh and incremental bodies of the text plan.
2+
3+
use crate::cli_text::_helpers::layout::surface_header;
4+
use crate::cli_text::_helpers::plan_sections::{
5+
full_refresh_count_lines, functions_lines, plan_name_width, provider_usage_lines, pruned_lines,
6+
python_entry_lines, ready_summary, reason_group_lines, remaining_stale_lines,
7+
routine_model_lines, seed_lines, source_freshness_lines, source_load_lines, warning_lines,
8+
};
9+
use crate::cli_text::_helpers::tree_rows::treeify;
10+
use crate::cli_text::constants::{
11+
PLAN_READY_TITLE, PRE_SQL_INGRESS_PHASE, PYTHON_INGRESS_TITLE, PYTHON_READ_SIDE_TITLE,
12+
READ_SIDE_PHASE,
13+
};
14+
use crate::cli_text::models::{CliStyle, ModelPlanText, PlanTextReport};
15+
16+
fn header_lines(report: &PlanTextReport, full_refresh: bool) -> Vec<String> {
17+
if !report.include_header {
18+
return Vec::new();
19+
}
20+
vec![surface_header(
21+
CliStyle { use_color: true },
22+
PLAN_READY_TITLE,
23+
&ready_summary(report, full_refresh),
24+
)]
25+
}
26+
27+
/// Every active model is a full refresh: counts by materialization instead of detail.
28+
pub(crate) fn full_refresh_lines(
29+
report: &PlanTextReport,
30+
active: &[&ModelPlanText],
31+
) -> Vec<String> {
32+
let width: usize = plan_name_width(report);
33+
let mut lines: Vec<String> = header_lines(report, true);
34+
lines.extend(provider_usage_lines(report));
35+
lines.extend(python_entry_lines(
36+
(report, PRE_SQL_INGRESS_PHASE, PYTHON_INGRESS_TITLE),
37+
width,
38+
));
39+
lines.extend(source_load_lines(report, width));
40+
lines.extend(functions_lines(report, true, width));
41+
lines.extend(functions_lines(report, false, width));
42+
if !lines.is_empty() {
43+
lines.push(String::new());
44+
}
45+
lines.extend(full_refresh_count_lines(active));
46+
lines.extend(seed_lines(report));
47+
lines.extend(python_entry_lines(
48+
(report, READ_SIDE_PHASE, PYTHON_READ_SIDE_TITLE),
49+
width,
50+
));
51+
lines.extend(warning_lines(report));
52+
lines.extend(report.pre_build_lines.iter().cloned());
53+
lines
54+
}
55+
56+
/// Sections by change reason, drawn as trees.
57+
pub(crate) fn incremental_lines(report: &PlanTextReport, active: &[&ModelPlanText]) -> Vec<String> {
58+
let width: usize = plan_name_width(report);
59+
let mut lines: Vec<String> = header_lines(report, false);
60+
if report.include_freshness_diagnostics {
61+
lines.extend(source_freshness_lines(report));
62+
}
63+
lines.extend(remaining_stale_lines(report));
64+
lines.extend(provider_usage_lines(report));
65+
lines.extend(pruned_lines(report));
66+
lines.extend(report.pre_build_lines.iter().cloned());
67+
lines.extend(python_entry_lines(
68+
(report, PRE_SQL_INGRESS_PHASE, PYTHON_INGRESS_TITLE),
69+
width,
70+
));
71+
lines.extend(source_load_lines(report, width));
72+
lines.extend(functions_lines(report, true, width));
73+
lines.extend(reason_group_lines(report, active, width));
74+
lines.extend(routine_model_lines(report, active, width));
75+
lines.extend(functions_lines(report, false, width));
76+
lines.extend(seed_lines(report));
77+
lines.extend(python_entry_lines(
78+
(report, READ_SIDE_PHASE, PYTHON_READ_SIDE_TITLE),
79+
width,
80+
));
81+
let mut lines: Vec<String> = treeify(lines);
82+
lines.extend(warning_lines(report));
83+
lines
84+
}

0 commit comments

Comments
 (0)