You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iotdb.apache.org by sp...@apache.org on 2026/05/16 05:47:31 UTC

(iotdb) branch fs/inner-view updated (600b516f8ae -> 33a3a30d763)

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

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


    from 600b516f8ae add head
     new 08ad58029e7 add write
     new d0672836c6f use sidecat file
     new 06842599445 add tee
     new 0065d16a0c6 check old version
     new eed096463f3 update join
     new 33a3a30d763 add unix and extend

The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../plans/2026-04-29-cli-filesystem-mode.md        | 169 +++++-
 .../2026-05-02-cli-fs-unix-standard-extensions.md  |  55 ++
 .../specs/2026-04-29-cli-filesystem-mode-design.md | 374 +++++++++---
 .../java/org/apache/iotdb/cli/AbstractCli.java     |  35 ++
 .../src/main/java/org/apache/iotdb/cli/Cli.java    |  17 +-
 .../org/apache/iotdb/cli/fs/FilesystemShell.java   | 551 +++++++++++++++++-
 .../iotdb/cli/fs/command/FilesystemCommand.java    |  73 ++-
 .../cli/fs/command/FilesystemCommandParser.java    | 285 +++++++++-
 .../org/apache/iotdb/cli/fs/node/FsNodeType.java   |   5 +-
 ...ovider.java => FilesystemMutationProvider.java} |  23 +-
 .../cli/fs/provider/FilesystemSchemaProvider.java  |  16 +
 .../cli/fs/provider/TableCsvAppendPlanner.java     | 286 ++++++++++
 .../provider/TableFilesystemMutationProvider.java  | 165 ++++++
 .../fs/provider/TableFilesystemSchemaProvider.java | 361 ++++++++++--
 .../iotdb/cli/fs/provider/TableFilesystemSql.java  |  17 +-
 .../fs/provider/TreeFilesystemSchemaProvider.java  |  29 +
 .../UnsupportedFilesystemMutationProvider.java     |  69 +++
 .../apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java   |   7 +
 .../org/apache/iotdb/cli/fs/sql/SqlExecutor.java   |   2 +
 .../org/apache/iotdb/cli/utils/JlineUtils.java     |   4 +-
 .../java/org/apache/iotdb/cli/AbstractCliTest.java |  29 +
 .../apache/iotdb/cli/CliFilesystemModeTest.java    |  35 +-
 .../apache/iotdb/cli/fs/FilesystemShellTest.java   | 625 +++++++++++++++++++--
 .../fs/command/FilesystemCommandParserTest.java    | 252 ++++++++-
 .../TableFilesystemMutationProviderTest.java       | 229 ++++++++
 .../TableFilesystemSchemaProviderTest.java         | 260 +++++++--
 .../iotdb/cli/fs/sql/JdbcSqlExecutorTest.java      |  10 +
 .../org/apache/iotdb/cli/utils/JlineUtilsTest.java |   7 +
 28 files changed, 3668 insertions(+), 322 deletions(-)
 create mode 100644 docs/superpowers/plans/2026-05-02-cli-fs-unix-standard-extensions.md
 copy iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/{FilesystemSchemaProvider.java => FilesystemMutationProvider.java} (63%)
 create mode 100644 iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableCsvAppendPlanner.java
 create mode 100644 iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
 copy iotdb-core/datanode/src/main/java/org/apache/iotdb/db/utils/windowing/exception/WindowingException.java => iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSql.java (71%)
 create mode 100644 iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
 create mode 100644 iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java


(iotdb) 04/06: check old version

Posted by sp...@apache.org.
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 0065d16a0c6caec5f340b760a1df6b4e8d0bac22
Author: spricoder <sp...@qq.com>
AuthorDate: Thu Apr 30 19:03:40 2026 +0800

    check old version
---
 .../plans/2026-04-29-cli-filesystem-mode.md        |  41 +++--
 .../specs/2026-04-29-cli-filesystem-mode-design.md |  70 ++++----
 .../org/apache/iotdb/cli/fs/FilesystemShell.java   |  68 ++++++-
 .../org/apache/iotdb/cli/fs/node/FsNodeType.java   |   2 -
 .../cli/fs/provider/TableCsvAppendPlanner.java     |  30 +---
 .../provider/TableFilesystemMutationProvider.java  |  21 ++-
 .../fs/provider/TableFilesystemSchemaProvider.java | 174 +++++++-----------
 .../TableFilesystemSql.java}                       |  31 ++--
 .../apache/iotdb/cli/CliFilesystemModeTest.java    |   9 +-
 .../apache/iotdb/cli/fs/FilesystemShellTest.java   | 197 ++++++++++++++-------
 .../fs/command/FilesystemCommandParserTest.java    |  59 +++---
 .../TableFilesystemMutationProviderTest.java       |  23 ++-
 .../TableFilesystemSchemaProviderTest.java         | 190 ++++++++++++--------
 13 files changed, 526 insertions(+), 389 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 0a85d3525b5..a39d65c880c 100644
--- a/docs/superpowers/plans/2026-04-29-cli-filesystem-mode.md
+++ b/docs/superpowers/plans/2026-04-29-cli-filesystem-mode.md
@@ -73,8 +73,8 @@ These notes capture follow-up implementation experience for quickly resuming thi
   - `ls -a` and `ll -a` include `.` and `..`; `ll` reuses `ls` option parsing as the long-listing
     alias.
   - `tree` prints indented names only.
-  - `cat /db/table.csv` prints CSV records. Legacy compatibility table/column paths and `paste`
-    continue to print tab-separated values.
+  - `cat /db/table.csv`, `cat /db/table.schema`, and `cat /db/table.meta` print regular file
+    content as CSV lines.
   - `cut -d, -f2,3 /db/table.csv` is the Unix-compatible multi-field projection form for CSV-first
     table files. It is delimiter-based text cutting, not CSV quote parsing and not a database
     column-selection dialect.
@@ -82,8 +82,8 @@ These notes capture follow-up implementation experience for quickly resuming thi
     default read limit.
   - `stat` is the place to show metadata.
 - Do not add filesystem command dialects such as `cat --columns` or `select`. Multi-column reads
-  should use `cut` for CSV-first table files; legacy column paths may still use
-  `paste /db/table/col1 /db/table/col2`.
+  over table data should use `cut` for CSV-first table files. `paste` remains the Unix command for
+  side-by-side line joining of regular files, not a database column-selection dialect.
 - Table provider can optimize Unix-looking commands internally:
   - Table mode is CSV-first: a database is a directory, and each table is exposed as
     `/db/table.csv` with `/db/table.schema` and `/db/table.meta` sidecar regular files.
@@ -94,9 +94,10 @@ These notes capture follow-up implementation experience for quickly resuming thi
     preserved.
   - `cat /db/table.meta` -> `SHOW TABLES DETAILS FROM db`, filtered to the table and formatted as
     CSV with IoTDB result columns preserved.
-  - Legacy `/db/table/column` paths may remain as compatibility paths or migration sources.
-  - Legacy `cat /db/table/col` -> `SELECT col FROM db.table LIMIT 20`
-  - Legacy `paste /db/table/col1 /db/table/col2` -> `SELECT col1, col2 FROM db.table LIMIT 20`
+  - Bare table paths such as `/db/table` and column paths such as `/db/table/column` are not part
+    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.
 - Table-mode write boundaries are intentionally narrow and only active with
   `--fs_write_mode enabled`:
   - `mkdir /db` creates a database.
@@ -113,10 +114,10 @@ These notes capture follow-up implementation experience for quickly resuming thi
 - Interactive filesystem command errors must be handled at the single-command loop level. A
   `SQLException` from `FilesystemShell.execute()` should print `<command>: <message>` and continue
   the prompt, not bubble out to `receiveCommands()` and exit the CLI.
-- The observed `cat time` exit came from path resolution and error bubbling: from `/testtest`,
-  `cat time` resolves to `/testtest/time`; table mode interpreted that as table `testtest.time`;
-  the server returned `550`; the unchecked propagation exited the CLI. Keep a regression test for
-  this behavior.
+- The observed `cat time` exit came from path resolution and error bubbling in the older table-path
+  model. In the sidecar model, from `/testtest`, `cat time` resolves to `/testtest/time`, which is
+  not a sidecar file and should report `cat: Path is not readable: /testtest/time` while keeping the
+  prompt alive.
 
 ## Data Append Design
 
@@ -131,7 +132,8 @@ Design decisions already approved:
 - Only append writes are supported. Truncate, overwrite, random writes, row deletion, and operating
   system redirection such as `>> /db/table.csv` are out of scope.
 - The target must be a table-mode data file `/<database>/<table>.csv`.
-- `tee` without `-a`, sidecar files, legacy column paths, and tree-mode paths are rejected.
+- `tee` without `-a`, sidecar files, bare table paths, column paths, and tree-mode paths are
+  rejected.
 - Writes require `--access_mode filesystem --sql_dialect table --fs_write_mode enabled`.
 - Non-interactive mode reads CSV from stdin until EOF and then submits.
 - Interactive mode enters an append buffer. `:wq` validates and submits, `:q!` discards, and `:q`
@@ -166,16 +168,16 @@ Design decisions already approved:
 | `stat [path]` | Print filesystem-style metadata for a node. | `stat /db/table.csv` |
 | `cat <path>...` | Print readable paths sequentially. | `cat /db/table.csv` |
 | `head [-n lines] <path>` | Print the first rows or text lines; short form such as `-5` is accepted. | `head -n 5 /db/table.csv` |
-| `tail [-n lines] <path>` | Print the last rows or text lines where supported. | `tail -n 5 /db/table.csv` |
-| `wc -l <path>` | Print logical count and path. | `wc -l /db/table.csv` |
+| `tail [-n lines] <path>` | Print the last file lines where supported. | `tail -n 5 /db/table.csv` |
+| `wc -l <path>` | Print file-line count and path. | `wc -l /db/table.schema` |
 | `grep <pattern> <path>` | Print rows or lines containing a literal substring. | `grep spricoder /db/table.csv` |
 | `find [path] [-name name]` | Recursively print matching paths; `-name` is exact node-name matching. | `find /db -name table.csv` |
 | `less <path>` | Non-interactive read alias using the default read limit. | `less /db/table.csv` |
 | `more <path>` | Non-interactive read alias using the default read limit. | `more /db/table.schema` |
 | `file <path>` | Print `directory`, `regular file`, or `unknown`. | `file /db/table.meta` |
