This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new e74f7a74b33 branch-4.1: [fix](variant) Preserve empty key during
variant compaction (#65169)
e74f7a74b33 is described below
commit e74f7a74b33ea1ac0aa7dc11818b46b63d16c85f
Author: lihangyu <[email protected]>
AuthorDate: Fri Jul 3 20:39:36 2026 +0800
branch-4.1: [fix](variant) Preserve empty key during variant compaction
(#65169)
cherry-pick #64641
---
be/src/exec/common/variant_util.cpp | 47 +++++++-
be/test/exec/common/schema_util_test.cpp | 121 +++++++++++++++++++--
.../test_variant_empty_key_sparse_bucket.out | 10 ++
.../test_variant_empty_key_sparse_bucket.groovy | 94 ++++++++++++++++
4 files changed, 262 insertions(+), 10 deletions(-)
diff --git a/be/src/exec/common/variant_util.cpp
b/be/src/exec/common/variant_util.cpp
index 8835d97b84e..c7ca413e008 100644
--- a/be/src/exec/common/variant_util.cpp
+++ b/be/src/exec/common/variant_util.cpp
@@ -106,6 +106,44 @@
namespace doris::variant_util {
#include "common/compile_check_begin.h"
+namespace {
+
+PathInData make_full_subcolumn_path(const TabletColumnPtr& parent_column,
std::string_view path) {
+ if (!path.empty()) {
+ return PathInData(parent_column->name_lower_case() + "." +
std::string(path));
+ }
+
+ // Keep the empty JSON key as a real path part. The variant root is
`parts.empty()`;
+ // an empty key is `parts.size() == 1 && parts[0].key.empty()` after
popping root.
+ PathInDataBuilder builder;
+ return builder.append(parent_column->name_lower_case(), false).append("",
false).build();
+}
+
+void append_empty_key_subcolumn_from_stats(TabletSchema::PathsSetInfo&
paths_set_info,
+ const TabletColumnPtr&
parent_column,
+ TabletSchemaSPtr& output_schema) {
+ if (!paths_set_info.sub_path_set.contains("") ||
paths_set_info.sparse_path_set.contains("") ||
+ paths_set_info.subcolumn_indexes.contains("")) {
+ return;
+ }
+
+ auto column_name = parent_column->name_lower_case() + ".";
+ auto column_path = make_full_subcolumn_path(parent_column, "");
+
+ TabletColumn subcolumn;
+ subcolumn.set_name(column_name);
+ subcolumn.set_type(FieldType::OLAP_FIELD_TYPE_VARIANT);
+ subcolumn.set_parent_unique_id(parent_column->unique_id());
+ subcolumn.set_path_info(column_path);
+ subcolumn.set_aggregation_method(parent_column->aggregation());
+
subcolumn.set_variant_max_subcolumns_count(parent_column->variant_max_subcolumns_count());
+
subcolumn.set_variant_enable_doc_mode(parent_column->variant_enable_doc_mode());
+ subcolumn.set_is_nullable(true);
+ output_schema->append_column(subcolumn);
+}
+
+} // namespace
+
// NestedGroup's physical children and offsets are produced by
NestedGroupWriteProvider, not by
// appending TabletSchema extracted columns here. This predicate keeps only
ordinary Variant paths
// that are outside the NG tree, for example `v.owner` beside `v.items[*]`.
@@ -1040,7 +1078,8 @@ void
VariantCompactionUtil::get_compaction_subcolumns_from_subpaths(
// append subcolumns
for (const auto& subpath : sorted_subpaths) {
auto column_name = parent_column->name_lower_case() + "." +
subpath.to_string();
- auto column_path = PathInData(column_name);
+ auto column_path = make_full_subcolumn_path(parent_column,
+
std::string_view(subpath.data, subpath.size));
const auto& find_data_types =
path_to_data_types.find(PathInData(subpath));
@@ -1107,7 +1146,7 @@ void
VariantCompactionUtil::get_compaction_subcolumns_from_data_types(
DataTypePtr data_type;
get_least_supertype_jsonb(data_types, &data_type);
auto column_name = parent_column->name_lower_case() + "." +
path.get_path();
- auto column_path = PathInData(column_name);
+ auto column_path = make_full_subcolumn_path(parent_column,
path.get_path());
TabletColumn sub_column =
get_column_by_type(data_type, column_name,
ExtraInfo {.unique_id = -1,
@@ -1122,6 +1161,10 @@ void
VariantCompactionUtil::get_compaction_subcolumns_from_data_types(
VLOG_DEBUG << "append sub column " << path.get_path() << " data type "
<< data_type->get_name();
}
+ // The data-type map can contain the variant root as PathInData(), while
an empty JSON key
+ // only appears in path stats as "". If stats selected it as a
materialized path, append it
+ // with an explicit empty path part so it does not collide with root.
+ append_empty_key_subcolumn_from_stats(paths_set_info, parent_column,
output_schema);
}
// Build the temporary schema for compaction.
diff --git a/be/test/exec/common/schema_util_test.cpp
b/be/test/exec/common/schema_util_test.cpp
index 68876a22bfe..e2cfa6ff824 100644
--- a/be/test/exec/common/schema_util_test.cpp
+++ b/be/test/exec/common/schema_util_test.cpp
@@ -449,6 +449,46 @@ TEST_F(SchemaUtilTest, get_subpaths_equal_to_max) {
uid_to_paths_set_info[1].sub_path_set.end());
}
+TEST_F(SchemaUtilTest, get_subpaths_selects_empty_key_as_subpath) {
+ variant_util::PathToNoneNullValues path_stats = {
+ {"", 1000}, {"path1", 900}, {"path2", 800}, {"path3", 700}};
+
+ TabletSchema::PathsSetInfo limited_paths;
+ variant_util::VariantCompactionUtil::get_subpaths(2, path_stats,
limited_paths);
+ EXPECT_TRUE(limited_paths.sub_path_set.contains(""));
+ EXPECT_FALSE(limited_paths.sparse_path_set.contains(""));
+ EXPECT_TRUE(limited_paths.sub_path_set.contains("path1"));
+ EXPECT_TRUE(limited_paths.sparse_path_set.contains("path2"));
+ EXPECT_TRUE(limited_paths.sparse_path_set.contains("path3"));
+
+ TabletSchema::PathsSetInfo exact_limit_paths;
+ variant_util::VariantCompactionUtil::get_subpaths(4, path_stats,
exact_limit_paths);
+ EXPECT_TRUE(exact_limit_paths.sub_path_set.contains(""));
+ EXPECT_FALSE(exact_limit_paths.sparse_path_set.contains(""));
+ EXPECT_TRUE(exact_limit_paths.sub_path_set.contains("path1"));
+ EXPECT_TRUE(exact_limit_paths.sub_path_set.contains("path2"));
+ EXPECT_TRUE(exact_limit_paths.sub_path_set.contains("path3"));
+
+ TabletSchema::PathsSetInfo unlimited_paths;
+ variant_util::VariantCompactionUtil::get_subpaths(0, path_stats,
unlimited_paths);
+ EXPECT_TRUE(unlimited_paths.sub_path_set.contains(""));
+ EXPECT_TRUE(unlimited_paths.sparse_path_set.empty());
+ EXPECT_FALSE(unlimited_paths.sparse_path_set.contains(""));
+ EXPECT_TRUE(unlimited_paths.sub_path_set.contains("path1"));
+ EXPECT_TRUE(unlimited_paths.sub_path_set.contains("path2"));
+ EXPECT_TRUE(unlimited_paths.sub_path_set.contains("path3"));
+
+ variant_util::PathToNoneNullValues low_rank_empty_key_stats = {
+ {"path1", 1000}, {"path2", 900}, {"", 100}};
+ TabletSchema::PathsSetInfo low_rank_empty_key_paths;
+ variant_util::VariantCompactionUtil::get_subpaths(2,
low_rank_empty_key_stats,
+
low_rank_empty_key_paths);
+ EXPECT_FALSE(low_rank_empty_key_paths.sub_path_set.contains(""));
+ EXPECT_TRUE(low_rank_empty_key_paths.sparse_path_set.contains(""));
+ EXPECT_TRUE(low_rank_empty_key_paths.sub_path_set.contains("path1"));
+ EXPECT_TRUE(low_rank_empty_key_paths.sub_path_set.contains("path2"));
+}
+
TEST_F(SchemaUtilTest, get_subpaths_multiple_variants) {
TabletSchema schema;
TabletColumn variant1;
@@ -1522,6 +1562,7 @@ TEST_F(SchemaUtilTest, get_compaction_nested_columns) {
TEST_F(SchemaUtilTest, get_compaction_subcolumns_from_subpaths) {
TabletColumn variant;
+ variant.set_name("v1");
variant.set_unique_id(30);
variant.set_variant_max_subcolumns_count(3);
variant.set_aggregation_method(FieldAggregationMethod::OLAP_FIELD_AGGREGATION_NONE);
@@ -1532,6 +1573,7 @@ TEST_F(SchemaUtilTest,
get_compaction_subcolumns_from_subpaths) {
TabletColumnPtr parent_column = std::make_shared<TabletColumn>(variant);
TabletSchema::PathsSetInfo paths_set_info;
+ paths_set_info.sub_path_set.insert("");
paths_set_info.sub_path_set.insert("a");
paths_set_info.sub_path_set.insert("b");
doris::variant_util::PathToDataTypes path_to_data_types;
@@ -1540,10 +1582,20 @@ TEST_F(SchemaUtilTest,
get_compaction_subcolumns_from_subpaths) {
variant_util::VariantCompactionUtil::get_compaction_subcolumns_from_subpaths(
paths_set_info, parent_column, schema, path_to_data_types,
sparse_paths, output_schema);
- EXPECT_EQ(output_schema->num_columns(), 2);
+ EXPECT_EQ(output_schema->num_columns(), 3);
+ bool found_empty_key = false;
for (const auto& column : output_schema->columns()) {
+ if (column->name() == "v1.") {
+ found_empty_key = true;
+ const auto relative_path =
column->path_info_ptr()->copy_pop_front();
+ EXPECT_FALSE(relative_path.empty());
+ EXPECT_TRUE(relative_path.get_path().empty());
+ ASSERT_EQ(relative_path.get_parts().size(), 1);
+ EXPECT_TRUE(relative_path.get_parts()[0].key.empty());
+ }
EXPECT_EQ(column->type(), FieldType::OLAP_FIELD_TYPE_VARIANT);
}
+ EXPECT_TRUE(found_empty_key);
output_schema = std::make_shared<TabletSchema>();
path_to_data_types.clear();
@@ -1551,10 +1603,14 @@ TEST_F(SchemaUtilTest,
get_compaction_subcolumns_from_subpaths) {
path_to_data_types[PathInData("b")] = {std::make_shared<DataTypeString>()};
variant_util::VariantCompactionUtil::get_compaction_subcolumns_from_subpaths(
paths_set_info, parent_column, schema, path_to_data_types,
sparse_paths, output_schema);
- EXPECT_EQ(output_schema->num_columns(), 2);
+ EXPECT_EQ(output_schema->num_columns(), 3);
bool found_int = false, found_str = false;
+ found_empty_key = false;
for (const auto& column : output_schema->columns()) {
- if (column->name().ends_with("a")) {
+ if (column->name() == "v1.") {
+ found_empty_key = true;
+ EXPECT_EQ(column->type(), FieldType::OLAP_FIELD_TYPE_VARIANT);
+ } else if (column->name().ends_with("a")) {
EXPECT_EQ(column->type(), FieldType::OLAP_FIELD_TYPE_INT);
found_int = true;
} else if (column->name().ends_with("b")) {
@@ -1562,15 +1618,15 @@ TEST_F(SchemaUtilTest,
get_compaction_subcolumns_from_subpaths) {
found_str = true;
}
}
- EXPECT_TRUE(found_int && found_str);
+ EXPECT_TRUE(found_empty_key && found_int && found_str);
output_schema = std::make_shared<TabletSchema>();
sparse_paths.insert("a");
variant_util::VariantCompactionUtil::get_compaction_subcolumns_from_subpaths(
paths_set_info, parent_column, schema, path_to_data_types,
sparse_paths, output_schema);
- EXPECT_EQ(output_schema->num_columns(), 2);
+ EXPECT_EQ(output_schema->num_columns(), 3);
for (const auto& column : output_schema->columns()) {
- if (column->name().ends_with("a")) {
+ if (column->name() == "v1." || column->name().ends_with("a")) {
EXPECT_EQ(column->type(), FieldType::OLAP_FIELD_TYPE_VARIANT);
} else if (column->name().ends_with("b")) {
EXPECT_EQ(column->type(), FieldType::OLAP_FIELD_TYPE_STRING);
@@ -1585,7 +1641,7 @@ TEST_F(SchemaUtilTest,
get_compaction_subcolumns_from_subpaths) {
}
variant_util::VariantCompactionUtil::get_compaction_subcolumns_from_subpaths(
paths_set_info, parent_column, schema, path_to_data_types,
sparse_paths, output_schema);
- EXPECT_EQ(output_schema->num_columns(), 2);
+ EXPECT_EQ(output_schema->num_columns(), 3);
for (const auto& column : output_schema->columns()) {
EXPECT_EQ(column->type(), FieldType::OLAP_FIELD_TYPE_VARIANT);
}
@@ -1704,6 +1760,8 @@ TEST_F(SchemaUtilTest,
get_compaction_subcolumns_from_data_types) {
path_to_data_types[PathInData("b")] =
{std::make_shared<DataTypeString>()}; // -> STRING
path_to_data_types[PathInData("typed", true)] =
{std::make_shared<DataTypeString>()};
path_to_data_types[PathInData("shared")] =
{std::make_shared<DataTypeInt32>()};
+ path_to_data_types[PathInData("")] = {std::make_shared<DataTypeString>()};
+ path_to_data_types[PathInData()] = {std::make_shared<DataTypeString>()};
TabletSchemaSPtr output_schema = std::make_shared<TabletSchema>();
TabletSchema::PathsSetInfo paths_set_info;
@@ -1711,8 +1769,9 @@ TEST_F(SchemaUtilTest,
get_compaction_subcolumns_from_data_types) {
variant_util::VariantCompactionUtil::get_compaction_subcolumns_from_data_types(
paths_set_info, parent_column, target, path_to_data_types,
output_schema);
- EXPECT_EQ(output_schema->num_columns(), 3);
+ EXPECT_EQ(output_schema->num_columns(), 4);
bool found_a = false, found_b = false, found_typed = false, found_shared =
false;
+ int empty_key_column_count = 0;
for (const auto& col : output_schema->columns()) {
if (col->name() == "v1.a") {
found_a = true;
@@ -1737,9 +1796,19 @@ TEST_F(SchemaUtilTest,
get_compaction_subcolumns_from_data_types) {
EXPECT_EQ(col->type(), FieldType::OLAP_FIELD_TYPE_INT);
EXPECT_EQ(col->parent_unique_id(), 1);
EXPECT_EQ(col->path_info_ptr()->get_path(), "v1.shared");
+ } else if (col->name() == "v1.") {
+ ++empty_key_column_count;
+ EXPECT_EQ(col->type(), FieldType::OLAP_FIELD_TYPE_STRING);
+ EXPECT_EQ(col->parent_unique_id(), 1);
+ const auto relative_path = col->path_info_ptr()->copy_pop_front();
+ EXPECT_FALSE(relative_path.empty());
+ EXPECT_TRUE(relative_path.get_path().empty());
+ ASSERT_EQ(relative_path.get_parts().size(), 1);
+ EXPECT_TRUE(relative_path.get_parts()[0].key.empty());
}
}
EXPECT_TRUE(found_a && found_b && found_shared);
+ EXPECT_EQ(empty_key_column_count, 1);
EXPECT_FALSE(found_typed);
ASSERT_TRUE(paths_set_info.subcolumn_indexes.find("a") !=
@@ -1751,11 +1820,47 @@ TEST_F(SchemaUtilTest,
get_compaction_subcolumns_from_data_types) {
EXPECT_FALSE(paths_set_info.subcolumn_indexes.contains("typed"));
ASSERT_TRUE(paths_set_info.subcolumn_indexes.contains("shared"));
EXPECT_EQ(paths_set_info.subcolumn_indexes.at("shared").size(), 1);
+ ASSERT_TRUE(paths_set_info.subcolumn_indexes.contains(""));
+ EXPECT_EQ(paths_set_info.subcolumn_indexes.at("").size(), 1);
EXPECT_FALSE(paths_set_info.typed_path_set.contains("typed"));
EXPECT_TRUE(paths_set_info.sub_path_set.contains("a"));
EXPECT_TRUE(paths_set_info.sub_path_set.contains("b"));
EXPECT_TRUE(paths_set_info.sub_path_set.contains("shared"));
EXPECT_FALSE(paths_set_info.sub_path_set.contains("typed"));
+ EXPECT_TRUE(paths_set_info.sub_path_set.contains(""));
+ EXPECT_FALSE(paths_set_info.sparse_path_set.contains(""));
+
+ doris::variant_util::PathToDataTypes root_path_to_data_types;
+ root_path_to_data_types[PathInData()] =
{std::make_shared<DataTypeString>()};
+ TabletSchemaSPtr root_output_schema = std::make_shared<TabletSchema>();
+ TabletSchema::PathsSetInfo root_paths_set_info;
+
+
variant_util::VariantCompactionUtil::get_compaction_subcolumns_from_data_types(
+ root_paths_set_info, parent_column, target,
root_path_to_data_types,
+ root_output_schema);
+
+ EXPECT_EQ(root_output_schema->num_columns(), 0);
+ EXPECT_FALSE(root_paths_set_info.sparse_path_set.contains(""));
+ EXPECT_FALSE(root_paths_set_info.sub_path_set.contains(""));
+
+ TabletSchemaSPtr empty_key_output_schema =
std::make_shared<TabletSchema>();
+ TabletSchema::PathsSetInfo empty_key_paths_set_info;
+ empty_key_paths_set_info.sub_path_set.insert("");
+
+
variant_util::VariantCompactionUtil::get_compaction_subcolumns_from_data_types(
+ empty_key_paths_set_info, parent_column, target,
root_path_to_data_types,
+ empty_key_output_schema);
+
+ ASSERT_EQ(empty_key_output_schema->num_columns(), 1);
+ const auto& empty_key_column = empty_key_output_schema->column(0);
+ EXPECT_EQ(empty_key_column.name(), "v1.");
+ EXPECT_EQ(empty_key_column.type(), FieldType::OLAP_FIELD_TYPE_VARIANT);
+ EXPECT_EQ(empty_key_column.parent_unique_id(), 1);
+ const auto relative_path =
empty_key_column.path_info_ptr()->copy_pop_front();
+ EXPECT_FALSE(relative_path.empty());
+ EXPECT_TRUE(relative_path.get_path().empty());
+ ASSERT_EQ(relative_path.get_parts().size(), 1);
+ EXPECT_TRUE(relative_path.get_parts()[0].key.empty());
}
// Test has_different_structure_in_same_path function indirectly through
check_variant_has_no_ambiguous_paths
diff --git
a/regression-test/data/variant_p0/test_variant_empty_key_sparse_bucket.out
b/regression-test/data/variant_p0/test_variant_empty_key_sparse_bucket.out
new file mode 100644
index 00000000000..ad3bf7e402b
--- /dev/null
+++ b/regression-test/data/variant_p0/test_variant_empty_key_sparse_bucket.out
@@ -0,0 +1,10 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !empty_key_no_sparse_values --
+1 BEFORE RENAME
+2 42
+3 AFTER RENAME
+
+-- !empty_key_sparse_values --
+7 UPPER CASE
+8 16
+9 8888888
diff --git
a/regression-test/suites/variant_p0/test_variant_empty_key_sparse_bucket.groovy
b/regression-test/suites/variant_p0/test_variant_empty_key_sparse_bucket.groovy
new file mode 100644
index 00000000000..2829f4e6277
--- /dev/null
+++
b/regression-test/suites/variant_p0/test_variant_empty_key_sparse_bucket.groovy
@@ -0,0 +1,94 @@
+// 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.
+
+suite("test_variant_empty_key_sparse_bucket", "nonConcurrent") {
+ sql "SET default_variant_enable_doc_mode = false"
+ sql "SET use_v3_storage_format = false"
+ sql "SET default_variant_enable_typed_paths_to_sparse = false"
+ sql "SET default_variant_sparse_hash_shard_count = 3"
+ sql "SET enable_rewrite_element_at_to_slot = true"
+
+ sql "SET default_variant_max_subcolumns_count = 0"
+ sql "DROP TABLE IF EXISTS test_variant_empty_key_no_sparse"
+ sql """
+ CREATE TABLE test_variant_empty_key_no_sparse (
+ k bigint,
+ v variant
+ )
+ DUPLICATE KEY(`k`)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ properties("replication_num" = "1", "disable_auto_compaction" =
"true");
+ """
+
+ sql """
+ INSERT INTO test_variant_empty_key_no_sparse VALUES
+ (1, '{"hot_a": 1, "": "BEFORE RENAME"}'),
+ (2, '{"hot_a": 2, "": 42}')
+ """
+ sql "ALTER TABLE test_variant_empty_key_no_sparse RENAME COLUMN v Tags"
+ sql """
+ INSERT INTO test_variant_empty_key_no_sparse VALUES
+ (3, '{"hot_a": 3, "": "AFTER RENAME"}')
+ """
+
+ trigger_and_wait_compaction("test_variant_empty_key_no_sparse",
"cumulative")
+
+ qt_empty_key_no_sparse_values """
+ SELECT k, cast(Tags[''] as text)
+ FROM test_variant_empty_key_no_sparse
+ WHERE Tags[''] IS NOT NULL
+ ORDER BY k
+ """
+
+ sql "SET default_variant_max_subcolumns_count = 3"
+ sql "DROP TABLE IF EXISTS test_variant_empty_key_sparse_bucket"
+ sql """
+ CREATE TABLE test_variant_empty_key_sparse_bucket (
+ k bigint,
+ v variant
+ )
+ DUPLICATE KEY(`k`)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ properties("replication_num" = "1", "disable_auto_compaction" =
"true");
+ """
+
+ sql """
+ INSERT INTO test_variant_empty_key_sparse_bucket VALUES
+ (1, '{"hot_a": 1, "hot_b": 10, "hot_c": 100, "cold_1": "a"}'),
+ (2, '{"hot_a": 2, "hot_b": 20, "hot_c": 200, "cold_2": "b"}'),
+ (3, '{"hot_a": 3, "hot_b": 30, "hot_c": 300, "cold_3": "c"}'),
+ (4, '{"hot_a": 4, "hot_b": 40, "hot_c": 400, "cold_4": "d"}'),
+ (5, '{"hot_a": 5, "hot_b": 50, "hot_c": 500, "cold_5": "e"}'),
+ (6, '{"hot_a": 6, "hot_b": 60, "hot_c": 600, "cold_6": "f"}')
+ """
+ sql "ALTER TABLE test_variant_empty_key_sparse_bucket RENAME COLUMN v Tags"
+ sql """
+ INSERT INTO test_variant_empty_key_sparse_bucket VALUES
+ (7, '{"hot_a": 7, "hot_b": 70, "hot_c": 700, "": "UPPER CASE"}'),
+ (8, '{"hot_a": 8, "hot_b": 80, "hot_c": 800, "": 16}'),
+ (9, '{"hot_a": 9, "hot_b": 90, "hot_c": 900, "": 8888888}')
+ """
+
+ trigger_and_wait_compaction("test_variant_empty_key_sparse_bucket",
"cumulative")
+
+ qt_empty_key_sparse_values """
+ SELECT k, cast(Tags[''] as text)
+ FROM test_variant_empty_key_sparse_bucket
+ WHERE Tags[''] IS NOT NULL
+ ORDER BY k
+ """
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]