This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.0
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.0 by this push:
new 5d6e99e5130 branch-4.0: [enhancement](load)add LogicalPostProject to
cast outputs according to dest table's schema #57579 (#57940)
5d6e99e5130 is described below
commit 5d6e99e5130acf780257abeb222a0873795b3e12
Author: starocean999 <[email protected]>
AuthorDate: Wed Nov 12 16:38:48 2025 +0800
branch-4.0: [enhancement](load)add LogicalPostProject to cast outputs
according to dest table's schema #57579 (#57940)
picked from #57579
---
.../nereids/load/NereidsLoadPlanInfoCollector.java | 84 +++-----
.../doris/nereids/load/NereidsLoadUtils.java | 48 +++++
.../org/apache/doris/nereids/rules/RuleType.java | 2 +
.../trees/plans/logical/LogicalPostProject.java | 234 +++++++++++++++++++++
.../nereids/trees/plans/visitor/PlanVisitor.java | 5 +
5 files changed, 313 insertions(+), 60 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsLoadPlanInfoCollector.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsLoadPlanInfoCollector.java
index fe014958fc4..210249263a6 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsLoadPlanInfoCollector.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsLoadPlanInfoCollector.java
@@ -38,7 +38,6 @@ import org.apache.doris.common.DdlException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.UserException;
-import org.apache.doris.common.util.FileFormatConstants;
import org.apache.doris.nereids.CascadesContext;
import org.apache.doris.nereids.StatementContext;
import org.apache.doris.nereids.analyzer.Scope;
@@ -63,6 +62,7 @@ import
org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapTableSink;
import org.apache.doris.nereids.trees.plans.logical.LogicalOneRowRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPostProject;
import org.apache.doris.nereids.trees.plans.logical.LogicalPreFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
import org.apache.doris.nereids.trees.plans.visitor.DefaultPlanVisitor;
@@ -220,16 +220,6 @@ public class NereidsLoadPlanInfoCollector extends
DefaultPlanVisitor<Void, PlanT
return params;
}
-
- private String getHeaderType(String formatType) {
- if (formatType != null) {
- if
(formatType.equalsIgnoreCase(FileFormatConstants.FORMAT_CSV_WITH_NAMES)
- ||
formatType.equalsIgnoreCase(FileFormatConstants.FORMAT_CSV_WITH_NAMES_AND_TYPES))
{
- return formatType;
- }
- }
- return "";
- }
}
private LoadPlanInfo loadPlanInfo;
@@ -347,38 +337,6 @@ public class NereidsLoadPlanInfoCollector extends
DefaultPlanVisitor<Void, PlanT
}
}
- // For Broker load with multiple file groups, all file groups share
the same destTuple.
- // Create slots for destTuple only when processing the first file
group (when slots are empty).
- // Subsequent file groups will reuse the slots created by the first
file group.
- if (loadPlanInfo.destTuple.getSlots().isEmpty()) {
- List<Slot> slotList =
outputs.stream().map(NamedExpression::toSlot).collect(Collectors.toList());
-
- // ignore projectList's nullability and set the expr's nullable
info same as
- // dest table column
- // why do this? looks like be works in this way...
- // and we have to do some extra work in visitLogicalFilter because
this ood
- // behavior
- int size = slotList.size();
- List<Slot> newSlotList = new ArrayList<>(size);
- for (int i = 0; i < size; ++i) {
- SlotReference slot = (SlotReference) slotList.get(i);
- Column col = destTable.getColumn(slot.getName());
- if (col != null) {
- slot = slot.withColumn(col);
- if (col.isAutoInc()) {
- newSlotList.add(slot.withNullable(true));
- } else {
- newSlotList.add(slot.withNullable(col.isAllowNull()));
- }
- } else {
- newSlotList.add(slot);
- }
- }
-
- for (Slot slot : newSlotList) {
- context.createSlotDesc(loadPlanInfo.destTuple, (SlotReference)
slot, destTable);
- }
- }
List<SlotDescriptor> slotDescriptorList =
loadPlanInfo.destTuple.getSlots();
loadPlanInfo.destSlotIdToExprMap = Maps.newHashMap();
for (int i = 0; i < slotDescriptorList.size(); ++i) {
@@ -400,16 +358,35 @@ public class NereidsLoadPlanInfoCollector extends
DefaultPlanVisitor<Void, PlanT
return null;
}
+ @Override
+ public Void visitLogicalPostProject(LogicalPostProject<? extends Plan>
logicalPostProject,
+ PlanTranslatorContext context) {
+ List<NamedExpression> outputs = logicalPostProject.getOutputs();
+ for (NamedExpression expr : outputs) {
+ if (expr.containsType(AggregateFunction.class)) {
+ throw new AnalysisException("Don't support aggregation
function in load expression");
+ }
+ }
+
+ // For Broker load with multiple file groups, all file groups share
the same destTuple.
+ // Create slots for destTuple only when processing the first file
group (when slots are empty).
+ // Subsequent file groups will reuse the slots created by the first
file group.
+ if (loadPlanInfo.destTuple.getSlots().isEmpty()) {
+ List<Slot> slotList =
outputs.stream().map(NamedExpression::toSlot).collect(Collectors.toList());
+ for (Slot slot : slotList) {
+ context.createSlotDesc(loadPlanInfo.destTuple, (SlotReference)
slot, destTable);
+ }
+ }
+ logicalPostProject.child().accept(this, context);
+ return null;
+ }
+
@Override
public Void visitLogicalFilter(LogicalFilter<? extends Plan>
logicalFilter, PlanTranslatorContext context) {
logicalFilter.child().accept(this, context);
loadPlanInfo.postFilterExprList = new
ArrayList<>(logicalFilter.getConjuncts().size());
for (Expression conjunct : logicalFilter.getConjuncts()) {
Expr expr = ExpressionTranslator.translate(conjunct, context);
- // in visitLogicalProject, we set project exprs nullability same
as dest table columns
- // the conjunct's nullability is based on project exprs, so we
need clear the nullable info
- // and let conjunct calculate the nullability by itself to get the
correct nullable info
- clearNullableFromNereidsRecursively(expr);
loadPlanInfo.postFilterExprList.add(expr);
}
filterPredicate = logicalFilter.getPredicate();
@@ -428,19 +405,6 @@ public class NereidsLoadPlanInfoCollector extends
DefaultPlanVisitor<Void, PlanT
return null;
}
- /**
- * Recursively clear nullable info from expression and all its children
- */
- private void clearNullableFromNereidsRecursively(Expr expr) {
- if (expr == null) {
- return;
- }
- expr.clearNullableFromNereids();
- for (Expr child : expr.getChildren()) {
- clearNullableFromNereidsRecursively(child);
- }
- }
-
@Override
public Void visitLogicalPreFilter(LogicalPreFilter<? extends Plan>
logicalPreFilter,
PlanTranslatorContext context) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsLoadUtils.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsLoadUtils.java
index d96dfe3c717..bb4a480af98 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsLoadUtils.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsLoadUtils.java
@@ -20,6 +20,7 @@ package org.apache.doris.nereids.load;
import org.apache.doris.analysis.PartitionNames;
import org.apache.doris.catalog.AggregateType;
import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.Table;
import org.apache.doris.common.UserException;
import org.apache.doris.nereids.CascadesContext;
@@ -42,6 +43,7 @@ import
org.apache.doris.nereids.rules.rewrite.OneRewriteRuleFactory;
import org.apache.doris.nereids.trees.expressions.Alias;
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.SlotReference;
import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
import
org.apache.doris.nereids.trees.expressions.functions.scalar.JsonbParseErrorToNull;
@@ -54,6 +56,7 @@ import
org.apache.doris.nereids.trees.plans.logical.LogicalLoadProject;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapTableSink;
import org.apache.doris.nereids.trees.plans.logical.LogicalOneRowRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPostProject;
import org.apache.doris.nereids.trees.plans.logical.LogicalPreFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
import org.apache.doris.nereids.trees.plans.visitor.DefaultPlanVisitor;
@@ -199,13 +202,18 @@ public class NereidsLoadUtils {
CascadesContext cascadesContext = CascadesContext.initContext(new
StatementContext(), currentRootPlan,
PhysicalProperties.ANY);
ConnectContext ctx = cascadesContext.getConnectContext();
+ // we force convert nullable column to non-nullable column for load
+ // so set feDebug to false to avoid AdjustNullableRule report error
+ boolean oldFeDebugValue = ctx.getSessionVariable().feDebug;
try {
ctx.getSessionVariable().setDebugSkipFoldConstant(true);
+ ctx.getSessionVariable().feDebug = false;
Analyzer.buildCustomAnalyzer(cascadesContext,
ImmutableList.of(Analyzer.bottomUp(
new BindExpression(),
new LoadProjectRewrite(fileGroupInfo.getTargetTable()),
new BindSink(false),
+ new AddPostProject(),
new AddPostFilter(
context.fileGroup.getWhereExpr()
),
@@ -236,6 +244,7 @@ public class NereidsLoadUtils {
throw new UserException(exception.getMessage());
} finally {
ctx.getSessionVariable().setDebugSkipFoldConstant(false);
+ ctx.getSessionVariable().feDebug = oldFeDebugValue;
}
return (LogicalPlan) cascadesContext.getRewritePlan();
@@ -324,4 +333,43 @@ public class NereidsLoadUtils {
}).toRule(RuleType.ADD_POST_FILTER_FOR_LOAD);
}
}
+
+ /** AddPostProject
+ * The BindSink rule will produce the final project list for load, we need
cast the outputs according to
+ * dest table's schema
+ * */
+ private static class AddPostProject extends OneRewriteRuleFactory {
+ public AddPostProject() {
+ }
+
+ @Override
+ public Rule build() {
+ return logicalOlapTableSink().whenNot(plan -> plan.child()
instanceof LogicalPostProject
+ || plan.child() instanceof LogicalFilter).thenApply(ctx ->
{
+ LogicalOlapTableSink logicalOlapTableSink = ctx.root;
+ LogicalPlan childPlan = (LogicalPlan)
logicalOlapTableSink.child();
+ List<Slot> childOutputs = childPlan.getOutput();
+ OlapTable destTable =
logicalOlapTableSink.getTargetTable();
+ int size = childOutputs.size();
+ List<SlotReference> projectList = new
ArrayList<>(size);
+ for (int i = 0; i < size; ++i) {
+ SlotReference slot = (SlotReference)
childOutputs.get(i);
+ Column col = destTable.getColumn(slot.getName());
+ if (col != null) {
+ slot = slot.withColumn(col);
+ if (col.isAutoInc()) {
+ projectList.add(slot.withNullable(true));
+ } else {
+
projectList.add(slot.withNullable(col.isAllowNull()));
+ }
+ } else {
+ projectList.add(slot);
+ }
+ }
+ return logicalOlapTableSink.withChildren(
+ Lists.newArrayList(
+ new LogicalPostProject(projectList,
(Plan) logicalOlapTableSink.child(0))));
+ }).toRule(RuleType.ADD_POST_PROJECT_FOR_LOAD);
+ }
+ }
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleType.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleType.java
index 37e357d8f0d..85963bf73a6 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleType.java
@@ -260,6 +260,8 @@ public enum RuleType {
REWRITE_LOAD_PROJECT_FOR_STREAM_LOAD(RuleTypeClass.REWRITE),
// add post filter node for load
ADD_POST_FILTER_FOR_LOAD(RuleTypeClass.REWRITE),
+ // add post project node for load
+ ADD_POST_PROJECT_FOR_LOAD(RuleTypeClass.REWRITE),
// Merge Consecutive plan
MERGE_PROJECTS(RuleTypeClass.REWRITE),
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalPostProject.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalPostProject.java
new file mode 100644
index 00000000000..e3d1ad85296
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalPostProject.java
@@ -0,0 +1,234 @@
+// 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.trees.plans.logical;
+
+import org.apache.doris.nereids.analyzer.Unbound;
+import org.apache.doris.nereids.analyzer.UnboundStar;
+import org.apache.doris.nereids.memo.GroupExpression;
+import org.apache.doris.nereids.properties.DataTrait;
+import org.apache.doris.nereids.properties.LogicalProperties;
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.BoundStar;
+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.NoneMovableFunction;
+import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.algebra.Project;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.nereids.util.Utils;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.ImmutableSet;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Logical post project only use for load plan.
+ */
+public class LogicalPostProject<CHILD_TYPE extends Plan> extends
LogicalUnary<CHILD_TYPE>
+ implements Project, OutputPrunable {
+
+ private final List<NamedExpression> projects;
+ private final Supplier<Set<NamedExpression>> projectsSet;
+ private final boolean isDistinct;
+
+ public LogicalPostProject(List<NamedExpression> projects, CHILD_TYPE
child) {
+ this(projects, false, ImmutableList.of(child));
+ }
+
+ public LogicalPostProject(List<NamedExpression> projects, boolean
isDistinct, List<Plan> child) {
+ this(projects, isDistinct, Optional.empty(), Optional.empty(), child);
+ }
+
+ public LogicalPostProject(List<NamedExpression> projects, boolean
isDistinct, Plan child) {
+ this(projects, isDistinct,
+ Optional.empty(), Optional.empty(), ImmutableList.of(child));
+ }
+
+ private LogicalPostProject(List<NamedExpression> projects, boolean
isDistinct,
+ Optional<GroupExpression> groupExpression,
Optional<LogicalProperties> logicalProperties,
+ List<Plan> child) {
+ super(PlanType.LOGICAL_PROJECT, groupExpression, logicalProperties,
child);
+ Preconditions.checkArgument(projects != null, "projects can not be
null");
+ // only ColumnPrune rule may produce empty projects, this happens in
rewrite phase
+ // so if projects is empty, all plans have been bound already.
+ Preconditions.checkArgument(!projects.isEmpty() || !(child instanceof
Unbound),
+ "projects can not be empty when child plan is unbound");
+ this.projects = projects.isEmpty()
+ ? ImmutableList.of(new Alias(new TinyIntLiteral((byte) 1)))
+ : projects;
+ this.projectsSet = Suppliers.memoize(() ->
ImmutableSet.copyOf(this.projects));
+ this.isDistinct = isDistinct;
+ }
+
+ /**
+ * Get project list.
+ *
+ * @return all project of this node.
+ */
+ @Override
+ public List<NamedExpression> getProjects() {
+ return projects;
+ }
+
+ @Override
+ public List<Slot> computeOutput() {
+ Builder<Slot> slots =
ImmutableList.builderWithExpectedSize(projects.size());
+ for (NamedExpression project : projects) {
+ slots.add(project.toSlot());
+ }
+ return slots.build();
+ }
+
+ @Override
+ public String toString() {
+ return Utils.toSqlString("LogicalPostProject[" + id.asInt() + "]",
+ "distinct", isDistinct,
+ "projects", projects);
+ }
+
+ @Override
+ public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+ return visitor.visitLogicalPostProject(this, context);
+ }
+
+ @Override
+ public List<? extends Expression> getExpressions() {
+ return projects;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ LogicalPostProject<?> that = (LogicalPostProject<?>) o;
+ boolean equal = projectsSet.get().equals(that.projectsSet.get())
+ && isDistinct == that.isDistinct;
+ // TODO: should add exprId for UnBoundStar and BoundStar for equality
comparison
+ if (!projects.isEmpty() && (projects.get(0) instanceof UnboundStar ||
projects.get(0) instanceof BoundStar)) {
+ equal =
child().getLogicalProperties().equals(that.child().getLogicalProperties());
+ }
+ return equal;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(projectsSet.get(), isDistinct);
+ }
+
+ @Override
+ public LogicalPostProject<Plan> withChildren(List<Plan> children) {
+ Preconditions.checkArgument(children.size() == 1);
+ return new LogicalPostProject<>(projects, isDistinct,
Utils.fastToImmutableList(children));
+ }
+
+ @Override
+ public LogicalPostProject<Plan>
withGroupExpression(Optional<GroupExpression> groupExpression) {
+ return new LogicalPostProject<>(projects, isDistinct,
+ groupExpression, Optional.of(getLogicalProperties()),
children);
+ }
+
+ @Override
+ public Plan withGroupExprLogicalPropChildren(Optional<GroupExpression>
groupExpression,
+ Optional<LogicalProperties> logicalProperties, List<Plan>
children) {
+ Preconditions.checkArgument(children.size() == 1);
+ return new LogicalPostProject<>(projects, isDistinct,
+ groupExpression, logicalProperties, children);
+ }
+
+ public LogicalPostProject<Plan> withProjects(List<NamedExpression>
projects) {
+ return new LogicalPostProject<>(projects, isDistinct, children);
+ }
+
+ public LogicalPostProject<Plan> withProjectsAndChild(List<NamedExpression>
projects, Plan child) {
+ return new LogicalPostProject<>(projects, isDistinct,
ImmutableList.of(child));
+ }
+
+ public LogicalPostProject<Plan> withDistinct(boolean isDistinct) {
+ return new LogicalPostProject<>(projects, isDistinct, children);
+ }
+
+ public boolean isDistinct() {
+ return isDistinct;
+ }
+
+ @Override
+ public List<NamedExpression> getOutputs() {
+ return projects;
+ }
+
+ @Override
+ public Plan pruneOutputs(List<NamedExpression> prunedOutputs) {
+ List<NamedExpression> allProjects = new ArrayList<>(prunedOutputs);
+ for (NamedExpression expression : projects) {
+ if (expression.containsType(NoneMovableFunction.class)) {
+ if (!prunedOutputs.contains(expression)) {
+ allProjects.add(expression);
+ }
+ }
+ }
+ return withProjects(allProjects);
+ }
+
+ @Override
+ public JSONObject toJson() {
+ JSONObject logicalProject = super.toJson();
+ JSONObject properties = new JSONObject();
+ properties.put("Projects", projects.toString());
+ properties.put("IsDistinct", isDistinct);
+ logicalProject.put("Properties", properties);
+ return logicalProject;
+ }
+
+ @Override
+ public void computeUnique(DataTrait.Builder builder) {
+ }
+
+ @Override
+ public void computeUniform(DataTrait.Builder builder) {
+ }
+
+ @Override
+ public void computeEqualSet(DataTrait.Builder builder) {
+ }
+
+ @Override
+ public void computeFd(DataTrait.Builder builder) {
+ }
+
+ @Override
+ public boolean canProcessProject(List<NamedExpression> parentProjects) {
+ return false;
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/PlanVisitor.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/PlanVisitor.java
index 291f9ed5d80..2a1c8c4dc59 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/PlanVisitor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/PlanVisitor.java
@@ -40,6 +40,7 @@ import
org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
import org.apache.doris.nereids.trees.plans.logical.LogicalLoadProject;
import org.apache.doris.nereids.trees.plans.logical.LogicalPartitionTopN;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPostProject;
import org.apache.doris.nereids.trees.plans.logical.LogicalPreAggOnHint;
import org.apache.doris.nereids.trees.plans.logical.LogicalPreFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
@@ -227,6 +228,10 @@ public abstract class PlanVisitor<R, C> implements
CommandVisitor<R, C>, Relatio
return visit(project, context);
}
+ public R visitLogicalPostProject(LogicalPostProject<? extends Plan>
project, C context) {
+ return visit(project, context);
+ }
+
public R visitLogicalRepeat(LogicalRepeat<? extends Plan> repeat, C
context) {
return visit(repeat, context);
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]