This is an automated email from the ASF dual-hosted git repository.

SpriCoder pushed a commit to branch fs/inner-view
in repository https://gitbox.apache.org/repos/asf/iotdb.git

commit eed096463f38890039105a1416a95906267e35dd
Author: spricoder <[email protected]>
AuthorDate: Sat May 2 20:05:48 2026 +0800

    update join
---
 .../plans/2026-04-29-cli-filesystem-mode.md        |  7 +-
 .../specs/2026-04-29-cli-filesystem-mode-design.md | 16 +++-
 .../org/apache/iotdb/cli/fs/FilesystemShell.java   | 82 +++++++++++++++++-
 .../iotdb/cli/fs/command/FilesystemCommand.java    |  7 ++
 .../cli/fs/command/FilesystemCommandParser.java    | 61 ++++++++++++++
 .../org/apache/iotdb/cli/utils/JlineUtils.java     |  4 +-
 .../apache/iotdb/cli/fs/FilesystemShellTest.java   | 97 ++++++++++++++++++++++
 .../fs/command/FilesystemCommandParserTest.java    | 58 +++++++++++++
 .../org/apache/iotdb/cli/utils/JlineUtilsTest.java |  7 ++
 9 files changed, 330 insertions(+), 9 deletions(-)

diff --git a/docs/superpowers/plans/2026-04-29-cli-filesystem-mode.md 
b/docs/superpowers/plans/2026-04-29-cli-filesystem-mode.md
index a39d65c880c..f31646c213b 100644
--- a/docs/superpowers/plans/2026-04-29-cli-filesystem-mode.md
+++ b/docs/superpowers/plans/2026-04-29-cli-filesystem-mode.md
@@ -98,6 +98,8 @@ These notes capture follow-up implementation experience for 
quickly resuming thi
     of the sidecar model and should resolve to `unknown` or a not-readable 
error.
   - `paste /db/table1.csv /db/table2.csv` -> read each regular file as lines 
and join
     corresponding lines with tabs, padding missing trailing lines with empty 
fields.
+  - `join -t, -1 2 -2 1 /db/table1.csv /db/table2.csv` -> Unix text join over 
regular file lines.
+    It is not SQL join, does not sort inputs, and treats headers as ordinary 
lines.
 - Table-mode write boundaries are intentionally narrow and only active with
   `--fs_write_mode enabled`:
   - `mkdir /db` creates a database.
@@ -178,6 +180,7 @@ Design decisions already approved:
 | `du <path>` | Print provider count and path; table sidecars use file-line 
counts. | `du /db/table.csv` |
 | `cut -d<delimiter> -f<fields> <path>` | Delimiter-based Unix field 
selection; supports lists and closed ranges. | `cut -d, -f2,3 /db/table.csv` |
 | `paste <path>...` | Print multiple regular files side by side, joining 
corresponding lines with tabs. | `paste /db/t1.csv /db/t2.csv` |
+| `join [-t delimiter] [-1 field] [-2 field] <path1> <path2>` | Inner join two 
readable files by 1-based text fields; inputs are expected to be sorted by the 
join key like Unix `join`. | `join -t, -1 2 -2 1 /db/t1.csv /db/t2.csv` |
 | `tree [-L depth] [path]` | Print descendants with indentation and names 
only. | `tree -L 2 /db` |
 | `mkdir <path>` | Write-gated; in table mode with writes enabled, creates a 
database. | `mkdir /newdb` |
 | `rm <path>` | Write-gated; in table mode with writes enabled, only table CSV 
drop is allowed. | `rm /db/table.csv` |
@@ -203,8 +206,8 @@ Good subagent tasks for this branch:
 - Reviewing docs for consistency after implementation changes.
 - Assigning natural layers independently: `FsPath`, command parser, tree 
provider, table provider,
   shell output, CLI dispatch, and docs.
-- Checking whether `ls`, `tree`, `cat`, `paste`, and `stat` still match the 
design document's Unix
-  output semantics.
+- Checking whether `ls`, `tree`, `cat`, `paste`, `join`, and `stat` still 
match the design
+  document's Unix output semantics.
 - Reviewing whether SQL mode remains the default and whether existing SQL CLI 
behavior is still
   isolated from filesystem mode.
 - Inspecting exception paths, especially interactive filesystem commands where 
