Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion crates/fda/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,12 @@ pub enum ProgramAction {
#[arg(value_hint = ValueHint::Other, add = ArgValueCompleter::new(pipeline_names))]
name: String,
},
/// Retrieve program compilation errors and warnings.
Errors {
/// The name of the pipeline.
#[arg(value_hint = ValueHint::Other, add = ArgValueCompleter::new(pipeline_names))]
name: String,
},
}

#[derive(Subcommand)]
Expand All @@ -1011,7 +1017,7 @@ pub enum ConnectorAction {

#[cfg(test)]
mod tests {
use crate::cli::Cli;
use crate::cli::{Cli, Commands, ProgramAction};
use clap::Parser;

/// [clap] will panic inside `try_parse` if it finds anything invalid in the
Expand All @@ -1021,4 +1027,17 @@ mod tests {
fn basic_validation() {
let _ = Cli::try_parse();
}

#[test]
fn parse_program_errors_command() {
let cli = Cli::try_parse_from(["fda", "program", "errors", "pipeline"])
.expect("program errors command should parse");

assert!(matches!(
cli.command,
Commands::Pipeline(crate::cli::PipelineAction::Program {
action: ProgramAction::Errors { name }
}) if name == "pipeline"
));
}
}
198 changes: 197 additions & 1 deletion crates/fda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2428,6 +2428,85 @@ async fn connector(
};
}

fn format_program_errors(program_error: &ProgramError) -> String {
let mut output = String::new();
let mut has_output = false;

if let Some(sql) = &program_error.sql_compilation {
has_output = true;
output.push_str(&format!("SQL compiler exit code: {}\n", sql.exit_code));

if sql.messages.is_empty() {
output.push_str("No SQL compiler messages.\n");
} else {
for message in &sql.messages {
let severity = if message.warning { "warning" } else { "error" };
let error_type = &message.error_type;
let message_text = &message.message;
let start_line = message.start_line_number;
let start_column = message.start_column;

output.push_str(&format!(
"{severity}: {error_type}: {message_text} (line {start_line}, column {start_column})\n",
));

if let Some(snippet) = &message.snippet
&& !snippet.is_empty()
{
output.push_str(snippet);
if !snippet.ends_with('\n') {
output.push('\n');
}
}
}
}
}

if let Some(rust) = &program_error.rust_compilation {
if has_output {
output.push('\n');
}
has_output = true;
output.push_str(&format!("Rust compiler exit code: {}\n", rust.exit_code));

if !rust.stdout.is_empty() {
output.push_str("\nstdout:\n");
output.push_str(&rust.stdout);
if !rust.stdout.ends_with('\n') {
output.push('\n');
}
}
if !rust.stderr.is_empty() {
output.push_str("\nstderr:\n");
output.push_str(&rust.stderr);
if !rust.stderr.ends_with('\n') {
output.push('\n');
}
}
if rust.stdout.is_empty() && rust.stderr.is_empty() {
output.push_str("No Rust compiler stdout or stderr.\n");
}
}

if let Some(system_error) = &program_error.system_error {
if has_output {
output.push('\n');
}
has_output = true;
output.push_str("System error:\n");
output.push_str(system_error);
if !system_error.ends_with('\n') {
output.push('\n');
}
}

if !has_output {
output.push_str("No compilation errors or warnings.\n");
}

output
}

async fn program(format: OutputFormat, action: ProgramAction, client: Client) {
match action {
ProgramAction::Get {
Expand Down Expand Up @@ -2591,6 +2670,44 @@ async fn program(format: OutputFormat, action: ProgramAction, client: Client) {
}
}
}
ProgramAction::Errors { name } => {
let response = client
.get_pipeline()
.pipeline_name(name)
.send()
.await
.map_err(handle_errors_fatal(
client.baseurl().clone(),
"Failed to get program compilation errors",
1,
))
.unwrap();

let Some(program_error) = response.into_inner().program_error else {
eprintln!(
"Pipeline response did not include program errors. {}",
UPGRADE_NOTICE
);
std::process::exit(1);
};

match format {
OutputFormat::Text => {
print!("{}", format_program_errors(&program_error));
}
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&program_error)
.expect("Failed to serialize program errors")
);
}
_ => {
eprintln!("Unsupported output format: {}", format);
std::process::exit(1);
}
}
}
ProgramAction::Set {
name,
program_path,
Expand Down Expand Up @@ -2842,7 +2959,10 @@ fn init_logging(default_level: &str) {

#[cfg(test)]
mod tests {
use super::make_client;
use super::{format_program_errors, make_client};
use feldera_rest_api::types::{
ProgramError, RustCompilationInfo, SqlCompilationInfo, SqlCompilerMessage,
};
use std::io::Write;

// A single self-signed PEM-encoded certificate used only as test data. It
Expand Down Expand Up @@ -2933,4 +3053,80 @@ aC3Oy4iVrYGOq9v6uP9iblE=\n\
);
}
}

