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 a41bc7788f52ade33008d63520a2cf8421e364c3
Author: spricoder <[email protected]>
AuthorDate: Wed Apr 29 19:44:05 2026 +0800

    readonly toy
---
 .../plans/2026-04-29-cli-filesystem-mode.md        | 303 ++++++++++++++++++
 .../specs/2026-04-29-cli-filesystem-mode-design.md | 353 +++++++++++++++++++++
 .../java/org/apache/iotdb/cli/AbstractCli.java     |  35 ++
 .../src/main/java/org/apache/iotdb/cli/Cli.java    |  85 ++++-
 .../org/apache/iotdb/cli/fs/FilesystemShell.java   | 276 ++++++++++++++++
 .../iotdb/cli/fs/command/FilesystemCommand.java    | 112 +++++++
 .../cli/fs/command/FilesystemCommandParser.java    | 125 ++++++++
 .../java/org/apache/iotdb/cli/fs/node/FsNode.java  |  61 ++++
 .../org/apache/iotdb/cli/fs/node/FsNodeType.java   |  34 ++
 .../java/org/apache/iotdb/cli/fs/path/FsPath.java  | 127 ++++++++
 .../cli/fs/provider/FilesystemSchemaProvider.java  |  43 +++
 .../fs/provider/TableFilesystemSchemaProvider.java | 196 ++++++++++++
 .../fs/provider/TreeFilesystemSchemaProvider.java  | 169 ++++++++++
 .../apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java   |  57 ++++
 .../org/apache/iotdb/cli/fs/sql/SqlExecutor.java   |  28 ++
 .../java/org/apache/iotdb/cli/fs/sql/SqlRow.java   |  59 ++++
 .../org/apache/iotdb/cli/utils/JlineUtils.java     |  24 +-
 .../java/org/apache/iotdb/cli/AbstractCliTest.java |  39 +++
 .../apache/iotdb/cli/CliFilesystemModeTest.java    | 107 +++++++
 .../apache/iotdb/cli/fs/FilesystemShellTest.java   | 194 +++++++++++
 .../fs/command/FilesystemCommandParserTest.java    | 115 +++++++
 .../org/apache/iotdb/cli/fs/path/FsPathTest.java   |  71 +++++
 .../TableFilesystemSchemaProviderTest.java         | 189 +++++++++++
 .../provider/TreeFilesystemSchemaProviderTest.java | 164 ++++++++++
 .../iotdb/cli/fs/sql/JdbcSqlExecutorTest.java      |  71 +++++
 .../org/apache/iotdb/cli/utils/JlineUtilsTest.java |  60 ++++
 26 files changed, 3094 insertions(+), 3 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
new file mode 100644
index 00000000000..fff6ef69730
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-29-cli-filesystem-mode.md
@@ -0,0 +1,303 @@
+<!--
+
+    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.
+
+-->
+
+# CLI Filesystem Mode Implementation Plan
+
+> **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.
+
+**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.
+
+**Tech Stack:** Java 8, JUnit 4, Mockito, commons-cli, JLine, JDBC, existing 
IoTDB CLI test patterns.
+
+---
+
+## File Structure
+
+- Modify 
`iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java`: add 
access-mode option constants and parsing helpers.
+- Modify `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java`: route 
to SQL or filesystem mode.
+- Create 
`iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/path/FsPath.java`: 
normalize filesystem paths.
+- Create 
`iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommand.java`:
 command value object.
+- Create 
`iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParser.java`:
 parse filesystem commands.
