Repository: dathere/qsv (language: rust)

Issue:
BUG join --right-semi and --right-anti header column issue if the input files have different column order
---
**Describe the bug**
There are issues with the right-semi and right-anti command for join if the column order of file1 differs from file 2. The key column order of file1 seems to be used for the header for file 2.

**To Reproduce**
Steps to reproduce the behavior:

**file1.csv**
```
id,company_id,art_no
1,A1,1
2,A2,2
3,A3,3
```
**file2.csv**
```
id,art_no,company_id
1,1,B1
2,2,B2
3,5,B3
```
**command:**
`qsv join --right-semi art_no file1.csv art_no file2.csv -o right-semi.csv`

**result right-semi.csv:**
```
id,company_id,art_no
1,1,B1
2,2,B2
```

**Expected result:**
```
id,art_no,company_id
1,1,B1
2,2,B2
```

**command:**
`qsv join --right-anti art_no file1.csv art_no file2.csv -o right-anti.csv`

**result right-anti.csv:**
```
id,company_id,art_no
3,5,B3
```
**Expected result:**
```
id,art_no,company_id
3,5,B3
```

**Desktop (please complete the following information):**
 qsv 2.0.0-mimalloc-apply;fetch;foreach;geocode;Luau 0.653;prompt;to;polars-0.45.1-py-1.19.0-e0ef7f9;self_update-8-8;4.75 GiB-2.00 GiB-2.46 GiB-5.94 GiB (x86_64-pc-windows-msvc compiled with Rust 1.83) prebuilt

**Additional context**
Workaround:
`qsv join --left-semi art_no file2.csv art_no file1.csv -o right-semi.csv` works correctly
`qsv join --left-anti art_no file2.csv art_no file1.csv -o right-anti.csv` works correctly

joinp works correctly with --right-semi and --right-anti.



Discussion / hints:

