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()); + } +}
