MaterializedViewUtils.java
// 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.doris.nereids.rules.exploration.mv;
import org.apache.doris.catalog.OlapTable;
import org.apache.doris.common.Pair;
import org.apache.doris.nereids.CascadesContext;
import org.apache.doris.nereids.PlannerHook;
import org.apache.doris.nereids.StatementContext;
import org.apache.doris.nereids.memo.Group;
import org.apache.doris.nereids.memo.StructInfoMap;
import org.apache.doris.nereids.rules.RuleType;
import org.apache.doris.nereids.rules.analysis.BindRelation;
import org.apache.doris.nereids.rules.exploration.mv.PartitionIncrementMaintainer.PartitionIncrementCheckContext;
import org.apache.doris.nereids.rules.exploration.mv.PartitionIncrementMaintainer.PartitionIncrementChecker;
import org.apache.doris.nereids.rules.exploration.mv.RelatedTableInfo.RelatedTableColumnInfo;
import org.apache.doris.nereids.rules.rewrite.QueryPartitionCollector;
import org.apache.doris.nereids.trees.expressions.Alias;
import org.apache.doris.nereids.trees.expressions.ExprId;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.expressions.functions.scalar.DateTrunc;
import org.apache.doris.nereids.trees.expressions.functions.scalar.NonNullable;
import org.apache.doris.nereids.trees.expressions.functions.scalar.Nullable;
import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.PreAggStatus;
import org.apache.doris.nereids.trees.plans.algebra.Sink;
import org.apache.doris.nereids.trees.plans.logical.LogicalFileScan;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
import org.apache.doris.nereids.trees.plans.logical.LogicalRelation;
import org.apache.doris.nereids.trees.plans.physical.PhysicalCatalogRelation;
import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
import org.apache.doris.nereids.trees.plans.physical.PhysicalRelation;
import org.apache.doris.nereids.trees.plans.visitor.DefaultPlanVisitor;
import org.apache.doris.nereids.trees.plans.visitor.NondeterministicFunctionCollector;
import org.apache.doris.qe.SessionVariable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* The common util for materialized view
*/
public class MaterializedViewUtils {
/**
* Get related base table info which materialized view plan column reference,
* input param plan should be rewritten plan that sub query should be eliminated
*
* @param materializedViewPlan this should be rewritten or analyzed plan, should not be physical plan.
* @param column ref column name.
*/
@Deprecated
public static RelatedTableInfo getRelatedTableInfo(String column, String timeUnit,
Plan materializedViewPlan, CascadesContext cascadesContext) {
List<Slot> outputExpressions = materializedViewPlan.getOutput();
NamedExpression columnExpr = null;
// get column slot
for (Slot outputSlot : outputExpressions) {
if (outputSlot.getName().equalsIgnoreCase(column)) {
columnExpr = outputSlot;
break;
}
}
if (columnExpr == null) {
return RelatedTableInfo.failWith("partition column can not find from sql select column");
}
materializedViewPlan = PartitionIncrementMaintainer.removeSink(materializedViewPlan);
if (timeUnit != null) {
Expression dateTrunc = new DateTrunc(columnExpr, new VarcharLiteral(timeUnit));
columnExpr = new Alias(dateTrunc);
materializedViewPlan = new LogicalProject<>(ImmutableList.of(columnExpr), materializedViewPlan);
}
// Check sql pattern
PartitionIncrementCheckContext checkContext = new PartitionIncrementCheckContext(columnExpr, cascadesContext);
checkContext.getPartitionAndRefExpressionMap().put(columnExpr,
RelatedTableColumnInfo.of(columnExpr, null, true, false));
materializedViewPlan.accept(PartitionIncrementChecker.INSTANCE, checkContext);
List<RelatedTableColumnInfo> checkedTableColumnInfos =
PartitionIncrementMaintainer.getRelatedTableColumnInfosWithCheck(checkContext, tableColumnInfo ->
tableColumnInfo.isOriginalPartition() && tableColumnInfo.isFromTablePartitionColumn());
if (checkedTableColumnInfos == null) {
return RelatedTableInfo.failWith("multi partition column data types are different");
}
if (!checkedTableColumnInfos.isEmpty()) {
return RelatedTableInfo.successWith(checkedTableColumnInfos);
}
return RelatedTableInfo.failWith(String.format("can't not find valid partition track column, because %s",
String.join(",", checkContext.getFailReasons())));
}
/**
* Get related base table info which materialized view plan column reference,
* input param plan should be rewritten plan that sub query should be eliminated
*
* @param materializedViewPlan this should be rewritten or analyzed plan, should not be physical plan.
* @param column ref column name.
*/
public static RelatedTableInfo getRelatedTableInfos(String column, String timeUnit,
Plan materializedViewPlan, CascadesContext cascadesContext) {
List<Slot> outputExpressions = materializedViewPlan.getOutput();
NamedExpression columnExpr = null;
// get column slot
for (Slot outputSlot : outputExpressions) {
if (outputSlot.getName().equalsIgnoreCase(column)) {
columnExpr = outputSlot;
break;
}
}
if (columnExpr == null) {
return RelatedTableInfo.failWith("partition column can not find from sql select column");
}
materializedViewPlan = PartitionIncrementMaintainer.removeSink(materializedViewPlan);
if (timeUnit != null) {
Expression dateTrunc = new DateTrunc(columnExpr, new VarcharLiteral(timeUnit));
columnExpr = new Alias(dateTrunc);
materializedViewPlan = new LogicalProject<>(ImmutableList.of(columnExpr), materializedViewPlan);
}
// Check sql pattern
PartitionIncrementCheckContext checkContext = new PartitionIncrementCheckContext(columnExpr, cascadesContext);
checkContext.getPartitionAndRefExpressionMap().put(columnExpr,
RelatedTableColumnInfo.of(columnExpr, null, true, false));
materializedViewPlan.accept(PartitionIncrementChecker.INSTANCE, checkContext);
if (!checkPartitionRefExpression(checkContext.getPartitionAndRefExpressionMap().values())) {
return RelatedTableInfo.failWith(String.format(
"partition ref expressions is not consistent, partition ref expressions map is %s",
checkContext.getPartitionAndRefExpressionMap()));
}
List<RelatedTableColumnInfo> checkedTableColumnInfos =
PartitionIncrementMaintainer.getRelatedTableColumnInfosWithCheck(checkContext,
RelatedTableColumnInfo::isReachRelationCheck);
if (checkedTableColumnInfos == null) {
return RelatedTableInfo.failWith("multi partition column data types are different");
}
if (!checkedTableColumnInfos.isEmpty()) {
return RelatedTableInfo.successWith(checkedTableColumnInfos);
}
return RelatedTableInfo.failWith(String.format("can't not find valid partition track column, because %s",
String.join(",", checkContext.getFailReasons())));
}
/**
* Check the partition expression date_trunc num is valid or not
*/
private static boolean checkPartitionRefExpression(Collection<RelatedTableColumnInfo> refExpressions) {
for (RelatedTableColumnInfo tableColumnInfo : refExpressions) {
if (tableColumnInfo.getPartitionExpression().isPresent()) {
// If partition ref up expression is empty, return false directly
List<DateTrunc> dateTruncs =
tableColumnInfo.getPartitionExpression().get().collectToList(DateTrunc.class::isInstance);
if (dateTruncs.size() > 1) {
return false;
}
}
}
return true;
}
/**
* This method check the select query plan is contain the stmt as following or not
* <p>
* SELECT
* [hint_statement, ...]
* [ALL | DISTINCT | DISTINCTROW | ALL EXCEPT ( col_name1 [, col_name2, col_name3, ...] )]
* select_expr [, select_expr ...]
* [FROM table_references
* [PARTITION partition_list]
* [TABLET tabletid_list]
* [TABLESAMPLE sample_value [ROWS | PERCENT]
* [REPEATABLE pos_seek]]
* [WHERE where_condition]
* [GROUP BY [GROUPING SETS | ROLLUP | CUBE] {col_name | expr | position}]
* [HAVING where_condition]
* [ORDER BY {col_name | expr | position}
* [ASC | DESC], ...]
* [LIMIT {[offset,] row_count | row_count OFFSET offset}]
* [INTO OUTFILE 'file_name']
* <p>
* if analyzedPlan contains the stmt as following:
* [PARTITION partition_list]
* [TABLET tabletid_list] or
* [TABLESAMPLE sample_value [ROWS | PERCENT]
* * [REPEATABLE pos_seek]]
* this method will return true.
*/
public static boolean containTableQueryOperator(Plan analyzedPlan) {
return analyzedPlan.accept(TableQueryOperatorChecker.INSTANCE, null);
}
/**
* Transform to common table id, this is used by get query struct info, maybe little err when same table occur
* more than once, this is not a problem because the process of query rewrite by mv would consider more
*/
public static BitSet transformToCommonTableId(BitSet relationIdSet, Map<Integer, Integer> relationIdToTableIdMap) {
BitSet transformedBitset = new BitSet();
for (int i = relationIdSet.nextSetBit(0); i >= 0; i = relationIdSet.nextSetBit(i + 1)) {
Integer commonTableId = relationIdToTableIdMap.get(i);
if (commonTableId != null) {
transformedBitset.set(commonTableId);
}
}
return transformedBitset;
}
/**
* Extract struct info from plan, support to get struct info from logical plan or plan in group.
* @param plan maybe remove unnecessary plan node, and the logical output maybe wrong
* @param originalPlan original plan, the output is right
*/
public static List<StructInfo> extractStructInfo(Plan plan, Plan originalPlan, CascadesContext cascadesContext,
BitSet materializedViewTableSet) {
// If plan belong to some group, construct it with group struct info
if (plan.getGroupExpression().isPresent()) {
Group ownerGroup = plan.getGroupExpression().get().getOwnerGroup();
StructInfoMap structInfoMap = ownerGroup.getStructInfoMap();
// Refresh struct info in current level plan from top to bottom
SessionVariable sessionVariable = cascadesContext.getConnectContext().getSessionVariable();
structInfoMap.refresh(ownerGroup, cascadesContext, new BitSet(), new HashSet<>(),
sessionVariable.isEnableMaterializedViewNestRewrite());
structInfoMap.setRefreshVersion(cascadesContext.getMemo().getRefreshVersion());
Set<BitSet> queryTableSets = structInfoMap.getTableMaps();
ImmutableList.Builder<StructInfo> structInfosBuilder = ImmutableList.builder();
if (!queryTableSets.isEmpty()) {
for (BitSet queryTableSet : queryTableSets) {
// TODO As only support MatchMode.COMPLETE, so only get equaled query table struct info
BitSet queryCommonTableSet = MaterializedViewUtils.transformToCommonTableId(queryTableSet,
cascadesContext.getStatementContext().getRelationIdToCommonTableIdMap());
// compare relation id corresponding table id
if (!materializedViewTableSet.isEmpty()
&& !materializedViewTableSet.equals(queryCommonTableSet)) {
continue;
}
StructInfo structInfo = structInfoMap.getStructInfo(cascadesContext, queryTableSet, ownerGroup,
originalPlan, sessionVariable.isEnableMaterializedViewNestRewrite());
if (structInfo != null) {
structInfosBuilder.add(structInfo);
}
}
}
return structInfosBuilder.build();
}
// if plan doesn't belong to any group, construct it directly
return ImmutableList.of(StructInfo.of(plan, originalPlan, cascadesContext));
}
/**
* Generate scan plan for materialized view
* if MaterializationContext is already rewritten by materialized view, then should generate in real time
* when query rewrite, because one plan may hit the materialized view repeatedly and the mv scan output
* should be different
*/
public static Plan generateMvScanPlan(OlapTable table, long indexId,
List<Long> partitionIds,
PreAggStatus preAggStatus,
CascadesContext cascadesContext) {
LogicalOlapScan olapScan = new LogicalOlapScan(
cascadesContext.getStatementContext().getNextRelationId(),
table,
ImmutableList.of(table.getQualifiedDbName()),
ImmutableList.of(),
partitionIds,
indexId,
preAggStatus,
ImmutableList.of(),
// this must be empty, or it will be used to sample
ImmutableList.of(),
Optional.empty(),
ImmutableList.of());
return BindRelation.checkAndAddDeleteSignFilter(olapScan, cascadesContext.getConnectContext(),
olapScan.getTable());
}
/**
* Optimize by rules, this support optimize by custom rules by define different rewriter according to different
* rules, this method is only for materialized view rewrite
*/
public static Plan rewriteByRules(
CascadesContext cascadesContext, Function<CascadesContext, Plan> planRewriter,
Plan rewrittenPlan, Plan originPlan, boolean mvRewrite) {
if (originPlan == null || rewrittenPlan == null) {
return null;
}
if (originPlan.getOutputSet().size() != rewrittenPlan.getOutputSet().size()) {
return rewrittenPlan;
}
Plan tmpRewrittenPlan = rewrittenPlan;
// After RBO, slot order may change, so need originSlotToRewrittenExprId which record
// origin plan slot order
List<ExprId> rewrittenPlanOutputsBeforeOptimize =
rewrittenPlan.getOutput().stream().map(Slot::getExprId).collect(Collectors.toList());
// run rbo job on mv rewritten plan
CascadesContext rewrittenPlanContext = CascadesContext.initContext(
cascadesContext.getStatementContext(), rewrittenPlan,
cascadesContext.getCurrentJobContext().getRequiredProperties());
// Tmp old disable rule variable
Set<String> oldDisableRuleNames = rewrittenPlanContext.getStatementContext().getConnectContext()
.getSessionVariable()
.getDisableNereidsRuleNames();
rewrittenPlanContext.getStatementContext().getConnectContext().getSessionVariable()
.setDisableNereidsRules(String.join(",", ImmutableSet.of(RuleType.ADD_DEFAULT_LIMIT.name())));
rewrittenPlanContext.getStatementContext().invalidCache(SessionVariable.DISABLE_NEREIDS_RULES);
List<PlannerHook> removedMaterializedViewHooks = new ArrayList<>();
try {
if (!mvRewrite) {
removedMaterializedViewHooks = removeMaterializedViewHooks(rewrittenPlanContext.getStatementContext());
} else {
// Add MaterializationContext for new cascades context
cascadesContext.getMaterializationContexts().forEach(rewrittenPlanContext::addMaterializationContext);
}
rewrittenPlanContext.getConnectContext().setSkipAuth(true);
AtomicReference<Plan> rewriteResult = new AtomicReference<>();
rewrittenPlanContext.withPlanProcess(cascadesContext.showPlanProcess(), () -> {
rewriteResult.set(planRewriter.apply(rewrittenPlanContext));
});
cascadesContext.addPlanProcesses(rewrittenPlanContext.getPlanProcesses());
rewrittenPlan = rewriteResult.get();
} finally {
rewrittenPlanContext.getConnectContext().setSkipAuth(false);
// Recover old disable rules variable
rewrittenPlanContext.getStatementContext().getConnectContext().getSessionVariable()
.setDisableNereidsRules(String.join(",", oldDisableRuleNames));
rewrittenPlanContext.getStatementContext().invalidCache(SessionVariable.DISABLE_NEREIDS_RULES);
rewrittenPlanContext.getStatementContext().getPlannerHooks().addAll(removedMaterializedViewHooks);
}
if (rewrittenPlan == null) {
return null;
}
if (rewrittenPlan instanceof Sink) {
// can keep the right column order, no need to adjust
return rewrittenPlan;
}
Map<ExprId, Slot> rewrittenPlanAfterOptimizedExprIdToOutputMap = Maps.newLinkedHashMap();
for (Slot slot : rewrittenPlan.getOutput()) {
rewrittenPlanAfterOptimizedExprIdToOutputMap.put(slot.getExprId(), slot);
}
List<ExprId> rewrittenPlanOutputsAfterOptimized = rewrittenPlan.getOutput().stream()
.map(Slot::getExprId).collect(Collectors.toList());
// If project order doesn't change, return rewrittenPlan directly
if (rewrittenPlanOutputsBeforeOptimize.equals(rewrittenPlanOutputsAfterOptimized)) {
return rewrittenPlan;
}
// the expr id would change for some rule, once happened, not check result column order
List<NamedExpression> adjustedOrderProjects = new ArrayList<>();
for (ExprId exprId : rewrittenPlanOutputsBeforeOptimize) {
Slot output = rewrittenPlanAfterOptimizedExprIdToOutputMap.get(exprId);
if (output == null) {
// some rule change the output slot id, would cause error, so not optimize and return tmpRewrittenPlan
return tmpRewrittenPlan;
}
adjustedOrderProjects.add(output);
}
// If project order change, return rewrittenPlan with reordered projects
return new LogicalProject<>(adjustedOrderProjects, rewrittenPlan);
}
/**
* Normalize expression such as nullable property and output slot id
*/
public static Plan normalizeExpressions(Plan rewrittenPlan, Plan originPlan) {
if (rewrittenPlan.getOutput().size() != originPlan.getOutput().size()) {
return null;
}
// normalize nullable
List<NamedExpression> normalizeProjects = new ArrayList<>();
for (int i = 0; i < originPlan.getOutput().size(); i++) {
normalizeProjects.add(normalizeExpression(originPlan.getOutput().get(i), rewrittenPlan.getOutput().get(i)));
}
return new LogicalProject<>(normalizeProjects, rewrittenPlan);
}
/**
* Normalize expression with query, keep the consistency of exprId and nullable props with
* query
* Keep the replacedExpression slot property is the same as the sourceExpression
*/
public static NamedExpression normalizeExpression(
NamedExpression sourceExpression, NamedExpression replacedExpression) {
Expression innerExpression = replacedExpression;
if (replacedExpression.nullable() != sourceExpression.nullable()) {
// if enable join eliminate, query maybe inner join and mv maybe outer join.
// If the slot is at null generate side, the nullable maybe different between query and view
// So need to force to consistent.
innerExpression = sourceExpression.nullable()
? new Nullable(replacedExpression) : new NonNullable(replacedExpression);
}
return new Alias(sourceExpression.getExprId(), innerExpression, sourceExpression.getName());
}
/**
* removeMaterializedViewHooks
*
* @return removed materialized view hooks
*/
public static List<PlannerHook> removeMaterializedViewHooks(StatementContext statementContext) {
List<PlannerHook> tmpMaterializedViewHooks = new ArrayList<>();
Set<PlannerHook> otherHooks = new HashSet<>();
for (PlannerHook hook : statementContext.getPlannerHooks()) {
if (hook instanceof InitMaterializationContextHook) {
tmpMaterializedViewHooks.add(hook);
} else {
otherHooks.add(hook);
}
}
statementContext.clearMaterializedHooksBy(otherHooks);
return tmpMaterializedViewHooks;
}
/**
* Extract nondeterministic function form plan, if the function is in whiteExpressionSet,
* the function would be considered as deterministic function and will not return
* in the result expression result
*/
public static List<Expression> extractNondeterministicFunction(Plan plan) {
List<Expression> nondeterministicFunctions = new ArrayList<>();
plan.accept(NondeterministicFunctionCollector.INSTANCE, nondeterministicFunctions);
return nondeterministicFunctions;
}
/**
* Collect table used partitions, this is used for mv rewrite partition union
* can not cumulative, if called multi times, should clean firstly
*/
public static void collectTableUsedPartitions(Plan plan, CascadesContext cascadesContext) {
// the recorded partition is based on relation id
plan.accept(new QueryPartitionCollector(), cascadesContext);
}
/**
* Decide the statementContext if contain materialized view hook or not
*/
public static boolean containMaterializedViewHook(StatementContext statementContext) {
for (PlannerHook plannerHook : statementContext.getPlannerHooks()) {
// only collect when InitMaterializationContextHook exists in planner hooks
if (plannerHook instanceof InitMaterializationContextHook) {
return true;
}
}
return false;
}
/**
* Calc the chosen materialization context and all table used by final physical plan
*/
public static Pair<Map<List<String>, MaterializationContext>, BitSet> getChosenMaterializationAndUsedTable(
Plan physicalPlan, Map<List<String>, MaterializationContext> materializationContexts) {
final Map<List<String>, MaterializationContext> chosenMaterializationMap = new HashMap<>();
BitSet usedRelation = new BitSet();
physicalPlan.accept(new DefaultPlanVisitor<Void, Map<List<String>, MaterializationContext>>() {
@Override
public Void visitPhysicalCatalogRelation(PhysicalCatalogRelation catalogRelation,
Map<List<String>, MaterializationContext> chosenMaterializationMap) {
usedRelation.set(catalogRelation.getRelationId().asInt());
if (!(catalogRelation instanceof PhysicalOlapScan)) {
return null;
}
PhysicalOlapScan physicalOlapScan = (PhysicalOlapScan) catalogRelation;
OlapTable table = physicalOlapScan.getTable();
List<String> materializationIdentifier
= MaterializationContext.generateMaterializationIdentifierByIndexId(table,
physicalOlapScan.getSelectedIndexId() == table.getBaseIndexId()
? null : physicalOlapScan.getSelectedIndexId());
MaterializationContext materializationContext = materializationContexts.get(materializationIdentifier);
if (materializationContext == null) {
return null;
}
if (materializationContext.isFinalChosen(catalogRelation)) {
chosenMaterializationMap.put(materializationIdentifier, materializationContext);
}
return null;
}
@Override
public Void visitPhysicalRelation(PhysicalRelation physicalRelation,
Map<List<String>, MaterializationContext> context) {
usedRelation.set(physicalRelation.getRelationId().asInt());
return null;
}
}, chosenMaterializationMap);
return Pair.of(chosenMaterializationMap, usedRelation);
}
/**
* Check the query if Contains query operator
* Such sql as following should return true
* select * from orders TABLET(10098) because TABLET(10098) should return true
* select * from orders_partition PARTITION (day_2) because PARTITION (day_2)
* select * from orders index query_index_test because index query_index_test
* select * from orders TABLESAMPLE(20 percent) because TABLESAMPLE(20 percent)
* */
public static final class TableQueryOperatorChecker extends DefaultPlanVisitor<Boolean, Void> {
public static final TableQueryOperatorChecker INSTANCE = new TableQueryOperatorChecker();
@Override
public Boolean visitLogicalRelation(LogicalRelation relation, Void context) {
if (relation instanceof LogicalFileScan && ((LogicalFileScan) relation).getTableSample().isPresent()) {
return true;
}
if (relation instanceof LogicalOlapScan) {
LogicalOlapScan logicalOlapScan = (LogicalOlapScan) relation;
if (logicalOlapScan.getTableSample().isPresent()) {
// Contain sample, select * from orders TABLESAMPLE(20 percent)
return true;
}
if (!logicalOlapScan.getManuallySpecifiedTabletIds().isEmpty()) {
// Contain tablets, select * from orders TABLET(10098) because TABLET(10098)
return true;
}
if (!logicalOlapScan.getManuallySpecifiedPartitions().isEmpty()) {
// Contain specified partitions, select * from orders_partition PARTITION (day_2)
return true;
}
if (logicalOlapScan.getSelectedIndexId() != logicalOlapScan.getTable().getBaseIndexId()) {
// Contains select index or use sync mv in rbo rewrite
// select * from orders index query_index_test
return true;
}
}
return visit(relation, context);
}
@Override
public Boolean visit(Plan plan, Void context) {
for (Plan child : plan.children()) {
Boolean checkResult = child.accept(this, context);
if (checkResult) {
return checkResult;
}
}
return false;
}
}
}