Fix patch:
diff --git a/src/cmd/join.rs b/src/cmd/join.rs
index 3b8901560..2205a8aac 100644
--- a/src/cmd/join.rs
+++ b/src/cmd/join.rs
@@ -165,19 +165,19 @@ pub fn run(argv: &[&str]) -> CliResult<()> {
         // right anti join
         // swap left and right data sets and run left anti join
         (false, false, false, false, true, false, false, false) => {
-            state.write_headers1()?;
             let mut swapped_join = state;
             swap(&mut swapped_join.rdr1, &mut swapped_join.rdr2);
             swap(&mut swapped_join.sel1, &mut swapped_join.sel2);
+            swapped_join.write_headers1()?;
             swapped_join.left_join(true)
         },
         // right semi join
         // swap left and right data sets and run left semi join
         (false, false, false, false, false, true, false, false) => {
-            state.write_headers1()?;
             let mut swapped_join = state;
             swap(&mut swapped_join.rdr1, &mut swapped_join.rdr2);
             swap(&mut swapped_join.sel1, &mut swapped_join.sel2);
+            swapped_join.write_headers1()?;
             swapped_join.left_join(false)
         },
         // full outer join
@@ -471,9 +471,9 @@ impl<R: io::Read + io::Seek> ValueIndex<R> {
     ///
     /// * `rdr` - A CSV reader that implements Read + Seek
     /// * `sel` - A Selection that specifies which columns to index
-    /// * `casei` - If true, values are compared case-insensitively
-    /// * `zerosi` - If true, leading zeros are removed
-    /// * `nulls` - If true, rows with empty values are included in the index
+    /// * `casei` - If true, indexed values are compared case-insensitively
+    /// * `zerosi` - If true, indexed values are compared without leading zeros
+    /// * `nulls` - If true, indexed rows with empty values are included
     ///
     /// # Returns
     ///
@@ -486,7 +486,7 @@ impl<R: io::Read + io::Seek> ValueIndex<R> {
     ///
     /// - Header rows are included in the byte offset index but not the value index
     /// - Values are trimmed and optionally converted to lowercase before indexing
-    /// - Rows with empty values are skipped unless nulls=true
+    /// - Rows with empty indexed values are skipped unless nulls=true
     fn new(
         mut rdr: csv::Reader<R>,
         sel: &Selection,


Test patch:
diff --git a/tests/test_join.rs b/tests/test_join.rs
index 43425a1ff..5d8d3d23d 100644
--- a/tests/test_join.rs
+++ b/tests/test_join.rs
@@ -380,7 +380,7 @@ join_test!(
     |wrk: Workdir, mut cmd: process::Command, headers: bool| {
         cmd.arg("--right-semi");
         let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
-        let expected = make_rows(
+        let mut expected = make_rows(
             headers,
             true,
             vec![
@@ -389,6 +389,9 @@ join_test!(
                 svec!["Buffalo", "Ralph Wilson Stadium"],
             ],
         );
+        if headers {
+            expected[0] = svec!["city", "place"];
+        }
         assert_eq!(got, expected);
     }
 );
@@ -398,7 +401,7 @@ join_test!(
     |wrk: Workdir, mut cmd: process::Command, headers: bool| {
         cmd.arg("--right-semi").arg("--ignore-case");
         let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
-        let expected = make_rows(
+        let mut expected = make_rows(
             headers,
             true,
             vec![
@@ -408,6 +411,9 @@ join_test!(
                 svec!["BOSTON", "BOSTON COMMON"],
             ],
         );
+        if headers {
+            expected[0] = svec!["city", "place"];
+        }
         assert_eq!(got, expected);
     }
 );
@@ -417,7 +423,7 @@ join_test!(
     |wrk: Workdir, mut cmd: process::Command, headers: bool| {
         cmd.arg("--right-anti");
         let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
-        let expected = make_rows(
+        let mut expected = make_rows(
             headers,
             true,
             vec![
@@ -425,6 +431,9 @@ join_test!(
                 svec!["BOSTON", "BOSTON COMMON"],
             ],
         );
+        if headers {
+            expected[0] = svec!["city", "place"];
+        }
         assert_eq!(got, expected);
     }
 );
@@ -434,7 +443,10 @@ join_test!(
     |wrk: Workdir, mut cmd: process::Command, headers: bool| {
         cmd.arg("--right-anti").arg("--ignore-case");
         let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
-        let expected = make_rows(headers, true, vec![svec!["Orlando", "Disney World"]]);
+        let mut expected = make_rows(headers, true, vec![svec!["Orlando", "Disney World"]]);
+        if headers {
+            expected[0] = svec!["city", "place"];
+        }
         assert_eq!(got, expected);
     }
 );
@@ -902,3 +914,37 @@ fn join_with_whitespace() {
     ];
     assert_eq!(got, expected);
 }
+
+#[test]
+fn join_right_semi_header_order_issue_2434() {
+    let wrk = Workdir::new("join_right_semi_header_order_issue_2434");
+    wrk.create(
+        "file1.csv",
+        vec![
+            svec!["id", "company_id", "art_no"],
+            svec!["1", "A1", "1"],
+            svec!["2", "A2", "2"],
+            svec!["3", "A3", "3"],
+        ],
+    );
+    wrk.create(
+        "file2.csv",
+        vec![
+            svec!["id", "art_no", "company_id"],
+            svec!["1", "1", "B1"],
+            svec!["2", "2", "B2"],
+            svec!["3", "5", "B3"],
+        ],
+    );
+
+    let mut cmd = wrk.command("join");
+    cmd.args(["--right-semi", "art_no", "file1.csv", "art_no", "file2.csv"]);
+
+    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
+    let expected = vec![
+        svec!["id", "art_no", "company_id"],
+        svec!["1", "1", "B1"],
+        svec!["2", "2", "B2"],
+    ];
+    assert_eq!(got, expected);
+}