-| `du <path>` | Print logical count and path using provider count. | `du /db/table.csv` |
+| `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 readable paths side by side; table mode supports legacy same-table column paths. | `paste /db/table/key /db/table/value` |
+| `paste <path>...` | Print multiple regular files side by side, joining corresponding lines with tabs. | `paste /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` |
@@ -360,7 +362,8 @@ Use a mocked `SqlExecutor` to verify `list(/)`, `list(/db)`, `describe(/db/table
 `read(/db/table.csv)`, `describe(/db/table.schema)`, `readLines(/db/table.schema)`,
 `describe(/db/table.meta)`, and `readLines(/db/table.meta)` issue expected SQL and return typed nodes
 or text lines.
-Also cover legacy `/db/table/col` paths if compatibility behavior remains enabled.
+Also cover that bare table paths such as `/db/table` and column paths such as `/db/table/col` are
+not readable filesystem objects in the sidecar model.
 
 - [ ] **Step 2: Run test to verify it fails**
 
@@ -371,8 +374,8 @@ Expected: compilation failure or missing behavior.
 
 Implement CSV-first database/table sidecar mapping with centralized identifier rendering:
 `/db/table.csv` is the table data regular file, `/db/table.schema` is the schema sidecar regular
-file, and `/db/table.meta` is the metadata sidecar regular file. Keep `/db/table/column` only as a
-legacy compatibility path or migration source.
+file, and `/db/table.meta` is the metadata sidecar regular file. Do not expose `/db/table` as a
+directory or `/db/table/column` as a regular file.
 
 - [ ] **Step 4: Run test to verify it passes**
 
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 baeee519fd2..ed1d86742fd 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
@@ -155,9 +155,10 @@ The table data file is the primary path for user data operations:
 - `/<database>/<table>.meta`: sidecar table metadata rendered as CSV from the table row returned by
   `SHOW TABLES DETAILS FROM <database>`, preserving the column names and values returned by IoTDB.
 
-The legacy column-oriented path `/<database>/<table>/<column>` is not the primary table-model
-filesystem abstraction. It may remain as a compatibility path for existing read behavior during a
-migration period, or be documented as the migration source when moving users to the sidecar model.
+Bare table paths and column-oriented paths are intentionally outside the table filesystem model.
+`/<database>/<table>` must not be exposed as a directory, and
+`/<database>/<table>/<column>` must not be exposed as a regular file. Users inspect the table
+through the `.csv`, `.schema`, and `.meta` sidecars.
 
 | Filesystem Path | IoTDB Object | Node Type | Discovery |
 | --- | --- | --- | --- |
@@ -166,11 +167,11 @@ migration period, or be documented as the migration source when moving users to
 | `/<database>/<table>.csv` | Table data regular file | `TABLE_DATA_FILE` | `SHOW TABLES FROM <database>` |
 | `/<database>/<table>.schema` | Schema sidecar regular file | `TABLE_SCHEMA_FILE` | Exists via `SHOW TABLES FROM <database>`; content via `DESC <database>.<table> DETAILS` |
 | `/<database>/<table>.meta` | Metadata sidecar regular file | `TABLE_META_FILE` | Exists via `SHOW TABLES FROM <database>`; content via `SHOW TABLES DETAILS FROM <database>` |
-| `/<database>/<table>/<column>` | Legacy column compatibility path | `TABLE_COLUMN` | `DESC <database>.<table> DETAILS` |
 
 Table-model devices from `SHOW DEVICES FROM <table>` are not part of the first version's base path
-hierarchy because they are data-instance-oriented rather than schema-container-oriented. They can
-be added later as a virtual directory such as `/<database>/<table>/.devices`.
+hierarchy because they are data-instance-oriented rather than schema-container-oriented. If device
+metadata is exposed later, it must use a sidecar-compatible regular file path rather than turning
+`/<database>/<table>` into a directory.
 
 ## Path Rules
 
@@ -182,8 +183,8 @@ be added later as a virtual directory such as `/<database>/<table>/.devices`.
 - Table-model paths use `/database/table.csv`, `/database/table.schema`, and
   `/database/table.meta`. The database component is a directory; the table data and sidecar paths
   are regular files.
-- Legacy table-model paths of the form `/database/table/column` are compatibility paths or
-  migration inputs, not the primary table filesystem model.
+- Table-model paths of the form `/database/table` and `/database/table/column` are not filesystem
+  objects in the sidecar model.
 - Wildcard paths are not treated as filesystem nodes in the first version. Users should run
   wildcard SQL through normal SQL mode.
 - SQL escaping and identifier quoting must be centralized in provider helper methods. Command
@@ -203,18 +204,18 @@ semantics wherever the same command exists. Provider support can still vary by d
 | `ll [-a] [path]` | Long listing alias. Uses read-only permissions by default: directories as `dr-xr-xr-x`, files as `-r--r--r--`. | `ll -a /db` |
 | `cd <path>` | Change the current directory only if the target is a directory node. | `cd /db` |
 | `stat [path]` | Print filesystem-style metadata, including path, Unix type, and provider metadata. | `stat /db/table.csv` |
-| `cat <path>...` | Print one or more readable paths sequentially. Table `.csv`, `.schema`, and `.meta` sidecars print CSV lines; legacy table/column paths print tab-separated values. | `cat /db/table.csv` |
+| `cat <path>...` | Print one or more readable paths sequentially. Table `.csv`, `.schema`, and `.meta` sidecars print CSV lines. | `cat /db/table.csv` |
 | `head [-n lines] <path>` | Print the first rows or text lines for a readable path. Short numeric form such as `head -5 <path>` is also parsed. | `head -n 5 /db/table.csv` |
-| `tail [-n lines] <path>` | Print the last rows or text lines where the provider supports tail. Table `.csv` uses `ORDER BY time DESC LIMIT n` internally and returns original order. | `tail -n 5 /db/table.csv` |
-| `wc -l <path>` | Print logical row or line count plus path. Only `-l` is supported. | `wc -l /db/table.csv` |
+| `tail [-n lines] <path>` | Print the last file lines where the provider supports tail. Table `.csv` uses `ORDER BY time DESC LIMIT n` internally and returns original order. | `tail -n 5 /db/table.csv` |
+| `wc -l <path>` | Print file-line count plus path. Only `-l` is supported. | `wc -l /db/table.schema` |
 | `grep <pattern> <path>` | Print lines or rows containing the literal pattern. This is substring matching, not regular-expression matching. | `grep spricoder /db/table.csv` |
 | `find [path] [-name name]` | Recursively list the starting path and descendants whose node name exactly matches `name`; without `-name`, it prints all visited paths. | `find /db -name table.csv` |
 | `less <path>` | Current implementation prints readable content like `cat` with the default read limit; it is not an interactive pager. | `less /db/table.csv` |
 | `more <path>` | Current implementation prints readable content like `cat` with the default read limit; it is not an interactive pager. | `more /db/table.schema` |
 | `file <path>` | Print the Unix type for the path: `directory`, `regular file`, or `unknown`. | `file /db/table.meta` |
-| `du <path>` | Print logical count plus path, using the provider count operation. | `du /db/table.csv` |
+| `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 file-like paths side by side. Table mode currently supports legacy same-table column paths and optimizes them to one SQL projection. | `paste /db/table/key /db/table/value` |
+| `paste <path>...` | Read multiple regular files side by side and join corresponding lines with tabs. | `paste /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` |
@@ -252,11 +253,12 @@ Raw SQL should be run in the default SQL access mode.
 | `stat /db/table.meta` | `SHOW TABLES FROM db`, filtered to the table and rendered as filesystem metadata for the metadata sidecar. |
 | `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. |
 | `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. |
-| `stat /db/table/col` | Legacy compatibility: `DESC db.table DETAILS`, filtered to the column. |
-| `cat /db/table/col` | Legacy compatibility: `SELECT col FROM db.table LIMIT <limit>`. |
+| `/db/table` | Not a table-mode filesystem object in the sidecar model; should describe as `unknown` and reject reads. |
+| `/db/table/col` | Not a table-mode filesystem object in the sidecar model; should describe as `unknown` and reject reads. |
 
 ## Unix Output Semantics
 
@@ -267,12 +269,12 @@ exposing internal implementation types or Java debug-style structures in normal
   `ls -1`; it must not introduce comma-separated output or database-specific listing dialects.
 - `ls -a` and `ll -a` include `.` and `..` before normal entries.
 - `tree` prints the hierarchy with indentation and names only.
-- `cat` prints regular file content without Java object formatting. For table data files this is
-  CSV; for legacy compatibility table/column paths this remains tab-separated row values.
+- `cat` prints regular file content without Java object formatting. For table sidecars this is CSV.
 - `cut -d, -f2,3 /db/table.csv` is the Unix-compatible way to project fields from table CSV
   content. It performs delimiter-based text cutting like Unix `cut`; it does not parse CSV quoting
   or introduce table-specific column-selection flags.
-- `paste` prints multiple file-like paths side by side as tab-separated values.
+- `paste` prints multiple regular files side by side as tab-separated values. It does not select
+  database columns.
 - `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.
@@ -282,9 +284,9 @@ exposing internal implementation types or Java debug-style structures in normal
 - Error output should follow a command-prefixed style such as `cat: <message>` or
   `cd: <path>: Not a directory`.
 
-This means provider-internal node types such as `TABLE_DATABASE`, `TABLE_COLUMN`, or
+This means provider-internal node types such as `TABLE_DATABASE`, `TABLE_DATA_FILE`, or
 `TREE_DATABASE` must not appear in `ls` or `tree` output. Similarly, `SqlRow.asMap().toString()`
-must not be used for `cat` or `paste` output.
+must not be used for `cat`, `paste`, or sidecar output.
 
 ## Completion Semantics
 
@@ -305,23 +307,21 @@ Table-mode read behavior is currently:
 - `cat /db/table.schema` maps to `DESC db.table DETAILS` and preserves IoTDB result columns in CSV.
 - `cat /db/table.meta` maps to `SHOW TABLES DETAILS FROM db`, filters to the table row, and
   preserves IoTDB result columns in CSV.
-- Legacy `cat /db/table/column` may map to `SELECT column FROM db.table LIMIT <limit>` during the
-  migration period.
-- Legacy `paste /db/table/col1 /db/table/col2` may map to
-  `SELECT col1, col2 FROM db.table LIMIT <limit>` when all paths are columns from the same table.
+Table-mode no longer exposes `/db/table` as a directory or `/db/table/column` as a file. Those paths
+are unknown in the sidecar model and must not trigger table or column SQL reads.
 
-Although `paste` is implemented through one optimized SQL query for table mode, the user-facing
-semantics remain Unix-like: users pass multiple file paths to a standard Unix command rather than
-using a database-specific `select` command or `cat --columns` dialect.
+`paste` remains Unix-like: users pass multiple regular file paths and the shell joins
+corresponding lines with tabs. It must not become a database-specific `select` command or
+`cat --columns` dialect.
 
 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.
 
 In interactive filesystem mode, a single command failure must not terminate the CLI session. For
-example, if `cat time` is resolved from `/testtest` to `/testtest/time`, table mode treats that as a
-table path and may receive a server error such as `550: Table 'testtest.time' does not exist`. That
-error should be printed as `cat: 550: ...`, then the prompt should continue.
+example, if `cat time` is resolved from `/testtest` to `/testtest/time`, table mode should reject it
+as not readable because it is not `.csv`, `.schema`, or `.meta`. The error should be printed with
+the command name, then the prompt should continue.
 
 ## Append Data Write Semantics
 
@@ -347,7 +347,7 @@ The sidecar files remain read-only metadata views:
 
 - `tee -a /db/table.schema` is rejected.
 - `tee -a /db/table.meta` is rejected.
-- Legacy column paths such as `/db/table/col` are not writable append targets.
+- Column paths such as `/db/table/col` are not filesystem objects and are not writable append targets.
 - Tree-mode paths remain non-writable even when `--fs_write_mode enabled` is set.
 
 ### Input Modes
@@ -454,8 +454,8 @@ Filesystem mode separates reads from mutations. The schema provider owns read op
 - `readLines(FsPath path)` for text sidecars such as `.csv`, `.schema`, and `.meta`
 - `tail(FsPath path)` / `tailLines(FsPath path)` where the provider supports tail
 - `count(FsPath path)` where the provider supports logical count
-- `read(List<FsPath> paths)` for provider-optimized multi-path reads such as legacy table-column
-  `paste`
+- `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.
 
 The mutation provider owns the current write-gated operations:
 
@@ -541,8 +541,8 @@ Unit tests should cover the behavior without needing a live IoTDB instance where
   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
-  sidecars, IoTDB-preserved `.schema`/`.meta` content, multi-column legacy reads, and table
-  mutation restrictions.
+  sidecars, IoTDB-preserved `.schema`/`.meta` content, rejection of bare table and column paths,
+  and table mutation restrictions.
 - CLI option tests extending existing CLI unit coverage for default `access_mode`, filesystem
   mode, invalid mode values, and `fs_write_mode`.
 - Shell tests proving SQL mode remains the default and filesystem mode dispatches to
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 63c543d40c6..051c96b5b35 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
@@ -84,10 +84,10 @@ public class FilesystemShell {
         ctx.getPrinter().println(currentPath.toString());
         return true;
       case LS:
-        printNodes(provider.list(resolve(command.getPath())), isAllOption(command));
+        printList(command.getPath(), isAllOption(command), false);
         return true;
       case LL:
-        printLongNodes(provider.list(resolve(command.getPath())), isAllOption(command));
+        printList(command.getPath(), isAllOption(command), true);
         return true;
       case CD:
         changeDirectory(command.getPath());
@@ -136,7 +136,7 @@ public class FilesystemShell {
         printCut(command.getPath(), command.getOption(), command.getPattern());
         return true;
       case PASTE:
-        printRows(provider.read(resolve(command.getPaths()), DEFAULT_READ_LIMIT));
+        printPaste(command.getPaths());
         return true;
       case TEE:
         append(command.getPath(), false);
@@ -173,6 +173,15 @@ public class FilesystemShell {
   }
 
   private void printTree(FsPath path, int depth) throws SQLException {
+    FsNode node = provider.describe(path);
+    if (node.getType() == FsNodeType.UNKNOWN) {
+      ctx.getPrinter().println("tree: " + path + ": No such file or directory");
+      return;
+    }
+    if (!isDirectory(node.getType())) {
+      ctx.getPrinter().println(node.getName());
+      return;
+    }
     printTreeChildren(path, 0, depth);
   }
 
@@ -218,6 +227,28 @@ public class FilesystemShell {
     return resolvedPaths;
   }
 
+  private void printList(String path, boolean all, boolean longListing) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    FsNode node = provider.describe(resolvedPath);
+    if (node.getType() == FsNodeType.UNKNOWN) {
+      ctx.getPrinter().println("ls: " + resolvedPath + ": No such file or directory");
+      return;
+    }
+    if (!isDirectory(node.getType())) {
+      if (longListing) {
+        ctx.getPrinter().println(longMode(node.getType()) + "  1 iotdb iotdb 0 " + node.getName());
+      } else {
+        ctx.getPrinter().println(node.getName());
+      }
+      return;
+    }
+    if (longListing) {
+      printLongNodes(provider.list(resolvedPath), all);
+    } else {
+      printNodes(provider.list(resolvedPath), all);
+    }
+  }
+
   private void printNodes(List<FsNode> nodes, boolean all) {
     if (all) {
       ctx.getPrinter().println(".");
@@ -316,6 +347,19 @@ public class FilesystemShell {
     }
   }
 
+  private void printPaste(List<String> paths) throws SQLException {
+    List<List<String>> files = new ArrayList<>();
+    int maxLines = 0;
+    for (String path : paths) {
+      List<String> lines = readableLines(resolve(path), DEFAULT_READ_LIMIT);
+      files.add(lines);
+      maxLines = Math.max(maxLines, lines.size());
+    }
+    for (int i = 0; i < maxLines; i++) {
+      ctx.getPrinter().println(pasteLine(files, i));
+    }
+  }
+
   private List<String> readableLines(FsPath path, int limit) throws SQLException {
     if (isTextFile(path)) {
       return provider.readLines(path, limit);
@@ -495,9 +539,7 @@ public class FilesystemShell {
         || type == FsNodeType.TREE_DATABASE
         || type == FsNodeType.TREE_INTERNAL_PATH
         || type == FsNodeType.TREE_DEVICE
-        || type == FsNodeType.TABLE_DATABASE
-        || type == FsNodeType.TABLE_TABLE
-        || type == FsNodeType.TABLE_VIEW;
+        || type == FsNodeType.TABLE_DATABASE;
   }
 
   private static String longMode(FsNodeType type) {
@@ -540,6 +582,20 @@ public class FilesystemShell {
     return builder.toString();
   }
 
+  private static String pasteLine(List<List<String>> files, int lineIndex) {
+    StringBuilder builder = new StringBuilder();
+    for (int i = 0; i < files.size(); i++) {
+      if (i > 0) {
+        builder.append('\t');
+      }
+      List<String> lines = files.get(i);
+      if (lineIndex < lines.size()) {
+        builder.append(lines.get(lineIndex));
+      }
+    }
+    return builder.toString();
+  }
+
   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/node/FsNodeType.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
index df63ef5c817..0d7fb818d47 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
@@ -27,9 +27,7 @@ public enum FsNodeType {
   TREE_DEVICE,
   TREE_TIMESERIES,
   TABLE_DATABASE,
-  TABLE_TABLE,
   TABLE_VIEW,
-  TABLE_COLUMN,
   TABLE_DATA_FILE,
   TABLE_SCHEMA_FILE,
   TABLE_META_FILE,
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableCsvAppendPlanner.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableCsvAppendPlanner.java
index f5a0150c8e0..5ac20fa9013 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableCsvAppendPlanner.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableCsvAppendPlanner.java
@@ -46,7 +46,8 @@ class TableCsvAppendPlanner {
 
   private TableCsvAppendPlanner() {}
 
-  static List<String> plan(String tablePath, List<SqlRow> schemaRows, List<String> lines)
+  static List<String> plan(
+      String database, String table, List<SqlRow> schemaRows, List<String> lines)
       throws SQLException {
     List<CSVRecord> records = parse(lines);
     if (records.isEmpty()) {
@@ -60,7 +61,7 @@ class TableCsvAppendPlanner {
     if (csv.rows.isEmpty()) {
       return new ArrayList<>();
     }
-    return buildStatements(tablePath, csv.columns, csv.rows);
+    return buildStatements(database, table, csv.columns, csv.rows);
   }
 
   private static List<CSVRecord> parse(List<String> lines) throws SQLException {
@@ -213,13 +214,13 @@ class TableCsvAppendPlanner {
   }
 
   private static List<String> buildStatements(
-      String tablePath, List<TableColumn> columns, List<String> rows) {
+      String database, String table, List<TableColumn> columns, List<String> rows) {
     List<String> statements = new ArrayList<>();
     for (int start = 0; start < rows.size(); start += INSERT_BATCH_SIZE) {
       int end = Math.min(start + INSERT_BATCH_SIZE, rows.size());
       statements.add(
           "INSERT INTO "
-              + identifierPath(tablePath)
+              + TableFilesystemSql.tablePath(database, table)
               + "("
               + columnList(columns)
               + ") VALUES "
@@ -228,33 +229,14 @@ class TableCsvAppendPlanner {
     return statements;
   }
 
-  private static String identifierPath(String tablePath) {
-    String[] parts = tablePath.split("\\.", -1);
-    StringBuilder builder = new StringBuilder();
-    for (String part : parts) {
-      if (builder.length() > 0) {
-        builder.append('.');
-      }
-      builder.append(identifier(part));
-    }
-    return builder.toString();
-  }
-
   private static String columnList(List<TableColumn> columns) {
     List<String> names = new ArrayList<>();
     for (TableColumn column : columns) {
-      names.add(identifier(column.name));
+      names.add(TableFilesystemSql.identifier(column.name));
     }
     return join(names, ", ");
   }
 
-  private static String identifier(String value) {
-    if (value.matches("[A-Za-z_][A-Za-z0-9_]*")) {
-      return value;
-    }
-    return "\"" + value.replace("\"", "\"\"") + "\"";
-  }
-
   private static String join(List<String> values, String delimiter) {
     StringBuilder builder = new StringBuilder();
     for (String value : values) {
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
index 8a0e79ca58a..710ff02a609 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
@@ -42,7 +42,7 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
     if (path.getSegments().size() != 1) {
       throw invalidOperation();
     }
-    executor.execute("CREATE DATABASE " + path.getFileName());
+    executor.execute("CREATE DATABASE " + TableFilesystemSql.identifier(path.getFileName()));
   }
 
   @Override
@@ -61,7 +61,11 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
     if (!parent(source).equals(parent(target))) {
       throw invalidOperation();
     }
-    executor.execute("ALTER TABLE " + toTablePath(source) + " RENAME TO " + tableName(target));
+    executor.execute(
+        "ALTER TABLE "
+            + toTablePath(source)
+            + " RENAME TO "
+            + TableFilesystemSql.identifier(tableName(target)));
   }
 
   @Override
@@ -72,10 +76,12 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
     if (lines == null || lines.isEmpty()) {
       return;
     }
-    String tablePath = toTablePath(path);
     List<String> statements =
         TableCsvAppendPlanner.plan(
-            tablePath, executor.query("DESC " + tablePath + " DETAILS"), lines);
+            databaseName(path),
+            tableName(path),
+            executor.query("DESC " + toTablePath(path) + " DETAILS"),
+            lines);
     for (String statement : statements) {
       executor.execute(statement);
     }
@@ -86,8 +92,11 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
   }
 
   private static String toTablePath(FsPath path) {
-    List<String> segments = path.getSegments();
-    return segments.get(0) + "." + tableName(path);
+    return TableFilesystemSql.tablePath(databaseName(path), tableName(path));
+  }
+
+  private static String databaseName(FsPath path) {
+    return path.getSegments().get(0);
   }
 
   private static boolean isDataFile(FsPath path) {
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
index 879145b69d4..f5f60a6ab70 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
@@ -71,7 +71,7 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
       if (parseTableFile(path).kind != TableFileKind.UNKNOWN) {
         return new ArrayList<>();
       }
-      return listColumns(path);
+      return new ArrayList<>();
     }
     return new ArrayList<>();
   }
@@ -90,15 +90,8 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
       if (file.kind != TableFileKind.UNKNOWN) {
         return describeTableFile(path, file);
       }
-      return describeTable(path);
-    }
-    String columnName = path.getFileName();
-    for (FsNode node : listColumns(parent(path))) {
-      if (columnName.equals(node.getName())) {
-        return node;
-      }
     }
-    return new FsNode(columnName, path, FsNodeType.UNKNOWN);
+    return unknown(path);
   }
 
   @Override
@@ -106,16 +99,9 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     int depth = path.getSegments().size();
     TableFileRef file = parseTableFile(path);
     if (depth == 2 && file.kind == TableFileKind.DATA_CSV) {
+      ensureExists(path, file);
       return executor.query("SELECT * FROM " + file.toTablePath() + " LIMIT " + limit);
     }
-    if (depth == 2 && file.kind == TableFileKind.UNKNOWN) {
-      return executor.query("SELECT * FROM " + toTablePath(path) + " LIMIT " + limit);
-    }
-    if (depth == 3) {
-      String tablePath = toTablePath(parent(path));
-      return executor.query(
-          "SELECT " + path.getFileName() + " FROM " + tablePath + " LIMIT " + limit);
-    }
     throw new SQLException("Path is not readable: " + path);
   }
 
@@ -123,14 +109,18 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
   public List<String> readLines(FsPath path, int limit) throws SQLException {
     TableFileRef file = parseTableFile(path);
     if (file.kind == TableFileKind.DATA_CSV) {
-      return rowsToCsvLines(
-          executor.query("SELECT * FROM " + file.toTablePath() + " LIMIT " + limit));
+      ensureExists(path, file);
+      return head(
+          rowsToCsvLines(executor.query("SELECT * FROM " + file.toTablePath() + " LIMIT " + limit)),
+          limit);
     }
     if (file.kind == TableFileKind.SCHEMA) {
-      return rowsToCsvLines(executor.query("DESC " + file.toTablePath() + " DETAILS"));
+      ensureExists(path, file);
+      return head(rowsToCsvLines(executor.query("DESC " + file.toTablePath() + " DETAILS")), limit);
     }
     if (file.kind == TableFileKind.META) {
-      return metaLines(file);
+      ensureExists(path, file);
+      return head(metaLines(file), limit);
     }
     throw new SQLException("Path is not readable as text: " + path);
   }
@@ -141,23 +131,10 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     TableFileRef file = parseTableFile(path);
     List<SqlRow> rows;
     if (depth == 2 && file.kind == TableFileKind.DATA_CSV) {
+      ensureExists(path, file);
       rows =
           executor.query(
               "SELECT * FROM " + file.toTablePath() + " ORDER BY time DESC LIMIT " + limit);
-    } else if (depth == 2 && file.kind == TableFileKind.UNKNOWN) {
-      rows =
-          executor.query(
-              "SELECT * FROM " + toTablePath(path) + " ORDER BY time DESC LIMIT " + limit);
-    } else if (depth == 3) {
-      String tablePath = toTablePath(parent(path));
-      rows =
-          executor.query(
-              "SELECT "
-                  + path.getFileName()
-                  + " FROM "
-                  + tablePath
-                  + " ORDER BY time DESC LIMIT "
-                  + limit);
     } else {
       throw new SQLException("Path is not readable: " + path);
     }
@@ -168,14 +145,18 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
   @Override
   public List<String> tailLines(FsPath path, int limit) throws SQLException {
     TableFileRef file = parseTableFile(path);
-    if (file.kind != TableFileKind.DATA_CSV) {
-      throw new SQLException("Path does not support tail: " + path);
+    if (file.kind == TableFileKind.SCHEMA || file.kind == TableFileKind.META) {
+      return tail(readLines(path, Integer.MAX_VALUE), limit);
     }
-    List<SqlRow> rows =
-        executor.query(
-            "SELECT * FROM " + file.toTablePath() + " ORDER BY time DESC LIMIT " + limit);
-    Collections.reverse(rows);
-    return rowsToCsvLines(rows);
+    if (file.kind == TableFileKind.DATA_CSV) {
+      ensureExists(path, file);
+      List<SqlRow> rows =
+          executor.query(
+              "SELECT * FROM " + file.toTablePath() + " ORDER BY time DESC LIMIT " + limit);
+      Collections.reverse(rows);
+      return tail(rowsToCsvLines(rows), limit);
+    }
+    throw new SQLException("Path does not support tail: " + path);
   }
 
   @Override
@@ -184,16 +165,15 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     TableFileRef file = parseTableFile(path);
     List<SqlRow> rows;
     if (depth == 2 && file.kind == TableFileKind.DATA_CSV) {
+      ensureExists(path, file);
       rows = executor.query("SELECT COUNT(*) FROM " + file.toTablePath());
-    } else if (depth == 2 && file.kind == TableFileKind.UNKNOWN) {
-      rows = executor.query("SELECT COUNT(*) FROM " + toTablePath(path));
-    } else if (depth == 3) {
-      String tablePath = toTablePath(parent(path));
-      rows = executor.query("SELECT COUNT(" + path.getFileName() + ") FROM " + tablePath);
+      return countValue(rows) + 1;
+    } else if (depth == 2
+        && (file.kind == TableFileKind.SCHEMA || file.kind == TableFileKind.META)) {
+      return readLines(path, Integer.MAX_VALUE).size();
     } else {
       throw new SQLException("Path is not countable: " + path);
     }
-    return countValue(rows);
   }
 
   @Override
@@ -201,14 +181,13 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     if (paths.size() == 1) {
       return read(paths.get(0), limit);
     }
-    FsPath tablePath = parent(paths.get(0));
-    for (FsPath path : paths) {
-      if (path.getSegments().size() != 3 || !tablePath.equals(parent(path))) {
-        throw new SQLException("Paths must be columns from the same table");
-      }
+    throw new SQLException("Multiple paths should be read as regular files");
+  }
+
+  private void ensureExists(FsPath path, TableFileRef file) throws SQLException {
+    if (!tableExists(parent(path), file.table)) {
+      throw new SQLException("Path does not exist: " + path);
     }
-    return executor.query(
-        "SELECT " + columnList(paths) + " FROM " + toTablePath(tablePath) + " LIMIT " + limit);
   }
 
   private List<FsNode> listDatabases() throws SQLException {
@@ -232,16 +211,6 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     return new FsNode(database, path, FsNodeType.UNKNOWN);
   }
 
-  private FsNode describeTable(FsPath path) throws SQLException {
-    String table = path.getFileName();
-    for (FsNode node : listTables(parent(path))) {
-      if (table.equals(node.getName())) {
-        return node;
-      }
-    }
-    return new FsNode(table, path, FsNodeType.UNKNOWN);
-  }
-
   private FsNode describeTableFile(FsPath path, TableFileRef file) throws SQLException {
     for (String table : listTableNames(parent(path))) {
       if (table.equals(file.table)) {
@@ -251,18 +220,6 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     return new FsNode(path.getFileName(), path, FsNodeType.UNKNOWN);
   }
 
-  private List<FsNode> listTables(FsPath databasePath) throws SQLException {
-    List<FsNode> nodes = new ArrayList<>();
-    for (String table : listTableNames(databasePath)) {
-      nodes.add(
-          new FsNode(
-              table,
-              FsPath.absolute("/" + databasePath.getFileName() + "/" + table),
-              FsNodeType.TABLE_TABLE));
-    }
-    return nodes;
-  }
-
   private List<FsNode> listTableFiles(FsPath databasePath) throws SQLException {
     String database = databasePath.getFileName();
     List<FsNode> nodes = new ArrayList<>();
@@ -292,7 +249,8 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
   private List<String> listTableNames(FsPath databasePath) throws SQLException {
     String database = databasePath.getFileName();
     List<String> tables = new ArrayList<>();
-    for (SqlRow row : executor.query("SHOW TABLES FROM " + database)) {
+    for (SqlRow row :
+        executor.query("SHOW TABLES FROM " + TableFilesystemSql.identifier(database))) {
       String table = row.get("TableName");
       if (table != null) {
         tables.add(table);
@@ -301,33 +259,20 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     return tables;
   }
 
-  private List<FsNode> listColumns(FsPath tablePath) throws SQLException {
-    List<String> segments = tablePath.getSegments();
-    String database = segments.get(0);
-    String table = segments.get(1);
-    List<FsNode> nodes = new ArrayList<>();
-    for (SqlRow row : executor.query("DESC " + database + "." + table + " DETAILS")) {
-      String column = row.get("ColumnName");
-      if (column != null) {
-        nodes.add(
-            new FsNode(
-                column,
-                FsPath.absolute("/" + database + "/" + table + "/" + column),
-                FsNodeType.TABLE_COLUMN,
-                row.asMap()));
+  private boolean tableExists(FsPath databasePath, String table) throws SQLException {
+    for (String tableName : listTableNames(databasePath)) {
+      if (tableName.equals(table)) {
+        return true;
       }
     }
-    return nodes;
-  }
-
-  private static String toTablePath(FsPath path) {
-    List<String> segments = path.getSegments();
-    return segments.get(0) + "." + segments.get(1);
+    return false;
   }
 
   private List<String> metaLines(TableFileRef file) throws SQLException {
     List<SqlRow> rows = new ArrayList<>();
-    for (SqlRow row : executor.query("SHOW TABLES DETAILS FROM " + file.database)) {
+    for (SqlRow row :
+        executor.query(
+            "SHOW TABLES DETAILS FROM " + TableFilesystemSql.identifier(file.database))) {
       if (file.table.equals(row.get("TableName"))) {
         rows.add(row);
       }
@@ -394,17 +339,6 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     return new TableFileRef(database, table, kind).metadata();
   }
 
-  private static String columnList(List<FsPath> paths) {
-    StringBuilder builder = new StringBuilder();
-    for (FsPath path : paths) {
-      if (builder.length() > 0) {
-        builder.append(", ");
-      }
-      builder.append(path.getFileName());
-    }
-    return builder.toString();
-  }
-
   private static long countValue(List<SqlRow> rows) {
     if (rows.isEmpty() || rows.get(0).asMap().isEmpty()) {
       return 0;
@@ -424,6 +358,24 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     return FsPath.absolute(builder.toString());
   }
 
+  private static FsNode unknown(FsPath path) {
+    return new FsNode(path.getFileName(), path, FsNodeType.UNKNOWN);
+  }
+
+  private static List<String> head(List<String> lines, int limit) {
+    if (limit < 0 || lines.size() <= limit) {
+      return lines;
+    }
+    return new ArrayList<>(lines.subList(0, limit));
+  }
+
+  private static List<String> tail(List<String> lines, int limit) {
+    if (limit < 0 || lines.size() <= limit) {
+      return lines;
+    }
+    return new ArrayList<>(lines.subList(lines.size() - limit, lines.size()));
+  }
+
   private static class TableFileRef {
     private final String database;
     private final String table;
@@ -457,7 +409,7 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     }
 
     private String toTablePath() {
-      return database + "." + table;
+      return TableFilesystemSql.tablePath(database, table);
     }
 
     private FsNodeType nodeType() {
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSql.java
similarity index 66%
copy from iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
copy to iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSql.java
index df63ef5c817..9d0d549edac 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSql.java
@@ -17,21 +17,20 @@
  * under the License.
  */
 
-package org.apache.iotdb.cli.fs.node;
+package org.apache.iotdb.cli.fs.provider;
 
-public enum FsNodeType {
-  VIRTUAL_ROOT,
-  TREE_ROOT,
-  TREE_DATABASE,
-  TREE_INTERNAL_PATH,
-  TREE_DEVICE,
-  TREE_TIMESERIES,
-  TABLE_DATABASE,
-  TABLE_TABLE,
-  TABLE_VIEW,
-  TABLE_COLUMN,
-  TABLE_DATA_FILE,
-  TABLE_SCHEMA_FILE,
-  TABLE_META_FILE,
-  UNKNOWN
+final class TableFilesystemSql {
+
+  private TableFilesystemSql() {}
+
+  static String tablePath(String database, String table) {
+    return identifier(database) + "." + identifier(table);
+  }
+
+  static String identifier(String value) {
+    if (value.matches("[A-Za-z_][A-Za-z0-9_]*")) {
+      return value;
+    }
+    return "\"" + value.replace("\"", "\"\"") + "\"";
+  }
 }
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java
index d3d44a00972..1cbf8dcb6af 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java
@@ -38,6 +38,7 @@ import java.sql.SQLException;
 import java.sql.Statement;
 
 import static org.junit.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -45,8 +46,6 @@ public class CliFilesystemModeTest {
 
   @Mock private IoTDBConnection connection;
   @Mock private Statement statement;
-  @Mock private ResultSet resultSet;
-  @Mock private ResultSetMetaData metaData;
   @Mock private FilesystemShell shell;
   @Mock private LineReader lineReader;
 
@@ -63,11 +62,13 @@ public class CliFilesystemModeTest {
   @Test
   public void createFilesystemShellUsesTableProviderForTableDialect() throws Exception {
     when(connection.getSqlDialect()).thenReturn("table");
+    mockSingleColumnQuery("SHOW DATABASES", "Database", "db1");
     mockSingleColumnQuery("SHOW TABLES FROM db1", "TableName", "table1");
 
     FilesystemShell shell = Cli.createFilesystemShell(ctx, connection);
     shell.execute("ls /db1");
 
+    verify(statement).executeQuery("SHOW DATABASES");
     verify(statement).executeQuery("SHOW TABLES FROM db1");
   }
 
@@ -100,11 +101,13 @@ public class CliFilesystemModeTest {
   @Test
   public void createFilesystemShellUsesTreeProviderForTreeDialect() throws Exception {
     when(connection.getSqlDialect()).thenReturn("tree");
+    mockSingleColumnQuery("SHOW DATABASES", "Database", "root.sg");
     mockSingleColumnQuery("SHOW CHILD PATHS root.sg", "ChildPaths", "root.sg.d1");
 
     FilesystemShell shell = Cli.createFilesystemShell(ctx, connection);
     shell.execute("ls /root/sg");
 
+    verify(statement).executeQuery("SHOW DATABASES");
     verify(statement).executeQuery("SHOW CHILD PATHS root.sg");
   }
 
@@ -122,6 +125,8 @@ public class CliFilesystemModeTest {
   }
 
   private void mockSingleColumnQuery(String sql, String column, String value) throws Exception {
+    ResultSet resultSet = mock(ResultSet.class);
+    ResultSetMetaData metaData = mock(ResultSetMetaData.class);
     when(connection.createStatement()).thenReturn(statement);
     when(statement.executeQuery(sql)).thenReturn(resultSet);
     when(resultSet.getMetaData()).thenReturn(metaData);
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 5566022a31c..cc442be8be3 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
@@ -86,6 +86,8 @@ public class FilesystemShellTest {
 
   @Test
   public void executeLsPrintsChildNodes() throws SQLException {
+    when(provider.describe(FsPath.absolute("/")))
+        .thenReturn(new FsNode("/", FsPath.absolute("/"), FsNodeType.VIRTUAL_ROOT));
     when(provider.list(FsPath.absolute("/")))
         .thenReturn(
             Arrays.asList(
@@ -97,27 +99,34 @@ public class FilesystemShellTest {
     assertEquals("root" + System.lineSeparator() + "test" + System.lineSeparator(), out.toString());
     assertFalse(out.toString().contains(","));
     assertFalse(out.toString().contains("TREE_ROOT"));
+    verify(provider).describe(FsPath.absolute("/"));
     verify(provider).list(FsPath.absolute("/"));
   }
 
   @Test
   public void executeLlPrintsLongListing() throws SQLException {
+    when(provider.describe(FsPath.absolute("/")))
+        .thenReturn(new FsNode("/", FsPath.absolute("/"), FsNodeType.VIRTUAL_ROOT));
     when(provider.list(FsPath.absolute("/")))
         .thenReturn(
             Arrays.asList(
                 new FsNode("testtest", FsPath.absolute("/testtest"), FsNodeType.TABLE_DATABASE),
-                new FsNode("value", FsPath.absolute("/value"), FsNodeType.TABLE_COLUMN)));
+                new FsNode(
+                    "value.csv", FsPath.absolute("/value.csv"), FsNodeType.TABLE_DATA_FILE)));
 
     assertTrue(shell.execute("ll /"));
 
     assertTrue(out.toString().contains("dr-xr-xr-x"));
     assertTrue(out.toString().contains("-r--r--r--"));
     assertTrue(out.toString().contains("testtest"));
+    verify(provider).describe(FsPath.absolute("/"));
     verify(provider).list(FsPath.absolute("/"));
   }
 
   @Test
   public void executeLsLongOptionPrintsLongListing() throws SQLException {
+    when(provider.describe(FsPath.absolute("/")))
+        .thenReturn(new FsNode("/", FsPath.absolute("/"), FsNodeType.VIRTUAL_ROOT));
     when(provider.list(FsPath.absolute("/")))
         .thenReturn(
             Arrays.asList(
@@ -127,11 +136,14 @@ public class FilesystemShellTest {
 
     assertTrue(out.toString().contains("dr-xr-xr-x"));
     assertTrue(out.toString().contains("testtest"));
+    verify(provider).describe(FsPath.absolute("/"));
     verify(provider).list(FsPath.absolute("/"));
   }
 
   @Test
   public void executeLsAllPrintsDotEntries() throws SQLException {
+    when(provider.describe(FsPath.absolute("/")))
+        .thenReturn(new FsNode("/", FsPath.absolute("/"), FsNodeType.VIRTUAL_ROOT));
     when(provider.list(FsPath.absolute("/")))
         .thenReturn(
             Arrays.asList(
@@ -147,11 +159,14 @@ public class FilesystemShellTest {
             + "testtest"
             + System.lineSeparator(),
         out.toString());
+    verify(provider).describe(FsPath.absolute("/"));
     verify(provider).list(FsPath.absolute("/"));
   }
 
   @Test
   public void executeLlAllPrintsDotEntriesInLongListing() throws SQLException {
+    when(provider.describe(FsPath.absolute("/")))
+        .thenReturn(new FsNode("/", FsPath.absolute("/"), FsNodeType.VIRTUAL_ROOT));
     when(provider.list(FsPath.absolute("/")))
         .thenReturn(
             Arrays.asList(
@@ -163,9 +178,37 @@ public class FilesystemShellTest {
     assertTrue(out.toString().contains("dr-xr-xr-x  1 iotdb iotdb 0 .."));
     assertTrue(out.toString().contains("dr-xr-xr-x  1 iotdb iotdb 0 testtest"));
     assertFalse(out.toString().contains("-a"));
+    verify(provider).describe(FsPath.absolute("/"));
     verify(provider).list(FsPath.absolute("/"));
   }
 
+  @Test
+  public void executeLsRegularFilePrintsFileName() throws SQLException {
+    when(provider.describe(FsPath.absolute("/db1/table1.csv")))
+        .thenReturn(
+            new FsNode(
+                "table1.csv", FsPath.absolute("/db1/table1.csv"), FsNodeType.TABLE_DATA_FILE));
+
+    assertTrue(shell.execute("ls /db1/table1.csv"));
+
+    assertEquals("table1.csv" + System.lineSeparator(), out.toString());
+    verify(provider).describe(FsPath.absolute("/db1/table1.csv"));
+    verify(provider, times(0)).list(FsPath.absolute("/db1/table1.csv"));
+  }
+
+  @Test
+  public void executeLsUnknownPathPrintsNoSuchFile() throws SQLException {
+    when(provider.describe(FsPath.absolute("/db1/table1")))
+        .thenReturn(new FsNode("table1", FsPath.absolute("/db1/table1"), FsNodeType.UNKNOWN));
+
+    assertTrue(shell.execute("ls /db1/table1"));
+
+    assertEquals(
+        "ls: /db1/table1: No such file or directory" + System.lineSeparator(), out.toString());
+    verify(provider).describe(FsPath.absolute("/db1/table1"));
+    verify(provider, times(0)).list(FsPath.absolute("/db1/table1"));
+  }
+
   @Test
   public void executeCdUpdatesCurrentPath() throws SQLException {
     when(provider.describe(FsPath.absolute("/root")))
@@ -258,6 +301,8 @@ public class FilesystemShellTest {
 
   @Test
   public void executeTreePrintsChildrenUntilDepth() throws SQLException {
+    when(provider.describe(FsPath.absolute("/")))
+        .thenReturn(new FsNode("/", FsPath.absolute("/"), FsNodeType.VIRTUAL_ROOT));
     when(provider.list(FsPath.absolute("/")))
         .thenReturn(
             Arrays.asList(new FsNode("root", FsPath.absolute("/root"), FsNodeType.TREE_ROOT)));
@@ -271,20 +316,34 @@ public class FilesystemShellTest {
     assertTrue(out.toString().contains("sg"));
     assertFalse(out.toString().contains("TREE_ROOT"));
     assertFalse(out.toString().contains("TREE_DATABASE"));
+    verify(provider).describe(FsPath.absolute("/"));
     verify(provider).list(FsPath.absolute("/"));
     verify(provider).list(FsPath.absolute("/root"));
   }
 
   @Test
-  public void executeCatReadsTablePath() throws SQLException {
-    when(provider.read(FsPath.absolute("/db1/table1"), 20))
-        .thenReturn(Arrays.asList(SqlRow.of("Time", "1", "tag1", "a", "s1", "42")));
+  public void executeTreeUnknownPathPrintsNoSuchFile() throws SQLException {
+    when(provider.describe(FsPath.absolute("/db1/table1")))
+        .thenReturn(new FsNode("table1", FsPath.absolute("/db1/table1"), FsNodeType.UNKNOWN));
+
+    assertTrue(shell.execute("tree /db1/table1"));
+
+    assertEquals(
+        "tree: /db1/table1: No such file or directory" + System.lineSeparator(), out.toString());
+    verify(provider).describe(FsPath.absolute("/db1/table1"));
+    verify(provider, times(0)).list(FsPath.absolute("/db1/table1"));
+  }
+
+  @Test
+  public void executeCatReadsSchemaFileLines() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.schema"), 20))
+        .thenReturn(Arrays.asList("ColumnName,DataType", "key,STRING"));
 
-    assertTrue(shell.execute("cat /db1/table1"));
+    assertTrue(shell.execute("cat /db1/table1.schema"));
 
-    assertTrue(out.toString().contains("1\ta\t42"));
-    assertFalse(out.toString().contains("{"));
-    verify(provider).read(FsPath.absolute("/db1/table1"), 20);
+    assertTrue(out.toString().contains("ColumnName,DataType"));
+    assertTrue(out.toString().contains("key,STRING"));
+    verify(provider).readLines(FsPath.absolute("/db1/table1.schema"), 20);
   }
 
   @Test
@@ -300,29 +359,37 @@ public class FilesystemShellTest {
   }
 
   @Test
-  public void executeCatReadsMultiplePathsSequentially() throws SQLException {
-    when(provider.read(FsPath.absolute("/db1/table1/tag1"), 20))
-        .thenReturn(Arrays.asList(SqlRow.of("Time", "1", "tag1", "a")));
-    when(provider.read(FsPath.absolute("/db1/table1/s1"), 20))
-        .thenReturn(Arrays.asList(SqlRow.of("Time", "1", "s1", "42")));
+  public void executeCatReadsMultipleTextFilesSequentially() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 20))
+        .thenReturn(Arrays.asList("time,key", "1,a"));
+    when(provider.readLines(FsPath.absolute("/db1/table1.meta"), 20))
+        .thenReturn(Arrays.asList("TableName,Status", "table1,USING"));
 
-    assertTrue(shell.execute("cat /db1/table1/tag1 /db1/table1/s1"));
+    assertTrue(shell.execute("cat /db1/table1.csv /db1/table1.meta"));
 
-    assertTrue(out.toString().contains("1\ta"));
-    assertTrue(out.toString().contains("1\t42"));
-    verify(provider).read(FsPath.absolute("/db1/table1/tag1"), 20);
-    verify(provider).read(FsPath.absolute("/db1/table1/s1"), 20);
+    assertEquals(
+        "time,key"
+            + System.lineSeparator()
+            + "1,a"
+            + System.lineSeparator()
+            + "TableName,Status"
+            + System.lineSeparator()
+            + "table1,USING"
+            + System.lineSeparator(),
+        out.toString());
+    verify(provider).readLines(FsPath.absolute("/db1/table1.csv"), 20);
+    verify(provider).readLines(FsPath.absolute("/db1/table1.meta"), 20);
   }
 
   @Test
-  public void executeHeadReadsPathWithLimit() throws SQLException {
-    when(provider.read(FsPath.absolute("/db1/table1"), 5))
-        .thenReturn(Arrays.asList(SqlRow.of("Time", "1", "tag1", "a", "s1", "42")));
+  public void executeHeadReadsSchemaFileLinesWithLimit() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.schema"), 5))
+        .thenReturn(Arrays.asList("ColumnName,DataType", "key,STRING"));
 
-    assertTrue(shell.execute("head -n 5 /db1/table1"));
+    assertTrue(shell.execute("head -n 5 /db1/table1.schema"));
 
-    assertTrue(out.toString().contains("1\ta\t42"));
-    verify(provider).read(FsPath.absolute("/db1/table1"), 5);
+    assertTrue(out.toString().contains("ColumnName,DataType"));
+    verify(provider).readLines(FsPath.absolute("/db1/table1.schema"), 5);
   }
 
   @Test
@@ -338,14 +405,14 @@ public class FilesystemShellTest {
   }
 
   @Test
-  public void executeTailReadsPathWithLimit() throws SQLException {
-    when(provider.tail(FsPath.absolute("/db1/table1"), 3))
-        .thenReturn(Arrays.asList(SqlRow.of("Time", "2", "tag1", "b", "s1", "43")));
+  public void executeTailReadsSchemaFileLinesWithLimit() throws SQLException {
+    when(provider.tailLines(FsPath.absolute("/db1/table1.schema"), 3))
+        .thenReturn(Arrays.asList("key,STRING", "value,DOUBLE"));
 
-    assertTrue(shell.execute("tail -n 3 /db1/table1"));
+    assertTrue(shell.execute("tail -n 3 /db1/table1.schema"));
 
-    assertTrue(out.toString().contains("2\tb\t43"));
-    verify(provider).tail(FsPath.absolute("/db1/table1"), 3);
+    assertTrue(out.toString().contains("value,DOUBLE"));
+    verify(provider).tailLines(FsPath.absolute("/db1/table1.schema"), 3);
   }
 
   @Test
@@ -384,17 +451,17 @@ public class FilesystemShellTest {
 
   @Test
   public void executeGrepPrintsOnlyMatchingRows() throws SQLException {
-    when(provider.read(FsPath.absolute("/db1/table1"), 20))
+    when(provider.read(FsPath.absolute("/root/sg/d1/s1"), 20))
         .thenReturn(
             Arrays.asList(
                 SqlRow.of("Time", "1", "tag1", "spricoder", "s1", "42"),
                 SqlRow.of("Time", "2", "tag1", "other", "s1", "43")));
 
-    assertTrue(shell.execute("grep spricoder /db1/table1"));
+    assertTrue(shell.execute("grep spricoder /root/sg/d1/s1"));
 
     assertTrue(out.toString().contains("1\tspricoder\t42"));
     assertFalse(out.toString().contains("2\tother\t43"));
-    verify(provider).read(FsPath.absolute("/db1/table1"), 20);
+    verify(provider).read(FsPath.absolute("/root/sg/d1/s1"), 20);
   }
 
   @Test
@@ -403,34 +470,36 @@ public class FilesystemShellTest {
         .thenReturn(new FsNode("/", FsPath.absolute("/"), FsNodeType.VIRTUAL_ROOT));
     when(provider.describe(FsPath.absolute("/db1")))
         .thenReturn(new FsNode("db1", FsPath.absolute("/db1"), FsNodeType.TABLE_DATABASE));
-    when(provider.describe(FsPath.absolute("/db1/table1")))
-        .thenReturn(new FsNode("table1", FsPath.absolute("/db1/table1"), FsNodeType.TABLE_TABLE));
+    when(provider.describe(FsPath.absolute("/db1/table1.csv")))
+        .thenReturn(
+            new FsNode(
+                "table1.csv", FsPath.absolute("/db1/table1.csv"), FsNodeType.TABLE_DATA_FILE));
     when(provider.list(FsPath.absolute("/")))
         .thenReturn(
             Arrays.asList(new FsNode("db1", FsPath.absolute("/db1"), FsNodeType.TABLE_DATABASE)));
     when(provider.list(FsPath.absolute("/db1")))
         .thenReturn(
             Arrays.asList(
-                new FsNode("table1", FsPath.absolute("/db1/table1"), FsNodeType.TABLE_TABLE)));
-    when(provider.list(FsPath.absolute("/db1/table1"))).thenReturn(new ArrayList<>());
+                new FsNode(
+                    "table1.csv", FsPath.absolute("/db1/table1.csv"), FsNodeType.TABLE_DATA_FILE)));
 
-    assertTrue(shell.execute("find / -name table1"));
+    assertTrue(shell.execute("find / -name table1.csv"));
 
-    assertTrue(out.toString().contains("/db1/table1"));
+    assertTrue(out.toString().contains("/db1/table1.csv"));
     verify(provider).list(FsPath.absolute("/"));
     verify(provider).list(FsPath.absolute("/db1"));
   }
 
   @Test
-  public void executeLessAndMoreReadPath() throws SQLException {
-    when(provider.read(FsPath.absolute("/db1/table1"), 20))
-        .thenReturn(Arrays.asList(SqlRow.of("Time", "1", "tag1", "a", "s1", "42")));
+  public void executeLessAndMoreReadMetaFileLines() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.meta"), 20))
+        .thenReturn(Arrays.asList("TableName,Status", "table1,USING"));
 
-    assertTrue(shell.execute("less /db1/table1"));
-    assertTrue(shell.execute("more /db1/table1"));
+    assertTrue(shell.execute("less /db1/table1.meta"));
+    assertTrue(shell.execute("more /db1/table1.meta"));
 
-    assertTrue(out.toString().contains("1\ta\t42"));
-    verify(provider, times(2)).read(FsPath.absolute("/db1/table1"), 20);
+    assertTrue(out.toString().contains("table1,USING"));
+    verify(provider, times(2)).readLines(FsPath.absolute("/db1/table1.meta"), 20);
   }
 
   @Test
@@ -468,12 +537,11 @@ public class FilesystemShellTest {
   @Test
   public void executeFilePrintsUnixFileType() throws SQLException {
     when(provider.describe(FsPath.absolute("/db1/table1")))
-        .thenReturn(new FsNode("table1", FsPath.absolute("/db1/table1"), FsNodeType.TABLE_TABLE));
+        .thenReturn(new FsNode("table1", FsPath.absolute("/db1/table1"), FsNodeType.UNKNOWN));
 
     assertTrue(shell.execute("file /db1/table1"));
 
-    assertTrue(out.toString().contains("/db1/table1: directory"));
-    assertFalse(out.toString().contains("TABLE_TABLE"));
+    assertTrue(out.toString().contains("/db1/table1: unknown"));
     verify(provider).describe(FsPath.absolute("/db1/table1"));
   }
 
@@ -488,20 +556,24 @@ public class FilesystemShellTest {
   }
 
   @Test
-  public void executePasteReadsMultiplePaths() throws SQLException {
-    when(provider.read(
-            Arrays.asList(FsPath.absolute("/db1/table1/tag1"), FsPath.absolute("/db1/table1/s1")),
-            20))
-        .thenReturn(Arrays.asList(SqlRow.of("Time", "1", "tag1", "a", "s1", "42")));
+  public void executePasteReadsMultipleTextFilesLineByLine() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 20))
+        .thenReturn(Arrays.asList("time,key", "1,a", "2,b"));
+    when(provider.readLines(FsPath.absolute("/db1/table2.csv"), 20))
+        .thenReturn(Arrays.asList("time,value", "1,42"));
 
-    assertTrue(shell.execute("paste /db1/table1/tag1 /db1/table1/s1"));
+    assertTrue(shell.execute("paste /db1/table1.csv /db1/table2.csv"));
 
-    assertTrue(out.toString().contains("1\ta\t42"));
-    assertFalse(out.toString().contains("{"));
-    verify(provider)
-        .read(
-            Arrays.asList(FsPath.absolute("/db1/table1/tag1"), FsPath.absolute("/db1/table1/s1")),
-            20);
+    assertEquals(
+        "time,key\ttime,value"
+            + System.lineSeparator()
+            + "1,a\t1,42"
+            + System.lineSeparator()
+            + "2,b\t"
+            + System.lineSeparator(),
+        out.toString());
+    verify(provider).readLines(FsPath.absolute("/db1/table1.csv"), 20);
+    verify(provider).readLines(FsPath.absolute("/db1/table2.csv"), 20);
   }
 
   @Test
@@ -541,7 +613,8 @@ public class FilesystemShellTest {
         .thenReturn(
             Arrays.asList(
                 new FsNode("testtest", FsPath.absolute("/testtest"), FsNodeType.TABLE_DATABASE),
-                new FsNode("value", FsPath.absolute("/value"), FsNodeType.TABLE_COLUMN)));
+                new FsNode(
+                    "value.csv", FsPath.absolute("/value.csv"), FsNodeType.TABLE_DATA_FILE)));
 
     List<String> values = complete(shell.createCompleter(), "cd t");
 
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 eb97455c369..31971bf6a1e 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
@@ -79,90 +79,93 @@ public class FilesystemCommandParserTest {
   }
 
   @Test
-  public void parseCatTablePath() {
-    FilesystemCommand command = FilesystemCommandParser.parse("cat /db1/table1");
+  public void parseCatSidecarPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("cat /db1/table1.csv");
 
     assertEquals(FilesystemCommand.Type.CAT, command.getType());
-    assertEquals("/db1/table1", command.getPath());
+    assertEquals("/db1/table1.csv", command.getPath());
   }
 
   @Test
   public void parseCatMultiplePaths() {
     FilesystemCommand command =
-        FilesystemCommandParser.parse("cat /db1/table1/tag1 /db1/table1/s1");
+        FilesystemCommandParser.parse("cat /db1/table1.csv /db1/table1.meta");
 
     assertEquals(FilesystemCommand.Type.CAT, command.getType());
     assertEquals(2, command.getPaths().size());
-    assertEquals("/db1/table1/tag1", command.getPaths().get(0));
-    assertEquals("/db1/table1/s1", command.getPaths().get(1));
+    assertEquals("/db1/table1.csv", command.getPaths().get(0));
+    assertEquals("/db1/table1.meta", command.getPaths().get(1));
   }
 
   @Test
   public void parseHeadLimitAndPath() {
-    FilesystemCommand command = FilesystemCommandParser.parse("head -n 5 /db1/table1");
+    FilesystemCommand command = FilesystemCommandParser.parse("head -n 5 /db1/table1.csv");
 
     assertEquals(FilesystemCommand.Type.HEAD, command.getType());
-    assertEquals("/db1/table1", command.getPath());
+    assertEquals("/db1/table1.csv", command.getPath());
     assertEquals(5, command.getLimit());
   }
 
   @Test
   public void parseTailLimitAndPath() {
-    FilesystemCommand command = FilesystemCommandParser.parse("tail -n 3 /db1/table1");
+    FilesystemCommand command = FilesystemCommandParser.parse("tail -n 3 /db1/table1.csv");
 
     assertEquals(FilesystemCommand.Type.TAIL, command.getType());
-    assertEquals("/db1/table1", command.getPath());
+    assertEquals("/db1/table1.csv", command.getPath());
     assertEquals(3, command.getLimit());
   }
 
   @Test
   public void parseWcLineCountAndPath() {
-    FilesystemCommand command = FilesystemCommandParser.parse("wc -l /db1/table1");
+    FilesystemCommand command = FilesystemCommandParser.parse("wc -l /db1/table1.csv");
 
     assertEquals(FilesystemCommand.Type.WC, command.getType());
-    assertEquals("/db1/table1", command.getPath());
+    assertEquals("/db1/table1.csv", command.getPath());
     assertEquals("-l", command.getOption());
   }
 
   @Test
   public void parseGrepPatternAndPath() {
-    FilesystemCommand command = FilesystemCommandParser.parse("grep spricoder /db1/table1");
+    FilesystemCommand command = FilesystemCommandParser.parse("grep spricoder /db1/table1.csv");
 
     assertEquals(FilesystemCommand.Type.GREP, command.getType());
-    assertEquals("/db1/table1", command.getPath());
+    assertEquals("/db1/table1.csv", command.getPath());
     assertEquals("spricoder", command.getPattern());
   }
 
   @Test
   public void parseFindNamePatternAndPath() {
-    FilesystemCommand command = FilesystemCommandParser.parse("find /db1 -name table1");
+    FilesystemCommand command = FilesystemCommandParser.parse("find /db1 -name table1.csv");
 
     assertEquals(FilesystemCommand.Type.FIND, command.getType());
     assertEquals("/db1", command.getPath());
-    assertEquals("table1", command.getPattern());
+    assertEquals("table1.csv", command.getPattern());
   }
 
   @Test
   public void parseLessMoreFileAndDu() {
     assertEquals(
-        FilesystemCommand.Type.LESS, FilesystemCommandParser.parse("less /db1/table1").getType());
+        FilesystemCommand.Type.LESS,
+        FilesystemCommandParser.parse("less /db1/table1.csv").getType());
     assertEquals(
-        FilesystemCommand.Type.MORE, FilesystemCommandParser.parse("more /db1/table1").getType());
+        FilesystemCommand.Type.MORE,
+        FilesystemCommandParser.parse("more /db1/table1.csv").getType());
     assertEquals(
-        FilesystemCommand.Type.FILE, FilesystemCommandParser.parse("file /db1/table1").getType());
+        FilesystemCommand.Type.FILE,
+        FilesystemCommandParser.parse("file /db1/table1.csv").getType());
     assertEquals(
-        FilesystemCommand.Type.DU, FilesystemCommandParser.parse("du /db1/table1").getType());
+        FilesystemCommand.Type.DU, FilesystemCommandParser.parse("du /db1/table1.csv").getType());
   }
 
   @Test
   public void parsePastePaths() {
     FilesystemCommand command =
-        FilesystemCommandParser.parse("paste /db1/table1/tag1 /db1/table1/s1");
+        FilesystemCommandParser.parse("paste /db1/table1.csv /db1/table2.csv");
 
     assertEquals(FilesystemCommand.Type.PASTE, command.getType());
     assertEquals(2, command.getPaths().size());
-    assertEquals("/db1/table1/tag1", command.getPaths().get(0));
-    assertEquals("/db1/table1/s1", command.getPaths().get(1));
+    assertEquals("/db1/table1.csv", command.getPaths().get(0));
+    assertEquals("/db1/table2.csv", command.getPaths().get(1));
   }
 
   @Test
@@ -220,15 +223,15 @@ public class FilesystemCommandParserTest {
     assertEquals(FilesystemCommand.Type.MKDIR, mkdir.getType());
     assertEquals("/db1", mkdir.getPath());
 
-    FilesystemCommand rm = FilesystemCommandParser.parse("rm /db1/table1");
+    FilesystemCommand rm = FilesystemCommandParser.parse("rm /db1/table1.csv");
     assertEquals(FilesystemCommand.Type.RM, rm.getType());
-    assertEquals("/db1/table1", rm.getPath());
+    assertEquals("/db1/table1.csv", rm.getPath());
 
-    FilesystemCommand mv = FilesystemCommandParser.parse("mv /db1/table1 /db1/table2");
+    FilesystemCommand mv = FilesystemCommandParser.parse("mv /db1/table1.csv /db1/table2.csv");
     assertEquals(FilesystemCommand.Type.MV, mv.getType());
     assertEquals(2, mv.getPaths().size());
-    assertEquals("/db1/table1", mv.getPaths().get(0));
-    assertEquals("/db1/table2", mv.getPaths().get(1));
+    assertEquals("/db1/table1.csv", mv.getPaths().get(0));
+    assertEquals("/db1/table2.csv", mv.getPaths().get(1));
   }
 
   @Test
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
index 2f23c1d4f1d..17efc72a76a 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
@@ -68,7 +68,7 @@ public class TableFilesystemMutationProviderTest {
   }
 
   @Test
-  public void removeRejectsRootDatabaseSchemaMetaAndLegacyTableLevel() throws SQLException {
+  public void removeRejectsRootDatabaseSchemaMetaBareTableAndColumnPaths() throws SQLException {
     assertInvalidOperation(() -> provider.remove(FsPath.absolute("/")));
     assertInvalidOperation(() -> provider.remove(FsPath.absolute("/db1")));
     assertInvalidOperation(() -> provider.remove(FsPath.absolute("/db1/table1")));
@@ -134,6 +134,17 @@ public class TableFilesystemMutationProviderTest {
     verify(executor).execute("INSERT INTO db1.table1(time, value) VALUES (1, 2.0)");
   }
 
+  @Test
+  public void appendCsvQuotesSpecialIdentifiers() throws SQLException {
+    mockSpecialTableSchema();
+
+    provider.append(FsPath.absolute("/db-1/table-1.csv"), Arrays.asList("time,key-value", "1,a"));
+
+    verify(executor).query("DESC \"db-1\".\"table-1\" DETAILS");
+    verify(executor)
+        .execute("INSERT INTO \"db-1\".\"table-1\"(time, \"key-value\") VALUES (1, 'a')");
+  }
+
   @Test
   public void appendRejectsSidecarAndMissingTime() throws SQLException {
     assertInvalidOperation(
@@ -155,6 +166,16 @@ public class TableFilesystemMutationProviderTest {
                     "ColumnName", "value", "DataType", "DOUBLE")));
   }
 
+  private void mockSpecialTableSchema() throws SQLException {
+    when(executor.query("DESC \"db-1\".\"table-1\" DETAILS"))
+        .thenReturn(
+            org.apache.iotdb.cli.fs.sql.SqlRow.list(
+                org.apache.iotdb.cli.fs.sql.SqlRow.of(
+                    "ColumnName", "time", "DataType", "TIMESTAMP"),
+                org.apache.iotdb.cli.fs.sql.SqlRow.of(
+                    "ColumnName", "key-value", "DataType", "STRING")));
+  }
+
   private static void assertInvalidOperation(SqlOperation operation) throws SQLException {
     try {
       operation.run();
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
index b99a30761d7..cbf254b0eef 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
@@ -31,11 +31,13 @@ import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.sql.SQLException;
-import java.util.Arrays;
 import java.util.List;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 public class TableFilesystemSchemaProviderTest {
@@ -97,37 +99,24 @@ public class TableFilesystemSchemaProviderTest {
   }
 
   @Test
-  public void listTableReturnsColumns() throws SQLException {
-    when(executor.query("DESC db1.table1 DETAILS"))
-        .thenReturn(
-            SqlRow.list(
-                SqlRow.of("ColumnName", "tag1", "DataType", "STRING", "Category", "TAG"),
-                SqlRow.of("ColumnName", "s1", "DataType", "INT32", "Category", "FIELD")));
-
+  public void bareTablePathIsNotDirectoryInSidecarModel() throws SQLException {
     List<FsNode> children = provider.list(FsPath.absolute("/db1/table1"));
+    FsNode node = provider.describe(FsPath.absolute("/db1/table1"));
 
-    assertEquals(2, children.size());
-    assertEquals("tag1", children.get(0).getName());
-    assertEquals(FsNodeType.TABLE_COLUMN, children.get(0).getType());
-    assertEquals("TAG", children.get(0).getMetadata().get("Category"));
-    verify(executor).query("DESC db1.table1 DETAILS");
+    assertEquals(0, children.size());
+    assertEquals("table1", node.getName());
+    assertEquals(FsNodeType.UNKNOWN, node.getType());
+    verifyZeroInteractions(executor);
   }
 
   @Test
-  public void describeColumnReturnsColumnMetadata() throws SQLException {
-    when(executor.query("DESC db1.table1 DETAILS"))
-        .thenReturn(
-            SqlRow.list(
-                SqlRow.of("ColumnName", "tag1", "DataType", "STRING", "Category", "TAG"),
-                SqlRow.of("ColumnName", "s1", "DataType", "INT32", "Category", "FIELD")));
-
+  public void columnPathIsNotAFileInSidecarModel() throws SQLException {
     FsNode node = provider.describe(FsPath.absolute("/db1/table1/s1"));
 
     assertEquals("s1", node.getName());
     assertEquals("/db1/table1/s1", node.getPath().toString());
-    assertEquals(FsNodeType.TABLE_COLUMN, node.getType());
-    assertEquals("INT32", node.getMetadata().get("DataType"));
-    verify(executor).query("DESC db1.table1 DETAILS");
+    assertEquals(FsNodeType.UNKNOWN, node.getType());
+    verifyZeroInteractions(executor);
   }
 
   @Test
@@ -152,45 +141,18 @@ public class TableFilesystemSchemaProviderTest {
   }
 
   @Test
-  public void describeTableReturnsDirectoryNode() throws SQLException {
-    when(executor.query("SHOW TABLES FROM db1"))
-        .thenReturn(SqlRow.list(SqlRow.of("TableName", "table1")));
-
-    FsNode node = provider.describe(FsPath.absolute("/db1/table1"));
-
-    assertEquals("table1", node.getName());
-    assertEquals("/db1/table1", node.getPath().toString());
-    assertEquals(FsNodeType.TABLE_TABLE, node.getType());
-    verify(executor).query("SHOW TABLES FROM db1");
-  }
-
-  @Test
-  public void readColumnSelectsColumnFromTable() throws SQLException {
-    when(executor.query("SELECT s1 FROM db1.table1 LIMIT 5"))
-        .thenReturn(SqlRow.list(SqlRow.of("Time", "1", "s1", "42")));
-
-    List<SqlRow> rows = provider.read(FsPath.absolute("/db1/table1/s1"), 5);
-
-    assertEquals(1, rows.size());
-    assertEquals("42", rows.get(0).get("s1"));
-    verify(executor).query("SELECT s1 FROM db1.table1 LIMIT 5");
-  }
-
-  @Test
-  public void readTableSelectsAllColumnsFromTable() throws SQLException {
-    when(executor.query("SELECT * FROM db1.table1 LIMIT 5"))
-        .thenReturn(SqlRow.list(SqlRow.of("Time", "1", "tag1", "a", "s1", "42")));
-
-    List<SqlRow> rows = provider.read(FsPath.absolute("/db1/table1"), 5);
-
-    assertEquals(1, rows.size());
-    assertEquals("a", rows.get(0).get("tag1"));
-    assertEquals("42", rows.get(0).get("s1"));
-    verify(executor).query("SELECT * FROM db1.table1 LIMIT 5");
+  public void readBareTableAndColumnPathsAreRejected() throws SQLException {
+    assertSqlError(
+        () -> provider.read(FsPath.absolute("/db1/table1"), 5),
+        "Path is not readable: /db1/table1");
+    assertSqlError(
+        () -> provider.read(FsPath.absolute("/db1/table1/s1"), 5),
+        "Path is not readable: /db1/table1/s1");
   }
 
   @Test
   public void readTableCsvReturnsCsvLinesWithHeader() throws SQLException {
+    mockTableExists("db1", "table1");
     when(executor.query("SELECT * FROM db1.table1 LIMIT 5"))
         .thenReturn(SqlRow.list(SqlRow.of("Time", "1", "tag1", "a", "s1", "42")));
 
@@ -201,8 +163,24 @@ public class TableFilesystemSchemaProviderTest {
     verify(executor).query("SELECT * FROM db1.table1 LIMIT 5");
   }
 
+  @Test
+  public void readTableCsvQuotesSpecialIdentifiers() throws SQLException {
+    when(executor.query("SHOW TABLES FROM \"db-1\""))
+        .thenReturn(SqlRow.list(SqlRow.of("TableName", "table-1")));
+    when(executor.query("SELECT * FROM \"db-1\".\"table-1\" LIMIT 5"))
+        .thenReturn(SqlRow.list(SqlRow.of("Time", "1", "tag1", "a")));
+
+    List<String> lines = provider.readLines(FsPath.absolute("/db-1/table-1.csv"), 5);
+
+    assertEquals("Time,tag1", lines.get(0));
+    assertEquals("1,a", lines.get(1));
+    verify(executor).query("SHOW TABLES FROM \"db-1\"");
+    verify(executor).query("SELECT * FROM \"db-1\".\"table-1\" LIMIT 5");
+  }
+
   @Test
   public void readTableSchemaReturnsIoTDBDescCsvLines() throws SQLException {
+    mockTableExists("db1", "table1");
     when(executor.query("DESC db1.table1 DETAILS"))
         .thenReturn(
             SqlRow.list(
@@ -219,6 +197,7 @@ public class TableFilesystemSchemaProviderTest {
 
   @Test
   public void readTableMetaReturnsIoTDBTableCsvLines() throws SQLException {
+    mockTableExists("db1", "table1");
     when(executor.query("SHOW TABLES DETAILS FROM db1"))
         .thenReturn(
             SqlRow.list(
@@ -250,44 +229,101 @@ public class TableFilesystemSchemaProviderTest {
   }
 
   @Test
-  public void tailTableSelectsNewestRowsAndReturnsOriginalOrder() throws SQLException {
+  public void tailTableCsvReturnsLastPhysicalLines() throws SQLException {
+    mockTableExists("db1", "table1");
     when(executor.query("SELECT * FROM db1.table1 ORDER BY time DESC LIMIT 2"))
         .thenReturn(
             SqlRow.list(
-                SqlRow.of("Time", "2", "tag1", "b", "s1", "43"),
-                SqlRow.of("Time", "1", "tag1", "a", "s1", "42")));
+                SqlRow.of("Time", "3", "tag1", "c", "s1", "44"),
+                SqlRow.of("Time", "2", "tag1", "b", "s1", "43")));
 
-    List<SqlRow> rows = provider.tail(FsPath.absolute("/db1/table1"), 2);
+    List<String> lines = provider.tailLines(FsPath.absolute("/db1/table1.csv"), 2);
 
-    assertEquals("1", rows.get(0).get("Time"));
-    assertEquals("2", rows.get(1).get("Time"));
+    assertEquals("2,b,43", lines.get(0));
+    assertEquals("3,c,44", lines.get(1));
     verify(executor).query("SELECT * FROM db1.table1 ORDER BY time DESC LIMIT 2");
   }
 
   @Test
-  public void countTableSelectsRowCount() throws SQLException {
+  public void countTableCsvCountsHeaderAndRows() throws SQLException {
+    mockTableExists("db1", "table1");
     when(executor.query("SELECT COUNT(*) FROM db1.table1"))
         .thenReturn(SqlRow.list(SqlRow.of("count", "2")));
 
     long count = provider.count(FsPath.absolute("/db1/table1.csv"));
 
-    assertEquals(2L, count);
+    assertEquals(3L, count);
     verify(executor).query("SELECT COUNT(*) FROM db1.table1");
   }
 
   @Test
-  public void readColumnsSelectsMultipleColumnsFromSameTable() throws SQLException {
-    when(executor.query("SELECT tag1, s1 FROM db1.table1 LIMIT 5"))
-        .thenReturn(SqlRow.list(SqlRow.of("Time", "1", "tag1", "a", "s1", "42")));
+  public void countSchemaAndMetaCountsCsvLines() throws SQLException {
+    mockTableExists("db1", "table1");
+    when(executor.query("DESC db1.table1 DETAILS"))
+        .thenReturn(
+            SqlRow.list(
+                SqlRow.of("ColumnName", "tag1", "DataType", "STRING", "Category", "TAG"),
+                SqlRow.of("ColumnName", "s1", "DataType", "INT32", "Category", "FIELD")));
+    when(executor.query("SHOW TABLES DETAILS FROM db1"))
+        .thenReturn(
+            SqlRow.list(
+                SqlRow.of("TableName", "table1", "Status", "USING"),
+                SqlRow.of("TableName", "table2", "Status", "USING")));
 
-    List<SqlRow> rows =
-        provider.read(
-            Arrays.asList(FsPath.absolute("/db1/table1/tag1"), FsPath.absolute("/db1/table1/s1")),
-            5);
+    assertEquals(3L, provider.count(FsPath.absolute("/db1/table1.schema")));
+    assertEquals(2L, provider.count(FsPath.absolute("/db1/table1.meta")));
+    verify(executor).query("DESC db1.table1 DETAILS");
+    verify(executor).query("SHOW TABLES DETAILS FROM db1");
+  }
+
+  @Test
+  public void missingSidecarFilesAreRejectedBeforeReadingContent() throws SQLException {
+    when(executor.query("SHOW TABLES FROM db1"))
+        .thenReturn(SqlRow.list(SqlRow.of("TableName", "table2")));
+
+    assertSqlError(
+        () -> provider.readLines(FsPath.absolute("/db1/table1.csv"), 5),
+        "Path does not exist: /db1/table1.csv");
+    assertSqlError(
+        () -> provider.readLines(FsPath.absolute("/db1/table1.schema"), 5),
+        "Path does not exist: /db1/table1.schema");
+    assertSqlError(
+        () -> provider.readLines(FsPath.absolute("/db1/table1.meta"), 5),
+        "Path does not exist: /db1/table1.meta");
+
+    verify(executor, times(3)).query("SHOW TABLES FROM db1");
+  }
+
+  @Test
+  public void missingTableCsvTailAndCountAreRejectedBeforeDataQueries() throws SQLException {
+    when(executor.query("SHOW TABLES FROM db1"))
+        .thenReturn(SqlRow.list(SqlRow.of("TableName", "table2")));
+
+    assertSqlError(
+        () -> provider.tailLines(FsPath.absolute("/db1/table1.csv"), 2),
+        "Path does not exist: /db1/table1.csv");
+    assertSqlError(
+        () -> provider.count(FsPath.absolute("/db1/table1.csv")),
+        "Path does not exist: /db1/table1.csv");
+
+    verify(executor, times(2)).query("SHOW TABLES FROM db1");
+  }
+
+  private static void assertSqlError(SqlOperation operation, String message) throws SQLException {
+    try {
+      operation.run();
+      fail();
+    } catch (SQLException e) {
+      assertEquals(message, e.getMessage());
+    }
+  }
+
+  private void mockTableExists(String database, String table) throws SQLException {
+    when(executor.query("SHOW TABLES FROM " + database))
+        .thenReturn(SqlRow.list(SqlRow.of("TableName", table)));
+  }
 
-    assertEquals(1, rows.size());
-    assertEquals("a", rows.get(0).get("tag1"));
-    assertEquals("42", rows.get(0).get("s1"));
-    verify(executor).query("SELECT tag1, s1 FROM db1.table1 LIMIT 5");
+  private interface SqlOperation {
+    void run() throws SQLException;
   }
 }


(iotdb) 02/06: use sidecat file

Posted by sp...@apache.org.
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 d0672836c6ff1df5dc812f55ab71ac9c123e8f9e
Author: spricoder <sp...@qq.com>
AuthorDate: Thu Apr 30 17:58:29 2026 +0800

    use sidecat file
---
 .../plans/2026-04-29-cli-filesystem-mode.md        | 110 +++++++--
 .../specs/2026-04-29-cli-filesystem-mode-design.md | 238 +++++++++++++------
 .../org/apache/iotdb/cli/fs/FilesystemShell.java   | 186 +++++++++++++--
 .../iotdb/cli/fs/command/FilesystemCommand.java    |   6 +
 .../cli/fs/command/FilesystemCommandParser.java    |  59 ++++-
 .../org/apache/iotdb/cli/fs/node/FsNodeType.java   |   3 +
 .../cli/fs/provider/FilesystemSchemaProvider.java  |   8 +
 .../provider/TableFilesystemMutationProvider.java  |  18 +-
 .../fs/provider/TableFilesystemSchemaProvider.java | 257 ++++++++++++++++++++-
 .../apache/iotdb/cli/fs/FilesystemShellTest.java   | 181 +++++++++++++--
 .../fs/command/FilesystemCommandParserTest.java    |  48 ++++
 .../TableFilesystemMutationProviderTest.java       |  24 +-
 .../TableFilesystemSchemaProviderTest.java         |  88 ++++++-
 13 files changed, 1078 insertions(+), 148 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 fff6ef69730..081a5ce8748 100644
--- a/docs/superpowers/plans/2026-04-29-cli-filesystem-mode.md
+++ b/docs/superpowers/plans/2026-04-29-cli-filesystem-mode.md
@@ -23,7 +23,9 @@
 
 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
 
-**Goal:** Add an explicit read-only filesystem mode to IoTDB CLI while preserving default SQL CLI behavior.
+**Goal:** Add an explicit filesystem mode to IoTDB CLI while preserving default SQL CLI behavior.
+Filesystem mode is read-only by default; table mode has an opt-in minimal write loop behind
+`--fs_write_mode enabled`.
 
 **Architecture:** Add a small `org.apache.iotdb.cli.fs` subsystem with path parsing, command parsing, typed nodes, and tree/table schema providers backed by JDBC SQL. Existing `Cli` keeps ownership of startup parsing and dispatches to filesystem mode only when `--access_mode filesystem` is passed.
 
@@ -42,9 +44,12 @@
 - Create `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNode.java`: typed filesystem node.
 - Create `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java`: minimal JDBC query abstraction.
 - Create `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlRow.java`: row data object for tests and providers.
-- Create `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java`: read-only provider interface.
+- Create `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java`: schema and data read provider interface.
+- Create `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java`: write-gated mutation provider interface.
 - Create `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProvider.java`: tree SQL mapping.
 - Create `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java`: table SQL mapping.
+- Create `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java`: table write mapping.
+- Create `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java`: unsupported mutation provider.
 - Create `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java`: command execution surface and interactive shell.
 - Test `iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/path/FsPathTest.java`.
 - Test `iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParserTest.java`.
@@ -58,17 +63,52 @@ These notes capture follow-up implementation experience for quickly resuming thi
 
 - The current filesystem mode commands are implemented in-process by `FilesystemCommandParser` and
   `FilesystemShell`; they do not call `/bin/ls`, `/bin/cat`, or `java.nio.file.FileSystem`.
-- Keep user-visible output Unix-like:
-  - `ls` prints names only.
+- `FilesystemCommandParser` currently parses `sql <statement>`, but `FilesystemShell` does not
+  execute it and will report `Unsupported filesystem command: SQL`. Raw SQL should be run in SQL
+  access mode until this command is explicitly implemented.
+- Keep user-visible output strictly aligned with standard Unix/POSIX filesystem command semantics.
+  Any deviation is a bug unless it is explicitly documented as a temporary compatibility exception.
+  - `ls` prints names only. The baseline output should be one entry per line, matching `ls -1`; do
+    not use comma-separated output.
+  - `ls -a` and `ll -a` include `.` and `..`; `ll` reuses `ls` option parsing as the long-listing
+    alias.
   - `tree` prints indented names only.
-  - `cat` and `paste` print tab-separated values.
+  - `cat /db/table.csv` prints CSV records. Legacy compatibility table/column paths and `paste`
+    continue to print tab-separated values.
+  - `cut -d, -f2,3 /db/table.csv` is the Unix-compatible multi-field projection form for CSV-first
+    table files. It is delimiter-based text cutting, not CSV quote parsing and not a database
+    column-selection dialect.
+  - `less` and `more` are non-interactive read aliases today; they print readable content with the
+    default read limit.
   - `stat` is the place to show metadata.
-- Do not add filesystem command dialects such as `cat --columns` or `select`. Multi-column reads use
+- Do not add filesystem command dialects such as `cat --columns` or `select`. Multi-column reads
+  should use `cut` for CSV-first table files; legacy column paths may still use
   `paste /db/table/col1 /db/table/col2`.
 - Table provider can optimize Unix-looking commands internally:
-  - `cat /db/table` -> `SELECT * FROM db.table LIMIT 20`
-  - `cat /db/table/col` -> `SELECT col FROM db.table LIMIT 20`
-  - `paste /db/table/col1 /db/table/col2` -> `SELECT col1, col2 FROM db.table LIMIT 20`
+  - Table mode is CSV-first: a database is a directory, and each table is exposed as
+    `/db/table.csv` with `/db/table.schema` and `/db/table.meta` sidecar regular files.
+  - `ls /db` should list `table.csv`, `table.schema`, and `table.meta` entries for each table.
+  - `cat /db/table.csv` -> `SELECT * FROM db.table LIMIT 20`, formatted as CSV.
+  - `cut -d, -f2,3 /db/table.csv` -> delimiter-based field selection over the CSV records.
+  - `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.
+  - Legacy `/db/table/column` paths may remain as compatibility paths or migration sources.
+  - Legacy `cat /db/table/col` -> `SELECT col FROM db.table LIMIT 20`
+  - Legacy `paste /db/table/col1 /db/table/col2` -> `SELECT col1, col2 FROM db.table LIMIT 20`
+- Table-mode write boundaries are intentionally narrow and only active with
+  `--fs_write_mode enabled`:
+  - `mkdir /db` creates a database.
+  - `rm /db/table.csv` drops a table.
+  - `mv /db/t1.csv /db/t2.csv` renames a table inside the same database.
+  - Forbid `rm` or `mv` of `/db/table.schema` and `/db/table.meta`.
+  - Forbid `rm /db`.
+  - Forbid cross-database rename such as `mv /db1/t.csv /db2/t.csv`.
+- Tree-mode writes remain unsupported even when `--fs_write_mode enabled` is set.
+- Filesystem completion is mode-aware after login: filesystem mode installs
+  `FilesystemShell.createCompleter()`, which completes command names at the first word and path
+  children later, appending `/` to directories.
 - Interactive filesystem command errors must be handled at the single-command loop level. A
   `SQLException` from `FilesystemShell.execute()` should print `<command>: <message>` and continue
   the prompt, not bubble out to `receiveCommands()` and exit the CLI.
@@ -77,6 +117,34 @@ These notes capture follow-up implementation experience for quickly resuming thi
   the server returned `550`; the unchecked propagation exited the CLI. Keep a regression test for
   this behavior.
 
+## Supported Command Quick Reference
+
+| Command | Description | Example |
+| --- | --- | --- |
+| `pwd` | Print the current filesystem path. | `pwd` |
+| `ls [-a|-l|-la] [path]` | List child names; `-a` includes dot entries and `-l` enables long listing. | `ls -la /db` |
+| `ll [-a] [path]` | Long listing alias with read-only permissions in output. | `ll -a /db` |
+| `cd <path>` | Change directory when the target is a directory node. | `cd /db` |
+| `stat [path]` | Print filesystem-style metadata for a node. | `stat /db/table.csv` |
+| `cat <path>...` | Print readable paths sequentially. | `cat /db/table.csv` |
+| `head [-n lines] <path>` | Print the first rows or text lines; short form such as `-5` is accepted. | `head -n 5 /db/table.csv` |
+| `tail [-n lines] <path>` | Print the last rows or text lines where supported. | `tail -n 5 /db/table.csv` |
+| `wc -l <path>` | Print logical count and path. | `wc -l /db/table.csv` |
+| `grep <pattern> <path>` | Print rows or lines containing a literal substring. | `grep spricoder /db/table.csv` |
+| `find [path] [-name name]` | Recursively print matching paths; `-name` is exact node-name matching. | `find /db -name table.csv` |
+| `less <path>` | Non-interactive read alias using the default read limit. | `less /db/table.csv` |
+| `more <path>` | Non-interactive read alias using the default read limit. | `more /db/table.schema` |
+| `file <path>` | Print `directory`, `regular file`, or `unknown`. | `file /db/table.meta` |
+| `du <path>` | Print logical count and path using provider count. | `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 readable paths side by side; table mode supports legacy same-table column paths. | `paste /db/table/key /db/table/value` |
+| `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` |
+| `mv <source> <target>` | Write-gated; in table mode with writes enabled, only same-database table CSV rename is allowed. | `mv /db/t1.csv /db/t2.csv` |
+| `help` | Print filesystem-mode help. | `help` |
+| `exit` / `quit` | Exit filesystem mode. | `exit` |
+
 ## Subagent Usage Notes
 
 All later work on this feature may use subagents to accelerate execution. Prefer subagents when the
@@ -168,7 +236,7 @@ test commands listed below.
 - The broader filesystem-mode focused suite is:
 
   ```bash
-  mvn test -o -nsu -Dtest=AbstractCliTest,CliFilesystemModeTest,FsPathTest,FilesystemCommandParserTest,TreeFilesystemSchemaProviderTest,TableFilesystemSchemaProviderTest,FilesystemShellTest,JdbcSqlExecutorTest
+  mvn test -o -nsu -Dtest=AbstractCliTest,CliFilesystemModeTest,JlineUtilsTest,FsPathTest,FilesystemCommandParserTest,TreeFilesystemSchemaProviderTest,TableFilesystemSchemaProviderTest,TableFilesystemMutationProviderTest,FilesystemShellTest,JdbcSqlExecutorTest
   ```
 
 - Maven/Develocity may print `Operation not permitted` stack traces for writes under
@@ -249,7 +317,11 @@ Expected: all tree provider tests pass.
 
 - [ ] **Step 1: Write failing `TableFilesystemSchemaProviderTest`**
 
-Use a mocked `SqlExecutor` to verify `list(/)`, `list(/db)`, `list(/db/table)`, `describe(/db/table/col)`, and `read(/db/table/col)` issue expected SQL and return typed nodes.
+Use a mocked `SqlExecutor` to verify `list(/)`, `list(/db)`, `describe(/db/table.csv)`,
+`read(/db/table.csv)`, `describe(/db/table.schema)`, `readLines(/db/table.schema)`,
+`describe(/db/table.meta)`, and `readLines(/db/table.meta)` issue expected SQL and return typed nodes
+or text lines.
+Also cover legacy `/db/table/col` paths if compatibility behavior remains enabled.
 
 - [ ] **Step 2: Run test to verify it fails**
 
@@ -258,7 +330,10 @@ Expected: compilation failure or missing behavior.
 
 - [ ] **Step 3: Implement table provider**
 
-Implement database/table/column mapping with centralized identifier rendering.
+Implement CSV-first database/table sidecar mapping with centralized identifier rendering:
+`/db/table.csv` is the table data regular file, `/db/table.schema` is the schema sidecar regular
+file, and `/db/table.meta` is the metadata sidecar regular file. Keep `/db/table/column` only as a
+legacy compatibility path or migration source.
 
 - [ ] **Step 4: Run test to verify it passes**
 
@@ -282,22 +357,23 @@ Add the long option and route filesystem mode to `FilesystemShell` without chang
 
 - [ ] **Step 4: Run focused tests**
 
-Run: `mvn test -pl iotdb-client/cli -Dtest=AbstractCliTest,FsPathTest,FilesystemCommandParserTest,TreeFilesystemSchemaProviderTest,TableFilesystemSchemaProviderTest`
+Run: `mvn test -pl iotdb-client/cli -Dtest=AbstractCliTest,CliFilesystemModeTest,JlineUtilsTest,FsPathTest,FilesystemCommandParserTest,TreeFilesystemSchemaProviderTest,TableFilesystemSchemaProviderTest,TableFilesystemMutationProviderTest,FilesystemShellTest,JdbcSqlExecutorTest`
 Expected: all focused tests pass.
 
 ### Task 6: Verification
 
 - [ ] **Step 1: Run CLI module unit tests**
 
-Run: `mvn test -pl iotdb-client/cli`
-Expected: CLI module unit tests pass, or any unrelated pre-existing failure is documented with output.
+Run: `mvn test -o -nsu -Dtest=AbstractCliTest,CliFilesystemModeTest,JlineUtilsTest,FsPathTest,FilesystemCommandParserTest,TreeFilesystemSchemaProviderTest,TableFilesystemSchemaProviderTest,TableFilesystemMutationProviderTest,FilesystemShellTest,JdbcSqlExecutorTest` from `iotdb-client/cli`.
+Expected: focused filesystem-mode unit tests pass, or any unrelated pre-existing failure is
+documented with output.
 
 - [ ] **Step 2: Run formatting**
 
-Run: `mvn spotless:apply -pl iotdb-client/cli`
+Run: `mvn spotless:apply -o -nsu` from `iotdb-client/cli`.
 Expected: formatting applied without errors.
 
 - [ ] **Step 3: Re-run focused tests after formatting**
 
-Run: `mvn test -pl iotdb-client/cli -Dtest=AbstractCliTest,FsPathTest,FilesystemCommandParserTest,TreeFilesystemSchemaProviderTest,TableFilesystemSchemaProviderTest`
+Run: `mvn test -o -nsu -Dtest=AbstractCliTest,CliFilesystemModeTest,JlineUtilsTest,FsPathTest,FilesystemCommandParserTest,TreeFilesystemSchemaProviderTest,TableFilesystemSchemaProviderTest,TableFilesystemMutationProviderTest,FilesystemShellTest,JdbcSqlExecutorTest` from `iotdb-client/cli`.
 Expected: all focused tests pass.
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 0d40b79a167..2de2d8a71c6 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
@@ -27,9 +27,14 @@ Extend the IoTDB CLI with an explicit filesystem access mode that lets users bro
 metadata through directory-like commands while preserving the existing SQL CLI behavior by
 default.
 
-The first version is read-only. The architecture must still leave clear extension points for
-future write operations such as creating databases, creating tables, dropping schema objects, or
-writing data.
+Filesystem-mode command behavior and output must strictly follow standard Unix/POSIX filesystem
+command semantics wherever an equivalent command exists. A deviation is treated as a bug unless it
+is explicitly documented as a temporary compatibility exception.
+
+Filesystem mode is read-only by default. Table mode also exposes a minimal, opt-in write loop
+behind `--fs_write_mode enabled`; unsupported or unsafe write paths must fail with filesystem-style
+errors. The architecture must still leave clear extension points for richer future write operations
+such as creating tables, altering schema objects, or writing data rows.
 
 ## Non-Goals
 
@@ -37,7 +42,9 @@ writing data.
 - Do not change the default SQL CLI behavior.
 - Do not make filesystem commands available implicitly in the existing SQL mode.
 - Do not bypass server-side SQL, permissions, dialect handling, timeout handling, or SSL handling.
-- Do not implement write operations in the first version.
+- Do not expose broad write operations by default. Writes are disabled unless
+  `--fs_write_mode enabled` is set, and the current writable surface is intentionally limited to
+  table-mode database creation, table drop, and same-database table rename.
 
 ## Compatibility Requirements
 
@@ -52,6 +59,8 @@ Backward compatibility is a hard requirement.
   established meanings in CLI and tool scripts.
 - The new mode must be selected explicitly with a long option such as
   `--access_mode filesystem`; the default remains `sql`.
+- Filesystem writes must be selected explicitly with `--fs_write_mode enabled`; the default remains
+  `disabled`.
 - In filesystem mode, `-e` executes a filesystem command such as `ls /`, not a SQL statement. This
   distinction must be documented in help output.
 
@@ -91,6 +100,9 @@ Add a new access mode to the existing `Cli` entry point:
 - `--access_mode sql`: default. Runs the current SQL CLI path.
 - `--access_mode filesystem`: runs the new filesystem shell after the existing authentication and
   JDBC connection setup.
+- `--fs_write_mode disabled`: default. Filesystem write commands return read-only errors.
+- `--fs_write_mode enabled`: enables the table-mode mutation provider. Tree-mode writes still use
+  the unsupported mutation provider.
 
 The current `Cli` class remains responsible for command-line option parsing and connection setup.
 After the mode is known, it dispatches to either the existing SQL command loop or the new
@@ -131,14 +143,30 @@ on children, while `stat` performs more detailed resolution.
 
 ### Table Model Mapping
 
-When `sql_dialect=table`, filesystem paths map to relational schema objects.
+When `sql_dialect=table`, filesystem paths use a CSV-first sidecar model. A database is exposed as
+a directory. Each table is exposed as a data regular file named `<table>.csv`, with adjacent
+sidecar regular files named `<table>.schema` and `<table>.meta`.
+
+The table data file is the primary path for user data operations:
+
+- `/<database>/<table>.csv`: table rows as CSV-like tabular content.
+- `/<database>/<table>.schema`: sidecar schema content rendered as CSV from
+  `DESC <database>.<table> DETAILS`, preserving the column names and values returned by IoTDB.
+- `/<database>/<table>.meta`: sidecar table metadata rendered as CSV from the table row returned by
+  `SHOW TABLES DETAILS FROM <database>`, preserving the column names and values returned by IoTDB.
+
+The legacy column-oriented path `/<database>/<table>/<column>` is not the primary table-model
+filesystem abstraction. It may remain as a compatibility path for existing read behavior during a
+migration period, or be documented as the migration source when moving users to the sidecar model.
 
 | Filesystem Path | IoTDB Object | Node Type | Discovery |
 | --- | --- | --- | --- |
 | `/` | Virtual root | `VIRTUAL_ROOT` | `SHOW DATABASES` |
-| `/<database>` | Database | `TABLE_DATABASE` | `SHOW DATABASES` |
-| `/<database>/<table>` | Table or view | `TABLE_TABLE` / `TABLE_VIEW` | `SHOW TABLES DETAILS FROM <database>` |
-| `/<database>/<table>/<column>` | Column | `TABLE_COLUMN` | `DESC <database>.<table> DETAILS` |
+| `/<database>` | Database directory | `TABLE_DATABASE` | `SHOW DATABASES` |
+| `/<database>/<table>.csv` | Table data regular file | `TABLE_DATA_FILE` | `SHOW TABLES FROM <database>` |
+| `/<database>/<table>.schema` | Schema sidecar regular file | `TABLE_SCHEMA_FILE` | Exists via `SHOW TABLES FROM <database>`; content via `DESC <database>.<table> DETAILS` |
+| `/<database>/<table>.meta` | Metadata sidecar regular file | `TABLE_META_FILE` | Exists via `SHOW TABLES FROM <database>`; content via `SHOW TABLES DETAILS FROM <database>` |
+| `/<database>/<table>/<column>` | Legacy column compatibility path | `TABLE_COLUMN` | `DESC <database>.<table> DETAILS` |
 
 Table-model devices from `SHOW DEVICES FROM <table>` are not part of the first version's base path
 hierarchy because they are data-instance-oriented rather than schema-container-oriented. They can
@@ -151,31 +179,51 @@ be added later as a virtual directory such as `/<database>/<table>/.devices`.
 - Relative paths are resolved against the current directory.
 - Attempts to navigate above `/` resolve to `/`.
 - Tree-model paths must begin at `/root` for real IoTDB metadata.
-- Table-model paths use `/database/table/column`.
-- Wildcard paths are not treated as filesystem nodes in the first version. Users can run wildcard
-  SQL through `sql <statement>`.
+- Table-model paths use `/database/table.csv`, `/database/table.schema`, and
+  `/database/table.meta`. The database component is a directory; the table data and sidecar paths
+  are regular files.
+- Legacy table-model paths of the form `/database/table/column` are compatibility paths or
+  migration inputs, not the primary table filesystem model.
+- Wildcard paths are not treated as filesystem nodes in the first version. Users should run
+  wildcard SQL through normal SQL mode.
 - SQL escaping and identifier quoting must be centralized in provider helper methods. Command
   implementations must not hand-build SQL strings for IoTDB identifiers.
 
-## Commands
-
-The first version supports a compact read-only command set.
-
-| Command | Behavior |
-| --- | --- |
-| `pwd` | Print the current filesystem path. |
-| `ls [path]` | List child nodes for a directory. |
-| `cd <path>` | Change current directory if the target is a directory node. |
-| `stat [path]` | Print node type and metadata. |
-| `cat <path>` | Print file-like schema content for a leaf node. |
-| `paste <path>...` | Print multiple file-like paths side by side, following Unix `paste` semantics. |
-| `tree [path] [-L depth]` | Recursively list children with an explicit or default depth limit. |
-| `sql <statement>` | Execute a raw SQL statement through the existing SQL result printer. |
-| `help` | Print filesystem-mode help. |
-| `exit` / `quit` | Exit filesystem mode. |
-
-Unsupported write-oriented commands can be reserved for future use and return a clear read-only
-message if introduced before write support.
+## Supported Command Reference
+
+Filesystem mode commands are implemented in-process by `FilesystemCommandParser` and
+`FilesystemShell`. They are not delegated to `/bin/ls`, `/bin/cat`, `/bin/cut`, or
+`java.nio.file.FileSystem`, but their visible syntax and output should match Unix command
+semantics wherever the same command exists. Provider support can still vary by dialect and path.
+
+| Command | Description | Example |
+| --- | --- | --- |
+| `pwd` | Print the current filesystem path. | `pwd` |
+| `ls [-a|-l|-la] [path]` | List child names, one per line. `-a` includes `.` and `..`; `-l` uses long listing output. | `ls -a /db` |
+| `ll [-a] [path]` | Long listing alias. Uses read-only permissions by default: directories as `dr-xr-xr-x`, files as `-r--r--r--`. | `ll -a /db` |
+| `cd <path>` | Change the current directory only if the target is a directory node. | `cd /db` |
+| `stat [path]` | Print filesystem-style metadata, including path, Unix type, and provider metadata. | `stat /db/table.csv` |
+| `cat <path>...` | Print one or more readable paths sequentially. Table `.csv`, `.schema`, and `.meta` sidecars print CSV lines; legacy table/column paths print tab-separated values. | `cat /db/table.csv` |
+| `head [-n lines] <path>` | Print the first rows or text lines for a readable path. Short numeric form such as `head -5 <path>` is also parsed. | `head -n 5 /db/table.csv` |
+| `tail [-n lines] <path>` | Print the last rows or text lines where the provider supports tail. Table `.csv` uses `ORDER BY time DESC LIMIT n` internally and returns original order. | `tail -n 5 /db/table.csv` |
+| `wc -l <path>` | Print logical row or line count plus path. Only `-l` is supported. | `wc -l /db/table.csv` |
+| `grep <pattern> <path>` | Print lines or rows containing the literal pattern. This is substring matching, not regular-expression matching. | `grep spricoder /db/table.csv` |
+| `find [path] [-name name]` | Recursively list the starting path and descendants whose node name exactly matches `name`; without `-name`, it prints all visited paths. | `find /db -name table.csv` |
+| `less <path>` | Current implementation prints readable content like `cat` with the default read limit; it is not an interactive pager. | `less /db/table.csv` |
+| `more <path>` | Current implementation prints readable content like `cat` with the default read limit; it is not an interactive pager. | `more /db/table.schema` |
+| `file <path>` | Print the Unix type for the path: `directory`, `regular file`, or `unknown`. | `file /db/table.meta` |
+| `du <path>` | Print logical count plus path, using the provider count operation. | `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 file-like paths side by side. Table mode currently supports legacy same-table column paths and optimizes them to one SQL projection. | `paste /db/table/key /db/table/value` |
+| `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` |
+| `mv <source> <target>` | Write-gated command. With table mode and `--fs_write_mode enabled`, only same-database table CSV rename is allowed. | `mv /db/t1.csv /db/t2.csv` |
+| `help` | Print filesystem-mode help. | `help` |
+| `exit` / `quit` | Exit filesystem mode. | `exit` |
+
+The parser currently recognizes `sql <statement>`, but `FilesystemShell` does not execute it yet.
+Raw SQL should be run in the default SQL access mode.
 
 ## Command Mapping
 
@@ -196,24 +244,39 @@ message if introduced before write support.
 | Operation | SQL/API Mapping |
 | --- | --- |
 | `ls /` | `SHOW DATABASES`. |
-| `ls /db` | `SHOW TABLES FROM db`. |
-| `ls /db/table` | `DESC db.table`. |
+| `ls /db` | `SHOW TABLES FROM db`, formatted as `table.csv`, `table.schema`, and `table.meta` entries for each table. |
 | `stat /db` | `SHOW DATABASES DETAILS`, filtered to the database. |
-| `stat /db/table` | `SHOW TABLES DETAILS FROM db`, filtered to the table. |
-| `stat /db/table/col` | `DESC db.table DETAILS`, filtered to the column. |
-| `cat /db/table/col` | `DESC db.table DETAILS`, formatted as schema text for the column. |
+| `stat /db/table.csv` | `SHOW TABLES FROM db`, filtered to the table and rendered as filesystem metadata for the data file. |
+| `stat /db/table.schema` | `SHOW TABLES FROM db`, filtered to the table and rendered as filesystem metadata for the schema sidecar. |
+| `stat /db/table.meta` | `SHOW TABLES FROM db`, filtered to the table and rendered as filesystem metadata for the metadata sidecar. |
+| `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. |
+| `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. |
+| `stat /db/table/col` | Legacy compatibility: `DESC db.table DETAILS`, filtered to the column. |
+| `cat /db/table/col` | Legacy compatibility: `SELECT col FROM db.table LIMIT <limit>`. |
 
 ## Unix Output Semantics
 
-Filesystem mode should keep command output close to standard Unix command behavior. Avoid exposing
-internal implementation types or Java debug-style structures in normal command output.
+Filesystem mode must keep command output aligned with standard Unix command behavior. Avoid
+exposing internal implementation types or Java debug-style structures in normal command output.
 
-- `ls` prints child names only, one entry per line.
+- `ls` prints child names only. The baseline implementation should use one entry per line, matching
+  `ls -1`; it must not introduce comma-separated output or database-specific listing dialects.
+- `ls -a` and `ll -a` include `.` and `..` before normal entries.
 - `tree` prints the hierarchy with indentation and names only.
-- `cat` prints row content as tab-separated values, one row per line.
+- `cat` prints regular file content without Java object formatting. For table data files this is
+  CSV; for legacy compatibility table/column paths this remains tab-separated row values.
+- `cut -d, -f2,3 /db/table.csv` is the Unix-compatible way to project fields from table CSV
+  content. It performs delimiter-based text cutting like Unix `cut`; it does not parse CSV quoting
+  or introduce table-specific column-selection flags.
 - `paste` prints multiple file-like paths side by side as tab-separated values.
+- `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.
+- `mkdir`, `rm`, and `mv` are write-gated. In default mode they report a read-only filesystem.
+  When enabled, unsupported levels must report an invalid filesystem write operation instead of
+  falling through to broad SQL execution.
 - Error output should follow a command-prefixed style such as `cat: <message>` or
   `cd: <path>: Not a directory`.
 
@@ -221,19 +284,38 @@ This means provider-internal node types such as `TABLE_DATABASE`, `TABLE_COLUMN`
 `TREE_DATABASE` must not appear in `ls` or `tree` output. Similarly, `SqlRow.asMap().toString()`
 must not be used for `cat` or `paste` output.
 
+## Completion Semantics
+
+Interactive completion must be mode-aware:
+
+- SQL mode keeps the existing SQL completer.
+- Filesystem mode installs `FilesystemShell.createCompleter()`.
+- At the first word, filesystem completion suggests filesystem commands.
+- At later words, filesystem completion lists children from the relevant path, filters by prefix,
+  and appends `/` to directory candidates.
+- Completion failures are ignored so TAB never interrupts the interactive session.
+
 ## Read Semantics
 
 Table-mode read behavior is currently:
 
-- `cat /db/table` maps to `SELECT * FROM db.table LIMIT <limit>`.
-- `cat /db/table/column` maps to `SELECT column FROM db.table LIMIT <limit>`.
-- `paste /db/table/col1 /db/table/col2` maps to
+- `cat /db/table.csv` maps to `SELECT * FROM db.table LIMIT <limit>`.
+- `cat /db/table.schema` maps to `DESC db.table DETAILS` and preserves IoTDB result columns in CSV.
+- `cat /db/table.meta` maps to `SHOW TABLES DETAILS FROM db`, filters to the table row, and
+  preserves IoTDB result columns in CSV.
+- Legacy `cat /db/table/column` may map to `SELECT column FROM db.table LIMIT <limit>` during the
+  migration period.
+- Legacy `paste /db/table/col1 /db/table/col2` may map to
   `SELECT col1, col2 FROM db.table LIMIT <limit>` when all paths are columns from the same table.
 
 Although `paste` is implemented through one optimized SQL query for table mode, the user-facing
 semantics remain Unix-like: users pass multiple file paths to a standard Unix command rather than
 using a database-specific `select` command or `cat --columns` dialect.
 
+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.
+
 In interactive filesystem mode, a single command failure must not terminate the CLI session. For
 example, if `cat time` is resolved from `/testtest` to `/testtest/time`, table mode treats that as a
 table path and may receive a server error such as `550: Table 'testtest.time' does not exist`. That
@@ -244,39 +326,61 @@ error should be printed as `cat: 550: ...`, then the prompt should continue.
 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 handlers for `pwd`, `ls`, `cd`, `stat`, `cat`, `tree`,
-  `sql`, and `help`.
+- `command/*`: command parsing and command value objects for filesystem commands such as `pwd`,
+  `ls`, `cd`, `stat`, `cat`, `cut`, `paste`, `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`: read-only provider interface.
+- `provider/FilesystemSchemaProvider`: schema and data read provider interface.
+- `provider/FilesystemMutationProvider`: write-gated mutation provider interface.
 - `provider/TreeFilesystemSchemaProvider`: tree-model SQL mapping.
 - `provider/TableFilesystemSchemaProvider`: table-model SQL mapping.
+- `provider/TableFilesystemMutationProvider`: opt-in table-model write mapping.
+- `provider/UnsupportedFilesystemMutationProvider`: mutation provider used when writes are disabled
+  at the provider layer or unsupported by the dialect.
 - `sql/SqlExecutor`: JDBC statement execution and result extraction helpers.
 - `print/FilesystemPrinter`: text output for filesystem commands.
 
 Existing SQL CLI code should not be broadly refactored. The implementation should only add the
-small hooks needed to select filesystem mode and to let `sql <statement>` reuse existing SQL result
-printing.
+small hooks needed to select filesystem mode, install the mode-specific completer, and route
+filesystem commands through the provider layer. Raw SQL execution remains the responsibility of SQL
+access mode until a filesystem-mode `sql` command is explicitly implemented.
 
 ## Provider Interface Direction
 
-The first version needs read-only operations:
+Filesystem mode separates reads from mutations. The schema provider owns read operations:
 
-- `resolve(FsPath path)`
 - `list(FsPath path)`
 - `describe(FsPath path)`
 - `read(FsPath path)`
+- `readLines(FsPath path)` for text sidecars such as `.csv`, `.schema`, and `.meta`
+- `tail(FsPath path)` / `tailLines(FsPath path)` where the provider supports tail
+- `count(FsPath path)` where the provider supports logical count
+- `read(List<FsPath> paths)` for provider-optimized multi-path reads such as legacy table-column
+  `paste`
+
+The mutation provider owns the current write-gated operations:
+
+- `mkdir(FsPath path)`
+- `remove(FsPath path)`
+- `move(FsPath source, FsPath target)`
+
+When writes are disabled, `FilesystemShell` rejects `mkdir`, `rm`, and `mv` before calling the
+mutation provider. When writes are enabled, table mode uses `TableFilesystemMutationProvider`; tree
+mode still uses `UnsupportedFilesystemMutationProvider`.
+
+The current table-model write boundary is intentionally narrow:
 
-The interface should be shaped so future writes can be added without changing command parsing:
+- `mkdir /db` creates a database directory.
+- `rm /db/table.csv` drops the table data file and therefore the table.
+- `mv /db/t1.csv /db/t2.csv` renames a table within the same database.
 
-- `create(FsPath path, CreateOptions options)`
-- `delete(FsPath path, DeleteOptions options)`
-- `rename(FsPath source, FsPath target)`
-- `write(FsPath path, FsWriteContent content, WriteOptions options)`
+The sidecar files are metadata views, not independently writable objects:
 
-The first version exposes only read methods in the public provider interface. Write operation names
-remain documented here as future extension points, and command handlers for unsupported write-like
-commands return a read-only error instead of calling provider methods.
+- `rm /db/table.schema` and `rm /db/table.meta` are forbidden.
+- `mv /db/table.schema ...` and `mv /db/table.meta ...` are forbidden.
+- `rm /db` is forbidden; database deletion must remain an explicit SQL operation or a separately
+  designed filesystem command with stronger safeguards.
+- Cross-database table rename, such as `mv /db1/t.csv /db2/t.csv`, is forbidden.
 
 ## Dependency Strategy
 
@@ -285,7 +389,7 @@ Use existing, proven dependencies already present in the CLI module:
 - `commons-cli` for startup argument parsing.
 - JLine for terminal input, history, and autosuggestion.
 - JDBC and IoTDB SQL for metadata access.
-- Existing IoTDB result printing where raw SQL output is required.
+- Existing IoTDB result printing if a future filesystem-mode raw SQL command is added.
 
 Do not add a shell framework, FUSE dependency, or local filesystem abstraction library for the
 first version. The only custom path logic should be the IoTDB-specific mapping from slash paths to
@@ -330,14 +434,18 @@ 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 `ls`, `cd`, `stat`, `cat`, `tree -L`, `sql`,
-  `help`, `exit`, and `quit`.
+- 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
+  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.
+- Provider tests with a mocked `SqlExecutor`, verifying table-mode path-to-SQL mapping, CSV
+  sidecars, IoTDB-preserved `.schema`/`.meta` content, multi-column legacy reads, and table
+  mutation restrictions.
 - CLI option tests extending existing CLI unit coverage for default `access_mode`, filesystem
-  mode, and invalid mode values.
+  mode, invalid mode values, and `fs_write_mode`.
 - Shell tests proving SQL mode remains the default and filesystem mode dispatches to
-  `FilesystemShell`.
+  `FilesystemShell`, keeps single-command failures inside the prompt loop, and preserves Unix-like
+  output.
 
 Integration tests against a real IoTDB cluster can be added after the unit-tested shell behavior is
 stable.
@@ -349,5 +457,5 @@ stable.
 - Identifier escaping bugs: centralize escaping in provider helpers.
 - Backward compatibility regressions: make `sql` the default access mode and keep SQL-mode command
   processing unchanged.
-- Future write support complexity: route all object semantics through typed nodes and providers,
-  not through command-specific SQL construction.
+- Write support complexity: keep writes opt-in and narrow, and route object semantics through typed
+  paths and mutation providers rather than broad command-specific SQL construction.
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 8929c964524..3ac9eca9d94 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
@@ -40,6 +40,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Pattern;
 
 public class FilesystemShell {
 
@@ -47,7 +48,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", "paste", "tree", "help", "exit", "quit");
+          "more", "file", "du", "mkdir", "rm", "mv", "cut", "paste", "tree", "help", "exit",
+          "quit");
 
   private final CliContext ctx;
   private final FilesystemSchemaProvider provider;
@@ -77,10 +79,10 @@ public class FilesystemShell {
         ctx.getPrinter().println(currentPath.toString());
         return true;
       case LS:
-        printNodes(provider.list(resolve(command.getPath())));
+        printNodes(provider.list(resolve(command.getPath())), isAllOption(command));
         return true;
       case LL:
-        printLongNodes(provider.list(resolve(command.getPath())));
+        printLongNodes(provider.list(resolve(command.getPath())), isAllOption(command));
         return true;
       case CD:
         changeDirectory(command.getPath());
@@ -92,10 +94,10 @@ public class FilesystemShell {
         printSequentialReads(command.getPaths(), DEFAULT_READ_LIMIT);
         return true;
       case HEAD:
-        printRows(provider.read(resolve(command.getPath()), command.getLimit()));
+        printHead(command.getPath(), command.getLimit());
         return true;
       case TAIL:
-        printRows(provider.tail(resolve(command.getPath()), command.getLimit()));
+        printTail(command.getPath(), command.getLimit());
         return true;
       case WC:
         printLineCount(command.getPath());
@@ -108,7 +110,7 @@ public class FilesystemShell {
         return true;
       case LESS:
       case MORE:
-        printRows(provider.read(resolve(command.getPath()), DEFAULT_READ_LIMIT));
+        printReadable(command.getPath(), DEFAULT_READ_LIMIT);
         return true;
       case FILE:
         printFile(command.getPath());
@@ -125,6 +127,9 @@ public class FilesystemShell {
       case MV:
         move(command.getPaths());
         return true;
+      case CUT:
+        printCut(command.getPath(), command.getOption(), command.getPattern());
+        return true;
       case PASTE:
         printRows(provider.read(resolve(command.getPaths()), DEFAULT_READ_LIMIT));
         return true;
@@ -196,29 +201,31 @@ public class FilesystemShell {
     return resolvedPaths;
   }
 
-  private void printNodes(List<FsNode> nodes) {
-    StringBuilder builder = new StringBuilder();
-    for (FsNode node : nodes) {
-      if (builder.length() > 0) {
-        builder.append(',');
-      }
-      builder.append(node.getName());
+  private void printNodes(List<FsNode> nodes, boolean all) {
+    if (all) {
+      ctx.getPrinter().println(".");
+      ctx.getPrinter().println("..");
     }
-    if (builder.length() > 0) {
-      ctx.getPrinter().println(builder.toString());
+    for (FsNode node : nodes) {
+      ctx.getPrinter().println(node.getName());
     }
   }
 
-  private void printLongNodes(List<FsNode> nodes) {
+  private void printLongNodes(List<FsNode> nodes, boolean all) {
+    if (all) {
+      ctx.getPrinter().println(longMode(FsNodeType.VIRTUAL_ROOT) + "  1 iotdb iotdb 0 .");
+      ctx.getPrinter().println(longMode(FsNodeType.VIRTUAL_ROOT) + "  1 iotdb iotdb 0 ..");
+    }
     for (FsNode node : nodes) {
       ctx.getPrinter().println(longMode(node.getType()) + "  1 iotdb iotdb 0 " + node.getName());
     }
   }
 
   private void printNode(FsNode node) {
-    ctx.getPrinter().println(node.getName() + "\t" + node.getType());
+    ctx.getPrinter().println("File: " + node.getPath());
+    ctx.getPrinter().println("Type: " + unixType(node.getType()));
     for (Map.Entry<String, String> entry : node.getMetadata().entrySet()) {
-      ctx.getPrinter().println(entry.getKey() + "\t" + entry.getValue());
+      ctx.getPrinter().println(entry.getKey() + ": " + entry.getValue());
     }
   }
 
@@ -228,10 +235,38 @@ public class FilesystemShell {
     }
   }
 
+  private void printLines(List<String> lines) {
+    for (String line : lines) {
+      ctx.getPrinter().println(line);
+    }
+  }
+
   private void printSequentialReads(List<String> paths, int limit) throws SQLException {
     for (String path : paths) {
-      printRows(provider.read(resolve(path), limit));
+      printReadable(path, limit);
+    }
+  }
+
+  private void printReadable(String path, int limit) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    if (isTextFile(resolvedPath)) {
+      printLines(provider.readLines(resolvedPath, limit));
+      return;
+    }
+    printRows(provider.read(resolvedPath, limit));
+  }
+
+  private void printHead(String path, int limit) throws SQLException {
+    printReadable(path, limit);
+  }
+
+  private void printTail(String path, int limit) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    if (isTextFile(resolvedPath)) {
+      printLines(provider.tailLines(resolvedPath, limit));
+      return;
     }
+    printRows(provider.tail(resolvedPath, limit));
   }
 
   private void printLineCount(String path) throws SQLException {
@@ -240,7 +275,16 @@ public class FilesystemShell {
   }
 
   private void printMatchingRows(String path, String pattern) throws SQLException {
-    for (SqlRow row : provider.read(resolve(path), DEFAULT_READ_LIMIT)) {
+    FsPath resolvedPath = resolve(path);
+    if (isTextFile(resolvedPath)) {
+      for (String line : provider.readLines(resolvedPath, DEFAULT_READ_LIMIT)) {
+        if (line.contains(pattern)) {
+          ctx.getPrinter().println(line);
+        }
+      }
+      return;
+    }
+    for (SqlRow row : provider.read(resolvedPath, DEFAULT_READ_LIMIT)) {
       String line = joinValues(row);
       if (line.contains(pattern)) {
         ctx.getPrinter().println(line);
@@ -248,6 +292,24 @@ public class FilesystemShell {
     }
   }
 
+  private void printCut(String path, String delimiter, String fields) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    for (String line : readableLines(resolvedPath, DEFAULT_READ_LIMIT)) {
+      ctx.getPrinter().println(cutLine(line, delimiter, fields));
+    }
+  }
+
+  private List<String> readableLines(FsPath path, int limit) throws SQLException {
+    if (isTextFile(path)) {
+      return provider.readLines(path, limit);
+    }
+    List<String> lines = new ArrayList<>();
+    for (SqlRow row : provider.read(path, limit)) {
+      lines.add(joinValues(row));
+    }
+    return lines;
+  }
+
   private void printFind(FsPath path, String pattern) throws SQLException {
     FsNode node = provider.describe(path);
     if (matchesFind(node, pattern)) {
@@ -267,7 +329,8 @@ public class FilesystemShell {
 
   private void printFile(String path) throws SQLException {
     FsPath resolvedPath = resolve(path);
-    ctx.getPrinter().println(resolvedPath + ": " + provider.describe(resolvedPath).getType());
+    ctx.getPrinter()
+        .println(resolvedPath + ": " + unixType(provider.describe(resolvedPath).getType()));
   }
 
   private void printDiskUsage(String path) throws SQLException {
@@ -340,6 +403,7 @@ public class FilesystemShell {
     ctx.getPrinter().println("mkdir <path>");
     ctx.getPrinter().println("rm <path>");
     ctx.getPrinter().println("mv <source> <target>");
+    ctx.getPrinter().println("cut -d<delimiter> -f<fields> <path>");
     ctx.getPrinter().println("paste <path>...");
     ctx.getPrinter().println("tree [-L depth] [path]");
     ctx.getPrinter().println("exit");
@@ -363,6 +427,86 @@ public class FilesystemShell {
     return "-r--r--r--";
   }
 
+  private static String unixType(FsNodeType type) {
+    if (isDirectory(type)) {
+      return "directory";
+    }
+    if (type == FsNodeType.UNKNOWN) {
+      return "unknown";
+    }
+    return "regular file";
+  }
+
+  private static boolean isAllOption(FilesystemCommand command) {
+    return "-a".equals(command.getOption());
+  }
+
+  private static String cutLine(String line, String delimiter, String fields) {
+    if (!line.contains(delimiter)) {
+      return line;
+    }
+    String[] values = line.split(Pattern.quote(delimiter), -1);
+    boolean[] selected = selectedFields(fields, values.length);
+    StringBuilder builder = new StringBuilder();
+    for (int i = 0; i < values.length; i++) {
+      if (!selected[i]) {
+        continue;
+      }
+      if (builder.length() > 0) {
+        builder.append(delimiter);
+      }
+      builder.append(values[i]);
+    }
+    return builder.toString();
+  }
+
+  private static boolean[] selectedFields(String fields, int fieldCount) {
+    boolean[] selected = new boolean[fieldCount];
+    for (String field : fields.split(",")) {
+      selectField(field.trim(), selected);
+    }
+    return selected;
+  }
+
+  private static void selectField(String field, boolean[] selected) {
+    if (field.isEmpty()) {
+      return;
+    }
+    int dash = field.indexOf('-');
+    if (dash < 0) {
+      selectFieldNumber(field, selected);
+      return;
+    }
+    int start = parsePositiveInt(field.substring(0, dash));
+    int end = parsePositiveInt(field.substring(dash + 1));
+    if (start <= 0 || end <= 0 || start > end) {
+      return;
+    }
+    for (int i = start; i <= end && i <= selected.length; i++) {
+      selected[i - 1] = true;
+    }
+  }
+
+  private static void selectFieldNumber(String field, boolean[] selected) {
+    int fieldNumber = parsePositiveInt(field);
+    if (fieldNumber > 0 && fieldNumber <= selected.length) {
+      selected[fieldNumber - 1] = true;
+    }
+  }
+
+  private static int parsePositiveInt(String value) {
+    try {
+      return Integer.parseInt(value);
+    } catch (NumberFormatException e) {
+      return -1;
+    }
+  }
+
+  private static boolean isTextFile(FsPath path) {
+    String fileName = path.getFileName();
+    return fileName.endsWith(".csv") || fileName.endsWith(".schema") || fileName.endsWith(".meta");
+  }
+
   private class FilesystemCompleter implements Completer {
 
     @Override
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 67aa6561d41..df86856ab80 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
@@ -43,6 +43,7 @@ public class FilesystemCommand {
     MKDIR,
     RM,
     MV,
+    CUT,
     PASTE,
     TREE,
     SQL,
@@ -117,6 +118,11 @@ public class FilesystemCommand {
         type, path, Collections.singletonList(path), -1, -1, "", pattern, "", "");
   }
 
+  public static FilesystemCommand cut(String delimiter, String fields, String path) {
+    return new FilesystemCommand(
+        Type.CUT, path, Collections.singletonList(path), -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 61000c17ee3..62d267153b7 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
@@ -26,6 +26,7 @@ import java.util.Locale;
 public class FilesystemCommandParser {
 
   private static final String DEFAULT_PATH = ".";
+  private static final String DEFAULT_CUT_DELIMITER = "\t";
   private static final int DEFAULT_TREE_DEPTH = Integer.MAX_VALUE;
   private static final int DEFAULT_HEAD_LIMIT = 10;
 
@@ -54,10 +55,10 @@ public class FilesystemCommandParser {
     String[] tokens = line.split("\\s+");
     String command = tokens[0].toLowerCase(Locale.ROOT);
     if ("ls".equals(command)) {
-      return parseLs(tokens);
+      return parseList(tokens, false);
     }
     if ("ll".equals(command)) {
-      return FilesystemCommand.path(FilesystemCommand.Type.LL, pathArgument(tokens));
+      return parseList(tokens, true);
     }
     if ("cd".equals(command)) {
       return FilesystemCommand.path(FilesystemCommand.Type.CD, pathArgument(tokens));
@@ -104,6 +105,9 @@ public class FilesystemCommandParser {
     if ("mv".equals(command)) {
       return parseMv(tokens);
     }
+    if ("cut".equals(command)) {
+      return parseCut(tokens);
+    }
     if ("paste".equals(command)) {
       return parsePaste(tokens);
     }
@@ -132,6 +136,46 @@ public class FilesystemCommandParser {
     return FilesystemCommand.paths(FilesystemCommand.Type.PASTE, paths);
   }
 
+  private static FilesystemCommand parseCut(String[] tokens) {
+    String delimiter = DEFAULT_CUT_DELIMITER;
+    String fields = "";
+    String path = "";
+
+    for (int i = 1; i < tokens.length; i++) {
+      String token = tokens[i];
+      if ("-d".equals(token)) {
+        if (i + 1 >= tokens.length) {
+          return FilesystemCommand.invalid("Missing cut delimiter");
+        }
+        delimiter = tokens[++i];
+      } else if (token.startsWith("-d") && token.length() > 2) {
+        delimiter = token.substring(2);
+      } else if ("-f".equals(token)) {
+        if (i + 1 >= tokens.length) {
+          return FilesystemCommand.invalid("Missing cut fields");
+        }
+        fields = tokens[++i];
+      } else if (token.startsWith("-f") && token.length() > 2) {
+        fields = token.substring(2);
+      } else if (token.startsWith("-")) {
+        return FilesystemCommand.invalid("Unsupported cut option: " + token);
+      } else {
+        path = token;
+      }
+    }
+
+    if (delimiter.length() != 1) {
+      return FilesystemCommand.invalid("Cut delimiter must be a single character");
+    }
+    if (fields.isEmpty()) {
+      return FilesystemCommand.invalid("Missing cut fields");
+    }
+    if (path.isEmpty()) {
+      return FilesystemCommand.invalid("Missing cut path");
+    }
+    return FilesystemCommand.cut(delimiter, fields, path);
+  }
+
   private static FilesystemCommand parseRm(String[] tokens) {
     if (tokens.length < 2) {
       return FilesystemCommand.invalid("Missing rm path");
@@ -152,9 +196,10 @@ public class FilesystemCommandParser {
     return FilesystemCommand.paths(FilesystemCommand.Type.MV, paths);
   }
 
-  private static FilesystemCommand parseLs(String[] tokens) {
-    FilesystemCommand.Type type = FilesystemCommand.Type.LS;
+  private static FilesystemCommand parseList(String[] tokens, boolean longMode) {
+    FilesystemCommand.Type type = longMode ? FilesystemCommand.Type.LL : FilesystemCommand.Type.LS;
     String path = DEFAULT_PATH;
+    boolean all = false;
 
     for (int i = 1; i < tokens.length; i++) {
       String token = tokens[i];
@@ -163,7 +208,9 @@ public class FilesystemCommandParser {
           char option = token.charAt(j);
           if (option == 'l') {
             type = FilesystemCommand.Type.LL;
-          } else if (option != 'a') {
+          } else if (option == 'a') {
+            all = true;
+          } else {
             return FilesystemCommand.invalid("Unsupported ls option: -" + option);
           }
         }
@@ -171,7 +218,7 @@ public class FilesystemCommandParser {
         path = token;
       }
     }
-    return FilesystemCommand.path(type, path);
+    return FilesystemCommand.option(type, all ? "-a" : "", path);
   }
 
   private static FilesystemCommand parseCat(String[] tokens) {
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
index a38ac575520..df63ef5c817 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
@@ -30,5 +30,8 @@ public enum FsNodeType {
   TABLE_TABLE,
   TABLE_VIEW,
   TABLE_COLUMN,
+  TABLE_DATA_FILE,
+  TABLE_SCHEMA_FILE,
+  TABLE_META_FILE,
   UNKNOWN
 }
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
index de92c9bf983..ac015bd19ae 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
@@ -34,10 +34,18 @@ public interface FilesystemSchemaProvider {
 
   List<SqlRow> read(FsPath path, int limit) throws SQLException;
 
+  default List<String> readLines(FsPath path, int limit) throws SQLException {
+    throw new SQLException("Path is not readable as text: " + path);
+  }
+
   default List<SqlRow> tail(FsPath path, int limit) throws SQLException {
     throw new SQLException("Path does not support tail: " + path);
   }
 
+  default List<String> tailLines(FsPath path, int limit) throws SQLException {
+    throw new SQLException("Path does not support tail: " + path);
+  }
+
   default long count(FsPath path) throws SQLException {
     throw new SQLException("Path does not support count: " + path);
   }
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
index 153bd53d9ae..d8220fee7f7 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
@@ -29,6 +29,7 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
 
   private static final String INVALID_WRITE_OPERATION =
       "Invalid filesystem write operation for this path";
+  private static final String CSV_SUFFIX = ".csv";
 
   private final SqlExecutor executor;
 
@@ -46,7 +47,7 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
 
   @Override
   public void remove(FsPath path) throws SQLException {
-    if (path.getSegments().size() != 2) {
+    if (!isDataFile(path)) {
       throw invalidOperation();
     }
     executor.execute("DROP TABLE " + toTablePath(path));
@@ -54,13 +55,13 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
 
   @Override
   public void move(FsPath source, FsPath target) throws SQLException {
-    if (source.getSegments().size() != 2 || target.getSegments().size() != 2) {
+    if (!isDataFile(source) || !isDataFile(target)) {
       throw invalidOperation();
     }
     if (!parent(source).equals(parent(target))) {
       throw invalidOperation();
     }
-    executor.execute("ALTER TABLE " + toTablePath(source) + " RENAME TO " + target.getFileName());
+    executor.execute("ALTER TABLE " + toTablePath(source) + " RENAME TO " + tableName(target));
   }
 
   private static SQLException invalidOperation() {
@@ -69,7 +70,16 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
 
   private static String toTablePath(FsPath path) {
     List<String> segments = path.getSegments();
-    return segments.get(0) + "." + segments.get(1);
+    return segments.get(0) + "." + tableName(path);
+  }
+
+  private static boolean isDataFile(FsPath path) {
+    return path.getSegments().size() == 2 && path.getFileName().endsWith(CSV_SUFFIX);
+  }
+
+  private static String tableName(FsPath path) {
+    String fileName = path.getFileName();
+    return fileName.substring(0, fileName.length() - CSV_SUFFIX.length());
   }
 
   private static FsPath parent(FsPath path) {
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
index d735fa99a8b..879145b69d4 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
@@ -25,13 +25,33 @@ import org.apache.iotdb.cli.fs.path.FsPath;
 import org.apache.iotdb.cli.fs.sql.SqlExecutor;
 import org.apache.iotdb.cli.fs.sql.SqlRow;
 
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
+
+import java.io.IOException;
+import java.io.StringWriter;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
 
+  private enum TableFileKind {
+    DATA_CSV,
+    SCHEMA,
+    META,
+    UNKNOWN
+  }
+
+  private static final String CSV_SUFFIX = ".csv";
+  private static final String SCHEMA_SUFFIX = ".schema";
+  private static final String META_SUFFIX = ".meta";
+  private static final CSVFormat CSV_FORMAT =
+      CSVFormat.DEFAULT.builder().setRecordSeparator("").build();
+
   private final SqlExecutor executor;
 
   public TableFilesystemSchemaProvider(SqlExecutor executor) {
@@ -45,9 +65,12 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
       return listDatabases();
     }
     if (depth == 1) {
-      return listTables(path);
+      return listTableFiles(path);
     }
     if (depth == 2) {
+      if (parseTableFile(path).kind != TableFileKind.UNKNOWN) {
+        return new ArrayList<>();
+      }
       return listColumns(path);
     }
     return new ArrayList<>();
@@ -63,6 +86,10 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
       return describeDatabase(path);
     }
     if (depth == 2) {
+      TableFileRef file = parseTableFile(path);
+      if (file.kind != TableFileKind.UNKNOWN) {
+        return describeTableFile(path, file);
+      }
       return describeTable(path);
     }
     String columnName = path.getFileName();
@@ -77,7 +104,11 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
   @Override
   public List<SqlRow> read(FsPath path, int limit) throws SQLException {
     int depth = path.getSegments().size();
-    if (depth == 2) {
+    TableFileRef file = parseTableFile(path);
+    if (depth == 2 && file.kind == TableFileKind.DATA_CSV) {
+      return executor.query("SELECT * FROM " + file.toTablePath() + " LIMIT " + limit);
+    }
+    if (depth == 2 && file.kind == TableFileKind.UNKNOWN) {
       return executor.query("SELECT * FROM " + toTablePath(path) + " LIMIT " + limit);
     }
     if (depth == 3) {
@@ -88,11 +119,32 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     throw new SQLException("Path is not readable: " + path);
   }
 
+  @Override
+  public List<String> readLines(FsPath path, int limit) throws SQLException {
+    TableFileRef file = parseTableFile(path);
+    if (file.kind == TableFileKind.DATA_CSV) {
+      return rowsToCsvLines(
+          executor.query("SELECT * FROM " + file.toTablePath() + " LIMIT " + limit));
+    }
+    if (file.kind == TableFileKind.SCHEMA) {
+      return rowsToCsvLines(executor.query("DESC " + file.toTablePath() + " DETAILS"));
+    }
+    if (file.kind == TableFileKind.META) {
+      return metaLines(file);
+    }
+    throw new SQLException("Path is not readable as text: " + path);
+  }
+
   @Override
   public List<SqlRow> tail(FsPath path, int limit) throws SQLException {
     int depth = path.getSegments().size();
+    TableFileRef file = parseTableFile(path);
     List<SqlRow> rows;
-    if (depth == 2) {
+    if (depth == 2 && file.kind == TableFileKind.DATA_CSV) {
+      rows =
+          executor.query(
+              "SELECT * FROM " + file.toTablePath() + " ORDER BY time DESC LIMIT " + limit);
+    } else if (depth == 2 && file.kind == TableFileKind.UNKNOWN) {
       rows =
           executor.query(
               "SELECT * FROM " + toTablePath(path) + " ORDER BY time DESC LIMIT " + limit);
@@ -113,11 +165,27 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     return rows;
   }
 
+  @Override
+  public List<String> tailLines(FsPath path, int limit) throws SQLException {
+    TableFileRef file = parseTableFile(path);
+    if (file.kind != TableFileKind.DATA_CSV) {
+      throw new SQLException("Path does not support tail: " + path);
+    }
+    List<SqlRow> rows =
+        executor.query(
+            "SELECT * FROM " + file.toTablePath() + " ORDER BY time DESC LIMIT " + limit);
+    Collections.reverse(rows);
+    return rowsToCsvLines(rows);
+  }
+
   @Override
   public long count(FsPath path) throws SQLException {
     int depth = path.getSegments().size();
+    TableFileRef file = parseTableFile(path);
     List<SqlRow> rows;
-    if (depth == 2) {
+    if (depth == 2 && file.kind == TableFileKind.DATA_CSV) {
+      rows = executor.query("SELECT COUNT(*) FROM " + file.toTablePath());
+    } else if (depth == 2 && file.kind == TableFileKind.UNKNOWN) {
       rows = executor.query("SELECT COUNT(*) FROM " + toTablePath(path));
     } else if (depth == 3) {
       String tablePath = toTablePath(parent(path));
@@ -174,18 +242,63 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     return new FsNode(table, path, FsNodeType.UNKNOWN);
   }
 
+  private FsNode describeTableFile(FsPath path, TableFileRef file) throws SQLException {
+    for (String table : listTableNames(parent(path))) {
+      if (table.equals(file.table)) {
+        return new FsNode(file.fileName(), path, file.nodeType(), file.metadata());
+      }
+    }
+    return new FsNode(path.getFileName(), path, FsNodeType.UNKNOWN);
+  }
+
   private List<FsNode> listTables(FsPath databasePath) throws SQLException {
+    List<FsNode> nodes = new ArrayList<>();
+    for (String table : listTableNames(databasePath)) {
+      nodes.add(
+          new FsNode(
+              table,
+              FsPath.absolute("/" + databasePath.getFileName() + "/" + table),
+              FsNodeType.TABLE_TABLE));
+    }
+    return nodes;
+  }
+
+  private List<FsNode> listTableFiles(FsPath databasePath) throws SQLException {
     String database = databasePath.getFileName();
     List<FsNode> nodes = new ArrayList<>();
+    for (String table : listTableNames(databasePath)) {
+      nodes.add(
+          new FsNode(
+              table + CSV_SUFFIX,
+              FsPath.absolute("/" + database + "/" + table + CSV_SUFFIX),
+              FsNodeType.TABLE_DATA_FILE,
+              tableFileMetadata(database, table, TableFileKind.DATA_CSV)));
+      nodes.add(
+          new FsNode(
+              table + SCHEMA_SUFFIX,
+              FsPath.absolute("/" + database + "/" + table + SCHEMA_SUFFIX),
+              FsNodeType.TABLE_SCHEMA_FILE,
+              tableFileMetadata(database, table, TableFileKind.SCHEMA)));
+      nodes.add(
+          new FsNode(
+              table + META_SUFFIX,
+              FsPath.absolute("/" + database + "/" + table + META_SUFFIX),
+              FsNodeType.TABLE_META_FILE,
+              tableFileMetadata(database, table, TableFileKind.META)));
+    }
+    return nodes;
+  }
+
+  private List<String> listTableNames(FsPath databasePath) throws SQLException {
+    String database = databasePath.getFileName();
+    List<String> tables = new ArrayList<>();
     for (SqlRow row : executor.query("SHOW TABLES FROM " + database)) {
       String table = row.get("TableName");
       if (table != null) {
-        nodes.add(
-            new FsNode(
-                table, FsPath.absolute("/" + database + "/" + table), FsNodeType.TABLE_TABLE));
+        tables.add(table);
       }
     }
-    return nodes;
+    return tables;
   }
 
   private List<FsNode> listColumns(FsPath tablePath) throws SQLException {
@@ -212,6 +325,75 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     return segments.get(0) + "." + segments.get(1);
   }
 
+  private List<String> metaLines(TableFileRef file) throws SQLException {
+    List<SqlRow> rows = new ArrayList<>();
+    for (SqlRow row : executor.query("SHOW TABLES DETAILS FROM " + file.database)) {
+      if (file.table.equals(row.get("TableName"))) {
+        rows.add(row);
+      }
+    }
+    return rowsToCsvLines(rows);
+  }
+
+  private static List<String> rowsToCsvLines(List<SqlRow> rows) throws SQLException {
+    if (rows.isEmpty()) {
+      return new ArrayList<>();
+    }
+    List<String> lines = new ArrayList<>();
+    List<String> headers = new ArrayList<>(rows.get(0).asMap().keySet());
+    lines.add(csvRecord(headers));
+    for (SqlRow row : rows) {
+      List<String> values = new ArrayList<>();
+      for (String header : headers) {
+        values.add(row.get(header));
+      }
+      lines.add(csvRecord(values));
+    }
+    return lines;
+  }
+
+  private static String csvRecord(List<String> values) throws SQLException {
+    try {
+      StringWriter writer = new StringWriter();
+      try (CSVPrinter printer = new CSVPrinter(writer, CSV_FORMAT)) {
+        printer.printRecord(values);
+      }
+      return writer.toString();
+    } catch (IOException e) {
+      throw new SQLException("Failed to format CSV output", e);
+    }
+  }
+
+  private static TableFileRef parseTableFile(FsPath path) {
+    List<String> segments = path.getSegments();
+    if (segments.size() != 2) {
+      return TableFileRef.unknown(path);
+    }
+    String fileName = segments.get(1);
+    if (fileName.endsWith(CSV_SUFFIX)) {
+      return new TableFileRef(
+          segments.get(0), removeSuffix(fileName, CSV_SUFFIX), TableFileKind.DATA_CSV);
+    }
+    if (fileName.endsWith(SCHEMA_SUFFIX)) {
+      return new TableFileRef(
+          segments.get(0), removeSuffix(fileName, SCHEMA_SUFFIX), TableFileKind.SCHEMA);
+    }
+    if (fileName.endsWith(META_SUFFIX)) {
+      return new TableFileRef(
+          segments.get(0), removeSuffix(fileName, META_SUFFIX), TableFileKind.META);
+    }
+    return TableFileRef.unknown(path);
+  }
+
+  private static String removeSuffix(String value, String suffix) {
+    return value.substring(0, value.length() - suffix.length());
+  }
+
+  private static Map<String, String> tableFileMetadata(
+      String database, String table, TableFileKind kind) {
+    return new TableFileRef(database, table, kind).metadata();
+  }
+
   private static String columnList(List<FsPath> paths) {
     StringBuilder builder = new StringBuilder();
     for (FsPath path : paths) {
@@ -241,4 +423,63 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     }
     return FsPath.absolute(builder.toString());
   }
+
+  private static class TableFileRef {
+    private final String database;
+    private final String table;
+    private final TableFileKind kind;
+
+    private TableFileRef(String database, String table, TableFileKind kind) {
+      this.database = database;
+      this.table = table;
+      this.kind = kind;
+    }
+
+    private static TableFileRef unknown(FsPath path) {
+      List<String> segments = path.getSegments();
+      String database = segments.isEmpty() ? "" : segments.get(0);
+      String table = segments.size() < 2 ? "" : segments.get(1);
+      return new TableFileRef(database, table, TableFileKind.UNKNOWN);
+    }
+
+    private String fileName() {
+      switch (kind) {
+        case DATA_CSV:
+          return table + CSV_SUFFIX;
+        case SCHEMA:
+          return table + SCHEMA_SUFFIX;
+        case META:
+          return table + META_SUFFIX;
+        case UNKNOWN:
+        default:
+          return table;
+      }
+    }
+
+    private String toTablePath() {
+      return database + "." + table;
+    }
+
+    private FsNodeType nodeType() {
+      switch (kind) {
+        case DATA_CSV:
+          return FsNodeType.TABLE_DATA_FILE;
+        case SCHEMA:
+          return FsNodeType.TABLE_SCHEMA_FILE;
+        case META:
+          return FsNodeType.TABLE_META_FILE;
+        case UNKNOWN:
+        default:
+          return FsNodeType.UNKNOWN;
+      }
+    }
+
+    private Map<String, String> metadata() {
+      Map<String, String> metadata = new LinkedHashMap<>();
+      metadata.put("database", database);
+      metadata.put("table", table);
+      metadata.put("format", kind == TableFileKind.DATA_CSV ? "csv" : kind.name().toLowerCase());
+      return metadata;
+    }
+  }
 }
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 7461aad1367..3911b65f382 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
@@ -46,6 +46,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.times;
@@ -91,7 +92,8 @@ public class FilesystemShellTest {
 
     assertTrue(shell.execute("ls /"));
 
-    assertTrue(out.toString().contains("root,test"));
+    assertEquals("root" + System.lineSeparator() + "test" + System.lineSeparator(), out.toString());
+    assertFalse(out.toString().contains(","));
     assertFalse(out.toString().contains("TREE_ROOT"));
     verify(provider).list(FsPath.absolute("/"));
   }
@@ -126,6 +128,42 @@ public class FilesystemShellTest {
     verify(provider).list(FsPath.absolute("/"));
   }
 
+  @Test
+  public void executeLsAllPrintsDotEntries() throws SQLException {
+    when(provider.list(FsPath.absolute("/")))
+        .thenReturn(
+            Arrays.asList(
+                new FsNode("testtest", FsPath.absolute("/testtest"), FsNodeType.TABLE_DATABASE)));
+
+    assertTrue(shell.execute("ls -a /"));
+
+    assertEquals(
+        "."
+            + System.lineSeparator()
+            + ".."
+            + System.lineSeparator()
+            + "testtest"
+            + System.lineSeparator(),
+        out.toString());
+    verify(provider).list(FsPath.absolute("/"));
+  }
+
+  @Test
+  public void executeLlAllPrintsDotEntriesInLongListing() throws SQLException {
+    when(provider.list(FsPath.absolute("/")))
+        .thenReturn(
+            Arrays.asList(
+                new FsNode("testtest", FsPath.absolute("/testtest"), FsNodeType.TABLE_DATABASE)));
+
+    assertTrue(shell.execute("ll -a /"));
+
+    assertTrue(out.toString().contains("dr-xr-xr-x  1 iotdb iotdb 0 ."));
+    assertTrue(out.toString().contains("dr-xr-xr-x  1 iotdb iotdb 0 .."));
+    assertTrue(out.toString().contains("dr-xr-xr-x  1 iotdb iotdb 0 testtest"));
+    assertFalse(out.toString().contains("-a"));
+    verify(provider).list(FsPath.absolute("/"));
+  }
+
   @Test
   public void executeCdUpdatesCurrentPath() throws SQLException {
     when(provider.describe(FsPath.absolute("/root")))
@@ -155,12 +193,13 @@ public class FilesystemShellTest {
     shell = new FilesystemShell(shellContext(), provider, mutationProvider, true);
 
     assertTrue(shell.execute("mkdir /db1"));
-    assertTrue(shell.execute("rm /db1/table1"));
-    assertTrue(shell.execute("mv /db1/table1 /db1/table2"));
+    assertTrue(shell.execute("rm /db1/table1.csv"));
+    assertTrue(shell.execute("mv /db1/table1.csv /db1/table2.csv"));
 
     verify(mutationProvider).mkdir(FsPath.absolute("/db1"));
-    verify(mutationProvider).remove(FsPath.absolute("/db1/table1"));
-    verify(mutationProvider).move(FsPath.absolute("/db1/table1"), FsPath.absolute("/db1/table2"));
+    verify(mutationProvider).remove(FsPath.absolute("/db1/table1.csv"));
+    verify(mutationProvider)
+        .move(FsPath.absolute("/db1/table1.csv"), FsPath.absolute("/db1/table2.csv"));
   }
 
   @Test
@@ -194,6 +233,18 @@ public class FilesystemShellTest {
     verify(provider).read(FsPath.absolute("/db1/table1"), 20);
   }
 
+  @Test
+  public void executeCatPrintsCsvFileLines() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 20))
+        .thenReturn(Arrays.asList("Time,tag1,s1", "1,a,42"));
+
+    assertTrue(shell.execute("cat /db1/table1.csv"));
+
+    assertTrue(out.toString().contains("Time,tag1,s1"));
+    assertTrue(out.toString().contains("1,a,42"));
+    verify(provider).readLines(FsPath.absolute("/db1/table1.csv"), 20);
+  }
+
   @Test
   public void executeCatReadsMultiplePathsSequentially() throws SQLException {
     when(provider.read(FsPath.absolute("/db1/table1/tag1"), 20))
@@ -220,6 +271,18 @@ public class FilesystemShellTest {
     verify(provider).read(FsPath.absolute("/db1/table1"), 5);
   }
 
+  @Test
+  public void executeHeadReadsCsvFileLinesWithLimit() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 5))
+        .thenReturn(Arrays.asList("Time,tag1,s1", "1,a,42"));
+
+    assertTrue(shell.execute("head -n 5 /db1/table1.csv"));
+
+    assertTrue(out.toString().contains("Time,tag1,s1"));
+    assertTrue(out.toString().contains("1,a,42"));
+    verify(provider).readLines(FsPath.absolute("/db1/table1.csv"), 5);
+  }
+
   @Test
   public void executeTailReadsPathWithLimit() throws SQLException {
     when(provider.tail(FsPath.absolute("/db1/table1"), 3))
@@ -231,14 +294,38 @@ public class FilesystemShellTest {
     verify(provider).tail(FsPath.absolute("/db1/table1"), 3);
   }
 
+  @Test
+  public void executeTailReadsCsvFileLinesWithLimit() throws SQLException {
+    when(provider.tailLines(FsPath.absolute("/db1/table1.csv"), 3))
+        .thenReturn(Arrays.asList("Time,tag1,s1", "2,b,43"));
+
+    assertTrue(shell.execute("tail -n 3 /db1/table1.csv"));
+
+    assertTrue(out.toString().contains("Time,tag1,s1"));
+    assertTrue(out.toString().contains("2,b,43"));
+    verify(provider).tailLines(FsPath.absolute("/db1/table1.csv"), 3);
+  }
+
   @Test
   public void executeWcLineCountPrintsCountAndPath() throws SQLException {
-    when(provider.count(FsPath.absolute("/db1/table1"))).thenReturn(2L);
+    when(provider.count(FsPath.absolute("/db1/table1.csv"))).thenReturn(2L);
+
+    assertTrue(shell.execute("wc -l /db1/table1.csv"));
 
-    assertTrue(shell.execute("wc -l /db1/table1"));
+    assertTrue(out.toString().contains("2 /db1/table1.csv"));
+    verify(provider).count(FsPath.absolute("/db1/table1.csv"));
+  }
+
+  @Test
+  public void executeGrepFiltersCsvFileLines() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 20))
+        .thenReturn(Arrays.asList("Time,tag1,s1", "1,spricoder,42", "2,other,43"));
+
+    assertTrue(shell.execute("grep spricoder /db1/table1.csv"));
 
-    assertTrue(out.toString().contains("2 /db1/table1"));
-    verify(provider).count(FsPath.absolute("/db1/table1"));
+    assertTrue(out.toString().contains("1,spricoder,42"));
+    assertFalse(out.toString().contains("2,other,43"));
+    verify(provider).readLines(FsPath.absolute("/db1/table1.csv"), 20);
   }
 
   @Test
@@ -293,24 +380,57 @@ public class FilesystemShellTest {
   }
 
   @Test
-  public void executeFilePrintsNodeType() throws SQLException {
+  public void executeLessAndMoreReadCsvFileLines() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 20))
+        .thenReturn(Arrays.asList("Time,tag1,s1", "1,a,42"));
+
+    assertTrue(shell.execute("less /db1/table1.csv"));
+    assertTrue(shell.execute("more /db1/table1.csv"));
+
+    assertTrue(out.toString().contains("Time,tag1,s1"));
+    assertTrue(out.toString().contains("1,a,42"));
+    verify(provider, times(2)).readLines(FsPath.absolute("/db1/table1.csv"), 20);
+  }
+
+  @Test
+  public void executeStatPrintsUnixStyleMetadata() throws SQLException {
+    when(provider.describe(FsPath.absolute("/db1/table1.csv")))
+        .thenReturn(
+            new FsNode(
+                "table1.csv",
+                FsPath.absolute("/db1/table1.csv"),
+                FsNodeType.TABLE_DATA_FILE,
+                java.util.Collections.singletonMap("table", "table1")));
+
+    assertTrue(shell.execute("stat /db1/table1.csv"));
+
+    assertTrue(out.toString().contains("File: /db1/table1.csv"));
+    assertTrue(out.toString().contains("Type: regular file"));
+    assertTrue(out.toString().contains("table: table1"));
+    assertFalse(out.toString().contains("TABLE_DATA_FILE"));
+    verify(provider).describe(FsPath.absolute("/db1/table1.csv"));
+  }
+
+  @Test
+  public void executeFilePrintsUnixFileType() throws SQLException {
     when(provider.describe(FsPath.absolute("/db1/table1")))
         .thenReturn(new FsNode("table1", FsPath.absolute("/db1/table1"), FsNodeType.TABLE_TABLE));
 
     assertTrue(shell.execute("file /db1/table1"));
 
-    assertTrue(out.toString().contains("/db1/table1: TABLE_TABLE"));
+    assertTrue(out.toString().contains("/db1/table1: directory"));
+    assertFalse(out.toString().contains("TABLE_TABLE"));
     verify(provider).describe(FsPath.absolute("/db1/table1"));
   }
 
   @Test
   public void executeDuPrintsLogicalSizeAndPath() throws SQLException {
-    when(provider.count(FsPath.absolute("/db1/table1"))).thenReturn(2L);
+    when(provider.count(FsPath.absolute("/db1/table1.csv"))).thenReturn(2L);
 
-    assertTrue(shell.execute("du /db1/table1"));
+    assertTrue(shell.execute("du /db1/table1.csv"));
 
-    assertTrue(out.toString().contains("2\t/db1/table1"));
-    verify(provider).count(FsPath.absolute("/db1/table1"));
+    assertTrue(out.toString().contains("2\t/db1/table1.csv"));
+    verify(provider).count(FsPath.absolute("/db1/table1.csv"));
   }
 
   @Test
@@ -330,6 +450,37 @@ public class FilesystemShellTest {
             20);
   }
 
+  @Test
+  public void executeCutSelectsCsvFieldsByNumber() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 20))
+        .thenReturn(Arrays.asList("time,key,value", "1,spricoder,2.0", "2,other,3.0"));
+
+    assertTrue(shell.execute("cut -d, -f2,3 /db1/table1.csv"));
+
+    assertEquals(
+        "key,value"
+            + System.lineSeparator()
+            + "spricoder,2.0"
+            + System.lineSeparator()
+            + "other,3.0"
+            + System.lineSeparator(),
+        out.toString());
+    verify(provider).readLines(FsPath.absolute("/db1/table1.csv"), 20);
+  }
+
+  @Test
+  public void executeCutSelectsCsvFieldRange() throws SQLException {
+    when(provider.readLines(FsPath.absolute("/db1/table1.csv"), 20))
+        .thenReturn(Arrays.asList("time,key,value", "1,spricoder,2.0"));
+
+    assertTrue(shell.execute("cut -d, -f1-2 /db1/table1.csv"));
+
+    assertEquals(
+        "time,key" + System.lineSeparator() + "1,spricoder" + System.lineSeparator(),
+        out.toString());
+    verify(provider).readLines(FsPath.absolute("/db1/table1.csv"), 20);
+  }
+
   @Test
   public void completerCompletesChildrenFromCurrentDirectory() throws SQLException {
     when(provider.list(FsPath.absolute("/")))
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 7224876d239..8039335d797 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
@@ -43,12 +43,31 @@ public class FilesystemCommandParserTest {
     assertEquals("/db1", command.getPath());
   }
 
+  @Test
+  public void parseLlAllOptionAsCurrentDirectoryLongListCommand() {
+    FilesystemCommand command = FilesystemCommandParser.parse("ll -a");
+
+    assertEquals(FilesystemCommand.Type.LL, command.getType());
+    assertEquals(".", command.getPath());
+    assertEquals("-a", command.getOption());
+  }
+
+  @Test
+  public void parseLlCombinedOptionsAndPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("ll -al /db1");
+
+    assertEquals(FilesystemCommand.Type.LL, command.getType());
+    assertEquals("/db1", command.getPath());
+    assertEquals("-a", command.getOption());
+  }
+
   @Test
   public void parseLsLongOptionAsLongListCommand() {
     FilesystemCommand command = FilesystemCommandParser.parse("ls -la /db1");
 
     assertEquals(FilesystemCommand.Type.LL, command.getType());
     assertEquals("/db1", command.getPath());
+    assertEquals("-a", command.getOption());
   }
 
   @Test
@@ -146,6 +165,35 @@ public class FilesystemCommandParserTest {
     assertEquals("/db1/table1/s1", command.getPaths().get(1));
   }
 
+  @Test
+  public void parseCutDelimiterFieldsAndPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("cut -d, -f2,3 /db1/table1.csv");
+
+    assertEquals(FilesystemCommand.Type.CUT, command.getType());
+    assertEquals(",", command.getOption());
+    assertEquals("2,3", command.getPattern());
+    assertEquals("/db1/table1.csv", command.getPath());
+  }
+
+  @Test
+  public void parseCutSeparatedOptionArguments() {
+    FilesystemCommand command = FilesystemCommandParser.parse("cut -d , -f 1-2 /db1/table1.csv");
+
+    assertEquals(FilesystemCommand.Type.CUT, command.getType());
+    assertEquals(",", command.getOption());
+    assertEquals("1-2", command.getPattern());
+    assertEquals("/db1/table1.csv", command.getPath());
+  }
+
+  @Test
+  public void parseCutRequiresFieldsAndPath() {
+    assertEquals(
+        FilesystemCommand.Type.INVALID,
+        FilesystemCommandParser.parse("cut -d, /db1/table1.csv").getType());
+    assertEquals(
+        FilesystemCommand.Type.INVALID, FilesystemCommandParser.parse("cut -f2,3").getType());
+  }
+
   @Test
   public void parseWriteCommands() {
     FilesystemCommand mkdir = FilesystemCommandParser.parse("mkdir /db1");
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
index 955046688c4..2403bffffb3 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
@@ -55,26 +55,29 @@ public class TableFilesystemMutationProviderTest {
   @Test
   public void mkdirRejectsRootAndTableLevel() throws SQLException {
     assertInvalidOperation(() -> provider.mkdir(FsPath.absolute("/")));
-    assertInvalidOperation(() -> provider.mkdir(FsPath.absolute("/db1/table1")));
+    assertInvalidOperation(() -> provider.mkdir(FsPath.absolute("/db1/table1.csv")));
   }
 
   @Test
-  public void removeTableDropsTable() throws SQLException {
-    provider.remove(FsPath.absolute("/db1/table1"));
+  public void removeTableCsvDropsTable() throws SQLException {
+    provider.remove(FsPath.absolute("/db1/table1.csv"));
 
     verify(executor).execute("DROP TABLE db1.table1");
   }
 
   @Test
-  public void removeRejectsRootDatabaseAndColumnLevel() throws SQLException {
+  public void removeRejectsRootDatabaseSchemaMetaAndLegacyTableLevel() throws SQLException {
     assertInvalidOperation(() -> provider.remove(FsPath.absolute("/")));
     assertInvalidOperation(() -> provider.remove(FsPath.absolute("/db1")));
+    assertInvalidOperation(() -> provider.remove(FsPath.absolute("/db1/table1")));
+    assertInvalidOperation(() -> provider.remove(FsPath.absolute("/db1/table1.schema")));
+    assertInvalidOperation(() -> provider.remove(FsPath.absolute("/db1/table1.meta")));
     assertInvalidOperation(() -> provider.remove(FsPath.absolute("/db1/table1/s1")));
   }
 
   @Test
-  public void moveTableRenamesTableInSameDatabase() throws SQLException {
-    provider.move(FsPath.absolute("/db1/table1"), FsPath.absolute("/db1/table2"));
+  public void moveTableCsvRenamesTableInSameDatabase() throws SQLException {
+    provider.move(FsPath.absolute("/db1/table1.csv"), FsPath.absolute("/db1/table2.csv"));
 
     verify(executor).execute("ALTER TABLE db1.table1 RENAME TO table2");
   }
@@ -82,10 +85,17 @@ public class TableFilesystemMutationProviderTest {
   @Test
   public void moveRejectsUnsafeLevelsAndCrossDatabaseRename() throws SQLException {
     assertInvalidOperation(() -> provider.move(FsPath.absolute("/db1"), FsPath.absolute("/db2")));
+    assertInvalidOperation(
+        () -> provider.move(FsPath.absolute("/db1/table1"), FsPath.absolute("/db1/table2")));
+    assertInvalidOperation(
+        () ->
+            provider.move(
+                FsPath.absolute("/db1/table1.schema"), FsPath.absolute("/db1/table2.schema")));
     assertInvalidOperation(
         () -> provider.move(FsPath.absolute("/db1/table1/s1"), FsPath.absolute("/db1/table1/s2")));
     assertInvalidOperation(
-        () -> provider.move(FsPath.absolute("/db1/table1"), FsPath.absolute("/db2/table1")));
+        () ->
+            provider.move(FsPath.absolute("/db1/table1.csv"), FsPath.absolute("/db2/table1.csv")));
   }
 
   private static void assertInvalidOperation(SqlOperation operation) throws SQLException {
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
index 9d7c1bb6715..b99a30761d7 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
@@ -71,13 +71,31 @@ public class TableFilesystemSchemaProviderTest {
 
     List<FsNode> children = provider.list(FsPath.absolute("/db1"));
 
-    assertEquals(2, children.size());
-    assertEquals("/db1/table1", children.get(0).getPath().toString());
-    assertEquals(FsNodeType.TABLE_TABLE, children.get(0).getType());
-    assertEquals("/db1/view1", children.get(1).getPath().toString());
+    assertEquals(6, children.size());
+    assertEquals("table1.csv", children.get(0).getName());
+    assertEquals("/db1/table1.csv", children.get(0).getPath().toString());
+    assertEquals(FsNodeType.TABLE_DATA_FILE, children.get(0).getType());
+    assertEquals("table1.schema", children.get(1).getName());
+    assertEquals(FsNodeType.TABLE_SCHEMA_FILE, children.get(1).getType());
+    assertEquals("table1.meta", children.get(2).getName());
+    assertEquals(FsNodeType.TABLE_META_FILE, children.get(2).getType());
+    assertEquals("view1.csv", children.get(3).getName());
     verify(executor).query("SHOW TABLES FROM db1");
   }
 
+  @Test
+  public void describeTableCsvReturnsDataFileNode() throws SQLException {
+    when(executor.query("SHOW TABLES FROM db1"))
+        .thenReturn(SqlRow.list(SqlRow.of("TableName", "table1")));
+
+    FsNode node = provider.describe(FsPath.absolute("/db1/table1.csv"));
+
+    assertEquals("table1.csv", node.getName());
+    assertEquals(FsNodeType.TABLE_DATA_FILE, node.getType());
+    assertEquals("table1", node.getMetadata().get("table"));
+    assertEquals("csv", node.getMetadata().get("format"));
+  }
+
   @Test
   public void listTableReturnsColumns() throws SQLException {
     when(executor.query("DESC db1.table1 DETAILS"))
@@ -171,6 +189,66 @@ public class TableFilesystemSchemaProviderTest {
     verify(executor).query("SELECT * FROM db1.table1 LIMIT 5");
   }
 
+  @Test
+  public void readTableCsvReturnsCsvLinesWithHeader() throws SQLException {
+    when(executor.query("SELECT * FROM db1.table1 LIMIT 5"))
+        .thenReturn(SqlRow.list(SqlRow.of("Time", "1", "tag1", "a", "s1", "42")));
+
+    List<String> lines = provider.readLines(FsPath.absolute("/db1/table1.csv"), 5);
+
+    assertEquals("Time,tag1,s1", lines.get(0));
+    assertEquals("1,a,42", lines.get(1));
+    verify(executor).query("SELECT * FROM db1.table1 LIMIT 5");
+  }
+
+  @Test
+  public void readTableSchemaReturnsIoTDBDescCsvLines() throws SQLException {
+    when(executor.query("DESC db1.table1 DETAILS"))
+        .thenReturn(
+            SqlRow.list(
+                SqlRow.of("ColumnName", "tag1", "DataType", "STRING", "Category", "TAG"),
+                SqlRow.of("ColumnName", "s1", "DataType", "INT32", "Category", "FIELD")));
+
+    List<String> lines = provider.readLines(FsPath.absolute("/db1/table1.schema"), 5);
+
+    assertEquals("ColumnName,DataType,Category", lines.get(0));
+    assertEquals("tag1,STRING,TAG", lines.get(1));
+    assertEquals("s1,INT32,FIELD", lines.get(2));
+    verify(executor).query("DESC db1.table1 DETAILS");
+  }
+
+  @Test
+  public void readTableMetaReturnsIoTDBTableCsvLines() throws SQLException {
+    when(executor.query("SHOW TABLES DETAILS FROM db1"))
+        .thenReturn(
+            SqlRow.list(
+                SqlRow.of(
+                    "TableName",
+                    "table1",
+                    "TTL(ms)",
+                    "3600000",
+                    "Status",
+                    "USING",
+                    "Comment",
+                    "main table"),
+                SqlRow.of(
+                    "TableName",
+                    "table2",
+                    "TTL(ms)",
+                    "INF",
+                    "Status",
+                    "USING",
+                    "Comment",
+                    "archive")));
+
+    List<String> lines = provider.readLines(FsPath.absolute("/db1/table1.meta"), 5);
+
+    assertEquals("TableName,TTL(ms),Status,Comment", lines.get(0));
+    assertEquals("table1,3600000,USING,main table", lines.get(1));
+    assertEquals(2, lines.size());
+    verify(executor).query("SHOW TABLES DETAILS FROM db1");
+  }
+
   @Test
   public void tailTableSelectsNewestRowsAndReturnsOriginalOrder() throws SQLException {
     when(executor.query("SELECT * FROM db1.table1 ORDER BY time DESC LIMIT 2"))
@@ -191,7 +269,7 @@ public class TableFilesystemSchemaProviderTest {
     when(executor.query("SELECT COUNT(*) FROM db1.table1"))
         .thenReturn(SqlRow.list(SqlRow.of("count", "2")));
 
-    long count = provider.count(FsPath.absolute("/db1/table1"));
+    long count = provider.count(FsPath.absolute("/db1/table1.csv"));
 
     assertEquals(2L, count);
     verify(executor).query("SELECT COUNT(*) FROM db1.table1");


(iotdb) 01/06: add write

Posted by sp...@apache.org.
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 08ad58029e76a817b82a75ef2445a4206381a6d0
Author: spricoder <sp...@qq.com>
AuthorDate: Wed Apr 29 20:50:12 2026 +0800

    add write
---
 .../java/org/apache/iotdb/cli/AbstractCli.java     |  35 +++++
 .../src/main/java/org/apache/iotdb/cli/Cli.java    |  15 ++-
 .../org/apache/iotdb/cli/fs/FilesystemShell.java   | 142 ++++++++++++++++++++-
 .../iotdb/cli/fs/command/FilesystemCommand.java    |  57 ++++++++-
 .../cli/fs/command/FilesystemCommandParser.java    | 127 ++++++++++++++++++
 .../FilesystemMutationProvider.java}               |  13 +-
 .../cli/fs/provider/FilesystemSchemaProvider.java  |   8 ++
 .../provider/TableFilesystemMutationProvider.java  |  86 +++++++++++++
 .../fs/provider/TableFilesystemSchemaProvider.java |  48 +++++++
 .../fs/provider/TreeFilesystemSchemaProvider.java  |  29 +++++
 ... => UnsupportedFilesystemMutationProvider.java} |  29 +++--
 .../apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java   |   7 +
 .../org/apache/iotdb/cli/fs/sql/SqlExecutor.java   |   2 +
 .../org/apache/iotdb/cli/utils/JlineUtils.java     |   3 +-
 .../java/org/apache/iotdb/cli/AbstractCliTest.java |  29 +++++
 .../apache/iotdb/cli/CliFilesystemModeTest.java    |  26 ++++
 .../apache/iotdb/cli/fs/FilesystemShellTest.java   | 132 ++++++++++++++++++-
 .../fs/command/FilesystemCommandParserTest.java    |  65 ++++++++++
 .../TableFilesystemMutationProviderTest.java       | 103 +++++++++++++++
 .../TableFilesystemSchemaProviderTest.java         |  26 ++++
 .../iotdb/cli/fs/sql/JdbcSqlExecutorTest.java      |  10 ++
 21 files changed, 963 insertions(+), 29 deletions(-)

diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
index 5e7680d2cc5..1fce4ae8e27 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
@@ -74,6 +74,10 @@ public abstract class AbstractCli {
   static final String ACCESS_MODE_NAME = "access mode";
   static final String ACCESS_MODE_SQL = "sql";
   static final String ACCESS_MODE_FILESYSTEM = "filesystem";
+  static final String FS_WRITE_MODE_ARGS = "fs_write_mode";
+  static final String FS_WRITE_MODE_NAME = "fs write mode";
+  static final String FS_WRITE_MODE_DISABLED = "disabled";
+  static final String FS_WRITE_MODE_ENABLED = "enabled";
 
   private static final String EXECUTE_ARGS = "e";
 
@@ -141,6 +145,7 @@ public abstract class AbstractCli {
   static String execute;
   static boolean hasExecuteSQL = false;
   static String accessMode = ACCESS_MODE_SQL;
+  static String fsWriteMode = FS_WRITE_MODE_DISABLED;
 
   static Set<String> keywordSet = new HashSet<>();
 
@@ -166,6 +171,7 @@ public abstract class AbstractCli {
     keywordSet.add("-" + ISO8601_ARGS);
     keywordSet.add("-" + RPC_COMPRESS_ARGS);
     keywordSet.add("--" + ACCESS_MODE_ARGS);
+    keywordSet.add("--" + FS_WRITE_MODE_ARGS);
   }
 
   static Options createOptions() {
@@ -238,6 +244,17 @@ public abstract class AbstractCli {
             .build();
     options.addOption(accessMode);
 
+    Option fsWriteMode =
+        Option.builder()
+            .longOpt(FS_WRITE_MODE_ARGS)
+            .argName(FS_WRITE_MODE_NAME)
+            .hasArg()
+            .desc(
+                "Filesystem write mode, supports disabled and enabled. Default is disabled."
+                    + " (optional)")
+            .build();
+    options.addOption(fsWriteMode);
+
     Option isRpcCompressed =
         Option.builder(RPC_COMPRESS_ARGS)
             .argName(RPC_COMPRESS_NAME)
@@ -281,6 +298,24 @@ public abstract class AbstractCli {
     AbstractCli.accessMode = accessMode;
   }
 
+  static String getFsWriteMode(CliContext ctx, CommandLine commandLine) throws ArgsErrorException {
+    String value = commandLine.getOptionValue(FS_WRITE_MODE_ARGS, FS_WRITE_MODE_DISABLED);
+    String normalized = value.toLowerCase(Locale.ROOT);
+    if (FS_WRITE_MODE_DISABLED.equals(normalized) || FS_WRITE_MODE_ENABLED.equals(normalized)) {
+      return normalized;
+    }
+    String msg =
+        String.format(
+            "%s: Unsupported fs write mode '%s'. Supported values are disabled and enabled.",
+            IOTDB, value);
+    ctx.getPrinter().println(msg);
+    throw new ArgsErrorException(msg);
+  }
+
+  static void setFsWriteMode(String fsWriteMode) {
+    AbstractCli.fsWriteMode = fsWriteMode;
+  }
+
   static String checkRequiredArg(
       CliContext ctx,
       String arg,
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
index 4ded47379b9..ad5571efb41 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
@@ -20,9 +20,12 @@
 package org.apache.iotdb.cli;
 
 import org.apache.iotdb.cli.fs.FilesystemShell;
+import org.apache.iotdb.cli.fs.provider.FilesystemMutationProvider;
 import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.provider.TableFilesystemMutationProvider;
 import org.apache.iotdb.cli.fs.provider.TableFilesystemSchemaProvider;
 import org.apache.iotdb.cli.fs.provider.TreeFilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.provider.UnsupportedFilesystemMutationProvider;
 import org.apache.iotdb.cli.fs.sql.JdbcSqlExecutor;
 import org.apache.iotdb.cli.type.ExitType;
 import org.apache.iotdb.cli.utils.CliContext;
@@ -145,6 +148,7 @@ public class Cli extends AbstractCli {
         setSqlDialect(commandLine.getOptionValue(Config.SQL_DIALECT));
       }
       setAccessMode(getAccessMode(ctx, commandLine));
+      setFsWriteMode(getFsWriteMode(ctx, commandLine));
     } catch (ArgsErrorException e) {
       ctx.getPrinter()
           .println(IOTDB_ERROR_PREFIX + ": Input params error because " + e.getMessage());
@@ -224,6 +228,11 @@ public class Cli extends AbstractCli {
           .println(
               IOTDB_ERROR_PREFIX + ": Can't execute filesystem command because " + e.getMessage());
       ctx.exit(CODE_ERROR);
+    } catch (TException e) {
+      ctx.getPrinter()
+          .println(
+              IOTDB_ERROR_PREFIX + ": Can't execute filesystem command because " + e.getMessage());
+      ctx.exit(CODE_ERROR);
     }
   }
 
@@ -264,12 +273,16 @@ public class Cli extends AbstractCli {
   static FilesystemShell createFilesystemShell(CliContext ctx, IoTDBConnection connection) {
     JdbcSqlExecutor executor = new JdbcSqlExecutor(connection);
     FilesystemSchemaProvider provider;
+    FilesystemMutationProvider mutationProvider;
     if (Constant.TABLE_DIALECT.equalsIgnoreCase(connection.getSqlDialect())) {
       provider = new TableFilesystemSchemaProvider(executor);
+      mutationProvider = new TableFilesystemMutationProvider(executor);
     } else {
       provider = new TreeFilesystemSchemaProvider(executor);
+      mutationProvider = new UnsupportedFilesystemMutationProvider();
     }
-    return new FilesystemShell(ctx, provider);
+    return new FilesystemShell(
+        ctx, provider, mutationProvider, FS_WRITE_MODE_ENABLED.equals(fsWriteMode));
   }
 
   private static boolean readerReadLine(CliContext ctx, IoTDBConnection connection) {
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 6a3cf9e976d..8929c964524 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
@@ -24,7 +24,9 @@ import org.apache.iotdb.cli.fs.command.FilesystemCommandParser;
 import org.apache.iotdb.cli.fs.node.FsNode;
 import org.apache.iotdb.cli.fs.node.FsNodeType;
 import org.apache.iotdb.cli.fs.path.FsPath;
+import org.apache.iotdb.cli.fs.provider.FilesystemMutationProvider;
 import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.provider.UnsupportedFilesystemMutationProvider;
 import org.apache.iotdb.cli.fs.sql.SqlRow;
 import org.apache.iotdb.cli.utils.CliContext;
 
@@ -44,15 +46,28 @@ public class FilesystemShell {
   private static final int DEFAULT_READ_LIMIT = 20;
   private static final List<String> COMMANDS =
       Arrays.asList(
-          "pwd", "ls", "ll", "cd", "stat", "cat", "head", "paste", "tree", "help", "exit", "quit");
+          "pwd", "ls", "ll", "cd", "stat", "cat", "head", "tail", "wc", "grep", "find", "less",
+          "more", "file", "du", "mkdir", "rm", "mv", "paste", "tree", "help", "exit", "quit");
 
   private final CliContext ctx;
   private final FilesystemSchemaProvider provider;
+  private final FilesystemMutationProvider mutationProvider;
+  private final boolean writeEnabled;
   private FsPath currentPath = FsPath.absolute("/");
 
   public FilesystemShell(CliContext ctx, FilesystemSchemaProvider provider) {
+    this(ctx, provider, new UnsupportedFilesystemMutationProvider(), false);
+  }
+
+  public FilesystemShell(
+      CliContext ctx,
+      FilesystemSchemaProvider provider,
+      FilesystemMutationProvider mutationProvider,
+      boolean writeEnabled) {
     this.ctx = ctx;
     this.provider = provider;
+    this.mutationProvider = mutationProvider;
+    this.writeEnabled = writeEnabled;
   }
 
   public boolean execute(String input) throws SQLException {
@@ -79,6 +94,37 @@ public class FilesystemShell {
       case HEAD:
         printRows(provider.read(resolve(command.getPath()), command.getLimit()));
         return true;
+      case TAIL:
+        printRows(provider.tail(resolve(command.getPath()), command.getLimit()));
+        return true;
+      case WC:
+        printLineCount(command.getPath());
+        return true;
+      case GREP:
+        printMatchingRows(command.getPath(), command.getPattern());
+        return true;
+      case FIND:
+        printFind(resolve(command.getPath()), command.getPattern());
+        return true;
+      case LESS:
+      case MORE:
+        printRows(provider.read(resolve(command.getPath()), DEFAULT_READ_LIMIT));
+        return true;
+      case FILE:
+        printFile(command.getPath());
+        return true;
+      case DU:
+        printDiskUsage(command.getPath());
+        return true;
+      case MKDIR:
+        mkdir(command.getPath());
+        return true;
+      case RM:
+        remove(command.getPath());
+        return true;
+      case MV:
+        move(command.getPaths());
+        return true;
       case PASTE:
         printRows(provider.read(resolve(command.getPaths()), DEFAULT_READ_LIMIT));
         return true;
@@ -151,8 +197,15 @@ public class FilesystemShell {
   }
 
   private void printNodes(List<FsNode> nodes) {
+    StringBuilder builder = new StringBuilder();
     for (FsNode node : nodes) {
-      ctx.getPrinter().println(node.getName());
+      if (builder.length() > 0) {
+        builder.append(',');
+      }
+      builder.append(node.getName());
+    }
+    if (builder.length() > 0) {
+      ctx.getPrinter().println(builder.toString());
     }
   }
 
@@ -181,6 +234,80 @@ public class FilesystemShell {
     }
   }
 
+  private void printLineCount(String path) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    ctx.getPrinter().println(provider.count(resolvedPath) + " " + resolvedPath);
+  }
+
+  private void printMatchingRows(String path, String pattern) throws SQLException {
+    for (SqlRow row : provider.read(resolve(path), DEFAULT_READ_LIMIT)) {
+      String line = joinValues(row);
+      if (line.contains(pattern)) {
+        ctx.getPrinter().println(line);
+      }
+    }
+  }
+
+  private void printFind(FsPath path, String pattern) throws SQLException {
+    FsNode node = provider.describe(path);
+    if (matchesFind(node, pattern)) {
+      ctx.getPrinter().println(path.toString());
+    }
+    if (!isDirectory(node.getType())) {
+      return;
+    }
+    for (FsNode child : provider.list(path)) {
+      printFind(child.getPath(), pattern);
+    }
+  }
+
+  private static boolean matchesFind(FsNode node, String pattern) {
+    return pattern == null || pattern.isEmpty() || node.getName().equals(pattern);
+  }
+
+  private void printFile(String path) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    ctx.getPrinter().println(resolvedPath + ": " + provider.describe(resolvedPath).getType());
+  }
+
+  private void printDiskUsage(String path) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    ctx.getPrinter().println(provider.count(resolvedPath) + "\t" + resolvedPath);
+  }
+
+  private void mkdir(String path) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    if (!ensureWritable("mkdir", resolvedPath)) {
+      return;
+    }
+    mutationProvider.mkdir(resolvedPath);
+  }
+
+  private void remove(String path) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    if (!ensureWritable("rm", resolvedPath)) {
+      return;
+    }
+    mutationProvider.remove(resolvedPath);
+  }
+
+  private void move(List<String> paths) throws SQLException {
+    FsPath source = resolve(paths.get(0));
+    FsPath target = resolve(paths.get(1));
+    if (!ensureWritable("mv", source)) {
+      return;
+    }
+    mutationProvider.move(source, target);
+  }
+
+  private boolean ensureWritable(String command, FsPath path) {
+    if (writeEnabled) {
+      return true;
+    }
+    ctx.getPrinter().println(command + ": " + path + ": Read-only file system");
+    return false;
+  }
+
   private static String joinValues(SqlRow row) {
     StringBuilder builder = new StringBuilder();
     for (String value : row.asMap().values()) {
@@ -202,6 +329,17 @@ public class FilesystemShell {
     ctx.getPrinter().println("stat [path]");
     ctx.getPrinter().println("cat <path>...");
     ctx.getPrinter().println("head [-n lines] <path>");
+    ctx.getPrinter().println("tail [-n lines] <path>");
+    ctx.getPrinter().println("wc -l <path>");
+    ctx.getPrinter().println("grep <pattern> <path>");
+    ctx.getPrinter().println("find [path] [-name pattern]");
+    ctx.getPrinter().println("less <path>");
+    ctx.getPrinter().println("more <path>");
+    ctx.getPrinter().println("file <path>");
+    ctx.getPrinter().println("du <path>");
+    ctx.getPrinter().println("mkdir <path>");
+    ctx.getPrinter().println("rm <path>");
+    ctx.getPrinter().println("mv <source> <target>");
     ctx.getPrinter().println("paste <path>...");
     ctx.getPrinter().println("tree [-L depth] [path]");
     ctx.getPrinter().println("exit");
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 2cdd492fdc5..67aa6561d41 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
@@ -32,6 +32,17 @@ public class FilesystemCommand {
     STAT,
     CAT,
     HEAD,
+    TAIL,
+    WC,
+    GREP,
+    FIND,
+    LESS,
+    MORE,
+    FILE,
+    DU,
+    MKDIR,
+    RM,
+    MV,
     PASTE,
     TREE,
     SQL,
@@ -45,6 +56,8 @@ public class FilesystemCommand {
   private final List<String> paths;
   private final int depth;
   private final int limit;
+  private final String option;
+  private final String pattern;
   private final String statement;
   private final String errorMessage;
 
@@ -54,6 +67,8 @@ public class FilesystemCommand {
       List<String> paths,
       int depth,
       int limit,
+      String option,
+      String pattern,
       String statement,
       String errorMessage) {
     this.type = type;
@@ -61,40 +76,60 @@ public class FilesystemCommand {
     this.paths = paths;
     this.depth = depth;
     this.limit = limit;
+    this.option = option;
+    this.pattern = pattern;
     this.statement = statement;
     this.errorMessage = errorMessage;
   }
 
   public static FilesystemCommand simple(Type type) {
-    return new FilesystemCommand(type, "", Collections.emptyList(), -1, -1, "", "");
+    return new FilesystemCommand(type, "", Collections.emptyList(), -1, -1, "", "", "", "");
   }
 
   public static FilesystemCommand path(Type type, String path) {
-    return new FilesystemCommand(type, path, Collections.singletonList(path), -1, -1, "", "");
+    return new FilesystemCommand(
+        type, path, Collections.singletonList(path), -1, -1, "", "", "", "");
   }
 
   public static FilesystemCommand paths(Type type, List<String> paths) {
     String path = paths.isEmpty() ? "" : paths.get(0);
-    return new FilesystemCommand(type, path, Collections.unmodifiableList(paths), -1, -1, "", "");
+    return new FilesystemCommand(
+        type, path, Collections.unmodifiableList(paths), -1, -1, "", "", "", "");
   }
 
   public static FilesystemCommand head(String path, int limit) {
     return new FilesystemCommand(
-        Type.HEAD, path, Collections.singletonList(path), -1, limit, "", "");
+        Type.HEAD, path, Collections.singletonList(path), -1, limit, "", "", "", "");
+  }
+
+  public static FilesystemCommand tail(String path, int limit) {
+    return new FilesystemCommand(
+        Type.TAIL, path, Collections.singletonList(path), -1, limit, "", "", "", "");
+  }
+
+  public static FilesystemCommand option(Type type, String option, String path) {
+    return new FilesystemCommand(
+        type, path, Collections.singletonList(path), -1, -1, option, "", "", "");
+  }
+
+  public static FilesystemCommand pattern(Type type, String pattern, String path) {
+    return new FilesystemCommand(
+        type, path, Collections.singletonList(path), -1, -1, "", pattern, "", "");
   }
 
   public static FilesystemCommand tree(String path, int depth) {
     return new FilesystemCommand(
-        Type.TREE, path, Collections.singletonList(path), depth, -1, "", "");
+        Type.TREE, path, Collections.singletonList(path), depth, -1, "", "", "", "");
   }
 
   public static FilesystemCommand sql(String statement) {
-    return new FilesystemCommand(Type.SQL, "", Collections.emptyList(), -1, -1, statement, "");
+    return new FilesystemCommand(
+        Type.SQL, "", Collections.emptyList(), -1, -1, "", "", statement, "");
   }
 
   public static FilesystemCommand invalid(String errorMessage) {
     return new FilesystemCommand(
-        Type.INVALID, "", Collections.emptyList(), -1, -1, "", errorMessage);
+        Type.INVALID, "", Collections.emptyList(), -1, -1, "", "", "", errorMessage);
   }
 
   public Type getType() {
@@ -117,6 +152,14 @@ public class FilesystemCommand {
     return limit;
   }
 
+  public String getOption() {
+    return option;
+  }
+
+  public String getPattern() {
+    return pattern;
+  }
+
   public String getStatement() {
     return statement;
   }
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 7bdab81d178..61000c17ee3 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
@@ -71,6 +71,39 @@ public class FilesystemCommandParser {
     if ("head".equals(command)) {
       return parseHead(tokens);
     }
+    if ("tail".equals(command)) {
+      return parseTail(tokens);
+    }
+    if ("wc".equals(command)) {
+      return parseWc(tokens);
+    }
+    if ("grep".equals(command)) {
+      return parseGrep(tokens);
+    }
+    if ("find".equals(command)) {
+      return parseFind(tokens);
+    }
+    if ("less".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.LESS, pathArgument(tokens));
+    }
+    if ("more".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.MORE, pathArgument(tokens));
+    }
+    if ("file".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.FILE, pathArgument(tokens));
+    }
+    if ("du".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.DU, pathArgument(tokens));
+    }
+    if ("mkdir".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.MKDIR, pathArgument(tokens));
+    }
+    if ("rm".equals(command)) {
+      return parseRm(tokens);
+    }
+    if ("mv".equals(command)) {
+      return parseMv(tokens);
+    }
     if ("paste".equals(command)) {
       return parsePaste(tokens);
     }
@@ -99,6 +132,26 @@ public class FilesystemCommandParser {
     return FilesystemCommand.paths(FilesystemCommand.Type.PASTE, paths);
   }
 
+  private static FilesystemCommand parseRm(String[] tokens) {
+    if (tokens.length < 2) {
+      return FilesystemCommand.invalid("Missing rm path");
+    }
+    if (tokens[1].startsWith("-")) {
+      return FilesystemCommand.invalid("Unsupported rm option: " + tokens[1]);
+    }
+    return FilesystemCommand.path(FilesystemCommand.Type.RM, tokens[1]);
+  }
+
+  private static FilesystemCommand parseMv(String[] tokens) {
+    if (tokens.length < 3) {
+      return FilesystemCommand.invalid("Usage: mv <source> <target>");
+    }
+    List<String> paths = new ArrayList<>();
+    paths.add(tokens[1]);
+    paths.add(tokens[2]);
+    return FilesystemCommand.paths(FilesystemCommand.Type.MV, paths);
+  }
+
   private static FilesystemCommand parseLs(String[] tokens) {
     FilesystemCommand.Type type = FilesystemCommand.Type.LS;
     String path = DEFAULT_PATH;
@@ -164,6 +217,80 @@ public class FilesystemCommandParser {
     return FilesystemCommand.head(path, limit);
   }
 
+  private static FilesystemCommand parseTail(String[] tokens) {
+    String path = DEFAULT_PATH;
+    int limit = DEFAULT_HEAD_LIMIT;
+
+    for (int i = 1; i < tokens.length; i++) {
+      String token = tokens[i];
+      if ("-n".equals(token)) {
+        if (i + 1 >= tokens.length) {
+          return FilesystemCommand.invalid("Missing tail line count");
+        }
+        try {
+          limit = Integer.parseInt(tokens[++i]);
+        } catch (NumberFormatException e) {
+          return FilesystemCommand.invalid("Invalid tail line count: " + tokens[i]);
+        }
+      } else if (token.startsWith("-") && token.length() > 1) {
+        try {
+          limit = Integer.parseInt(token.substring(1));
+        } catch (NumberFormatException e) {
+          return FilesystemCommand.invalid("Unsupported tail option: " + token);
+        }
+      } else {
+        path = token;
+      }
+    }
+
+    if (limit < 0) {
+      return FilesystemCommand.invalid("Invalid tail line count: " + limit);
+    }
+    return FilesystemCommand.tail(path, limit);
+  }
+
+  private static FilesystemCommand parseWc(String[] tokens) {
+    String option = "-l";
+    String path = DEFAULT_PATH;
+
+    for (int i = 1; i < tokens.length; i++) {
+      String token = tokens[i];
+      if (token.startsWith("-")) {
+        if (!"-l".equals(token)) {
+          return FilesystemCommand.invalid("Unsupported wc option: " + token);
+        }
+        option = token;
+      } else {
+        path = token;
+      }
+    }
+    return FilesystemCommand.option(FilesystemCommand.Type.WC, option, path);
+  }
+
+  private static FilesystemCommand parseGrep(String[] tokens) {
+    if (tokens.length < 3) {
+      return FilesystemCommand.invalid("Usage: grep <pattern> <path>");
+    }
+    return FilesystemCommand.pattern(FilesystemCommand.Type.GREP, tokens[1], tokens[2]);
+  }
+
+  private static FilesystemCommand parseFind(String[] tokens) {
+    String path = tokens.length > 1 ? tokens[1] : DEFAULT_PATH;
+    String pattern = "";
+
+    for (int i = 2; i < tokens.length; i++) {
+      if ("-name".equals(tokens[i])) {
+        if (i + 1 >= tokens.length) {
+          return FilesystemCommand.invalid("Missing find name pattern");
+        }
+        pattern = tokens[++i];
+      } else {
+        return FilesystemCommand.invalid("Unsupported find option: " + tokens[i]);
+      }
+    }
+    return FilesystemCommand.pattern(FilesystemCommand.Type.FIND, pattern, path);
+  }
+
   private static FilesystemCommand parseTree(String[] tokens) {
     String path = DEFAULT_PATH;
     int depth = DEFAULT_TREE_DEPTH;
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
similarity index 74%
copy from iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java
copy to iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
index c372c2e3509..325025178ed 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
@@ -17,12 +17,17 @@
  * under the License.
  */
 
-package org.apache.iotdb.cli.fs.sql;
+package org.apache.iotdb.cli.fs.provider;
+
+import org.apache.iotdb.cli.fs.path.FsPath;
 
 import java.sql.SQLException;
-import java.util.List;
 
-public interface SqlExecutor {
+public interface FilesystemMutationProvider {
+
+  void mkdir(FsPath path) throws SQLException;
+
+  void remove(FsPath path) throws SQLException;
 
-  List<SqlRow> query(String sql) throws SQLException;
+  void move(FsPath source, FsPath target) throws SQLException;
 }
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
index f96f617f363..de92c9bf983 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
@@ -34,6 +34,14 @@ public interface FilesystemSchemaProvider {
 
   List<SqlRow> read(FsPath path, int limit) throws SQLException;
 
+  default List<SqlRow> tail(FsPath path, int limit) throws SQLException {
+    throw new SQLException("Path does not support tail: " + path);
+  }
+
+  default long count(FsPath path) throws SQLException {
+    throw new SQLException("Path does not support count: " + path);
+  }
+
   default List<SqlRow> read(List<FsPath> paths, int limit) throws SQLException {
     if (paths.size() == 1) {
       return read(paths.get(0), limit);
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
new file mode 100644
index 00000000000..153bd53d9ae
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.cli.fs.provider;
+
+import org.apache.iotdb.cli.fs.path.FsPath;
+import org.apache.iotdb.cli.fs.sql.SqlExecutor;
+
+import java.sql.SQLException;
+import java.util.List;
+
+public class TableFilesystemMutationProvider implements FilesystemMutationProvider {
+
+  private static final String INVALID_WRITE_OPERATION =
+      "Invalid filesystem write operation for this path";
+
+  private final SqlExecutor executor;
+
+  public TableFilesystemMutationProvider(SqlExecutor executor) {
+    this.executor = executor;
+  }
+
+  @Override
+  public void mkdir(FsPath path) throws SQLException {
+    if (path.getSegments().size() != 1) {
+      throw invalidOperation();
+    }
+    executor.execute("CREATE DATABASE " + path.getFileName());
+  }
+
+  @Override
+  public void remove(FsPath path) throws SQLException {
+    if (path.getSegments().size() != 2) {
+      throw invalidOperation();
+    }
+    executor.execute("DROP TABLE " + toTablePath(path));
+  }
+
+  @Override
+  public void move(FsPath source, FsPath target) throws SQLException {
+    if (source.getSegments().size() != 2 || target.getSegments().size() != 2) {
+      throw invalidOperation();
+    }
+    if (!parent(source).equals(parent(target))) {
+      throw invalidOperation();
+    }
+    executor.execute("ALTER TABLE " + toTablePath(source) + " RENAME TO " + target.getFileName());
+  }
+
+  private static SQLException invalidOperation() {
+    return new SQLException(INVALID_WRITE_OPERATION);
+  }
+
+  private static String toTablePath(FsPath path) {
+    List<String> segments = path.getSegments();
+    return segments.get(0) + "." + segments.get(1);
+  }
+
+  private static FsPath parent(FsPath path) {
+    List<String> segments = path.getSegments();
+    StringBuilder builder = new StringBuilder("/");
+    for (int i = 0; i < segments.size() - 1; i++) {
+      if (i > 0) {
+        builder.append('/');
+      }
+      builder.append(segments.get(i));
+    }
+    return FsPath.absolute(builder.toString());
+  }
+}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
index 031ca186ab8..d735fa99a8b 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
@@ -27,6 +27,7 @@ import org.apache.iotdb.cli.fs.sql.SqlRow;
 
 import java.sql.SQLException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
@@ -87,6 +88,46 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     throw new SQLException("Path is not readable: " + path);
   }
 
+  @Override
+  public List<SqlRow> tail(FsPath path, int limit) throws SQLException {
+    int depth = path.getSegments().size();
+    List<SqlRow> rows;
+    if (depth == 2) {
+      rows =
+          executor.query(
+              "SELECT * FROM " + toTablePath(path) + " ORDER BY time DESC LIMIT " + limit);
+    } else if (depth == 3) {
+      String tablePath = toTablePath(parent(path));
+      rows =
+          executor.query(
+              "SELECT "
+                  + path.getFileName()
+                  + " FROM "
+                  + tablePath
+                  + " ORDER BY time DESC LIMIT "
+                  + limit);
+    } else {
+      throw new SQLException("Path is not readable: " + path);
+    }
+    Collections.reverse(rows);
+    return rows;
+  }
+
+  @Override
+  public long count(FsPath path) throws SQLException {
+    int depth = path.getSegments().size();
+    List<SqlRow> rows;
+    if (depth == 2) {
+      rows = executor.query("SELECT COUNT(*) FROM " + toTablePath(path));
+    } else if (depth == 3) {
+      String tablePath = toTablePath(parent(path));
+      rows = executor.query("SELECT COUNT(" + path.getFileName() + ") FROM " + tablePath);
+    } else {
+      throw new SQLException("Path is not countable: " + path);
+    }
+    return countValue(rows);
+  }
+
   @Override
   public List<SqlRow> read(List<FsPath> paths, int limit) throws SQLException {
     if (paths.size() == 1) {
@@ -182,6 +223,13 @@ public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider {
     return builder.toString();
   }
 
+  private static long countValue(List<SqlRow> rows) {
+    if (rows.isEmpty() || rows.get(0).asMap().isEmpty()) {
+      return 0;
+    }
+    return Long.parseLong(rows.get(0).asMap().values().iterator().next());
+  }
+
   private static FsPath parent(FsPath path) {
     List<String> segments = path.getSegments();
     StringBuilder builder = new StringBuilder("/");
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProvider.java
index 0766bb271d4..292cb5da540 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProvider.java
@@ -27,6 +27,7 @@ import org.apache.iotdb.cli.fs.sql.SqlRow;
 
 import java.sql.SQLException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
@@ -78,6 +79,34 @@ public class TreeFilesystemSchemaProvider implements FilesystemSchemaProvider {
         "SELECT " + measurement + " FROM " + toTreePath(devicePath) + " LIMIT " + limit);
   }
 
+  @Override
+  public List<SqlRow> tail(FsPath path, int limit) throws SQLException {
+    String measurement = path.getFileName();
+    FsPath devicePath = parent(path);
+    List<SqlRow> rows =
+        executor.query(
+            "SELECT "
+                + measurement
+                + " FROM "
+                + toTreePath(devicePath)
+                + " ORDER BY time DESC LIMIT "
+                + limit);
+    Collections.reverse(rows);
+    return rows;
+  }
+
+  @Override
+  public long count(FsPath path) throws SQLException {
+    String measurement = path.getFileName();
+    FsPath devicePath = parent(path);
+    List<SqlRow> rows =
+        executor.query("SELECT COUNT(" + measurement + ") FROM " + toTreePath(devicePath));
+    if (rows.isEmpty() || rows.get(0).asMap().isEmpty()) {
+      return 0;
+    }
+    return Long.parseLong(rows.get(0).asMap().values().iterator().next());
+  }
+
   private List<FsNode> listTreeRoots() throws SQLException {
     Set<String> roots = new LinkedHashSet<>();
     for (SqlRow row : executor.query("SHOW DATABASES")) {
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
similarity index 62%
copy from iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
copy to iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
index f96f617f363..9f4a08ceb01 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
@@ -19,25 +19,30 @@
 
 package org.apache.iotdb.cli.fs.provider;
 
-import org.apache.iotdb.cli.fs.node.FsNode;
 import org.apache.iotdb.cli.fs.path.FsPath;
-import org.apache.iotdb.cli.fs.sql.SqlRow;
 
 import java.sql.SQLException;
-import java.util.List;
 
-public interface FilesystemSchemaProvider {
+public class UnsupportedFilesystemMutationProvider implements FilesystemMutationProvider {
 
-  List<FsNode> list(FsPath path) throws SQLException;
+  private static final String UNSUPPORTED = "Filesystem write operation is not supported";
 
-  FsNode describe(FsPath path) throws SQLException;
+  @Override
+  public void mkdir(FsPath path) throws SQLException {
+    throw unsupported();
+  }
+
+  @Override
+  public void remove(FsPath path) throws SQLException {
+    throw unsupported();
+  }
 
-  List<SqlRow> read(FsPath path, int limit) throws SQLException;
+  @Override
+  public void move(FsPath source, FsPath target) throws SQLException {
+    throw unsupported();
+  }
 
-  default List<SqlRow> read(List<FsPath> paths, int limit) throws SQLException {
-    if (paths.size() == 1) {
-      return read(paths.get(0), limit);
-    }
-    throw new SQLException("Multiple paths are not readable by this provider");
+  private static SQLException unsupported() {
+    return new SQLException(UNSUPPORTED);
   }
 }
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java
index 31a5925a64b..36a7e9016ee 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java
@@ -54,4 +54,11 @@ public class JdbcSqlExecutor implements SqlExecutor {
       return rows;
     }
   }
+
+  @Override
+  public void execute(String sql) throws SQLException {
+    try (Statement statement = connection.createStatement()) {
+      statement.execute(sql);
+    }
+  }
 }
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java
index c372c2e3509..6a6cb7dd39a 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java
@@ -25,4 +25,6 @@ import java.util.List;
 public interface SqlExecutor {
 
   List<SqlRow> query(String sql) throws SQLException;
+
+  void execute(String sql) throws SQLException;
 }
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 0a98529f6af..aa68d220e77 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
@@ -152,7 +152,8 @@ public class JlineUtils {
   static Completer createCompleter(String accessMode) {
     if ("filesystem".equalsIgnoreCase(accessMode)) {
       return new StringsCompleter(
-          "pwd", "ls", "ll", "cd", "stat", "cat", "head", "paste", "tree", "help", "exit", "quit");
+          "pwd", "ls", "ll", "cd", "stat", "cat", "head", "tail", "wc", "grep", "find", "less",
+          "more", "file", "du", "mkdir", "rm", "mv", "paste", "tree", "help", "exit", "quit");
     }
     return new StringsCompleter(SQL_KEYWORDS);
   }
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/AbstractCliTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/AbstractCliTest.java
index aa6ed5332fd..f2de555a8aa 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/AbstractCliTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/AbstractCliTest.java
@@ -136,6 +136,35 @@ public class AbstractCliTest {
     assertEquals(AbstractCli.ACCESS_MODE_FILESYSTEM, AbstractCli.getAccessMode(ctx, commandLine));
   }
 
+  @Test
+  public void testFsWriteModeEnabled() throws ParseException, ArgsErrorException {
+    CliContext ctx = new CliContext(System.in, System.out, System.err, ExitType.EXCEPTION);
+    Options options = AbstractCli.createOptions();
+    CommandLineParser parser = new DefaultParser();
+    CommandLine commandLine =
+        parser.parse(options, new String[] {"-u", "root", "--fs_write_mode", "enabled"});
+
+    assertEquals(AbstractCli.FS_WRITE_MODE_ENABLED, AbstractCli.getFsWriteMode(ctx, commandLine));
+  }
+
+  @Test
+  public void testFsWriteModeRejectsInvalidValue() throws ParseException {
+    CliContext ctx = new CliContext(System.in, System.out, System.err, ExitType.EXCEPTION);
+    Options options = AbstractCli.createOptions();
+    CommandLineParser parser = new DefaultParser();
+    CommandLine commandLine =
+        parser.parse(options, new String[] {"-u", "root", "--fs_write_mode", "bad"});
+
+    try {
+      AbstractCli.getFsWriteMode(ctx, commandLine);
+      fail();
+    } catch (ArgsErrorException e) {
+      assertEquals(
+          "IoTDB: Unsupported fs write mode 'bad'. Supported values are disabled and enabled.",
+          e.getMessage());
+    }
+  }
+
   @Test
   public void testAccessModeRejectsInvalidValue() throws ParseException {
     CliContext ctx = new CliContext(System.in, System.out, System.err, ExitType.EXCEPTION);
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java
index e50a51efd68..d3d44a00972 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java
@@ -71,6 +71,32 @@ public class CliFilesystemModeTest {
     verify(statement).executeQuery("SHOW TABLES FROM db1");
   }
 
+  @Test
+  public void createFilesystemShellUsesWriteModeForTableDialect() throws Exception {
+    when(connection.getSqlDialect()).thenReturn("table");
+    when(connection.createStatement()).thenReturn(statement);
+    AbstractCli.setFsWriteMode(AbstractCli.FS_WRITE_MODE_ENABLED);
+    try {
+      FilesystemShell shell = Cli.createFilesystemShell(ctx, connection);
+      shell.execute("mkdir /db1");
+    } finally {
+      AbstractCli.setFsWriteMode(AbstractCli.FS_WRITE_MODE_DISABLED);
+    }
+
+    verify(statement).execute("CREATE DATABASE db1");
+  }
+
+  @Test
+  public void createFilesystemShellRejectsWriteWhenWriteModeDisabled() throws Exception {
+    when(connection.getSqlDialect()).thenReturn("table");
+    AbstractCli.setFsWriteMode(AbstractCli.FS_WRITE_MODE_DISABLED);
+
+    FilesystemShell shell = Cli.createFilesystemShell(ctx, connection);
+    shell.execute("mkdir /db1");
+
+    org.junit.Assert.assertTrue(out.toString().contains("Read-only file system"));
+  }
+
   @Test
   public void createFilesystemShellUsesTreeProviderForTreeDialect() throws Exception {
     when(connection.getSqlDialect()).thenReturn("tree");
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 e4ea9161a85..7461aad1367 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
@@ -22,6 +22,7 @@ package org.apache.iotdb.cli.fs;
 import org.apache.iotdb.cli.fs.node.FsNode;
 import org.apache.iotdb.cli.fs.node.FsNodeType;
 import org.apache.iotdb.cli.fs.path.FsPath;
+import org.apache.iotdb.cli.fs.provider.FilesystemMutationProvider;
 import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
 import org.apache.iotdb.cli.fs.sql.SqlRow;
 import org.apache.iotdb.cli.type.ExitType;
@@ -47,12 +48,15 @@ import java.util.stream.Collectors;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 public class FilesystemShellTest {
 
   @Mock private FilesystemSchemaProvider provider;
+  @Mock private FilesystemMutationProvider mutationProvider;
 
   private ByteArrayOutputStream out;
   private FilesystemShell shell;
@@ -81,11 +85,13 @@ public class FilesystemShellTest {
   public void executeLsPrintsChildNodes() throws SQLException {
     when(provider.list(FsPath.absolute("/")))
         .thenReturn(
-            Arrays.asList(new FsNode("root", FsPath.absolute("/root"), FsNodeType.TREE_ROOT)));
+            Arrays.asList(
+                new FsNode("root", FsPath.absolute("/root"), FsNodeType.TREE_ROOT),
+                new FsNode("test", FsPath.absolute("/test"), FsNodeType.TREE_ROOT)));
 
     assertTrue(shell.execute("ls /"));
 
-    assertTrue(out.toString().contains("root"));
+    assertTrue(out.toString().contains("root,test"));
     assertFalse(out.toString().contains("TREE_ROOT"));
     verify(provider).list(FsPath.absolute("/"));
   }
@@ -136,6 +142,27 @@ public class FilesystemShellTest {
     assertFalse(shell.execute("exit"));
   }
 
+  @Test
+  public void executeWriteCommandRejectsReadOnlyMode() throws SQLException {
+    assertTrue(shell.execute("mkdir /db1"));
+
+    assertTrue(out.toString().contains("mkdir: /db1: Read-only file system"));
+    verifyZeroInteractions(mutationProvider);
+  }
+
+  @Test
+  public void executeWriteCommandsWhenEnabled() throws SQLException {
+    shell = new FilesystemShell(shellContext(), provider, mutationProvider, true);
+
+    assertTrue(shell.execute("mkdir /db1"));
+    assertTrue(shell.execute("rm /db1/table1"));
+    assertTrue(shell.execute("mv /db1/table1 /db1/table2"));
+
+    verify(mutationProvider).mkdir(FsPath.absolute("/db1"));
+    verify(mutationProvider).remove(FsPath.absolute("/db1/table1"));
+    verify(mutationProvider).move(FsPath.absolute("/db1/table1"), FsPath.absolute("/db1/table2"));
+  }
+
   @Test
   public void executeTreePrintsChildrenUntilDepth() throws SQLException {
     when(provider.list(FsPath.absolute("/")))
@@ -193,6 +220,99 @@ public class FilesystemShellTest {
     verify(provider).read(FsPath.absolute("/db1/table1"), 5);
   }
 
+  @Test
+  public void executeTailReadsPathWithLimit() throws SQLException {
+    when(provider.tail(FsPath.absolute("/db1/table1"), 3))
+        .thenReturn(Arrays.asList(SqlRow.of("Time", "2", "tag1", "b", "s1", "43")));
+
+    assertTrue(shell.execute("tail -n 3 /db1/table1"));
+
+    assertTrue(out.toString().contains("2\tb\t43"));
+    verify(provider).tail(FsPath.absolute("/db1/table1"), 3);
+  }
+
+  @Test
+  public void executeWcLineCountPrintsCountAndPath() throws SQLException {
+    when(provider.count(FsPath.absolute("/db1/table1"))).thenReturn(2L);
+
+    assertTrue(shell.execute("wc -l /db1/table1"));
+
+    assertTrue(out.toString().contains("2 /db1/table1"));
+    verify(provider).count(FsPath.absolute("/db1/table1"));
+  }
+
+  @Test
+  public void executeGrepPrintsOnlyMatchingRows() throws SQLException {
+    when(provider.read(FsPath.absolute("/db1/table1"), 20))
+        .thenReturn(
+            Arrays.asList(
+                SqlRow.of("Time", "1", "tag1", "spricoder", "s1", "42"),
+                SqlRow.of("Time", "2", "tag1", "other", "s1", "43")));
+
+    assertTrue(shell.execute("grep spricoder /db1/table1"));
+
+    assertTrue(out.toString().contains("1\tspricoder\t42"));
+    assertFalse(out.toString().contains("2\tother\t43"));
+    verify(provider).read(FsPath.absolute("/db1/table1"), 20);
+  }
+
+  @Test
+  public void executeFindPrintsMatchingDescendants() throws SQLException {
+    when(provider.describe(FsPath.absolute("/")))
+        .thenReturn(new FsNode("/", FsPath.absolute("/"), FsNodeType.VIRTUAL_ROOT));
+    when(provider.describe(FsPath.absolute("/db1")))
+        .thenReturn(new FsNode("db1", FsPath.absolute("/db1"), FsNodeType.TABLE_DATABASE));
+    when(provider.describe(FsPath.absolute("/db1/table1")))
+        .thenReturn(new FsNode("table1", FsPath.absolute("/db1/table1"), FsNodeType.TABLE_TABLE));
+    when(provider.list(FsPath.absolute("/")))
+        .thenReturn(
+            Arrays.asList(new FsNode("db1", FsPath.absolute("/db1"), FsNodeType.TABLE_DATABASE)));
+    when(provider.list(FsPath.absolute("/db1")))
+        .thenReturn(
+            Arrays.asList(
+                new FsNode("table1", FsPath.absolute("/db1/table1"), FsNodeType.TABLE_TABLE)));
+    when(provider.list(FsPath.absolute("/db1/table1"))).thenReturn(new ArrayList<>());
+
+    assertTrue(shell.execute("find / -name table1"));
+
+    assertTrue(out.toString().contains("/db1/table1"));
+    verify(provider).list(FsPath.absolute("/"));
+    verify(provider).list(FsPath.absolute("/db1"));
+  }
+
+  @Test
+  public void executeLessAndMoreReadPath() throws SQLException {
+    when(provider.read(FsPath.absolute("/db1/table1"), 20))
+        .thenReturn(Arrays.asList(SqlRow.of("Time", "1", "tag1", "a", "s1", "42")));
+
+    assertTrue(shell.execute("less /db1/table1"));
+    assertTrue(shell.execute("more /db1/table1"));
+
+    assertTrue(out.toString().contains("1\ta\t42"));
+    verify(provider, times(2)).read(FsPath.absolute("/db1/table1"), 20);
+  }
+
+  @Test
+  public void executeFilePrintsNodeType() throws SQLException {
+    when(provider.describe(FsPath.absolute("/db1/table1")))
+        .thenReturn(new FsNode("table1", FsPath.absolute("/db1/table1"), FsNodeType.TABLE_TABLE));
+
+    assertTrue(shell.execute("file /db1/table1"));
+
+    assertTrue(out.toString().contains("/db1/table1: TABLE_TABLE"));
+    verify(provider).describe(FsPath.absolute("/db1/table1"));
+  }
+
+  @Test
+  public void executeDuPrintsLogicalSizeAndPath() throws SQLException {
+    when(provider.count(FsPath.absolute("/db1/table1"))).thenReturn(2L);
+
+    assertTrue(shell.execute("du /db1/table1"));
+
+    assertTrue(out.toString().contains("2\t/db1/table1"));
+    verify(provider).count(FsPath.absolute("/db1/table1"));
+  }
+
   @Test
   public void executePasteReadsMultiplePaths() throws SQLException {
     when(provider.read(
@@ -231,4 +351,12 @@ public class FilesystemShellTest {
     completer.complete(null, parsedLine, candidates);
     return candidates.stream().map(Candidate::value).collect(Collectors.toList());
   }
+
+  private CliContext shellContext() {
+    return new CliContext(
+        new ByteArrayInputStream(new byte[0]),
+        new PrintStream(out),
+        System.err,
+        ExitType.EXCEPTION);
+  }
 }
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 2816912ab0b..7224876d239 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
@@ -87,6 +87,54 @@ public class FilesystemCommandParserTest {
     assertEquals(5, command.getLimit());
   }
 
+  @Test
+  public void parseTailLimitAndPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("tail -n 3 /db1/table1");
+
+    assertEquals(FilesystemCommand.Type.TAIL, command.getType());
+    assertEquals("/db1/table1", command.getPath());
+    assertEquals(3, command.getLimit());
+  }
+
+  @Test
+  public void parseWcLineCountAndPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("wc -l /db1/table1");
+
+    assertEquals(FilesystemCommand.Type.WC, command.getType());
+    assertEquals("/db1/table1", command.getPath());
+    assertEquals("-l", command.getOption());
+  }
+
+  @Test
+  public void parseGrepPatternAndPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("grep spricoder /db1/table1");
+
+    assertEquals(FilesystemCommand.Type.GREP, command.getType());
+    assertEquals("/db1/table1", command.getPath());
+    assertEquals("spricoder", command.getPattern());
+  }
+
+  @Test
+  public void parseFindNamePatternAndPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("find /db1 -name table1");
+
+    assertEquals(FilesystemCommand.Type.FIND, command.getType());
+    assertEquals("/db1", command.getPath());
+    assertEquals("table1", command.getPattern());
+  }
+
+  @Test
+  public void parseLessMoreFileAndDu() {
+    assertEquals(
+        FilesystemCommand.Type.LESS, FilesystemCommandParser.parse("less /db1/table1").getType());
+    assertEquals(
+        FilesystemCommand.Type.MORE, FilesystemCommandParser.parse("more /db1/table1").getType());
+    assertEquals(
+        FilesystemCommand.Type.FILE, FilesystemCommandParser.parse("file /db1/table1").getType());
+    assertEquals(
+        FilesystemCommand.Type.DU, FilesystemCommandParser.parse("du /db1/table1").getType());
+  }
+
   @Test
   public void parsePastePaths() {
     FilesystemCommand command =
@@ -98,6 +146,23 @@ public class FilesystemCommandParserTest {
     assertEquals("/db1/table1/s1", command.getPaths().get(1));
   }
 
+  @Test
+  public void parseWriteCommands() {
+    FilesystemCommand mkdir = FilesystemCommandParser.parse("mkdir /db1");
+    assertEquals(FilesystemCommand.Type.MKDIR, mkdir.getType());
+    assertEquals("/db1", mkdir.getPath());
+
+    FilesystemCommand rm = FilesystemCommandParser.parse("rm /db1/table1");
+    assertEquals(FilesystemCommand.Type.RM, rm.getType());
+    assertEquals("/db1/table1", rm.getPath());
+
+    FilesystemCommand mv = FilesystemCommandParser.parse("mv /db1/table1 /db1/table2");
+    assertEquals(FilesystemCommand.Type.MV, mv.getType());
+    assertEquals(2, mv.getPaths().size());
+    assertEquals("/db1/table1", mv.getPaths().get(0));
+    assertEquals("/db1/table2", mv.getPaths().get(1));
+  }
+
   @Test
   public void parseTreeDepthBeforePath() {
     FilesystemCommand command = FilesystemCommandParser.parse("tree -L 2 /root/sg");
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
new file mode 100644
index 00000000000..955046688c4
--- /dev/null
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.cli.fs.provider;
+
+import org.apache.iotdb.cli.fs.path.FsPath;
+import org.apache.iotdb.cli.fs.sql.SqlExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.sql.SQLException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.verify;
+
+public class TableFilesystemMutationProviderTest {
+
+  @Mock private SqlExecutor executor;
+
+  private TableFilesystemMutationProvider provider;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    provider = new TableFilesystemMutationProvider(executor);
+  }
+
+  @Test
+  public void mkdirDatabaseCreatesDatabase() throws SQLException {
+    provider.mkdir(FsPath.absolute("/db1"));
+
+    verify(executor).execute("CREATE DATABASE db1");
+  }
+
+  @Test
+  public void mkdirRejectsRootAndTableLevel() throws SQLException {
+    assertInvalidOperation(() -> provider.mkdir(FsPath.absolute("/")));
+    assertInvalidOperation(() -> provider.mkdir(FsPath.absolute("/db1/table1")));
+  }
+
+  @Test
+  public void removeTableDropsTable() throws SQLException {
+    provider.remove(FsPath.absolute("/db1/table1"));
+
+    verify(executor).execute("DROP TABLE db1.table1");
+  }
+
+  @Test
+  public void removeRejectsRootDatabaseAndColumnLevel() throws SQLException {
+    assertInvalidOperation(() -> provider.remove(FsPath.absolute("/")));
+    assertInvalidOperation(() -> provider.remove(FsPath.absolute("/db1")));
+    assertInvalidOperation(() -> provider.remove(FsPath.absolute("/db1/table1/s1")));
+  }
+
+  @Test
+  public void moveTableRenamesTableInSameDatabase() throws SQLException {
+    provider.move(FsPath.absolute("/db1/table1"), FsPath.absolute("/db1/table2"));
+
+    verify(executor).execute("ALTER TABLE db1.table1 RENAME TO table2");
+  }
+
+  @Test
+  public void moveRejectsUnsafeLevelsAndCrossDatabaseRename() throws SQLException {
+    assertInvalidOperation(() -> provider.move(FsPath.absolute("/db1"), FsPath.absolute("/db2")));
+    assertInvalidOperation(
+        () -> provider.move(FsPath.absolute("/db1/table1/s1"), FsPath.absolute("/db1/table1/s2")));
+    assertInvalidOperation(
+        () -> provider.move(FsPath.absolute("/db1/table1"), FsPath.absolute("/db2/table1")));
+  }
+
+  private static void assertInvalidOperation(SqlOperation operation) throws SQLException {
+    try {
+      operation.run();
+      fail();
+    } catch (SQLException e) {
+      assertEquals("Invalid filesystem write operation for this path", e.getMessage());
+    }
+  }
+
+  private interface SqlOperation {
+    void run() throws SQLException;
+  }
+}
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
index d846c3b7be8..9d7c1bb6715 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
@@ -171,6 +171,32 @@ public class TableFilesystemSchemaProviderTest {
     verify(executor).query("SELECT * FROM db1.table1 LIMIT 5");
   }
 
+  @Test
+  public void tailTableSelectsNewestRowsAndReturnsOriginalOrder() throws SQLException {
+    when(executor.query("SELECT * FROM db1.table1 ORDER BY time DESC LIMIT 2"))
+        .thenReturn(
+            SqlRow.list(
+                SqlRow.of("Time", "2", "tag1", "b", "s1", "43"),
+                SqlRow.of("Time", "1", "tag1", "a", "s1", "42")));
+
+    List<SqlRow> rows = provider.tail(FsPath.absolute("/db1/table1"), 2);
+
+    assertEquals("1", rows.get(0).get("Time"));
+    assertEquals("2", rows.get(1).get("Time"));
+    verify(executor).query("SELECT * FROM db1.table1 ORDER BY time DESC LIMIT 2");
+  }
+
+  @Test
+  public void countTableSelectsRowCount() throws SQLException {
+    when(executor.query("SELECT COUNT(*) FROM db1.table1"))
+        .thenReturn(SqlRow.list(SqlRow.of("count", "2")));
+
+    long count = provider.count(FsPath.absolute("/db1/table1"));
+
+    assertEquals(2L, count);
+    verify(executor).query("SELECT COUNT(*) FROM db1.table1");
+  }
+
   @Test
   public void readColumnsSelectsMultipleColumnsFromSameTable() throws SQLException {
     when(executor.query("SELECT tag1, s1 FROM db1.table1 LIMIT 5"))
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutorTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutorTest.java
index 83f073e30ea..ae405f859a0 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutorTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutorTest.java
@@ -68,4 +68,14 @@ public class JdbcSqlExecutorTest {
     verify(resultSet).close();
     verify(statement).close();
   }
+
+  @Test
+  public void executeRunsStatement() throws Exception {
+    when(connection.createStatement()).thenReturn(statement);
+
+    new JdbcSqlExecutor(connection).execute("CREATE DATABASE db1");
+
+    verify(statement).execute("CREATE DATABASE db1");
+    verify(statement).close();
+  }
 }


(iotdb) 03/06: add tee

Posted by sp...@apache.org.
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 068425994459ac28c90fca4a7c0cc9be2e51722c
Author: spricoder <sp...@qq.com>
AuthorDate: Thu Apr 30 18:31:33 2026 +0800

    add tee
---
 .../plans/2026-04-29-cli-filesystem-mode.md        |  39 +++
 .../specs/2026-04-29-cli-filesystem-mode-design.md | 102 +++++++
 .../src/main/java/org/apache/iotdb/cli/Cli.java    |   2 +-
 .../org/apache/iotdb/cli/fs/FilesystemShell.java   |  84 +++++-
 .../iotdb/cli/fs/command/FilesystemCommand.java    |   1 +
 .../cli/fs/command/FilesystemCommandParser.java    |  13 +
 .../fs/provider/FilesystemMutationProvider.java    |   3 +
 .../cli/fs/provider/TableCsvAppendPlanner.java     | 304 +++++++++++++++++++++
 .../provider/TableFilesystemMutationProvider.java  |  17 ++
 .../UnsupportedFilesystemMutationProvider.java     |   6 +
 .../org/apache/iotdb/cli/utils/JlineUtils.java     |   3 +-
 .../apache/iotdb/cli/fs/FilesystemShellTest.java   |  54 ++++
 .../fs/command/FilesystemCommandParserTest.java    |  20 ++
 .../TableFilesystemMutationProviderTest.java       |  57 ++++
 14 files changed, 701 insertions(+), 4 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 081a5ce8748..0a85d3525b5 100644
--- a/docs/superpowers/plans/2026-04-29-cli-filesystem-mode.md
+++ b/docs/superpowers/plans/2026-04-29-cli-filesystem-mode.md
@@ -102,6 +102,7 @@ These notes capture follow-up implementation experience for quickly resuming thi
   - `mkdir /db` creates a database.
   - `rm /db/table.csv` drops a table.
   - `mv /db/t1.csv /db/t2.csv` renames a table inside the same database.
+  - `tee -a /db/table.csv` appends CSV records as table rows.
   - Forbid `rm` or `mv` of `/db/table.schema` and `/db/table.meta`.
   - Forbid `rm /db`.
   - Forbid cross-database rename such as `mv /db1/t.csv /db2/t.csv`.
@@ -117,6 +118,43 @@ These notes capture follow-up implementation experience for quickly resuming thi
   the server returned `550`; the unchecked propagation exited the CLI. Keep a regression test for
   this behavior.
 
+## Data Append Design
+
+The first table data-write implementation supports only append semantics over CSV table files:
+
+```bash
+tee -a /db/table.csv
+```
+
+Design decisions already approved:
+
+- Only append writes are supported. Truncate, overwrite, random writes, row deletion, and operating
+  system redirection such as `>> /db/table.csv` are out of scope.
+- The target must be a table-mode data file `/<database>/<table>.csv`.
+- `tee` without `-a`, sidecar files, legacy column paths, and tree-mode paths are rejected.
+- Writes require `--access_mode filesystem --sql_dialect table --fs_write_mode enabled`.
+- Non-interactive mode reads CSV from stdin until EOF and then submits.
+- Interactive mode enters an append buffer. `:wq` validates and submits, `:q!` discards, and `:q`
+  exits only if the buffer is empty.
+- The first version does not support `:w`.
+- CSV parsing should use Apache Commons CSV or another proven parser already available to the
+  module.
+- Header and headerless input are both supported.
+- Header input may write a subset of columns but must include `time`.
+- Headerless input must provide all columns in the current `/db/table.csv` output order.
+- Every record must explicitly provide `time`; `time` cannot be empty or `\N`.
+- `\N` is the explicit SQL `NULL` marker for non-time columns. Empty fields are not automatically
+  NULL.
+- Client-side validation covers path, write mode, CSV shape, known columns, and required `time`.
+- IoTDB performs type validation, timestamp-format validation, permissions checks, and final insert
+  semantics.
+- Validated records map to SQL of the form
+  `INSERT INTO db.table(col1,col2,...) VALUES (...), (...), ...`.
+- Insert statements should be chunked, for example 1000 records per statement.
+- The full buffer must pass client validation before any insert is attempted.
+- Server-side insert failure is not compensated or retried automatically. Interactive mode keeps the
+  buffer; non-interactive mode exits with failure.
+
 ## Supported Command Quick Reference
 
 | Command | Description | Example |
@@ -142,6 +180,7 @@ These notes capture follow-up implementation experience for quickly resuming thi
 | `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` |
 | `mv <source> <target>` | Write-gated; in table mode with writes enabled, only same-database table CSV rename is allowed. | `mv /db/t1.csv /db/t2.csv` |
+| `tee -a <path>` | Write-gated; in table mode with writes enabled, appends CSV input to a table data file. | `tee -a /db/table.csv` |
 | `help` | Print filesystem-mode help. | `help` |
 | `exit` / `quit` | Exit filesystem mode. | `exit` |
 
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 2de2d8a71c6..baeee519fd2 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
@@ -219,6 +219,7 @@ semantics wherever the same command exists. Provider support can still vary by d
 | `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` |
 | `mv <source> <target>` | Write-gated command. With table mode and `--fs_write_mode enabled`, only same-database table CSV rename is allowed. | `mv /db/t1.csv /db/t2.csv` |
+| `tee -a <path>` | Write-gated append command. With table mode and `--fs_write_mode enabled`, only `tee -a /db/table.csv` appends CSV records as rows. | `tee -a /db/table.csv` |
 | `help` | Print filesystem-mode help. | `help` |
 | `exit` / `quit` | Exit filesystem mode. | `exit` |
 
@@ -251,6 +252,7 @@ Raw SQL should be run in the default SQL access mode.
 | `stat /db/table.meta` | `SHOW TABLES FROM db`, filtered to the table and rendered as filesystem metadata for the metadata sidecar. |
 | `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. |
+| `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. |
 | `stat /db/table/col` | Legacy compatibility: `DESC db.table DETAILS`, filtered to the column. |
@@ -321,6 +323,102 @@ example, if `cat time` is resolved from `/testtest` to `/testtest/time`, table m
 table path and may receive a server error such as `550: Table 'testtest.time' does not exist`. That
 error should be printed as `cat: 550: ...`, then the prompt should continue.
 
+## Append Data Write Semantics
+
+Table data writes use Unix append semantics over the table CSV file:
+
+```bash
+tee -a /db/table.csv
+```
+
+Only append writes are in scope for the first data-write implementation. Truncation, overwrite,
+random writes, partial row deletion, and shell redirection such as `>> /db/table.csv` are out of
+scope because filesystem mode is an IoTDB CLI command surface, not an operating-system mount.
+
+Append writes are allowed only when all of these conditions hold:
+
+- CLI is running with `--access_mode filesystem`.
+- CLI is running with `--sql_dialect table`.
+- CLI is running with `--fs_write_mode enabled`.
+- Target path is exactly a table data file of the form `/<database>/<table>.csv`.
+- Command is `tee -a`; `tee` without `-a` is rejected.
+
+The sidecar files remain read-only metadata views:
+
+- `tee -a /db/table.schema` is rejected.
+- `tee -a /db/table.meta` is rejected.
+- Legacy column paths such as `/db/table/col` are not writable append targets.
+- Tree-mode paths remain non-writable even when `--fs_write_mode enabled` is set.
+
+### Input Modes
+
+Non-interactive mode reads CSV from standard input until EOF, then submits once:
+
+```bash
+printf 'time,key,value\n1,spricoder,2.0\n' | \
+  start-cli.sh -h 127.0.0.1 -p 6667 -u root -pw root \
+  --sql_dialect table --access_mode filesystem --fs_write_mode enabled \
+  -e 'tee -a /db/table.csv'
+```
+
+Interactive mode enters an append buffer:
+
+```text
+IoTDB:fs> tee -a /db/table.csv
+time,key,value
+1,spricoder,2.0
+2,spricoder,1.5
+:wq
+```
+
+The append buffer supports a minimal Vim-style command set:
+
+- `:wq`: validate, submit, and exit the append buffer after a successful write.
+- `:q!`: discard the buffer and exit.
+- `:q`: exit only when the buffer is empty; otherwise print guidance to use `:wq` or `:q!`.
+
+The first version intentionally does not support `:w`, because writing while keeping the buffer
+open creates ambiguity around repeated submissions and buffer clearing.
+
+### CSV Input Rules
+
+Append input is CSV and is parsed with a proven CSV parser, not by ad hoc string splitting.
+
+- Both header and headerless input are supported.
+- Header input may provide a subset of columns, but it must include `time`.
+- Headerless input must provide all columns in the current `/db/table.csv` output order.
+- Every appended record must explicitly provide `time`.
+- `time` must not be empty and must not be `\N`.
+- `\N` represents SQL `NULL` for non-time columns.
+- Empty fields are not automatically treated as `NULL`.
+- Client-side validation checks path, write mode, CSV shape, known columns, and required `time`.
+- IoTDB remains responsible for type validation, timestamp-format validation, permissions, and
+  final insert semantics.
+
+### SQL Mapping And Failure Semantics
+
+The provider should map validated append input to table-model SQL:
+
+```sql
+INSERT INTO db.table(col1,col2,...) VALUES (...), (...), ...
+```
+
+Implementation rules:
+
+- Use `DESC db.table DETAILS` or an equivalent schema query to get the table columns and output
+  order.
+- Use SQL identifier rendering and SQL literal escaping helpers; never concatenate raw user values
+  into SQL.
+- Convert `\N` to `NULL`.
+- Submit rows in fixed-size chunks, for example 1000 records per `INSERT`, to avoid oversized SQL
+  statements.
+- Client-side validation must pass for the full buffer before any write is attempted.
+- If a server-side insert fails, the CLI does not compensate or retry automatically. Server state is
+  whatever IoTDB actually committed.
+- In interactive mode, a server-side failure keeps the append buffer so the user can correct input
+  or abandon it with `:q!`.
+- In non-interactive mode, a failure prints a command-prefixed error and exits with failure.
+
 ## Proposed Code Structure
 
 New code should live under `org.apache.iotdb.cli.fs`.
@@ -332,6 +430,7 @@ New code should live under `org.apache.iotdb.cli.fs`.
 - `node/FsNode`, `node/FsNodeType`, `node/FsNodeMetadata`: typed metadata model.
 - `provider/FilesystemSchemaProvider`: schema and data read provider interface.
 - `provider/FilesystemMutationProvider`: write-gated mutation provider interface.
+- `provider/TableCsvAppendPlanner` or equivalent helper: CSV append validation and SQL planning.
 - `provider/TreeFilesystemSchemaProvider`: tree-model SQL mapping.
 - `provider/TableFilesystemSchemaProvider`: table-model SQL mapping.
 - `provider/TableFilesystemMutationProvider`: opt-in table-model write mapping.
@@ -363,6 +462,8 @@ The mutation provider owns the current write-gated operations:
 - `mkdir(FsPath path)`
 - `remove(FsPath path)`
 - `move(FsPath source, FsPath target)`
+- `append(FsPath path, List<String> csvLines)` or an equivalent table-mode append method for
+  `tee -a /db/table.csv`
 
 When writes are disabled, `FilesystemShell` rejects `mkdir`, `rm`, and `mv` before calling the
 mutation provider. When writes are enabled, table mode uses `TableFilesystemMutationProvider`; tree
@@ -373,6 +474,7 @@ The current table-model write boundary is intentionally narrow:
 - `mkdir /db` creates a database directory.
 - `rm /db/table.csv` drops the table data file and therefore the table.
 - `mv /db/t1.csv /db/t2.csv` renames a table within the same database.
+- `tee -a /db/table.csv` appends CSV records as table rows.
 
 The sidecar files are metadata views, not independently writable objects:
 
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
index ad5571efb41..dc42910214c 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
@@ -221,7 +221,7 @@ public class Cli extends AbstractCli {
       connection.setQueryTimeout(queryTimeout);
       properties = connection.getServerProperties();
       timestampPrecision = properties.getTimestampPrecision();
-      createFilesystemShell(ctx, connection).execute(execute);
+      createFilesystemShell(ctx, connection).executeNonInteractive(execute);
       ctx.exit(CODE_OK);
     } catch (SQLException e) {
       ctx.getPrinter()
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 3ac9eca9d94..63c543d40c6 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
@@ -32,9 +32,14 @@ import org.apache.iotdb.cli.utils.CliContext;
 
 import org.jline.reader.Candidate;
 import org.jline.reader.Completer;
+import org.jline.reader.EndOfFileException;
 import org.jline.reader.LineReader;
 import org.jline.reader.ParsedLine;
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -48,8 +53,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");
+          "more", "file", "du", "mkdir", "rm", "mv", "cut", "paste", "tree", "help", "exit", "quit",
+          "tee");
 
   private final CliContext ctx;
   private final FilesystemSchemaProvider provider;
@@ -133,6 +138,9 @@ public class FilesystemShell {
       case PASTE:
         printRows(provider.read(resolve(command.getPaths()), DEFAULT_READ_LIMIT));
         return true;
+      case TEE:
+        append(command.getPath(), false);
+        return true;
       case HELP:
         printHelp();
         return true;
@@ -151,6 +159,15 @@ public class FilesystemShell {
     }
   }
 
+  public boolean executeNonInteractive(String input) throws SQLException {
+    FilesystemCommand command = FilesystemCommandParser.parse(input);
+    if (command.getType() == FilesystemCommand.Type.TEE) {
+      append(command.getPath(), true);
+      return true;
+    }
+    return execute(input);
+  }
+
   public Completer createCompleter() {
     return new FilesystemCompleter();
   }
@@ -363,6 +380,68 @@ public class FilesystemShell {
     mutationProvider.move(source, target);
   }
 
+  private void append(String path, boolean nonInteractive) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    if (!ensureWritable("tee", resolvedPath)) {
+      return;
+    }
+    if (nonInteractive || ctx.getLineReader() == null) {
+      mutationProvider.append(resolvedPath, readStandardInputLines());
+      return;
+    }
+    appendInteractive(resolvedPath);
+  }
+
+  private List<String> readStandardInputLines() throws SQLException {
+    List<String> lines = new ArrayList<>();
+    try {
+      BufferedReader reader =
+          new BufferedReader(new InputStreamReader(ctx.getIn(), StandardCharsets.UTF_8));
+      String line;
+      while ((line = reader.readLine()) != null) {
+        lines.add(line);
+      }
+      return lines;
+    } catch (IOException e) {
+      throw new SQLException("Failed to read standard input", e);
+    }
+  }
+
+  private void appendInteractive(FsPath path) throws SQLException {
+    List<String> lines = new ArrayList<>();
+    while (true) {
+      String line;
+      try {
+        line = ctx.getLineReader().readLine("tee> ", null);
+      } catch (EndOfFileException e) {
+        if (!lines.isEmpty()) {
+          ctx.getPrinter().println("tee: use :wq to write or :q! to quit without writing");
+        }
+        return;
+      }
+      if (":wq".equals(line)) {
+        try {
+          mutationProvider.append(path, lines);
+          return;
+        } catch (SQLException e) {
+          ctx.getPrinter().println("tee: " + e.getMessage());
+          continue;
+        }
+      }
+      if (":q!".equals(line)) {
+        return;
+      }
+      if (":q".equals(line)) {
+        if (lines.isEmpty()) {
+          return;
+        }
+        ctx.getPrinter().println("tee: use :wq to write or :q! to quit without writing");
+        continue;
+      }
+      lines.add(line);
+    }
+  }
+
   private boolean ensureWritable(String command, FsPath path) {
     if (writeEnabled) {
       return true;
@@ -405,6 +484,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("tee -a <path>");
     ctx.getPrinter().println("tree [-L depth] [path]");
     ctx.getPrinter().println("exit");
   }
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 df86856ab80..2f77a787a54 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,
+    TEE,
     TREE,
     SQL,
     HELP,
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 62d267153b7..75992cf0fc7 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
@@ -111,6 +111,9 @@ public class FilesystemCommandParser {
     if ("paste".equals(command)) {
       return parsePaste(tokens);
     }
+    if ("tee".equals(command)) {
+      return parseTee(tokens);
+    }
     if ("tree".equals(command)) {
       return parseTree(tokens);
     }
@@ -136,6 +139,16 @@ public class FilesystemCommandParser {
     return FilesystemCommand.paths(FilesystemCommand.Type.PASTE, paths);
   }
 
+  private static FilesystemCommand parseTee(String[] tokens) {
+    if (tokens.length != 3) {
+      return FilesystemCommand.invalid("Usage: tee -a <path>");
+    }
+    if (!"-a".equals(tokens[1])) {
+      return FilesystemCommand.invalid("Unsupported tee option: " + tokens[1]);
+    }
+    return FilesystemCommand.option(FilesystemCommand.Type.TEE, "-a", tokens[2]);
+  }
+
   private static FilesystemCommand parseCut(String[] tokens) {
     String delimiter = DEFAULT_CUT_DELIMITER;
     String fields = "";
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
index 325025178ed..3d1d3afbd88 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.cli.fs.provider;
 import org.apache.iotdb.cli.fs.path.FsPath;
 
 import java.sql.SQLException;
+import java.util.List;
 
 public interface FilesystemMutationProvider {
 
@@ -30,4 +31,6 @@ public interface FilesystemMutationProvider {
   void remove(FsPath path) throws SQLException;
 
   void move(FsPath source, FsPath target) throws SQLException;
+
+  void append(FsPath path, List<String> lines) throws SQLException;
 }
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableCsvAppendPlanner.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableCsvAppendPlanner.java
new file mode 100644
index 00000000000..f5a0150c8e0
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableCsvAppendPlanner.java
@@ -0,0 +1,304 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.cli.fs.provider;
+
+import org.apache.iotdb.cli.fs.sql.SqlRow;
+
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVRecord;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+class TableCsvAppendPlanner {
+
+  private static final int INSERT_BATCH_SIZE = 1000;
+  private static final String INVALID_WRITE_OPERATION =
+      "Invalid filesystem write operation for this path";
+  private static final String NULL_MARKER = "\\N";
+  private static final CSVFormat CSV_FORMAT =
+      CSVFormat.DEFAULT.builder().setIgnoreEmptyLines(true).build();
+
+  private TableCsvAppendPlanner() {}
+
+  static List<String> plan(String tablePath, List<SqlRow> schemaRows, List<String> lines)
+      throws SQLException {
+    List<CSVRecord> records = parse(lines);
+    if (records.isEmpty()) {
+      return new ArrayList<>();
+    }
+    List<TableColumn> schemaColumns = schemaColumns(schemaRows);
+    if (schemaColumns.isEmpty()) {
+      throw invalidOperation();
+    }
+    ParsedCsv csv = parseCsv(schemaColumns, records);
+    if (csv.rows.isEmpty()) {
+      return new ArrayList<>();
+    }
+    return buildStatements(tablePath, csv.columns, csv.rows);
+  }
+
+  private static List<CSVRecord> parse(List<String> lines) throws SQLException {
+    StringBuilder builder = new StringBuilder();
+    for (String line : lines) {
+      if (builder.length() > 0) {
+        builder.append(System.lineSeparator());
+      }
+      builder.append(line);
+    }
+    try {
+      try (CSVParser parser = CSVParser.parse(builder.toString(), CSV_FORMAT)) {
+        return parser.getRecords();
+      }
+    } catch (IOException e) {
+      throw new SQLException("Failed to parse CSV input", e);
+    }
+  }
+
+  private static List<TableColumn> schemaColumns(List<SqlRow> rows) {
+    List<TableColumn> columns = new ArrayList<>();
+    for (SqlRow row : rows) {
+      String name = row.get("ColumnName");
+      String type = row.get("DataType");
+      if (name != null && type != null) {
+        columns.add(new TableColumn(name, type));
+      }
+    }
+    return columns;
+  }
+
+  private static ParsedCsv parseCsv(List<TableColumn> schemaColumns, List<CSVRecord> records)
+      throws SQLException {
+    Map<String, TableColumn> columnsByName = columnsByName(schemaColumns);
+    CSVRecord first = records.get(0);
+    boolean hasHeader = looksLikeHeader(first, columnsByName);
+    List<TableColumn> selectedColumns;
+    int firstDataIndex;
+    if (hasHeader) {
+      selectedColumns = selectedColumns(first, columnsByName);
+      firstDataIndex = 1;
+    } else {
+      selectedColumns = schemaColumns;
+      firstDataIndex = 0;
+    }
+    if (!containsTime(selectedColumns)) {
+      throw invalidOperation();
+    }
+
+    List<String> rows = new ArrayList<>();
+    for (int i = firstDataIndex; i < records.size(); i++) {
+      rows.add(recordValues(records.get(i), selectedColumns));
+    }
+    return new ParsedCsv(selectedColumns, rows);
+  }
+
+  private static Map<String, TableColumn> columnsByName(List<TableColumn> columns) {
+    Map<String, TableColumn> columnsByName = new LinkedHashMap<>();
+    for (TableColumn column : columns) {
+      columnsByName.put(column.name.toLowerCase(Locale.ROOT), column);
+    }
+    return columnsByName;
+  }
+
+  private static boolean looksLikeHeader(CSVRecord record, Map<String, TableColumn> columnsByName) {
+    if (record.size() == 0) {
+      return false;
+    }
+    for (String value : record) {
+      if (!columnsByName.containsKey(value.toLowerCase(Locale.ROOT))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static List<TableColumn> selectedColumns(
+      CSVRecord header, Map<String, TableColumn> columnsByName) throws SQLException {
+    List<TableColumn> columns = new ArrayList<>();
+    Set<String> selected = new HashSet<>();
+    for (String value : header) {
+      String normalized = value.toLowerCase(Locale.ROOT);
+      TableColumn column = columnsByName.get(normalized);
+      if (column == null || selected.contains(normalized)) {
+        throw invalidOperation();
+      }
+      selected.add(normalized);
+      columns.add(column);
+    }
+    return columns;
+  }
+
+  private static boolean containsTime(List<TableColumn> columns) {
+    for (TableColumn column : columns) {
+      if (column.isTime()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static String recordValues(CSVRecord record, List<TableColumn> columns)
+      throws SQLException {
+    if (record.size() != columns.size()) {
+      throw invalidOperation();
+    }
+    StringBuilder builder = new StringBuilder("(");
+    for (int i = 0; i < columns.size(); i++) {
+      if (i > 0) {
+        builder.append(", ");
+      }
+      builder.append(sqlValue(columns.get(i), record.get(i)));
+    }
+    builder.append(')');
+    return builder.toString();
+  }
+
+  private static String sqlValue(TableColumn column, String value) throws SQLException {
+    if (column.isTime()) {
+      if (isNull(value)) {
+        throw invalidOperation();
+      }
+      return timestampValue(value);
+    }
+    if (NULL_MARKER.equals(value)) {
+      return "NULL";
+    }
+    if (column.isTextual() || column.isDateLike()) {
+      return quote(value);
+    }
+    if (value.isEmpty()) {
+      throw invalidOperation();
+    }
+    return value;
+  }
+
+  private static boolean isNull(String value) {
+    return value == null || value.isEmpty() || NULL_MARKER.equals(value);
+  }
+
+  private static String timestampValue(String value) {
+    if (value.matches("[-+]?\\d+")) {
+      return value;
+    }
+    return quote(value);
+  }
+
+  private static String quote(String value) {
+    return "'" + value.replace("'", "''") + "'";
+  }
+
+  private static List<String> buildStatements(
+      String tablePath, List<TableColumn> columns, List<String> rows) {
+    List<String> statements = new ArrayList<>();
+    for (int start = 0; start < rows.size(); start += INSERT_BATCH_SIZE) {
+      int end = Math.min(start + INSERT_BATCH_SIZE, rows.size());
+      statements.add(
+          "INSERT INTO "
+              + identifierPath(tablePath)
+              + "("
+              + columnList(columns)
+              + ") VALUES "
+              + join(rows.subList(start, end), ", "));
+    }
+    return statements;
+  }
+
+  private static String identifierPath(String tablePath) {
+    String[] parts = tablePath.split("\\.", -1);
+    StringBuilder builder = new StringBuilder();
+    for (String part : parts) {
+      if (builder.length() > 0) {
+        builder.append('.');
+      }
+      builder.append(identifier(part));
+    }
+    return builder.toString();
+  }
+
+  private static String columnList(List<TableColumn> columns) {
+    List<String> names = new ArrayList<>();
+    for (TableColumn column : columns) {
+      names.add(identifier(column.name));
+    }
+    return join(names, ", ");
+  }
+
+  private static String identifier(String value) {
+    if (value.matches("[A-Za-z_][A-Za-z0-9_]*")) {
+      return value;
+    }
+    return "\"" + value.replace("\"", "\"\"") + "\"";
+  }
+
+  private static String join(List<String> values, String delimiter) {
+    StringBuilder builder = new StringBuilder();
+    for (String value : values) {
+      if (builder.length() > 0) {
+        builder.append(delimiter);
+      }
+      builder.append(value);
+    }
+    return builder.toString();
+  }
+
+  private static SQLException invalidOperation() {
+    return new SQLException(INVALID_WRITE_OPERATION);
+  }
+
+  private static class ParsedCsv {
+    private final List<TableColumn> columns;
+    private final List<String> rows;
+
+    private ParsedCsv(List<TableColumn> columns, List<String> rows) {
+      this.columns = columns;
+      this.rows = rows;
+    }
+  }
+
+  private static class TableColumn {
+    private final String name;
+    private final String type;
+
+    private TableColumn(String name, String type) {
+      this.name = name;
+      this.type = type.toUpperCase(Locale.ROOT);
+    }
+
+    private boolean isTime() {
+      return "TIME".equalsIgnoreCase(name);
+    }
+
+    private boolean isTextual() {
+      return type.contains("STRING") || type.contains("TEXT") || type.contains("BLOB");
+    }
+
+    private boolean isDateLike() {
+      return type.contains("DATE") || type.contains("TIMESTAMP");
+    }
+  }
+}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
index d8220fee7f7..8a0e79ca58a 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
@@ -64,6 +64,23 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
     executor.execute("ALTER TABLE " + toTablePath(source) + " RENAME TO " + tableName(target));
   }
 
+  @Override
+  public void append(FsPath path, List<String> lines) throws SQLException {
+    if (!isDataFile(path)) {
+      throw invalidOperation();
+    }
+    if (lines == null || lines.isEmpty()) {
+      return;
+    }
+    String tablePath = toTablePath(path);
+    List<String> statements =
+        TableCsvAppendPlanner.plan(
+            tablePath, executor.query("DESC " + tablePath + " DETAILS"), lines);
+    for (String statement : statements) {
+      executor.execute(statement);
+    }
+  }
+
   private static SQLException invalidOperation() {
     return new SQLException(INVALID_WRITE_OPERATION);
   }
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
index 9f4a08ceb01..2d061c32e4e 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.cli.fs.provider;
 import org.apache.iotdb.cli.fs.path.FsPath;
 
 import java.sql.SQLException;
+import java.util.List;
 
 public class UnsupportedFilesystemMutationProvider implements FilesystemMutationProvider {
 
@@ -42,6 +43,11 @@ public class UnsupportedFilesystemMutationProvider implements FilesystemMutation
     throw unsupported();
   }
 
+  @Override
+  public void append(FsPath path, List<String> lines) throws SQLException {
+    throw unsupported();
+  }
+
   private static SQLException unsupported() {
     return new SQLException(UNSUPPORTED);
   }
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 aa68d220e77..704e10f6600 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,7 +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", "paste", "tree", "help", "exit", "quit");
+          "more", "file", "du", "mkdir", "rm", "mv", "cut", "paste", "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 3911b65f382..5566022a31c 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
@@ -30,6 +30,7 @@ import org.apache.iotdb.cli.utils.CliContext;
 
 import org.jline.reader.Candidate;
 import org.jline.reader.Completer;
+import org.jline.reader.LineReader;
 import org.jline.reader.ParsedLine;
 import org.jline.reader.impl.DefaultParser;
 import org.junit.Before;
@@ -58,6 +59,7 @@ public class FilesystemShellTest {
 
   @Mock private FilesystemSchemaProvider provider;
   @Mock private FilesystemMutationProvider mutationProvider;
+  @Mock private LineReader lineReader;
 
   private ByteArrayOutputStream out;
   private FilesystemShell shell;
@@ -202,6 +204,58 @@ public class FilesystemShellTest {
         .move(FsPath.absolute("/db1/table1.csv"), FsPath.absolute("/db1/table2.csv"));
   }
 
+  @Test
+  public void executeTeeRejectsReadOnlyMode() throws SQLException {
+    assertTrue(shell.execute("tee -a /db1/table1.csv"));
+
+    assertTrue(out.toString().contains("tee: /db1/table1.csv: Read-only file system"));
+    verifyZeroInteractions(mutationProvider);
+  }
+
+  @Test
+  public void executeTeeReadsInteractiveBufferUntilWriteQuit() throws SQLException {
+    CliContext ctx = shellContext();
+    ctx.setLineReader(lineReader);
+    shell = new FilesystemShell(ctx, provider, mutationProvider, true);
+    when(lineReader.readLine("tee> ", null)).thenReturn("time,key,value", "1,spricoder,2.0", ":wq");
+
+    assertTrue(shell.execute("tee -a /db1/table1.csv"));
+
+    verify(mutationProvider)
+        .append(
+            FsPath.absolute("/db1/table1.csv"), Arrays.asList("time,key,value", "1,spricoder,2.0"));
+  }
+
+  @Test
+  public void executeTeeQuitWarnsBeforeDiscardingInteractiveBuffer() throws SQLException {
+    CliContext ctx = shellContext();
+    ctx.setLineReader(lineReader);
+    shell = new FilesystemShell(ctx, provider, mutationProvider, true);
+    when(lineReader.readLine("tee> ", null)).thenReturn("time,key,value", ":q", ":q!");
+
+    assertTrue(shell.execute("tee -a /db1/table1.csv"));
+
+    assertTrue(out.toString().contains("tee: use :wq to write or :q! to quit without writing"));
+    verifyZeroInteractions(mutationProvider);
+  }
+
+  @Test
+  public void executeTeeNonInteractiveReadsStandardInputUntilEof() throws SQLException {
+    CliContext ctx =
+        new CliContext(
+            new ByteArrayInputStream("time,key,value\n1,spricoder,2.0\n".getBytes()),
+            new PrintStream(out),
+            System.err,
+            ExitType.EXCEPTION);
+    shell = new FilesystemShell(ctx, provider, mutationProvider, true);
+
+    assertTrue(shell.executeNonInteractive("tee -a /db1/table1.csv"));
+
+    verify(mutationProvider)
+        .append(
+            FsPath.absolute("/db1/table1.csv"), Arrays.asList("time,key,value", "1,spricoder,2.0"));
+  }
+
   @Test
   public void executeTreePrintsChildrenUntilDepth() throws SQLException {
     when(provider.list(FsPath.absolute("/")))
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 8039335d797..eb97455c369 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
@@ -194,6 +194,26 @@ public class FilesystemCommandParserTest {
         FilesystemCommand.Type.INVALID, FilesystemCommandParser.parse("cut -f2,3").getType());
   }
 
+  @Test
+  public void parseTeeAppendPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("tee -a /db1/table1.csv");
+
+    assertEquals(FilesystemCommand.Type.TEE, command.getType());
+    assertEquals("-a", command.getOption());
+    assertEquals("/db1/table1.csv", command.getPath());
+  }
+
+  @Test
+  public void parseTeeRequiresAppendOptionAndPath() {
+    assertEquals(
+        FilesystemCommand.Type.INVALID,
+        FilesystemCommandParser.parse("tee /db1/table1.csv").getType());
+    assertEquals(FilesystemCommand.Type.INVALID, FilesystemCommandParser.parse("tee -a").getType());
+    assertEquals(
+        FilesystemCommand.Type.INVALID,
+        FilesystemCommandParser.parse("tee -p /db1/table1.csv").getType());
+  }
+
   @Test
   public void parseWriteCommands() {
     FilesystemCommand mkdir = FilesystemCommandParser.parse("mkdir /db1");
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
index 2403bffffb3..2f23c1d4f1d 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
@@ -28,10 +28,12 @@ import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.sql.SQLException;
+import java.util.Arrays;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 public class TableFilesystemMutationProviderTest {
 
@@ -98,6 +100,61 @@ public class TableFilesystemMutationProviderTest {
             provider.move(FsPath.absolute("/db1/table1.csv"), FsPath.absolute("/db2/table1.csv")));
   }
 
+  @Test
+  public void appendCsvWithHeaderBuildsMultiRowInsert() throws SQLException {
+    mockTableSchema();
+
+    provider.append(
+        FsPath.absolute("/db1/table1.csv"),
+        Arrays.asList("time,key,value", "1,spricoder,2.0", "2,other,\\N"));
+
+    verify(executor).query("DESC db1.table1 DETAILS");
+    verify(executor)
+        .execute(
+            "INSERT INTO db1.table1(time, key, value) VALUES "
+                + "(1, 'spricoder', 2.0), (2, 'other', NULL)");
+  }
+
+  @Test
+  public void appendCsvWithoutHeaderUsesFullSchemaOrder() throws SQLException {
+    mockTableSchema();
+
+    provider.append(FsPath.absolute("/db1/table1.csv"), Arrays.asList("1,spricoder,2.0"));
+
+    verify(executor)
+        .execute("INSERT INTO db1.table1(time, key, value) VALUES (1, 'spricoder', 2.0)");
+  }
+
+  @Test
+  public void appendCsvWithHeaderAllowsPartialColumns() throws SQLException {
+    mockTableSchema();
+
+    provider.append(FsPath.absolute("/db1/table1.csv"), Arrays.asList("time,value", "1,2.0"));
+
+    verify(executor).execute("INSERT INTO db1.table1(time, value) VALUES (1, 2.0)");
+  }
+
+  @Test
+  public void appendRejectsSidecarAndMissingTime() throws SQLException {
+    assertInvalidOperation(
+        () -> provider.append(FsPath.absolute("/db1/table1.schema"), Arrays.asList("time", "1")));
+
+    mockTableSchema();
+    assertInvalidOperation(
+        () -> provider.append(FsPath.absolute("/db1/table1.csv"), Arrays.asList("key", "a")));
+  }
+
+  private void mockTableSchema() throws SQLException {
+    when(executor.query("DESC db1.table1 DETAILS"))
+        .thenReturn(
+            org.apache.iotdb.cli.fs.sql.SqlRow.list(
+                org.apache.iotdb.cli.fs.sql.SqlRow.of(
+                    "ColumnName", "time", "DataType", "TIMESTAMP"),
+                org.apache.iotdb.cli.fs.sql.SqlRow.of("ColumnName", "key", "DataType", "STRING"),
+                org.apache.iotdb.cli.fs.sql.SqlRow.of(
+                    "ColumnName", "value", "DataType", "DOUBLE")));
+  }
+
   private static void assertInvalidOperation(SqlOperation operation) throws SQLException {
     try {
       operation.run();


(iotdb) 06/06: add unix and extend

Posted by sp...@apache.org.
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 33a3a30d7637d94015032a6d1b71a100819a77df
Author: spricoder <sp...@qq.com>
AuthorDate: Sat May 2 20:27:37 2026 +0800

    add unix and extend
---
 .../2026-05-02-cli-fs-unix-standard-extensions.md  | 55 ++++++++++++++++++++++
 .../org/apache/iotdb/cli/fs/FilesystemShell.java   | 37 +++++++++++++--
 .../iotdb/cli/fs/command/FilesystemCommand.java    |  2 +
 .../cli/fs/command/FilesystemCommandParser.java    | 25 ++++++++++
 .../fs/provider/FilesystemMutationProvider.java    |  6 +++
 .../provider/TableFilesystemMutationProvider.java  | 43 +++++++++++++++++
 .../UnsupportedFilesystemMutationProvider.java     | 15 ++++++
 .../apache/iotdb/cli/fs/FilesystemShellTest.java   | 44 +++++++++++++++++
 .../fs/command/FilesystemCommandParserTest.java    | 36 ++++++++++++++
 .../TableFilesystemMutationProviderTest.java       | 38 +++++++++++++++
 10 files changed, 297 insertions(+), 4 deletions(-)

diff --git a/docs/superpowers/plans/2026-05-02-cli-fs-unix-standard-extensions.md b/docs/superpowers/plans/2026-05-02-cli-fs-unix-standard-extensions.md
new file mode 100644
index 00000000000..635667782fa
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-02-cli-fs-unix-standard-extensions.md
@@ -0,0 +1,55 @@
+# CLI Filesystem Unix Standard Extensions Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Extend CLI filesystem mode with a first slice of standard Unix-style database operations.
+
+**Architecture:** Keep command parsing in `FilesystemCommandParser`, command dispatch in `FilesystemShell`, and database mutations behind `FilesystemMutationProvider`. Table-mode mutations map to SQL through `TableFilesystemMutationProvider`; tree-mode writes remain unsupported through `UnsupportedFilesystemMutationProvider`.
+
+**Tech Stack:** Java, JUnit 4, Mockito, Maven module `iotdb-client/cli`.
+
+---
+
+### Task 1: Add Standard Command Parsing
+
+**Files:**
+- Modify: `iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParserTest.java`
+- Modify: `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommand.java`
+- Modify: `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParser.java`
+
+- [ ] Write failing tests for `rmdir <path>`, `rm -r <path>`, `cp <source> <target>`, and `ls -R [path]`.
+- [ ] Run `mvn -pl iotdb-client/cli -Dtest=FilesystemCommandParserTest test` and verify the new tests fail because commands are not implemented.
+- [ ] Implement the minimal parser changes.
+- [ ] Re-run the parser test and verify it passes.
+
+### Task 2: Add Shell Dispatch
+
+**Files:**
+- Modify: `iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/FilesystemShellTest.java`
+- Modify: `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java`
+- Modify: `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java`
+- Modify: `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java`
+
+- [ ] Write failing shell tests proving `rmdir`, `rm -r`, and `cp` call the mutation provider only when writes are enabled, and `ls -R` recursively lists children.
+- [ ] Run `mvn -pl iotdb-client/cli -Dtest=FilesystemShellTest test` and verify the tests fail for missing behavior.
+- [ ] Implement minimal shell dispatch and provider interface methods.
+- [ ] Re-run the shell test and verify it passes.
+
+### Task 3: Add Table Mutation SQL
+
+**Files:**
+- Modify: `iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java`
+- Modify: `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java`
+
+- [ ] Write failing provider tests for dropping a database through `rmdir`/recursive remove and copying `/db/t1.schema` to `/db/t2.schema`.
+- [ ] Run `mvn -pl iotdb-client/cli -Dtest=TableFilesystemMutationProviderTest test` and verify failures.
+- [ ] Implement minimal table mutation SQL: `DROP DATABASE <db>` and `CREATE TABLE <target> LIKE <source>`.
+- [ ] Re-run provider tests and verify they pass.
+
+### Task 4: Regression Verification
+
+**Files:**
+- Existing fs-mode tests.
+
+- [ ] Run `mvn -pl iotdb-client/cli -Dtest=FilesystemCommandParserTest,FilesystemShellTest,TableFilesystemMutationProviderTest,CliFilesystemModeTest test`.
+- [ ] Fix only failures caused by this change.
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 407744fc03c..508a0847050 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
@@ -54,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", "join", "tree", "help", "exit",
-          "quit", "tee");
+          "more", "file", "du", "mkdir", "rmdir", "rm", "mv", "cp", "cut", "paste", "join", "tree",
+          "help", "exit", "quit", "tee");
 
   private final CliContext ctx;
   private final FilesystemSchemaProvider provider;
@@ -127,12 +127,18 @@ public class FilesystemShell {
       case MKDIR:
         mkdir(command.getPath());
         return true;
+      case RMDIR:
+        rmdir(command.getPath());
+        return true;
       case RM:
-        remove(command.getPath());
+        remove(command.getPath(), command.getOption());
         return true;
       case MV:
         move(command.getPaths());
         return true;
+      case CP:
+        copy(command.getPaths());
+        return true;
       case CUT:
         printCut(command.getPath(), command.getOption(), command.getPattern());
         return true;
@@ -432,11 +438,23 @@ public class FilesystemShell {
     mutationProvider.mkdir(resolvedPath);
   }
 
-  private void remove(String path) throws SQLException {
+  private void rmdir(String path) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    if (!ensureWritable("rmdir", resolvedPath)) {
+      return;
+    }
+    mutationProvider.rmdir(resolvedPath);
+  }
+
+  private void remove(String path, String option) throws SQLException {
     FsPath resolvedPath = resolve(path);
     if (!ensureWritable("rm", resolvedPath)) {
       return;
     }
+    if ("-r".equals(option)) {
+      mutationProvider.removeRecursive(resolvedPath);
+      return;
+    }
     mutationProvider.remove(resolvedPath);
   }
 
@@ -449,6 +467,15 @@ public class FilesystemShell {
     mutationProvider.move(source, target);
   }
 
+  private void copy(List<String> paths) throws SQLException {
+    FsPath source = resolve(paths.get(0));
+    FsPath target = resolve(paths.get(1));
+    if (!ensureWritable("cp", source)) {
+      return;
+    }
+    mutationProvider.copy(source, target);
+  }
+
   private void append(String path, boolean nonInteractive) throws SQLException {
     FsPath resolvedPath = resolve(path);
     if (!ensureWritable("tee", resolvedPath)) {
@@ -549,8 +576,10 @@ public class FilesystemShell {
     ctx.getPrinter().println("file <path>");
     ctx.getPrinter().println("du <path>");
     ctx.getPrinter().println("mkdir <path>");
+    ctx.getPrinter().println("rmdir <path>");
     ctx.getPrinter().println("rm <path>");
     ctx.getPrinter().println("mv <source> <target>");
+    ctx.getPrinter().println("cp <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>");
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 7d379236a78..14a6f19f814 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
@@ -41,8 +41,10 @@ public class FilesystemCommand {
     FILE,
     DU,
     MKDIR,
+    RMDIR,
     RM,
     MV,
+    CP,
     CUT,
     PASTE,
     JOIN,
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 565b9bad58f..8b1aea2c1f1 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
@@ -100,12 +100,18 @@ public class FilesystemCommandParser {
     if ("mkdir".equals(command)) {
       return FilesystemCommand.path(FilesystemCommand.Type.MKDIR, pathArgument(tokens));
     }
+    if ("rmdir".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.RMDIR, pathArgument(tokens));
+    }
     if ("rm".equals(command)) {
       return parseRm(tokens);
     }
     if ("mv".equals(command)) {
       return parseMv(tokens);
     }
+    if ("cp".equals(command)) {
+      return parseCp(tokens);
+    }
     if ("cut".equals(command)) {
       return parseCut(tokens);
     }
@@ -255,6 +261,9 @@ public class FilesystemCommandParser {
       return FilesystemCommand.invalid("Missing rm path");
     }
     if (tokens[1].startsWith("-")) {
+      if ("-r".equals(tokens[1]) && tokens.length >= 3) {
+        return FilesystemCommand.option(FilesystemCommand.Type.RM, "-r", tokens[2]);
+      }
       return FilesystemCommand.invalid("Unsupported rm option: " + tokens[1]);
     }
     return FilesystemCommand.path(FilesystemCommand.Type.RM, tokens[1]);
@@ -270,10 +279,21 @@ public class FilesystemCommandParser {
     return FilesystemCommand.paths(FilesystemCommand.Type.MV, paths);
   }
 
+  private static FilesystemCommand parseCp(String[] tokens) {
+    if (tokens.length < 3) {
+      return FilesystemCommand.invalid("Usage: cp <source> <target>");
+    }
+    List<String> paths = new ArrayList<>();
+    paths.add(tokens[1]);
+    paths.add(tokens[2]);
+    return FilesystemCommand.paths(FilesystemCommand.Type.CP, paths);
+  }
+
   private static FilesystemCommand parseList(String[] tokens, boolean longMode) {
     FilesystemCommand.Type type = longMode ? FilesystemCommand.Type.LL : FilesystemCommand.Type.LS;
     String path = DEFAULT_PATH;
     boolean all = false;
+    boolean recursive = false;
 
     for (int i = 1; i < tokens.length; i++) {
       String token = tokens[i];
@@ -284,6 +304,8 @@ public class FilesystemCommandParser {
             type = FilesystemCommand.Type.LL;
           } else if (option == 'a') {
             all = true;
+          } else if (option == 'R') {
+            recursive = true;
           } else {
             return FilesystemCommand.invalid("Unsupported ls option: -" + option);
           }
@@ -292,6 +314,9 @@ public class FilesystemCommandParser {
         path = token;
       }
     }
+    if (recursive) {
+      return FilesystemCommand.tree(path, DEFAULT_TREE_DEPTH);
+    }
     return FilesystemCommand.option(type, all ? "-a" : "", path);
   }
 
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
index 3d1d3afbd88..81c7df6beeb 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
@@ -28,9 +28,15 @@ public interface FilesystemMutationProvider {
 
   void mkdir(FsPath path) throws SQLException;
 
+  void rmdir(FsPath path) throws SQLException;
+
   void remove(FsPath path) throws SQLException;
 
+  void removeRecursive(FsPath path) throws SQLException;
+
   void move(FsPath source, FsPath target) throws SQLException;
 
+  void copy(FsPath source, FsPath target) throws SQLException;
+
   void append(FsPath path, List<String> lines) throws SQLException;
 }
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
index 710ff02a609..e4642be8999 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
@@ -30,6 +30,7 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
   private static final String INVALID_WRITE_OPERATION =
       "Invalid filesystem write operation for this path";
   private static final String CSV_SUFFIX = ".csv";
+  private static final String SCHEMA_SUFFIX = ".schema";
 
   private final SqlExecutor executor;
 
@@ -45,6 +46,11 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
     executor.execute("CREATE DATABASE " + TableFilesystemSql.identifier(path.getFileName()));
   }
 
+  @Override
+  public void rmdir(FsPath path) throws SQLException {
+    dropDatabase(path);
+  }
+
   @Override
   public void remove(FsPath path) throws SQLException {
     if (!isDataFile(path)) {
@@ -53,6 +59,11 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
     executor.execute("DROP TABLE " + toTablePath(path));
   }
 
+  @Override
+  public void removeRecursive(FsPath path) throws SQLException {
+    dropDatabase(path);
+  }
+
   @Override
   public void move(FsPath source, FsPath target) throws SQLException {
     if (!isDataFile(source) || !isDataFile(target)) {
@@ -68,6 +79,18 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
             + TableFilesystemSql.identifier(tableName(target)));
   }
 
+  @Override
+  public void copy(FsPath source, FsPath target) throws SQLException {
+    if (!isSchemaFile(source) || !isSchemaFile(target)) {
+      throw invalidOperation();
+    }
+    executor.execute(
+        "CREATE TABLE "
+            + toTablePath(target, SCHEMA_SUFFIX)
+            + " LIKE "
+            + toTablePath(source, SCHEMA_SUFFIX));
+  }
+
   @Override
   public void append(FsPath path, List<String> lines) throws SQLException {
     if (!isDataFile(path)) {
@@ -91,10 +114,21 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
     return new SQLException(INVALID_WRITE_OPERATION);
   }
 
+  private void dropDatabase(FsPath path) throws SQLException {
+    if (path.getSegments().size() != 1) {
+      throw invalidOperation();
+    }
+    executor.execute("DROP DATABASE " + TableFilesystemSql.identifier(path.getFileName()));
+  }
+
   private static String toTablePath(FsPath path) {
     return TableFilesystemSql.tablePath(databaseName(path), tableName(path));
   }
 
+  private static String toTablePath(FsPath path, String suffix) {
+    return TableFilesystemSql.tablePath(databaseName(path), tableName(path, suffix));
+  }
+
   private static String databaseName(FsPath path) {
     return path.getSegments().get(0);
   }
@@ -103,11 +137,20 @@ public class TableFilesystemMutationProvider implements FilesystemMutationProvid
     return path.getSegments().size() == 2 && path.getFileName().endsWith(CSV_SUFFIX);
   }
 
+  private static boolean isSchemaFile(FsPath path) {
+    return path.getSegments().size() == 2 && path.getFileName().endsWith(SCHEMA_SUFFIX);
+  }
+
   private static String tableName(FsPath path) {
     String fileName = path.getFileName();
     return fileName.substring(0, fileName.length() - CSV_SUFFIX.length());
   }
 
+  private static String tableName(FsPath path, String suffix) {
+    String fileName = path.getFileName();
+    return fileName.substring(0, fileName.length() - suffix.length());
+  }
+
   private static FsPath parent(FsPath path) {
     List<String> segments = path.getSegments();
     StringBuilder builder = new StringBuilder("/");
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
index 2d061c32e4e..9b3cf6b3353 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
@@ -33,16 +33,31 @@ public class UnsupportedFilesystemMutationProvider implements FilesystemMutation
     throw unsupported();
   }
 
+  @Override
+  public void rmdir(FsPath path) throws SQLException {
+    throw unsupported();
+  }
+
   @Override
   public void remove(FsPath path) throws SQLException {
     throw unsupported();
   }
 
+  @Override
+  public void removeRecursive(FsPath path) throws SQLException {
+    throw unsupported();
+  }
+
   @Override
   public void move(FsPath source, FsPath target) throws SQLException {
     throw unsupported();
   }
 
+  @Override
+  public void copy(FsPath source, FsPath target) throws SQLException {
+    throw unsupported();
+  }
+
   @Override
   public void append(FsPath path, List<String> lines) throws SQLException {
     throw unsupported();
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 6cb435ede80..78e9e88516c 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
@@ -247,6 +247,28 @@ public class FilesystemShellTest {
         .move(FsPath.absolute("/db1/table1.csv"), FsPath.absolute("/db1/table2.csv"));
   }
 
+  @Test
+  public void executeStandardWriteCommandsWhenEnabled() throws SQLException {
+    shell = new FilesystemShell(shellContext(), provider, mutationProvider, true);
+
+    assertTrue(shell.execute("rmdir /db1"));
+    assertTrue(shell.execute("rm -r /db2"));
+    assertTrue(shell.execute("cp /db1/table1.schema /db1/table2.schema"));
+
+    verify(mutationProvider).rmdir(FsPath.absolute("/db1"));
+    verify(mutationProvider).removeRecursive(FsPath.absolute("/db2"));
+    verify(mutationProvider)
+        .copy(FsPath.absolute("/db1/table1.schema"), FsPath.absolute("/db1/table2.schema"));
+  }
+
+  @Test
+  public void executeRecursiveRemoveRejectsReadOnlyMode() throws SQLException {
+    assertTrue(shell.execute("rm -r /db1"));
+
+    assertTrue(out.toString().contains("rm: /db1: Read-only file system"));
+    verifyZeroInteractions(mutationProvider);
+  }
+
   @Test
   public void executeTeeRejectsReadOnlyMode() throws SQLException {
     assertTrue(shell.execute("tee -a /db1/table1.csv"));
@@ -321,6 +343,28 @@ public class FilesystemShellTest {
     verify(provider).list(FsPath.absolute("/root"));
   }
 
+  @Test
+  public void executeLsRecursivePrintsChildren() throws SQLException {
+    when(provider.describe(FsPath.absolute("/")))
+        .thenReturn(new FsNode("/", FsPath.absolute("/"), FsNodeType.VIRTUAL_ROOT));
+    when(provider.list(FsPath.absolute("/")))
+        .thenReturn(
+            Arrays.asList(new FsNode("db1", FsPath.absolute("/db1"), FsNodeType.TABLE_DATABASE)));
+    when(provider.list(FsPath.absolute("/db1")))
+        .thenReturn(
+            Arrays.asList(
+                new FsNode(
+                    "table1.csv", FsPath.absolute("/db1/table1.csv"), FsNodeType.TABLE_DATA_FILE)));
+
+    assertTrue(shell.execute("ls -R /"));
+
+    assertTrue(out.toString().contains("db1"));
+    assertTrue(out.toString().contains("table1.csv"));
+    verify(provider).describe(FsPath.absolute("/"));
+    verify(provider).list(FsPath.absolute("/"));
+    verify(provider).list(FsPath.absolute("/db1"));
+  }
+
   @Test
   public void executeTreeUnknownPathPrintsNoSuchFile() throws SQLException {
     when(provider.describe(FsPath.absolute("/db1/table1")))
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 df0bf2ffdc0..edf9f4bb6be 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
@@ -292,6 +292,42 @@ public class FilesystemCommandParserTest {
     assertEquals("/db1/table2.csv", mv.getPaths().get(1));
   }
 
+  @Test
+  public void parseRmdirCommand() {
+    FilesystemCommand command = FilesystemCommandParser.parse("rmdir /db1");
+
+    assertEquals(FilesystemCommand.Type.RMDIR, command.getType());
+    assertEquals("/db1", command.getPath());
+  }
+
+  @Test
+  public void parseRmRecursiveCommand() {
+    FilesystemCommand command = FilesystemCommandParser.parse("rm -r /db1");
+
+    assertEquals(FilesystemCommand.Type.RM, command.getType());
+    assertEquals("-r", command.getOption());
+    assertEquals("/db1", command.getPath());
+  }
+
+  @Test
+  public void parseCpCommand() {
+    FilesystemCommand command =
+        FilesystemCommandParser.parse("cp /db1/table1.schema /db1/table2.schema");
+
+    assertEquals(FilesystemCommand.Type.CP, command.getType());
+    assertEquals(2, command.getPaths().size());
+    assertEquals("/db1/table1.schema", command.getPaths().get(0));
+    assertEquals("/db1/table2.schema", command.getPaths().get(1));
+  }
+
+  @Test
+  public void parseLsRecursiveAsTreeCommand() {
+    FilesystemCommand command = FilesystemCommandParser.parse("ls -R /db1");
+
+    assertEquals(FilesystemCommand.Type.TREE, command.getType());
+    assertEquals("/db1", command.getPath());
+  }
+
   @Test
   public void parseTreeDepthBeforePath() {
     FilesystemCommand command = FilesystemCommandParser.parse("tree -L 2 /root/sg");
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
index 17efc72a76a..0a944b223a1 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
@@ -77,6 +77,28 @@ public class TableFilesystemMutationProviderTest {
     assertInvalidOperation(() -> provider.remove(FsPath.absolute("/db1/table1/s1")));
   }
 
+  @Test
+  public void rmdirDatabaseDropsDatabase() throws SQLException {
+    provider.rmdir(FsPath.absolute("/db1"));
+
+    verify(executor).execute("DROP DATABASE db1");
+  }
+
+  @Test
+  public void removeRecursiveDatabaseDropsDatabase() throws SQLException {
+    provider.removeRecursive(FsPath.absolute("/db1"));
+
+    verify(executor).execute("DROP DATABASE db1");
+  }
+
+  @Test
+  public void rmdirAndRemoveRecursiveRejectUnsafeLevels() throws SQLException {
+    assertInvalidOperation(() -> provider.rmdir(FsPath.absolute("/")));
+    assertInvalidOperation(() -> provider.rmdir(FsPath.absolute("/db1/table1.csv")));
+    assertInvalidOperation(() -> provider.removeRecursive(FsPath.absolute("/")));
+    assertInvalidOperation(() -> provider.removeRecursive(FsPath.absolute("/db1/table1.csv")));
+  }
+
   @Test
   public void moveTableCsvRenamesTableInSameDatabase() throws SQLException {
     provider.move(FsPath.absolute("/db1/table1.csv"), FsPath.absolute("/db1/table2.csv"));
@@ -100,6 +122,22 @@ public class TableFilesystemMutationProviderTest {
             provider.move(FsPath.absolute("/db1/table1.csv"), FsPath.absolute("/db2/table1.csv")));
   }
 
+  @Test
+  public void copySchemaCreatesTableLikeSource() throws SQLException {
+    provider.copy(FsPath.absolute("/db1/table1.schema"), FsPath.absolute("/db1/table2.schema"));
+
+    verify(executor).execute("CREATE TABLE db1.table2 LIKE db1.table1");
+  }
+
+  @Test
+  public void copyRejectsNonSchemaPaths() throws SQLException {
+    assertInvalidOperation(
+        () ->
+            provider.copy(FsPath.absolute("/db1/table1.csv"), FsPath.absolute("/db1/table2.csv")));
+    assertInvalidOperation(
+        () -> provider.copy(FsPath.absolute("/db1/table1.schema"), FsPath.absolute("/db1")));
+  }
+
   @Test
   public void appendCsvWithHeaderBuildsMultiRowInsert() throws SQLException {
     mockTableSchema();


(iotdb) 05/06: update join

Posted by sp...@apache.org.
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 <sp...@qq.com>
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<>();