+- Create 
`iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java`: 
node type enum.
+- 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/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/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`.
+- Test 
`iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProviderTest.java`.
+- Test 
`iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java`.
+- Modify 
`iotdb-client/cli/src/test/java/org/apache/iotdb/cli/AbstractCliTest.java`: 
access-mode defaults and validation.
+
+## Current Implementation Notes
+
+These notes capture follow-up implementation experience for quickly resuming 
this branch.
+
+- 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.
+  - `tree` prints indented names only.
+  - `cat` and `paste` print tab-separated values.
+  - `stat` is the place to show metadata.
+- Do not add filesystem command dialects such as `cat --columns` or `select`. 
Multi-column reads 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`
+- 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.
+
+## Subagent Usage Notes
+
+All later work on this feature may use subagents to accelerate execution. 
Prefer subagents when the
+work can be split into independent, bounded tasks; keep tightly coupled or 
immediately blocking
+work in the main session.
+
+Good subagent tasks for this branch:
+
+- Read-only exploration of one subsystem, for example parser behavior in
+  `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command`.
+- Read-only review of Unix command semantics before adding or changing a 
filesystem command.
+- Implementing one isolated slice with a disjoint write set, for example only 
provider tests and
+  provider code for a table-mode read behavior.
+- Running or reviewing one focused test group while the main session works on 
a different slice.
+- 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.
+- 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
+  must not exit the shell.
+- Reviewing Maven output to distinguish real test failures from sandbox or 
Develocity noise.
+
+Avoid subagents for:
+
+- Changes that require editing the same files in parallel.
+- Immediate blocking work where the next local step depends on the result.
+- Broad refactors across CLI, provider, shell, and docs at the same time.
+- Final integration decisions touching startup compatibility in `Cli.java` or 
`AbstractCli.java`.
+- Cross-cutting behavior that simultaneously changes parser, shell, provider, 
and tests.
+- Root-cause analysis where the fix direction is still unclear and requires 
one coherent debugging
+  thread.
+- Merging multiple subagent results, resolving file conflicts, or deciding 
whether to expand the
+  test scope.
+- Any git operation. Git status, diff, add, commit, branch, push, or reset 
still require explicit
+  user confirmation before running.
+
+Subagent task categories:
+
+- Read-only subagents may inspect code, docs, tests, and command output. Use 
them for current-state
+  analysis, risk review, Unix semantic checks, and test coverage gap analysis.
+- Implementation subagents may edit only the files explicitly assigned to 
them. Give each worker a
+  single package or feature slice, such as table-provider reads plus matching 
provider tests.
+- Verification subagents run specified commands, summarize results, and 
identify likely causes of
+  failures. They must not change files unless explicitly reassigned.
+
+The main session owns final decisions, conflict resolution, and the final 
verification story.
+
+When dispatching a subagent, include this checklist in the prompt:
+
+- State whether the task is read-only or may edit files.
+- Name the exact files or package the subagent owns.
+- Explicitly say: `Do not run git commands. Do not inspect git status or git 
diff unless the user
+  explicitly approves it.`
+- Tell implementation subagents that they are not alone in the codebase, must 
not revert unrelated
+  edits, and must accommodate changes made by others.
+- Require a final summary with changed files, tests run, and any residual risk.
+- For implementation work, require TDD: write or update the failing test 
first, verify red, then
+  implement.
+- For verification work, require read-only behavior: report the command, 
result, failure summary,
+  and suspected root cause without modifying files.
+
+The main session must review subagent output before finalizing. For code 
changes, run
+`mvn spotless:apply -o -nsu` before compile/test, then run the focused and 
broader filesystem-mode
+test commands listed below.
+
+## Fast Test And Build Notes
+
+- After code edits and before compile/test, run Spotless first:
+
+  ```bash
+  mvn spotless:apply -o -nsu
+  ```
+
+- Prefer running Maven from `iotdb-client/cli` for focused CLI work:
+
+  ```bash
+  cd iotdb-client/cli
+  ```
+
+- Prefer offline/no-snapshot-update mode when dependencies are already local:
+
+  ```bash
+  mvn test -o -nsu -Dtest=FilesystemShellTest,CliFilesystemModeTest
+  ```
+
+- The broader filesystem-mode focused suite is:
+
+  ```bash
+  mvn test -o -nsu 
-Dtest=AbstractCliTest,CliFilesystemModeTest,FsPathTest,FilesystemCommandParserTest,TreeFilesystemSchemaProviderTest,TableFilesystemSchemaProviderTest,FilesystemShellTest,JdbcSqlExecutorTest
+  ```
+
+- Maven/Develocity may print `Operation not permitted` stack traces for writes 
under
+  `~/.m2/.develocity` in the sandbox. Check the final Maven result. In 
observed runs,
+  `spotless:apply` still ended with `BUILD SUCCESS` despite those Develocity 
warnings.
+
+- A good TDD sequence for this area:
+  1. Add or update focused tests in parser, shell, and provider layers.
+  2. Run the focused tests and verify the expected red failure.
+  3. Implement the minimal code.
+  4. Run `mvn spotless:apply -o -nsu`.
+  5. Run the focused test command.
+  6. Run the broader filesystem-mode suite above.
+
+- Git operations require explicit user confirmation before running any `git` 
command.
+
+### Task 1: Path Model
+
+- [ ] **Step 1: Write failing `FsPathTest`**
+
+Create tests for absolute paths, relative paths, `.`, `..`, repeated slashes, 
empty input, and not moving above root.
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `mvn test -pl iotdb-client/cli -Dtest=FsPathTest`
+Expected: compilation failure because `FsPath` does not exist.
+
+- [ ] **Step 3: Implement `FsPath`**
+
+Implement only normalization, resolution, `isRoot`, `getSegments`, 
`getFileName`, and `toString`.
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `mvn test -pl iotdb-client/cli -Dtest=FsPathTest`
+Expected: all `FsPathTest` tests pass.
+
+### Task 2: Command Parser
+
+- [ ] **Step 1: Write failing `FilesystemCommandParserTest`**
+
+Cover `pwd`, `ls`, `ls /root`, `cd ..`, `stat`, `cat /x`, `tree -L 2 /root`, 
`sql SHOW DATABASES`, `help`, `exit`, invalid `tree -L bad`, and unknown 
commands.
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `mvn test -pl iotdb-client/cli -Dtest=FilesystemCommandParserTest`
+Expected: compilation failure because command classes do not exist.
+
+- [ ] **Step 3: Implement parser classes**
+
+Implement a small parser using whitespace tokenization with special handling 
for `sql`, whose body preserves the rest of the input.
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `mvn test -pl iotdb-client/cli -Dtest=FilesystemCommandParserTest`
+Expected: all command parser tests pass.
+
+### Task 3: Tree Provider SQL Mapping
+
+- [ ] **Step 1: Write failing `TreeFilesystemSchemaProviderTest`**
+
+Use a mocked `SqlExecutor` to verify `list(/)`, `list(/root)`, 
`list(/root/sg)`, `describe(/root/sg/d1/s1)`, and `read(/root/sg/d1/s1)` issue 
the expected SQL and return typed nodes.
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `mvn test -pl iotdb-client/cli -Dtest=TreeFilesystemSchemaProviderTest`
+Expected: compilation failure because provider classes do not exist.
+
+- [ ] **Step 3: Implement provider and node model**
+
+Implement read-only provider methods needed by the tests, using centralized 
path-to-tree-SQL conversion.
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `mvn test -pl iotdb-client/cli -Dtest=TreeFilesystemSchemaProviderTest`
+Expected: all tree provider tests pass.
+
+### Task 4: Table Provider SQL Mapping
+
+- [ ] **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.
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `mvn test -pl iotdb-client/cli -Dtest=TableFilesystemSchemaProviderTest`
+Expected: compilation failure or missing behavior.
+
+- [ ] **Step 3: Implement table provider**
+
+Implement database/table/column mapping with centralized identifier rendering.
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `mvn test -pl iotdb-client/cli -Dtest=TableFilesystemSchemaProviderTest`
+Expected: all table provider tests pass.
+
+### Task 5: CLI Access Mode Dispatch
+
+- [ ] **Step 1: Extend `AbstractCliTest` with failing access-mode tests**
+
+Cover default `sql`, accepted `filesystem`, and rejected invalid access mode.
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `mvn test -pl iotdb-client/cli -Dtest=AbstractCliTest`
+Expected: failure because access-mode constants/helpers do not exist.
+
+- [ ] **Step 3: Implement access-mode option and shell dispatch hook**
+
+Add the long option and route filesystem mode to `FilesystemShell` without 
changing default SQL mode.
+
+- [ ] **Step 4: Run focused tests**
+
+Run: `mvn test -pl iotdb-client/cli 
-Dtest=AbstractCliTest,FsPathTest,FilesystemCommandParserTest,TreeFilesystemSchemaProviderTest,TableFilesystemSchemaProviderTest`
+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.
+
+- [ ] **Step 2: Run formatting**
+
+Run: `mvn spotless:apply -pl 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`
+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
new file mode 100644
index 00000000000..0d40b79a167
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-29-cli-filesystem-mode-design.md
@@ -0,0 +1,353 @@
+<!--
+
+    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.
+
+-->
+
+# IoTDB CLI Filesystem Mode Design
+
+## Goal
+
+Extend the IoTDB CLI with an explicit filesystem access mode that lets users 
browse IoTDB
+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.
+
+## Non-Goals
+
+- Do not implement a FUSE or operating-system-level mount.
+- 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.
+
+## Compatibility Requirements
+
+Backward compatibility is a hard requirement.
+
+- Existing CLI invocations without a new access-mode argument must behave as 
they do today.
+- Existing SQL mode command parsing, result printing, pagination, `help`, 
`import`, `set`, `show`,
+  `exit`, and `quit` behavior must remain unchanged unless explicitly running 
in filesystem mode.
+- Existing login arguments remain valid: `-h`, `-p`, `-u`, `-pw`, `-timeout`, 
`-sql_dialect`, and
+  SSL options.
+- No new short option should be introduced, because short options such as `-p` 
already have
+  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`.
+- In filesystem mode, `-e` executes a filesystem command such as `ls /`, not a 
SQL statement. This
+  distinction must be documented in help output.
+
+## Agentic Collaboration Policy
+
+All follow-up work on this feature may use subagents to accelerate analysis, 
implementation, and
+verification. Subagents are allowed by default, as long as their tasks are 
scoped and reviewed.
+
+Appropriate subagent work includes:
+
+- Read-only codebase exploration, such as checking existing CLI parser 
patterns, provider behavior,
+  or test conventions.
+- Independent design review, such as checking whether a proposed command still 
matches Unix
+  filesystem semantics.
+- Focused implementation tasks with explicit ownership of disjoint files or 
modules.
+- Focused verification tasks, such as running a specific test class or 
reviewing whether output
+  remains Unix-like.
+- Documentation review, such as checking that design, plan, and implementation 
notes remain
+  consistent.
+
+Subagents must not be used as unbounded background workers. Each delegated 
task should include:
+
+- The exact files or subsystem to inspect or edit.
+- Whether the task is read-only or may write files.
+- The expected output, such as a summary, changed file list, failing test, or 
patch.
+- A reminder that git operations require explicit user confirmation.
+- A reminder not to revert unrelated workspace changes.
+
+When multiple subagents are used in parallel, their write scopes must be 
disjoint. The main agent
+remains responsible for reviewing their results, integrating changes, running 
Spotless where code
+was edited, and running the focused verification commands.
+
+## Entry Point
+
+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.
+
+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
+filesystem shell.
+
+The filesystem shell still uses `IoTDBConnection` and JDBC `Statement` 
internally. It does not add a
+new client protocol.
+
+## Object Model
+
+Filesystem mode exposes an IoTDB metadata view. A filesystem path maps to a 
typed IoTDB metadata
+node. The node type drives command behavior and future write semantics.
+
+### Common Node Types
+
+- `VIRTUAL_ROOT`: synthetic `/` root.
+- `UNKNOWN`: a path that cannot be resolved to an IoTDB object.
+
+### Tree Model Mapping
+
+When `sql_dialect=tree`, filesystem paths map to the tree metadata hierarchy.
+
+| Filesystem Path | IoTDB Object | Node Type | Discovery |
+| --- | --- | --- | --- |
+| `/` | Virtual root | `VIRTUAL_ROOT` | Fixed |
+| `/root` | Tree root | `TREE_ROOT` | Fixed |
+| `/root/<db>` | Database | `TREE_DATABASE` | `SHOW DATABASES <path>` |
+| `/root/<...>` | Internal path | `TREE_INTERNAL_PATH` | `SHOW CHILD PATHS 
<path>` |
+| `/root/<...>/<device>` | Device | `TREE_DEVICE` | `SHOW DEVICES <path>` |
+| `/root/<...>/<measurement>` | Timeseries | `TREE_TIMESERIES` | `SHOW 
TIMESERIES <path>` |
+
+Tree paths are converted from slash-separated filesystem paths to 
dot-separated IoTDB paths. For
+example, `/root/sg/d1/s1` maps to `root.sg.d1.s1`.
+
+A tree path can be ambiguous: the same textual path can be an internal path 
and a device. The
+resolver should use staged checks and return the most specific metadata it can 
prove. `ls` focuses
+on children, while `stat` performs more detailed resolution.
+
+### Table Model Mapping
+
+When `sql_dialect=table`, filesystem paths map to relational schema objects.
+
+| 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` |
+
+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`.
+
+## Path Rules
+
+- `/` is always the filesystem root.
+- `.` and `..` are supported.
+- 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>`.
+- 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.
+
+## Command Mapping
+
+### Tree Model
+
+| Operation | SQL/API Mapping |
+| --- | --- |
+| `ls /` | Return fixed child `root/`. |
+| `ls /root` | `SHOW CHILD PATHS root`. |
+| `ls /root/sg` | `SHOW CHILD PATHS root.sg` for child directories and `SHOW 
TIMESERIES root.sg.*` for one-level measurement leaves. |
+| `stat /root/sg` | Check `SHOW DATABASES root.sg`, then 
child/device/timeseries metadata if needed. |
+| `stat /root/sg/d1` | Check `SHOW DEVICES root.sg.d1` and `SHOW TIMESERIES 
root.sg.d1`. |
+| `cat /root/sg/d1/s1` | `SHOW TIMESERIES root.sg.d1.s1`, formatted as schema 
text. |
+| `tree /root/sg -L 2` | Repeated one-level `SHOW CHILD PATHS` calls with a 
depth limit. |
+
+### Table Model
+
+| Operation | SQL/API Mapping |
+| --- | --- |
+| `ls /` | `SHOW DATABASES`. |
+| `ls /db` | `SHOW TABLES FROM db`. |
+| `ls /db/table` | `DESC db.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. |
+
+## 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.
+
+- `ls` prints child names only, one entry per line.
+- `tree` prints the hierarchy with indentation and names only.
+- `cat` prints row content as tab-separated values, one row per line.
+- `paste` prints multiple file-like paths side by side as tab-separated values.
+- `stat` is the command that may expose typed metadata, because Unix `stat` is 
explicitly about
+  object metadata.
+- 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
+`TREE_DATABASE` must not appear in `ls` or `tree` output. Similarly, 
`SqlRow.asMap().toString()`
+must not be used for `cat` or `paste` output.
+
+## 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
+  `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.
+
+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.
+
+## Proposed Code Structure
+
+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`.
+- `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/TreeFilesystemSchemaProvider`: tree-model SQL mapping.
+- `provider/TableFilesystemSchemaProvider`: table-model SQL mapping.
+- `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.
+
+## Provider Interface Direction
+
+The first version needs read-only operations:
+
+- `resolve(FsPath path)`
+- `list(FsPath path)`
+- `describe(FsPath path)`
+- `read(FsPath path)`
+
+The interface should be shaped so future writes can be added without changing 
command parsing:
+
+- `create(FsPath path, CreateOptions options)`
+- `delete(FsPath path, DeleteOptions options)`
+- `rename(FsPath source, FsPath target)`
+- `write(FsPath path, FsWriteContent content, WriteOptions options)`
+
+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.
+
+## Dependency Strategy
+
+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.
+
+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
+tree/table metadata identifiers.
+
+## Error Semantics
+
+- A missing path returns `No such path`.
+- Running directory-only commands on leaf nodes returns `Not a directory`.
+- Running leaf-only commands on directories returns `Is a directory`.
+- `cd` updates the current directory only after successful resolution.
+- `tree` uses a default depth limit to avoid accidentally scanning large 
schemas.
+- Command errors do not close the JDBC connection or exit the shell.
+
+## Parallel Work Guidance
+
+The filesystem mode is intentionally decomposed into path, command, provider, 
shell, and CLI
+dispatch layers so future work can be developed and verified in parallel.
+
+All operations may use subagents for acceleration. Parallel work must still 
respect ownership
+boundaries:
+
+- Path tasks own `fs/path` and path tests.
+- Command parser tasks own `fs/command` and parser tests.
+- Provider tasks own one provider package slice and matching provider tests.
+- Shell output and command-loop tasks own `FilesystemShell` and shell tests.
+- CLI dispatch and compatibility tasks own `Cli`, `AbstractCli`, and CLI tests.
+- Documentation tasks own the design and plan files under `docs/superpowers`.
+
+Read-only design review can run in parallel with local implementation. 
Implementation subagents may
+run in parallel only when their write scopes are disjoint. Verification 
subagents may run focused
+tests and summarize output while implementation continues, but they should not 
modify files unless
+explicitly reassigned as implementation workers.
+
+The main session remains responsible for integrating results, resolving 
conflicts, preserving SQL
+mode backward compatibility, enforcing Unix output semantics, running Spotless 
after code edits,
+and running the focused verification suite before claiming completion.
+
+## Testing Strategy
+
+Unit tests should cover the behavior without needing a live IoTDB instance 
wherever possible.
+
+- `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`.
+- 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.
+- CLI option tests extending existing CLI unit coverage for default 
`access_mode`, filesystem
+  mode, and invalid mode values.
+- Shell tests proving SQL mode remains the default and filesystem mode 
dispatches to
+  `FilesystemShell`.
+
+Integration tests against a real IoTDB cluster can be added after the 
unit-tested shell behavior is
+stable.
+
+## Risks and Mitigations
+
+- Tree path ambiguity: use staged resolution and return the most specific 
proven node type.
+- Large schema scans: limit `tree` depth by default and list only one level in 
`ls`.
+- 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.
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 688158e78b2..5e7680d2cc5 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
@@ -48,6 +48,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
 
