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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Building qsv from source now also patches `hashbrown` to Polars' fork (the same pin Polars itself uses), because Polars 2.0 relies on prefetch APIs that aren't in a hashbrown release yet ([rust-lang/hashbrown#727](https://github.com/rust-lang/hashbrown/pull/727)).

### Fixed
- **`search --quick` and `searchset --quick` no longer report a row when nothing matched** ([#4767](https://github.com/dathere/qsv/pull/4767)). On an unindexed file, they printed the total row count to stderr where the first match's row number goes: `search` did so on every no-match, and `searchset` with `--not-one`. A caller couldn't tell that from a match on the last row. (DataPusher+'s quick PII screen tripped on this.) Now no match prints nothing. Without `--not-one` it still exits 1; with it, it exits 0. The indexed (parallel) path already behaved this way.
- **`describegpt` no longer writes your API key into the files it generates** ([#4766](https://github.com/dathere/qsv/pull/4766)). The attribution it adds to each data dictionary, description and tag set records the command line that ran, and that included the value of `--api-key` (or `-k`) and `--ckan-token`. Those values are now replaced with `[REDACTED]`, however they were written (`--api-key KEY`, `--api-key=KEY`, `-kKEY`, `-Ak KEY`). Files generated by earlier versions still contain the key, so redact or regenerate any you have shared, and consider rotating the key. Passing it in `QSV_LLM_APIKEY` (or `QSV_CKAN_TOKEN`) instead keeps it out of the command line, and out of `ps`, altogether.
- **`viz --geojson auto` (and `census:*`) handles an explicit `--feature-id-key` consistently on every path** ([#4735](https://github.com/dathere/qsv/issues/4735), [#4763](https://github.com/dathere/qsv/pull/4763)). [#4734](https://github.com/dathere/qsv/pull/4734) fixed this for state columns; three other paths still used the key auto picked:
- **County names and geocoded place names** resolve to Census GEOIDs, so they can only match on `properties.GEOID`. With another key (e.g. `properties.NAME`) they passed the coverage check and then drew a map that dropped every row. They now refuse the run and say to omit `--feature-id-key`. This applies to `viz choropleth` and `viz smart`.
Expand Down
9 changes: 6 additions & 3 deletions src/cmd/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -566,10 +566,13 @@ impl Args {
// Handle quick mode separately. --json implies --quiet per USAGE,
// so the row-number print is also suppressed when --json is set.
if self.flag_quick {
if !(self.flag_quiet || self.flag_json) {
eprintln!("{row_ctr}");
// with no match there is no row to report (row_ctr is then the row count)
if match_ctr > 0 {
if !(self.flag_quiet || self.flag_json) {
eprintln!("{row_ctr}");
}
info!("quick search first match at {row_ctr}");
}
info!("quick search first match at {row_ctr}");
if match_ctr == 0 && !self.flag_not_one {
return Err(CliError::NoMatch());
}
Expand Down
3 changes: 2 additions & 1 deletion src/cmd/searchset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,8 @@ impl Args {

if match_row_ctr == 0 && !flag_not_one {
return Err(CliError::NoMatch());
} else if flag_quick {
} else if flag_quick && match_row_ctr > 0 {
// with --not-one and no match, there is no row to report
if !self.flag_quiet {
eprintln!("{row_ctr}");
}
Expand Down
34 changes: 34 additions & 0 deletions tests/test_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,40 @@ fn search_match_quick() {
assert_eq!(got, "");
}

/// With no match, --quick reports no row: plain --quick fails silently (exit 1), and
/// --not-one succeeds silently. Before, the sequential path printed the row COUNT in
/// both cases, which a caller reads as the row of a match.
#[test]
fn search_quick_nomatch_reports_no_row() {
let wrk = Workdir::new("search_quick_nomatch_reports_no_row");
wrk.create("data.csv", data(true));

let mut cmd = wrk.command("search");
cmd.arg("^zzz").arg("--quick").arg("data.csv");
assert_eq!(wrk.stderr_on_error(&mut cmd), "");

let mut cmd = wrk.command("search");
cmd.arg("^zzz")
.arg("--quick")
.arg("--not-one")
.arg("data.csv");
let (got, got_err): (String, String) = wrk.stdout_and_stderr_on_success(&mut cmd);
assert_eq!(got_err, "");
assert_eq!(got, "");

// the indexed (parallel) path agrees
let mut idx = wrk.command("index");
idx.arg("data.csv");
wrk.assert_success(&mut idx);
let mut cmd = wrk.command("search");
cmd.arg("^zzz")
.arg("--quick")
.arg("--not-one")
.args(["--jobs", "2"])
.arg("data.csv");
assert_eq!(wrk.stderr_on_success(&mut cmd), "");
}

#[test]
fn search_match_quick_json() {
// regression: --quick + --json must not emit an unclosed JSON array `[`,
Expand Down
35 changes: 35 additions & 0 deletions tests/test_searchset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,41 @@ fn searchset_quick_nomatch() {
wrk.assert_err(&mut cmd);
}

/// With no match, --quick reports no row: plain --quick fails silently (exit 1), and
/// --not-one succeeds silently. Before, the sequential path printed the row COUNT with
/// --not-one, which a caller reads as the row of a match.
#[test]
fn searchset_quick_nomatch_reports_no_row() {
let wrk = Workdir::new("searchset_quick_nomatch_reports_no_row");
wrk.create("data.csv", data(true));
wrk.create("regexset.txt", regexset_no_match_file());

let mut cmd = wrk.command("searchset");
cmd.arg("regexset.txt").arg("--quick").arg("data.csv");
assert_eq!(wrk.stderr_on_error(&mut cmd), "");

let mut cmd = wrk.command("searchset");
cmd.arg("regexset.txt")
.arg("--quick")
.arg("--not-one")
.arg("data.csv");
let (got, got_err): (String, String) = wrk.stdout_and_stderr_on_success(&mut cmd);
assert_eq!(got_err, "");
assert_eq!(got, "");

// the indexed (parallel) path agrees
let mut idx = wrk.command("index");
idx.arg("data.csv");
wrk.assert_success(&mut idx);
let mut cmd = wrk.command("searchset");
cmd.arg("regexset.txt")
.arg("--quick")
.arg("--not-one")
.args(["--jobs", "2"])
.arg("data.csv");
assert_eq!(wrk.stderr_on_success(&mut cmd), "");
}

#[test]
fn searchset_unicode() {
let wrk = Workdir::new("searchset");
Expand Down
Loading