one failed command
diff --git a/docs/superpowers/specs/2026-04-29-cli-filesystem-mode-design.md 
b/docs/superpowers/specs/2026-04-29-cli-filesystem-mode-design.md
index ed1d86742fd..8380042bf26 100644
--- a/docs/superpowers/specs/2026-04-29-cli-filesystem-mode-design.md
+++ b/docs/superpowers/specs/2026-04-29-cli-filesystem-mode-design.md
@@ -216,6 +216,7 @@ semantics wherever the same command exists. Provider 
support can still vary by d
 | `du <path>` | Print provider count plus path. Table sidecars use file-line 
counts. | `du /db/table.csv` |
 | `cut -d<delimiter> -f<fields> <path>` | Apply Unix delimiter-based field 
selection to each line. The delimiter must be one character. Field lists and 
closed ranges such as `2,3` and `1-2` are supported. | `cut -d, -f2,3 
/db/table.csv` |
 | `paste <path>...` | Read multiple regular files side by side and join 
corresponding lines with tabs. | `paste /db/t1.csv /db/t2.csv` |
+| `join [-t delimiter] [-1 field] [-2 field] <path1> <path2>` | Apply Unix 
inner join semantics to two readable files using 1-based fields. Default 
delimiter is whitespace; with `-t`, output uses the same delimiter. Inputs are 
expected to be sorted by join key. | `join -t, -1 2 -2 1 /db/t1.csv /db/t2.csv` 
|
 | `tree [-L depth] [path]` | Recursively print descendants with indentation 
and names only. `-L` limits recursion depth. | `tree -L 2 /db` |
 | `mkdir <path>` | Write-gated command. With table mode and `--fs_write_mode 
enabled`, `mkdir /db` creates a database. Otherwise it returns a read-only or 
unsupported error. | `mkdir /newdb` |
 | `rm <path>` | Write-gated command. With table mode and `--fs_write_mode 
enabled`, only `rm /db/table.csv` is allowed and maps to table drop. | `rm 
/db/table.csv` |
@@ -254,6 +255,7 @@ Raw SQL should be run in the default SQL access mode.
 | `cat /db/table.csv` | `SELECT * FROM db.table LIMIT <limit>`, formatted as 
CSV records. |
 | `cut -d, -f2,3 /db/table.csv` | Delimiter-based text field projection over 
the CSV records. |
 | `paste /db/t1.csv /db/t2.csv` | Read each regular file as CSV/text lines and 
join corresponding lines with tabs. |
+| `join -t, -1 2 -2 1 /db/t1.csv /db/t2.csv` | Read both regular files as text 
lines and perform Unix-style inner join on the selected fields. |
 | `tee -a /db/table.csv` | Parse CSV input with Apache Commons CSV, validate 
columns and required `time`, then execute chunked `INSERT INTO db.table(...) 
VALUES ...`. |
 | `cat /db/table.schema` | `DESC db.table DETAILS`, formatted as CSV with 
IoTDB result columns preserved. |
 | `cat /db/table.meta` | `SHOW TABLES DETAILS FROM db`, filtered to the table 
and formatted as CSV with IoTDB result columns preserved. |
@@ -275,6 +277,9 @@ exposing internal implementation types or Java debug-style 
structures in normal
   or introduce table-specific column-selection flags.
 - `paste` prints multiple regular files side by side as tab-separated values. 
It does not select
   database columns.
+- `join` performs the standard Unix text join over exactly two readable files. 
It is not SQL join,
+  does not sort inputs, and treats CSV headers as ordinary input lines. 
Default delimiter handling
+  follows Unix whitespace splitting; `-t,` is the CSV sidecar form.
 - `less` and `more` are currently non-interactive read aliases with the 
default read limit.
 - `stat` is the command that may expose typed metadata, because Unix `stat` is 
explicitly about
   object metadata.
@@ -314,6 +319,10 @@ are unknown in the sidecar model and must not trigger 
table or column SQL reads.
 corresponding lines with tabs. It must not become a database-specific `select` 
command or
 `cat --columns` dialect.
 
+`join` also remains Unix-like: users pass exactly two regular file paths and 
optional text field
+selection flags. It must not become a SQL join alias or infer database 
relationships from table
+metadata.
+
 For CSV-first table files, multi-column projection should prefer Unix `cut` 
syntax such as
 `cut -d, -f2,3 /db/table.csv`. The implementation may later optimize this 