#[test]
fn format_program_errors_empty() {
let output = format_program_errors(&ProgramError {
sql_compilation: None,
rust_compilation: None,
system_error: None,
});

assert_eq!(output, "No compilation errors or warnings.\n");
}

#[test]
fn format_program_errors_sql_messages() {
let output = format_program_errors(&ProgramError {
sql_compilation: Some(SqlCompilationInfo {
exit_code: 1,
messages: vec![SqlCompilerMessage {
start_line_number: 2,
start_column: 4,
end_line_number: 2,
end_column: 8,
warning: true,
error_type: "PRIMARY KEY cannot be nullable".to_string(),
message: "PRIMARY KEY column 'C' has type INTEGER, which is nullable"
.to_string(),
snippet: Some(" 2| c INT PRIMARY KEY\n ^^^^^\n".to_string()),
}],
}),
rust_compilation: None,
system_error: None,
});

assert_eq!(
output,
"SQL compiler exit code: 1\n\
warning: PRIMARY KEY cannot be nullable: PRIMARY KEY column 'C' has type INTEGER, which is nullable (line 2, column 4)\n\
2| c INT PRIMARY KEY\n\

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether we need a "verbosity" argument which would enable the snippet being shown

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed the ci fmt issue in e0a907d. on the snippet bit, i'd keep snippets in text output for now. personally, i think hiding them would make this cmd less useful, since the whole point is to avoid digging through the full pipeline json just to understand a compiler error. json still gives the raw payload for ppl/scripts that want to handle it differently. if output ends up too noisy, i'd rather add a specific --no-snippets or --summary flag later than introduce a new verbosity arg just for this cmd.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Verbosity" would be a new fda flag, which applies to all other fda commands. But that's a separate issue we should file and solve.

^^^^^\n"
);
}

#[test]
fn format_program_errors_rust_output() {
let output = format_program_errors(&ProgramError {
sql_compilation: None,
rust_compilation: Some(RustCompilationInfo {
exit_code: 101,
stdout: "checking pipeline\n".to_string(),
stderr: "error: failed to compile".to_string(),
}),
system_error: None,
});

assert_eq!(
output,
"Rust compiler exit code: 101\n\
\n\
stdout:\n\
checking pipeline\n\
\n\
stderr:\n\
error: failed to compile\n"
);
}

#[test]
fn format_program_errors_system_error() {
let output = format_program_errors(&ProgramError {
sql_compilation: None,
rust_compilation: None,
system_error: Some("compiler service unavailable".to_string()),
});

assert_eq!(output, "System error:\ncompiler service unavailable\n");
}
}
17 changes: 17 additions & 0 deletions crates/fda/test.bash
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ fda pipelines
fda shutdown p1 || true
fda delete --force p1 || true
fda delete --force p2 || true
fda delete --force pbad || true
fda clear pudf && fda delete pudf || true
fda delete --force pclock || true
fda delete unknown || true
Expand Down Expand Up @@ -91,6 +92,21 @@ fda event p1 latest
fda event p1 latest status
fda event p1 latest all

cat > bad-program.sql <<EOF
SELECT invalid
EOF
fda create pbad bad-program.sql
for _ in {1..60}; do
if fda program status pbad | grep -q "SqlError"; then
break
fi
sleep 1
done
fda program status pbad | grep "SqlError"
fda program errors pbad | grep -i "error:"
fda --format json program errors pbad | jq -e '.sql_compilation.messages | length > 0'
fda delete --force pbad

fda create pudf program.sql --udf-toml udf.toml --udf-rs udf.rs
compare_output "fda program get pudf --udf-toml" "cat udf.toml"
compare_output "fda program get pudf --udf-rs" "cat udf.rs"
Expand Down Expand Up @@ -275,6 +291,7 @@ fail_on_success fda --insecure --tls-cert /tmp/does-not-matter.pem pipelines
fail_on_success fda -k --tls-cert /tmp/does-not-matter.pem pipelines

rm program.sql
rm bad-program.sql
rm udf.toml
rm udf.rs
rm -f test-support-bundle-full.zip
Expand Down