diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ea9dfff8a2b..af3fa23f2882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,6 +125,15 @@ codegen's "needs backticks" rule accepted `$` because it was copied from the SQL side, where Postgres does allow it in an identifier. (@prql-bot, #6322) +- `prqlc watch` no longer exits when a `.prql` file in the watched tree fails to + compile. The initial pass aborted on the first error, so the command never + reached the watch loop — precisely the state watch mode exists to iterate out + of. Both the initial pass and the watch loop now print the failing path + alongside the error and carry on. The loop previously discarded every error it + hit, so a failure that produces no compiler diagnostic — an unwritable `.sql` + output path, say — left the watcher running silently and writing nothing. + (@prql-bot, #6313) + **Documentation**: - The `prql-java` README now documents the actual API. It advertised a diff --git a/prqlc/prqlc/src/cli/watch.rs b/prqlc/prqlc/src/cli/watch.rs index 773428b19ab2..a829750d36a7 100644 --- a/prqlc/prqlc/src/cli/watch.rs +++ b/prqlc/prqlc/src/cli/watch.rs @@ -41,12 +41,26 @@ pub fn run(command: &mut WatchArgs) -> Result<()> { fn find_and_compile(path: &Path, opt: &prqlc::Options) -> Result<()> { for entry in WalkDir::new(path) { - compile_path(entry?.path(), opt)?; + compile_path_reporting_errors(entry?.path(), opt); } Ok(()) } +/// Compile `path`, printing any error rather than propagating it. +/// +/// A file that doesn't compile is the ordinary starting state for `watch`, and +/// the one it exists to iterate out of, so neither the initial pass nor the +/// watch loop stops for one. `compile_path` prints the compiler's own +/// diagnostics, but it also returns errors it hasn't printed — an unwritable +/// `.sql` path, say — which would otherwise leave the watcher silently +/// producing nothing. +fn compile_path_reporting_errors(path: &Path, opt: &prqlc::Options) { + if let Err(error) = compile_path(path, opt) { + println!("{}: {error}", path.display()); + } +} + fn watch_and_compile(path: &Path, opt: &prqlc::Options) -> Result<()> { let cwd = std::env::current_dir().ok(); @@ -77,7 +91,7 @@ fn watch_and_compile(path: &Path, opt: &prqlc::Options) -> Result<()> { &path }; - let _ignore = compile_path(relative_path, opt); + compile_path_reporting_errors(relative_path, opt); } } @@ -134,3 +148,32 @@ fn compile_path(path: &Path, opt: &prqlc::Options) -> Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::TempDir; + + use super::*; + + /// A file that fails to compile used to abort the initial pass, so + /// `prqlc watch` exited before printing "Watching path" — the one state + /// watch mode exists to iterate out of. The other files must still compile, + /// and the walk must reach the end. + #[test] + fn initial_compile_continues_past_a_failing_file() { + let dir = TempDir::new().unwrap(); + // Two good files, since `WalkDir` doesn't order entries: whichever + // side of the failing file the walk puts them on, both must compile. + fs::write(dir.path().join("a_good.prql"), "from tracks\n").unwrap(); + fs::write(dir.path().join("b_bad.prql"), "from tracks | filter\n").unwrap(); + fs::write(dir.path().join("c_good.prql"), "from albums\n").unwrap(); + + find_and_compile(dir.path(), &prqlc::Options::default()).unwrap(); + + assert!(dir.path().join("a_good.sql").is_file()); + assert!(dir.path().join("c_good.sql").is_file()); + assert!(!dir.path().join("b_bad.sql").exists()); + } +}