internally, but the
 public interface must remain the standard `cut` form.
@@ -425,7 +434,7 @@ New code should live under `org.apache.iotdb.cli.fs`.
 
 - `FilesystemShell`: filesystem-mode command loop and `-e` single-command 
execution.
 - `command/*`: command parsing and command value objects for filesystem 
commands such as `pwd`,
-  `ls`, `cd`, `stat`, `cat`, `cut`, `paste`, `tree`, and `help`.
+  `ls`, `cd`, `stat`, `cat`, `cut`, `paste`, `join`, `tree`, and `help`.
 - `path/FsPath`: path normalization and resolution for absolute and relative 
paths.
 - `node/FsNode`, `node/FsNodeType`, `node/FsNodeMetadata`: typed metadata 
model.
 - `provider/FilesystemSchemaProvider`: schema and data read provider interface.
@@ -455,7 +464,8 @@ Filesystem mode separates reads from mutations. The schema 
provider owns read op
 - `tail(FsPath path)` / `tailLines(FsPath path)` where the provider supports 
tail
 - `count(FsPath path)` where the provider supports logical count
 - `read(List<FsPath> paths)` remains available for providers that need 
optimized multi-path reads,
-  but table mode sidecar `paste` is implemented as regular file line joining 
in the shell.
+  but table mode sidecar `paste` and `join` are implemented as regular file 
text operations in the
+  shell.
 
 The mutation provider owns the current write-gated operations:
 
@@ -537,7 +547,7 @@ Unit tests should cover the behavior without needing a live 
IoTDB instance where
 - `FsPath` tests for absolute paths, relative paths, `.`, `..`, empty input, 
and attempts to move
   above root.
 - Command parser tests for valid and invalid forms of all supported shell 
commands, including
-  listing options, `head`/`tail` limits, `wc -l`, `find -name`, `cut`, 
`paste`, write-gated
+  listing options, `head`/`tail` limits, `wc -l`, `find -name`, `cut`, 
`paste`, `join`, write-gated
   commands, and the parser-only `sql` form.
 - Provider tests with a mocked `SqlExecutor`, verifying tree-mode path-to-SQL 
mapping.
 - Provider tests with a mocked `SqlExecutor`, verifying table-mode path-to-SQL 
mapping, CSV
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java
index 051c96b5b35..407744fc03c 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java
@@ -43,6 +43,7 @@ import java.nio.charset.StandardCharsets;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Pattern;
@@ -53,8 +54,8 @@ public class FilesystemShell {
   private static final List<String> COMMANDS =
       Arrays.asList(
           "pwd", "ls", "ll", "cd", "stat", "cat", "head", "tail", "wc", 
"grep", "find", "less",
-          "more", "file", "du", "mkdir", "rm", "mv", "cut", "paste", "tree", 
"help", "exit", "quit",
-          "tee");
+          "more", "file", "du", "mkdir", "rm", "mv", "cut", "paste", "join", 
"tree", "help", "exit",
+          "quit", "tee");
 
   private final CliContext ctx;
   private final FilesystemSchemaProvider provider;
@@ -138,6 +139,9 @@ public class FilesystemShell {
       case PASTE:
         printPaste(command.getPaths());
         return true;
+      case JOIN:
+        printJoin(command.getPaths(), command.getOption(), 
command.getPattern());
+        return true;
       case TEE:
         append(command.getPath(), false);
         return true;
@@ -360,6 +364,27 @@ public class FilesystemShell {
     }
   }
 
+  private void printJoin(List<String> paths, String delimiter, String fields) 
throws SQLException {
+    int[] joinFields = joinFields(fields);
+    List<String> leftLines = readableLines(resolve(paths.get(0)), 
DEFAULT_READ_LIMIT);
+    List<String> rightLines = readableLines(resolve(paths.get(1)), 
DEFAULT_READ_LIMIT);
+    Map<String, List<String[]>> rightRows = joinRowsByKey(rightLines, 
delimiter, joinFields[1]);
+
+    for (String leftLine : leftLines) {
+      String[] left = splitJoinFields(leftLine, delimiter);
+      if (!hasField(left, joinFields[0])) {
+        continue;
+      }
+      List<String[]> matches = rightRows.get(left[joinFields[0] - 1]);
+      if (matches == null) {
+        continue;
+      }
+      for (String[] right : matches) {
+        ctx.getPrinter().println(joinLine(left, right, joinFields[0], 
joinFields[1], delimiter));
+      }
+    }
+  }
+
   private List<String> readableLines(FsPath path, int limit) throws 