@@ -69,6 +70,11 @@ public abstract class AbstractCli {
   static final String USERNAME_ARGS = "u";
   static final String USERNAME_NAME = "username";
 
+  static final String ACCESS_MODE_ARGS = "access_mode";
+  static final String ACCESS_MODE_NAME = "access mode";
+  static final String ACCESS_MODE_SQL = "sql";
+  static final String ACCESS_MODE_FILESYSTEM = "filesystem";
+
   private static final String EXECUTE_ARGS = "e";
 
   static final String USE_SSL_ARGS = "usessl";
@@ -134,6 +140,7 @@ public abstract class AbstractCli {
 
   static String execute;
   static boolean hasExecuteSQL = false;
+  static String accessMode = ACCESS_MODE_SQL;
 
   static Set<String> keywordSet = new HashSet<>();
 
@@ -158,6 +165,7 @@ public abstract class AbstractCli {
     keywordSet.add("-" + EXECUTE_ARGS);
     keywordSet.add("-" + ISO8601_ARGS);
     keywordSet.add("-" + RPC_COMPRESS_ARGS);
+    keywordSet.add("--" + ACCESS_MODE_ARGS);
   }
 
   static Options createOptions() {
@@ -221,6 +229,15 @@ public abstract class AbstractCli {
             .build();
     options.addOption(execute);
 
+    Option accessMode =
+        Option.builder()
+            .longOpt(ACCESS_MODE_ARGS)
+            .argName(ACCESS_MODE_NAME)
+            .hasArg()
+            .desc("Access mode, supports sql and filesystem. Default is sql. 
(optional)")
+            .build();
+    options.addOption(accessMode);
+
     Option isRpcCompressed =
         Option.builder(RPC_COMPRESS_ARGS)
             .argName(RPC_COMPRESS_NAME)
@@ -246,6 +263,24 @@ public abstract class AbstractCli {
     return options;
   }
 
+  static String getAccessMode(CliContext ctx, CommandLine commandLine) throws 
ArgsErrorException {
+    String value = commandLine.getOptionValue(ACCESS_MODE_ARGS, 
ACCESS_MODE_SQL);
+    String normalized = value.toLowerCase(Locale.ROOT);
+    if (ACCESS_MODE_SQL.equals(normalized) || 
ACCESS_MODE_FILESYSTEM.equals(normalized)) {
+      return normalized;
+    }
+    String msg =
+        String.format(
+            "%s: Unsupported access mode '%s'. Supported values are sql and 
filesystem.",
+            IOTDB, value);
+    ctx.getPrinter().println(msg);
+    throw new ArgsErrorException(msg);
+  }
+
+  static void setAccessMode(String accessMode) {
+    AbstractCli.accessMode = accessMode;
+  }
+
   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 d1bbd6165f6..4ded47379b9 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
@@ -19,11 +19,17 @@
 
 package org.apache.iotdb.cli;
 
+import org.apache.iotdb.cli.fs.FilesystemShell;
+import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.provider.TableFilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.provider.TreeFilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.sql.JdbcSqlExecutor;
 import org.apache.iotdb.cli.type.ExitType;
 import org.apache.iotdb.cli.utils.CliContext;
 import org.apache.iotdb.cli.utils.JlineUtils;
 import org.apache.iotdb.exception.ArgsErrorException;
 import org.apache.iotdb.jdbc.Config;
+import org.apache.iotdb.jdbc.Constant;
 import org.apache.iotdb.jdbc.IoTDBConnection;
 import org.apache.iotdb.rpc.RpcUtils;
 
@@ -98,7 +104,7 @@ public class Cli extends AbstractCli {
       ctx.getPrinter().println(IOTDB_ERROR_PREFIX + ": Exit cli with error: " 
+ e.getMessage());
       ctx.exit(CODE_ERROR);
     }
-    LineReader lineReader = JlineUtils.getLineReader(ctx, username, host, 
port);
+    LineReader lineReader = JlineUtils.getLineReader(ctx, username, host, 
port, accessMode);
     if (ctx.isDisableCliHistory()) {
       lineReader.getVariables().put(LineReader.DISABLE_HISTORY, Boolean.TRUE);
     }
@@ -138,6 +144,11 @@ public class Cli extends AbstractCli {
       if (commandLine.hasOption(Config.SQL_DIALECT)) {
         setSqlDialect(commandLine.getOptionValue(Config.SQL_DIALECT));
       }
+      setAccessMode(getAccessMode(ctx, commandLine));
+    } catch (ArgsErrorException e) {
+      ctx.getPrinter()
+          .println(IOTDB_ERROR_PREFIX + ": Input params error because " + 
e.getMessage());
+      return false;
     } catch (ParseException e) {
       ctx.getPrinter()
           .println(
@@ -170,7 +181,11 @@ public class Cli extends AbstractCli {
       constructProperties();
       if (hasExecuteSQL && password != null) {
         ctx.getLineReader().getVariables().put(LineReader.DISABLE_HISTORY, 
Boolean.TRUE);
-        executeSql(ctx);
+        if (ACCESS_MODE_FILESYSTEM.equals(accessMode)) {
+          executeFilesystemCommand(ctx);
+        } else {
+          executeSql(ctx);
+        }
       }
       receiveCommands(ctx);
     } catch (Exception e) {
@@ -195,6 +210,23 @@ public class Cli extends AbstractCli {
     }
   }
 
+  private static void executeFilesystemCommand(CliContext ctx) {
+    try (IoTDBConnection connection =
+        (IoTDBConnection)
+            DriverManager.getConnection(Config.IOTDB_URL_PREFIX + host + ":" + 
port + "/", info)) {
+      connection.setQueryTimeout(queryTimeout);
+      properties = connection.getServerProperties();
+      timestampPrecision = properties.getTimestampPrecision();
+      createFilesystemShell(ctx, connection).execute(execute);
+      ctx.exit(CODE_OK);
+    } catch (SQLException e) {
+      ctx.getPrinter()
+          .println(
+              IOTDB_ERROR_PREFIX + ": Can't execute filesystem command because 
" + e.getMessage());
+      ctx.exit(CODE_ERROR);
+    }
+  }
+
   private static void receiveCommands(CliContext ctx) throws TException {
     try (IoTDBConnection connection =
         (IoTDBConnection)
@@ -206,6 +238,17 @@ public class Cli extends AbstractCli {
       echoStarting(ctx);
       displayLogo(ctx, properties.getLogo(), properties.getVersion(), 
properties.getBuildInfo());
       ctx.getPrinter().println(String.format("Successfully login at %s:%s", 
host, port));
+      if (ACCESS_MODE_FILESYSTEM.equals(accessMode)) {
+        FilesystemShell shell = createFilesystemShell(ctx, connection);
+        JlineUtils.setCompleter(ctx.getLineReader(), shell.createCompleter());
+        while (true) {
+          boolean readLine = filesystemReaderReadLine(ctx, shell);
+          if (readLine) {
+            break;
+          }
+        }
+        return;
+      }
       while (true) {
         boolean readLine = readerReadLine(ctx, connection);
         if (readLine) {
@@ -218,6 +261,17 @@ public class Cli extends AbstractCli {
     }
   }
 
+  static FilesystemShell createFilesystemShell(CliContext ctx, IoTDBConnection 
connection) {
+    JdbcSqlExecutor executor = new JdbcSqlExecutor(connection);
+    FilesystemSchemaProvider provider;
+    if (Constant.TABLE_DIALECT.equalsIgnoreCase(connection.getSqlDialect())) {
+      provider = new TableFilesystemSchemaProvider(executor);
+    } else {
+      provider = new TreeFilesystemSchemaProvider(executor);
+    }
+    return new FilesystemShell(ctx, provider);
+  }
+
   private static boolean readerReadLine(CliContext ctx, IoTDBConnection 
connection) {
     String s;
     try {
@@ -241,6 +295,33 @@ public class Cli extends AbstractCli {
     return false;
   }
 
+  static boolean filesystemReaderReadLine(CliContext ctx, FilesystemShell 
shell) {
+    String s = "";
+    try {
+      s = ctx.getLineReader().readLine(cliPrefix + ":fs> ", null);
+      return !shell.execute(s);
+    } catch (SQLException e) {
+      ctx.getPrinter().println(filesystemCommandName(s) + ": " + 
e.getMessage());
+    } catch (UserInterruptException e) {
+      readLine(ctx);
+    } catch (EndOfFileException e) {
+      ctx.exit(CODE_OK);
+    } catch (IllegalArgumentException e) {
+      if (e.getMessage().contains("history")) {
+        return false;
+      }
+      throw e;
+    }
+    return false;
+  }
+
+  private static String filesystemCommandName(String input) {
+    if (input == null || input.trim().isEmpty()) {
+      return "filesystem";
+    }
+    return input.trim().split("\\s+", 2)[0];
+  }
+
   private static void readLine(CliContext ctx) {
     try {
       ctx.getLineReader().readLine("Press CTRL+C again to exit, or press ENTER 
to continue", '\0');
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
new file mode 100644
index 00000000000..4c8372bb8ad
--- /dev/null
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java
@@ -0,0 +1,276 @@
+/*
+ * 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;
+
+import org.apache.iotdb.cli.fs.command.FilesystemCommand;
+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.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.sql.SqlRow;
+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 java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+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", "paste", "tree", "help", 
"exit", "quit");
+
+  private final CliContext ctx;
+  private final FilesystemSchemaProvider provider;
+  private FsPath currentPath = FsPath.absolute("/");
+
+  public FilesystemShell(CliContext ctx, FilesystemSchemaProvider provider) {
+    this.ctx = ctx;
+    this.provider = provider;
+  }
+
+  public boolean execute(String input) throws SQLException {
+    FilesystemCommand command = FilesystemCommandParser.parse(input);
+    switch (command.getType()) {
+      case PWD:
+        ctx.getPrinter().println(currentPath.toString());
+        return true;
+      case LS:
+        printNodes(provider.list(resolve(command.getPath())));
+        return true;
+      case LL:
+        printLongNodes(provider.list(resolve(command.getPath())));
+        return true;
+      case CD:
+        changeDirectory(command.getPath());
+        return true;
+      case STAT:
+        printNode(provider.describe(resolve(command.getPath())));
+        return true;
+      case CAT:
+        printRows(provider.read(resolve(command.getPath()), 
DEFAULT_READ_LIMIT));
+        return true;
+      case PASTE:
+        printRows(provider.read(resolve(command.getPaths()), 
DEFAULT_READ_LIMIT));
+        return true;
+      case HELP:
+        printHelp();
+        return true;
+      case EXIT:
+        return false;
+      case TREE:
+        printTree(resolve(command.getPath()), command.getDepth());
+        return true;
+      case INVALID:
+        ctx.getPrinter().println(command.getErrorMessage());
+        return true;
+      case SQL:
+      default:
+        ctx.getPrinter().println("Unsupported filesystem command: " + 
command.getType());
+        return true;
+    }
+  }
+
+  public Completer createCompleter() {
+    return new FilesystemCompleter();
+  }
+
+  private void printTree(FsPath path, int depth) throws SQLException {
+    printTreeChildren(path, 0, depth);
+  }
+
+  private void printTreeChildren(FsPath path, int currentDepth, int maxDepth) 
throws SQLException {
+    if (currentDepth >= maxDepth) {
+      return;
+    }
+    for (FsNode node : provider.list(path)) {
+      ctx.getPrinter().println(indent(currentDepth) + node.getName());
+      if (isDirectory(node.getType())) {
+        printTreeChildren(node.getPath(), currentDepth + 1, maxDepth);
+      }
+    }
+  }
+
+  private static String indent(int depth) {
+    StringBuilder builder = new StringBuilder();
+    for (int i = 0; i < depth; i++) {
+      builder.append("  ");
+    }
+    return builder.toString();
+  }
+
+  private void changeDirectory(String path) throws SQLException {
+    FsPath target = resolve(path);
+    FsNode node = provider.describe(target);
+    if (isDirectory(node.getType())) {
+      currentPath = target;
+    } else {
+      ctx.getPrinter().println("cd: " + target + ": Not a directory");
+    }
+  }
+
+  private FsPath resolve(String path) {
+    return currentPath.resolve(path);
+  }
+
+  private List<FsPath> resolve(List<String> paths) {
+    List<FsPath> resolvedPaths = new ArrayList<>();
+    for (String path : paths) {
+      resolvedPaths.add(resolve(path));
+    }
+    return resolvedPaths;
+  }
+
+  private void printNodes(List<FsNode> nodes) {
+    for (FsNode node : nodes) {
+      ctx.getPrinter().println(node.getName());
+    }
+  }
+
+  private void printLongNodes(List<FsNode> nodes) {
+    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());
+    for (Map.Entry<String, String> entry : node.getMetadata().entrySet()) {
+      ctx.getPrinter().println(entry.getKey() + "\t" + entry.getValue());
+    }
+  }
+
+  private void printRows(List<SqlRow> rows) {
+    for (SqlRow row : rows) {
+      ctx.getPrinter().println(joinValues(row));
+    }
+  }
+
+  private static String joinValues(SqlRow row) {
+    StringBuilder builder = new StringBuilder();
+    for (String value : row.asMap().values()) {
+      if (builder.length() > 0) {
+        builder.append('\t');
+      }
+      if (value != null) {
+        builder.append(value);
+      }
+    }
+    return builder.toString();
+  }
+
+  private void printHelp() {
+    ctx.getPrinter().println("pwd");
+    ctx.getPrinter().println("ls [path]");
+    ctx.getPrinter().println("ll [path]");
+    ctx.getPrinter().println("cd <path>");
+    ctx.getPrinter().println("stat [path]");
+    ctx.getPrinter().println("cat <path>");
+    ctx.getPrinter().println("paste <path>...");
+    ctx.getPrinter().println("exit");
+  }
+
+  private static boolean isDirectory(FsNodeType type) {
+    return type == FsNodeType.VIRTUAL_ROOT
+        || type == FsNodeType.TREE_ROOT
+        || 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;
+  }
+
+  private static String longMode(FsNodeType type) {
+    if (isDirectory(type)) {
+      return "dr-xr-xr-x";
+    }
+    return "-r--r--r--";
+  }
+
+  private class FilesystemCompleter implements Completer {
+
+    @Override
+    public void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
+      if (line.wordIndex() == 0) {
+        completeCommand(line.word(), candidates);
+        return;
+      }
+      completePath(line.word(), candidates);
+    }
+
+    private void completeCommand(String prefix, List<Candidate> candidates) {
+      for (String command : COMMANDS) {
+        if (command.startsWith(prefix)) {
+          candidates.add(new Candidate(command));
+        }
+      }
+    }
+
+    private void completePath(String word, List<Candidate> candidates) {
+      try {
+        FsPath basePath = completionBasePath(word);
+        String prefix = completionPrefix(word);
+        for (FsNode node : provider.list(basePath)) {
+          if (!node.getName().startsWith(prefix)) {
+            continue;
+          }
+          String value = completionValue(word, node);
+          candidates.add(new Candidate(value));
+        }
+      } catch (SQLException e) {
+        // Ignore completion errors to keep TAB non-disruptive.
+      }
+    }
+
+    private FsPath completionBasePath(String word) {
+      int slash = word.lastIndexOf('/');
+      if (slash < 0) {
+        return currentPath;
+      }
+      String parent = slash == 0 ? "/" : word.substring(0, slash);
+      return resolve(parent);
+    }
+
+    private String completionPrefix(String word) {
+      int slash = word.lastIndexOf('/');
+      if (slash < 0) {
+        return word;
+      }
+      return word.substring(slash + 1);
+    }
+
+    private String completionValue(String word, FsNode node) {
+      int slash = word.lastIndexOf('/');
+      String parent = slash < 0 ? "" : word.substring(0, slash + 1);
+      String suffix = isDirectory(node.getType()) ? "/" : "";
+      return parent + node.getName() + suffix;
+    }
+  }
+}
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
new file mode 100644
index 00000000000..f375d008669
--- /dev/null
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommand.java
@@ -0,0 +1,112 @@
+/*
+ * 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.command;
+
+import java.util.Collections;
+import java.util.List;
+
+public class FilesystemCommand {
+
+  public enum Type {
+    PWD,
+    LS,
+    LL,
+    CD,
+    STAT,
+    CAT,
+    PASTE,
+    TREE,
+    SQL,
+    HELP,
+    EXIT,
+    INVALID
+  }
+
+  private final Type type;
+  private final String path;
+  private final List<String> paths;
+  private final int depth;
+  private final String statement;
+  private final String errorMessage;
+
+  private FilesystemCommand(
+      Type type,
+      String path,
+      List<String> paths,
+      int depth,
+      String statement,
+      String errorMessage) {
+    this.type = type;
+    this.path = path;
+    this.paths = paths;
+    this.depth = depth;
+    this.statement = statement;
+    this.errorMessage = errorMessage;
+  }
+
+  public static FilesystemCommand simple(Type type) {
+    return new FilesystemCommand(type, "", Collections.emptyList(), -1, "", 
"");
+  }
+
+  public static FilesystemCommand path(Type type, String path) {
+    return new FilesystemCommand(type, path, Collections.singletonList(path), 
-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, "", "");
+  }
+
+  public static FilesystemCommand tree(String path, int depth) {
+    return new FilesystemCommand(Type.TREE, path, 
Collections.singletonList(path), depth, "", "");
+  }
+
+  public static FilesystemCommand sql(String statement) {
+    return new FilesystemCommand(Type.SQL, "", Collections.emptyList(), -1, 
statement, "");
+  }
+
+  public static FilesystemCommand invalid(String errorMessage) {
+    return new FilesystemCommand(Type.INVALID, "", Collections.emptyList(), 
-1, "", errorMessage);
+  }
+
+  public Type getType() {
+    return type;
+  }
+
+  public String getPath() {
+    return path;
+  }
+
+  public List<String> getPaths() {
+    return paths;
+  }
+
+  public int getDepth() {
+    return depth;
+  }
+
+  public String getStatement() {
+    return statement;
+  }
+
+  public String getErrorMessage() {
+    return errorMessage;
+  }
+}
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
new file mode 100644
index 00000000000..5fc2021edcd
--- /dev/null
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParser.java
@@ -0,0 +1,125 @@
+/*
+ * 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.command;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+public class FilesystemCommandParser {
+
+  private static final String DEFAULT_PATH = ".";
+  private static final int DEFAULT_TREE_DEPTH = Integer.MAX_VALUE;
+
+  private FilesystemCommandParser() {}
+
+  public static FilesystemCommand parse(String input) {
+    String line = input == null ? "" : input.trim();
+    if (line.isEmpty()) {
+      return FilesystemCommand.invalid("Empty command");
+    }
+
+    String lowerLine = line.toLowerCase(Locale.ROOT);
+    if (lowerLine.equals("pwd")) {
+      return FilesystemCommand.simple(FilesystemCommand.Type.PWD);
+    }
+    if (lowerLine.equals("help")) {
+      return FilesystemCommand.simple(FilesystemCommand.Type.HELP);
+    }
+    if (lowerLine.equals("exit") || lowerLine.equals("quit")) {
+      return FilesystemCommand.simple(FilesystemCommand.Type.EXIT);
+    }
+    if (lowerLine.startsWith("sql ")) {
+      return parseSql(line);
+    }
+
+    String[] tokens = line.split("\\s+");
+    String command = tokens[0].toLowerCase(Locale.ROOT);
+    if ("ls".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.LS, 
pathArgument(tokens));
+    }
+    if ("ll".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.LL, 
pathArgument(tokens));
+    }
+    if ("cd".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.CD, 
pathArgument(tokens));
+    }
+    if ("stat".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.STAT, 
pathArgument(tokens));
+    }
+    if ("cat".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.CAT, 
pathArgument(tokens));
+    }
+    if ("paste".equals(command)) {
+      return parsePaste(tokens);
+    }
+    if ("tree".equals(command)) {
+      return parseTree(tokens);
+    }
+    return FilesystemCommand.invalid("Unknown command: " + tokens[0]);
+  }
+
+  private static FilesystemCommand parseSql(String line) {
+    String statement = line.substring(3).trim();
+    if (statement.isEmpty()) {
+      return FilesystemCommand.invalid("SQL statement is empty");
+    }
+    return FilesystemCommand.sql(statement);
+  }
+
+  private static FilesystemCommand parsePaste(String[] tokens) {
+    if (tokens.length < 2) {
+      return FilesystemCommand.invalid("Missing paste path");
+    }
+    List<String> paths = new ArrayList<>();
+    for (int i = 1; i < tokens.length; i++) {
+      paths.add(tokens[i]);
+    }
+    return FilesystemCommand.paths(FilesystemCommand.Type.PASTE, paths);
+  }
+
+  private static FilesystemCommand parseTree(String[] tokens) {
+    String path = DEFAULT_PATH;
+    int depth = DEFAULT_TREE_DEPTH;
+
+    for (int i = 1; i < tokens.length; i++) {
+      if ("-L".equals(tokens[i])) {
+        if (i + 1 >= tokens.length) {
+          return FilesystemCommand.invalid("Missing tree depth");
+        }
+        try {
+          depth = Integer.parseInt(tokens[++i]);
+        } catch (NumberFormatException e) {
+          return FilesystemCommand.invalid("Invalid tree depth: " + tokens[i]);
+        }
+        if (depth < 0) {
+          return FilesystemCommand.invalid("Invalid tree depth: " + depth);
+        }
+      } else {
+        path = tokens[i];
+      }
+    }
+    return FilesystemCommand.tree(path, depth);
+  }
+
+  private static String pathArgument(String[] tokens) {
+    return tokens.length > 1 ? tokens[1] : DEFAULT_PATH;
+  }
+}
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNode.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNode.java
new file mode 100644
index 00000000000..c0c69132d32
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNode.java
@@ -0,0 +1,61 @@
+/*
+ * 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.node;
+
+import org.apache.iotdb.cli.fs.path.FsPath;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class FsNode {
+
+  private final String name;
+  private final FsPath path;
+  private final FsNodeType type;
+  private final Map<String, String> metadata;
+
+  public FsNode(String name, FsPath path, FsNodeType type) {
+    this(name, path, type, Collections.<String, String>emptyMap());
+  }
+
+  public FsNode(String name, FsPath path, FsNodeType type, Map<String, String> 
metadata) {
+    this.name = name;
+    this.path = path;
+    this.type = type;
+    this.metadata = Collections.unmodifiableMap(new LinkedHashMap<>(metadata));
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public FsPath getPath() {
+    return path;
+  }
+
+  public FsNodeType getType() {
+    return type;
+  }
+
+  public Map<String, String> getMetadata() {
+    return metadata;
+  }
+}
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
new file mode 100644
index 00000000000..a38ac575520
--- /dev/null
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
@@ -0,0 +1,34 @@
+/*
+ * 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.node;
+
+public enum FsNodeType {
+  VIRTUAL_ROOT,
+  TREE_ROOT,
+  TREE_DATABASE,
+  TREE_INTERNAL_PATH,
+  TREE_DEVICE,
+  TREE_TIMESERIES,
+  TABLE_DATABASE,
+  TABLE_TABLE,
+  TABLE_VIEW,
+  TABLE_COLUMN,
+  UNKNOWN
+}
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/path/FsPath.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/path/FsPath.java
new file mode 100644
index 00000000000..8a82dc786f2
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/path/FsPath.java
@@ -0,0 +1,127 @@
+/*
+ * 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.path;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.List;
+import java.util.Objects;
+
+public class FsPath {
+
+  private static final String SEPARATOR = "/";
+
+  private final List<String> segments;
+
+  private FsPath(List<String> segments) {
+    this.segments = Collections.unmodifiableList(new ArrayList<>(segments));
+  }
+
+  public static FsPath absolute(String path) {
+    return new FsPath(normalize(path));
+  }
+
+  public FsPath resolve(String path) {
+    if (path == null || path.isEmpty()) {
+      return this;
+    }
+    if (path.startsWith(SEPARATOR)) {
+      return absolute(path);
+    }
+
+    List<String> combined = new ArrayList<>(segments);
+    combined.addAll(split(path));
+    return new FsPath(normalize(combined));
+  }
+
+  public boolean isRoot() {
+    return segments.isEmpty();
+  }
+
+  public List<String> getSegments() {
+    return segments;
+  }
+
+  public String getFileName() {
+    if (segments.isEmpty()) {
+      return "";
+    }
+    return segments.get(segments.size() - 1);
+  }
+
+  @Override
+  public String toString() {
+    if (segments.isEmpty()) {
+      return SEPARATOR;
+    }
+    return SEPARATOR + String.join(SEPARATOR, segments);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof FsPath)) {
+      return false;
+    }
+    FsPath fsPath = (FsPath) o;
+    return Objects.equals(segments, fsPath.segments);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(segments);
+  }
+
+  private static List<String> normalize(String path) {
+    return normalize(split(path));
+  }
+
+  private static List<String> normalize(List<String> pathSegments) {
+    Deque<String> normalized = new ArrayDeque<>();
+    for (String segment : pathSegments) {
+      if (segment.isEmpty() || ".".equals(segment)) {
+        continue;
+      }
+      if ("..".equals(segment)) {
+        if (!normalized.isEmpty()) {
+          normalized.removeLast();
+        }
+        continue;
+      }
+      normalized.addLast(segment);
+    }
+    return new ArrayList<>(normalized);
+  }
+
+  private static List<String> split(String path) {
+    if (path == null || path.isEmpty()) {
+      return Collections.emptyList();
+    }
+    List<String> result = new ArrayList<>();
+    for (String segment : path.split(SEPARATOR)) {
+      result.add(segment);
+    }
+    return result;
+  }
+}
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
new file mode 100644
index 00000000000..f96f617f363
--- /dev/null
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
@@ -0,0 +1,43 @@
+/*
+ * 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.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 {
+
+  List<FsNode> list(FsPath path) throws SQLException;
+
+  FsNode describe(FsPath path) throws SQLException;
+
+  List<SqlRow> read(FsPath path, int limit) throws SQLException;
+
+  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");
+  }
+}
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
new file mode 100644
index 00000000000..031ca186ab8
--- /dev/null
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
@@ -0,0 +1,196 @@
+/*
+ * 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.node.FsNode;
+import org.apache.iotdb.cli.fs.node.FsNodeType;
+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 java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider 
{
+
+  private final SqlExecutor executor;
+
+  public TableFilesystemSchemaProvider(SqlExecutor executor) {
+    this.executor = executor;
+  }
+
+  @Override
+  public List<FsNode> list(FsPath path) throws SQLException {
+    int depth = path.getSegments().size();
+    if (depth == 0) {
+      return listDatabases();
+    }
+    if (depth == 1) {
+      return listTables(path);
+    }
+    if (depth == 2) {
+      return listColumns(path);
+    }
+    return new ArrayList<>();
+  }
+
+  @Override
+  public FsNode describe(FsPath path) throws SQLException {
+    int depth = path.getSegments().size();
+    if (depth == 0) {
+      return new FsNode("/", path, FsNodeType.VIRTUAL_ROOT);
+    }
+    if (depth == 1) {
+      return describeDatabase(path);
+    }
+    if (depth == 2) {
+      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);
+  }
+
+  @Override
+  public List<SqlRow> read(FsPath path, int limit) throws SQLException {
+    int depth = path.getSegments().size();
+    if (depth == 2) {
+      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);
+  }
+
+  @Override
+  public List<SqlRow> read(List<FsPath> paths, int limit) throws SQLException {
+    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");
+      }
+    }
+    return executor.query(
+        "SELECT " + columnList(paths) + " FROM " + toTablePath(tablePath) + " 
LIMIT " + limit);
+  }
+
+  private List<FsNode> listDatabases() throws SQLException {
+    List<FsNode> nodes = new ArrayList<>();
+    for (SqlRow row : executor.query("SHOW DATABASES")) {
+      String database = row.get("Database");
+      if (database != null) {
+        nodes.add(new FsNode(database, FsPath.absolute("/" + database), 
FsNodeType.TABLE_DATABASE));
+      }
+    }
+    return nodes;
+  }
+
+  private FsNode describeDatabase(FsPath path) throws SQLException {
+    String database = path.getFileName();
+    for (FsNode node : listDatabases()) {
+      if (database.equals(node.getName())) {
+        return node;
+      }
+    }
+    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 List<FsNode> listTables(FsPath databasePath) throws SQLException {
+    String database = databasePath.getFileName();
+    List<FsNode> nodes = 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));
+      }
+    }
+    return nodes;
+  }
+
+  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()));
+      }
+    }
+    return nodes;
+  }
+
+  private static String toTablePath(FsPath path) {
+    List<String> segments = path.getSegments();
+    return segments.get(0) + "." + segments.get(1);
+  }
+
+  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 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/TreeFilesystemSchemaProvider.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProvider.java
new file mode 100644
index 00000000000..0766bb271d4
--- /dev/null
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProvider.java
@@ -0,0 +1,169 @@
+/*
+ * 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.node.FsNode;
+import org.apache.iotdb.cli.fs.node.FsNodeType;
+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 java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+public class TreeFilesystemSchemaProvider implements FilesystemSchemaProvider {
+
+  private static final String ROOT = "root";
+
+  private final SqlExecutor executor;
+
+  public TreeFilesystemSchemaProvider(SqlExecutor executor) {
+    this.executor = executor;
+  }
+
+  @Override
+  public List<FsNode> list(FsPath path) throws SQLException {
+    if (path.isRoot()) {
+      return listTreeRoots();
+    }
+    if (ROOT.equals(path.toString().substring(1))) {
+      return listDatabases();
+    }
+    return listChildPaths(path);
+  }
+
+  @Override
+  public FsNode describe(FsPath path) throws SQLException {
+    if (path.isRoot()) {
+      return new FsNode("/", path, FsNodeType.VIRTUAL_ROOT);
+    }
+    if (isTreeRoot(path)) {
+      return new FsNode(ROOT, path, FsNodeType.TREE_ROOT);
+    }
+    if (isDatabase(path)) {
+      return new FsNode(path.getFileName(), path, FsNodeType.TREE_DATABASE);
+    }
+    List<SqlRow> rows = executor.query("SHOW TIMESERIES " + toTreePath(path));
+    if (rows.isEmpty()) {
+      return new FsNode(path.getFileName(), path, FsNodeType.UNKNOWN);
+    }
+    return new FsNode(path.getFileName(), path, FsNodeType.TREE_TIMESERIES, 
rows.get(0).asMap());
+  }
+
+  @Override
+  public List<SqlRow> read(FsPath path, int limit) throws SQLException {
+    String measurement = path.getFileName();
+    FsPath devicePath = parent(path);
+    return executor.query(
+        "SELECT " + measurement + " FROM " + toTreePath(devicePath) + " LIMIT 
" + limit);
+  }
+
+  private List<FsNode> listTreeRoots() throws SQLException {
+    Set<String> roots = new LinkedHashSet<>();
+    for (SqlRow row : executor.query("SHOW DATABASES")) {
+      String database = row.get("Database");
+      if (database != null && database.startsWith(ROOT)) {
+        roots.add(ROOT);
+      }
+    }
+    List<FsNode> nodes = new ArrayList<>();
+    for (String root : roots) {
+      nodes.add(new FsNode(root, FsPath.absolute("/" + root), 
FsNodeType.TREE_ROOT));
+    }
+    return nodes;
+  }
+
+  private List<FsNode> listDatabases() throws SQLException {
+    List<FsNode> nodes = new ArrayList<>();
+    for (SqlRow row : executor.query("SHOW DATABASES")) {
+      String database = row.get("Database");
+      if (database == null || !database.startsWith(ROOT + ".")) {
+        continue;
+      }
+      String name = database.substring((ROOT + ".").length());
+      if (!name.contains(".")) {
+        nodes.add(
+            new FsNode(name, FsPath.absolute("/" + ROOT + "/" + name), 
FsNodeType.TREE_DATABASE));
+      }
+    }
+    return nodes;
+  }
+
+  private List<FsNode> listChildPaths(FsPath path) throws SQLException {
+    List<FsNode> nodes = new ArrayList<>();
+    for (SqlRow row : executor.query("SHOW CHILD PATHS " + toTreePath(path))) {
+      String childPath = row.get("ChildPaths");
+      if (childPath == null) {
+        continue;
+      }
+      FsPath fsPath = fromTreePath(childPath);
+      nodes.add(new FsNode(fsPath.getFileName(), fsPath, 
FsNodeType.TREE_INTERNAL_PATH));
+    }
+    return nodes;
+  }
+
+  private boolean isTreeRoot(FsPath path) {
+    List<String> segments = path.getSegments();
+    return segments.size() == 1 && ROOT.equals(segments.get(0));
+  }
+
+  private boolean isDatabase(FsPath path) throws SQLException {
+    if (path.getSegments().size() != 2 || 
!ROOT.equals(path.getSegments().get(0))) {
+      return false;
+    }
+    String treePath = toTreePath(path);
+    for (SqlRow row : executor.query("SHOW DATABASES")) {
+      if (treePath.equals(row.get("Database"))) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static String toTreePath(FsPath path) {
+    StringBuilder builder = new StringBuilder();
+    for (String segment : path.getSegments()) {
+      if (builder.length() > 0) {
+        builder.append('.');
+      }
+      builder.append(segment);
+    }
+    return builder.toString();
+  }
+
+  private static FsPath fromTreePath(String treePath) {
+    return FsPath.absolute("/" + treePath.replace('.', '/'));
+  }
+
+  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/sql/JdbcSqlExecutor.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java
new file mode 100644
index 00000000000..31a5925a64b
--- /dev/null
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java
@@ -0,0 +1,57 @@
+/*
+ * 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.sql;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class JdbcSqlExecutor implements SqlExecutor {
+
+  private final Connection connection;
+
+  public JdbcSqlExecutor(Connection connection) {
+    this.connection = connection;
+  }
+
+  @Override
+  public List<SqlRow> query(String sql) throws SQLException {
+    try (Statement statement = connection.createStatement();
+        ResultSet resultSet = statement.executeQuery(sql)) {
+      ResultSetMetaData metaData = resultSet.getMetaData();
+      int columnCount = metaData.getColumnCount();
+      List<SqlRow> rows = new ArrayList<>();
+      while (resultSet.next()) {
+        Map<String, String> values = new LinkedHashMap<>();
+        for (int column = 1; column <= columnCount; column++) {
+          values.put(metaData.getColumnLabel(column), 
resultSet.getString(column));
+        }
+        rows.add(new SqlRow(values));
+      }
+      return rows;
+    }
+  }
+}
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
new file mode 100644
index 00000000000..c372c2e3509
--- /dev/null
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java
@@ -0,0 +1,28 @@
+/*
+ * 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.sql;
+
+import java.sql.SQLException;
+import java.util.List;
+
+public interface SqlExecutor {
+
+  List<SqlRow> query(String sql) throws SQLException;
+}
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlRow.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlRow.java
new file mode 100644
index 00000000000..c806aa2a064
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlRow.java
@@ -0,0 +1,59 @@
+/*
+ * 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.sql;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class SqlRow {
+
+  private final Map<String, String> values;
+
+  public SqlRow(Map<String, String> values) {
+    this.values = Collections.unmodifiableMap(new LinkedHashMap<>(values));
+  }
+
+  public static SqlRow of(String... keyValues) {
+    if (keyValues.length % 2 != 0) {
+      throw new IllegalArgumentException("SqlRow keyValues must contain 
pairs");
+    }
+    Map<String, String> values = new LinkedHashMap<>();
+    for (int i = 0; i < keyValues.length; i += 2) {
+      values.put(keyValues[i], keyValues[i + 1]);
+    }
+    return new SqlRow(values);
+  }
+
+  public static List<SqlRow> list(SqlRow... rows) {
+    return new ArrayList<>(Arrays.asList(rows));
+  }
+
+  public String get(String column) {
+    return values.get(column);
+  }
+
+  public Map<String, String> asMap() {
+    return values;
+  }
+}
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 c902598cd3f..b244b00ebf4 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
@@ -21,9 +21,11 @@ package org.apache.iotdb.cli.utils;
 
 import org.apache.iotdb.db.qp.sql.SqlLexer;
 
+import org.jline.reader.Completer;
 import org.jline.reader.LineReader;
 import org.jline.reader.LineReader.Option;
 import org.jline.reader.LineReaderBuilder;
+import org.jline.reader.impl.LineReaderImpl;
 import org.jline.reader.impl.completer.StringsCompleter;
 import org.jline.terminal.Size;
 import org.jline.terminal.Terminal;
@@ -56,6 +58,12 @@ public class JlineUtils {
 
   public static LineReader getLineReader(CliContext ctx, String username, 
String host, String port)
       throws IOException {
+    return getLineReader(ctx, username, host, port, "sql");
+  }
+
+  public static LineReader getLineReader(
+      CliContext ctx, String username, String host, String port, String 
accessMode)
+      throws IOException {
     Logger.getLogger("org.jline").setLevel(Level.OFF);
 
     // Defaulting to a dumb terminal when a supported terminal can not be 
correctly created
@@ -107,7 +115,7 @@ public class JlineUtils {
     // avoid incorrect inputs.
     //    builder.highlighter(new IoTDBSyntaxHighlighter());
 
-    builder.completer(new StringsCompleter(SQL_KEYWORDS));
+    builder.completer(createCompleter(accessMode));
 
     builder.option(Option.CASE_INSENSITIVE_SEARCH, true);
     builder.option(Option.CASE_INSENSITIVE, true);
@@ -140,4 +148,18 @@ public class JlineUtils {
     autosuggestionWidgets.enable();
     return lineReader;
   }
+
+  static Completer createCompleter(String accessMode) {
+    if ("filesystem".equalsIgnoreCase(accessMode)) {
+      return new StringsCompleter(
+          "pwd", "ls", "ll", "cd", "stat", "cat", "paste", "tree", "help", 
"exit", "quit");
+    }
+    return new StringsCompleter(SQL_KEYWORDS);
+  }
+
+  public static void setCompleter(LineReader lineReader, Completer completer) {
+    if (lineReader instanceof LineReaderImpl) {
+      ((LineReaderImpl) lineReader).setCompleter(completer);
+    }
+  }
 }
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 635676ce2af..aa6ed5332fd 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
@@ -115,6 +115,45 @@ public class AbstractCliTest {
     }
   }
 
+  @Test
+  public void testAccessModeDefaultSql() 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"});
+
+    assertEquals(AbstractCli.ACCESS_MODE_SQL, AbstractCli.getAccessMode(ctx, 
commandLine));
+  }
+
+  @Test
+  public void testAccessModeFilesystem() 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", "--access_mode", 
"filesystem"});
+
+    assertEquals(AbstractCli.ACCESS_MODE_FILESYSTEM, 
AbstractCli.getAccessMode(ctx, commandLine));
+  }
+
+  @Test
+  public void testAccessModeRejectsInvalidValue() 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", "--access_mode", 
"unknown"});
+
+    try {
+      AbstractCli.getAccessMode(ctx, commandLine);
+      fail();
+    } catch (ArgsErrorException e) {
+      assertEquals(
+          "IoTDB: Unsupported access mode 'unknown'. Supported values are sql 
and filesystem.",
+          e.getMessage());
+    }
+  }
+
   @Test
   public void testRemovePasswordArgs() {
     AbstractCli.init();
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
new file mode 100644
index 00000000000..e50a51efd68
--- /dev/null
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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;
+
+import org.apache.iotdb.cli.fs.FilesystemShell;
+import org.apache.iotdb.cli.type.ExitType;
+import org.apache.iotdb.cli.utils.CliContext;
+import org.apache.iotdb.jdbc.IoTDBConnection;
+
+import org.jline.reader.LineReader;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+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;
+
+  private CliContext ctx;
+  private ByteArrayOutputStream out;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    out = new ByteArrayOutputStream();
+    ctx = new CliContext(System.in, new PrintStream(out), System.err, 
ExitType.EXCEPTION);
+  }
+
+  @Test
+  public void createFilesystemShellUsesTableProviderForTableDialect() throws 
Exception {
+    when(connection.getSqlDialect()).thenReturn("table");
+    mockSingleColumnQuery("SHOW TABLES FROM db1", "TableName", "table1");
+
+    FilesystemShell shell = Cli.createFilesystemShell(ctx, connection);
+    shell.execute("ls /db1");
+
+    verify(statement).executeQuery("SHOW TABLES FROM db1");
+  }
+
+  @Test
+  public void createFilesystemShellUsesTreeProviderForTreeDialect() throws 
Exception {
+    when(connection.getSqlDialect()).thenReturn("tree");
+    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 CHILD PATHS root.sg");
+  }
+
+  @Test
+  public void filesystemReaderPrintsCommandErrorAndContinues() throws 
Exception {
+    ctx.setLineReader(lineReader);
+    when(lineReader.readLine("IoTDB:fs> ", null)).thenReturn("cat time");
+    when(shell.execute("cat time")).thenThrow(new SQLException("550: Table 
does not exist"));
+
+    boolean shouldStop = Cli.filesystemReaderReadLine(ctx, shell);
+
+    assertFalse(shouldStop);
+    verify(shell).execute("cat time");
+    org.junit.Assert.assertTrue(out.toString().contains("cat: 550: Table does 
not exist"));
+  }
+
+  private void mockSingleColumnQuery(String sql, String column, String value) 
throws Exception {
+    when(connection.createStatement()).thenReturn(statement);
+    when(statement.executeQuery(sql)).thenReturn(resultSet);
+    when(resultSet.getMetaData()).thenReturn(metaData);
+    when(metaData.getColumnCount()).thenReturn(1);
+    when(metaData.getColumnLabel(1)).thenReturn(column);
+    when(resultSet.next()).thenReturn(true, false);
+    when(resultSet.getString(1)).thenReturn(value);
+  }
+}
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
new file mode 100644
index 00000000000..e49ee0daf8a
--- /dev/null
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/FilesystemShellTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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;
+
+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.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.sql.SqlRow;
+import org.apache.iotdb.cli.type.ExitType;
+import org.apache.iotdb.cli.utils.CliContext;
+
+import org.jline.reader.Candidate;
+import org.jline.reader.Completer;
+import org.jline.reader.ParsedLine;
+import org.jline.reader.impl.DefaultParser;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class FilesystemShellTest {
+
+  @Mock private FilesystemSchemaProvider provider;
+
+  private ByteArrayOutputStream out;
+  private FilesystemShell shell;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    out = new ByteArrayOutputStream();
+    CliContext ctx =
+        new CliContext(
+            new ByteArrayInputStream(new byte[0]),
+            new PrintStream(out),
+            System.err,
+            ExitType.EXCEPTION);
+    shell = new FilesystemShell(ctx, provider);
+  }
+
+  @Test
+  public void executePwdPrintsCurrentPath() throws SQLException {
+    assertTrue(shell.execute("pwd"));
+
+    assertTrue(out.toString().contains("/"));
+  }
+
+  @Test
+  public void executeLsPrintsChildNodes() throws SQLException {
+    when(provider.list(FsPath.absolute("/")))
+        .thenReturn(
+            Arrays.asList(new FsNode("root", FsPath.absolute("/root"), 
FsNodeType.TREE_ROOT)));
+
+    assertTrue(shell.execute("ls /"));
+
+    assertTrue(out.toString().contains("root"));
+    assertFalse(out.toString().contains("TREE_ROOT"));
+    verify(provider).list(FsPath.absolute("/"));
+  }
+
+  @Test
+  public void executeLlPrintsLongListing() throws SQLException {
+    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)));
+
+    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).list(FsPath.absolute("/"));
+  }
+
+  @Test
+  public void executeCdUpdatesCurrentPath() throws SQLException {
+    when(provider.describe(FsPath.absolute("/root")))
+        .thenReturn(new FsNode("root", FsPath.absolute("/root"), 
FsNodeType.TREE_ROOT));
+
+    assertTrue(shell.execute("cd /root"));
+    assertTrue(shell.execute("pwd"));
+
+    assertTrue(out.toString().contains("/root"));
+  }
+
+  @Test
+  public void executeExitStopsShell() throws SQLException {
+    assertFalse(shell.execute("exit"));
+  }
+
+  @Test
+  public void executeTreePrintsChildrenUntilDepth() throws SQLException {
+    when(provider.list(FsPath.absolute("/")))
+        .thenReturn(
+            Arrays.asList(new FsNode("root", FsPath.absolute("/root"), 
FsNodeType.TREE_ROOT)));
+    when(provider.list(FsPath.absolute("/root")))
+        .thenReturn(
+            Arrays.asList(new FsNode("sg", FsPath.absolute("/root/sg"), 
FsNodeType.TREE_DATABASE)));
+
+    assertTrue(shell.execute("tree -L 2 /"));
+
+    assertTrue(out.toString().contains("root"));
+    assertTrue(out.toString().contains("sg"));
+    assertFalse(out.toString().contains("TREE_ROOT"));
+    assertFalse(out.toString().contains("TREE_DATABASE"));
+    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")));
+
+    assertTrue(shell.execute("cat /db1/table1"));
+
+    assertTrue(out.toString().contains("1\ta\t42"));
+    assertFalse(out.toString().contains("{"));
+    verify(provider).read(FsPath.absolute("/db1/table1"), 20);
+  }
+
+  @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")));
+
+    assertTrue(shell.execute("paste /db1/table1/tag1 /db1/table1/s1"));
+
+    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);
+  }
+
+  @Test
+  public void completerCompletesChildrenFromCurrentDirectory() throws 
SQLException {
+    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)));
+
+    List<String> values = complete(shell.createCompleter(), "cd t");
+
+    assertTrue(values.contains("testtest/"));
+    assertFalse(values.contains("value"));
+    verify(provider).list(FsPath.absolute("/"));
+  }
+
+  private static List<String> complete(Completer completer, String line) {
+    ParsedLine parsedLine = new DefaultParser().parse(line, line.length());
+    List<Candidate> candidates = new ArrayList<>();
+    completer.complete(null, parsedLine, candidates);
+    return 
candidates.stream().map(Candidate::value).collect(Collectors.toList());
+  }
+}
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
new file mode 100644
index 00000000000..360e5cbe593
--- /dev/null
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParserTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.command;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class FilesystemCommandParserTest {
+
+  @Test
+  public void parseSimpleCommands() {
+    assertEquals(FilesystemCommand.Type.PWD, 
FilesystemCommandParser.parse("pwd").getType());
+    assertEquals(FilesystemCommand.Type.HELP, 
FilesystemCommandParser.parse("help").getType());
+    assertEquals(FilesystemCommand.Type.EXIT, 
FilesystemCommandParser.parse("exit").getType());
+    assertEquals(FilesystemCommand.Type.EXIT, 
FilesystemCommandParser.parse("quit").getType());
+  }
+
+  @Test
+  public void parseLlAsLongListCommand() {
+    FilesystemCommand command = FilesystemCommandParser.parse("ll /db1");
+
+    assertEquals(FilesystemCommand.Type.LL, command.getType());
+    assertEquals("/db1", command.getPath());
+  }
+
+  @Test
+  public void parsePathCommand() {
+    FilesystemCommand command = FilesystemCommandParser.parse("  ls   /root/sg 
 ");
+
+    assertEquals(FilesystemCommand.Type.LS, command.getType());
+    assertEquals("/root/sg", command.getPath());
+  }
+
+  @Test
+  public void parseCatTablePath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("cat 
/db1/table1");
+
+    assertEquals(FilesystemCommand.Type.CAT, command.getType());
+    assertEquals("/db1/table1", command.getPath());
+  }
+
+  @Test
+  public void parsePastePaths() {
+    FilesystemCommand command =
+        FilesystemCommandParser.parse("paste /db1/table1/tag1 /db1/table1/s1");
+
+    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));
+  }
+
+  @Test
+  public void parseTreeDepthBeforePath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("tree -L 2 
/root/sg");
+
+    assertEquals(FilesystemCommand.Type.TREE, command.getType());
+    assertEquals("/root/sg", command.getPath());
+    assertEquals(2, command.getDepth());
+  }
+
+  @Test
+  public void parseTreeDepthAfterPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("tree /root/sg 
-L 3");
+
+    assertEquals(FilesystemCommand.Type.TREE, command.getType());
+    assertEquals("/root/sg", command.getPath());
+    assertEquals(3, command.getDepth());
+  }
+
+  @Test
+  public void parseSqlPreservesStatementBody() {
+    FilesystemCommand command =
+        FilesystemCommandParser.parse("sql SELECT * FROM root.sg.d1 WHERE s1 > 
1");
+
+    assertEquals(FilesystemCommand.Type.SQL, command.getType());
+    assertEquals("SELECT * FROM root.sg.d1 WHERE s1 > 1", 
command.getStatement());
+  }
+
+  @Test
+  public void parseInvalidCommand() {
+    FilesystemCommand command = FilesystemCommandParser.parse("unknown /root");
+
+    assertEquals(FilesystemCommand.Type.INVALID, command.getType());
+    assertFalse(command.getErrorMessage().isEmpty());
+  }
+
+  @Test
+  public void parseInvalidTreeDepth() {
+    FilesystemCommand command = FilesystemCommandParser.parse("tree -L bad 
/root");
+
+    assertEquals(FilesystemCommand.Type.INVALID, command.getType());
+    assertTrue(command.getErrorMessage().contains("depth"));
+  }
+}
diff --git 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/path/FsPathTest.java 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/path/FsPathTest.java
new file mode 100644
index 00000000000..763cdd3f277
--- /dev/null
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/path/FsPathTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.path;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class FsPathTest {
+
+  @Test
+  public void normalizeAbsolutePath() {
+    FsPath path = FsPath.absolute("/root//sg/./d1/../d2/");
+
+    assertEquals("/root/sg/d2", path.toString());
+    assertEquals(Arrays.asList("root", "sg", "d2"), path.getSegments());
+    assertEquals("d2", path.getFileName());
+  }
+
+  @Test
+  public void normalizeEmptyPathAsRoot() {
+    FsPath path = FsPath.absolute("");
+
+    assertTrue(path.isRoot());
+    assertEquals("/", path.toString());
+    assertEquals(Collections.emptyList(), path.getSegments());
+  }
+
+  @Test
+  public void resolveRelativePathAgainstBase() {
+    FsPath base = FsPath.absolute("/root/sg/d1");
+
+    assertEquals("/root/sg/d2/s1", base.resolve("../d2/s1").toString());
+  }
+
+  @Test
+  public void resolveAbsolutePathIgnoresBase() {
+    FsPath base = FsPath.absolute("/root/sg/d1");
+
+    assertEquals("/root/other", base.resolve("/root/other").toString());
+  }
+
+  @Test
+  public void cannotMoveAboveRoot() {
+    FsPath path = FsPath.absolute("/root").resolve("../../..");
+
+    assertEquals("/", path.toString());
+    assertTrue(path.isRoot());
+  }
+}
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
new file mode 100644
index 00000000000..d846c3b7be8
--- /dev/null
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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.node.FsNode;
+import org.apache.iotdb.cli.fs.node.FsNodeType;
+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.junit.Before;
+import org.junit.Test;
+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.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TableFilesystemSchemaProviderTest {
+
+  @Mock private SqlExecutor executor;
+
+  private TableFilesystemSchemaProvider provider;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    provider = new TableFilesystemSchemaProvider(executor);
+  }
+
+  @Test
+  public void listRootReturnsDatabases() throws SQLException {
+    when(executor.query("SHOW DATABASES"))
+        .thenReturn(SqlRow.list(SqlRow.of("Database", "db1"), 
SqlRow.of("Database", "db2")));
+
+    List<FsNode> children = provider.list(FsPath.absolute("/"));
+
+    assertEquals(2, children.size());
+    assertEquals("/db1", children.get(0).getPath().toString());
+    assertEquals(FsNodeType.TABLE_DATABASE, children.get(0).getType());
+    assertEquals("/db2", children.get(1).getPath().toString());
+    verify(executor).query("SHOW DATABASES");
+  }
+
+  @Test
+  public void listDatabaseReturnsTables() throws SQLException {
+    when(executor.query("SHOW TABLES FROM db1"))
+        .thenReturn(SqlRow.list(SqlRow.of("TableName", "table1"), 
SqlRow.of("TableName", "view1")));
+
+    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());
+    verify(executor).query("SHOW TABLES FROM db1");
+  }
+
+  @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")));
+
+    List<FsNode> children = provider.list(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");
+  }
+
+  @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")));
+
+    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");
+  }
+
+  @Test
+  public void describeVirtualRootReturnsDirectoryNode() throws SQLException {
+    FsNode node = provider.describe(FsPath.absolute("/"));
+
+    assertEquals("/", node.getName());
+    assertEquals("/", node.getPath().toString());
+    assertEquals(FsNodeType.VIRTUAL_ROOT, node.getType());
+  }
+
+  @Test
+  public void describeDatabaseReturnsDirectoryNode() throws SQLException {
+    when(executor.query("SHOW 
DATABASES")).thenReturn(SqlRow.list(SqlRow.of("Database", "db1")));
+
+    FsNode node = provider.describe(FsPath.absolute("/db1"));
+
+    assertEquals("db1", node.getName());
+    assertEquals("/db1", node.getPath().toString());
+    assertEquals(FsNodeType.TABLE_DATABASE, node.getType());
+    verify(executor).query("SHOW DATABASES");
+  }
+
+  @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");
+  }
+
+  @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")));
+
+    List<SqlRow> rows =
+        provider.read(
+            Arrays.asList(FsPath.absolute("/db1/table1/tag1"), 
FsPath.absolute("/db1/table1/s1")),
+            5);
+
+    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");
+  }
+}
diff --git 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProviderTest.java
 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProviderTest.java
new file mode 100644
index 00000000000..5a2db8ef616
--- /dev/null
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProviderTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.node.FsNode;
+import org.apache.iotdb.cli.fs.node.FsNodeType;
+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.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TreeFilesystemSchemaProviderTest {
+
+  @Mock private SqlExecutor executor;
+
+  private TreeFilesystemSchemaProvider provider;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    provider = new TreeFilesystemSchemaProvider(executor);
+  }
+
+  @Test
+  public void listRootDiscoversTreeRootFromDatabases() throws SQLException {
+    when(executor.query("SHOW DATABASES"))
+        .thenReturn(
+            SqlRow.list(SqlRow.of("Database", "root.sg"), 
SqlRow.of("Database", "root.ln")));
+
+    List<FsNode> children = provider.list(FsPath.absolute("/"));
+
+    assertEquals(1, children.size());
+    assertEquals("root", children.get(0).getName());
+    assertEquals("/root", children.get(0).getPath().toString());
+    assertEquals(FsNodeType.TREE_ROOT, children.get(0).getType());
+    verify(executor).query("SHOW DATABASES");
+  }
+
+  @Test
+  public void listTreeRootReturnsDatabases() throws SQLException {
+    when(executor.query("SHOW DATABASES"))
+        .thenReturn(
+            SqlRow.list(SqlRow.of("Database", "root.sg"), 
SqlRow.of("Database", "root.ln")));
+
+    List<FsNode> children = provider.list(FsPath.absolute("/root"));
+
+    assertEquals(2, children.size());
+    assertEquals("/root/sg", children.get(0).getPath().toString());
+    assertEquals(FsNodeType.TREE_DATABASE, children.get(0).getType());
+    assertEquals("/root/ln", children.get(1).getPath().toString());
+    verify(executor).query("SHOW DATABASES");
+  }
+
+  @Test
+  public void listInternalTreePathReturnsChildren() throws SQLException {
+    when(executor.query("SHOW CHILD PATHS root.sg"))
+        .thenReturn(
+            SqlRow.list(
+                SqlRow.of("ChildPaths", "root.sg.d1"), SqlRow.of("ChildPaths", 
"root.sg.d2")));
+
+    List<FsNode> children = provider.list(FsPath.absolute("/root/sg"));
+
+    assertEquals(2, children.size());
+    assertEquals("d1", children.get(0).getName());
+    assertEquals("/root/sg/d1", children.get(0).getPath().toString());
+    assertEquals(FsNodeType.TREE_INTERNAL_PATH, children.get(0).getType());
+    verify(executor).query("SHOW CHILD PATHS root.sg");
+  }
+
+  @Test
+  public void describeTimeseriesReturnsMetadataNode() throws SQLException {
+    when(executor.query("SHOW TIMESERIES root.sg.d1.s1"))
+        .thenReturn(
+            SqlRow.list(
+                SqlRow.of(
+                    "Timeseries",
+                    "root.sg.d1.s1",
+                    "Alias",
+                    "",
+                    "Database",
+                    "root.sg",
+                    "DataType",
+                    "INT32")));
+
+    FsNode node = provider.describe(FsPath.absolute("/root/sg/d1/s1"));
+
+    assertEquals("s1", node.getName());
+    assertEquals("/root/sg/d1/s1", node.getPath().toString());
+    assertEquals(FsNodeType.TREE_TIMESERIES, node.getType());
+    assertEquals("INT32", node.getMetadata().get("DataType"));
+    verify(executor).query("SHOW TIMESERIES root.sg.d1.s1");
+  }
+
+  @Test
+  public void describeVirtualRootReturnsDirectoryNode() throws SQLException {
+    FsNode node = provider.describe(FsPath.absolute("/"));
+
+    assertEquals("/", node.getName());
+    assertEquals("/", node.getPath().toString());
+    assertEquals(FsNodeType.VIRTUAL_ROOT, node.getType());
+  }
+
+  @Test
+  public void describeTreeRootReturnsDirectoryNode() throws SQLException {
+    FsNode node = provider.describe(FsPath.absolute("/root"));
+
+    assertEquals("root", node.getName());
+    assertEquals("/root", node.getPath().toString());
+    assertEquals(FsNodeType.TREE_ROOT, node.getType());
+  }
+
+  @Test
+  public void describeDatabaseReturnsDirectoryNode() throws SQLException {
+    when(executor.query("SHOW DATABASES"))
+        .thenReturn(SqlRow.list(SqlRow.of("Database", "root.sg")));
+
+    FsNode node = provider.describe(FsPath.absolute("/root/sg"));
+
+    assertEquals("sg", node.getName());
+    assertEquals("/root/sg", node.getPath().toString());
+    assertEquals(FsNodeType.TREE_DATABASE, node.getType());
+    verify(executor).query("SHOW DATABASES");
+  }
+
+  @Test
+  public void readTimeseriesSelectsMeasurementFromDevice() throws SQLException 
{
+    when(executor.query("SELECT s1 FROM root.sg.d1 LIMIT 10"))
+        .thenReturn(SqlRow.list(SqlRow.of("Time", "1", "s1", "42")));
+
+    List<SqlRow> rows = provider.read(FsPath.absolute("/root/sg/d1/s1"), 10);
+
+    assertEquals(1, rows.size());
+    assertEquals("42", rows.get(0).get("s1"));
+    verify(executor).query("SELECT s1 FROM root.sg.d1 LIMIT 10");
+  }
+}
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
new file mode 100644
index 00000000000..83f073e30ea
--- /dev/null
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutorTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.sql;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.Statement;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class JdbcSqlExecutorTest {
+
+  @Mock private Connection connection;
+  @Mock private Statement statement;
+  @Mock private ResultSet resultSet;
+  @Mock private ResultSetMetaData metaData;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+  }
+
+  @Test
+  public void queryConvertsResultSetToSqlRows() throws Exception {
+    when(connection.createStatement()).thenReturn(statement);
+    when(statement.executeQuery("SHOW DATABASES")).thenReturn(resultSet);
+    when(resultSet.getMetaData()).thenReturn(metaData);
+    when(metaData.getColumnCount()).thenReturn(2);
+    when(metaData.getColumnLabel(1)).thenReturn("Database");
+    when(metaData.getColumnLabel(2)).thenReturn("TTL");
+    when(resultSet.next()).thenReturn(true, true, false);
+    when(resultSet.getString(1)).thenReturn("root.sg", "root.ln");
+    when(resultSet.getString(2)).thenReturn("INF", "INF");
+
+    List<SqlRow> rows = new JdbcSqlExecutor(connection).query("SHOW 
DATABASES");
+
+    assertEquals(2, rows.size());
+    assertEquals("root.sg", rows.get(0).get("Database"));
+    assertEquals("INF", rows.get(0).get("TTL"));
+    assertEquals("root.ln", rows.get(1).get("Database"));
+    verify(statement).executeQuery("SHOW DATABASES");
+    verify(resultSet).close();
+    verify(statement).close();
+  }
+}
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
new file mode 100644
index 00000000000..6cc0fd9dd56
--- /dev/null
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/utils/JlineUtilsTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.utils;
+
+import org.jline.reader.Candidate;
+import org.jline.reader.Completer;
+import org.jline.reader.ParsedLine;
+import org.jline.reader.impl.DefaultParser;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class JlineUtilsTest {
+
+  @Test
+  public void sqlCompleterUsesSqlKeywords() {
+    List<String> values = complete(JlineUtils.createCompleter("sql"), "SEL");
+
+    assertTrue(values.contains("SELECT"));
+    assertFalse(values.contains("ls"));
+  }
+
+  @Test
+  public void filesystemCompleterUsesFilesystemCommands() {
+    List<String> values = complete(JlineUtils.createCompleter("filesystem"), 
"c");
+
+    assertTrue(values.contains("cat"));
+    assertTrue(values.contains("cd"));
+    assertFalse(values.contains("CREATE"));
+  }
+
+  private static List<String> complete(Completer completer, String line) {
+    ParsedLine parsedLine = new DefaultParser().parse(line, line.length());
+    List<Candidate> candidates = new ArrayList<>();
+    completer.complete(null, parsedLine, candidates);
+    return 
candidates.stream().map(Candidate::value).collect(Collectors.toList());
+  }
+}

Reply via email to