Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "fb"
version = "0.2.2"
version = "0.2.3"
edition = "2021"
license = "Apache-2.0"

Expand Down
16 changes: 13 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
eprintln!("Press Ctrl+D to exit.");
}
let mut buffer: String = String::new();
let mut has_error = false;
loop {
let prompt = if !is_tty {
// No prompt when stdout is not a terminal (e.g., piped)
Expand Down Expand Up @@ -115,7 +116,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
rl.append_history(&history_path)?;

for q in queries {
let _ = query(&mut context, q).await;
if query(&mut context, q).await.is_err() {
has_error = true;
}
}

buffer.clear();
Expand All @@ -135,7 +138,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
for q in queries {
rl.add_history_entry(q.trim())?;
rl.append_history(&history_path)?;
let _ = query(&mut context, q).await;
if query(&mut context, q).await.is_err() {
has_error = true;
}
}
}
}
Expand All @@ -144,6 +149,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
Err(err) => {
eprintln!("Error: {:?}", err);
has_error = true;
break;
}
}
Expand All @@ -153,5 +159,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
eprintln!("Saved history to {:?}", history_path)
}

Ok(())
if has_error {
Err("One or more queries failed".into())
} else {
Ok(())
}
}
25 changes: 21 additions & 4 deletions src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ pub async fn query(context: &mut Context, query_text: String) -> Result<(), Box<
}))
};

let mut query_failed = false;

select! {
_ = signal::ctrl_c() => {
finish_token.cancel();
Expand All @@ -139,6 +141,7 @@ pub async fn query(context: &mut Context, query_text: String) -> Result<(), Box<
if !context.args.concise {
eprintln!("^C");
}
query_failed = true;
}
response = async_resp => {
let elapsed = start.elapsed();
Expand Down Expand Up @@ -189,15 +192,23 @@ pub async fn query(context: &mut Context, query_text: String) -> Result<(), Box<
eprintln!("URL: {}", context.url);
}

let status = resp.status();
let body = resp.text().await?;

// on stdout, on purpose
println!("{}", resp.text().await?);
print!("{}", body);

if !status.is_success() {
query_failed = true;
}
}
Err(error) => {
if context.args.verbose {
eprintln!("Failed to send the request: {:?}", error);
} else {
eprintln!("Failed to send the request: {}", error.to_string());
}
query_failed = true;
},
};

Expand All @@ -212,7 +223,11 @@ pub async fn query(context: &mut Context, query_text: String) -> Result<(), Box<
}
};

Ok(())
if query_failed {
Err("Query failed".into())
} else {
Ok(())
}
}

#[derive(Parser)]
Expand Down Expand Up @@ -247,16 +262,18 @@ mod tests {
use crate::args::get_args;

#[tokio::test]
async fn test_query() {
async fn test_query_connection_error() {
let mut args = get_args().unwrap();
args.host = "localhost:8123".to_string();
args.database = "test_db".to_string();
args.concise = true; // suppress output

let mut context = Context::new(args);
let query_text = "select 42".to_string();

// Query should fail when server is not available
let result = query(&mut context, query_text).await;
assert!(result.is_ok());
assert!(result.is_err());
}

#[test]
Expand Down
65 changes: 59 additions & 6 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ fn test_set_format() {
// First set format to TSV
let (success, stdout, _) = run_fb(&["--core", "--concise", "-f", "TabSeparatedWithNamesAndTypes", "SELECT 42;"]);
assert!(success);
assert_eq!(stdout, "?column?\nint\n42\n\n");
assert_eq!(stdout, "?column?\nint\n42\n");
}

#[test]
Expand Down Expand Up @@ -88,16 +88,17 @@ fn test_params_escaping() {

assert!(output.status.success());
let mut lines = stdout.lines();
// First query result
assert_eq!(lines.next().unwrap(), "?column?");
lines.next();
assert_eq!(lines.next().unwrap(), "text");
assert_eq!(lines.next().unwrap(), "a=}&");
lines.next();
// Second query result
assert_eq!(lines.next().unwrap(), "?column?");
lines.next();
assert_eq!(lines.next().unwrap(), "text");
assert_eq!(lines.next().unwrap(), "a=}&");
lines.next();
// Third query result
assert_eq!(lines.next().unwrap(), "?column?");
lines.next();
assert_eq!(lines.next().unwrap(), "text");
assert_eq!(lines.next().unwrap(), "b=}&");
}

Expand Down Expand Up @@ -235,3 +236,55 @@ fn test_json_output_fully_parseable() {
);
}
}

#[test]
fn test_exit_code_on_connection_error() {
// Test that exit code is non-zero when server is not available
let (success, _, stderr) = run_fb(&["--host", "localhost:59999", "--concise", "SELECT 1"]);

assert!(!success, "Exit code should be non-zero when connection fails");
assert!(
stderr.contains("Failed to send the request"),
"stderr should contain connection error message, got: {}",
stderr
);
}

#[test]
fn test_exit_code_on_query_error() {
// Test that exit code is non-zero when query returns an error (e.g., syntax error)
let (success, stdout, _) = run_fb(&["--core", "--concise", "SELEC INVALID SYNTAX"]);

assert!(!success, "Exit code should be non-zero when query fails");
// The server should return an error message in the response
assert!(
stdout.to_lowercase().contains("error") || stdout.to_lowercase().contains("exception"),
"stdout should contain error message from server, got: {}",
stdout
);
}

#[test]
fn test_exit_code_on_query_error_interactive() {
// Test that exit code is non-zero when any query fails in interactive mode
let mut child = Command::new(env!("CARGO_BIN_EXE_fb"))
.args(&["--core", "--concise"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();

let mut stdin = child.stdin.take().unwrap();
writeln!(stdin, "SELECT 1;").unwrap(); // Valid query
writeln!(stdin, "SELEC INVALID;").unwrap(); // Invalid query
writeln!(stdin, "SELECT 2;").unwrap(); // Valid query
drop(stdin);

let output = child.wait_with_output().unwrap();

assert!(
!output.status.success(),
"Exit code should be non-zero when any query in session fails"
);
}