SQLException {
     if (isTextFile(path)) {
       return provider.readLines(path, limit);
@@ -528,6 +553,7 @@ public class FilesystemShell {
     ctx.getPrinter().println("mv <source> <target>");
     ctx.getPrinter().println("cut -d<delimiter> -f<fields> <path>");
     ctx.getPrinter().println("paste <path>...");
+    ctx.getPrinter().println("join [-t delimiter] [-1 field] [-2 field] 
<path1> <path2>");
     ctx.getPrinter().println("tee -a <path>");
     ctx.getPrinter().println("tree [-L depth] [path]");
     ctx.getPrinter().println("exit");
@@ -596,6 +622,58 @@ public class FilesystemShell {
     return builder.toString();
   }
 
+  private static int[] joinFields(String fields) {
+    String[] values = fields.split(",", -1);
+    return new int[] {parsePositiveInt(values[0]), 
parsePositiveInt(values[1])};
+  }
+
+  private static Map<String, List<String[]>> joinRowsByKey(
+      List<String> lines, String delimiter, int keyField) {
+    Map<String, List<String[]>> rowsByKey = new LinkedHashMap<>();
+    for (String line : lines) {
+      String[] fields = splitJoinFields(line, delimiter);
+      if (!hasField(fields, keyField)) {
+        continue;
+      }
+      String key = fields[keyField - 1];
+      rowsByKey.computeIfAbsent(key, ignored -> new ArrayList<>()).add(fields);
+    }
+    return rowsByKey;
+  }
+
+  private static String[] splitJoinFields(String line, String delimiter) {
+    if (delimiter.isEmpty()) {
+      String trimmed = line.trim();
+      if (trimmed.isEmpty()) {
+        return new String[0];
+      }
+      return trimmed.split("\\s+");
+    }
+    return line.split(Pattern.quote(delimiter), -1);
+  }
+
+  private static boolean hasField(String[] fields, int fieldNumber) {
+    return fieldNumber > 0 && fieldNumber <= fields.length;
+  }
+
+  private static String joinLine(
+      String[] left, String[] right, int leftKeyField, int rightKeyField, 
String delimiter) {
+    String outputDelimiter = delimiter.isEmpty() ? " " : delimiter;
+    List<String> output = new ArrayList<>();
+    output.add(left[leftKeyField - 1]);
+    addNonKeyFields(output, left, leftKeyField);
+    addNonKeyFields(output, right, rightKeyField);
+    return String.join(outputDelimiter, output);
+  }
+
+  private static void addNonKeyFields(List<String> output, String[] fields, 
int keyField) {
+    for (int i = 0; i < fields.length; i++) {
+      if (i != keyField - 1) {
+        output.add(fields[i]);
+      }
+    }
+  }
+
   private static boolean[] selectedFields(String fields, int fieldCount) {
     boolean[] selected = new boolean[fieldCount];
     for (String field : fields.split(",")) {
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommand.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommand.java
index 2f77a787a54..7d379236a78 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommand.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommand.java
@@ -45,6 +45,7 @@ public class FilesystemCommand {
     MV,
     CUT,
     PASTE,
+    JOIN,
     TEE,
     TREE,
     SQL,
@@ -124,6 +125,12 @@ public class FilesystemCommand {
         Type.CUT, path, Collections.singletonList(path), -1, -1, delimiter, 
fields, "", "");
   }
 
+  public static FilesystemCommand join(String delimiter, String fields, 
List<String> paths) {
+    String path = paths.isEmpty() ? "" : paths.get(0);
+    return new FilesystemCommand(
+        Type.JOIN, path, Collections.unmodifiableList(paths), -1, -1, 
delimiter, fields, "", "");
+  }
+
   public static FilesystemCommand tree(String path, int depth) {
     return new FilesystemCommand(
         Type.TREE, path, Collections.singletonList(path), depth, -1, "", "", 
"", "");
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParser.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParser.java
index 75992cf0fc7..565b9bad58f 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParser.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParser.java
@@ -27,6 +27,7 @@ public class FilesystemCommandParser {
 
   private static final String DEFAULT_PATH = ".";
   private static final String DEFAULT_CUT_DELIMITER = "\t";
+  private static final String DEFAULT_JOIN_DELIMITER = "";
   private static final int DEFAULT_TREE_DEPTH = Integer.MAX_VALUE;
   private static final int DEFAULT_HEAD_LIMIT = 10;
 
@@ -111,6 +112,9 @@ public class FilesystemCommandParser {
     if ("paste".equals(command)) {
       return parsePaste(tokens);
     }
+    if ("join".equals(command)) {
+      return parseJoin(tokens);
+    }
     if ("tee".equals(command)) {
       return parseTee(tokens);
     }
@@ -139,6 +143,63 @@ public class FilesystemCommandParser {
     return FilesystemCommand.paths(FilesystemCommand.Type.PASTE, paths);
   }
 
+  private static FilesystemCommand parseJoin(String[] tokens) {
+    String delimiter = DEFAULT_JOIN_DELIMITER;
+    int leftField = 1;
+    int rightField = 1;
+    List<String> paths = new ArrayList<>();
+
+    for (int i = 1; i < tokens.length; i++) {
+      String token = tokens[i];
+      if ("-t".equals(token)) {
+        if (i + 1 >= tokens.length) {
+          return invalidJoinUsage();
+        }
+        delimiter = tokens[++i];
+      } else if (token.startsWith("-t") && token.length() > 2) {
+        delimiter = token.substring(2);
+      } else if ("-1".equals(token)) {
+        if (i + 1 >= tokens.length) {
+          return invalidJoinUsage();
+        }
+        leftField = parseJoinField(tokens[++i]);
+      } else if ("-2".equals(token)) {
+        if (i + 1 >= tokens.length) {
+          return invalidJoinUsage();
+        }
+        rightField = parseJoinField(tokens[++i]);
+      } else if (token.startsWith("-")) {
+        return FilesystemCommand.invalid("Unsupported join option: " + token);
+      } else {
+        paths.add(token);
+      }
+    }
+
+    if (delimiter.length() > 1) {
+      return FilesystemCommand.invalid("Join delimiter must be a single 
character");
+    }
+    if (leftField <= 0 || rightField <= 0) {
+      return FilesystemCommand.invalid("Invalid join field");
+    }
+    if (paths.size() != 2) {
+      return invalidJoinUsage();
+    }
+    return FilesystemCommand.join(delimiter, leftField + "," + rightField, 
paths);
+  }
+
+  private static int parseJoinField(String value) {
+    try {
+      return Integer.parseInt(value);
+    } catch (NumberFormatException e) {
+      return -1;
+    }
+  }
+
+  private static FilesystemCommand invalidJoinUsage() {
+    return FilesystemCommand.invalid(
+        "Usage: join [-t delimiter] [-1 field] [-2 field] <path1> <path2>");
+  }
+
   private static FilesystemCommand parseTee(String[] tokens) {
     if (tokens.length != 3) {
       return FilesystemCommand.invalid("Usage: tee -a <path>");
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/utils/JlineUtils.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/utils/JlineUtils.java
index 704e10f6600..e773f7b521a 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/utils/JlineUtils.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/utils/JlineUtils.java
@@ -153,8 +153,8 @@ public class JlineUtils {
     if ("filesystem".equalsIgnoreCase(accessMode)) {
       return new StringsCompleter(
           "pwd", "ls", "ll", "cd", "stat", "cat", "head", "tail", "wc", 
"grep", "find", "less",
-          "more", "file", "du", "mkdir", "rm", "mv", "cut", "paste", "tee", 
"tree", "help", "exit",
-          "quit");
+          "more", "file", "du", "mkdir", "rm", "mv", "cut", "paste", "join", 
"tee", "tree", "help",
+          "exit", "quit");
     }
     return new StringsCompleter(SQL_KEYWORDS);
   }
diff --git 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/FilesystemShellTest.java
 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/FilesystemShellTest.java
index cc442be8be3..6cb435ede80 100644
--- 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/FilesystemShellTest.java
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/FilesystemShellTest.java
@@ -576,6 +576,96 @@ public class FilesystemShellTest {
     verify(provider).readLines(FsPath.absolute("/db1/table2.csv"), 20);
   }
 
+  @Test
+  public void executeJoinMatchesCsvRowsByFirstField() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 20))
+        .thenReturn(Arrays.asList("key,value", "a,10", "b,20", "c,30"));
+    when(provider.readLines(FsPath.absolute("/db1/table2.csv"), 20))
+        .thenReturn(Arrays.asList("key,status", "a,ok", "b,bad", "d,missing"));
+
+    assertTrue(shell.execute("join -t, /db1/table1.csv /db1/table2.csv"));
+
+    assertEquals(
+        "key,value,status"
+            + System.lineSeparator()
+            + "a,10,ok"
+            + System.lineSeparator()
+            + "b,20,bad"
+            + System.lineSeparator(),
+        out.toString());
+    verify(provider).readLines(FsPath.absolute("/db1/table1.csv"), 20);
+    verify(provider).readLines(FsPath.absolute("/db1/table2.csv"), 20);
+  }
+
+  @Test
+  public void executeJoinSupportsDifferentKeyFields() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 20))
+        .thenReturn(Arrays.asList("time,key,value", "1,a,10", "2,b,20"));
+    when(provider.readLines(FsPath.absolute("/db1/table2.csv"), 20))
+        .thenReturn(Arrays.asList("key,status", "a,ok", "b,bad"));
+
+    assertTrue(shell.execute("join -t, -1 2 -2 1 /db1/table1.csv 
/db1/table2.csv"));
+
+    assertEquals(
+        "key,time,value,status"
+            + System.lineSeparator()
+            + "a,1,10,ok"
+            + System.lineSeparator()
+            + "b,2,20,bad"
+            + System.lineSeparator(),
+        out.toString());
+  }
+
+  @Test
+  public void executeJoinPrintsCartesianMatchesForDuplicateKeys() throws 
SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 20))
+        .thenReturn(Arrays.asList("a,10", "a,11"));
+    when(provider.readLines(FsPath.absolute("/db1/table2.csv"), 20))
+        .thenReturn(Arrays.asList("a,ok", "a,good"));
+
+    assertTrue(shell.execute("join -t, /db1/table1.csv /db1/table2.csv"));
+
+    assertEquals(
+        "a,10,ok"
+            + System.lineSeparator()
+            + "a,10,good"
+            + System.lineSeparator()
+            + "a,11,ok"
+            + System.lineSeparator()
+            + "a,11,good"
+            + System.lineSeparator(),
+        out.toString());
+  }
+
+  @Test
+  public void executeJoinSkipsRowsWithoutKeyOrMatch() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 20))
+        .thenReturn(Arrays.asList("a,10", "missing-key", "b,20"));
+    when(provider.readLines(FsPath.absolute("/db1/table2.csv"), 20))
+        .thenReturn(Arrays.asList("a,ok", "c,unused"));
+
+    assertTrue(shell.execute("join -t, -1 2 -2 1 /db1/table1.csv 
/db1/table2.csv"));
+
+    assertEquals("", out.toString());
+  }
+
+  @Test
+  public void executeJoinUsesReadableRowsForNonTextPath() throws SQLException {
+    when(provider.read(FsPath.absolute("/root/sg/d1/s1"), 20))
+        .thenReturn(
+            Arrays.asList(SqlRow.of("Time", "1", "s1", "10"), 
SqlRow.of("Time", "2", "s1", "20")));
+    when(provider.read(FsPath.absolute("/root/sg/d1/s2"), 20))
+        .thenReturn(
+            Arrays.asList(
+                SqlRow.of("Time", "1", "s2", "ok"), SqlRow.of("Time", "3", 
"s2", "skip")));
+
+    assertTrue(shell.execute("join /root/sg/d1/s1 /root/sg/d1/s2"));
+
+    assertEquals("1 10 ok" + System.lineSeparator(), out.toString());
+    verify(provider).read(FsPath.absolute("/root/sg/d1/s1"), 20);
+    verify(provider).read(FsPath.absolute("/root/sg/d1/s2"), 20);
+  }
+
   @Test
   public void executeCutSelectsCsvFieldsByNumber() throws SQLException {
     when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 20))
@@ -623,6 +713,13 @@ public class FilesystemShellTest {
     verify(provider).list(FsPath.absolute("/"));
   }
 
+  @Test
+  public void completerCompletesJoinCommand() {
+    List<String> values = complete(shell.createCompleter(), "j");
+
+    assertTrue(values.contains("join"));
+  }
+
   private static List<String> complete(Completer completer, String line) {
     ParsedLine parsedLine = new DefaultParser().parse(line, line.length());
     List<Candidate> candidates = new ArrayList<>();
diff --git 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParserTest.java
 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParserTest.java
index 31971bf6a1e..df0bf2ffdc0 100644
--- 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParserTest.java
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParserTest.java
@@ -168,6 +168,64 @@ public class FilesystemCommandParserTest {
     assertEquals("/db1/table2.csv", command.getPaths().get(1));
   }
 
+  @Test
+  public void parseJoinPathsUsesDefaultDelimiterAndFields() {
+    FilesystemCommand command =
+        FilesystemCommandParser.parse("join /db1/table1.csv /db1/table2.csv");
+
+    assertEquals(FilesystemCommand.Type.JOIN, command.getType());
+    assertEquals("", command.getOption());
+    assertEquals("1,1", command.getPattern());
+    assertEquals(2, command.getPaths().size());
+    assertEquals("/db1/table1.csv", command.getPaths().get(0));
+    assertEquals("/db1/table2.csv", command.getPaths().get(1));
+  }
+
+  @Test
+  public void parseJoinDelimiterAndFields() {
+    FilesystemCommand command =
+        FilesystemCommandParser.parse("join -t, -1 2 -2 1 /db1/table1.csv 
/db1/table2.csv");
+
+    assertEquals(FilesystemCommand.Type.JOIN, command.getType());
+    assertEquals(",", command.getOption());
+    assertEquals("2,1", command.getPattern());
+    assertEquals("/db1/table1.csv", command.getPaths().get(0));
+    assertEquals("/db1/table2.csv", command.getPaths().get(1));
+  }
+
+  @Test
+  public void parseJoinSeparatedDelimiter() {
+    FilesystemCommand command =
+        FilesystemCommandParser.parse("join -t , /db1/table1.csv 
/db1/table2.csv");
+
+    assertEquals(FilesystemCommand.Type.JOIN, command.getType());
+    assertEquals(",", command.getOption());
+    assertEquals("1,1", command.getPattern());
+  }
+
+  @Test
+  public void parseJoinRejectsInvalidArguments() {
+    assertEquals(FilesystemCommand.Type.INVALID, 
FilesystemCommandParser.parse("join").getType());
+    assertEquals(
+        FilesystemCommand.Type.INVALID,
+        FilesystemCommandParser.parse("join /db1/table1.csv").getType());
+    assertEquals(
+        FilesystemCommand.Type.INVALID,
+        FilesystemCommandParser.parse("join /db1/a.csv /db1/b.csv 
/db1/c.csv").getType());
+    assertEquals(
+        FilesystemCommand.Type.INVALID,
+        FilesystemCommandParser.parse("join -t /db1/a.csv 
/db1/b.csv").getType());
+    assertEquals(
+        FilesystemCommand.Type.INVALID,
+        FilesystemCommandParser.parse("join -t:: /db1/a.csv 
/db1/b.csv").getType());
+    assertEquals(
+        FilesystemCommand.Type.INVALID,
+        FilesystemCommandParser.parse("join -1 0 /db1/a.csv 
/db1/b.csv").getType());
+    assertEquals(
+        FilesystemCommand.Type.INVALID,
+        FilesystemCommandParser.parse("join -x /db1/a.csv 
/db1/b.csv").getType());
+  }
+
   @Test
   public void parseCutDelimiterFieldsAndPath() {
     FilesystemCommand command = FilesystemCommandParser.parse("cut -d, -f2,3 
/db1/table1.csv");
diff --git 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/utils/JlineUtilsTest.java 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/utils/JlineUtilsTest.java
index 6cc0fd9dd56..d8d2ac84da8 100644
--- 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/utils/JlineUtilsTest.java
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/utils/JlineUtilsTest.java
@@ -51,6 +51,13 @@ public class JlineUtilsTest {
     assertFalse(values.contains("CREATE"));
   }
 
+  @Test
+  public void filesystemCompleterIncludesJoinCommand() {
+    List<String> values = complete(JlineUtils.createCompleter("filesystem"), 
"j");
+
+    assertTrue(values.contains("join"));
+  }
+
   private static List<String> complete(Completer completer, String line) {
     ParsedLine parsedLine = new DefaultParser().parse(line, line.length());
     List<Candidate> candidates = new ArrayList<>();

Reply via email to