Skip to content

Commit dc8c570

Browse files
committed
feat(cli): add in-place sql file formatter
- add a minimal binary interface for formatting a single sql file in place - keep failure behavior explicit with non-zero exits for invalid args and io errors - add integration tests for usage errors, missing files, and successful formatting
1 parent e9ee999 commit dc8c570

File tree

2 files changed

+108
-0
lines changed

2 files changed

+108
-0
lines changed

src/main.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use std::{env, fs, process};
2+
3+
fn run() -> Result<(), String> {
4+
let mut args = env::args();
5+
let program = args.next().unwrap_or_else(|| String::from("sqlformat"));
6+
7+
let filename = match (args.next(), args.next()) {
8+
(Some(filename), None) => filename,
9+
_ => return Err(format!("Usage: {program} <filename>")),
10+
};
11+
12+
let input = fs::read_to_string(&filename)
13+
.map_err(|err| format!("Error reading '{filename}': {err}"))?;
14+
15+
let formatted = sqlformat::format(
16+
&input,
17+
&sqlformat::QueryParams::None,
18+
&sqlformat::FormatOptions::default(),
19+
);
20+
21+
fs::write(&filename, formatted).map_err(|err| format!("Error writing '{filename}': {err}"))?;
22+
23+
Ok(())
24+
}
25+
26+
fn main() {
27+
if let Err(err) = run() {
28+
eprintln!("{err}");
29+
process::exit(1);
30+
}
31+
}

tests/cli.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use std::fs;
2+
use std::path::PathBuf;
3+
use std::process::Command;
4+
use std::time::{SystemTime, UNIX_EPOCH};
5+
6+
fn sqlformat_bin() -> PathBuf {
7+
std::env::var_os("CARGO_BIN_EXE_sqlformat")
8+
.map(PathBuf::from)
9+
.expect("binary path should be provided by cargo test")
10+
}
11+
12+
fn unique_temp_file_path(name: &str) -> PathBuf {
13+
let nanos = SystemTime::now()
14+
.duration_since(UNIX_EPOCH)
15+
.expect("system clock should be after unix epoch")
16+
.as_nanos();
17+
18+
std::env::temp_dir().join(format!(
19+
"sqlformat-cli-{name}-{}-{nanos}.sql",
20+
std::process::id()
21+
))
22+
}
23+
24+
#[test]
25+
fn exits_non_zero_and_prints_usage_for_missing_filename() {
26+
let output = Command::new(sqlformat_bin())
27+
.output()
28+
.expect("binary should execute");
29+
30+
assert!(!output.status.success());
31+
32+
let stderr = String::from_utf8_lossy(&output.stderr);
33+
assert!(stderr.contains("Usage:"));
34+
}
35+
36+
#[test]
37+
fn exits_non_zero_when_input_file_does_not_exist() {
38+
let missing_path = unique_temp_file_path("missing");
39+
if missing_path.exists() {
40+
fs::remove_file(&missing_path).expect("pre-existing temp file should be removable");
41+
}
42+
43+
let output = Command::new(sqlformat_bin())
44+
.arg(&missing_path)
45+
.output()
46+
.expect("binary should execute");
47+
48+
assert!(!output.status.success());
49+
50+
let stderr = String::from_utf8_lossy(&output.stderr);
51+
assert!(stderr.contains("Error reading"));
52+
assert!(!missing_path.exists());
53+
}
54+
55+
#[test]
56+
fn formats_file_in_place_with_default_options() {
57+
let sql_path = unique_temp_file_path("format");
58+
let input = "SELECT count(*),Column1 FROM Table1;";
59+
fs::write(&sql_path, input).expect("test input file should be writable");
60+
61+
let output = Command::new(sqlformat_bin())
62+
.arg(&sql_path)
63+
.output()
64+
.expect("binary should execute");
65+
66+
assert!(output.status.success());
67+
68+
let expected = sqlformat::format(
69+
input,
70+
&sqlformat::QueryParams::None,
71+
&sqlformat::FormatOptions::default(),
72+
);
73+
let actual = fs::read_to_string(&sql_path).expect("formatted file should be readable");
74+
assert_eq!(actual, expected);
75+
76+
fs::remove_file(&sql_path).expect("test output file should be removable");
77+
}

0 commit comments

Comments
 (0)