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
The following commit(s) were added to refs/heads/fs/inner-view by this push:
new 521463ed9bb [FS]feat: update virtual directory browsing (#18651)
521463ed9bb is described below
commit 521463ed9bb81551d339a4f9010d82f8b86e38fb
Author: Zeyu Zhang <[email protected]>
AuthorDate: Wed Sep 16 17:49:09 2026 +0800
[FS]feat: update virtual directory browsing (#18651)
* [FS] optimize virtual directory 2
* [FS] optimize tree resolver
* [FS] optimize virtual directory
* [FS] Adapt virtual directories to upstream filesystem commands
Delegate model, schema, metadata, statistics, counts, and SQL execution to
upstream providers. Remove obsolete schema sidecars from virtual listings,
localize virtual directory messages, and verify canonical and virtual command
compatibility.
---
.../apache/iotdb/cli/i18n/FsVirtualMessages.java | 58 ++++++
.../apache/iotdb/cli/i18n/FsVirtualMessages.java | 58 ++++++
.../src/main/java/org/apache/iotdb/cli/Cli.java | 13 +-
.../org/apache/iotdb/cli/fs/FilesystemShell.java | 1 +
.../org/apache/iotdb/cli/fs/node/FsNodeType.java | 1 +
.../iotdb/cli/fs/provider/TreeFilesystemSql.java | 93 +++++++++
.../ByDatabaseVirtualDirectoryResolver.java | 164 +++++++++++++++
.../TableByTableVirtualDirectoryResolver.java | 222 ++++++++++++++++++++
.../TreeByAttributeVirtualDirectoryResolver.java} | 35 ++--
.../TreeByMeasurementVirtualDirectoryResolver.java | 172 ++++++++++++++++
.../TreeByMetadataVirtualDirectoryResolver.java | 228 +++++++++++++++++++++
.../TreeByTagVirtualDirectoryResolver.java} | 33 +--
.../cli/fs/virtualdir/TreeTimeseriesSupport.java | 103 ++++++++++
.../cli/fs/virtualdir/VirtualDirectoryNodes.java | 87 ++++++++
.../cli/fs/virtualdir/VirtualDirectoryPaths.java | 89 ++++++++
.../fs/virtualdir/VirtualDirectoryResolver.java | 67 ++++++
.../fs/virtualdir/VirtualDirectoryResolvers.java | 49 +++++
.../virtualdir/VirtualDirectorySchemaProvider.java | 205 ++++++++++++++++++
.../fs/virtualdir/VirtualDirectorySegments.java | 98 +++++++++
.../TableVirtualDirectoryResolverTest.java | 121 +++++++++++
.../TreeVirtualDirectoryResolverTest.java | 186 +++++++++++++++++
.../VirtualDirectoryCompatibilityTest.java | 163 +++++++++++++++
.../VirtualDirectorySchemaProviderTest.java | 141 +++++++++++++
23 files changed, 2359 insertions(+), 28 deletions(-)
diff --git
a/iotdb-client/cli/src/main/i18n/en/org/apache/iotdb/cli/i18n/FsVirtualMessages.java
b/iotdb-client/cli/src/main/i18n/en/org/apache/iotdb/cli/i18n/FsVirtualMessages.java
new file mode 100644
index 00000000000..9654f2a34e3
--- /dev/null
+++
b/iotdb-client/cli/src/main/i18n/en/org/apache/iotdb/cli/i18n/FsVirtualMessages.java
@@ -0,0 +1,58 @@
+/*
+ * 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.i18n;
+
+public final class FsVirtualMessages {
+
+ private FsVirtualMessages() {}
+
+ public static final String EXCEPTION_PATH_IS_NOT_READABLE_ARG_4B338AD7 =
+ "Path is not readable: %s";
+
+ public static final String
EXCEPTION_PATH_IS_NOT_READABLE_AS_TEXT_ARG_5E500867 =
+ "Path is not readable as text: %s";
+
+ public static final String EXCEPTION_PATH_DOES_NOT_SUPPORT_TAIL_ARG_86CF3B99
=
+ "Path does not support tail: %s";
+
+ public static final String
EXCEPTION_PATH_DOES_NOT_SUPPORT_COUNT_ARG_83DCE591 =
+ "Path does not support count: %s";
+
+ public static final String
EXCEPTION_INVALID_VIRTUAL_PATH_SEGMENT_ARG_639C0A91 =
+ "Invalid virtual path segment: %s";
+
+ public static final String
EXCEPTION_MULTIPLE_PATHS_ARE_NOT_READABLE_WHEN_VIRTUAL_PATHS_ARE_INCLUDED_8EB63307
=
+ "Multiple paths are not readable when virtual paths are included";
+
+ public static final String
MESSAGE_BROWSE_CANONICAL_OBJECTS_GROUPED_BY_DATABASE_D3614B5A =
+ "Browse canonical objects grouped by database";
+
+ public static final String
MESSAGE_BROWSE_TABLE_MODEL_FILES_GROUPED_BY_TABLE_NAME_50CA8C5E =
+ "Browse table-model files grouped by table name";
+
+ public static final String
MESSAGE_BROWSE_TREE_TIMESERIES_GROUPED_BY_MEASUREMENT_NAME_C96857DD =
+ "Browse tree timeseries grouped by measurement name";
+
+ public static final String
MESSAGE_BROWSE_TREE_TIMESERIES_GROUPED_BY_TAG_KEY_AND_VALUE_5C5893DA =
+ "Browse tree timeseries grouped by tag key and value";
+
+ public static final String
MESSAGE_BROWSE_TREE_TIMESERIES_GROUPED_BY_ATTRIBUTE_KEY_AND_VALUE_80D0EB64 =
+ "Browse tree timeseries grouped by attribute key and value";
+}
diff --git
a/iotdb-client/cli/src/main/i18n/zh/org/apache/iotdb/cli/i18n/FsVirtualMessages.java
b/iotdb-client/cli/src/main/i18n/zh/org/apache/iotdb/cli/i18n/FsVirtualMessages.java
new file mode 100644
index 00000000000..ed242773f54
--- /dev/null
+++
b/iotdb-client/cli/src/main/i18n/zh/org/apache/iotdb/cli/i18n/FsVirtualMessages.java
@@ -0,0 +1,58 @@
+/*
+ * 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.i18n;
+
+public final class FsVirtualMessages {
+
+ private FsVirtualMessages() {}
+
+ public static final String EXCEPTION_PATH_IS_NOT_READABLE_ARG_4B338AD7 =
+ "路径不可读:%s";
+
+ public static final String
EXCEPTION_PATH_IS_NOT_READABLE_AS_TEXT_ARG_5E500867 =
+ "路径无法作为文本读取:%s";
+
+ public static final String EXCEPTION_PATH_DOES_NOT_SUPPORT_TAIL_ARG_86CF3B99
=
+ "路径不支持 tail:%s";
+
+ public static final String
EXCEPTION_PATH_DOES_NOT_SUPPORT_COUNT_ARG_83DCE591 =
+ "路径不支持 count:%s";
+
+ public static final String
EXCEPTION_INVALID_VIRTUAL_PATH_SEGMENT_ARG_639C0A91 =
+ "无效的虚拟路径段:%s";
+
+ public static final String
EXCEPTION_MULTIPLE_PATHS_ARE_NOT_READABLE_WHEN_VIRTUAL_PATHS_ARE_INCLUDED_8EB63307
=
+ "包含虚拟路径时不支持多路径读取";
+
+ public static final String
MESSAGE_BROWSE_CANONICAL_OBJECTS_GROUPED_BY_DATABASE_D3614B5A =
+ "按数据库浏览原始对象";
+
+ public static final String
MESSAGE_BROWSE_TABLE_MODEL_FILES_GROUPED_BY_TABLE_NAME_50CA8C5E =
+ "按表名浏览表模型文件";
+
+ public static final String
MESSAGE_BROWSE_TREE_TIMESERIES_GROUPED_BY_MEASUREMENT_NAME_C96857DD =
+ "按测点名称浏览树模型时间序列";
+
+ public static final String
MESSAGE_BROWSE_TREE_TIMESERIES_GROUPED_BY_TAG_KEY_AND_VALUE_5C5893DA =
+ "按标签键和值浏览树模型时间序列";
+
+ public static final String
MESSAGE_BROWSE_TREE_TIMESERIES_GROUPED_BY_ATTRIBUTE_KEY_AND_VALUE_80D0EB64 =
+ "按属性键和值浏览树模型时间序列";
+}
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 c410cc6a364..c99fee5e83a 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
@@ -27,6 +27,8 @@ import
org.apache.iotdb.cli.fs.provider.TableFilesystemSchemaProvider;
import org.apache.iotdb.cli.fs.provider.TreeFilesystemSchemaProvider;
import org.apache.iotdb.cli.fs.provider.UnsupportedFilesystemMutationProvider;
import org.apache.iotdb.cli.fs.sql.JdbcSqlExecutor;
+import org.apache.iotdb.cli.fs.virtualdir.VirtualDirectoryResolvers;
+import org.apache.iotdb.cli.fs.virtualdir.VirtualDirectorySchemaProvider;
import org.apache.iotdb.cli.i18n.CliMessages;
import org.apache.iotdb.cli.type.ExitType;
import org.apache.iotdb.cli.utils.CliContext;
@@ -340,13 +342,20 @@ public class Cli extends AbstractCli {
static FilesystemShell createFilesystemShell(CliContext ctx, IoTDBConnection
connection) {
JdbcSqlExecutor executor = new JdbcSqlExecutor(connection);
+ FilesystemSchemaProvider baseProvider;
FilesystemSchemaProvider provider;
FilesystemMutationProvider mutationProvider;
if (Constant.TABLE_DIALECT.equalsIgnoreCase(connection.getSqlDialect())) {
- provider = new TableFilesystemSchemaProvider(executor);
+ baseProvider = new TableFilesystemSchemaProvider(executor);
+ provider =
+ new VirtualDirectorySchemaProvider(
+ baseProvider, VirtualDirectoryResolvers.forTable(executor,
baseProvider));
mutationProvider = new TableFilesystemMutationProvider(executor);
} else {
- provider = new TreeFilesystemSchemaProvider(executor);
+ baseProvider = new TreeFilesystemSchemaProvider(executor);
+ provider =
+ new VirtualDirectorySchemaProvider(
+ baseProvider, VirtualDirectoryResolvers.forTree(executor,
baseProvider));
mutationProvider = new UnsupportedFilesystemMutationProvider();
}
return new FilesystemShell(
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java
index f6669c79b8f..cdce7e2eb8f 100644
---
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java
@@ -570,6 +570,7 @@ public class FilesystemShell {
private static boolean isDirectory(FsNodeType type) {
return type == FsNodeType.VIRTUAL_ROOT
+ || type == FsNodeType.VIRTUAL_DIRECTORY
|| type == FsNodeType.TREE_ROOT
|| type == FsNodeType.TREE_DATABASE
|| type == FsNodeType.TREE_INTERNAL_PATH
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
index eb020fc055b..d06bca9596c 100644
---
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
@@ -21,6 +21,7 @@ package org.apache.iotdb.cli.fs.node;
public enum FsNodeType {
VIRTUAL_ROOT,
+ VIRTUAL_DIRECTORY,
TREE_ROOT,
TREE_DATABASE,
TREE_INTERNAL_PATH,
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSql.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSql.java
new file mode 100644
index 00000000000..8ea2352838e
--- /dev/null
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSql.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.cli.fs.provider;
+
+import org.apache.iotdb.cli.fs.path.FsPath;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class TreeFilesystemSql {
+
+ private TreeFilesystemSql() {}
+
+ public 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();
+ }
+
+ public static FsPath fromTreePath(String treePath) {
+ if (treePath == null || treePath.isEmpty()) {
+ return FsPath.absolute("/");
+ }
+ return FsPath.absolute("/" + String.join("/", splitTreePath(treePath)));
+ }
+
+ public static String measurementName(String timeseries) {
+ List<String> segments = splitTreePath(timeseries);
+ return segments.isEmpty() ? timeseries : segments.get(segments.size() - 1);
+ }
+
+ public static String parentTreePath(String treePath) {
+ List<String> segments = splitTreePath(treePath);
+ if (segments.size() <= 1) {
+ return "";
+ }
+ return String.join(".", segments.subList(0, segments.size() - 1));
+ }
+
+ public 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());
+ }
+
+ private static List<String> splitTreePath(String treePath) {
+ List<String> segments = new ArrayList<>();
+ StringBuilder current = new StringBuilder();
+ boolean quoted = false;
+ for (int i = 0; i < treePath.length(); i++) {
+ char c = treePath.charAt(i);
+ if (c == '`') {
+ quoted = !quoted;
+ current.append(c);
+ } else if (c == '.' && !quoted) {
+ segments.add(current.toString());
+ current.setLength(0);
+ } else {
+ current.append(c);
+ }
+ }
+ segments.add(current.toString());
+ return segments;
+ }
+}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/ByDatabaseVirtualDirectoryResolver.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/ByDatabaseVirtualDirectoryResolver.java
new file mode 100644
index 00000000000..16b9d326272
--- /dev/null
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/ByDatabaseVirtualDirectoryResolver.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.virtualdir;
+
+import org.apache.iotdb.cli.fs.node.FsNode;
+import org.apache.iotdb.cli.fs.path.FsPath;
+import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.sql.SqlExecutor;
+import org.apache.iotdb.cli.fs.sql.SqlRow;
+import org.apache.iotdb.cli.i18n.FsVirtualMessages;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ByDatabaseVirtualDirectoryResolver implements
VirtualDirectoryResolver {
+
+ static final String NAME = "by-database";
+
+ private final SqlExecutor executor;
+ private final FilesystemSchemaProvider delegate;
+ private final boolean treeModel;
+
+ public ByDatabaseVirtualDirectoryResolver(
+ SqlExecutor executor, FilesystemSchemaProvider delegate, boolean
treeModel) {
+ this.executor = executor;
+ this.delegate = delegate;
+ this.treeModel = treeModel;
+ }
+
+ @Override
+ public String name() {
+ return NAME;
+ }
+
+ @Override
+ public FsNode rootNode() {
+ return VirtualDirectoryNodes.root(
+ NAME,
FsVirtualMessages.MESSAGE_BROWSE_CANONICAL_OBJECTS_GROUPED_BY_DATABASE_D3614B5A);
+ }
+
+ @Override
+ public List<FsNode> list(FsPath path) throws SQLException {
+ List<String> segments = VirtualDirectoryPaths.resolverSegments(path);
+ if (segments.isEmpty()) {
+ return listDatabases();
+ }
+ List<FsNode> nodes = new ArrayList<>();
+ for (FsNode node : delegate.list(canonicalPath(segments))) {
+ nodes.add(VirtualDirectoryNodes.rewrite(node,
path.resolve(node.getName()), NAME));
+ }
+ return nodes;
+ }
+
+ @Override
+ public FsNode describe(FsPath path) throws SQLException {
+ List<String> segments = VirtualDirectoryPaths.resolverSegments(path);
+ if (segments.isEmpty()) {
+ return rootNode();
+ }
+ if (segments.size() == 1) {
+ return describeDatabase(segments.get(0));
+ }
+ FsNode node = delegate.describe(canonicalPath(segments));
+ return VirtualDirectoryNodes.rewrite(node, path, NAME);
+ }
+
+ @Override
+ public List<SqlRow> read(FsPath path, int limit) throws SQLException {
+ return delegate.read(canonicalPath(path), limit);
+ }
+
+ @Override
+ public List<String> readLines(FsPath path, int limit) throws SQLException {
+ return delegate.readLines(canonicalPath(path), limit);
+ }
+
+ @Override
+ public List<SqlRow> tail(FsPath path, int limit) throws SQLException {
+ return delegate.tail(canonicalPath(path), limit);
+ }
+
+ @Override
+ public List<String> tailLines(FsPath path, int limit) throws SQLException {
+ return delegate.tailLines(canonicalPath(path), limit);
+ }
+
+ @Override
+ public long count(FsPath path) throws SQLException {
+ return delegate.count(canonicalPath(path));
+ }
+
+ 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) {
+ continue;
+ }
+ FsPath path =
VirtualDirectoryPaths.resolverRootPath(NAME).resolve(database);
+ nodes.add(
+ VirtualDirectoryNodes.directory(
+ database, path, NAME,
canonicalDatabasePath(database).toString(), "database"));
+ }
+ return nodes;
+ }
+
+ private FsNode describeDatabase(String database) throws SQLException {
+ for (SqlRow row : executor.query("SHOW DATABASES")) {
+ if (database.equals(row.get("Database"))) {
+ FsPath path =
VirtualDirectoryPaths.resolverRootPath(NAME).resolve(database);
+ return VirtualDirectoryNodes.directory(
+ database, path, NAME, canonicalDatabasePath(database).toString(),
"database");
+ }
+ }
+ return VirtualDirectoryPaths.unknown(
+ VirtualDirectoryPaths.resolverRootPath(NAME).resolve(database));
+ }
+
+ @Override
+ public FsPath canonicalPath(FsPath path) throws SQLException {
+ List<String> segments = VirtualDirectoryPaths.resolverSegments(path);
+ if (segments.size() <= 1) {
+ throw new SQLException(
+
String.format(FsVirtualMessages.EXCEPTION_PATH_IS_NOT_READABLE_ARG_4B338AD7,
path));
+ }
+ return canonicalPath(segments);
+ }
+
+ private FsPath canonicalPath(List<String> segments) {
+ if (segments.isEmpty()) {
+ return FsPath.absolute("/");
+ }
+ StringBuilder builder = new
StringBuilder(canonicalDatabasePath(segments.get(0)).toString());
+ for (int i = 1; i < segments.size(); i++) {
+ builder.append('/').append(segments.get(i));
+ }
+ return FsPath.absolute(builder.toString());
+ }
+
+ private FsPath canonicalDatabasePath(String database) {
+ if (treeModel) {
+ return FsPath.absolute("/" + database.replace('.', '/'));
+ }
+ return FsPath.absolute("/" + database);
+ }
+}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TableByTableVirtualDirectoryResolver.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TableByTableVirtualDirectoryResolver.java
new file mode 100644
index 00000000000..32fcc40fdef
--- /dev/null
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TableByTableVirtualDirectoryResolver.java
@@ -0,0 +1,222 @@
+/*
+ * 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.virtualdir;
+
+import org.apache.iotdb.cli.fs.node.FsNode;
+import org.apache.iotdb.cli.fs.path.FsPath;
+import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.sql.SqlExecutor;
+import org.apache.iotdb.cli.fs.sql.SqlRow;
+import org.apache.iotdb.cli.i18n.FsVirtualMessages;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+public class TableByTableVirtualDirectoryResolver implements
VirtualDirectoryResolver {
+
+ static final String NAME = "by-table";
+
+ private final SqlExecutor executor;
+ private final FilesystemSchemaProvider delegate;
+
+ public TableByTableVirtualDirectoryResolver(
+ SqlExecutor executor, FilesystemSchemaProvider delegate) {
+ this.executor = executor;
+ this.delegate = delegate;
+ }
+
+ @Override
+ public String name() {
+ return NAME;
+ }
+
+ @Override
+ public FsNode rootNode() {
+ return VirtualDirectoryNodes.root(
+ NAME,
FsVirtualMessages.MESSAGE_BROWSE_TABLE_MODEL_FILES_GROUPED_BY_TABLE_NAME_50CA8C5E);
+ }
+
+ @Override
+ public List<FsNode> list(FsPath path) throws SQLException {
+ List<String> segments = VirtualDirectoryPaths.resolverSegments(path);
+ if (segments.isEmpty()) {
+ return listTableDirectories();
+ }
+ if (segments.size() == 1) {
+ return listDatabaseDirectories(path, segments.get(0));
+ }
+ if (segments.size() == 2) {
+ return listTableFiles(path, segments.get(0), segments.get(1));
+ }
+ return new ArrayList<>();
+ }
+
+ @Override
+ public FsNode describe(FsPath path) throws SQLException {
+ List<String> segments = VirtualDirectoryPaths.resolverSegments(path);
+ if (segments.isEmpty()) {
+ return rootNode();
+ }
+ if (segments.size() == 1) {
+ return tableExistsInAnyDatabase(segments.get(0))
+ ? VirtualDirectoryNodes.directory(segments.get(0), path, NAME, "",
"table")
+ : VirtualDirectoryPaths.unknown(path);
+ }
+ if (segments.size() == 2) {
+ return tableExists(segments.get(1), segments.get(0))
+ ? VirtualDirectoryNodes.directory(segments.get(1), path, NAME, "",
"database")
+ : VirtualDirectoryPaths.unknown(path);
+ }
+ if (segments.size() == 3) {
+ return VirtualDirectoryNodes.rewrite(
+ delegate.describe(canonicalTableFilePath(segments)), path, NAME);
+ }
+ return VirtualDirectoryPaths.unknown(path);
+ }
+
+ @Override
+ public List<SqlRow> read(FsPath path, int limit) throws SQLException {
+ return delegate.read(canonicalPath(path), limit);
+ }
+
+ @Override
+ public List<String> readLines(FsPath path, int limit) throws SQLException {
+ return delegate.readLines(canonicalPath(path), limit);
+ }
+
+ @Override
+ public List<SqlRow> tail(FsPath path, int limit) throws SQLException {
+ return delegate.tail(canonicalPath(path), limit);
+ }
+
+ @Override
+ public List<String> tailLines(FsPath path, int limit) throws SQLException {
+ return delegate.tailLines(canonicalPath(path), limit);
+ }
+
+ @Override
+ public long count(FsPath path) throws SQLException {
+ return delegate.count(canonicalPath(path));
+ }
+
+ private List<FsNode> listTableDirectories() throws SQLException {
+ Set<String> tables = new LinkedHashSet<>();
+ for (String database : databases()) {
+ tables.addAll(tables(database));
+ }
+ List<FsNode> nodes = new ArrayList<>();
+ for (String table : tables) {
+ nodes.add(
+ VirtualDirectoryNodes.directory(
+ table,
+ VirtualDirectoryPaths.resolverRootPath(NAME).resolve(table),
+ NAME,
+ "",
+ "table"));
+ }
+ return nodes;
+ }
+
+ private List<FsNode> listDatabaseDirectories(FsPath path, String table)
throws SQLException {
+ List<FsNode> nodes = new ArrayList<>();
+ for (String database : databases()) {
+ if (!tables(database).contains(table)) {
+ continue;
+ }
+ nodes.add(
+ VirtualDirectoryNodes.directory(database, path.resolve(database),
NAME, "", "database"));
+ }
+ return nodes;
+ }
+
+ private List<FsNode> listTableFiles(FsPath path, String table, String
database)
+ throws SQLException {
+ List<FsNode> nodes = new ArrayList<>();
+ if (!tableExists(database, table)) {
+ return nodes;
+ }
+ String[] fileNames = {table + ".csv", table + ".meta"};
+ for (String fileName : fileNames) {
+ FsPath canonicalPath = FsPath.absolute("/" + database + "/" + fileName);
+ FsNode node = delegate.describe(canonicalPath);
+ nodes.add(VirtualDirectoryNodes.rewrite(node, path.resolve(fileName),
NAME));
+ }
+ return nodes;
+ }
+
+ private boolean tableExistsInAnyDatabase(String table) throws SQLException {
+ for (String database : databases()) {
+ if (tables(database).contains(table)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean tableExists(String database, String table) throws
SQLException {
+ return tables(database).contains(table);
+ }
+
+ private List<String> databases() throws SQLException {
+ List<String> databases = new ArrayList<>();
+ for (SqlRow row : executor.query("SHOW DATABASES")) {
+ String database = row.get("Database");
+ if (database != null) {
+ databases.add(database);
+ }
+ }
+ return databases;
+ }
+
+ private List<String> tables(String database) throws SQLException {
+ List<String> tables = new ArrayList<>();
+ for (SqlRow row : executor.query("SHOW TABLES FROM " +
identifier(database))) {
+ String table = row.get("TableName");
+ if (table != null) {
+ tables.add(table);
+ }
+ }
+ return tables;
+ }
+
+ @Override
+ public FsPath canonicalPath(FsPath path) throws SQLException {
+ List<String> segments = VirtualDirectoryPaths.resolverSegments(path);
+ if (segments.size() != 3) {
+ throw new SQLException(
+
String.format(FsVirtualMessages.EXCEPTION_PATH_IS_NOT_READABLE_ARG_4B338AD7,
path));
+ }
+ return canonicalTableFilePath(segments);
+ }
+
+ private static FsPath canonicalTableFilePath(List<String> segments) {
+ return FsPath.absolute("/" + segments.get(1) + "/" + segments.get(2));
+ }
+
+ private static String identifier(String value) {
+ if (value.matches("[A-Za-z_][A-Za-z0-9_]*")) {
+ return value;
+ }
+ return "\"" + value.replace("\"", "\"\"") + "\"";
+ }
+}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeByAttributeVirtualDirectoryResolver.java
similarity index 52%
copy from
iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
copy to
iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeByAttributeVirtualDirectoryResolver.java
index eb020fc055b..4dc8fdb8b3b 100644
---
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeByAttributeVirtualDirectoryResolver.java
@@ -17,18 +17,27 @@
* under the License.
*/
-package org.apache.iotdb.cli.fs.node;
+package org.apache.iotdb.cli.fs.virtualdir;
-public enum FsNodeType {
- VIRTUAL_ROOT,
- TREE_ROOT,
- TREE_DATABASE,
- TREE_INTERNAL_PATH,
- TREE_DEVICE,
- TREE_TIMESERIES,
- TABLE_DATABASE,
- TABLE_VIEW,
- TABLE_DATA_FILE,
- TABLE_META_FILE,
- UNKNOWN
+import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.sql.SqlExecutor;
+import org.apache.iotdb.cli.i18n.FsVirtualMessages;
+
+public class TreeByAttributeVirtualDirectoryResolver
+ extends TreeByMetadataVirtualDirectoryResolver {
+
+ static final String NAME = "by-attribute";
+
+ public TreeByAttributeVirtualDirectoryResolver(
+ SqlExecutor executor, FilesystemSchemaProvider delegate) {
+ super(
+ NAME,
+ "Attributes",
+ FsVirtualMessages
+
.MESSAGE_BROWSE_TREE_TIMESERIES_GROUPED_BY_ATTRIBUTE_KEY_AND_VALUE_80D0EB64,
+ "attribute-key",
+ "attribute-value",
+ executor,
+ delegate);
+ }
}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeByMeasurementVirtualDirectoryResolver.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeByMeasurementVirtualDirectoryResolver.java
new file mode 100644
index 00000000000..649b797ae83
--- /dev/null
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeByMeasurementVirtualDirectoryResolver.java
@@ -0,0 +1,172 @@
+/*
+ * 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.virtualdir;
+
+import org.apache.iotdb.cli.fs.node.FsNode;
+import org.apache.iotdb.cli.fs.path.FsPath;
+import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.sql.SqlExecutor;
+import org.apache.iotdb.cli.fs.sql.SqlRow;
+import org.apache.iotdb.cli.i18n.FsVirtualMessages;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class TreeByMeasurementVirtualDirectoryResolver implements
VirtualDirectoryResolver {
+
+ static final String NAME = "by-measurement";
+
+ private final SqlExecutor executor;
+ private final FilesystemSchemaProvider delegate;
+
+ public TreeByMeasurementVirtualDirectoryResolver(
+ SqlExecutor executor, FilesystemSchemaProvider delegate) {
+ this.executor = executor;
+ this.delegate = delegate;
+ }
+
+ @Override
+ public String name() {
+ return NAME;
+ }
+
+ @Override
+ public FsNode rootNode() {
+ return VirtualDirectoryNodes.root(
+ NAME,
+
FsVirtualMessages.MESSAGE_BROWSE_TREE_TIMESERIES_GROUPED_BY_MEASUREMENT_NAME_C96857DD);
+ }
+
+ @Override
+ public List<FsNode> list(FsPath path) throws SQLException {
+ List<String> segments = VirtualDirectoryPaths.resolverSegments(path);
+ if (segments.isEmpty()) {
+ return listMeasurementDirectories();
+ }
+ if (segments.size() == 1) {
+ return listTimeseriesFiles(
+ path, "SHOW TIMESERIES root.**." +
VirtualDirectorySegments.decode(segments.get(0)));
+ }
+ return new ArrayList<>();
+ }
+
+ @Override
+ public FsNode describe(FsPath path) throws SQLException {
+ List<String> segments = VirtualDirectoryPaths.resolverSegments(path);
+ if (segments.isEmpty()) {
+ return rootNode();
+ }
+ if (segments.size() == 1) {
+ String measurement = VirtualDirectorySegments.decode(segments.get(0));
+ return VirtualDirectoryNodes.directory(
+ path.getFileName(),
+ path,
+ NAME,
+ "",
+ "measurement",
+ TreeTimeseriesSupport.displayMetadata("measurement", measurement));
+ }
+ if (segments.size() == 2) {
+ String timeseries = VirtualDirectorySegments.decode(segments.get(1));
+ return VirtualDirectoryNodes.rewrite(
+
delegate.describe(TreeTimeseriesSupport.canonicalTimeseriesPath(timeseries)),
+ path.getFileName(),
+ path,
+ NAME,
+ TreeTimeseriesSupport.displayMetadata("timeseries", timeseries));
+ }
+ return VirtualDirectoryPaths.unknown(path);
+ }
+
+ @Override
+ public List<SqlRow> read(FsPath path, int limit) throws SQLException {
+ return delegate.read(canonicalPath(path), limit);
+ }
+
+ @Override
+ public List<SqlRow> tail(FsPath path, int limit) throws SQLException {
+ return delegate.tail(canonicalPath(path), limit);
+ }
+
+ @Override
+ public long count(FsPath path) throws SQLException {
+ return delegate.count(canonicalPath(path));
+ }
+
+ private List<FsNode> listMeasurementDirectories() throws SQLException {
+ Set<String> measurements = new LinkedHashSet<>();
+ for (SqlRow row : TreeTimeseriesSupport.query(executor, "SHOW TIMESERIES
root.**")) {
+ String timeseries = TreeTimeseriesSupport.timeseriesPath(row);
+ if (timeseries != null) {
+ measurements.add(TreeTimeseriesSupport.measurementName(timeseries));
+ }
+ }
+ List<FsNode> nodes = new ArrayList<>();
+ for (String measurement : measurements) {
+ String encodedMeasurement = VirtualDirectorySegments.encode(measurement);
+ nodes.add(
+ VirtualDirectoryNodes.directory(
+ encodedMeasurement,
+
VirtualDirectoryPaths.resolverRootPath(NAME).resolve(encodedMeasurement),
+ NAME,
+ "",
+ "measurement",
+ TreeTimeseriesSupport.displayMetadata("measurement",
measurement)));
+ }
+ return nodes;
+ }
+
+ private List<FsNode> listTimeseriesFiles(FsPath path, String sql) throws
SQLException {
+ List<FsNode> nodes = new ArrayList<>();
+ for (SqlRow row : TreeTimeseriesSupport.query(executor, sql)) {
+ String timeseries = TreeTimeseriesSupport.timeseriesPath(row);
+ if (timeseries == null) {
+ continue;
+ }
+ String encodedTimeseries = VirtualDirectorySegments.encode(timeseries);
+ Map<String, String> metadata = new LinkedHashMap<>();
+ metadata.put("timeseries", timeseries);
+ nodes.add(
+ VirtualDirectoryNodes.rewrite(
+ TreeTimeseriesSupport.timeseriesNode(row),
+ encodedTimeseries,
+ path.resolve(encodedTimeseries),
+ NAME,
+ metadata));
+ }
+ return nodes;
+ }
+
+ @Override
+ public FsPath canonicalPath(FsPath path) throws SQLException {
+ List<String> segments = VirtualDirectoryPaths.resolverSegments(path);
+ if (segments.size() != 2) {
+ throw new SQLException(
+
String.format(FsVirtualMessages.EXCEPTION_PATH_IS_NOT_READABLE_ARG_4B338AD7,
path));
+ }
+ return TreeTimeseriesSupport.canonicalTimeseriesPath(
+ VirtualDirectorySegments.decode(segments.get(1)));
+ }
+}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeByMetadataVirtualDirectoryResolver.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeByMetadataVirtualDirectoryResolver.java
new file mode 100644
index 00000000000..2192b4fbf74
--- /dev/null
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeByMetadataVirtualDirectoryResolver.java
@@ -0,0 +1,228 @@
+/*
+ * 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.virtualdir;
+
+import org.apache.iotdb.cli.fs.node.FsNode;
+import org.apache.iotdb.cli.fs.path.FsPath;
+import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.sql.SqlExecutor;
+import org.apache.iotdb.cli.fs.sql.SqlRow;
+import org.apache.iotdb.cli.i18n.FsVirtualMessages;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+abstract class TreeByMetadataVirtualDirectoryResolver implements
VirtualDirectoryResolver {
+
+ private static final String SHOW_ALL_TIMESERIES = "SHOW TIMESERIES root.**";
+
+ private final String name;
+ private final String metadataColumn;
+ private final String description;
+ private final String keyKind;
+ private final String valueKind;
+ private final FilesystemSchemaProvider delegate;
+ private final SqlExecutor executor;
+
+ TreeByMetadataVirtualDirectoryResolver(
+ String name,
+ String metadataColumn,
+ String description,
+ String keyKind,
+ String valueKind,
+ SqlExecutor executor,
+ FilesystemSchemaProvider delegate) {
+ this.name = name;
+ this.metadataColumn = metadataColumn;
+ this.description = description;
+ this.keyKind = keyKind;
+ this.valueKind = valueKind;
+ this.executor = executor;
+ this.delegate = delegate;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public FsNode rootNode() {
+ return VirtualDirectoryNodes.root(name, description);
+ }
+
+ @Override
+ public List<FsNode> list(FsPath path) throws SQLException {
+ List<String> segments = VirtualDirectoryPaths.resolverSegments(path);
+ if (segments.isEmpty()) {
+ return listKeyDirectories();
+ }
+ if (segments.size() == 1) {
+ return listValueDirectories(path,
VirtualDirectorySegments.decode(segments.get(0)));
+ }
+ if (segments.size() == 2) {
+ return listTimeseriesFiles(
+ path,
+ VirtualDirectorySegments.decode(segments.get(0)),
+ VirtualDirectorySegments.decode(segments.get(1)));
+ }
+ return new ArrayList<>();
+ }
+
+ @Override
+ public FsNode describe(FsPath path) throws SQLException {
+ List<String> segments = VirtualDirectoryPaths.resolverSegments(path);
+ if (segments.isEmpty()) {
+ return rootNode();
+ }
+ if (segments.size() == 1) {
+ String key = VirtualDirectorySegments.decode(segments.get(0));
+ return VirtualDirectoryNodes.directory(
+ path.getFileName(),
+ path,
+ name,
+ "",
+ keyKind,
+ TreeTimeseriesSupport.displayMetadata("metadataKey", key));
+ }
+ if (segments.size() == 2) {
+ String value = VirtualDirectorySegments.decode(segments.get(1));
+ return VirtualDirectoryNodes.directory(
+ path.getFileName(),
+ path,
+ name,
+ "",
+ valueKind,
+ TreeTimeseriesSupport.displayMetadata("metadataValue", value));
+ }
+ if (segments.size() == 3) {
+ String timeseries = VirtualDirectorySegments.decode(segments.get(2));
+ return VirtualDirectoryNodes.rewrite(
+
delegate.describe(TreeTimeseriesSupport.canonicalTimeseriesPath(timeseries)),
+ path.getFileName(),
+ path,
+ name,
+ TreeTimeseriesSupport.displayMetadata("timeseries", timeseries));
+ }
+ return VirtualDirectoryPaths.unknown(path);
+ }
+
+ @Override
+ public List<SqlRow> read(FsPath path, int limit) throws SQLException {
+ return delegate.read(canonicalPath(path), limit);
+ }
+
+ @Override
+ public List<SqlRow> tail(FsPath path, int limit) throws SQLException {
+ return delegate.tail(canonicalPath(path), limit);
+ }
+
+ @Override
+ public long count(FsPath path) throws SQLException {
+ return delegate.count(canonicalPath(path));
+ }
+
+ private List<FsNode> listKeyDirectories() throws SQLException {
+ Set<String> keys = new LinkedHashSet<>();
+ for (SqlRow row : TreeTimeseriesSupport.query(executor,
SHOW_ALL_TIMESERIES)) {
+ keys.addAll(TreeTimeseriesSupport.parseMetadataMap(row,
metadataColumn).keySet());
+ }
+ List<FsNode> nodes = new ArrayList<>();
+ for (String key : keys) {
+ String encodedKey = VirtualDirectorySegments.encode(key);
+ nodes.add(
+ VirtualDirectoryNodes.directory(
+ encodedKey,
+ VirtualDirectoryPaths.resolverRootPath(name).resolve(encodedKey),
+ name,
+ "",
+ keyKind,
+ TreeTimeseriesSupport.displayMetadata("metadataKey", key)));
+ }
+ return nodes;
+ }
+
+ private List<FsNode> listValueDirectories(FsPath path, String key) throws
SQLException {
+ Set<String> values = new LinkedHashSet<>();
+ for (SqlRow row : TreeTimeseriesSupport.query(executor,
SHOW_ALL_TIMESERIES)) {
+ String value = TreeTimeseriesSupport.parseMetadataMap(row,
metadataColumn).get(key);
+ if (value != null) {
+ values.add(value);
+ }
+ }
+ List<FsNode> nodes = new ArrayList<>();
+ for (String value : values) {
+ String encodedValue = VirtualDirectorySegments.encode(value);
+ nodes.add(
+ VirtualDirectoryNodes.directory(
+ encodedValue,
+ path.resolve(encodedValue),
+ name,
+ "",
+ valueKind,
+ TreeTimeseriesSupport.displayMetadata("metadataValue", value)));
+ }
+ return nodes;
+ }
+
+ private List<FsNode> listTimeseriesFiles(FsPath path, String key, String
value)
+ throws SQLException {
+ List<FsNode> nodes = new ArrayList<>();
+ for (SqlRow row :
+ TreeTimeseriesSupport.query(
+ executor,
+ SHOW_ALL_TIMESERIES
+ + " WHERE "
+ + TreeTimeseriesSupport.quote(key)
+ + " = "
+ + TreeTimeseriesSupport.quote(value))) {
+ String timeseries = TreeTimeseriesSupport.timeseriesPath(row);
+ if (timeseries == null) {
+ continue;
+ }
+ String encodedTimeseries = VirtualDirectorySegments.encode(timeseries);
+ Map<String, String> metadata =
+ TreeTimeseriesSupport.displayMetadata("timeseries", timeseries);
+ nodes.add(
+ VirtualDirectoryNodes.rewrite(
+ TreeTimeseriesSupport.timeseriesNode(row),
+ encodedTimeseries,
+ path.resolve(encodedTimeseries),
+ name,
+ metadata));
+ }
+ return nodes;
+ }
+
+ @Override
+ public FsPath canonicalPath(FsPath path) throws SQLException {
+ List<String> segments = VirtualDirectoryPaths.resolverSegments(path);
+ if (segments.size() != 3) {
+ throw new SQLException(
+
String.format(FsVirtualMessages.EXCEPTION_PATH_IS_NOT_READABLE_ARG_4B338AD7,
path));
+ }
+ return TreeTimeseriesSupport.canonicalTimeseriesPath(
+ VirtualDirectorySegments.decode(segments.get(2)));
+ }
+}
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/virtualdir/TreeByTagVirtualDirectoryResolver.java
similarity index 54%
copy from
iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
copy to
iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeByTagVirtualDirectoryResolver.java
index eb020fc055b..cdbc85070cd 100644
---
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/node/FsNodeType.java
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeByTagVirtualDirectoryResolver.java
@@ -17,18 +17,25 @@
* under the License.
*/
-package org.apache.iotdb.cli.fs.node;
+package org.apache.iotdb.cli.fs.virtualdir;
-public enum FsNodeType {
- VIRTUAL_ROOT,
- TREE_ROOT,
- TREE_DATABASE,
- TREE_INTERNAL_PATH,
- TREE_DEVICE,
- TREE_TIMESERIES,
- TABLE_DATABASE,
- TABLE_VIEW,
- TABLE_DATA_FILE,
- TABLE_META_FILE,
- UNKNOWN
+import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.sql.SqlExecutor;
+import org.apache.iotdb.cli.i18n.FsVirtualMessages;
+
+public class TreeByTagVirtualDirectoryResolver extends
TreeByMetadataVirtualDirectoryResolver {
+
+ static final String NAME = "by-tag";
+
+ public TreeByTagVirtualDirectoryResolver(
+ SqlExecutor executor, FilesystemSchemaProvider delegate) {
+ super(
+ NAME,
+ "Tags",
+
FsVirtualMessages.MESSAGE_BROWSE_TREE_TIMESERIES_GROUPED_BY_TAG_KEY_AND_VALUE_5C5893DA,
+ "tag-key",
+ "tag-value",
+ executor,
+ delegate);
+ }
}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeTimeseriesSupport.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeTimeseriesSupport.java
new file mode 100644
index 00000000000..78411b062b0
--- /dev/null
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/TreeTimeseriesSupport.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.cli.fs.virtualdir;
+
+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.TreeFilesystemSql;
+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.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+final class TreeTimeseriesSupport {
+
+ private static final String TIMESERIES_COLUMN = "Timeseries";
+
+ private TreeTimeseriesSupport() {}
+
+ static List<SqlRow> query(SqlExecutor executor, String sql) throws
SQLException {
+ List<SqlRow> rows = executor.query(sql);
+ return rows == null ? new ArrayList<>() : rows;
+ }
+
+ static String timeseriesPath(SqlRow row) {
+ return row.get(TIMESERIES_COLUMN);
+ }
+
+ static String measurementName(String timeseries) {
+ return TreeFilesystemSql.measurementName(timeseries);
+ }
+
+ static FsPath canonicalTimeseriesPath(String timeseries) {
+ return TreeFilesystemSql.fromTreePath(timeseries);
+ }
+
+ static FsNode timeseriesNode(SqlRow row) {
+ String timeseries = timeseriesPath(row);
+ return new FsNode(
+ timeseries, canonicalTimeseriesPath(timeseries),
FsNodeType.TREE_TIMESERIES, row.asMap());
+ }
+
+ static Map<String, String> parseMetadataMap(SqlRow row, String column) {
+ Map<String, String> values = new LinkedHashMap<>();
+ String text = row.get(column);
+ if (text == null) {
+ return values;
+ }
+ String trimmed = text.trim();
+ if (trimmed.isEmpty() || "null".equalsIgnoreCase(trimmed)) {
+ return values;
+ }
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
+ trimmed = trimmed.substring(1, trimmed.length() - 1);
+ }
+ if (trimmed.trim().isEmpty()) {
+ return values;
+ }
+ for (String entry : trimmed.split(",")) {
+ int equals = entry.indexOf('=');
+ if (equals <= 0) {
+ continue;
+ }
+ String key = entry.substring(0, equals).trim();
+ String value = entry.substring(equals + 1).trim();
+ if (!key.isEmpty()) {
+ values.put(key, value);
+ }
+ }
+ return values;
+ }
+
+ static String quote(String value) {
+ return "'" + value.replace("'", "''") + "'";
+ }
+
+ static Map<String, String> displayMetadata(String key, String value) {
+ Map<String, String> metadata = new LinkedHashMap<>();
+ metadata.put(key, value);
+ return metadata;
+ }
+}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryNodes.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryNodes.java
new file mode 100644
index 00000000000..f5ea2d7c26a
--- /dev/null
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryNodes.java
@@ -0,0 +1,87 @@
+/*
+ * 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.virtualdir;
+
+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 java.util.LinkedHashMap;
+import java.util.Map;
+
+final class VirtualDirectoryNodes {
+
+ private VirtualDirectoryNodes() {}
+
+ static FsNode root(String name, String description) {
+ Map<String, String> metadata = new LinkedHashMap<>();
+ metadata.put("kind", "virtual-directory");
+ metadata.put("description", description);
+ return new FsNode(
+ name, VirtualDirectoryPaths.resolverRootPath(name),
FsNodeType.VIRTUAL_DIRECTORY, metadata);
+ }
+
+ static FsNode directory(
+ String name, FsPath path, String resolverName, String canonicalPath,
String kind) {
+ Map<String, String> metadata = new LinkedHashMap<>();
+ metadata.put("kind", kind);
+ metadata.put("virtualDirectory", resolverName);
+ if (canonicalPath != null && !canonicalPath.isEmpty()) {
+ metadata.put("canonicalPath", canonicalPath);
+ }
+ return new FsNode(name, path, FsNodeType.VIRTUAL_DIRECTORY, metadata);
+ }
+
+ static FsNode directory(
+ String name,
+ FsPath path,
+ String resolverName,
+ String canonicalPath,
+ String kind,
+ Map<String, String> extraMetadata) {
+ Map<String, String> metadata = new LinkedHashMap<>(extraMetadata);
+ metadata.put("kind", kind);
+ metadata.put("virtualDirectory", resolverName);
+ if (canonicalPath != null && !canonicalPath.isEmpty()) {
+ metadata.put("canonicalPath", canonicalPath);
+ }
+ return new FsNode(name, path, FsNodeType.VIRTUAL_DIRECTORY, metadata);
+ }
+
+ static FsNode rewrite(FsNode node, FsPath virtualPath, String resolverName) {
+ return rewrite(node, node.getName(), virtualPath, resolverName, new
LinkedHashMap<>());
+ }
+
+ static FsNode rewrite(
+ FsNode node,
+ String virtualName,
+ FsPath virtualPath,
+ String resolverName,
+ Map<String, String> extraMetadata) {
+ if (node.getType() == FsNodeType.UNKNOWN) {
+ return VirtualDirectoryPaths.unknown(virtualPath);
+ }
+ Map<String, String> metadata = new LinkedHashMap<>(node.getMetadata());
+ metadata.putAll(extraMetadata);
+ metadata.put("virtualDirectory", resolverName);
+ metadata.put("canonicalPath", node.getPath().toString());
+ return new FsNode(virtualName, virtualPath, node.getType(), metadata);
+ }
+}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryPaths.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryPaths.java
new file mode 100644
index 00000000000..02fca47f237
--- /dev/null
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryPaths.java
@@ -0,0 +1,89 @@
+/*
+ * 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.virtualdir;
+
+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 java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+final class VirtualDirectoryPaths {
+
+ static final String VIRTUAL_ROOT_NAME = ".virtual";
+ static final FsPath VIRTUAL_ROOT_PATH = FsPath.absolute("/" +
VIRTUAL_ROOT_NAME);
+
+ private VirtualDirectoryPaths() {}
+
+ static boolean isVirtualRoot(FsPath path) {
+ List<String> segments = path.getSegments();
+ return segments.size() == 1 && VIRTUAL_ROOT_NAME.equals(segments.get(0));
+ }
+
+ static boolean isVirtualPath(FsPath path) {
+ List<String> segments = path.getSegments();
+ return !segments.isEmpty() && VIRTUAL_ROOT_NAME.equals(segments.get(0));
+ }
+
+ static String resolverName(FsPath path) {
+ List<String> segments = path.getSegments();
+ return segments.size() < 2 ? "" : segments.get(1);
+ }
+
+ static List<String> resolverSegments(FsPath path) {
+ List<String> segments = path.getSegments();
+ if (segments.size() <= 2) {
+ return new ArrayList<>();
+ }
+ return new ArrayList<>(segments.subList(2, segments.size()));
+ }
+
+ static FsPath resolverRootPath(String resolverName) {
+ return FsPath.absolute("/" + VIRTUAL_ROOT_NAME + "/" + resolverName);
+ }
+
+ static FsPath virtualPath(String resolverName, List<String> segments) {
+ StringBuilder builder = new StringBuilder();
+
builder.append('/').append(VIRTUAL_ROOT_NAME).append('/').append(resolverName);
+ for (String segment : segments) {
+ builder.append('/').append(segment);
+ }
+ return FsPath.absolute(builder.toString());
+ }
+
+ static FsNode virtualRootNode() {
+ Map<String, String> metadata = new LinkedHashMap<>();
+ metadata.put("kind", "virtual-root");
+ return new FsNode(VIRTUAL_ROOT_NAME, VIRTUAL_ROOT_PATH,
FsNodeType.VIRTUAL_DIRECTORY, metadata);
+ }
+
+ static FsNode directory(String name, FsPath path, String kind) {
+ Map<String, String> metadata = new LinkedHashMap<>();
+ metadata.put("kind", kind);
+ return new FsNode(name, path, FsNodeType.VIRTUAL_DIRECTORY, metadata);
+ }
+
+ static FsNode unknown(FsPath path) {
+ return new FsNode(path.getFileName(), path, FsNodeType.UNKNOWN);
+ }
+}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryResolver.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryResolver.java
new file mode 100644
index 00000000000..6b40063ed2c
--- /dev/null
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryResolver.java
@@ -0,0 +1,67 @@
+/*
+ * 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.virtualdir;
+
+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 org.apache.iotdb.cli.i18n.FsVirtualMessages;
+
+import java.sql.SQLException;
+import java.util.List;
+
+public interface VirtualDirectoryResolver {
+
+ /** Resolve a virtual data object to the upstream provider's canonical path.
*/
+ FsPath canonicalPath(FsPath path) throws SQLException;
+
+ String name();
+
+ FsNode rootNode();
+
+ List<FsNode> list(FsPath path) throws SQLException;
+
+ FsNode describe(FsPath path) throws SQLException;
+
+ default List<SqlRow> read(FsPath path, int limit) throws SQLException {
+ throw new SQLException(
+
String.format(FsVirtualMessages.EXCEPTION_PATH_IS_NOT_READABLE_ARG_4B338AD7,
path));
+ }
+
+ default List<String> readLines(FsPath path, int limit) throws SQLException {
+ throw new SQLException(
+
String.format(FsVirtualMessages.EXCEPTION_PATH_IS_NOT_READABLE_AS_TEXT_ARG_5E500867,
path));
+ }
+
+ default List<SqlRow> tail(FsPath path, int limit) throws SQLException {
+ throw new SQLException(
+
String.format(FsVirtualMessages.EXCEPTION_PATH_DOES_NOT_SUPPORT_TAIL_ARG_86CF3B99,
path));
+ }
+
+ default List<String> tailLines(FsPath path, int limit) throws SQLException {
+ throw new SQLException(
+
String.format(FsVirtualMessages.EXCEPTION_PATH_DOES_NOT_SUPPORT_TAIL_ARG_86CF3B99,
path));
+ }
+
+ default long count(FsPath path) throws SQLException {
+ throw new SQLException(
+
String.format(FsVirtualMessages.EXCEPTION_PATH_DOES_NOT_SUPPORT_COUNT_ARG_83DCE591,
path));
+ }
+}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryResolvers.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryResolvers.java
new file mode 100644
index 00000000000..b24d2f40e6b
--- /dev/null
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryResolvers.java
@@ -0,0 +1,49 @@
+/*
+ * 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.virtualdir;
+
+import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.sql.SqlExecutor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class VirtualDirectoryResolvers {
+
+ private VirtualDirectoryResolvers() {}
+
+ public static List<VirtualDirectoryResolver> forTree(
+ SqlExecutor executor, FilesystemSchemaProvider delegate) {
+ List<VirtualDirectoryResolver> resolvers = new ArrayList<>();
+ resolvers.add(new ByDatabaseVirtualDirectoryResolver(executor, delegate,
true));
+ resolvers.add(new TreeByMeasurementVirtualDirectoryResolver(executor,
delegate));
+ resolvers.add(new TreeByTagVirtualDirectoryResolver(executor, delegate));
+ resolvers.add(new TreeByAttributeVirtualDirectoryResolver(executor,
delegate));
+ return resolvers;
+ }
+
+ public static List<VirtualDirectoryResolver> forTable(
+ SqlExecutor executor, FilesystemSchemaProvider delegate) {
+ List<VirtualDirectoryResolver> resolvers = new ArrayList<>();
+ resolvers.add(new ByDatabaseVirtualDirectoryResolver(executor, delegate,
false));
+ resolvers.add(new TableByTableVirtualDirectoryResolver(executor,
delegate));
+ return resolvers;
+ }
+}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectorySchemaProvider.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectorySchemaProvider.java
new file mode 100644
index 00000000000..5f17b12373e
--- /dev/null
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectorySchemaProvider.java
@@ -0,0 +1,205 @@
+/*
+ * 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.virtualdir;
+
+import org.apache.iotdb.cli.fs.node.FsColumn;
+import org.apache.iotdb.cli.fs.node.FsNode;
+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.i18n.FsVirtualMessages;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class VirtualDirectorySchemaProvider implements
FilesystemSchemaProvider {
+
+ private final FilesystemSchemaProvider delegate;
+ private final Map<String, VirtualDirectoryResolver> resolvers = new
LinkedHashMap<>();
+
+ public VirtualDirectorySchemaProvider(
+ FilesystemSchemaProvider delegate, List<VirtualDirectoryResolver>
resolvers) {
+ this.delegate = delegate;
+ for (VirtualDirectoryResolver resolver : resolvers) {
+ this.resolvers.put(resolver.name(), resolver);
+ }
+ }
+
+ @Override
+ public String model() {
+ return delegate.model();
+ }
+
+ @Override
+ public List<SqlRow> executeSql(String sql) throws SQLException {
+ return delegate.executeSql(sql);
+ }
+
+ @Override
+ public List<FsColumn> columns(FsPath path) throws SQLException {
+ return delegate.columns(canonicalPath(path));
+ }
+
+ @Override
+ public List<SqlRow> schema(FsPath path) throws SQLException {
+ return delegate.schema(canonicalPath(path));
+ }
+
+ @Override
+ public List<SqlRow> meta(FsPath path) throws SQLException {
+ return delegate.meta(canonicalPath(path));
+ }
+
+ @Override
+ public List<SqlRow> stats(FsPath path) throws SQLException {
+ return delegate.stats(canonicalPath(path));
+ }
+
+ @Override
+ public List<SqlRow> countRows(FsPath path) throws SQLException {
+ return delegate.countRows(canonicalPath(path));
+ }
+
+ private FsPath canonicalPath(FsPath path) throws SQLException {
+ VirtualDirectoryResolver resolver =
+ readableResolver(path,
FsVirtualMessages.EXCEPTION_PATH_IS_NOT_READABLE_ARG_4B338AD7);
+ return resolver == null ? path : resolver.canonicalPath(path);
+ }
+
+ @Override
+ public List<FsNode> list(FsPath path) throws SQLException {
+ if (path.isRoot()) {
+ return listRoot(path);
+ }
+ if (VirtualDirectoryPaths.isVirtualRoot(path)) {
+ return listVirtualRoots();
+ }
+ if (VirtualDirectoryPaths.isVirtualPath(path)) {
+ VirtualDirectoryResolver resolver = resolver(path);
+ return resolver == null ? new ArrayList<>() : resolver.list(path);
+ }
+ return delegate.list(path);
+ }
+
+ @Override
+ public FsNode describe(FsPath path) throws SQLException {
+ if (VirtualDirectoryPaths.isVirtualRoot(path)) {
+ return VirtualDirectoryPaths.virtualRootNode();
+ }
+ if (VirtualDirectoryPaths.isVirtualPath(path)) {
+ VirtualDirectoryResolver resolver = resolver(path);
+ return resolver == null ? VirtualDirectoryPaths.unknown(path) :
resolver.describe(path);
+ }
+ return delegate.describe(path);
+ }
+
+ @Override
+ public List<SqlRow> read(FsPath path, int limit) throws SQLException {
+ VirtualDirectoryResolver resolver =
+ readableResolver(path,
FsVirtualMessages.EXCEPTION_PATH_IS_NOT_READABLE_ARG_4B338AD7);
+ return resolver == null ? delegate.read(path, limit) : resolver.read(path,
limit);
+ }
+
+ @Override
+ public List<String> readLines(FsPath path, int limit) throws SQLException {
+ VirtualDirectoryResolver resolver =
+ readableResolver(
+ path,
FsVirtualMessages.EXCEPTION_PATH_IS_NOT_READABLE_AS_TEXT_ARG_5E500867);
+ return resolver == null ? delegate.readLines(path, limit) :
resolver.readLines(path, limit);
+ }
+
+ @Override
+ public List<SqlRow> tail(FsPath path, int limit) throws SQLException {
+ VirtualDirectoryResolver resolver =
+ readableResolver(path,
FsVirtualMessages.EXCEPTION_PATH_DOES_NOT_SUPPORT_TAIL_ARG_86CF3B99);
+ return resolver == null ? delegate.tail(path, limit) : resolver.tail(path,
limit);
+ }
+
+ @Override
+ public List<String> tailLines(FsPath path, int limit) throws SQLException {
+ VirtualDirectoryResolver resolver =
+ readableResolver(path,
FsVirtualMessages.EXCEPTION_PATH_DOES_NOT_SUPPORT_TAIL_ARG_86CF3B99);
+ return resolver == null ? delegate.tailLines(path, limit) :
resolver.tailLines(path, limit);
+ }
+
+ @Override
+ public long count(FsPath path) throws SQLException {
+ VirtualDirectoryResolver resolver =
+ readableResolver(
+ path,
FsVirtualMessages.EXCEPTION_PATH_DOES_NOT_SUPPORT_COUNT_ARG_83DCE591);
+ return resolver == null ? delegate.count(path) : resolver.count(path);
+ }
+
+ @Override
+ public List<SqlRow> read(List<FsPath> paths, int limit) throws SQLException {
+ if (paths.size() == 1) {
+ return read(paths.get(0), limit);
+ }
+ for (FsPath path : paths) {
+ if (VirtualDirectoryPaths.isVirtualPath(path)) {
+ throw new SQLException(
+ FsVirtualMessages
+
.EXCEPTION_MULTIPLE_PATHS_ARE_NOT_READABLE_WHEN_VIRTUAL_PATHS_ARE_INCLUDED_8EB63307);
+ }
+ }
+ return delegate.read(paths, limit);
+ }
+
+ private List<FsNode> listRoot(FsPath path) throws SQLException {
+ List<FsNode> nodes = new ArrayList<>();
+ for (FsNode node : delegate.list(path)) {
+ if (!VirtualDirectoryPaths.VIRTUAL_ROOT_NAME.equals(node.getName())) {
+ nodes.add(node);
+ }
+ }
+ nodes.add(VirtualDirectoryPaths.virtualRootNode());
+ return nodes;
+ }
+
+ private List<FsNode> listVirtualRoots() {
+ List<FsNode> nodes = new ArrayList<>();
+ for (VirtualDirectoryResolver resolver : resolvers.values()) {
+ nodes.add(resolver.rootNode());
+ }
+ return nodes;
+ }
+
+ private VirtualDirectoryResolver resolver(FsPath path) {
+ if (!VirtualDirectoryPaths.isVirtualPath(path) ||
VirtualDirectoryPaths.isVirtualRoot(path)) {
+ return null;
+ }
+ return resolvers.get(VirtualDirectoryPaths.resolverName(path));
+ }
+
+ private VirtualDirectoryResolver readableResolver(FsPath path, String
errorTemplate)
+ throws SQLException {
+ if (!VirtualDirectoryPaths.isVirtualPath(path)) {
+ return null;
+ }
+ VirtualDirectoryResolver resolver = resolver(path);
+ if (resolver == null) {
+ throw new SQLException(String.format(errorTemplate, path));
+ }
+ return resolver;
+ }
+}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectorySegments.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectorySegments.java
new file mode 100644
index 00000000000..ccd79e39bbe
--- /dev/null
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectorySegments.java
@@ -0,0 +1,98 @@
+/*
+ * 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.virtualdir;
+
+import org.apache.iotdb.cli.i18n.FsVirtualMessages;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.sql.SQLException;
+
+final class VirtualDirectorySegments {
+
+ private static final char[] HEX = "0123456789ABCDEF".toCharArray();
+
+ private VirtualDirectorySegments() {}
+
+ static String encode(String value) {
+ StringBuilder builder = new StringBuilder();
+ for (byte rawByte : value.getBytes(StandardCharsets.UTF_8)) {
+ int b = rawByte & 0xFF;
+ if (isUnreserved(b)) {
+ builder.append((char) b);
+ } else {
+ builder.append('%').append(HEX[b >>> 4]).append(HEX[b & 0x0F]);
+ }
+ }
+ return builder.toString();
+ }
+
+ static String decode(String segment) throws SQLException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ for (int i = 0; i < segment.length(); ) {
+ char c = segment.charAt(i);
+ if (c == '%') {
+ if (i + 2 >= segment.length()) {
+ throw new SQLException(
+ String.format(
+
FsVirtualMessages.EXCEPTION_INVALID_VIRTUAL_PATH_SEGMENT_ARG_639C0A91,
segment));
+ }
+ int high = hexValue(segment.charAt(i + 1));
+ int low = hexValue(segment.charAt(i + 2));
+ if (high < 0 || low < 0) {
+ throw new SQLException(
+ String.format(
+
FsVirtualMessages.EXCEPTION_INVALID_VIRTUAL_PATH_SEGMENT_ARG_639C0A91,
segment));
+ }
+ bytes.write((high << 4) + low);
+ i += 3;
+ continue;
+ }
+ int codePoint = segment.codePointAt(i);
+ byte[] rawBytes = new
String(Character.toChars(codePoint)).getBytes(StandardCharsets.UTF_8);
+ bytes.write(rawBytes, 0, rawBytes.length);
+ i += Character.charCount(codePoint);
+ }
+ return new String(bytes.toByteArray(), StandardCharsets.UTF_8);
+ }
+
+ private static boolean isUnreserved(int b) {
+ return (b >= 'A' && b <= 'Z')
+ || (b >= 'a' && b <= 'z')
+ || (b >= '0' && b <= '9')
+ || b == '-'
+ || b == '.'
+ || b == '_'
+ || b == '~';
+ }
+
+ private static int hexValue(char c) {
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ }
+ if (c >= 'A' && c <= 'F') {
+ return c - 'A' + 10;
+ }
+ if (c >= 'a' && c <= 'f') {
+ return c - 'a' + 10;
+ }
+ return -1;
+ }
+}
diff --git
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/virtualdir/TableVirtualDirectoryResolverTest.java
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/virtualdir/TableVirtualDirectoryResolverTest.java
new file mode 100644
index 00000000000..8e2680bb0e9
--- /dev/null
+++
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/virtualdir/TableVirtualDirectoryResolverTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.virtualdir;
+
+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.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 TableVirtualDirectoryResolverTest {
+
+ @Mock private SqlExecutor executor;
+ @Mock private FilesystemSchemaProvider delegate;
+
+ private ByDatabaseVirtualDirectoryResolver byDatabase;
+ private TableByTableVirtualDirectoryResolver byTable;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ byDatabase = new ByDatabaseVirtualDirectoryResolver(executor, delegate,
false);
+ byTable = new TableByTableVirtualDirectoryResolver(executor, delegate);
+ }
+
+ @Test
+ public void byDatabaseListsDatabasesAsVirtualDirectories() throws
SQLException {
+ when(executor.query("SHOW DATABASES"))
+ .thenReturn(SqlRow.list(SqlRow.of("Database", "db1"),
SqlRow.of("Database", "db2")));
+
+ List<FsNode> nodes =
byDatabase.list(FsPath.absolute("/.virtual/by-database"));
+
+ assertEquals(2, nodes.size());
+ assertEquals("db1", nodes.get(0).getName());
+ assertEquals("/db1", nodes.get(0).getMetadata().get("canonicalPath"));
+ }
+
+ @Test
+ public void byDatabaseListsCanonicalChildrenUnderVirtualPath() throws
SQLException {
+ when(delegate.list(FsPath.absolute("/db1")))
+ .thenReturn(
+ Arrays.asList(
+ new FsNode(
+ "table1.csv", FsPath.absolute("/db1/table1.csv"),
FsNodeType.TABLE_DATA_FILE)));
+
+ List<FsNode> nodes =
byDatabase.list(FsPath.absolute("/.virtual/by-database/db1"));
+
+ assertEquals(1, nodes.size());
+ assertEquals("/.virtual/by-database/db1/table1.csv",
nodes.get(0).getPath().toString());
+ assertEquals("/db1/table1.csv",
nodes.get(0).getMetadata().get("canonicalPath"));
+ }
+
+ @Test
+ public void byTableGroupsTablesBeforeDatabasesAndSidecars() throws
SQLException {
+ when(executor.query("SHOW DATABASES"))
+ .thenReturn(SqlRow.list(SqlRow.of("Database", "db1"),
SqlRow.of("Database", "db2")));
+ when(executor.query("SHOW TABLES FROM db1"))
+ .thenReturn(SqlRow.list(SqlRow.of("TableName", "t1")));
+ when(executor.query("SHOW TABLES FROM db2"))
+ .thenReturn(SqlRow.list(SqlRow.of("TableName", "t1")));
+ when(delegate.describe(FsPath.absolute("/db1/t1.csv")))
+ .thenReturn(
+ new FsNode("t1.csv", FsPath.absolute("/db1/t1.csv"),
FsNodeType.TABLE_DATA_FILE));
+ when(delegate.describe(FsPath.absolute("/db1/t1.meta")))
+ .thenReturn(
+ new FsNode("t1.meta", FsPath.absolute("/db1/t1.meta"),
FsNodeType.TABLE_META_FILE));
+
+ List<FsNode> tables = byTable.list(FsPath.absolute("/.virtual/by-table"));
+ List<FsNode> databases =
byTable.list(FsPath.absolute("/.virtual/by-table/t1"));
+ List<FsNode> files =
byTable.list(FsPath.absolute("/.virtual/by-table/t1/db1"));
+
+ assertEquals("t1", tables.get(0).getName());
+ assertEquals(2, databases.size());
+ assertEquals("db1", databases.get(0).getName());
+ assertEquals(2, files.size());
+ assertEquals("/.virtual/by-table/t1/db1/t1.csv",
files.get(0).getPath().toString());
+ assertEquals("/db1/t1.csv",
files.get(0).getMetadata().get("canonicalPath"));
+ }
+
+ @Test
+ public void byTableReadsThroughCanonicalTableFilePath() throws SQLException {
+ when(delegate.readLines(FsPath.absolute("/db1/t1.csv"), 5))
+ .thenReturn(Arrays.asList("Time,s1", "1,42"));
+
+ List<String> lines =
byTable.readLines(FsPath.absolute("/.virtual/by-table/t1/db1/t1.csv"), 5);
+
+ assertEquals("1,42", lines.get(1));
+ verify(delegate).readLines(FsPath.absolute("/db1/t1.csv"), 5);
+ }
+}
diff --git
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/virtualdir/TreeVirtualDirectoryResolverTest.java
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/virtualdir/TreeVirtualDirectoryResolverTest.java
new file mode 100644
index 00000000000..b95942ed960
--- /dev/null
+++
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/virtualdir/TreeVirtualDirectoryResolverTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.virtualdir;
+
+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.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 TreeVirtualDirectoryResolverTest {
+
+ @Mock private SqlExecutor executor;
+ @Mock private FilesystemSchemaProvider delegate;
+
+ private TreeByMeasurementVirtualDirectoryResolver byMeasurement;
+ private TreeByTagVirtualDirectoryResolver byTag;
+ private TreeByAttributeVirtualDirectoryResolver byAttribute;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ byMeasurement = new TreeByMeasurementVirtualDirectoryResolver(executor,
delegate);
+ byTag = new TreeByTagVirtualDirectoryResolver(executor, delegate);
+ byAttribute = new TreeByAttributeVirtualDirectoryResolver(executor,
delegate);
+ }
+
+ @Test
+ public void byMeasurementListsMeasurementDirectories() throws SQLException {
+ when(executor.query("SHOW TIMESERIES root.**"))
+ .thenReturn(
+ SqlRow.list(
+ SqlRow.of("Timeseries", "root.sg.d1.s1"),
+ SqlRow.of("Timeseries", "root.sg.d2.s2"),
+ SqlRow.of("Timeseries", "root.sg.d3.s1")));
+
+ List<FsNode> nodes =
byMeasurement.list(FsPath.absolute("/.virtual/by-measurement"));
+
+ assertEquals(2, nodes.size());
+ assertEquals("s1", nodes.get(0).getName());
+ assertEquals("s2", nodes.get(1).getName());
+ assertEquals(FsNodeType.VIRTUAL_DIRECTORY, nodes.get(0).getType());
+ }
+
+ @Test
+ public void byMeasurementListsTimeseriesFiles() throws SQLException {
+ when(executor.query("SHOW TIMESERIES root.**.s1"))
+ .thenReturn(SqlRow.list(SqlRow.of("Timeseries", "root.sg.d1.s1",
"DataType", "INT32")));
+
+ List<FsNode> nodes =
byMeasurement.list(FsPath.absolute("/.virtual/by-measurement/s1"));
+
+ assertEquals(1, nodes.size());
+ assertEquals("root.sg.d1.s1", nodes.get(0).getName());
+ assertEquals("/.virtual/by-measurement/s1/root.sg.d1.s1",
nodes.get(0).getPath().toString());
+ assertEquals("/root/sg/d1/s1",
nodes.get(0).getMetadata().get("canonicalPath"));
+ }
+
+ @Test
+ public void byMeasurementReadsThroughCanonicalTimeseriesPath() throws
SQLException {
+ when(delegate.read(FsPath.absolute("/root/sg/d1/s1"), 10))
+ .thenReturn(SqlRow.list(SqlRow.of("Time", "1", "s1", "42")));
+
+ List<SqlRow> rows =
+
byMeasurement.read(FsPath.absolute("/.virtual/by-measurement/s1/root.sg.d1.s1"),
10);
+
+ assertEquals("42", rows.get(0).get("s1"));
+ verify(delegate).read(FsPath.absolute("/root/sg/d1/s1"), 10);
+ }
+
+ @Test
+ public void byMeasurementEncodesMeasurementAndTimeseriesSegments() throws
SQLException {
+ when(executor.query("SHOW TIMESERIES root.**"))
+ .thenReturn(SqlRow.list(SqlRow.of("Timeseries", "root.sg.d1.`sensor
temp`")));
+ when(executor.query("SHOW TIMESERIES root.**.`sensor temp`"))
+ .thenReturn(SqlRow.list(SqlRow.of("Timeseries", "root.sg.d1.`sensor
temp`")));
+
+ List<FsNode> measurements =
byMeasurement.list(FsPath.absolute("/.virtual/by-measurement"));
+ List<FsNode> files =
+
byMeasurement.list(FsPath.absolute("/.virtual/by-measurement/%60sensor%20temp%60"));
+
+ assertEquals("%60sensor%20temp%60", measurements.get(0).getName());
+ assertEquals("`sensor temp`",
measurements.get(0).getMetadata().get("measurement"));
+ assertEquals("root.sg.d1.%60sensor%20temp%60", files.get(0).getName());
+ assertEquals("root.sg.d1.`sensor temp`",
files.get(0).getMetadata().get("timeseries"));
+ }
+
+ @Test
+ public void byTagListsTagKeysAndValues() throws SQLException {
+ when(executor.query("SHOW TIMESERIES root.**"))
+ .thenReturn(
+ SqlRow.list(
+ SqlRow.of("Timeseries", "root.sg.d1.s1", "Tags", "{unit=c,
site=A}"),
+ SqlRow.of("Timeseries", "root.sg.d2.s1", "Tags", "{unit=f}")));
+
+ List<FsNode> keys = byTag.list(FsPath.absolute("/.virtual/by-tag"));
+ List<FsNode> values = byTag.list(FsPath.absolute("/.virtual/by-tag/unit"));
+
+ assertEquals("unit", keys.get(0).getName());
+ assertEquals("site", keys.get(1).getName());
+ assertEquals("c", values.get(0).getName());
+ assertEquals("f", values.get(1).getName());
+ }
+
+ @Test
+ public void byTagListsMatchingTimeseriesFiles() throws SQLException {
+ when(executor.query("SHOW TIMESERIES root.** WHERE 'unit' = 'c'"))
+ .thenReturn(
+ SqlRow.list(
+ SqlRow.of("Timeseries", "root.sg.d1.s1", "Tags", "{unit=c}",
"DataType", "INT32")));
+
+ List<FsNode> nodes =
byTag.list(FsPath.absolute("/.virtual/by-tag/unit/c"));
+
+ assertEquals(1, nodes.size());
+ assertEquals("root.sg.d1.s1", nodes.get(0).getName());
+ assertEquals("/root/sg/d1/s1",
nodes.get(0).getMetadata().get("canonicalPath"));
+ verify(executor).query("SHOW TIMESERIES root.** WHERE 'unit' = 'c'");
+ }
+
+ @Test
+ public void byTagEncodesKeyAndValuePathSegments() throws SQLException {
+ when(executor.query("SHOW TIMESERIES root.**"))
+ .thenReturn(
+ SqlRow.list(
+ SqlRow.of("Timeseries", "root.sg.d1.s1", "Tags", "{site
zone=building/1}")));
+
+ List<FsNode> keys = byTag.list(FsPath.absolute("/.virtual/by-tag"));
+ List<FsNode> values =
byTag.list(FsPath.absolute("/.virtual/by-tag/site%20zone"));
+
+ assertEquals("site%20zone", keys.get(0).getName());
+ assertEquals("site zone", keys.get(0).getMetadata().get("metadataKey"));
+ assertEquals("building%2F1", values.get(0).getName());
+ assertEquals("building/1",
values.get(0).getMetadata().get("metadataValue"));
+ }
+
+ @Test
+ public void byAttributeListsMatchingTimeseriesFiles() throws SQLException {
+ when(executor.query("SHOW TIMESERIES root.** WHERE 'owner' = 'ops'"))
+ .thenReturn(
+ SqlRow.list(
+ SqlRow.of(
+ "Timeseries",
+ "root.sg.d1.s1",
+ "Attributes",
+ "{owner=ops}",
+ "DataType",
+ "INT32")));
+
+ List<FsNode> nodes =
byAttribute.list(FsPath.absolute("/.virtual/by-attribute/owner/ops"));
+
+ assertEquals(1, nodes.size());
+ assertEquals("root.sg.d1.s1", nodes.get(0).getName());
+ assertEquals("/root/sg/d1/s1",
nodes.get(0).getMetadata().get("canonicalPath"));
+ assertEquals("root.sg.d1.s1",
nodes.get(0).getMetadata().get("timeseries"));
+ verify(executor).query("SHOW TIMESERIES root.** WHERE 'owner' = 'ops'");
+ }
+}
diff --git
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryCompatibilityTest.java
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryCompatibilityTest.java
new file mode 100644
index 00000000000..015b7136670
--- /dev/null
+++
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectoryCompatibilityTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.virtualdir;
+
+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.SqlExecutor;
+import org.apache.iotdb.cli.fs.sql.SqlRow;
+import org.apache.iotdb.cli.type.ExitType;
+import org.apache.iotdb.cli.utils.CliContext;
+
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class VirtualDirectoryCompatibilityTest {
+
+ @Test
+ public void tableCommandsKeepUpstreamResultsForCanonicalAndVirtualPaths()
throws Exception {
+ SqlExecutor executor = mock(SqlExecutor.class);
+ when(executor.query("SHOW
DATABASES")).thenReturn(SqlRow.list(SqlRow.of("Database", "db")));
+ when(executor.query("SHOW TABLES FROM db"))
+ .thenReturn(SqlRow.list(SqlRow.of("TableName", "sensors")));
+ when(executor.query("SHOW TABLES DETAILS FROM db"))
+ .thenReturn(SqlRow.list(SqlRow.of("TableName", "sensors", "TTL(ms)",
"INF")));
+ when(executor.query("DESC db.sensors DETAILS"))
+ .thenReturn(
+ SqlRow.list(
+ SqlRow.of("ColumnName", "time", "DataType", "TIMESTAMP",
"Category", "TIME"),
+ SqlRow.of("ColumnName", "site", "DataType", "STRING",
"Category", "TAG"),
+ SqlRow.of("ColumnName", "value", "DataType", "INT64",
"Category", "FIELD")));
+ when(executor.query("SELECT * FROM db.sensors ORDER BY time ASC, site ASC
NULLS LAST"))
+ .thenReturn(SqlRow.list(SqlRow.of("time", "1", "site", "north",
"value", "42")));
+ TableFilesystemSchemaProvider upstream = new
TableFilesystemSchemaProvider(executor);
+ VirtualDirectorySchemaProvider wrapped =
+ new VirtualDirectorySchemaProvider(
+ upstream, VirtualDirectoryResolvers.forTable(executor, upstream));
+
+ for (String command :
+ Arrays.asList(
+ "cat -f csv",
+ "schema -f csv",
+ "meta -f csv",
+ "stats -f csv",
+ "count -f csv",
+ "head --start 0 -m value -f ndjson --tag-filter site eq north")) {
+ String expected = run(upstream, command + " /db/sensors.csv");
+ assertFalse(expected.isEmpty());
+ assertEquals(expected, run(wrapped, command + " /db/sensors.csv"));
+ assertEquals(expected, run(wrapped, command + "
/.virtual/by-database/db/sensors.csv"));
+ assertEquals(expected, run(wrapped, command + "
/.virtual/by-table/sensors/db/sensors.csv"));
+ }
+ String listing = run(wrapped, "ls /.virtual/by-table/sensors/db");
+ assertTrue(listing.contains("sensors.csv"));
+ assertTrue(listing.contains("sensors.meta"));
+ assertFalse(listing.contains("sensors.schema"));
+ }
+
+ @Test
+ public void treeCommandsKeepUpstreamResultsAcrossEveryVirtualResolver()
throws Exception {
+ SqlExecutor executor = mock(SqlExecutor.class);
+ when(executor.query("SHOW DATABASES"))
+ .thenReturn(SqlRow.list(SqlRow.of("Database", "root.sg")));
+ when(executor.query("SHOW TIMESERIES root.sg.d1.s1"))
+ .thenReturn(
+ SqlRow.list(
+ SqlRow.of(
+ "Timeseries",
+ "root.sg.d1.s1",
+ "DataType",
+ "INT64",
+ "Encoding",
+ "RLE",
+ "Compression",
+ "LZ4")));
+ when(executor.query("SELECT s1 FROM root.sg.d1 ORDER BY time ASC"))
+ .thenReturn(SqlRow.list(SqlRow.of("Time", "1", "root.sg.d1.s1",
"42")));
+ when(executor.query("SELECT * FROM root.sg.d1 ORDER BY time ASC"))
+ .thenReturn(SqlRow.list(SqlRow.of("Time", "1", "root.sg.d1.s1",
"42")));
+ TreeFilesystemSchemaProvider upstream = new
TreeFilesystemSchemaProvider(executor);
+ VirtualDirectorySchemaProvider wrapped =
+ new VirtualDirectorySchemaProvider(
+ upstream, VirtualDirectoryResolvers.forTree(executor, upstream));
+
+ for (String command :
+ Arrays.asList(
+ "cat -f csv",
+ "schema -f csv",
+ "meta -f csv",
+ "stats -f csv",
+ "count -f csv",
+ "head --start 0 -m s1 -f ndjson")) {
+ String expected = run(upstream, command + " /root/sg/d1/s1");
+ assertFalse(expected.isEmpty());
+ assertEquals(expected, run(wrapped, command + " /root/sg/d1/s1"));
+ for (String path :
+ Arrays.asList(
+ "by-database/root.sg/d1/s1",
+ "by-measurement/s1/root.sg.d1.s1",
+ "by-tag/unit/c/root.sg.d1.s1",
+ "by-attribute/owner/ops/root.sg.d1.s1")) {
+ assertEquals(expected, run(wrapped, command + " /.virtual/" + path));
+ }
+ }
+ }
+
+ @Test
+ public void sqlEscapeStillUsesUpstreamProvider() throws Exception {
+ SqlExecutor executor = mock(SqlExecutor.class);
+ when(executor.executeQueryOrUpdate("SHOW DATABASES"))
+ .thenReturn(SqlRow.list(SqlRow.of("Database", "db")));
+ TableFilesystemSchemaProvider upstream = new
TableFilesystemSchemaProvider(executor);
+ VirtualDirectorySchemaProvider wrapped =
+ new VirtualDirectorySchemaProvider(upstream, Collections.emptyList());
+ assertEquals(run(upstream, "sql SHOW DATABASES"), run(wrapped, "sql SHOW
DATABASES"));
+ assertEquals("table", wrapped.model());
+ }
+
+ private static String run(FilesystemSchemaProvider provider, String command)
{
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ ByteArrayOutputStream errors = new ByteArrayOutputStream();
+ FilesystemShell shell =
+ new FilesystemShell(
+ new CliContext(
+ new ByteArrayInputStream(new byte[0]),
+ new PrintStream(output),
+ new PrintStream(errors),
+ ExitType.EXCEPTION),
+ provider);
+ int status = shell.runNonInteractive(command);
+ assertEquals(command + ": " + errors.toString(), FilesystemShell.SUCCESS,
status);
+ return output.toString();
+ }
+}
diff --git
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectorySchemaProviderTest.java
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectorySchemaProviderTest.java
new file mode 100644
index 00000000000..8699d5e8361
--- /dev/null
+++
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/virtualdir/VirtualDirectorySchemaProviderTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.virtualdir;
+
+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.i18n.FsVirtualMessages;
+
+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.junit.Assert.fail;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class VirtualDirectorySchemaProviderTest {
+
+ @Mock private FilesystemSchemaProvider delegate;
+ @Mock private VirtualDirectoryResolver resolver;
+
+ private VirtualDirectorySchemaProvider provider;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(resolver.name()).thenReturn("by-test");
+ when(resolver.rootNode())
+ .thenReturn(
+ new FsNode(
+ "by-test", FsPath.absolute("/.virtual/by-test"),
FsNodeType.VIRTUAL_DIRECTORY));
+ provider = new VirtualDirectorySchemaProvider(delegate,
Arrays.asList(resolver));
+ }
+
+ @Test
+ public void listRootAddsVirtualDirectoryRoot() throws SQLException {
+ when(delegate.list(FsPath.absolute("/")))
+ .thenReturn(
+ Arrays.asList(
+ new FsNode("root", FsPath.absolute("/root"),
FsNodeType.TREE_ROOT),
+ new FsNode(".virtual", FsPath.absolute("/.virtual"),
FsNodeType.TABLE_DATABASE)));
+
+ List<FsNode> nodes = provider.list(FsPath.absolute("/"));
+
+ assertEquals(2, nodes.size());
+ assertEquals("root", nodes.get(0).getName());
+ assertEquals(".virtual", nodes.get(1).getName());
+ assertEquals(FsNodeType.VIRTUAL_DIRECTORY, nodes.get(1).getType());
+ }
+
+ @Test
+ public void listVirtualRootReturnsRegisteredResolvers() throws SQLException {
+ List<FsNode> nodes = provider.list(FsPath.absolute("/.virtual"));
+
+ assertEquals(1, nodes.size());
+ assertEquals("by-test", nodes.get(0).getName());
+ verify(resolver).rootNode();
+ }
+
+ @Test
+ public void virtualPathDispatchesToResolver() throws SQLException {
+ FsPath path = FsPath.absolute("/.virtual/by-test/value");
+ when(resolver.describe(path))
+ .thenReturn(new FsNode("value", path, FsNodeType.VIRTUAL_DIRECTORY));
+
+ FsNode node = provider.describe(path);
+
+ assertEquals("value", node.getName());
+ verify(resolver).describe(path);
+ }
+
+ @Test
+ public void virtualReadDispatchesToResolver() throws SQLException {
+ FsPath path = FsPath.absolute("/.virtual/by-test/value");
+ when(resolver.read(path, 5)).thenReturn(SqlRow.list(SqlRow.of("value",
"42")));
+
+ List<SqlRow> rows = provider.read(path, 5);
+
+ assertEquals("42", rows.get(0).get("value"));
+ verify(resolver).read(path, 5);
+ }
+
+ @Test
+ public void unknownVirtualPathDoesNotDelegateRead() throws SQLException {
+ FsPath path = FsPath.absolute("/.virtual/missing/value");
+
+ try {
+ provider.read(path, 1);
+ fail("Expected unknown virtual path to be rejected");
+ } catch (SQLException e) {
+ assertEquals(
+
String.format(FsVirtualMessages.EXCEPTION_PATH_IS_NOT_READABLE_ARG_4B338AD7,
path),
+ e.getMessage());
+ }
+ verify(delegate, never()).read(path, 1);
+ }
+
+ @Test
+ public void multipleReadRejectsVirtualPathBeforeDelegating() throws
SQLException {
+ List<FsPath> paths =
+ Arrays.asList(FsPath.absolute("/db/table.csv"),
FsPath.absolute("/.virtual/by-test/value"));
+
+ try {
+ provider.read(paths, 1);
+ fail("Expected mixed virtual multi-read to be rejected");
+ } catch (SQLException e) {
+ assertEquals(
+ FsVirtualMessages
+
.EXCEPTION_MULTIPLE_PATHS_ARE_NOT_READABLE_WHEN_VIRTUAL_PATHS_ARE_INCLUDED_8EB63307,
+ e.getMessage());
+ }
+ verify(delegate, never()).read(paths, 1);
+ }
+}