This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 1d523fc0f0d [branch-4.2][fix](point-query) Keep unsafe projections on
the normal execution path (#70191)
1d523fc0f0d is described below
commit 1d523fc0f0dfde2b66d76fea65c93adce52249c5
Author: Jerry Hu <[email protected]>
AuthorDate: Thu Nov 5 09:35:52 2026 +0800
[branch-4.2][fix](point-query) Keep unsafe projections on the normal
execution path (#70191)
### What problem does this PR solve?
Issue Number: None
Related: #65051, #69903. This supersedes #69998 with the simplified BE
policy.
Nested-column pruning can narrow complex slot descriptors, while
short-circuit point queries materialize full row-store JSONB values.
Decoding those values with a narrowed descriptor can read the wrong
layout. Keeping the current full descriptor alone also does not
establish compatibility with historical complex-column payloads after
schema changes.
### Changes
- Decide short-circuit eligibility before nested pruning: allow only
simple scalar slots/literals (including aliases). Complex projections,
nested-field access and metadata-producing expressions use normal
planning; an unreferenced complex column does not disable a scalar point
query.
- Retain normal nested-column pruning rather than forcing complex
queries through full row-store decoding.
- Validate only the actual row-store input descriptor (tuple 0), not
auxiliary index tuples.
- Preserve the effective pruning setting across `SET_VAR` rollback,
revalidate prepared reuse, replan routing-related hints, and clear
inherited short-circuit state before fallback planning. Retain the
upstream stale prepared-plan refresh/reparse behavior.
- Simplify BE to reject every nonempty `all_access_paths` or
`predicate_access_paths` list. Do not interpret legacy/typed path
dialects, infer schema compatibility, or remap UIDs.
- Cover strict rejection, no-path payload/default decoding, conservative
FE routing, prepared execution, and schema-evolution queries.
### Behavior and compatibility boundaries
- This deliberately trades some short-circuit optimization for safety.
Complex and nontrivial projections can use normal execution even when
nested pruning is disabled. Simple scalar point queries retain their
fast path.
- Old FE requests carrying access paths are rejected, including
otherwise compatible whole-column DATA roots and scalar metadata
requests. The BE error does **not** automatically retry with normal FE
planning.
- Requests without access paths retain the existing decoder/default
behavior. This is not a general fix for complex-schema evolution from
old FE requests or producer-schema-aware row caching.
- No Thrift/protobuf or stored row-format changes.
### Validation
- Focused FE tests: **28/28 passed** (`ShortCircuitPointQueryTest`,
`ShortCircuitQueryContextTest`, `ExecuteCommandTest`).
- Fresh ASAN BE unit-test binary built successfully; focused tests:
**12/12 passed** (`PointQueryReusableTest.*`,
`LookupConnectionCacheTest.*`, `RowCacheTest.*`). The no-path test
decodes actual JSONB and checks both payload-present and default-value
cases.
- Focused clang-format 16 verification and `git diff --check` passed.
- The SQL suite and expected output are unchanged by the final BE
simplification. SQL regression, complete FE/BE server binary builds,
live mixed-version deployment and performance benchmarks were **not
rerun on the final source snapshot**; no full-CI success is claimed.
```bash
./run-fe-ut.sh --run
'org.apache.doris.nereids.rules.rewrite.ShortCircuitPointQueryTest,org.apache.doris.qe.ShortCircuitQueryContextTest,org.apache.doris.nereids.trees.plans.commands.ExecuteCommandTest'
./run-be-ut.sh -j 24 --run \
--filter='PointQueryReusableTest.*:LookupConnectionCacheTest.*:RowCacheTest.*'
clang-format --dry-run --Werror \
be/src/service/point_query_executor.cpp \
be/test/service/point_query_exector_test.cpp
git diff --check
```
---
be/src/service/point_query_executor.cpp | 11 ++
be/test/service/point_query_exector_test.cpp | 155 +++++++++++++++
.../LogicalResultSinkToShortCircuitPointQuery.java | 33 +++-
.../trees/plans/commands/ExecuteCommand.java | 18 ++
.../apache/doris/qe/ShortCircuitQueryContext.java | 41 +++-
.../java/org/apache/doris/qe/StmtExecutor.java | 3 +-
.../rules/rewrite/ShortCircuitPointQueryTest.java | 29 +++
.../trees/plans/commands/ExecuteCommandTest.java | 83 +++++++-
.../doris/qe/ShortCircuitQueryContextTest.java | 83 ++++++++
.../test_rowstore_nested_pruning_minimal.out | 68 +++++++
.../test_rowstore_nested_pruning_minimal.groovy | 208 +++++++++++++++++++++
11 files changed, 725 insertions(+), 7 deletions(-)
diff --git a/be/src/service/point_query_executor.cpp
b/be/src/service/point_query_executor.cpp
index 018afe2f389..0f83aaf6893 100644
--- a/be/src/service/point_query_executor.cpp
+++ b/be/src/service/point_query_executor.cpp
@@ -26,6 +26,7 @@
#include <google/protobuf/extension_set.h>
#include <stdlib.h>
+#include <algorithm>
#include <climits>
#include <memory>
#include <unordered_map>
@@ -136,6 +137,16 @@ Status Reusable::init(const TDescriptorTable& t_desc_tbl,
const std::vector<TExp
_runtime_state->set_query_options(query_options);
RETURN_IF_ERROR(DescriptorTbl::create(_runtime_state->obj_pool(),
t_desc_tbl, &_desc_tbl));
_runtime_state->set_desc_tbl(_desc_tbl);
+ // Point queries decode full row-store values and do not interpret access
paths.
+ // Keep old FE descriptors with access paths away from the row-store
decoder.
+ for (const auto* slot : tuple_desc()->slots()) {
+ if (!slot->all_access_paths().empty() ||
!slot->predicate_access_paths().empty()) {
+ return Status::InternalError(
+ "Short-circuit point query does not support column access
paths, "
+ "slot: {}. Please upgrade FE to use normal planning for
these queries.",
+ slot->col_name());
+ }
+ }
_block_pool.resize(block_size);
for (auto& i : _block_pool) {
i = Block::create_unique(tuple_desc()->slots(), 2);
diff --git a/be/test/service/point_query_exector_test.cpp
b/be/test/service/point_query_exector_test.cpp
index 74bd7606e39..d67961db388 100644
--- a/be/test/service/point_query_exector_test.cpp
+++ b/be/test/service/point_query_exector_test.cpp
@@ -16,13 +16,23 @@
// under the License.
#include <gen_cpp/AgentService_types.h>
+#include <gen_cpp/Descriptors_constants.h>
#include <gen_cpp/Descriptors_types.h>
#include <gen_cpp/Types_types.h>
#include <gen_cpp/internal_service.pb.h>
#include <gtest/gtest.h>
+#include <string>
+#include <utility>
+#include <vector>
+
#include "common/object_pool.h"
#include "core/block/block.h"
+#include "core/block/column_with_type_and_name.h"
+#include "core/column/column_string.h"
+#include "core/data_type/data_type_string.h"
+#include "core/data_type/data_type_struct.h"
+#include "core/data_type_serde/data_type_serde.h"
#include "exprs/vexpr.h"
#include "runtime/descriptor_helper.h"
#include "runtime/descriptors.h"
@@ -30,6 +40,8 @@
#include "runtime/runtime_state.h"
#include "service/point_query_executor.h"
#include "storage/tablet/tablet_schema.h"
+#include "storage/utils.h"
+#include "util/jsonb/serialize.h"
namespace doris {
@@ -197,9 +209,152 @@ std::shared_ptr<TabletSchema>
ReusableTestHelper::tablet_schema = []() {
schema->append_column(TabletColumn(FieldAggregationMethod::OLAP_FIELD_AGGREGATION_NONE,
FieldType::OLAP_FIELD_TYPE_BIGINT));
}
+ TabletColumn delete_sign;
+ delete_sign.set_name(DELETE_SIGN);
+ delete_sign.set_unique_id(1000);
+ delete_sign.set_type(FieldType::OLAP_FIELD_TYPE_TINYINT);
+ schema->append_column(delete_sign);
return schema;
}();
+TEST(PointQueryReusableTest, AcceptDescriptorsWithoutAccessPaths) {
+ auto descriptor = ReusableTestHelper::create_descriptor_tablet();
+ for (auto& slot : descriptor.slotDescriptors) {
+ slot.__set_all_access_paths({});
+ slot.__set_predicate_access_paths({});
+ }
+ Reusable reusable;
+ auto status =
+ reusable.init(descriptor, ReusableTestHelper::output_exprs,
+ ReusableTestHelper::query_options,
*ReusableTestHelper::tablet_schema, 2);
+ ASSERT_TRUE(status.ok()) << status.to_string();
+}
+
+TEST(PointQueryReusableTest, RejectAllColumnAccessPaths) {
+ std::vector<TColumnAccessPath> paths;
+ for (const auto& components : std::vector<std::vector<std::string>> {
+ {"k1"}, {"k1", "leaf"}, {"k1", "OFFSET"}, {"k1", "NULL"}}) {
+ TColumnAccessPath data_path;
+ data_path.__set_type(TAccessPathType::DATA);
+ TDataAccessPath data;
+ data.__set_path(components);
+ data_path.__set_data_access_path(data);
+ paths.push_back(data_path); // Legacy DATA paths have no version field.
+
data_path.__set_version(g_Descriptors_constants.TCOLUMN_ACCESS_PATH_VERSION_TYPED);
+ paths.push_back(data_path);
+
+ TColumnAccessPath meta_path;
+ meta_path.__set_type(TAccessPathType::META);
+
meta_path.__set_version(g_Descriptors_constants.TCOLUMN_ACCESS_PATH_VERSION_TYPED);
+ TMetaAccessPath meta;
+ meta.__set_path(components);
+ meta_path.__set_meta_access_path(meta);
+ paths.push_back(meta_path);
+ }
+ paths.emplace_back(); // Even a malformed path must not bypass the guard.
+ for (size_t path_idx = 0; path_idx < paths.size(); ++path_idx) {
+ SCOPED_TRACE(path_idx);
+ for (const bool predicate : {false, true}) {
+ SCOPED_TRACE(predicate);
+ auto descriptor = ReusableTestHelper::create_descriptor_tablet();
+ auto& slot = descriptor.slotDescriptors[0];
+ slot.__set_all_access_paths(predicate ? TColumnAccessPaths {}
+ : TColumnAccessPaths
{paths[path_idx]});
+ slot.__set_predicate_access_paths(predicate ? TColumnAccessPaths
{paths[path_idx]}
+ : TColumnAccessPaths
{});
+ Reusable reusable;
+ auto status = reusable.init(descriptor,
ReusableTestHelper::output_exprs,
+ ReusableTestHelper::query_options,
+ *ReusableTestHelper::tablet_schema, 2);
+ ASSERT_FALSE(status.ok());
+ EXPECT_NE(status.to_string().find("does not support column access
paths"),
+ std::string::npos);
+ EXPECT_NE(status.to_string().find("k1"), std::string::npos);
+ }
+ }
+}
+
+TEST(PointQueryReusableTest, RejectFullStructRootAccessPaths) {
+ TabletSchema schema;
+ schema.copy_from(*ReusableTestHelper::tablet_schema);
+ TabletColumn column;
+ column.set_name("typed");
+ column.set_unique_id(100);
+ column.set_type(FieldType::OLAP_FIELD_TYPE_STRUCT);
+ TabletColumn child;
+ child.set_name("leaf");
+ child.set_type(FieldType::OLAP_FIELD_TYPE_STRING);
+ column.add_sub_column(child);
+ schema.append_column(column);
+
+ auto descriptor = ReusableTestHelper::create_descriptor_tablet();
+ descriptor.slotDescriptors.resize(1);
+ auto& slot = descriptor.slotDescriptors[0];
+ slot.__set_colName("typed");
+ slot.__set_col_unique_id(100);
+ const DataTypePtr full_type = std::make_shared<DataTypeStruct>(
+ DataTypes {std::make_shared<DataTypeString>()}, Strings {"leaf"});
+ slot.__set_slotType(full_type->to_thrift());
+ TColumnAccessPath root;
+ root.__set_type(TAccessPathType::DATA);
+ TDataAccessPath data;
+ data.__set_path({"typed"});
+ root.__set_data_access_path(data);
+ for (const bool predicate : {false, true}) {
+ slot.__set_all_access_paths(predicate ? TColumnAccessPaths {} :
TColumnAccessPaths {root});
+ slot.__set_predicate_access_paths(predicate ? TColumnAccessPaths {root}
+ : TColumnAccessPaths {});
+ Reusable reusable;
+ auto status = reusable.init(descriptor,
ReusableTestHelper::output_exprs,
+ ReusableTestHelper::query_options, schema,
2);
+ ASSERT_FALSE(status.ok());
+ EXPECT_NE(status.to_string().find("does not support column access
paths"),
+ std::string::npos);
+ }
+}
+
+TEST(PointQueryReusableTest, MissingScalarWithoutPathsUsesPayloadOrFeDefault) {
+ const DataTypePtr string_type = std::make_shared<DataTypeString>();
+ const std::string stored_value = "payload";
+ for (const bool payload_has_uid : {false, true}) {
+ TabletColumn source_column;
+ source_column.set_name("source");
+ source_column.set_unique_id(payload_has_uid ? 100 : 99);
+ source_column.set_type(FieldType::OLAP_FIELD_TYPE_STRING);
+ TabletSchema source_schema;
+ source_schema.append_column(source_column);
+ auto values = string_type->create_column();
+ values->insert_data(stored_value.data(), stored_value.size());
+ Block source;
+ source.insert({std::move(values), string_type, "source"});
+ auto jsonb_column = ColumnString::create();
+ JsonbSerializeUtil::block_to_jsonb(source_schema, source,
*jsonb_column, 1,
+ DataTypeSerDeSPtrs
{string_type->get_serde()}, {});
+ auto descriptor = ReusableTestHelper::create_descriptor_tablet();
+ descriptor.slotDescriptors.resize(1);
+ auto& slot = descriptor.slotDescriptors[0];
+ slot.__set_colName("new_column");
+ slot.__set_col_unique_id(100);
+ slot.__set_slotType(string_type->to_thrift());
+ slot.__set_nullIndicatorBit(-1);
+ slot.__set_col_default_value("abcd");
+
+ Reusable reusable;
+ auto status = reusable.init(descriptor,
ReusableTestHelper::output_exprs,
+ ReusableTestHelper::query_options,
+ *ReusableTestHelper::tablet_schema, 1);
+ ASSERT_TRUE(status.ok()) << status.to_string();
+ Block decoded(reusable.tuple_desc()->slots(), 1);
+ status =
JsonbSerializeUtil::jsonb_to_block(reusable.get_data_type_serdes(),
*jsonb_column,
+
reusable.get_col_uid_to_idx(), decoded,
+
reusable.get_col_default_values(), {});
+ ASSERT_TRUE(status.ok()) << status.to_string();
+ ASSERT_EQ(decoded.rows(), 1);
+
EXPECT_EQ(decoded.get_by_position(0).column->get_data_at(0).to_string(),
+ payload_has_uid ? stored_value : "abcd");
+ }
+}
+
// RowCache test class
class RowCacheTest : public testing::Test {
protected:
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
index 51bdc44b66b..733b4b47ad8 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LogicalResultSinkToShortCircuitPointQuery.java
@@ -23,6 +23,7 @@ import org.apache.doris.datasource.doris.RemoteOlapTable;
import org.apache.doris.nereids.StatementContext;
import org.apache.doris.nereids.rules.Rule;
import org.apache.doris.nereids.rules.RuleType;
+import org.apache.doris.nereids.trees.expressions.Alias;
import org.apache.doris.nereids.trees.expressions.Cast;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.Expression;
@@ -30,6 +31,7 @@ import
org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.ConnectContext.ConnectType;
@@ -46,6 +48,34 @@ import java.util.Set;
*/
public class LogicalResultSinkToShortCircuitPointQuery implements
RewriteRuleFactory {
+ private boolean isSimpleScalarProjection(Expression expression) {
+ if (expression instanceof Alias) {
+ return isSimpleScalarProjection(expression.child(0));
+ }
+ if (!expression.getDataType().isPrimitive() ||
expression.getDataType().isVariantType()) {
+ return false;
+ }
+ if (expression instanceof SlotReference) {
+ SlotReference slot = (SlotReference) expression;
+ return !slot.hasSubColPath()
+ && slot.getAllAccessPaths().map(List::isEmpty).orElse(true)
+ &&
slot.getPredicateAccessPaths().map(List::isEmpty).orElse(true)
+ && slot.getOriginalColumn().map(column ->
column.getType().isScalarType()
+ && !column.getType().isVariantType()).orElse(true);
+ }
+ return expression.isLiteral();
+ }
+
+ private boolean projectionMatchShortCircuitCondition(Plan root) {
+ // This decision precedes nested pruning and partition pruning.
Restrict the fast path to
+ // scalar slots/literals, which cannot acquire nested or metadata-only
access paths later.
+ // In particular, a scalar result such as s.a or length(v) is not a
scalar slot projection.
+ // Uncertain expressions take the normal path; do not restart planning
after pruning.
+ return
root.getExpressions().stream().allMatch(this::isSimpleScalarProjection)
+ && (!(root.child(0) instanceof LogicalProject)
+ ||
root.child(0).getExpressions().stream().allMatch(this::isSimpleScalarProjection));
+ }
+
private Expression removeCast(Expression expression) {
if (expression instanceof Cast) {
return expression.child(0);
@@ -112,7 +142,8 @@ public class LogicalResultSinkToShortCircuitPointQuery
implements RewriteRuleFac
}
}
// set short circuit flag and modify nothing to the plan
- if (olapTable.getBaseSchemaKeyColumns().size() <= colNames.size()) {
+ if (olapTable.getBaseSchemaKeyColumns().size() <= colNames.size()
+ && projectionMatchShortCircuitCondition(root)) {
statementContext.setShortCircuitQuery(true);
}
return root;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
index e985132527f..c212d4f6394 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommand.java
@@ -26,6 +26,7 @@ import org.apache.doris.nereids.analyzer.UnboundRelation;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.glue.LogicalPlanAdapter;
import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.nereids.properties.SelectHintSetVar;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.Placeholder;
import org.apache.doris.nereids.trees.expressions.SubqueryExpr;
@@ -36,15 +37,18 @@ import
org.apache.doris.nereids.trees.plans.commands.insert.InsertOverwriteTable
import
org.apache.doris.nereids.trees.plans.commands.insert.OlapGroupCommitInsertExecutor;
import org.apache.doris.nereids.trees.plans.commands.merge.MergeIntoCommand;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalSelectHint;
import org.apache.doris.nereids.trees.plans.logical.LogicalSqlCache;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import org.apache.doris.planner.GroupCommitPlanner;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.PointQueryExecutor;
import org.apache.doris.qe.PreparedStatementContext;
+import org.apache.doris.qe.SessionVariable;
import org.apache.doris.qe.ShortCircuitQueryContext;
import org.apache.doris.qe.StmtExecutor;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -77,6 +81,18 @@ public class ExecuteCommand extends Command {
return stmtName;
}
+ @VisibleForTesting
+ static boolean hasShortCircuitRoutingHint(LogicalPlan plan) {
+ // Direct execution skips preprocessing (and therefore SET_VAR).
Re-plan statements with
+ // routing hints rather than comparing a cached effective value with
the bare session.
+ return plan.anyMatch(node -> node instanceof LogicalSelectHint
+ && ((LogicalSelectHint<?>) node).getHints().stream()
+ .filter(hint -> hint instanceof SelectHintSetVar)
+ .anyMatch(hint -> ((SelectHintSetVar)
hint).getParameters().keySet().stream().anyMatch(key ->
+
key.equalsIgnoreCase(SessionVariable.ENABLE_SHORT_CIRCUIT_QUERY)
+ ||
key.equalsIgnoreCase(SessionVariable.ENABLE_PRUNE_NESTED_COLUMN))));
+ }
+
@Override
public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
return visitor.visit(this, context);
@@ -152,6 +168,7 @@ public class ExecuteCommand extends Command {
executor.setParsedStmt(planAdapter);
boolean hasShortCircuitContext =
preparedStmtCtx.shortCircuitQueryContext.isPresent();
boolean shortCircuitContextReusable = hasShortCircuitContext
+ && !hasShortCircuitRoutingHint(logicalPlan)
&&
preparedStmtCtx.shortCircuitQueryContext.get().isReusable(ctx);
// Reuse the cached short-circuit plan only when table metadata is
unchanged and the statement
// has no nondeterministic functions. Otherwise fall back to the
normal execution path below.
@@ -187,6 +204,7 @@ public class ExecuteCommand extends Command {
// Drop the previously cached short-circuit context: either it was
reusable and returned
// early above, has just been refreshed here, or is stale and we are
about to re-plan.
preparedStmtCtx.shortCircuitQueryContext = Optional.empty();
+ statementContext.setShortCircuitQuery(false);
executor.execute();
StatementContext executedStatementContext =
executor.getContext().getStatementContext();
ShortCircuitQueryContext shortCircuitQueryContext =
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java
index b48b196bb4e..58a1e139505 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java
@@ -23,9 +23,13 @@ import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.Type;
import org.apache.doris.planner.OlapScanNode;
import org.apache.doris.planner.Planner;
+import org.apache.doris.thrift.TDescriptorTable;
import org.apache.doris.thrift.TExpr;
import org.apache.doris.thrift.TExprList;
+import org.apache.doris.thrift.TPrimitiveType;
import org.apache.doris.thrift.TQueryOptions;
+import org.apache.doris.thrift.TSlotDescriptor;
+import org.apache.doris.thrift.TTypeNodeType;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
@@ -42,6 +46,9 @@ import java.util.UUID;
import java.util.stream.Collectors;
public class ShortCircuitQueryContext {
+ // BE Reusable::tuple_desc() consumes tuple 0. Auxiliary tuples, such as a
full base-index
+ // tuple created for an explicitly selected rollup, are not row-store
decoding inputs.
+ private static final int POINT_QUERY_TUPLE_ID = 0;
// Cached for better CPU performance, since serialize DescriptorTable and
// outputExprs are heavy work
public final Planner planner;
@@ -59,6 +66,7 @@ public class ShortCircuitQueryContext {
private final long fileCacheQueryLimitBytes;
private final long partitionTopologyVersion;
public final String tableName;
+ private final boolean enablePruneNestedColumns;
public final OlapScanNode scanNode;
public final Queriable analzyedQuery;
@@ -84,11 +92,16 @@ public class ShortCircuitQueryContext {
return returnTypes;
}
- public ShortCircuitQueryContext(Planner planner, Queriable analzyedQuery)
throws TException {
+ public ShortCircuitQueryContext(Planner planner, Queriable analzyedQuery,
+ SessionVariable effectiveSession) throws TException {
this.planner = planner;
+ TDescriptorTable descriptors = planner.getDescTable().toThrift();
+ validateDescriptors(descriptors);
this.serializedDescTable = ByteString.copyFrom(
- new
TSerializer().serialize(planner.getDescTable().toThrift()));
+ new TSerializer().serialize(descriptors));
TQueryOptions options = planner.getQueryOptions() != null ?
planner.getQueryOptions() : new TQueryOptions();
+ // Capture effective planning options here, before StmtExecutor rolls
back SET_VAR.
+ this.enablePruneNestedColumns =
effectiveSession.enablePruneNestedColumns;
this.fileCacheQueryLimitBytes = options.isSetFileCacheQueryLimitBytes()
? options.getFileCacheQueryLimitBytes()
: -1;
@@ -129,17 +142,39 @@ public class ShortCircuitQueryContext {
this.schemaVersion = schemaVersion;
this.fileCacheQueryLimitBytes = fileCacheQueryLimitBytes;
this.partitionTopologyVersion = tbl.getPartitionTopologyVersion();
+ this.enablePruneNestedColumns = new
SessionVariable().enablePruneNestedColumns;
this.scanNode = null;
this.analzyedQuery = null;
}
public boolean isReusable(ConnectContext ctx) {
- return !this.tbl.isDropped && Objects.equals(this.tableName,
this.tbl.getName())
+ return ctx.getSessionVariable().isEnableShortCircuitQuery()
+ && this.enablePruneNestedColumns ==
ctx.getSessionVariable().enablePruneNestedColumns
+ && !this.tbl.isDropped && Objects.equals(this.tableName,
this.tbl.getName())
&& this.tbl.getBaseSchemaVersion() == this.schemaVersion
&& this.fileCacheQueryLimitBytes ==
ctx.getSessionVariable().fileCacheQueryLimitBytes
&& this.tbl.getPartitionTopologyVersion() ==
this.partitionTopologyVersion;
}
+ @VisibleForTesting
+ static void validateDescriptors(TDescriptorTable descriptors) throws
TException {
+ if (descriptors.isSetSlotDescriptors()) {
+ for (TSlotDescriptor slot : descriptors.getSlotDescriptors()) {
+ if (slot.getParent() != POINT_QUERY_TUPLE_ID) {
+ continue;
+ }
+ if ((slot.isSetAllAccessPaths() &&
!slot.getAllAccessPaths().isEmpty())
+ || (slot.isSetPredicateAccessPaths() &&
!slot.getPredicateAccessPaths().isEmpty())
+ || (slot.isSetColumnPaths() &&
!slot.getColumnPaths().isEmpty())
+ || slot.getSlotType().getTypesSize() != 1
+ || slot.getSlotType().getTypes().get(0).getType() !=
TTypeNodeType.SCALAR
+ ||
slot.getSlotType().getTypes().get(0).getScalarType().getType() ==
TPrimitiveType.VARIANT) {
+ throw new TException("Short-circuit point queries require
scalar slots without access paths");
+ }
+ }
+ }
+ }
+
public void sanitize() {
Preconditions.checkNotNull(serializedDescTable);
Preconditions.checkNotNull(serializedOutputExpr);
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
index 09068413e79..2f43c49db93 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
@@ -1428,7 +1428,8 @@ public class StmtExecutor {
if (statementContext.isShortCircuitQuery()) {
ShortCircuitQueryContext shortCircuitQueryContext =
statementContext.getShortCircuitQueryContext();
if (shortCircuitQueryContext == null) {
- shortCircuitQueryContext = new
ShortCircuitQueryContext(planner, (Queriable) parsedStmt);
+ shortCircuitQueryContext = new
ShortCircuitQueryContext(planner, (Queriable) parsedStmt,
+ context.getSessionVariable());
// ExecuteCommand publishes this same context after a
successful first prepared execution.
statementContext.setShortCircuitQueryContext(shortCircuitQueryContext);
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
index 512033b35a5..fd18f851551 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ShortCircuitPointQueryTest.java
@@ -80,6 +80,10 @@ class ShortCircuitPointQueryTest extends TestWithFeService
+ " \"light_schema_change\" = \"true\",\n"
+ " \"store_row_column\" = \"true\"\n"
+ ");");
+ createTable("CREATE TABLE tbl_complex_point_query (k INT, v STRING, s
STRUCT<a:INT,b:STRING>) "
+ + "UNIQUE KEY(k) DISTRIBUTED BY HASH(k) BUCKETS 1 PROPERTIES "
+ + "('replication_num'='1',
'enable_unique_key_merge_on_write'='true', "
+ + "'light_schema_change'='true', 'store_row_column'='true')");
}
@Test
@@ -143,6 +147,31 @@ class ShortCircuitPointQueryTest extends TestWithFeService
Assertions.assertFalse(connectContext.getStatementContext().isShortCircuitQuery());
}
+ @Test
+ void testReferencedComplexAndMetadataExpressionsUseNormalPath() {
+ boolean prune =
connectContext.getSessionVariable().enablePruneNestedColumns;
+ try {
+ for (boolean enabled : new boolean[] {false, true}) {
+ connectContext.getSessionVariable().enablePruneNestedColumns =
enabled;
+ for (String projection : new String[] {"s", "struct_element(s,
'a')", "length(v)",
+ "v is null", "length(v) + 1"}) {
+ rewrite("select " + projection + " from
tbl_complex_point_query where k = 1");
+
Assertions.assertFalse(connectContext.getStatementContext().isShortCircuitQuery(),
projection);
+ }
+ }
+ } finally {
+ connectContext.getSessionVariable().enablePruneNestedColumns =
prune;
+ }
+ }
+
+ @Test
+ void testUnusedComplexColumnDoesNotDisableScalarPointQuery() {
+ for (String projection : new String[] {"k", "v", "k as key_alias, v as
value_alias", "1"}) {
+ rewrite("select " + projection + " from tbl_complex_point_query
where k = 1");
+
Assertions.assertTrue(connectContext.getStatementContext().isShortCircuitQuery(),
projection);
+ }
+ }
+
@Test
void testRemoteOlapTableDoesNotUseShortCircuit() throws Exception {
Database database =
Env.getCurrentInternalCatalog().getDbOrMetaException("test");
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommandTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommandTest.java
index 24495c6d9a4..21c2526172f 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommandTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ExecuteCommandTest.java
@@ -313,7 +313,7 @@ public class ExecuteCommandTest {
// A real ShortCircuitQueryContext (built from a mocked planner) that
passes isReusable().
Planner planner = Mockito.mock(Planner.class);
- Mockito.when(planner.getQueryOptions()).thenReturn(new
TQueryOptions());
+ Mockito.when(planner.getQueryOptions()).thenReturn(new
TQueryOptions().setEnablePruneNestedColumn(true));
DescriptorTable descriptorTable = new DescriptorTable();
descriptorTable.createTupleDescriptor();
Mockito.when(planner.getDescTable()).thenReturn(descriptorTable);
@@ -325,7 +325,8 @@ public class ExecuteCommandTest {
Mockito.when(scanNode.getTableNameInPlan()).thenReturn("tbl");
Mockito.when(scanNode.getConjuncts()).thenReturn(Collections.emptyList());
Mockito.when(planner.getScanNodes()).thenReturn(Collections.singletonList(scanNode));
- ShortCircuitQueryContext cachedPlan = new
ShortCircuitQueryContext(planner, Mockito.mock(Queriable.class));
+ ShortCircuitQueryContext cachedPlan = new
ShortCircuitQueryContext(planner, Mockito.mock(Queriable.class),
+ new SessionVariable());
preparedStatement.shortCircuitQueryContext = Optional.of(cachedPlan);
StmtExecutor executor = Mockito.mock(StmtExecutor.class);
@@ -350,6 +351,84 @@ public class ExecuteCommandTest {
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
}
+ @Test
+ public void testRoutingHintsRequirePlanning() {
+
Assertions.assertTrue(ExecuteCommand.hasShortCircuitRoutingHint(parseWithContext(
+ "select /*+ SET_VAR(enable_prune_nested_column=true) */ v from
t where k=1")));
+
Assertions.assertTrue(ExecuteCommand.hasShortCircuitRoutingHint(parseWithContext(
+ "select /*+ SET_VAR(enable_short_circuit_query=true) */ v from
t where k=1")));
+
Assertions.assertFalse(ExecuteCommand.hasShortCircuitRoutingHint(parseWithContext(
+ "select v from t where k=1")));
+ }
+
+ @Test
+ public void testFallbackClearsInheritedShortCircuitEligibility() throws
Exception {
+ for (int scenario = 0; scenario < 3; scenario++) {
+ boolean hasCachedPlan = scenario != 0;
+ String hint = scenario == 2 ? "/*+
SET_VAR(enable_prune_nested_column=true) */" : "";
+ String sql = "select " + hint + " v from tbl where k=1";
+ LogicalPlan plan = parseWithContext(sql);
+ ConnectContext connection = Mockito.spy(new ConnectContext());
+ StatementContext previous = new StatementContext(connection, new
OriginStatement(sql, 0));
+ previous.setShortCircuitQuery(true);
+ connection.setStatementContext(previous);
+ PrepareCommand prepare = new PrepareCommand("stmt", plan,
Collections.emptyList(),
+ new OriginStatement(sql, 0));
+ PreparedStatementContext prepared = new
PreparedStatementContext(prepare, connection, previous, "stmt");
+ if (hasCachedPlan) {
+ ShortCircuitQueryContext cached =
Mockito.mock(ShortCircuitQueryContext.class);
+ // A routing hint requires refresh even if the cached metadata
remains reusable.
+
Mockito.when(cached.isReusable(connection)).thenReturn(!hint.isEmpty());
+ prepared.shortCircuitQueryContext = Optional.of(cached);
+ }
+
Mockito.doReturn(prepared).when(connection).getPreparedStementContext("stmt");
+ StmtExecutor executor = Mockito.mock(StmtExecutor.class);
+ Mockito.when(executor.getContext()).thenReturn(connection);
+ Mockito.doAnswer(invocation -> {
+ connection.setStatementContext(invocation.getArgument(0));
+ return null;
+ }).when(executor).setStatementContext(Mockito.any());
+ Mockito.doAnswer(ignored -> {
+
Assertions.assertFalse(prepared.getStatementContext().isShortCircuitQuery());
+
Assertions.assertNull(prepared.getStatementContext().getShortCircuitQueryContext());
+
Assertions.assertFalse(prepared.shortCircuitQueryContext.isPresent());
+ return null;
+ }).when(executor).execute();
+ ConnectContext threadContext = ConnectContext.get();
+ connection.setThreadLocalInfo();
+ try {
+ new ExecuteCommand("stmt", prepare, previous).run(connection,
executor);
+ Mockito.verify(executor).execute();
+ if (hasCachedPlan) {
+ Assertions.assertNotSame(prepare, prepared.command,
+ "stale cached plans must retain upstream reparse
behavior");
+ } else {
+ Assertions.assertSame(prepare, prepared.command);
+ }
+ } finally {
+ ConnectContext.remove();
+ if (threadContext != null) {
+ threadContext.setThreadLocalInfo();
+ }
+ }
+ }
+ }
+
+ private LogicalPlan parseWithContext(String sql) {
+ ConnectContext previous = ConnectContext.get();
+ ConnectContext parsing = new ConnectContext();
+ parsing.setStatementContext(new StatementContext(parsing, new
OriginStatement(sql, 0)));
+ parsing.setThreadLocalInfo();
+ try {
+ return new NereidsParser().parseSingle(sql);
+ } finally {
+ ConnectContext.remove();
+ if (previous != null) {
+ previous.setThreadLocalInfo();
+ }
+ }
+ }
+
private String resolveNextSnapshot(TableScanParams scanParams,
AtomicInteger snapshotId) {
return scanParams.getOrResolveMapParams(ignored -> ImmutableMap.of(
"scan.snapshot-id",
String.valueOf(snapshotId.incrementAndGet())))
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/qe/ShortCircuitQueryContextTest.java
b/fe/fe-core/src/test/java/org/apache/doris/qe/ShortCircuitQueryContextTest.java
index 59ebd8c586c..a0605e81418 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/qe/ShortCircuitQueryContextTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/qe/ShortCircuitQueryContextTest.java
@@ -17,6 +17,8 @@
package org.apache.doris.qe;
+import org.apache.doris.analysis.DescriptorTable;
+import org.apache.doris.analysis.Queriable;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.KeysType;
import org.apache.doris.catalog.MaterializedIndex;
@@ -25,12 +27,27 @@ import org.apache.doris.catalog.Partition;
import org.apache.doris.catalog.PrimitiveType;
import org.apache.doris.catalog.RandomDistributionInfo;
import org.apache.doris.catalog.SinglePartitionInfo;
+import org.apache.doris.planner.OlapScanNode;
+import org.apache.doris.planner.Planner;
+import org.apache.doris.thrift.TAccessPathType;
+import org.apache.doris.thrift.TColumnAccessPath;
+import org.apache.doris.thrift.TDataAccessPath;
+import org.apache.doris.thrift.TDescriptorTable;
+import org.apache.doris.thrift.TPrimitiveType;
+import org.apache.doris.thrift.TQueryOptions;
+import org.apache.doris.thrift.TScalarType;
+import org.apache.doris.thrift.TSlotDescriptor;
import org.apache.doris.thrift.TStorageType;
+import org.apache.doris.thrift.TTypeDesc;
+import org.apache.doris.thrift.TTypeNode;
+import org.apache.doris.thrift.TTypeNodeType;
+import org.apache.thrift.TException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -105,4 +122,70 @@ public class ShortCircuitQueryContextTest {
Assertions.assertFalse(context.isReusable(connectContext(-1)));
}
+ @Test
+ public void testEffectiveRoutingOptionsAreCapturedBeforePublication()
throws Exception {
+ // Planner query options may have been created before preprocessing
applied SET_VAR.
+ TQueryOptions options = new
TQueryOptions().setEnablePruneNestedColumn(false);
+ SessionVariable effective = new SessionVariable();
+ effective.enablePruneNestedColumns = true;
+ ShortCircuitQueryContext context = plannedContext(options, effective);
+ ConnectContext connection = connectContext(-1);
+ Assertions.assertTrue(context.isReusable(connection));
+ // Simulate SET_VAR rollback before ExecuteCommand publishes this
already-built context.
+ connection.getSessionVariable().enablePruneNestedColumns = false;
+ effective.enablePruneNestedColumns = false;
+ Assertions.assertFalse(context.isReusable(connection));
+ connection.getSessionVariable().enablePruneNestedColumns = true;
+ Assertions.assertTrue(context.isReusable(connection));
+ Assertions.assertTrue(connection.getSessionVariable().setVarOnce(
+ SessionVariable.ENABLE_SHORT_CIRCUIT_QUERY, "false"));
+ Assertions.assertFalse(context.isReusable(connection));
+ }
+
+ @Test
+ public void testValidateActualSerializedDescriptor() throws Exception {
+ TTypeNode type = new TTypeNode().setType(TTypeNodeType.SCALAR)
+ .setScalarType(new TScalarType().setType(TPrimitiveType.INT));
+ TSlotDescriptor slot = new TSlotDescriptor().setSlotType(
+ new TTypeDesc().setTypes(Collections.singletonList(type)));
+ TDescriptorTable descriptors = new TDescriptorTable()
+ .setSlotDescriptors(Collections.singletonList(slot));
+ ShortCircuitQueryContext.validateDescriptors(descriptors);
+ TColumnAccessPath path = new TColumnAccessPath(TAccessPathType.DATA)
+ .setDataAccessPath(new
TDataAccessPath(Collections.singletonList("k")));
+ slot.setAllAccessPaths(Collections.singletonList(path));
+ Assertions.assertThrows(TException.class, () ->
ShortCircuitQueryContext.validateDescriptors(descriptors));
+ slot.setAllAccessPaths(Collections.emptyList());
+ slot.setPredicateAccessPaths(Collections.singletonList(path));
+ Assertions.assertThrows(TException.class, () ->
ShortCircuitQueryContext.validateDescriptors(descriptors));
+ slot.setPredicateAccessPaths(Collections.emptyList());
+ slot.setColumnPaths(Collections.singletonList("leaf"));
+ Assertions.assertThrows(TException.class, () ->
ShortCircuitQueryContext.validateDescriptors(descriptors));
+ slot.setColumnPaths(Collections.emptyList());
+ type.setType(TTypeNodeType.STRUCT);
+ Assertions.assertThrows(TException.class, () ->
ShortCircuitQueryContext.validateDescriptors(descriptors));
+
type.setType(TTypeNodeType.SCALAR).getScalarType().setType(TPrimitiveType.VARIANT);
+ Assertions.assertThrows(TException.class, () ->
ShortCircuitQueryContext.validateDescriptors(descriptors));
+ type.getScalarType().setType(TPrimitiveType.INT);
+ TSlotDescriptor auxiliary = slot.deepCopy().setId(1).setParent(1);
+
auxiliary.getSlotType().getTypes().get(0).setType(TTypeNodeType.STRUCT);
+ descriptors.setSlotDescriptors(Arrays.asList(slot, auxiliary));
+ ShortCircuitQueryContext.validateDescriptors(descriptors);
+ auxiliary.setParent(0);
+ Assertions.assertThrows(TException.class, () ->
ShortCircuitQueryContext.validateDescriptors(descriptors));
+ }
+
+ private ShortCircuitQueryContext plannedContext(TQueryOptions options,
SessionVariable effective) throws Exception {
+ Planner planner = Mockito.mock(Planner.class);
+ Mockito.when(planner.getQueryOptions()).thenReturn(options);
+ DescriptorTable descriptors = new DescriptorTable();
+ descriptors.createTupleDescriptor();
+ Mockito.when(planner.getDescTable()).thenReturn(descriptors);
+ OlapScanNode scan = Mockito.mock(OlapScanNode.class);
+ OlapTable olapTable = table("tbl", 10);
+ Mockito.when(scan.getOlapTable()).thenReturn(olapTable);
+ Mockito.when(scan.getTableNameInPlan()).thenReturn("tbl");
+
Mockito.when(planner.getScanNodes()).thenReturn(Collections.singletonList(scan));
+ return new ShortCircuitQueryContext(planner,
Mockito.mock(Queriable.class), effective);
+ }
}
diff --git
a/regression-test/data/point_query_p0/test_rowstore_nested_pruning_minimal.out
b/regression-test/data/point_query_p0/test_rowstore_nested_pruning_minimal.out
new file mode 100644
index 00000000000..96eb457c3d0
--- /dev/null
+++
b/regression-test/data/point_query_p0/test_rowstore_nested_pruning_minimal.out
@@ -0,0 +1,68 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !without_short_circuit --
+\N
+
+-- !existing_key_without_short_circuit --
+13
+
+-- !without_pruning --
+\N
+
+-- !existing_key_without_pruning --
+13
+
+-- !fallback_with_pruning --
+\N
+
+-- !existing_key_fallback_with_pruning --
+13
+
+-- !fallback_with_pruning_cached --
+\N
+
+-- !existing_key_fallback_with_pruning_repeated --
+13
+
+-- !scalar_control --
+2
+
+-- !warm_old_blob --
+1
+
+-- !default_metadata_before_load --
+4 true
+
+-- !old_struct_before_load --
+7 \N
+
+-- !old_struct_after_load --
+7 \N
+
+-- !new_struct_after_load --
+8 bee
+
+-- !default_metadata_after_load --
+4 true
+
+-- !old_struct_without_pruning --
+7 \N
+
+-- !prepared_scalar_first --
+1
+
+-- !prepared_scalar_reuse --
+2
+
+-- !prepared_scalar_options_changed --
+2
+
+-- !prepared_scalar_schema_changed --
+1
+
+-- !prepared_hint_first --
+abcd
+
+-- !prepared_hint_repeated --
+new
+
+-- !auxiliary_tuple_missing_key --
diff --git
a/regression-test/suites/point_query_p0/test_rowstore_nested_pruning_minimal.groovy
b/regression-test/suites/point_query_p0/test_rowstore_nested_pruning_minimal.groovy
new file mode 100644
index 00000000000..c2684a132da
--- /dev/null
+++
b/regression-test/suites/point_query_p0/test_rowstore_nested_pruning_minimal.groovy
@@ -0,0 +1,208 @@
+// 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_rowstore_nested_pruning_minimal", "p0,nonConcurrent") {
+ sql "DROP TABLE IF EXISTS rowstore_nested_pruning_minimal"
+ sql """
+ CREATE TABLE rowstore_nested_pruning_minimal (
+ pk INT,
+ typed
STRUCT<string_leaf:STRING,typed_map:MAP<STRING,STRUCT<string_leaf:STRING>>>
+ ) UNIQUE KEY(pk)
+ DISTRIBUTED BY HASH(pk) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true",
+ "store_row_column" = "true"
+ )
+ """
+ sql """
+ INSERT INTO rowstore_nested_pruning_minimal VALUES
+ (2, NAMED_STRUCT('string_leaf', 'b-typed-2',
+ 'typed_map', MAP('b_key', NAMED_STRUCT('string_leaf',
'b-map-typed-2'))))
+ """
+ sql "SYNC"
+ sql "SET enable_sql_cache=false"
+ sql "SET query_timeout=10"
+
+ sql "SET enable_short_circuit_query=false"
+ sql "SET enable_prune_nested_column=true"
+ order_qt_without_short_circuit """
+ SELECT CHARACTER_LENGTH(struct_element(element_at(
+ struct_element(typed, 'typed_map'), 'tail'), 'string_leaf')) AS
character_len
+ FROM rowstore_nested_pruning_minimal WHERE pk=2
+ """
+ order_qt_existing_key_without_short_circuit """
+ SELECT CHARACTER_LENGTH(struct_element(element_at(
+ struct_element(typed, 'typed_map'), 'b_key'), 'string_leaf')) AS
character_len
+ FROM rowstore_nested_pruning_minimal WHERE pk=2
+ """
+
+ sql "SET enable_short_circuit_query=true"
+ sql "SET enable_prune_nested_column=false"
+ order_qt_without_pruning """
+ SELECT CHARACTER_LENGTH(struct_element(element_at(
+ struct_element(typed, 'typed_map'), 'tail'), 'string_leaf')) AS
character_len
+ FROM rowstore_nested_pruning_minimal WHERE pk=2
+ """
+ order_qt_existing_key_without_pruning """
+ SELECT CHARACTER_LENGTH(struct_element(element_at(
+ struct_element(typed, 'typed_map'), 'b_key'), 'string_leaf')) AS
character_len
+ FROM rowstore_nested_pruning_minimal WHERE pk=2
+ """
+
+ sql "SET enable_prune_nested_column=true"
+ explain {
+ sql """
+ SELECT CHARACTER_LENGTH(struct_element(element_at(
+ struct_element(typed, 'typed_map'), 'tail'), 'string_leaf'))
AS character_len
+ FROM rowstore_nested_pruning_minimal WHERE pk=2
+ """
+ notContains "SHORT-CIRCUIT"
+ }
+ order_qt_fallback_with_pruning """
+ SELECT CHARACTER_LENGTH(struct_element(element_at(
+ struct_element(typed, 'typed_map'), 'tail'), 'string_leaf')) AS
character_len
+ FROM rowstore_nested_pruning_minimal WHERE pk=2
+ """
+ order_qt_existing_key_fallback_with_pruning """
+ SELECT CHARACTER_LENGTH(struct_element(element_at(
+ struct_element(typed, 'typed_map'), 'b_key'), 'string_leaf')) AS
character_len
+ FROM rowstore_nested_pruning_minimal WHERE pk=2
+ """
+ order_qt_fallback_with_pruning_cached """
+ SELECT CHARACTER_LENGTH(struct_element(element_at(
+ struct_element(typed, 'typed_map'), 'tail'), 'string_leaf')) AS
character_len
+ FROM rowstore_nested_pruning_minimal WHERE pk=2
+ """
+ order_qt_existing_key_fallback_with_pruning_repeated """
+ SELECT CHARACTER_LENGTH(struct_element(element_at(
+ struct_element(typed, 'typed_map'), 'b_key'), 'string_leaf')) AS
character_len
+ FROM rowstore_nested_pruning_minimal WHERE pk=2
+ """
+
+ // Scalar-only queries retain the fast path even on a table containing
complex columns.
+ explain {
+ sql "SELECT pk FROM rowstore_nested_pruning_minimal WHERE pk=2"
+ contains "SHORT-CIRCUIT"
+ }
+ qt_scalar_control "SELECT pk FROM rowstore_nested_pruning_minimal WHERE
pk=2"
+
+ def evolution = "rowstore_nested_pruning_evolution"
+ sql "DROP TABLE IF EXISTS ${evolution}"
+ sql """CREATE TABLE ${evolution} (k INT NOT NULL, s STRUCT<a:INT>)
+ UNIQUE KEY(k) DISTRIBUTED BY HASH(k) BUCKETS 1
+ PROPERTIES ('replication_num'='1',
'enable_unique_key_merge_on_write'='true',
+ 'light_schema_change'='true', 'store_row_column'='true',
+ 'disable_auto_compaction'='true')"""
+ sql "INSERT INTO ${evolution} VALUES (1, NAMED_STRUCT('a', 7))"
+ // A scalar point query caches the complete old row-store blob when
RowCache is enabled.
+ qt_warm_old_blob "SELECT k FROM ${evolution} WHERE k=1"
+ sql "ALTER TABLE ${evolution} MODIFY COLUMN s STRUCT<a:INT,b:STRING>"
+ waitForSchemaChangeDone {
+ sql "SHOW ALTER TABLE COLUMN WHERE TableName='${evolution}' ORDER BY
CreateTime DESC LIMIT 1"
+ time 600
+ }
+ sql "ALTER TABLE ${evolution} ADD COLUMN v STRING NOT NULL DEFAULT 'abcd'"
+ waitForSchemaChangeDone {
+ sql "SHOW ALTER TABLE COLUMN WHERE TableName='${evolution}' ORDER BY
CreateTime DESC LIMIT 1"
+ time 600
+ }
+ sql "ALTER TABLE ${evolution} ADD COLUMN v_null STRING NULL"
+ waitForSchemaChangeDone {
+ sql "SHOW ALTER TABLE COLUMN WHERE TableName='${evolution}' ORDER BY
CreateTime DESC LIMIT 1"
+ time 600
+ }
+ // FE has the new columns, but no load has propagated their schema to BE.
+ explain {
+ sql "SELECT length(v), v_null IS NULL FROM ${evolution} WHERE k=1"
+ notContains "SHORT-CIRCUIT"
+ }
+ qt_default_metadata_before_load "SELECT length(v), v_null IS NULL FROM
${evolution} WHERE k=1"
+ explain {
+ sql "SELECT struct_element(s, 'a') FROM ${evolution} WHERE k=1"
+ notContains "SHORT-CIRCUIT"
+ }
+ qt_old_struct_before_load "SELECT struct_element(s, 'a'),
struct_element(s, 'b') FROM ${evolution} WHERE k=1"
+ sql "INSERT INTO ${evolution} VALUES (2, NAMED_STRUCT('a', 8, 'b', 'bee'),
'new', NULL)"
+ qt_old_struct_after_load "SELECT struct_element(s, 'a'), struct_element(s,
'b') FROM ${evolution} WHERE k=1"
+ qt_new_struct_after_load "SELECT struct_element(s, 'a'), struct_element(s,
'b') FROM ${evolution} WHERE k=2"
+ qt_default_metadata_after_load "SELECT length(v), v_null IS NULL FROM
${evolution} WHERE k=1"
+ sql "SET enable_prune_nested_column=false"
+ qt_old_struct_without_pruning "SELECT struct_element(s, 'a'),
struct_element(s, 'b') FROM ${evolution} WHERE k=1"
+ sql "SET enable_prune_nested_column=true"
+
+ // Reuse one actual server-prepared handle, rather than preparing each SQL
independently.
+ def prepareUrl = getServerPrepareJdbcUrl(context.config.jdbcUrl,
context.dbName, false)
+ connect(context.config.jdbcUser, context.config.jdbcPassword, prepareUrl) {
+ sql "SET enable_short_circuit_query=true"
+ sql "SET enable_sql_cache=false"
+ sql "SET enable_prune_nested_column=true"
+ def statement = prepareStatement("SELECT k FROM ${evolution} WHERE
k=?")
+ assertEquals(com.mysql.cj.jdbc.ServerPreparedStatement,
statement.class)
+ try {
+ statement.setInt(1, 1)
+ qe_prepared_scalar_first statement
+ statement.setInt(1, 2)
+ qe_prepared_scalar_reuse statement
+ sql "SET enable_prune_nested_column=false"
+ qe_prepared_scalar_options_changed statement
+ sql "ALTER TABLE ${evolution} ADD COLUMN extra INT NULL"
+ waitForSchemaChangeDone {
+ sql "SHOW ALTER TABLE COLUMN WHERE TableName='${evolution}'
ORDER BY CreateTime DESC LIMIT 1"
+ time 600
+ }
+ statement.setInt(1, 1)
+ qe_prepared_scalar_schema_changed statement
+ } finally {
+ statement.close()
+ }
+ def beforeHint = sql("SELECT @@enable_prune_nested_column")
+ assertEquals(1, beforeHint.size())
+ def hinted = prepareStatement("SELECT /*+
SET_VAR(enable_prune_nested_column=true) */ v FROM ${evolution} WHERE k=?")
+ assertEquals(com.mysql.cj.jdbc.ServerPreparedStatement, hinted.class)
+ try {
+ hinted.setInt(1, 1)
+ qe_prepared_hint_first hinted
+ hinted.setInt(1, 2)
+ qe_prepared_hint_repeated hinted
+ assertEquals(beforeHint, sql("SELECT
@@enable_prune_nested_column"))
+ } finally {
+ hinted.close()
+ }
+ }
+ // Explicit rollups can add an auxiliary full base tuple. It is not the
tuple decoded by BE.
+ def rollupTable = "rowstore_nested_pruning_rollup"
+ sql "DROP TABLE IF EXISTS ${rollupTable}"
+ sql """CREATE TABLE ${rollupTable} (k1 INT NOT NULL, k2 INT NOT NULL, s
STRUCT<a:INT>)
+ UNIQUE KEY(k1,k2) DISTRIBUTED BY HASH(k1) BUCKETS 1
+ PROPERTIES ('replication_num'='1',
'enable_unique_key_merge_on_write'='true',
+ 'light_schema_change'='true')"""
+ sql "ALTER TABLE ${rollupTable} ADD ROLLUP r(k2,k1)"
+ waitingMVTaskFinishedByMvName(context.dbName, rollupTable, "r")
+ sql "ALTER TABLE ${rollupTable} SET ('store_row_column'='true')"
+ waitForSchemaChangeDone {
+ sql "SHOW ALTER TABLE COLUMN WHERE TableName='${rollupTable}' ORDER BY
CreateTime DESC LIMIT 1"
+ time 600
+ }
+ explain {
+ sql "SELECT k1 FROM ${rollupTable} INDEX r WHERE k1=999 AND k2=999"
+ contains "SHORT-CIRCUIT"
+ }
+ qt_auxiliary_tuple_missing_key "SELECT k1 FROM ${rollupTable} INDEX r
WHERE k1=999 AND k2=999"
+ sql "DROP TABLE ${rollupTable}"
+ sql "DROP TABLE ${evolution}"
+ sql "DROP TABLE rowstore_nested_pruning_minimal"
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]