LogicalPlanBuilder.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.parser;

import org.apache.doris.analysis.ArithmeticExpr.Operator;
import org.apache.doris.analysis.BrokerDesc;
import org.apache.doris.analysis.ColumnNullableType;
import org.apache.doris.analysis.StorageBackend;
import org.apache.doris.analysis.TableName;
import org.apache.doris.analysis.TableScanParams;
import org.apache.doris.analysis.TableSnapshot;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.AggregateType;
import org.apache.doris.catalog.BuiltinAggregateFunctions;
import org.apache.doris.catalog.BuiltinTableGeneratingFunctions;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.KeysType;
import org.apache.doris.catalog.ScalarType;
import org.apache.doris.common.Config;
import org.apache.doris.common.FeConstants;
import org.apache.doris.common.Pair;
import org.apache.doris.common.util.PropertyAnalyzer;
import org.apache.doris.job.common.IntervalUnit;
import org.apache.doris.load.loadv2.LoadTask;
import org.apache.doris.mtmv.MTMVPartitionInfo.MTMVPartitionType;
import org.apache.doris.mtmv.MTMVRefreshEnum.BuildMode;
import org.apache.doris.mtmv.MTMVRefreshEnum.RefreshMethod;
import org.apache.doris.mtmv.MTMVRefreshEnum.RefreshTrigger;
import org.apache.doris.mtmv.MTMVRefreshInfo;
import org.apache.doris.mtmv.MTMVRefreshSchedule;
import org.apache.doris.mtmv.MTMVRefreshTriggerInfo;
import org.apache.doris.nereids.DorisParser;
import org.apache.doris.nereids.DorisParser.AddConstraintContext;
import org.apache.doris.nereids.DorisParser.AggClauseContext;
import org.apache.doris.nereids.DorisParser.AggStateDataTypeContext;
import org.apache.doris.nereids.DorisParser.AliasQueryContext;
import org.apache.doris.nereids.DorisParser.AliasedQueryContext;
import org.apache.doris.nereids.DorisParser.AlterMTMVContext;
import org.apache.doris.nereids.DorisParser.AlterStorageVaultContext;
import org.apache.doris.nereids.DorisParser.AlterSystemRenameComputeGroupContext;
import org.apache.doris.nereids.DorisParser.AlterViewContext;
import org.apache.doris.nereids.DorisParser.ArithmeticBinaryContext;
import org.apache.doris.nereids.DorisParser.ArithmeticUnaryContext;
import org.apache.doris.nereids.DorisParser.ArrayLiteralContext;
import org.apache.doris.nereids.DorisParser.ArraySliceContext;
import org.apache.doris.nereids.DorisParser.BooleanExpressionContext;
import org.apache.doris.nereids.DorisParser.BooleanLiteralContext;
import org.apache.doris.nereids.DorisParser.BracketDistributeTypeContext;
import org.apache.doris.nereids.DorisParser.BracketRelationHintContext;
import org.apache.doris.nereids.DorisParser.BuildModeContext;
import org.apache.doris.nereids.DorisParser.CallProcedureContext;
import org.apache.doris.nereids.DorisParser.CancelMTMVTaskContext;
import org.apache.doris.nereids.DorisParser.CastDataTypeContext;
import org.apache.doris.nereids.DorisParser.CollateContext;
import org.apache.doris.nereids.DorisParser.ColumnDefContext;
import org.apache.doris.nereids.DorisParser.ColumnDefsContext;
import org.apache.doris.nereids.DorisParser.ColumnReferenceContext;
import org.apache.doris.nereids.DorisParser.CommentDistributeTypeContext;
import org.apache.doris.nereids.DorisParser.CommentRelationHintContext;
import org.apache.doris.nereids.DorisParser.ComparisonContext;
import org.apache.doris.nereids.DorisParser.ComplexColTypeContext;
import org.apache.doris.nereids.DorisParser.ComplexColTypeListContext;
import org.apache.doris.nereids.DorisParser.ComplexDataTypeContext;
import org.apache.doris.nereids.DorisParser.ConstantContext;
import org.apache.doris.nereids.DorisParser.CreateIndexAnalyzerContext;
import org.apache.doris.nereids.DorisParser.CreateIndexTokenFilterContext;
import org.apache.doris.nereids.DorisParser.CreateIndexTokenizerContext;
import org.apache.doris.nereids.DorisParser.CreateMTMVContext;
import org.apache.doris.nereids.DorisParser.CreateProcedureContext;
import org.apache.doris.nereids.DorisParser.CreateRowPolicyContext;
import org.apache.doris.nereids.DorisParser.CreateTableContext;
import org.apache.doris.nereids.DorisParser.CreateTableLikeContext;
import org.apache.doris.nereids.DorisParser.CreateViewContext;
import org.apache.doris.nereids.DorisParser.CteContext;
import org.apache.doris.nereids.DorisParser.DataTypeWithNullableContext;
import org.apache.doris.nereids.DorisParser.DecimalLiteralContext;
import org.apache.doris.nereids.DorisParser.DeleteContext;
import org.apache.doris.nereids.DorisParser.DereferenceContext;
import org.apache.doris.nereids.DorisParser.DropCatalogRecycleBinContext;
import org.apache.doris.nereids.DorisParser.DropConstraintContext;
import org.apache.doris.nereids.DorisParser.DropIndexAnalyzerContext;
import org.apache.doris.nereids.DorisParser.DropIndexTokenFilterContext;
import org.apache.doris.nereids.DorisParser.DropIndexTokenizerContext;
import org.apache.doris.nereids.DorisParser.DropMTMVContext;
import org.apache.doris.nereids.DorisParser.DropProcedureContext;
import org.apache.doris.nereids.DorisParser.DropRoleContext;
import org.apache.doris.nereids.DorisParser.ElementAtContext;
import org.apache.doris.nereids.DorisParser.ExistContext;
import org.apache.doris.nereids.DorisParser.ExplainContext;
import org.apache.doris.nereids.DorisParser.ExportContext;
import org.apache.doris.nereids.DorisParser.ExpressionWithEofContext;
import org.apache.doris.nereids.DorisParser.FixedPartitionDefContext;
import org.apache.doris.nereids.DorisParser.FromClauseContext;
import org.apache.doris.nereids.DorisParser.GroupingElementContext;
import org.apache.doris.nereids.DorisParser.GroupingSetContext;
import org.apache.doris.nereids.DorisParser.HavingClauseContext;
import org.apache.doris.nereids.DorisParser.HintAssignmentContext;
import org.apache.doris.nereids.DorisParser.HintStatementContext;
import org.apache.doris.nereids.DorisParser.IdentifierContext;
import org.apache.doris.nereids.DorisParser.IdentifierListContext;
import org.apache.doris.nereids.DorisParser.IdentifierSeqContext;
import org.apache.doris.nereids.DorisParser.InPartitionDefContext;
import org.apache.doris.nereids.DorisParser.IndexDefContext;
import org.apache.doris.nereids.DorisParser.IndexDefsContext;
import org.apache.doris.nereids.DorisParser.InlineTableContext;
import org.apache.doris.nereids.DorisParser.InsertTableContext;
import org.apache.doris.nereids.DorisParser.IntegerLiteralContext;
import org.apache.doris.nereids.DorisParser.IntervalContext;
import org.apache.doris.nereids.DorisParser.Is_not_null_predContext;
import org.apache.doris.nereids.DorisParser.IsnullContext;
import org.apache.doris.nereids.DorisParser.JoinCriteriaContext;
import org.apache.doris.nereids.DorisParser.JoinRelationContext;
import org.apache.doris.nereids.DorisParser.KillQueryContext;
import org.apache.doris.nereids.DorisParser.LambdaExpressionContext;
import org.apache.doris.nereids.DorisParser.LateralViewContext;
import org.apache.doris.nereids.DorisParser.LessThanPartitionDefContext;
import org.apache.doris.nereids.DorisParser.LimitClauseContext;
import org.apache.doris.nereids.DorisParser.LogicalBinaryContext;
import org.apache.doris.nereids.DorisParser.LogicalNotContext;
import org.apache.doris.nereids.DorisParser.MapLiteralContext;
import org.apache.doris.nereids.DorisParser.MultiStatementsContext;
import org.apache.doris.nereids.DorisParser.MultipartIdentifierContext;
import org.apache.doris.nereids.DorisParser.MvPartitionContext;
import org.apache.doris.nereids.DorisParser.NamedExpressionContext;
import org.apache.doris.nereids.DorisParser.NamedExpressionSeqContext;
import org.apache.doris.nereids.DorisParser.NullLiteralContext;
import org.apache.doris.nereids.DorisParser.OutFileClauseContext;
import org.apache.doris.nereids.DorisParser.ParenthesizedExpressionContext;
import org.apache.doris.nereids.DorisParser.PartitionSpecContext;
import org.apache.doris.nereids.DorisParser.PartitionValueDefContext;
import org.apache.doris.nereids.DorisParser.PartitionValueListContext;
import org.apache.doris.nereids.DorisParser.PartitionsDefContext;
import org.apache.doris.nereids.DorisParser.PauseMTMVContext;
import org.apache.doris.nereids.DorisParser.PlanTypeContext;
import org.apache.doris.nereids.DorisParser.PredicateContext;
import org.apache.doris.nereids.DorisParser.PredicatedContext;
import org.apache.doris.nereids.DorisParser.PrimitiveDataTypeContext;
import org.apache.doris.nereids.DorisParser.PropertyClauseContext;
import org.apache.doris.nereids.DorisParser.PropertyItemContext;
import org.apache.doris.nereids.DorisParser.PropertyItemListContext;
import org.apache.doris.nereids.DorisParser.PropertyKeyContext;
import org.apache.doris.nereids.DorisParser.PropertyValueContext;
import org.apache.doris.nereids.DorisParser.QualifiedNameContext;
import org.apache.doris.nereids.DorisParser.QueryContext;
import org.apache.doris.nereids.DorisParser.QueryOrganizationContext;
import org.apache.doris.nereids.DorisParser.QueryTermContext;
import org.apache.doris.nereids.DorisParser.RefreshMTMVContext;
import org.apache.doris.nereids.DorisParser.RefreshMethodContext;
import org.apache.doris.nereids.DorisParser.RefreshScheduleContext;
import org.apache.doris.nereids.DorisParser.RefreshTriggerContext;
import org.apache.doris.nereids.DorisParser.RegularQuerySpecificationContext;
import org.apache.doris.nereids.DorisParser.RelationContext;
import org.apache.doris.nereids.DorisParser.ResumeMTMVContext;
import org.apache.doris.nereids.DorisParser.RollupDefContext;
import org.apache.doris.nereids.DorisParser.RollupDefsContext;
import org.apache.doris.nereids.DorisParser.RowConstructorContext;
import org.apache.doris.nereids.DorisParser.RowConstructorItemContext;
import org.apache.doris.nereids.DorisParser.SampleByPercentileContext;
import org.apache.doris.nereids.DorisParser.SampleByRowsContext;
import org.apache.doris.nereids.DorisParser.SampleContext;
import org.apache.doris.nereids.DorisParser.SelectClauseContext;
import org.apache.doris.nereids.DorisParser.SelectColumnClauseContext;
import org.apache.doris.nereids.DorisParser.SelectHintContext;
import org.apache.doris.nereids.DorisParser.SetOperationContext;
import org.apache.doris.nereids.DorisParser.ShowConfigContext;
import org.apache.doris.nereids.DorisParser.ShowConstraintContext;
import org.apache.doris.nereids.DorisParser.ShowCreateMTMVContext;
import org.apache.doris.nereids.DorisParser.ShowCreateProcedureContext;
import org.apache.doris.nereids.DorisParser.ShowIndexAnalyzerContext;
import org.apache.doris.nereids.DorisParser.ShowIndexTokenFilterContext;
import org.apache.doris.nereids.DorisParser.ShowIndexTokenizerContext;
import org.apache.doris.nereids.DorisParser.ShowProcedureStatusContext;
import org.apache.doris.nereids.DorisParser.ShowViewContext;
import org.apache.doris.nereids.DorisParser.SimpleColumnDefContext;
import org.apache.doris.nereids.DorisParser.SimpleColumnDefsContext;
import org.apache.doris.nereids.DorisParser.SingleStatementContext;
import org.apache.doris.nereids.DorisParser.SortClauseContext;
import org.apache.doris.nereids.DorisParser.SortItemContext;
import org.apache.doris.nereids.DorisParser.StarContext;
import org.apache.doris.nereids.DorisParser.StatementDefaultContext;
import org.apache.doris.nereids.DorisParser.StepPartitionDefContext;
import org.apache.doris.nereids.DorisParser.StringLiteralContext;
import org.apache.doris.nereids.DorisParser.StructLiteralContext;
import org.apache.doris.nereids.DorisParser.SubqueryContext;
import org.apache.doris.nereids.DorisParser.SubqueryExpressionContext;
import org.apache.doris.nereids.DorisParser.SystemVariableContext;
import org.apache.doris.nereids.DorisParser.TableAliasContext;
import org.apache.doris.nereids.DorisParser.TableNameContext;
import org.apache.doris.nereids.DorisParser.TableValuedFunctionContext;
import org.apache.doris.nereids.DorisParser.TypeConstructorContext;
import org.apache.doris.nereids.DorisParser.UnitIdentifierContext;
import org.apache.doris.nereids.DorisParser.UnsupportedContext;
import org.apache.doris.nereids.DorisParser.UpdateAssignmentContext;
import org.apache.doris.nereids.DorisParser.UpdateAssignmentSeqContext;
import org.apache.doris.nereids.DorisParser.UpdateContext;
import org.apache.doris.nereids.DorisParser.UserIdentifyContext;
import org.apache.doris.nereids.DorisParser.UserVariableContext;
import org.apache.doris.nereids.DorisParser.VariantContext;
import org.apache.doris.nereids.DorisParser.VariantPredefinedFieldsContext;
import org.apache.doris.nereids.DorisParser.VariantSubColTypeContext;
import org.apache.doris.nereids.DorisParser.VariantSubColTypeListContext;
import org.apache.doris.nereids.DorisParser.VariantTypeDefinitionsContext;
import org.apache.doris.nereids.DorisParser.VariantWithFieldsAndPropsContext;
import org.apache.doris.nereids.DorisParser.VariantWithOnlyFieldsContext;
import org.apache.doris.nereids.DorisParser.VariantWithOnlyPropsContext;
import org.apache.doris.nereids.DorisParser.WhereClauseContext;
import org.apache.doris.nereids.DorisParser.WindowFrameContext;
import org.apache.doris.nereids.DorisParser.WindowSpecContext;
import org.apache.doris.nereids.DorisParser.WithRemoteStorageSystemContext;
import org.apache.doris.nereids.DorisParserBaseVisitor;
import org.apache.doris.nereids.StatementContext;
import org.apache.doris.nereids.analyzer.UnboundAlias;
import org.apache.doris.nereids.analyzer.UnboundFunction;
import org.apache.doris.nereids.analyzer.UnboundInlineTable;
import org.apache.doris.nereids.analyzer.UnboundRelation;
import org.apache.doris.nereids.analyzer.UnboundResultSink;
import org.apache.doris.nereids.analyzer.UnboundSlot;
import org.apache.doris.nereids.analyzer.UnboundStar;
import org.apache.doris.nereids.analyzer.UnboundTVFRelation;
import org.apache.doris.nereids.analyzer.UnboundTableSinkCreator;
import org.apache.doris.nereids.analyzer.UnboundVariable;
import org.apache.doris.nereids.analyzer.UnboundVariable.VariableType;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.exceptions.NotSupportedException;
import org.apache.doris.nereids.exceptions.ParseException;
import org.apache.doris.nereids.hint.DistributeHint;
import org.apache.doris.nereids.properties.OrderKey;
import org.apache.doris.nereids.properties.SelectHint;
import org.apache.doris.nereids.properties.SelectHintLeading;
import org.apache.doris.nereids.properties.SelectHintOrdered;
import org.apache.doris.nereids.properties.SelectHintSetVar;
import org.apache.doris.nereids.properties.SelectHintUseCboRule;
import org.apache.doris.nereids.trees.TableSample;
import org.apache.doris.nereids.trees.expressions.Add;
import org.apache.doris.nereids.trees.expressions.Alias;
import org.apache.doris.nereids.trees.expressions.And;
import org.apache.doris.nereids.trees.expressions.BitAnd;
import org.apache.doris.nereids.trees.expressions.BitNot;
import org.apache.doris.nereids.trees.expressions.BitOr;
import org.apache.doris.nereids.trees.expressions.BitXor;
import org.apache.doris.nereids.trees.expressions.CaseWhen;
import org.apache.doris.nereids.trees.expressions.Cast;
import org.apache.doris.nereids.trees.expressions.DefaultValueSlot;
import org.apache.doris.nereids.trees.expressions.Divide;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.Exists;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.GreaterThan;
import org.apache.doris.nereids.trees.expressions.GreaterThanEqual;
import org.apache.doris.nereids.trees.expressions.InPredicate;
import org.apache.doris.nereids.trees.expressions.InSubquery;
import org.apache.doris.nereids.trees.expressions.IntegralDivide;
import org.apache.doris.nereids.trees.expressions.IsNull;
import org.apache.doris.nereids.trees.expressions.LessThan;
import org.apache.doris.nereids.trees.expressions.LessThanEqual;
import org.apache.doris.nereids.trees.expressions.Like;
import org.apache.doris.nereids.trees.expressions.ListQuery;
import org.apache.doris.nereids.trees.expressions.MatchAll;
import org.apache.doris.nereids.trees.expressions.MatchAny;
import org.apache.doris.nereids.trees.expressions.MatchPhrase;
import org.apache.doris.nereids.trees.expressions.MatchPhraseEdge;
import org.apache.doris.nereids.trees.expressions.MatchPhrasePrefix;
import org.apache.doris.nereids.trees.expressions.MatchRegexp;
import org.apache.doris.nereids.trees.expressions.Mod;
import org.apache.doris.nereids.trees.expressions.Multiply;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
import org.apache.doris.nereids.trees.expressions.Not;
import org.apache.doris.nereids.trees.expressions.NullSafeEqual;
import org.apache.doris.nereids.trees.expressions.Or;
import org.apache.doris.nereids.trees.expressions.OrderExpression;
import org.apache.doris.nereids.trees.expressions.Placeholder;
import org.apache.doris.nereids.trees.expressions.Properties;
import org.apache.doris.nereids.trees.expressions.Regexp;
import org.apache.doris.nereids.trees.expressions.ScalarSubquery;
import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
import org.apache.doris.nereids.trees.expressions.Subtract;
import org.apache.doris.nereids.trees.expressions.TimestampArithmetic;
import org.apache.doris.nereids.trees.expressions.WhenClause;
import org.apache.doris.nereids.trees.expressions.WindowExpression;
import org.apache.doris.nereids.trees.expressions.WindowFrame;
import org.apache.doris.nereids.trees.expressions.functions.Function;
import org.apache.doris.nereids.trees.expressions.functions.agg.Count;
import org.apache.doris.nereids.trees.expressions.functions.scalar.Array;
import org.apache.doris.nereids.trees.expressions.functions.scalar.ArraySlice;
import org.apache.doris.nereids.trees.expressions.functions.scalar.Char;
import org.apache.doris.nereids.trees.expressions.functions.scalar.ConvertTo;
import org.apache.doris.nereids.trees.expressions.functions.scalar.CurrentDate;
import org.apache.doris.nereids.trees.expressions.functions.scalar.CurrentTime;
import org.apache.doris.nereids.trees.expressions.functions.scalar.CurrentUser;
import org.apache.doris.nereids.trees.expressions.functions.scalar.ElementAt;
import org.apache.doris.nereids.trees.expressions.functions.scalar.EncryptKeyRef;
import org.apache.doris.nereids.trees.expressions.functions.scalar.Lambda;
import org.apache.doris.nereids.trees.expressions.functions.scalar.Now;
import org.apache.doris.nereids.trees.expressions.functions.scalar.SessionUser;
import org.apache.doris.nereids.trees.expressions.functions.scalar.Xor;
import org.apache.doris.nereids.trees.expressions.literal.ArrayLiteral;
import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DateLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DateTimeLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
import org.apache.doris.nereids.trees.expressions.literal.DateV2Literal;
import org.apache.doris.nereids.trees.expressions.literal.DecimalLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DecimalV3Literal;
import org.apache.doris.nereids.trees.expressions.literal.DoubleLiteral;
import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
import org.apache.doris.nereids.trees.expressions.literal.Interval;
import org.apache.doris.nereids.trees.expressions.literal.LargeIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.Literal;
import org.apache.doris.nereids.trees.expressions.literal.MapLiteral;
import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
import org.apache.doris.nereids.trees.expressions.literal.SmallIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
import org.apache.doris.nereids.trees.expressions.literal.StructLiteral;
import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;
import org.apache.doris.nereids.trees.plans.DistributeType;
import org.apache.doris.nereids.trees.plans.JoinType;
import org.apache.doris.nereids.trees.plans.LimitPhase;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.algebra.Aggregate;
import org.apache.doris.nereids.trees.plans.algebra.InlineTable;
import org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
import org.apache.doris.nereids.trees.plans.commands.AddConstraintCommand;
import org.apache.doris.nereids.trees.plans.commands.AlterMTMVCommand;
import org.apache.doris.nereids.trees.plans.commands.AlterStorageVaultCommand;
import org.apache.doris.nereids.trees.plans.commands.AlterSystemRenameComputeGroupCommand;
import org.apache.doris.nereids.trees.plans.commands.AlterViewCommand;
import org.apache.doris.nereids.trees.plans.commands.CallCommand;
import org.apache.doris.nereids.trees.plans.commands.CancelMTMVTaskCommand;
import org.apache.doris.nereids.trees.plans.commands.Command;
import org.apache.doris.nereids.trees.plans.commands.Constraint;
import org.apache.doris.nereids.trees.plans.commands.CreateIndexAnalyzerCommand;
import org.apache.doris.nereids.trees.plans.commands.CreateIndexTokenFilterCommand;
import org.apache.doris.nereids.trees.plans.commands.CreateIndexTokenizerCommand;
import org.apache.doris.nereids.trees.plans.commands.CreateJobCommand;
import org.apache.doris.nereids.trees.plans.commands.CreateMTMVCommand;
import org.apache.doris.nereids.trees.plans.commands.CreatePolicyCommand;
import org.apache.doris.nereids.trees.plans.commands.CreateProcedureCommand;
import org.apache.doris.nereids.trees.plans.commands.CreateTableCommand;
import org.apache.doris.nereids.trees.plans.commands.CreateTableLikeCommand;
import org.apache.doris.nereids.trees.plans.commands.CreateViewCommand;
import org.apache.doris.nereids.trees.plans.commands.DeleteFromCommand;
import org.apache.doris.nereids.trees.plans.commands.DeleteFromUsingCommand;
import org.apache.doris.nereids.trees.plans.commands.DropAnalyzeJobCommand;
import org.apache.doris.nereids.trees.plans.commands.DropCatalogRecycleBinCommand;
import org.apache.doris.nereids.trees.plans.commands.DropCatalogRecycleBinCommand.IdType;
import org.apache.doris.nereids.trees.plans.commands.DropConstraintCommand;
import org.apache.doris.nereids.trees.plans.commands.DropIndexAnalyzerCommand;
import org.apache.doris.nereids.trees.plans.commands.DropIndexTokenFilterCommand;
import org.apache.doris.nereids.trees.plans.commands.DropIndexTokenizerCommand;
import org.apache.doris.nereids.trees.plans.commands.DropMTMVCommand;
import org.apache.doris.nereids.trees.plans.commands.DropProcedureCommand;
import org.apache.doris.nereids.trees.plans.commands.DropRoleCommand;
import org.apache.doris.nereids.trees.plans.commands.ExplainCommand;
import org.apache.doris.nereids.trees.plans.commands.ExplainCommand.ExplainLevel;
import org.apache.doris.nereids.trees.plans.commands.ExportCommand;
import org.apache.doris.nereids.trees.plans.commands.KillAnalyzeJobCommand;
import org.apache.doris.nereids.trees.plans.commands.KillConnectionCommand;
import org.apache.doris.nereids.trees.plans.commands.KillQueryCommand;
import org.apache.doris.nereids.trees.plans.commands.LoadCommand;
import org.apache.doris.nereids.trees.plans.commands.PauseMTMVCommand;
import org.apache.doris.nereids.trees.plans.commands.RefreshMTMVCommand;
import org.apache.doris.nereids.trees.plans.commands.ReplayCommand;
import org.apache.doris.nereids.trees.plans.commands.ResumeMTMVCommand;
import org.apache.doris.nereids.trees.plans.commands.ShowConfigCommand;
import org.apache.doris.nereids.trees.plans.commands.ShowConstraintsCommand;
import org.apache.doris.nereids.trees.plans.commands.ShowCreateMTMVCommand;
import org.apache.doris.nereids.trees.plans.commands.ShowCreateProcedureCommand;
import org.apache.doris.nereids.trees.plans.commands.ShowIndexAnalyzerCommand;
import org.apache.doris.nereids.trees.plans.commands.ShowIndexTokenFilterCommand;
import org.apache.doris.nereids.trees.plans.commands.ShowIndexTokenizerCommand;
import org.apache.doris.nereids.trees.plans.commands.ShowProcedureStatusCommand;
import org.apache.doris.nereids.trees.plans.commands.ShowViewCommand;
import org.apache.doris.nereids.trees.plans.commands.UnsupportedCommand;
import org.apache.doris.nereids.trees.plans.commands.UpdateCommand;
import org.apache.doris.nereids.trees.plans.commands.info.AlterMTMVInfo;
import org.apache.doris.nereids.trees.plans.commands.info.AlterMTMVPropertyInfo;
import org.apache.doris.nereids.trees.plans.commands.info.AlterMTMVRefreshInfo;
import org.apache.doris.nereids.trees.plans.commands.info.AlterMTMVRenameInfo;
import org.apache.doris.nereids.trees.plans.commands.info.AlterMTMVReplaceInfo;
import org.apache.doris.nereids.trees.plans.commands.info.AlterViewInfo;
import org.apache.doris.nereids.trees.plans.commands.info.BulkLoadDataDesc;
import org.apache.doris.nereids.trees.plans.commands.info.BulkStorageDesc;
import org.apache.doris.nereids.trees.plans.commands.info.CancelMTMVTaskInfo;
import org.apache.doris.nereids.trees.plans.commands.info.ColumnDefinition;
import org.apache.doris.nereids.trees.plans.commands.info.CreateJobInfo;
import org.apache.doris.nereids.trees.plans.commands.info.CreateMTMVInfo;
import org.apache.doris.nereids.trees.plans.commands.info.CreateTableInfo;
import org.apache.doris.nereids.trees.plans.commands.info.CreateTableLikeInfo;
import org.apache.doris.nereids.trees.plans.commands.info.CreateViewInfo;
import org.apache.doris.nereids.trees.plans.commands.info.DMLCommandType;
import org.apache.doris.nereids.trees.plans.commands.info.DefaultValue;
import org.apache.doris.nereids.trees.plans.commands.info.DistributionDescriptor;
import org.apache.doris.nereids.trees.plans.commands.info.DropMTMVInfo;
import org.apache.doris.nereids.trees.plans.commands.info.FixedRangePartition;
import org.apache.doris.nereids.trees.plans.commands.info.FuncNameInfo;
import org.apache.doris.nereids.trees.plans.commands.info.GeneratedColumnDesc;
import org.apache.doris.nereids.trees.plans.commands.info.InPartition;
import org.apache.doris.nereids.trees.plans.commands.info.IndexDefinition;
import org.apache.doris.nereids.trees.plans.commands.info.LessThanPartition;
import org.apache.doris.nereids.trees.plans.commands.info.MTMVPartitionDefinition;
import org.apache.doris.nereids.trees.plans.commands.info.PartitionDefinition;
import org.apache.doris.nereids.trees.plans.commands.info.PartitionDefinition.MaxValue;
import org.apache.doris.nereids.trees.plans.commands.info.PartitionTableInfo;
import org.apache.doris.nereids.trees.plans.commands.info.PauseMTMVInfo;
import org.apache.doris.nereids.trees.plans.commands.info.RefreshMTMVInfo;
import org.apache.doris.nereids.trees.plans.commands.info.ResumeMTMVInfo;
import org.apache.doris.nereids.trees.plans.commands.info.RollupDefinition;
import org.apache.doris.nereids.trees.plans.commands.info.ShowCreateMTMVInfo;
import org.apache.doris.nereids.trees.plans.commands.info.SimpleColumnDefinition;
import org.apache.doris.nereids.trees.plans.commands.info.StepPartition;
import org.apache.doris.nereids.trees.plans.commands.info.TableNameInfo;
import org.apache.doris.nereids.trees.plans.commands.insert.BatchInsertIntoTableCommand;
import org.apache.doris.nereids.trees.plans.commands.insert.InsertIntoTableCommand;
import org.apache.doris.nereids.trees.plans.commands.insert.InsertOverwriteTableCommand;
import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
import org.apache.doris.nereids.trees.plans.logical.LogicalCTE;
import org.apache.doris.nereids.trees.plans.logical.LogicalExcept;
import org.apache.doris.nereids.trees.plans.logical.LogicalFileSink;
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalGenerate;
import org.apache.doris.nereids.trees.plans.logical.LogicalHaving;
import org.apache.doris.nereids.trees.plans.logical.LogicalIntersect;
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.LogicalOneRowRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
import org.apache.doris.nereids.trees.plans.logical.LogicalPreAggOnHint;
import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
import org.apache.doris.nereids.trees.plans.logical.LogicalRepeat;
import org.apache.doris.nereids.trees.plans.logical.LogicalSelectHint;
import org.apache.doris.nereids.trees.plans.logical.LogicalSink;
import org.apache.doris.nereids.trees.plans.logical.LogicalSort;
import org.apache.doris.nereids.trees.plans.logical.LogicalSubQueryAlias;
import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
import org.apache.doris.nereids.trees.plans.logical.UsingJoin;
import org.apache.doris.nereids.types.AggStateType;
import org.apache.doris.nereids.types.ArrayType;
import org.apache.doris.nereids.types.BigIntType;
import org.apache.doris.nereids.types.BooleanType;
import org.apache.doris.nereids.types.DataType;
import org.apache.doris.nereids.types.DateTimeType;
import org.apache.doris.nereids.types.DateTimeV2Type;
import org.apache.doris.nereids.types.DateType;
import org.apache.doris.nereids.types.DateV2Type;
import org.apache.doris.nereids.types.LargeIntType;
import org.apache.doris.nereids.types.MapType;
import org.apache.doris.nereids.types.StructField;
import org.apache.doris.nereids.types.StructType;
import org.apache.doris.nereids.types.VarcharType;
import org.apache.doris.nereids.types.VariantField;
import org.apache.doris.nereids.types.VariantType;
import org.apache.doris.nereids.types.coercion.CharacterType;
import org.apache.doris.nereids.util.ExpressionUtils;
import org.apache.doris.nereids.util.RelationUtil;
import org.apache.doris.nereids.util.Utils;
import org.apache.doris.policy.FilterType;
import org.apache.doris.policy.PolicyTypeEnum;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.SqlModeHelper;
import org.apache.doris.system.NodeType;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.RuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.RuleNode;
import org.antlr.v4.runtime.tree.TerminalNode;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * Build a logical plan tree with unbounded nodes.
 */
@SuppressWarnings({"OptionalUsedAsFieldOrParameterType", "OptionalGetWithoutIsPresent"})
public class LogicalPlanBuilder extends DorisParserBaseVisitor<Object> {
    // Sort the parameters with token position to keep the order with original placeholders
    // in prepared statement.Otherwise, the order maybe broken
    private final Map<Token, Placeholder> tokenPosToParameters = Maps.newTreeMap((pos1, pos2) -> {
        int line = pos1.getLine() - pos2.getLine();
        if (line != 0) {
            return line;
        }
        return pos1.getCharPositionInLine() - pos2.getCharPositionInLine();
    });

    private final Map<Integer, ParserRuleContext> selectHintMap;

    public LogicalPlanBuilder(Map<Integer, ParserRuleContext> selectHintMap) {
        this.selectHintMap = selectHintMap;
    }

    @SuppressWarnings("unchecked")
    protected <T> T typedVisit(ParseTree ctx) {
        return (T) ctx.accept(this);
    }

    /**
     * Override the default behavior for all visit methods. This will only return a non-null result
     * when the context has only one child. This is done because there is no generic method to
     * combine the results of the context children. In all other cases null is returned.
     */
    @Override
    public Object visitChildren(RuleNode node) {
        if (node.getChildCount() == 1) {
            return node.getChild(0).accept(this);
        } else {
            return null;
        }
    }

    @Override
    public LogicalPlan visitSingleStatement(SingleStatementContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> (LogicalPlan) visit(ctx.statement()));
    }

    @Override
    public Expression visitExpressionWithEof(ExpressionWithEofContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> typedVisit(ctx.expression()));
    }

    @Override
    public LogicalPlan visitStatementDefault(StatementDefaultContext ctx) {
        LogicalPlan plan = plan(ctx.query());
        if (ctx.outFileClause() != null) {
            plan = withOutFile(plan, ctx.outFileClause());
        } else {
            plan = new UnboundResultSink<>(plan);
        }
        return withExplain(plan, ctx.explain());
    }

    @Override
    public LogicalPlan visitCreateScheduledJob(DorisParser.CreateScheduledJobContext ctx) {
        Optional<String> label = ctx.label == null ? Optional.empty() : Optional.of(ctx.label.getText());
        Optional<String> atTime = ctx.atTime == null ? Optional.empty() : Optional.of(ctx.atTime.getText());
        Optional<Boolean> immediateStartOptional = ctx.CURRENT_TIMESTAMP() == null ? Optional.of(false) :
                Optional.of(true);
        Optional<String> startTime = ctx.startTime == null ? Optional.empty() : Optional.of(ctx.startTime.getText());
        Optional<String> endsTime = ctx.endsTime == null ? Optional.empty() : Optional.of(ctx.endsTime.getText());
        Optional<Long> interval = ctx.timeInterval == null ? Optional.empty() :
                Optional.of(Long.valueOf(ctx.timeInterval.getText()));
        Optional<String> intervalUnit = ctx.timeUnit == null ? Optional.empty() : Optional.of(ctx.timeUnit.getText());
        String comment =
                visitCommentSpec(ctx.commentSpec());
        String executeSql = getOriginSql(ctx.supportedDmlStatement());
        CreateJobInfo createJobInfo = new CreateJobInfo(label, atTime, interval, intervalUnit, startTime,
                endsTime, immediateStartOptional, comment, executeSql);
        return new CreateJobCommand(createJobInfo);
    }

    @Override
    public String visitCommentSpec(DorisParser.CommentSpecContext ctx) {
        String commentSpec = ctx == null ? "''" : ctx.STRING_LITERAL().getText();
        return LogicalPlanBuilderAssistant.escapeBackSlash(commentSpec.substring(1, commentSpec.length() - 1));
    }

    @Override
    public LogicalPlan visitInsertTable(InsertTableContext ctx) {
        boolean isOverwrite = ctx.INTO() == null;
        ImmutableList.Builder<String> tableName = ImmutableList.builder();
        if (null != ctx.tableName) {
            List<String> nameParts = visitMultipartIdentifier(ctx.tableName);
            tableName.addAll(nameParts);
        } else if (null != ctx.tableId) {
            // process group commit insert table command send by be
            TableName name = Env.getCurrentEnv().getCurrentCatalog()
                    .getTableNameByTableId(Long.valueOf(ctx.tableId.getText()));
            tableName.add(name.getDb());
            tableName.add(name.getTbl());
            ConnectContext.get().setDatabase(name.getDb());
        } else {
            throw new ParseException("tableName and tableId cannot both be null");
        }
        Optional<String> labelName = (ctx.labelName == null) ? Optional.empty() : Optional.of(ctx.labelName.getText());
        Optional<String> branchName = Optional.empty();
        if (ctx.optSpecBranch() != null) {
            branchName = Optional.of(ctx.optSpecBranch().name.getText());
        }
        List<String> colNames = ctx.cols == null ? ImmutableList.of() : visitIdentifierList(ctx.cols);
        // TODO visit partitionSpecCtx
        LogicalPlan plan = visitQuery(ctx.query());
        // partitionSpec may be NULL. means auto detect partition. only available when IOT
        Pair<Boolean, List<String>> partitionSpec = visitPartitionSpec(ctx.partitionSpec());
        // partitionSpec.second :
        // null - auto detect
        // zero - whole table
        // others - specific partitions
        boolean isAutoDetect = partitionSpec.second == null;
        LogicalSink<?> sink = UnboundTableSinkCreator.createUnboundTableSinkMaybeOverwrite(
                tableName.build(),
                colNames,
                ImmutableList.of(), // hints
                partitionSpec.first, // isTemp
                partitionSpec.second, // partition names
                isAutoDetect,
                isOverwrite,
                ConnectContext.get().getSessionVariable().isEnableUniqueKeyPartialUpdate(),
                ConnectContext.get().getSessionVariable().getPartialUpdateNewRowPolicy(),
                ctx.tableId == null ? DMLCommandType.INSERT : DMLCommandType.GROUP_COMMIT,
                plan);
        Optional<LogicalPlan> cte = Optional.empty();
        if (ctx.cte() != null) {
            cte = Optional.ofNullable(withCte(plan, ctx.cte()));
        }
        LogicalPlan command;
        if (isOverwrite) {
            command = new InsertOverwriteTableCommand(sink, labelName, cte, branchName);
        } else {
            if (ConnectContext.get() != null && ConnectContext.get().isTxnModel()
                    && sink.child() instanceof InlineTable
                    && sink.child().getExpressions().stream().allMatch(Expression::isConstant)) {
                // FIXME: In legacy, the `insert into select 1` is handled as `insert into values`.
                //  In nereids, the original way is throw an AnalysisException and fallback to legacy.
                //  Now handle it as `insert into select`(a separate load job), should fix it as the legacy.
                command = new BatchInsertIntoTableCommand(sink);
            } else {
                command = new InsertIntoTableCommand(sink, labelName, Optional.empty(), cte, true, branchName);
            }
        }
        return withExplain(command, ctx.explain());
    }

    /**
     * return a pair, first will be true if partitions is temp partition, select is a list to present partition list.
     */
    @Override
    public Pair<Boolean, List<String>> visitPartitionSpec(PartitionSpecContext ctx) {
        List<String> partitions = ImmutableList.of();
        boolean temporaryPartition = false;
        if (ctx != null) {
            temporaryPartition = ctx.TEMPORARY() != null;
            if (ctx.ASTERISK() != null) {
                partitions = null;
            } else if (ctx.partition != null) {
                partitions = ImmutableList.of(ctx.partition.getText());
            } else {
                partitions = visitIdentifierList(ctx.partitions);
            }
        }
        return Pair.of(temporaryPartition, partitions);
    }

    @Override
    public Command visitCreateMTMV(CreateMTMVContext ctx) {
        if (ctx.buildMode() == null && ctx.refreshMethod() == null && ctx.refreshTrigger() == null
                && ctx.cols == null && ctx.keys == null
                && ctx.HASH() == null && ctx.RANDOM() == null && ctx.BUCKETS() == null) {
            // TODO support create sync mv
            return new UnsupportedCommand();
        }

        List<String> nameParts = visitMultipartIdentifier(ctx.mvName);
        BuildMode buildMode = visitBuildMode(ctx.buildMode());
        RefreshMethod refreshMethod = visitRefreshMethod(ctx.refreshMethod());
        MTMVRefreshTriggerInfo refreshTriggerInfo = visitRefreshTrigger(ctx.refreshTrigger());
        LogicalPlan logicalPlan = visitQuery(ctx.query());
        String querySql = getOriginSql(ctx.query());

        int bucketNum = FeConstants.default_bucket_num;
        if (ctx.INTEGER_VALUE() != null) {
            bucketNum = Integer.parseInt(ctx.INTEGER_VALUE().getText());
        }
        DistributionDescriptor desc;
        if (ctx.HASH() != null) {
            desc = new DistributionDescriptor(true, ctx.AUTO() != null, bucketNum,
                    visitIdentifierList(ctx.hashKeys));
        } else {
            desc = new DistributionDescriptor(false, ctx.AUTO() != null, bucketNum, null);
        }

        Map<String, String> properties = ctx.propertyClause() != null
                ? Maps.newHashMap(visitPropertyClause(ctx.propertyClause())) : Maps.newHashMap();
        String comment = ctx.STRING_LITERAL() == null ? "" : LogicalPlanBuilderAssistant.escapeBackSlash(
                ctx.STRING_LITERAL().getText().substring(1, ctx.STRING_LITERAL().getText().length() - 1));

        return new CreateMTMVCommand(new CreateMTMVInfo(ctx.EXISTS() != null, new TableNameInfo(nameParts),
                ctx.keys != null ? visitIdentifierList(ctx.keys) : ImmutableList.of(),
                comment,
                desc, properties, logicalPlan, querySql,
                new MTMVRefreshInfo(buildMode, refreshMethod, refreshTriggerInfo),
                ctx.cols == null ? Lists.newArrayList() : visitSimpleColumnDefs(ctx.cols),
                visitMTMVPartitionInfo(ctx.mvPartition())
        ));
    }

    /**
     * get MTMVPartitionDefinition
     *
     * @param ctx MvPartitionContext
     * @return MTMVPartitionDefinition
     */
    public MTMVPartitionDefinition visitMTMVPartitionInfo(MvPartitionContext ctx) {
        MTMVPartitionDefinition mtmvPartitionDefinition = new MTMVPartitionDefinition();
        if (ctx == null) {
            mtmvPartitionDefinition.setPartitionType(MTMVPartitionType.SELF_MANAGE);
        } else if (ctx.partitionKey != null) {
            mtmvPartitionDefinition.setPartitionType(MTMVPartitionType.FOLLOW_BASE_TABLE);
            mtmvPartitionDefinition.setPartitionCol(ctx.partitionKey.getText());
        } else {
            mtmvPartitionDefinition.setPartitionType(MTMVPartitionType.EXPR);
            Expression functionCallExpression = visitFunctionCallExpression(ctx.partitionExpr);
            mtmvPartitionDefinition.setFunctionCallExpression(functionCallExpression);
        }
        return mtmvPartitionDefinition;
    }

    @Override
    public List<SimpleColumnDefinition> visitSimpleColumnDefs(SimpleColumnDefsContext ctx) {
        if (ctx == null) {
            return null;
        }
        return ctx.cols.stream()
                .map(this::visitSimpleColumnDef)
                .collect(ImmutableList.toImmutableList());
    }

    @Override
    public SimpleColumnDefinition visitSimpleColumnDef(SimpleColumnDefContext ctx) {
        String comment = ctx.STRING_LITERAL() == null ? "" : LogicalPlanBuilderAssistant.escapeBackSlash(
                ctx.STRING_LITERAL().getText().substring(1, ctx.STRING_LITERAL().getText().length() - 1));
        return new SimpleColumnDefinition(ctx.colName.getText(), comment);
    }

    /**
     * get originSql
     *
     * @param ctx context
     * @return originSql
     */
    public String getOriginSql(ParserRuleContext ctx) {
        int startIndex = ctx.start.getStartIndex();
        int stopIndex = ctx.stop.getStopIndex();
        org.antlr.v4.runtime.misc.Interval interval = new org.antlr.v4.runtime.misc.Interval(startIndex, stopIndex);
        return ctx.start.getInputStream().getText(interval);
    }

    @Override
    public MTMVRefreshTriggerInfo visitRefreshTrigger(RefreshTriggerContext ctx) {
        if (ctx == null) {
            return new MTMVRefreshTriggerInfo(RefreshTrigger.MANUAL);
        }
        if (ctx.MANUAL() != null) {
            return new MTMVRefreshTriggerInfo(RefreshTrigger.MANUAL);
        }
        if (ctx.COMMIT() != null) {
            return new MTMVRefreshTriggerInfo(RefreshTrigger.COMMIT);
        }
        if (ctx.SCHEDULE() != null) {
            return new MTMVRefreshTriggerInfo(RefreshTrigger.SCHEDULE, visitRefreshSchedule(ctx.refreshSchedule()));
        }
        return new MTMVRefreshTriggerInfo(RefreshTrigger.MANUAL);
    }

    @Override
    public ReplayCommand visitReplay(DorisParser.ReplayContext ctx) {
        if (ctx.replayCommand().replayType().DUMP() != null) {
            LogicalPlan plan = plan(ctx.replayCommand().replayType().query());
            return new ReplayCommand(PlanType.REPLAY_COMMAND, null, plan, ReplayCommand.ReplayType.DUMP);
        } else if (ctx.replayCommand().replayType().PLAY() != null) {
            String tmpPath = ctx.replayCommand().replayType().filePath.getText();
            String path = LogicalPlanBuilderAssistant.escapeBackSlash(tmpPath.substring(1, tmpPath.length() - 1));
            return new ReplayCommand(PlanType.REPLAY_COMMAND, path, null, ReplayCommand.ReplayType.PLAY);
        }
        return null;
    }

    @Override
    public MTMVRefreshSchedule visitRefreshSchedule(RefreshScheduleContext ctx) {
        int interval = Integer.parseInt(ctx.INTEGER_VALUE().getText());
        String startTime = ctx.STARTS() == null ? null
                : ctx.STRING_LITERAL().getText().substring(1, ctx.STRING_LITERAL().getText().length() - 1);
        IntervalUnit unit = visitMvRefreshUnit(ctx.refreshUnit);
        return new MTMVRefreshSchedule(startTime, interval, unit);
    }

    /**
     * get IntervalUnit,only enable_job_schedule_second_for_test is true, can use second
     *
     * @param ctx ctx
     * @return IntervalUnit
     */
    public IntervalUnit visitMvRefreshUnit(IdentifierContext ctx) {
        IntervalUnit intervalUnit = IntervalUnit.fromString(ctx.getText().toUpperCase());
        if (null == intervalUnit) {
            throw new AnalysisException("interval time unit can not be " + ctx.getText());
        }
        if (intervalUnit.equals(IntervalUnit.SECOND)
                && !Config.enable_job_schedule_second_for_test) {
            throw new AnalysisException("interval time unit can not be second");
        }
        return intervalUnit;
    }

    @Override
    public RefreshMethod visitRefreshMethod(RefreshMethodContext ctx) {
        if (ctx == null) {
            return RefreshMethod.AUTO;
        }
        return RefreshMethod.valueOf(ctx.getText().toUpperCase());
    }

    @Override
    public BuildMode visitBuildMode(BuildModeContext ctx) {
        if (ctx == null) {
            return BuildMode.IMMEDIATE;
        }
        if (ctx.DEFERRED() != null) {
            return BuildMode.DEFERRED;
        } else if (ctx.IMMEDIATE() != null) {
            return BuildMode.IMMEDIATE;
        }
        return BuildMode.IMMEDIATE;
    }

    @Override
    public RefreshMTMVCommand visitRefreshMTMV(RefreshMTMVContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.mvName);
        List<String> partitions = ImmutableList.of();
        if (ctx.partitionSpec() != null) {
            if (ctx.partitionSpec().TEMPORARY() != null) {
                throw new AnalysisException("Not allowed to specify TEMPORARY ");
            }
            if (ctx.partitionSpec().partition != null) {
                partitions = ImmutableList.of(ctx.partitionSpec().partition.getText());
            } else {
                partitions = visitIdentifierList(ctx.partitionSpec().partitions);
            }
        }
        return new RefreshMTMVCommand(new RefreshMTMVInfo(new TableNameInfo(nameParts),
                partitions, ctx.COMPLETE() != null));
    }

    @Override
    public Command visitDropMTMV(DropMTMVContext ctx) {
        if (ctx.tableName != null) {
            // TODO support drop sync mv
            return new UnsupportedCommand();
        }
        List<String> nameParts = visitMultipartIdentifier(ctx.mvName);
        return new DropMTMVCommand(new DropMTMVInfo(new TableNameInfo(nameParts), ctx.EXISTS() != null));
    }

    @Override
    public PauseMTMVCommand visitPauseMTMV(PauseMTMVContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.mvName);
        return new PauseMTMVCommand(new PauseMTMVInfo(new TableNameInfo(nameParts)));
    }

    @Override
    public ResumeMTMVCommand visitResumeMTMV(ResumeMTMVContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.mvName);
        return new ResumeMTMVCommand(new ResumeMTMVInfo(new TableNameInfo(nameParts)));
    }

    @Override
    public ShowCreateMTMVCommand visitShowCreateMTMV(ShowCreateMTMVContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.mvName);
        return new ShowCreateMTMVCommand(new ShowCreateMTMVInfo(new TableNameInfo(nameParts)));
    }

    @Override
    public CancelMTMVTaskCommand visitCancelMTMVTask(CancelMTMVTaskContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.mvName);
        long taskId = Long.parseLong(ctx.taskId.getText());
        return new CancelMTMVTaskCommand(new CancelMTMVTaskInfo(new TableNameInfo(nameParts), taskId));
    }

    @Override
    public AlterMTMVCommand visitAlterMTMV(AlterMTMVContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.mvName);
        TableNameInfo mvName = new TableNameInfo(nameParts);
        AlterMTMVInfo alterMTMVInfo = null;
        if (ctx.RENAME() != null) {
            alterMTMVInfo = new AlterMTMVRenameInfo(mvName, ctx.newName.getText());
        } else if (ctx.REFRESH() != null) {
            MTMVRefreshInfo refreshInfo = new MTMVRefreshInfo();
            if (ctx.refreshMethod() != null) {
                refreshInfo.setRefreshMethod(visitRefreshMethod(ctx.refreshMethod()));
            }
            if (ctx.refreshTrigger() != null) {
                refreshInfo.setRefreshTriggerInfo(visitRefreshTrigger(ctx.refreshTrigger()));
            }
            alterMTMVInfo = new AlterMTMVRefreshInfo(mvName, refreshInfo);
        } else if (ctx.SET() != null) {
            alterMTMVInfo = new AlterMTMVPropertyInfo(mvName,
                    Maps.newHashMap(visitPropertyItemList(ctx.fileProperties)));
        } else if (ctx.REPLACE() != null) {
            String newName = ctx.newName.getText();
            Map<String, String> properties = ctx.propertyClause() != null
                    ? Maps.newHashMap(visitPropertyClause(ctx.propertyClause())) : Maps.newHashMap();
            alterMTMVInfo = new AlterMTMVReplaceInfo(mvName, newName, properties);
        }
        return new AlterMTMVCommand(alterMTMVInfo);
    }

    @Override
    public LogicalPlan visitAlterView(AlterViewContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.name);
        String comment = ctx.commentSpec() == null ? null : visitCommentSpec(ctx.commentSpec());
        AlterViewInfo info;
        if (ctx.query() == null) {
            info = new AlterViewInfo(new TableNameInfo(nameParts), comment);
        } else {
            String querySql = getOriginSql(ctx.query());
            info = new AlterViewInfo(new TableNameInfo(nameParts), comment, querySql,
                    ctx.cols == null ? Lists.newArrayList() : visitSimpleColumnDefs(ctx.cols));
        }
        return new AlterViewCommand(info);
    }

    @Override
    public LogicalPlan visitAlterStorageVault(AlterStorageVaultContext ctx) {
        List<String> nameParts = this.visitMultipartIdentifier(ctx.name);
        String vaultName = nameParts.get(0);
        Map<String, String> properties = this.visitPropertyClause(ctx.properties);
        return new AlterStorageVaultCommand(vaultName, properties);
    }

    @Override
    public LogicalPlan visitAlterSystemRenameComputeGroup(AlterSystemRenameComputeGroupContext ctx) {
        return new AlterSystemRenameComputeGroupCommand(ctx.name.getText(), ctx.newName.getText());
    }

    @Override
    public LogicalPlan visitShowConstraint(ShowConstraintContext ctx) {
        List<String> parts = visitMultipartIdentifier(ctx.table);
        return new ShowConstraintsCommand(parts);
    }

    @Override
    public LogicalPlan visitAddConstraint(AddConstraintContext ctx) {
        List<String> parts = visitMultipartIdentifier(ctx.table);
        UnboundRelation curTable = new UnboundRelation(StatementScopeIdGenerator.newRelationId(), parts);
        ImmutableList<Slot> slots = visitIdentifierList(ctx.constraint().slots).stream()
                .map(UnboundSlot::new)
                .collect(ImmutableList.toImmutableList());
        Constraint constraint;
        if (ctx.constraint().UNIQUE() != null) {
            constraint = Constraint.newUniqueConstraint(curTable, slots);
        } else if (ctx.constraint().PRIMARY() != null) {
            constraint = Constraint.newPrimaryKeyConstraint(curTable, slots);
        } else if (ctx.constraint().FOREIGN() != null) {
            ImmutableList<Slot> referencedSlots = visitIdentifierList(ctx.constraint().referencedSlots).stream()
                    .map(UnboundSlot::new)
                    .collect(ImmutableList.toImmutableList());
            List<String> nameParts = visitMultipartIdentifier(ctx.constraint().referenceTable);
            LogicalPlan referenceTable = new UnboundRelation(StatementScopeIdGenerator.newRelationId(), nameParts);
            constraint = Constraint.newForeignKeyConstraint(curTable, slots, referenceTable, referencedSlots);
        } else {
            throw new AnalysisException("Unsupported constraint " + ctx.getText());
        }
        return new AddConstraintCommand(ctx.constraintName.getText().toLowerCase(), constraint);
    }

    @Override
    public LogicalPlan visitDropConstraint(DropConstraintContext ctx) {
        List<String> parts = visitMultipartIdentifier(ctx.table);
        UnboundRelation curTable = new UnboundRelation(StatementScopeIdGenerator.newRelationId(), parts);
        return new DropConstraintCommand(ctx.constraintName.getText().toLowerCase(), curTable);
    }

    @Override
    public LogicalPlan visitUpdate(UpdateContext ctx) {
        LogicalPlan query = LogicalPlanBuilderAssistant.withCheckPolicy(new UnboundRelation(
                StatementScopeIdGenerator.newRelationId(), visitMultipartIdentifier(ctx.tableName)));
        query = withTableAlias(query, ctx.tableAlias());
        if (ctx.fromClause() != null) {
            query = withRelations(query, ctx.fromClause().relations().relation());
        }
        query = withFilter(query, Optional.ofNullable(ctx.whereClause()));
        String tableAlias = null;
        if (ctx.tableAlias().strictIdentifier() != null) {
            tableAlias = ctx.tableAlias().getText();
        }
        Optional<LogicalPlan> cte = Optional.empty();
        if (ctx.cte() != null) {
            cte = Optional.ofNullable(withCte(query, ctx.cte()));
        }
        return withExplain(new UpdateCommand(visitMultipartIdentifier(ctx.tableName), tableAlias,
                visitUpdateAssignmentSeq(ctx.updateAssignmentSeq()), query, cte), ctx.explain());
    }

    @Override
    public LogicalPlan visitDelete(DeleteContext ctx) {
        List<String> tableName = visitMultipartIdentifier(ctx.tableName);
        Pair<Boolean, List<String>> partitionSpec = visitPartitionSpec(ctx.partitionSpec());
        // TODO: now dont support delete auto detect partition.
        if (partitionSpec == null) {
            throw new ParseException("Now don't support auto detect partitions in deleting", ctx);
        }
        LogicalPlan query = withTableAlias(LogicalPlanBuilderAssistant.withCheckPolicy(
                new UnboundRelation(StatementScopeIdGenerator.newRelationId(), tableName,
                        partitionSpec.second, partitionSpec.first)), ctx.tableAlias());
        String tableAlias = null;
        if (ctx.tableAlias().strictIdentifier() != null) {
            tableAlias = ctx.tableAlias().getText();
        }

        Command deleteCommand;
        if (ctx.USING() == null && ctx.cte() == null) {
            query = withFilter(query, Optional.ofNullable(ctx.whereClause()));
            deleteCommand = new DeleteFromCommand(tableName, tableAlias, partitionSpec.first,
                    partitionSpec.second, query);
        } else {
            // convert to insert into select
            if (ctx.USING() != null) {
                query = withRelations(query, ctx.relations().relation());
            }
            query = withFilter(query, Optional.ofNullable(ctx.whereClause()));
            Optional<LogicalPlan> cte = Optional.empty();
            if (ctx.cte() != null) {
                cte = Optional.ofNullable(withCte(query, ctx.cte()));
            }
            deleteCommand = new DeleteFromUsingCommand(tableName, tableAlias,
                    partitionSpec.first, partitionSpec.second, query, cte);
        }
        if (ctx.explain() != null) {
            return withExplain(deleteCommand, ctx.explain());
        } else {
            return deleteCommand;
        }
    }

    @Override
    public LogicalPlan visitExport(ExportContext ctx) {
        // TODO: replace old class name like ExportStmt, BrokerDesc, Expr with new nereid class name
        List<String> tableName = visitMultipartIdentifier(ctx.tableName);
        List<String> partitions = ctx.partition == null ? ImmutableList.of() : visitIdentifierList(ctx.partition);

        // handle path string
        String tmpPath = ctx.filePath.getText();
        String path = LogicalPlanBuilderAssistant.escapeBackSlash(tmpPath.substring(1, tmpPath.length() - 1));

        Optional<Expression> expr = Optional.empty();
        if (ctx.whereClause() != null) {
            expr = Optional.of(getExpression(ctx.whereClause().booleanExpression()));
        }

        Map<String, String> filePropertiesMap = ImmutableMap.of();
        if (ctx.propertyClause() != null) {
            filePropertiesMap = visitPropertyClause(ctx.propertyClause());
        }

        Optional<BrokerDesc> brokerDesc = Optional.empty();
        if (ctx.withRemoteStorageSystem() != null) {
            brokerDesc = Optional.ofNullable(visitWithRemoteStorageSystem(ctx.withRemoteStorageSystem()));
        }
        return new ExportCommand(tableName, partitions, expr, path, filePropertiesMap, brokerDesc);
    }

    @Override
    public Map<String, String> visitPropertyClause(PropertyClauseContext ctx) {
        return ctx == null ? ImmutableMap.of() : visitPropertyItemList(ctx.fileProperties);
    }

    @Override
    public Map<String, String> visitPropertyItemList(PropertyItemListContext ctx) {
        if (ctx == null || ctx.properties == null) {
            return ImmutableMap.of();
        }
        Builder<String, String> propertiesMap = ImmutableMap.builder();
        for (PropertyItemContext argument : ctx.properties) {
            String key = parsePropertyKey(argument.key);
            String value = parsePropertyValue(argument.value);
            propertiesMap.put(key, value);
        }
        return propertiesMap.build();
    }

    @Override
    public BrokerDesc visitWithRemoteStorageSystem(WithRemoteStorageSystemContext ctx) {
        BrokerDesc brokerDesc = null;

        Map<String, String> brokerPropertiesMap = visitPropertyItemList(ctx.brokerProperties);

        if (ctx.S3() != null) {
            brokerDesc = new BrokerDesc("S3", StorageBackend.StorageType.S3, brokerPropertiesMap);
        } else if (ctx.HDFS() != null) {
            brokerDesc = new BrokerDesc("HDFS", StorageBackend.StorageType.HDFS, brokerPropertiesMap);
        } else if (ctx.LOCAL() != null) {
            brokerDesc = new BrokerDesc("HDFS", StorageBackend.StorageType.LOCAL, brokerPropertiesMap);
        } else if (ctx.BROKER() != null) {
            brokerDesc = new BrokerDesc(visitIdentifierOrText(ctx.brokerName), brokerPropertiesMap);
        }
        return brokerDesc;
    }

    /**
     * Visit multi-statements.
     */
    @Override
    public List<Pair<LogicalPlan, StatementContext>> visitMultiStatements(MultiStatementsContext ctx) {
        List<Pair<LogicalPlan, StatementContext>> logicalPlans = Lists.newArrayList();
        for (org.apache.doris.nereids.DorisParser.StatementContext statement : ctx.statement()) {
            StatementContext statementContext = new StatementContext();
            ConnectContext connectContext = ConnectContext.get();
            if (connectContext != null) {
                connectContext.setStatementContext(statementContext);
                statementContext.setConnectContext(connectContext);
            }
            logicalPlans.add(Pair.of(
                    ParserUtils.withOrigin(ctx, () -> (LogicalPlan) visit(statement)), statementContext));
            List<Placeholder> params = new ArrayList<>(tokenPosToParameters.values());
            statementContext.setPlaceholders(params);
            tokenPosToParameters.clear();
        }
        return logicalPlans;
    }

    /**
     * Visit load-statements.
     */
    @Override
    public LogicalPlan visitLoad(DorisParser.LoadContext ctx) {

        BulkStorageDesc bulkDesc = null;
        if (ctx.withRemoteStorageSystem() != null) {
            Map<String, String> bulkProperties =
                    new HashMap<>(visitPropertyItemList(ctx.withRemoteStorageSystem().brokerProperties));
            if (ctx.withRemoteStorageSystem().S3() != null) {
                bulkDesc = new BulkStorageDesc("S3", BulkStorageDesc.StorageType.S3, bulkProperties);
            } else if (ctx.withRemoteStorageSystem().HDFS() != null) {
                bulkDesc = new BulkStorageDesc("HDFS", BulkStorageDesc.StorageType.HDFS, bulkProperties);
            } else if (ctx.withRemoteStorageSystem().LOCAL() != null) {
                bulkDesc = new BulkStorageDesc("LOCAL_HDFS", BulkStorageDesc.StorageType.LOCAL, bulkProperties);
            } else if (ctx.withRemoteStorageSystem().BROKER() != null
                    && ctx.withRemoteStorageSystem().identifierOrText().getText() != null) {
                bulkDesc = new BulkStorageDesc(ctx.withRemoteStorageSystem().identifierOrText().getText(),
                        bulkProperties);
            }
        }
        ImmutableList.Builder<BulkLoadDataDesc> dataDescriptions = new ImmutableList.Builder<>();
        List<String> labelParts = visitMultipartIdentifier(ctx.lableName);
        String labelName = null;
        String labelDbName = null;
        if (ConnectContext.get().getDatabase().isEmpty() && labelParts.size() == 1) {
            throw new AnalysisException("Current database is not set.");
        } else if (labelParts.size() == 1) {
            labelName = labelParts.get(0);
        } else if (labelParts.size() == 2) {
            labelDbName = labelParts.get(0);
            labelName = labelParts.get(1);
        } else if (labelParts.size() == 3) {
            labelDbName = labelParts.get(1);
            labelName = labelParts.get(2);
        } else {
            throw new AnalysisException("labelParts in load should be [ctl.][db.]label");
        }

        for (DorisParser.DataDescContext ddc : ctx.dataDescs) {
            List<String> nameParts = Lists.newArrayList();
            if (labelDbName != null) {
                nameParts.add(labelDbName);
            }
            nameParts.add(ddc.targetTableName.getText());
            List<String> tableName = RelationUtil.getQualifierName(ConnectContext.get(), nameParts);
            List<String> colNames = (ddc.columns == null ? ImmutableList.of() : visitIdentifierList(ddc.columns));
            List<String> columnsFromPath = (ddc.columnsFromPath == null ? ImmutableList.of()
                    : visitIdentifierList(ddc.columnsFromPath.identifierList()));
            List<String> partitions = ddc.partition == null ? ImmutableList.of() : visitIdentifierList(ddc.partition);
            // TODO: multi location
            List<String> multiFilePaths = new ArrayList<>();
            for (Token filePath : ddc.filePaths) {
                multiFilePaths.add(filePath.getText().substring(1, filePath.getText().length() - 1));
            }
            List<String> filePaths = ddc.filePath == null ? ImmutableList.of() : multiFilePaths;
            Map<String, Expression> colMappings;
            if (ddc.columnMapping == null) {
                colMappings = ImmutableMap.of();
            } else {
                colMappings = new HashMap<>();
                for (DorisParser.MappingExprContext mappingExpr : ddc.columnMapping.mappingSet) {
                    colMappings.put(mappingExpr.mappingCol.getText(), getExpression(mappingExpr.expression()));
                }
            }

            LoadTask.MergeType mergeType = ddc.mergeType() == null ? LoadTask.MergeType.APPEND
                    : LoadTask.MergeType.valueOf(ddc.mergeType().getText());

            Optional<String> fileFormat = ddc.format == null ? Optional.empty()
                    : Optional.of(visitIdentifierOrText(ddc.format));
            Optional<String> separator = ddc.separator == null ? Optional.empty() : Optional.of(ddc.separator.getText()
                    .substring(1, ddc.separator.getText().length() - 1));
            Optional<String> comma = ddc.comma == null ? Optional.empty() : Optional.of(ddc.comma.getText()
                    .substring(1, ddc.comma.getText().length() - 1));
            Map<String, String> dataProperties = ddc.propertyClause() == null ? new HashMap<>()
                    : visitPropertyClause(ddc.propertyClause());
            dataDescriptions.add(new BulkLoadDataDesc(
                    tableName,
                    partitions,
                    filePaths,
                    colNames,
                    columnsFromPath,
                    colMappings,
                    new BulkLoadDataDesc.FileFormatDesc(separator, comma, fileFormat),
                    false,
                    ddc.preFilter == null ? Optional.empty() : Optional.of(getExpression(ddc.preFilter.expression())),
                    ddc.where == null ? Optional.empty() : Optional.of(getExpression(ddc.where.booleanExpression())),
                    mergeType,
                    ddc.deleteOn == null ? Optional.empty() : Optional.of(getExpression(ddc.deleteOn.expression())),
                    ddc.sequenceColumn == null ? Optional.empty()
                            : Optional.of(ddc.sequenceColumn.identifier().getText()), dataProperties));
        }
        Map<String, String> properties = Collections.emptyMap();
        if (ctx.propertyClause() != null) {
            properties = visitPropertyItemList(ctx.propertyClause().propertyItemList());
        }
        String commentSpec = ctx.commentSpec() == null ? "''" : ctx.commentSpec().STRING_LITERAL().getText();
        String comment =
                LogicalPlanBuilderAssistant.escapeBackSlash(commentSpec.substring(1, commentSpec.length() - 1));
        return new LoadCommand(labelName, dataDescriptions.build(), bulkDesc, properties, comment);
    }

    /* ********************************************************************************************
     * Plan parsing
     * ******************************************************************************************** */

    /**
     * process lateral view, add a {@link org.apache.doris.nereids.trees.plans.logical.LogicalGenerate} on plan.
     */
    protected LogicalPlan withGenerate(LogicalPlan plan, LateralViewContext ctx) {
        if (ctx.LATERAL() == null) {
            return plan;
        }
        String generateName = ctx.tableName.getText();
        // if later view explode map type, we need to add a project to convert map to struct
        String columnName = ctx.columnNames.get(0).getText();
        List<String> expandColumnNames = ImmutableList.of();

        // explode can pass multiple columns
        // then use struct to return the result of the expansion of multiple columns.
        if (ctx.columnNames.size() > 1
                || BuiltinTableGeneratingFunctions.INSTANCE.getReturnManyColumnFunctions()
                .contains(ctx.functionName.getText())) {
            columnName = ConnectContext.get() != null
                    ? ConnectContext.get().getStatementContext().generateColumnName() : "expand_cols";
            expandColumnNames = ctx.columnNames.stream()
                    .map(RuleContext::getText).collect(ImmutableList.toImmutableList());
        }
        String functionName = ctx.functionName.getText();
        List<Expression> arguments = ctx.expression().stream()
                .<Expression>map(this::typedVisit)
                .collect(ImmutableList.toImmutableList());
        Function unboundFunction = new UnboundFunction(functionName, arguments);
        return new LogicalGenerate<>(ImmutableList.of(unboundFunction),
                ImmutableList.of(new UnboundSlot(generateName, columnName)), ImmutableList.of(expandColumnNames), plan);
    }

    /**
     * process CTE and store the results in a logical plan node LogicalCTE
     */
    private LogicalPlan withCte(LogicalPlan plan, CteContext ctx) {
        if (ctx == null) {
            return plan;
        }
        return new LogicalCTE<>((List) visit(ctx.aliasQuery(), LogicalSubQueryAlias.class), plan);
    }

    /**
     * process CTE's alias queries and column aliases
     */
    @Override
    public LogicalSubQueryAlias<Plan> visitAliasQuery(AliasQueryContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            LogicalPlan queryPlan = plan(ctx.query());
            Optional<List<String>> columnNames = optionalVisit(ctx.columnAliases(), () ->
                    ctx.columnAliases().identifier().stream()
                            .map(RuleContext::getText)
                            .collect(ImmutableList.toImmutableList())
            );
            return new LogicalSubQueryAlias<>(ctx.identifier().getText(), columnNames, queryPlan);
        });
    }

    @Override
    public Command visitCreateRowPolicy(CreateRowPolicyContext ctx) {
        FilterType filterType = FilterType.of(ctx.type.getText());
        List<String> nameParts = visitMultipartIdentifier(ctx.table);
        return new CreatePolicyCommand(PolicyTypeEnum.ROW, ctx.name.getText(),
                ctx.EXISTS() != null, nameParts, Optional.of(filterType),
                ctx.user == null ? null : visitUserIdentify(ctx.user),
                ctx.roleName == null ? null : ctx.roleName.getText(),
                Optional.of(getExpression(ctx.booleanExpression())), ImmutableMap.of());
    }

    @Override
    public String visitIdentifierOrText(DorisParser.IdentifierOrTextContext ctx) {
        if (ctx.STRING_LITERAL() != null) {
            return ctx.STRING_LITERAL().getText().substring(1, ctx.STRING_LITERAL().getText().length() - 1);
        } else {
            return ctx.identifier().getText();
        }
    }

    @Override
    public UserIdentity visitUserIdentify(UserIdentifyContext ctx) {
        String user = visitIdentifierOrText(ctx.user);
        String host = null;
        if (ctx.host != null) {
            host = visitIdentifierOrText(ctx.host);
        }
        if (host == null) {
            host = "%";
        }
        boolean isDomain = ctx.LEFT_PAREN() != null;
        return new UserIdentity(user, host, isDomain);
    }

    @Override
    public LogicalPlan visitQuery(QueryContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            // TODO: need to add withQueryResultClauses and withCTE
            LogicalPlan query = plan(ctx.queryTerm());
            query = withCte(query, ctx.cte());
            return withQueryOrganization(query, ctx.queryOrganization());
        });
    }

    @Override
    public LogicalPlan visitSetOperation(SetOperationContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {

            if (ctx.UNION() != null) {
                Qualifier qualifier = getQualifier(ctx);
                List<QueryTermContext> contexts = Lists.newArrayList(ctx.right);
                QueryTermContext current = ctx.left;
                while (true) {
                    if (current instanceof SetOperationContext
                            && getQualifier((SetOperationContext) current) == qualifier
                            && ((SetOperationContext) current).UNION() != null) {
                        contexts.add(((SetOperationContext) current).right);
                        current = ((SetOperationContext) current).left;
                    } else {
                        contexts.add(current);
                        break;
                    }
                }
                Collections.reverse(contexts);
                List<LogicalPlan> logicalPlans = contexts.stream().map(this::plan).collect(Collectors.toList());
                return reduceToLogicalPlanTree(0, logicalPlans.size() - 1, logicalPlans, qualifier);
            } else {
                LogicalPlan leftQuery = plan(ctx.left);
                LogicalPlan rightQuery = plan(ctx.right);
                Qualifier qualifier = getQualifier(ctx);

                List<Plan> newChildren = ImmutableList.of(leftQuery, rightQuery);
                LogicalPlan plan;
                if (ctx.UNION() != null) {
                    plan = new LogicalUnion(qualifier, newChildren);
                } else if (ctx.EXCEPT() != null || ctx.MINUS() != null) {
                    plan = new LogicalExcept(qualifier, newChildren);
                } else if (ctx.INTERSECT() != null) {
                    plan = new LogicalIntersect(qualifier, newChildren);
                } else {
                    throw new ParseException("not support", ctx);
                }
                return plan;
            }
        });
    }

    private Qualifier getQualifier(SetOperationContext ctx) {
        if (ctx.setQuantifier() == null || ctx.setQuantifier().DISTINCT() != null) {
            return Qualifier.DISTINCT;
        } else {
            return Qualifier.ALL;
        }
    }

    private static LogicalPlan logicalPlanCombiner(LogicalPlan left, LogicalPlan right, Qualifier qualifier) {
        return new LogicalUnion(qualifier, ImmutableList.of(left, right));
    }

    /**
     * construct avl union tree
     */
    public static LogicalPlan reduceToLogicalPlanTree(int low, int high,
            List<LogicalPlan> logicalPlans, Qualifier qualifier) {
        switch (high - low) {
            case 0:
                return logicalPlans.get(low);
            case 1:
                return logicalPlanCombiner(logicalPlans.get(low), logicalPlans.get(high), qualifier);
            default:
                int mid = low + (high - low) / 2;
                return logicalPlanCombiner(
                        reduceToLogicalPlanTree(low, mid, logicalPlans, qualifier),
                        reduceToLogicalPlanTree(mid + 1, high, logicalPlans, qualifier),
                        qualifier
                );
        }
    }

    @Override
    public LogicalPlan visitSubquery(SubqueryContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> plan(ctx.query()));
    }

    @Override
    public LogicalPlan visitRegularQuerySpecification(RegularQuerySpecificationContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            SelectClauseContext selectCtx = ctx.selectClause();
            LogicalPlan selectPlan;
            LogicalPlan relation;
            if (ctx.fromClause() == null) {
                SelectColumnClauseContext columnCtx = selectCtx.selectColumnClause();
                if (columnCtx.EXCEPT() != null) {
                    throw new ParseException("select-except cannot be used in one row relation", selectCtx);
                }
                relation = new LogicalOneRowRelation(StatementScopeIdGenerator.newRelationId(),
                        ImmutableList.of(new Alias(Literal.of(0))));
            } else {
                relation = visitFromClause(ctx.fromClause());
            }
            if (ctx.intoClause() != null && !ConnectContext.get().isRunProcedure()) {
                throw new ParseException("Only procedure supports insert into variables", selectCtx);
            }
            selectPlan = withSelectQuerySpecification(
                    ctx, relation,
                    selectCtx,
                    Optional.ofNullable(ctx.whereClause()),
                    Optional.ofNullable(ctx.aggClause()),
                    Optional.ofNullable(ctx.havingClause()));
            selectPlan = withQueryOrganization(selectPlan, ctx.queryOrganization());
            if ((selectHintMap == null) || selectHintMap.isEmpty()) {
                return selectPlan;
            }
            List<ParserRuleContext> selectHintContexts = Lists.newArrayList();
            List<ParserRuleContext> preAggOnHintContexts = Lists.newArrayList();
            for (Integer key : selectHintMap.keySet()) {
                if (key > selectCtx.getStart().getStopIndex() && key < selectCtx.getStop().getStartIndex()) {
                    selectHintContexts.add(selectHintMap.get(key));
                } else {
                    preAggOnHintContexts.add(selectHintMap.get(key));
                }
            }
            return withHints(selectPlan, selectHintContexts, preAggOnHintContexts);
        });
    }

    @Override
    public LogicalPlan visitInlineTable(InlineTableContext ctx) {
        List<RowConstructorContext> rowConstructorContexts = ctx.rowConstructor();
        ImmutableList.Builder<List<NamedExpression>> rows
                = ImmutableList.builderWithExpectedSize(rowConstructorContexts.size());
        for (RowConstructorContext rowConstructorContext : rowConstructorContexts) {
            rows.add(visitRowConstructor(rowConstructorContext));
        }
        return new UnboundInlineTable(rows.build());
    }

    /**
     * Create an aliased table reference. This is typically used in FROM clauses.
     */
    protected LogicalPlan withTableAlias(LogicalPlan plan, TableAliasContext ctx) {
        if (ctx.strictIdentifier() == null) {
            return plan;
        }
        return ParserUtils.withOrigin(ctx.strictIdentifier(), () -> {
            String alias = ctx.strictIdentifier().getText();
            if (null != ctx.identifierList()) {
                throw new ParseException("Do not implemented", ctx);
                // TODO: multi-colName
            }
            return new LogicalSubQueryAlias<>(alias, plan);
        });
    }

    @Override
    public LogicalPlan visitTableName(TableNameContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.multipartIdentifier());
        List<String> partitionNames = new ArrayList<>();
        boolean isTempPart = false;
        if (ctx.specifiedPartition() != null) {
            isTempPart = ctx.specifiedPartition().TEMPORARY() != null;
            if (ctx.specifiedPartition().identifier() != null) {
                partitionNames.add(ctx.specifiedPartition().identifier().getText());
            } else {
                partitionNames.addAll(visitIdentifierList(ctx.specifiedPartition().identifierList()));
            }
        }

        Optional<String> indexName = Optional.empty();
        if (ctx.materializedViewName() != null) {
            indexName = Optional.ofNullable(ctx.materializedViewName().indexName.getText());
        }

        List<Long> tabletIdLists = new ArrayList<>();
        if (ctx.tabletList() != null) {
            ctx.tabletList().tabletIdList.stream().forEach(tabletToken -> {
                tabletIdLists.add(Long.parseLong(tabletToken.getText()));
            });
        }

        final List<String> relationHints;
        if (ctx.relationHint() != null) {
            relationHints = typedVisit(ctx.relationHint());
        } else {
            relationHints = ImmutableList.of();
        }

        TableScanParams scanParams = null;
        if (ctx.optScanParams() != null) {
            Map<String, String> map = visitPropertyItemList(ctx.optScanParams().mapParams);
            List<String> list;
            if (ctx.optScanParams().listParams != null) {
                list = visitIdentifierSeq(ctx.optScanParams().listParams);
            } else {
                list = ImmutableList.of();
            }
            scanParams = new TableScanParams(ctx.optScanParams().funcName.getText(), map, list);
        }

        TableSnapshot tableSnapshot = null;
        if (ctx.tableSnapshot() != null) {
            if (ctx.tableSnapshot().TIME() != null) {
                tableSnapshot = TableSnapshot.timeOf(stripQuotes(ctx.tableSnapshot().time.getText()));
            } else {
                tableSnapshot = TableSnapshot.versionOf(stripQuotes(ctx.tableSnapshot().version.getText()));
            }
        }

        TableSample tableSample = ctx.sample() == null ? null : (TableSample) visit(ctx.sample());
        UnboundRelation relation = new UnboundRelation(StatementScopeIdGenerator.newRelationId(),
                nameParts, partitionNames, isTempPart, tabletIdLists, relationHints,
                Optional.ofNullable(tableSample), indexName, scanParams, Optional.ofNullable(tableSnapshot));

        LogicalPlan checkedRelation = LogicalPlanBuilderAssistant.withCheckPolicy(relation);
        LogicalPlan plan = withTableAlias(checkedRelation, ctx.tableAlias());
        for (LateralViewContext lateralViewContext : ctx.lateralView()) {
            plan = withGenerate(plan, lateralViewContext);
        }
        return plan;
    }

    public static String stripQuotes(String str) {
        if ((str.charAt(0) == '\'' && str.charAt(str.length() - 1) == '\'')
                || (str.charAt(0) == '\"' && str.charAt(str.length() - 1) == '\"')) {
            str = str.substring(1, str.length() - 1);
        }
        return str;
    }

    @Override
    public LogicalPlan visitAliasedQuery(AliasedQueryContext ctx) {
        if (ctx.tableAlias().getText().equals("")) {
            throw new ParseException("Every derived table must have its own alias", ctx);
        }
        LogicalPlan plan = withTableAlias(visitQuery(ctx.query()), ctx.tableAlias());
        for (LateralViewContext lateralViewContext : ctx.lateralView()) {
            plan = withGenerate(plan, lateralViewContext);
        }
        return plan;
    }

    @Override
    public LogicalPlan visitTableValuedFunction(TableValuedFunctionContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            String functionName = ctx.tvfName.getText();

            Map<String, String> map = visitPropertyItemList(ctx.properties);
            LogicalPlan relation = new UnboundTVFRelation(StatementScopeIdGenerator.newRelationId(),
                    functionName, new Properties(map));
            return withTableAlias(relation, ctx.tableAlias());
        });
    }

    /**
     * Create a star (i.e. all) expression; this selects all elements (in the specified object).
     * Both un-targeted (global) and targeted aliases are supported.
     */
    @Override
    public Expression visitStar(StarContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            final QualifiedNameContext qualifiedNameContext = ctx.qualifiedName();
            List<String> target;
            if (qualifiedNameContext != null) {
                target = qualifiedNameContext.identifier()
                        .stream()
                        .map(RuleContext::getText)
                        .collect(ImmutableList.toImmutableList());
            } else {
                target = ImmutableList.of();
            }
            return new UnboundStar(target);
        });
    }

    /**
     * Create an aliased expression if an alias is specified. Both single and multi-aliases are
     * supported.
     */
    @Override
    public NamedExpression visitNamedExpression(NamedExpressionContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            Expression expression = getExpression(ctx.expression());
            if (ctx.identifierOrText() == null) {
                if (expression instanceof NamedExpression) {
                    return (NamedExpression) expression;
                } else {
                    int start = ctx.expression().start.getStartIndex();
                    int stop = ctx.expression().stop.getStopIndex();
                    String alias = ctx.start.getInputStream()
                            .getText(new org.antlr.v4.runtime.misc.Interval(start, stop));
                    if (expression instanceof Literal) {
                        if (expression instanceof StringLikeLiteral) {
                            alias = LogicalPlanBuilderAssistant.getStringLiteralAlias((
                                    (StringLikeLiteral) expression).getStringValue());
                        }
                        return new Alias(expression, alias, true);
                    } else {
                        return new UnboundAlias(expression, alias, true);
                    }
                }
            }
            String alias = visitIdentifierOrText(ctx.identifierOrText());
            if (expression instanceof Literal) {
                return new Alias(expression, alias);
            }
            return new UnboundAlias(expression, alias);
        });
    }

    @Override
    public Expression visitSystemVariable(SystemVariableContext ctx) {
        VariableType type = null;
        if (ctx.kind == null) {
            type = VariableType.DEFAULT;
        } else if (ctx.kind.getType() == DorisParser.SESSION) {
            type = VariableType.SESSION;
        } else if (ctx.kind.getType() == DorisParser.GLOBAL) {
            type = VariableType.GLOBAL;
        }
        if (type == null) {
            throw new ParseException("Unsupported system variable: " + ctx.getText(), ctx);
        }
        return new UnboundVariable(ctx.identifier().getText(), type);
    }

    @Override
    public Expression visitUserVariable(UserVariableContext ctx) {
        return new UnboundVariable(ctx.identifierOrText().getText(), VariableType.USER);
    }

    /**
     * Create a comparison expression. This compares two expressions. The following comparison
     * operators are supported:
     * - Equal: '=' or '=='
     * - Null-safe Equal: '<=>'
     * - Not Equal: '<>' or '!='
     * - Less than: '<'
     * - Less then or Equal: '<='
     * - Greater than: '>'
     * - Greater then or Equal: '>='
     */
    @Override
    public Expression visitComparison(ComparisonContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            Expression left = getExpression(ctx.left);
            Expression right = getExpression(ctx.right);
            TerminalNode operator = (TerminalNode) ctx.comparisonOperator().getChild(0);
            switch (operator.getSymbol().getType()) {
                case DorisParser.EQ:
                    return new EqualTo(left, right);
                case DorisParser.NEQ:
                    return new Not(new EqualTo(left, right));
                case DorisParser.LT:
                    return new LessThan(left, right);
                case DorisParser.GT:
                    return new GreaterThan(left, right);
                case DorisParser.LTE:
                    return new LessThanEqual(left, right);
                case DorisParser.GTE:
                    return new GreaterThanEqual(left, right);
                case DorisParser.NSEQ:
                    return new NullSafeEqual(left, right);
                default:
                    throw new ParseException("Unsupported comparison expression: "
                            + operator.getSymbol().getText(), ctx);
            }
        });
    }

    /**
     * Create a not expression.
     * format: NOT Expression
     * for example:
     * not 1
     * not 1=1
     */
    @Override
    public Expression visitLogicalNot(LogicalNotContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> new Not(getExpression(ctx.booleanExpression())));
    }

    @Override
    public Expression visitLogicalBinary(LogicalBinaryContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            // Code block copy from Spark
            // sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala

            // Collect all similar left hand contexts.
            List<BooleanExpressionContext> contexts = Lists.newArrayList(ctx.right);
            BooleanExpressionContext current = ctx.left;
            while (true) {
                if (current instanceof LogicalBinaryContext
                        && ((LogicalBinaryContext) current).operator.getType() == ctx.operator.getType()) {
                    contexts.add(((LogicalBinaryContext) current).right);
                    current = ((LogicalBinaryContext) current).left;
                } else {
                    contexts.add(current);
                    break;
                }
            }
            // Reverse the contexts to have them in the same sequence as in the SQL statement & turn them
            // into expressions.
            Collections.reverse(contexts);
            List<Expression> expressions = contexts.stream().map(this::getExpression).collect(Collectors.toList());
            if (ctx.operator.getType() == DorisParser.AND) {
                return new And(expressions);
            } else if (ctx.operator.getType() == DorisParser.OR) {
                return new Or(expressions);
            } else {
                // Create a balanced tree.
                return reduceToExpressionTree(0, expressions.size() - 1, expressions, ctx);
            }
        });
    }

    @Override
    public Expression visitLambdaExpression(LambdaExpressionContext ctx) {
        ImmutableList<String> args = ctx.args.stream()
                .map(RuleContext::getText)
                .collect(ImmutableList.toImmutableList());
        Expression body = (Expression) visit(ctx.body);
        return new Lambda(args, body);
    }

    private Expression expressionCombiner(Expression left, Expression right, LogicalBinaryContext ctx) {
        switch (ctx.operator.getType()) {
            case DorisParser.LOGICALAND:
            case DorisParser.AND:
                return new And(left, right);
            case DorisParser.OR:
                return new Or(left, right);
            case DorisParser.XOR:
                return new Xor(left, right);
            default:
                throw new ParseException("Unsupported logical binary type: " + ctx.operator.getText(), ctx);
        }
    }

    private Expression reduceToExpressionTree(int low, int high,
            List<Expression> expressions, LogicalBinaryContext ctx) {
        switch (high - low) {
            case 0:
                return expressions.get(low);
            case 1:
                return expressionCombiner(expressions.get(low), expressions.get(high), ctx);
            default:
                int mid = low + (high - low) / 2;
                return expressionCombiner(
                        reduceToExpressionTree(low, mid, expressions, ctx),
                        reduceToExpressionTree(mid + 1, high, expressions, ctx),
                        ctx
                );
        }
    }

    /**
     * Create a predicated expression. A predicated expression is a normal expression with a
     * predicate attached to it, for example:
     * {{{
     * a + 1 IS NULL
     * }}}
     */
    @Override
    public Expression visitPredicated(PredicatedContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            Expression e = getExpression(ctx.valueExpression());
            return ctx.predicate() == null ? e : withPredicate(e, ctx.predicate());
        });
    }

    @Override
    public Expression visitArithmeticUnary(ArithmeticUnaryContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            Expression e = typedVisit(ctx.valueExpression());
            switch (ctx.operator.getType()) {
                case DorisParser.PLUS:
                    return e;
                case DorisParser.SUBTRACT:
                    IntegerLiteral zero = new IntegerLiteral(0);
                    return new Subtract(zero, e);
                case DorisParser.TILDE:
                    return new BitNot(e);
                default:
                    throw new ParseException("Unsupported arithmetic unary type: " + ctx.operator.getText(), ctx);
            }
        });
    }

    @Override
    public Expression visitArithmeticBinary(ArithmeticBinaryContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            Expression left = getExpression(ctx.left);
            Expression right = getExpression(ctx.right);

            int type = ctx.operator.getType();
            if (left instanceof Interval) {
                if (type != DorisParser.PLUS) {
                    throw new ParseException("Only supported: " + Operator.ADD, ctx);
                }
                Interval interval = (Interval) left;
                return new TimestampArithmetic(Operator.ADD, right, interval.value(), interval.timeUnit());
            }

            if (right instanceof Interval) {
                Operator op;
                if (type == DorisParser.PLUS) {
                    op = Operator.ADD;
                } else if (type == DorisParser.SUBTRACT) {
                    op = Operator.SUBTRACT;
                } else {
                    throw new ParseException("Only supported: " + Operator.ADD + " and " + Operator.SUBTRACT, ctx);
                }
                Interval interval = (Interval) right;
                return new TimestampArithmetic(op, left, interval.value(), interval.timeUnit());
            }

            return ParserUtils.withOrigin(ctx, () -> {
                switch (type) {
                    case DorisParser.ASTERISK:
                        return new Multiply(left, right);
                    case DorisParser.SLASH:
                        return new Divide(left, right);
                    case DorisParser.MOD:
                        return new Mod(left, right);
                    case DorisParser.PLUS:
                        return new Add(left, right);
                    case DorisParser.SUBTRACT:
                        return new Subtract(left, right);
                    case DorisParser.DIV:
                        return new IntegralDivide(left, right);
                    case DorisParser.HAT:
                        return new BitXor(left, right);
                    case DorisParser.PIPE:
                        return new BitOr(left, right);
                    case DorisParser.AMPERSAND:
                        return new BitAnd(left, right);
                    default:
                        throw new ParseException(
                                "Unsupported arithmetic binary type: " + ctx.operator.getText(), ctx);
                }
            });
        });
    }

    @Override
    public Expression visitCurrentDate(DorisParser.CurrentDateContext ctx) {
        return new CurrentDate();
    }

    @Override
    public Expression visitCurrentTime(DorisParser.CurrentTimeContext ctx) {
        return new CurrentTime();
    }

    @Override
    public Expression visitCurrentTimestamp(DorisParser.CurrentTimestampContext ctx) {
        return new Now();
    }

    @Override
    public Expression visitLocalTime(DorisParser.LocalTimeContext ctx) {
        return new CurrentTime();
    }

    @Override
    public Expression visitLocalTimestamp(DorisParser.LocalTimestampContext ctx) {
        return new Now();
    }

    @Override
    public Expression visitCurrentUser(DorisParser.CurrentUserContext ctx) {
        return new CurrentUser();
    }

    @Override
    public Expression visitSessionUser(DorisParser.SessionUserContext ctx) {
        return new SessionUser();
    }

    @Override
    public Expression visitDoublePipes(DorisParser.DoublePipesContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            Expression left = getExpression(ctx.left);
            Expression right = getExpression(ctx.right);
            if (SqlModeHelper.hasPipeAsConcat()) {
                return new UnboundFunction("concat", Lists.newArrayList(left, right));
            } else {
                return new Or(left, right);
            }
        });
    }

    /**
     * Create a value based [[CaseWhen]] expression. This has the following SQL form:
     * {{{
     * CASE [expression]
     * WHEN [value] THEN [expression]
     * ...
     * ELSE [expression]
     * END
     * }}}
     */
    @Override
    public Expression visitSimpleCase(DorisParser.SimpleCaseContext context) {
        Expression e = getExpression(context.value);
        List<WhenClause> whenClauses = context.whenClause().stream()
                .map(w -> new WhenClause(new EqualTo(e, getExpression(w.condition)), getExpression(w.result)))
                .collect(ImmutableList.toImmutableList());
        if (context.elseExpression == null) {
            return new CaseWhen(whenClauses);
        }
        return new CaseWhen(whenClauses, getExpression(context.elseExpression));
    }

    /**
     * Create a condition based [[CaseWhen]] expression. This has the following SQL syntax:
     * {{{
     * CASE
     * WHEN [predicate] THEN [expression]
     * ...
     * ELSE [expression]
     * END
     * }}}
     *
     * @param context the parse tree
     */
    @Override
    public Expression visitSearchedCase(DorisParser.SearchedCaseContext context) {
        List<WhenClause> whenClauses = context.whenClause().stream()
                .map(w -> new WhenClause(getExpression(w.condition), getExpression(w.result)))
                .collect(ImmutableList.toImmutableList());
        if (context.elseExpression == null) {
            return new CaseWhen(whenClauses);
        }
        return new CaseWhen(whenClauses, getExpression(context.elseExpression));
    }

    @Override
    public Expression visitCast(DorisParser.CastContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> processCast(getExpression(ctx.expression()), ctx.castDataType()));
    }

    @Override
    public UnboundFunction visitExtract(DorisParser.ExtractContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            String functionName = ctx.field.getText();
            return new UnboundFunction(functionName, false,
                    Collections.singletonList(getExpression(ctx.source)));
        });
    }

    @Override
    public Expression visitEncryptKey(DorisParser.EncryptKeyContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            String db = ctx.dbName == null ? "" : ctx.dbName.getText();
            String key = ctx.keyName.getText();
            return new EncryptKeyRef(new StringLiteral(db), new StringLiteral(key));
        });
    }

    @Override
    public Expression visitCharFunction(DorisParser.CharFunctionContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            String charSet = ctx.charSet == null ? "utf8" : ctx.charSet.getText();
            List<Expression> arguments = ImmutableList.<Expression>builder()
                    .add(new StringLiteral(charSet))
                    .addAll(visit(ctx.arguments, Expression.class))
                    .build();
            return new Char(arguments);
        });
    }

    @Override
    public Expression visitConvertCharSet(DorisParser.ConvertCharSetContext ctx) {
        return ParserUtils.withOrigin(ctx,
                () -> new ConvertTo(getExpression(ctx.argument), new StringLiteral(ctx.charSet.getText())));
    }

    @Override
    public Expression visitConvertType(DorisParser.ConvertTypeContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> processCast(getExpression(ctx.argument), ctx.castDataType()));
    }

    @Override
    public DataType visitCastDataType(CastDataTypeContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            if (ctx.dataType() != null) {
                return ((DataType) typedVisit(ctx.dataType())).conversion();
            } else if (ctx.UNSIGNED() != null) {
                return LargeIntType.UNSIGNED;
            } else {
                return BigIntType.SIGNED;
            }
        });
    }

    private Expression processCast(Expression expression, CastDataTypeContext castDataTypeContext) {
        DataType dataType = visitCastDataType(castDataTypeContext);
        Expression cast = new Cast(expression, dataType, true);
        if (dataType.isStringLikeType() && ((CharacterType) dataType).getLen() >= 0) {
            if (dataType.isVarcharType() && ((VarcharType) dataType).isWildcardVarchar()) {
                return cast;
            }
            List<Expression> args = ImmutableList.of(
                    cast,
                    new TinyIntLiteral((byte) 1),
                    Literal.of(((CharacterType) dataType).getLen())
            );
            return new UnboundFunction("substr", args);
        } else {
            return cast;
        }
    }

    @Override
    public Expression visitFunctionCallExpression(DorisParser.FunctionCallExpressionContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            String functionName = ctx.functionIdentifier().functionNameIdentifier().getText();
            boolean isDistinct = ctx.DISTINCT() != null;
            List<Expression> params = Lists.newArrayList();
            params.addAll(visit(ctx.expression(), Expression.class));
            List<OrderKey> orderKeys = visit(ctx.sortItem(), OrderKey.class);
            params.addAll(orderKeys.stream().map(OrderExpression::new).collect(Collectors.toList()));

            List<UnboundStar> unboundStars = ExpressionUtils.collectAll(params, UnboundStar.class::isInstance);
            if (!unboundStars.isEmpty()) {
                if (ctx.functionIdentifier().dbName == null && functionName.equalsIgnoreCase("count")) {
                    if (unboundStars.size() > 1) {
                        throw new ParseException(
                                "'*' can only be used once in conjunction with COUNT: " + functionName, ctx);
                    }
                    if (!unboundStars.get(0).getQualifier().isEmpty()) {
                        throw new ParseException("'*' can not has qualifier: " + unboundStars.size(), ctx);
                    }
                    if (ctx.windowSpec() != null) {
                        if (isDistinct) {
                            throw new ParseException("DISTINCT not allowed in analytic function: " + functionName, ctx);
                        }
                        return withWindowSpec(ctx.windowSpec(), new Count());
                    }
                    return new Count();
                }
                throw new ParseException("'*' can only be used in conjunction with COUNT: " + functionName, ctx);
            } else {
                String dbName = null;
                if (ctx.functionIdentifier().dbName != null) {
                    dbName = ctx.functionIdentifier().dbName.getText();
                }
                UnboundFunction function = new UnboundFunction(dbName, functionName, isDistinct, params);
                if (ctx.windowSpec() != null) {
                    if (isDistinct) {
                        throw new ParseException("DISTINCT not allowed in analytic function: " + functionName, ctx);
                    }
                    return withWindowSpec(ctx.windowSpec(), function);
                }
                return function;
            }
        });
    }

    /**
     * deal with window function definition
     */
    private WindowExpression withWindowSpec(WindowSpecContext ctx, Expression function) {
        List<Expression> partitionKeyList = Lists.newArrayList();
        if (ctx.partitionClause() != null) {
            partitionKeyList = visit(ctx.partitionClause().expression(), Expression.class);
        }

        List<OrderExpression> orderKeyList = Lists.newArrayList();
        if (ctx.sortClause() != null) {
            orderKeyList = visit(ctx.sortClause().sortItem(), OrderKey.class).stream()
                    .map(orderKey -> new OrderExpression(orderKey))
                    .collect(Collectors.toList());
        }

        if (ctx.windowFrame() != null) {
            return new WindowExpression(function, partitionKeyList, orderKeyList, withWindowFrame(ctx.windowFrame()));
        }
        return new WindowExpression(function, partitionKeyList, orderKeyList);
    }

    /**
     * deal with optional expressions
     */
    private <T, C> Optional<C> optionalVisit(T ctx, Supplier<C> func) {
        return Optional.ofNullable(ctx).map(a -> func.get());
    }

    /**
     * deal with window frame
     */
    private WindowFrame withWindowFrame(WindowFrameContext ctx) {
        WindowFrame.FrameUnitsType frameUnitsType = WindowFrame.FrameUnitsType.valueOf(
                ctx.frameUnits().getText().toUpperCase());
        WindowFrame.FrameBoundary leftBoundary = withFrameBound(ctx.start);
        if (ctx.end != null) {
            WindowFrame.FrameBoundary rightBoundary = withFrameBound(ctx.end);
            return new WindowFrame(frameUnitsType, leftBoundary, rightBoundary);
        }
        return new WindowFrame(frameUnitsType, leftBoundary);
    }

    private WindowFrame.FrameBoundary withFrameBound(DorisParser.FrameBoundaryContext ctx) {
        Optional<Expression> expression = Optional.empty();
        if (ctx.expression() != null) {
            expression = Optional.of(getExpression(ctx.expression()));
            // todo: use isConstant() to resolve Function in expression; currently we only
            //  support literal expression
            if (!expression.get().isLiteral()) {
                throw new ParseException("Unsupported expression in WindowFrame : " + expression, ctx);
            }
        }

        WindowFrame.FrameBoundType frameBoundType = null;
        switch (ctx.boundType.getType()) {
            case DorisParser.PRECEDING:
                if (ctx.UNBOUNDED() != null) {
                    frameBoundType = WindowFrame.FrameBoundType.UNBOUNDED_PRECEDING;
                } else {
                    frameBoundType = WindowFrame.FrameBoundType.PRECEDING;
                }
                break;
            case DorisParser.CURRENT:
                frameBoundType = WindowFrame.FrameBoundType.CURRENT_ROW;
                break;
            case DorisParser.FOLLOWING:
                if (ctx.UNBOUNDED() != null) {
                    frameBoundType = WindowFrame.FrameBoundType.UNBOUNDED_FOLLOWING;
                } else {
                    frameBoundType = WindowFrame.FrameBoundType.FOLLOWING;
                }
                break;
            default:
        }
        return new WindowFrame.FrameBoundary(expression, frameBoundType);
    }

    @Override
    public Expression visitInterval(IntervalContext ctx) {
        return new Interval(getExpression(ctx.value), visitUnitIdentifier(ctx.unit));
    }

    @Override
    public String visitUnitIdentifier(UnitIdentifierContext ctx) {
        return ctx.getText();
    }

    @Override
    public Expression visitTypeConstructor(TypeConstructorContext ctx) {
        String value = ctx.STRING_LITERAL().getText();
        value = value.substring(1, value.length() - 1);
        String type = ctx.type.getText().toUpperCase();
        switch (type) {
            case "DATE":
                try {
                    return Config.enable_date_conversion ? new DateV2Literal(value) : new DateLiteral(value);
                } catch (Exception e) {
                    return new Cast(new StringLiteral(value),
                            Config.enable_date_conversion ? DateV2Type.INSTANCE : DateType.INSTANCE);
                }
            case "TIMESTAMP":
                try {
                    return Config.enable_date_conversion ? new DateTimeV2Literal(value) : new DateTimeLiteral(value);
                } catch (Exception e) {
                    return new Cast(new StringLiteral(value),
                            Config.enable_date_conversion ? DateTimeV2Type.MAX : DateTimeType.INSTANCE);
                }
            case "DATEV2":
                try {
                    return new DateV2Literal(value);
                } catch (Exception e) {
                    return new Cast(new StringLiteral(value), DateV2Type.INSTANCE);
                }
            case "DATEV1":
                try {
                    return new DateLiteral(value);
                } catch (Exception e) {
                    return new Cast(new StringLiteral(value), DateType.INSTANCE);
                }
            default:
                throw new ParseException("Unsupported data type : " + type, ctx);
        }
    }

    @Override
    public Expression visitDereference(DereferenceContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            Expression e = getExpression(ctx.base);
            if (e instanceof UnboundSlot) {
                UnboundSlot unboundAttribute = (UnboundSlot) e;
                List<String> nameParts = Lists.newArrayList(unboundAttribute.getNameParts());
                nameParts.add(ctx.fieldName.getText());
                UnboundSlot slot = new UnboundSlot(nameParts, Optional.empty());
                return slot;
            } else {
                // todo: base is an expression, may be not a table name.
                throw new ParseException("Unsupported dereference expression: " + ctx.getText(), ctx);
            }
        });
    }

    @Override
    public Expression visitElementAt(ElementAtContext ctx) {
        return new ElementAt(typedVisit(ctx.value), typedVisit(ctx.index));
    }

    @Override
    public Expression visitArraySlice(ArraySliceContext ctx) {
        if (ctx.end != null) {
            return new ArraySlice(typedVisit(ctx.value), typedVisit(ctx.begin), typedVisit(ctx.end));
        } else {
            return new ArraySlice(typedVisit(ctx.value), typedVisit(ctx.begin));
        }
    }

    @Override
    public Expression visitColumnReference(ColumnReferenceContext ctx) {
        // todo: handle quoted and unquoted
        return UnboundSlot.quoted(ctx.getText());
    }

    /**
     * Create a NULL literal expression.
     */
    @Override
    public Literal visitNullLiteral(NullLiteralContext ctx) {
        return new NullLiteral();
    }

    @Override
    public Literal visitBooleanLiteral(BooleanLiteralContext ctx) {
        Boolean b = Boolean.valueOf(ctx.getText());
        return BooleanLiteral.of(b);
    }

    @Override
    public Literal visitIntegerLiteral(IntegerLiteralContext ctx) {
        BigInteger bigInt = new BigInteger(ctx.getText());
        if (BigInteger.valueOf(bigInt.byteValue()).equals(bigInt)) {
            return new TinyIntLiteral(bigInt.byteValue());
        } else if (BigInteger.valueOf(bigInt.shortValue()).equals(bigInt)) {
            return new SmallIntLiteral(bigInt.shortValue());
        } else if (BigInteger.valueOf(bigInt.intValue()).equals(bigInt)) {
            return new IntegerLiteral(bigInt.intValue());
        } else if (BigInteger.valueOf(bigInt.longValue()).equals(bigInt)) {
            return new BigIntLiteral(bigInt.longValueExact());
        } else {
            return new LargeIntLiteral(bigInt);
        }
    }

    @Override
    public Literal visitStringLiteral(StringLiteralContext ctx) {
        String txt = ctx.STRING_LITERAL().getText();
        String s = txt.substring(1, txt.length() - 1);
        if (txt.charAt(0) == '\'') {
            // for single quote string, '' should be converted to '
            s = s.replace("''", "'");
        } else if (txt.charAt(0) == '"') {
            // for double quote string, "" should be converted to "
            s = s.replace("\"\"", "\"");
        }
        if (!SqlModeHelper.hasNoBackSlashEscapes()) {
            s = LogicalPlanBuilderAssistant.escapeBackSlash(s);
        }
        int strLength = Utils.containChinese(s) ? s.length() * StringLikeLiteral.CHINESE_CHAR_BYTE_LENGTH : s.length();
        if (strLength > ScalarType.MAX_VARCHAR_LENGTH) {
            return new StringLiteral(s);
        }
        return new VarcharLiteral(s, strLength);
    }

    @Override
    public Expression visitPlaceholder(DorisParser.PlaceholderContext ctx) {
        Placeholder parameter = new Placeholder(ConnectContext.get().getStatementContext().getNextPlaceholderId());
        tokenPosToParameters.put(ctx.start, parameter);
        return parameter;
    }

    /**
     * cast all items to same types.
     * TODO remove this function after we refactor type coercion.
     */
    private List<Literal> typeCoercionItems(List<Literal> items) {
        Array array = new Array(items.toArray(new Literal[0]));
        if (array.expectedInputTypes().isEmpty()) {
            return ImmutableList.of();
        }
        DataType dataType = array.expectedInputTypes().get(0);
        return items.stream()
                .map(item -> item.checkedCastTo(dataType))
                .map(Literal.class::cast)
                .collect(ImmutableList.toImmutableList());
    }

    @Override
    public ArrayLiteral visitArrayLiteral(ArrayLiteralContext ctx) {
        List<Literal> items = ctx.items.stream().<Literal>map(this::typedVisit).collect(Collectors.toList());
        if (items.isEmpty()) {
            return new ArrayLiteral(items);
        }
        return new ArrayLiteral(typeCoercionItems(items));
    }

    @Override
    public MapLiteral visitMapLiteral(MapLiteralContext ctx) {
        List<Literal> items = ctx.items.stream().<Literal>map(this::typedVisit).collect(Collectors.toList());
        if (items.size() % 2 != 0) {
            throw new ParseException("map can't be odd parameters, need even parameters", ctx);
        }
        List<Literal> keys = Lists.newArrayList();
        List<Literal> values = Lists.newArrayList();
        for (int i = 0; i < items.size(); i++) {
            if (i % 2 == 0) {
                keys.add(items.get(i));
            } else {
                values.add(items.get(i));
            }
        }
        return new MapLiteral(typeCoercionItems(keys), typeCoercionItems(values));
    }

    @Override
    public Object visitStructLiteral(StructLiteralContext ctx) {
        List<Literal> fields = ctx.items.stream().<Literal>map(this::typedVisit).collect(Collectors.toList());
        return new StructLiteral(fields);
    }

    @Override
    public Expression visitParenthesizedExpression(ParenthesizedExpressionContext ctx) {
        return getExpression(ctx.expression());
    }

    @Override
    public List<NamedExpression> visitRowConstructor(RowConstructorContext ctx) {
        List<RowConstructorItemContext> rowConstructorItemContexts = ctx.rowConstructorItem();
        ImmutableList.Builder<NamedExpression> columns
                = ImmutableList.builderWithExpectedSize(rowConstructorItemContexts.size());
        for (RowConstructorItemContext rowConstructorItemContext : rowConstructorItemContexts) {
            columns.add(visitRowConstructorItem(rowConstructorItemContext));
        }
        return columns.build();
    }

    @Override
    public NamedExpression visitRowConstructorItem(RowConstructorItemContext ctx) {
        ConstantContext constant = ctx.constant();
        if (constant != null) {
            return new Alias((Expression) constant.accept(this));
        } else if (ctx.DEFAULT() != null) {
            return new DefaultValueSlot();
        } else {
            return visitNamedExpression(ctx.namedExpression());
        }
    }

    @Override
    public List<Expression> visitNamedExpressionSeq(NamedExpressionSeqContext namedCtx) {
        return visit(namedCtx.namedExpression(), Expression.class);
    }

    @Override
    public LogicalPlan visitRelation(RelationContext ctx) {
        return plan(ctx.relationPrimary());
    }

    @Override
    public LogicalPlan visitFromClause(FromClauseContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> visitRelations(ctx.relations()));
    }

    @Override
    public LogicalPlan visitRelations(DorisParser.RelationsContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> withRelations(null, ctx.relation()));
    }

    @Override
    public LogicalPlan visitRelationList(DorisParser.RelationListContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> withRelations(null, ctx.relations().relation()));
    }

    /* ********************************************************************************************
     * Table Identifier parsing
     * ******************************************************************************************** */

    @Override
    public List<String> visitMultipartIdentifier(MultipartIdentifierContext ctx) {
        return ctx.parts.stream()
                .map(RuleContext::getText)
                .collect(ImmutableList.toImmutableList());
    }

    /**
     * Create a Sequence of Strings for a parenthesis enclosed alias list.
     */
    @Override
    public List<String> visitIdentifierList(IdentifierListContext ctx) {
        return visitIdentifierSeq(ctx.identifierSeq());
    }

    /**
     * Create a Sequence of Strings for an identifier list.
     */
    @Override
    public List<String> visitIdentifierSeq(IdentifierSeqContext ctx) {
        return ctx.ident.stream()
                .map(RuleContext::getText)
                .collect(ImmutableList.toImmutableList());
    }

    @Override
    public EqualTo visitUpdateAssignment(UpdateAssignmentContext ctx) {
        return new EqualTo(new UnboundSlot(visitMultipartIdentifier(ctx.multipartIdentifier()), Optional.empty()),
                getExpression(ctx.expression()));
    }

    @Override
    public List<EqualTo> visitUpdateAssignmentSeq(UpdateAssignmentSeqContext ctx) {
        return ctx.assignments.stream()
                .map(this::visitUpdateAssignment)
                .collect(Collectors.toList());
    }

    /**
     * get OrderKey.
     *
     * @param ctx SortItemContext
     * @return SortItems
     */
    @Override
    public OrderKey visitSortItem(SortItemContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            boolean isAsc = ctx.DESC() == null;
            boolean isNullFirst = ctx.FIRST() != null || (ctx.LAST() == null && isAsc);
            Expression expression = typedVisit(ctx.expression());
            return new OrderKey(expression, isAsc, isNullFirst);
        });
    }

    private <T> List<T> visit(List<? extends ParserRuleContext> contexts, Class<T> clazz) {
        return contexts.stream()
                .map(this::visit)
                .map(clazz::cast)
                .collect(ImmutableList.toImmutableList());
    }

    private LogicalPlan plan(ParserRuleContext tree) {
        return (LogicalPlan) tree.accept(this);
    }

    /* ********************************************************************************************
     * create table parsing
     * ******************************************************************************************** */

    @Override
    public LogicalPlan visitCreateView(CreateViewContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.name);
        String comment = ctx.STRING_LITERAL() == null ? "" : LogicalPlanBuilderAssistant.escapeBackSlash(
                ctx.STRING_LITERAL().getText().substring(1, ctx.STRING_LITERAL().getText().length() - 1));
        String querySql = getOriginSql(ctx.query());
        if (ctx.REPLACE() != null && ctx.EXISTS() != null) {
            throw new AnalysisException("[OR REPLACE] and [IF NOT EXISTS] cannot used at the same time");
        }
        CreateViewInfo info = new CreateViewInfo(ctx.EXISTS() != null, ctx.REPLACE() != null,
                new TableNameInfo(nameParts),
                comment, querySql,
                ctx.cols == null ? Lists.newArrayList() : visitSimpleColumnDefs(ctx.cols));
        return new CreateViewCommand(info);
    }

    @Override
    public LogicalPlan visitCreateTable(CreateTableContext ctx) {
        String ctlName = null;
        String dbName = null;
        String tableName = null;
        List<String> nameParts = visitMultipartIdentifier(ctx.name);
        // TODO: support catalog
        if (nameParts.size() == 1) {
            // dbName should be set
            dbName = ConnectContext.get().getDatabase();
            tableName = nameParts.get(0);
        } else if (nameParts.size() == 2) {
            dbName = nameParts.get(0);
            tableName = nameParts.get(1);
        } else if (nameParts.size() == 3) {
            ctlName = nameParts.get(0);
            dbName = nameParts.get(1);
            tableName = nameParts.get(2);
        } else {
            throw new AnalysisException("nameParts in create table should be [ctl.][db.]tbl");
        }
        KeysType keysType = null;
        if (ctx.DUPLICATE() != null) {
            keysType = KeysType.DUP_KEYS;
        } else if (ctx.AGGREGATE() != null) {
            keysType = KeysType.AGG_KEYS;
        } else if (ctx.UNIQUE() != null) {
            keysType = KeysType.UNIQUE_KEYS;
        }
        // when engineName is null, get engineName from current catalog later
        String engineName = ctx.engine != null ? ctx.engine.getText().toLowerCase() : null;
        int bucketNum = FeConstants.default_bucket_num;
        if (ctx.INTEGER_VALUE() != null) {
            bucketNum = Integer.parseInt(ctx.INTEGER_VALUE().getText());
        }
        String comment = ctx.STRING_LITERAL() == null ? "" : LogicalPlanBuilderAssistant.escapeBackSlash(
                ctx.STRING_LITERAL().getText().substring(1, ctx.STRING_LITERAL().getText().length() - 1));
        DistributionDescriptor desc = null;
        if (ctx.HASH() != null) {
            desc = new DistributionDescriptor(true, ctx.autoBucket != null, bucketNum,
                    visitIdentifierList(ctx.hashKeys));
        } else if (ctx.RANDOM() != null) {
            desc = new DistributionDescriptor(false, ctx.autoBucket != null, bucketNum, null);
        }
        Map<String, String> properties = ctx.properties != null
                // NOTICE: we should not generate immutable map here, because it will be modified when analyzing.
                ? Maps.newHashMap(visitPropertyClause(ctx.properties))
                : Maps.newHashMap();
        Map<String, String> extProperties = ctx.extProperties != null
                // NOTICE: we should not generate immutable map here, because it will be modified when analyzing.
                ? Maps.newHashMap(visitPropertyClause(ctx.extProperties))
                : Maps.newHashMap();

        // solve partition by
        PartitionTableInfo partitionInfo;
        if (ctx.partition != null) {
            partitionInfo = (PartitionTableInfo) ctx.partitionTable().accept(this);
        } else {
            partitionInfo = PartitionTableInfo.EMPTY;
        }

        if (ctx.columnDefs() != null) {
            if (ctx.AS() != null) {
                throw new AnalysisException("Should not define the entire column in CTAS");
            }
            return new CreateTableCommand(Optional.empty(), new CreateTableInfo(
                    ctx.EXISTS() != null,
                    ctx.EXTERNAL() != null,
                    ctx.TEMPORARY() != null,
                    ctlName,
                    dbName,
                    tableName,
                    visitColumnDefs(ctx.columnDefs()),
                    ctx.indexDefs() != null ? visitIndexDefs(ctx.indexDefs()) : ImmutableList.of(),
                    engineName,
                    keysType,
                    ctx.keys != null ? visitIdentifierList(ctx.keys) : ImmutableList.of(),
                    comment,
                    partitionInfo,
                    desc,
                    ctx.rollupDefs() != null ? visitRollupDefs(ctx.rollupDefs()) : ImmutableList.of(),
                    properties,
                    extProperties,
                    ctx.clusterKeys != null ? visitIdentifierList(ctx.clusterKeys) : ImmutableList.of()));
        } else if (ctx.AS() != null) {
            return new CreateTableCommand(Optional.of(visitQuery(ctx.query())), new CreateTableInfo(
                    ctx.EXISTS() != null,
                    ctx.EXTERNAL() != null,
                    ctx.TEMPORARY() != null,
                    ctlName,
                    dbName,
                    tableName,
                    ctx.ctasCols != null ? visitIdentifierList(ctx.ctasCols) : null,
                    engineName,
                    keysType,
                    ctx.keys != null ? visitIdentifierList(ctx.keys) : ImmutableList.of(),
                    comment,
                    partitionInfo,
                    desc,
                    ctx.rollupDefs() != null ? visitRollupDefs(ctx.rollupDefs()) : ImmutableList.of(),
                    properties,
                    extProperties,
                    ctx.clusterKeys != null ? visitIdentifierList(ctx.clusterKeys) : ImmutableList.of()));
        } else {
            throw new AnalysisException("Should contain at least one column in a table");
        }
    }

    @Override
    public PartitionTableInfo visitPartitionTable(DorisParser.PartitionTableContext ctx) {
        boolean isAutoPartition = ctx.autoPartition != null;
        ImmutableList<Expression> partitionList = ctx.partitionList.identityOrFunction().stream()
                .map(partition -> {
                    IdentifierContext identifier = partition.identifier();
                    if (identifier != null) {
                        return UnboundSlot.quoted(identifier.getText());
                    } else {
                        return visitFunctionCallExpression(partition.functionCallExpression());
                    }
                })
                .collect(ImmutableList.toImmutableList());
        return new PartitionTableInfo(
                isAutoPartition,
                ctx.RANGE() != null ? "RANGE" : "LIST",
                ctx.partitions != null ? visitPartitionsDef(ctx.partitions) : null,
                partitionList);
    }

    @Override
    public List<ColumnDefinition> visitColumnDefs(ColumnDefsContext ctx) {
        return ctx.cols.stream().map(this::visitColumnDef).collect(Collectors.toList());
    }

    @Override
    public ColumnDefinition visitColumnDef(ColumnDefContext ctx) {
        String colName = ctx.colName.getText();
        DataType colType = ctx.type instanceof PrimitiveDataTypeContext
                ? visitPrimitiveDataType(((PrimitiveDataTypeContext) ctx.type))
                : ctx.type instanceof ComplexDataTypeContext
                        ? visitComplexDataType((ComplexDataTypeContext) ctx.type)
                    : ctx.type instanceof VariantPredefinedFieldsContext
                            ? visitVariantPredefinedFields((VariantPredefinedFieldsContext) ctx.type)
                        : visitAggStateDataType((AggStateDataTypeContext) ctx.type);
        colType = colType.conversion();
        boolean isKey = ctx.KEY() != null;
        ColumnNullableType nullableType = ColumnNullableType.DEFAULT;
        if (ctx.NOT() != null) {
            nullableType = ColumnNullableType.NOT_NULLABLE;
        } else if (ctx.nullable != null) {
            nullableType = ColumnNullableType.NULLABLE;
        }
        String aggTypeString = ctx.aggType != null ? ctx.aggType.getText() : null;
        Optional<DefaultValue> defaultValue = Optional.empty();
        Optional<DefaultValue> onUpdateDefaultValue = Optional.empty();
        if (ctx.DEFAULT() != null) {
            if (ctx.INTEGER_VALUE() != null) {
                if (ctx.SUBTRACT() == null) {
                    defaultValue = Optional.of(new DefaultValue(ctx.INTEGER_VALUE().getText()));
                } else {
                    defaultValue = Optional.of(new DefaultValue("-" + ctx.INTEGER_VALUE().getText()));
                }
            } else if (ctx.DECIMAL_VALUE() != null) {
                if (ctx.SUBTRACT() == null) {
                    defaultValue = Optional.of(new DefaultValue(ctx.DECIMAL_VALUE().getText()));
                } else {
                    defaultValue = Optional.of(new DefaultValue("-" + ctx.DECIMAL_VALUE().getText()));
                }
            } else if (ctx.stringValue != null) {
                defaultValue = Optional.of(new DefaultValue(toStringValue(ctx.stringValue.getText())));
            } else if (ctx.nullValue != null) {
                defaultValue = Optional.of(DefaultValue.NULL_DEFAULT_VALUE);
            } else if (ctx.defaultTimestamp != null) {
                if (ctx.defaultValuePrecision == null) {
                    defaultValue = Optional.of(DefaultValue.CURRENT_TIMESTAMP_DEFAULT_VALUE);
                } else {
                    defaultValue = Optional.of(DefaultValue
                            .currentTimeStampDefaultValueWithPrecision(
                                    Long.valueOf(ctx.defaultValuePrecision.getText())));
                }
            } else if (ctx.CURRENT_DATE() != null) {
                defaultValue = Optional.of(DefaultValue.CURRENT_DATE_DEFAULT_VALUE);
            } else if (ctx.PI() != null) {
                defaultValue = Optional.of(DefaultValue.PI_DEFAULT_VALUE);
            } else if (ctx.E() != null) {
                defaultValue = Optional.of(DefaultValue.E_NUM_DEFAULT_VALUE);
            } else if (ctx.BITMAP_EMPTY() != null) {
                defaultValue = Optional.of(DefaultValue.BITMAP_EMPTY_DEFAULT_VALUE);
            }
        }
        if (ctx.UPDATE() != null) {
            if (ctx.onUpdateValuePrecision == null) {
                onUpdateDefaultValue = Optional.of(DefaultValue.CURRENT_TIMESTAMP_DEFAULT_VALUE);
            } else {
                onUpdateDefaultValue = Optional.of(DefaultValue
                        .currentTimeStampDefaultValueWithPrecision(
                                Long.valueOf(ctx.onUpdateValuePrecision.getText())));
            }
        }
        AggregateType aggType = null;
        if (aggTypeString != null) {
            try {
                aggType = AggregateType.valueOf(aggTypeString.toUpperCase());
            } catch (Exception e) {
                throw new AnalysisException(String.format("Aggregate type %s is unsupported", aggTypeString),
                        e.getCause());
            }
        }
        //comment should remove '\' and '(") at the beginning and end
        String comment = ctx.comment != null ? ctx.comment.getText().substring(1, ctx.comment.getText().length() - 1)
                .replace("\\", "") : "";
        long autoIncInitValue = -1;
        if (ctx.AUTO_INCREMENT() != null) {
            if (ctx.autoIncInitValue != null) {
                // AUTO_INCREMENT(Value) Value >= 0.
                autoIncInitValue = Long.valueOf(ctx.autoIncInitValue.getText());
                if (autoIncInitValue < 0) {
                    throw new AnalysisException("AUTO_INCREMENT start value can not be negative.");
                }
            } else {
                // AUTO_INCREMENT default 1.
                autoIncInitValue = Long.valueOf(1);
            }
        }
        Optional<GeneratedColumnDesc> desc = ctx.generatedExpr != null
                ? Optional.of(new GeneratedColumnDesc(ctx.generatedExpr.getText(), getExpression(ctx.generatedExpr)))
                : Optional.empty();
        return new ColumnDefinition(colName, colType, isKey, aggType, nullableType, autoIncInitValue, defaultValue,
                onUpdateDefaultValue, comment, desc);
    }

    @Override
    public List<IndexDefinition> visitIndexDefs(IndexDefsContext ctx) {
        return ctx.indexes.stream().map(this::visitIndexDef).collect(Collectors.toList());
    }

    @Override
    public IndexDefinition visitIndexDef(IndexDefContext ctx) {
        String indexName = ctx.indexName.getText();
        List<String> indexCols = visitIdentifierList(ctx.cols);
        Map<String, String> properties = visitPropertyItemList(ctx.properties);
        String indexType = ctx.indexType != null ? ctx.indexType.getText().toUpperCase() : null;
        //comment should remove '\' and '(") at the beginning and end
        String comment = ctx.comment == null ? "" : LogicalPlanBuilderAssistant.escapeBackSlash(
                ctx.comment.getText().substring(1, ctx.STRING_LITERAL().getText().length() - 1));
        // change BITMAP index to INVERTED index
        if (Config.enable_create_bitmap_index_as_inverted_index
                && "BITMAP".equalsIgnoreCase(indexType)) {
            indexType = "INVERTED";
        }
        return new IndexDefinition(indexName, indexCols, indexType, properties, comment);
    }

    @Override
    public List<PartitionDefinition> visitPartitionsDef(PartitionsDefContext ctx) {
        return ctx.partitions.stream()
                .map(p -> ((PartitionDefinition) visit(p))).collect(Collectors.toList());
    }

    @Override
    public PartitionDefinition visitPartitionDef(DorisParser.PartitionDefContext ctx) {
        PartitionDefinition partitionDefinition = (PartitionDefinition) visit(ctx.getChild(0));
        if (ctx.partitionProperties != null) {
            partitionDefinition.withProperties(visitPropertyItemList(ctx.partitionProperties));
        }
        return partitionDefinition;
    }

    @Override
    public PartitionDefinition visitLessThanPartitionDef(LessThanPartitionDefContext ctx) {
        String partitionName = ctx.partitionName.getText();
        if (ctx.MAXVALUE() == null) {
            List<Expression> lessThanValues = visitPartitionValueList(ctx.partitionValueList());
            return new LessThanPartition(ctx.EXISTS() != null, partitionName, lessThanValues);
        } else {
            return new LessThanPartition(ctx.EXISTS() != null, partitionName,
                    ImmutableList.of(MaxValue.INSTANCE));
        }
    }

    @Override
    public PartitionDefinition visitFixedPartitionDef(FixedPartitionDefContext ctx) {
        String partitionName = ctx.partitionName.getText();
        List<Expression> lowerBounds = visitPartitionValueList(ctx.lower);
        List<Expression> upperBounds = visitPartitionValueList(ctx.upper);
        return new FixedRangePartition(ctx.EXISTS() != null, partitionName, lowerBounds, upperBounds);
    }

    @Override
    public PartitionDefinition visitStepPartitionDef(StepPartitionDefContext ctx) {
        List<Expression> fromExpression = visitPartitionValueList(ctx.from);
        List<Expression> toExpression = visitPartitionValueList(ctx.to);
        return new StepPartition(false, null, fromExpression, toExpression,
                Long.parseLong(ctx.unitsAmount.getText()), ctx.unit != null ? ctx.unit.getText() : null);
    }

    @Override
    public PartitionDefinition visitInPartitionDef(InPartitionDefContext ctx) {
        List<List<Expression>> values;
        if (ctx.constants == null) {
            values = ctx.partitionValueLists.stream().map(this::visitPartitionValueList)
                    .collect(Collectors.toList());
        } else {
            values = visitPartitionValueList(ctx.constants).stream().map(ImmutableList::of)
                    .collect(Collectors.toList());
        }
        return new InPartition(ctx.EXISTS() != null, ctx.partitionName.getText(), values);
    }

    @Override
    public List<Expression> visitPartitionValueList(PartitionValueListContext ctx) {
        return ctx.values.stream()
                .map(this::visitPartitionValueDef)
                .collect(Collectors.toList());
    }

    @Override
    public Expression visitPartitionValueDef(PartitionValueDefContext ctx) {
        if (ctx.INTEGER_VALUE() != null) {
            if (ctx.SUBTRACT() != null) {
                return Literal.of("-" + ctx.INTEGER_VALUE().getText());
            }
            return Literal.of(ctx.INTEGER_VALUE().getText());
        } else if (ctx.STRING_LITERAL() != null) {
            return Literal.of(toStringValue(ctx.STRING_LITERAL().getText()));
        } else if (ctx.MAXVALUE() != null) {
            return MaxValue.INSTANCE;
        } else if (ctx.NULL() != null) {
            return Literal.of(null);
        }
        throw new AnalysisException("Unsupported partition value: " + ctx.getText());
    }

    @Override
    public List<RollupDefinition> visitRollupDefs(RollupDefsContext ctx) {
        return ctx.rollups.stream().map(this::visitRollupDef).collect(Collectors.toList());
    }

    @Override
    public RollupDefinition visitRollupDef(RollupDefContext ctx) {
        String rollupName = ctx.rollupName.getText();
        List<String> rollupCols = visitIdentifierList(ctx.rollupCols);
        List<String> dupKeys = ctx.dupKeys == null ? ImmutableList.of() : visitIdentifierList(ctx.dupKeys);
        Map<String, String> properties = ctx.properties == null ? Maps.newHashMap()
                : visitPropertyClause(ctx.properties);
        return new RollupDefinition(rollupName, rollupCols, dupKeys, properties);
    }

    private String toStringValue(String literal) {
        return literal.substring(1, literal.length() - 1);
    }

    /* ********************************************************************************************
     * Expression parsing
     * ******************************************************************************************** */

    /**
     * Create an expression from the given context. This method just passes the context on to the
     * visitor and only takes care of typing (We assume that the visitor returns an Expression here).
     */
    private Expression getExpression(ParserRuleContext ctx) {
        return typedVisit(ctx);
    }

    private LogicalPlan withExplain(LogicalPlan inputPlan, ExplainContext ctx) {
        if (ctx == null) {
            return inputPlan;
        }
        return ParserUtils.withOrigin(ctx, () -> {
            ExplainLevel explainLevel = ExplainLevel.NORMAL;

            if (ctx.planType() != null) {
                if (ctx.level == null || !ctx.level.getText().equalsIgnoreCase("plan")) {
                    throw new ParseException("Only explain plan can use plan type: " + ctx.planType().getText(), ctx);
                }
            }

            boolean showPlanProcess = false;
            if (ctx.level != null) {
                if (!ctx.level.getText().equalsIgnoreCase("plan")) {
                    explainLevel = ExplainLevel.valueOf(ctx.level.getText().toUpperCase(Locale.ROOT));
                } else {
                    explainLevel = parseExplainPlanType(ctx.planType());

                    if (ctx.PROCESS() != null) {
                        showPlanProcess = true;
                    }
                }
            }
            return new ExplainCommand(explainLevel, inputPlan, showPlanProcess);
        });
    }

    private LogicalPlan withOutFile(LogicalPlan plan, OutFileClauseContext ctx) {
        if (ctx == null) {
            return plan;
        }
        String format = "csv";
        if (ctx.format != null) {
            format = ctx.format.getText();
        }

        Map<String, String> properties = ImmutableMap.of();
        if (ctx.propertyClause() != null) {
            properties = visitPropertyClause(ctx.propertyClause());
        }
        Literal filePath = (Literal) visit(ctx.filePath);
        return new LogicalFileSink<>(filePath.getStringValue(), format, properties, ImmutableList.of(), plan);
    }

    private LogicalPlan withQueryOrganization(LogicalPlan inputPlan, QueryOrganizationContext ctx) {
        if (ctx == null) {
            return inputPlan;
        }
        Optional<SortClauseContext> sortClauseContext = Optional.ofNullable(ctx.sortClause());
        Optional<LimitClauseContext> limitClauseContext = Optional.ofNullable(ctx.limitClause());
        LogicalPlan sort = withSort(inputPlan, sortClauseContext);
        return withLimit(sort, limitClauseContext);
    }

    private LogicalPlan withSort(LogicalPlan input, Optional<SortClauseContext> sortCtx) {
        return input.optionalMap(sortCtx, () -> {
            List<OrderKey> orderKeys = visit(sortCtx.get().sortItem(), OrderKey.class);
            return new LogicalSort<>(orderKeys, input);
        });
    }

    private LogicalPlan withLimit(LogicalPlan input, Optional<LimitClauseContext> limitCtx) {
        return input.optionalMap(limitCtx, () -> {
            long limit = Long.parseLong(limitCtx.get().limit.getText());
            if (limit < 0) {
                throw new ParseException("Limit requires non-negative number", limitCtx.get());
            }
            long offset = 0;
            Token offsetToken = limitCtx.get().offset;
            if (offsetToken != null) {
                offset = Long.parseLong(offsetToken.getText());
            }
            return new LogicalLimit<>(limit, offset, LimitPhase.ORIGIN, input);
        });
    }

    /**
     * Add a regular (SELECT) query specification to a logical plan. The query specification
     * is the core of the logical plan, this is where sourcing (FROM clause), projection (SELECT),
     * aggregation (GROUP BY ... HAVING ...) and filtering (WHERE) takes place.
     *
     * <p>Note that query hints are ignored (both by the parser and the builder).
     */
    protected LogicalPlan withSelectQuerySpecification(
            ParserRuleContext ctx,
            LogicalPlan inputRelation,
            SelectClauseContext selectClause,
            Optional<WhereClauseContext> whereClause,
            Optional<AggClauseContext> aggClause,
            Optional<HavingClauseContext> havingClause) {
        return ParserUtils.withOrigin(ctx, () -> {
            // from -> where -> group by -> having -> select
            LogicalPlan filter = withFilter(inputRelation, whereClause);
            SelectColumnClauseContext selectColumnCtx = selectClause.selectColumnClause();
            LogicalPlan aggregate = withAggregate(filter, selectColumnCtx, aggClause);
            boolean isDistinct = (selectClause.DISTINCT() != null);
            if (!(aggregate instanceof Aggregate) && havingClause.isPresent()) {
                // create a project node for pattern match of ProjectToGlobalAggregate rule
                // then ProjectToGlobalAggregate rule can insert agg node as LogicalHaving node's child
                LogicalPlan project;
                if (selectColumnCtx.EXCEPT() != null) {
                    List<NamedExpression> expressions = getNamedExpressions(selectColumnCtx.namedExpressionSeq());
                    if (!expressions.stream().allMatch(UnboundSlot.class::isInstance)) {
                        throw new ParseException("only column name is supported in except clause", selectColumnCtx);
                    }
                    UnboundStar star = new UnboundStar(ImmutableList.of());
                    project = new LogicalProject<>(ImmutableList.of(star), expressions, isDistinct, aggregate);
                } else {
                    List<NamedExpression> projects = getNamedExpressions(selectColumnCtx.namedExpressionSeq());
                    project = new LogicalProject<>(projects, ImmutableList.of(), isDistinct, aggregate);
                }
                return new LogicalHaving<>(ExpressionUtils.extractConjunctionToSet(
                        getExpression((havingClause.get().booleanExpression()))), project);
            } else {
                LogicalPlan having = withHaving(aggregate, havingClause);
                return withProjection(having, selectColumnCtx, aggClause, isDistinct);
            }
        });
    }

    /**
     * Join one more [[LogicalPlan]]s to the current logical plan.
     */
    private LogicalPlan withJoinRelations(LogicalPlan input, RelationContext ctx) {
        LogicalPlan last = input;
        for (JoinRelationContext join : ctx.joinRelation()) {
            JoinType joinType;
            if (join.joinType().CROSS() != null) {
                joinType = JoinType.CROSS_JOIN;
            } else if (join.joinType().FULL() != null) {
                joinType = JoinType.FULL_OUTER_JOIN;
            } else if (join.joinType().SEMI() != null) {
                if (join.joinType().LEFT() != null) {
                    joinType = JoinType.LEFT_SEMI_JOIN;
                } else {
                    joinType = JoinType.RIGHT_SEMI_JOIN;
                }
            } else if (join.joinType().ANTI() != null) {
                if (join.joinType().LEFT() != null) {
                    joinType = JoinType.LEFT_ANTI_JOIN;
                } else {
                    joinType = JoinType.RIGHT_ANTI_JOIN;
                }
            } else if (join.joinType().LEFT() != null) {
                joinType = JoinType.LEFT_OUTER_JOIN;
            } else if (join.joinType().RIGHT() != null) {
                joinType = JoinType.RIGHT_OUTER_JOIN;
            } else if (join.joinType().INNER() != null) {
                joinType = JoinType.INNER_JOIN;
            } else if (join.joinCriteria() != null) {
                joinType = JoinType.INNER_JOIN;
            } else {
                joinType = JoinType.CROSS_JOIN;
            }
            DistributeType distributeType = Optional.ofNullable(join.distributeType()).map(hintCtx -> {
                String hint = typedVisit(join.distributeType());
                if (DistributeType.JoinDistributeType.SHUFFLE.toString().equalsIgnoreCase(hint)) {
                    return DistributeType.SHUFFLE_RIGHT;
                } else if (DistributeType.JoinDistributeType.BROADCAST.toString().equalsIgnoreCase(hint)) {
                    return DistributeType.BROADCAST_RIGHT;
                } else {
                    throw new ParseException("Invalid join hint: " + hint, hintCtx);
                }
            }).orElse(DistributeType.NONE);
            DistributeHint distributeHint = new DistributeHint(distributeType);
            // TODO: natural join, lateral join, union join
            JoinCriteriaContext joinCriteria = join.joinCriteria();
            Optional<Expression> condition = Optional.empty();
            List<Expression> ids = null;
            if (joinCriteria != null) {
                if (join.joinType().CROSS() != null) {
                    throw new ParseException("Cross join can't be used with ON clause", joinCriteria);
                }
                if (joinCriteria.booleanExpression() != null) {
                    condition = Optional.ofNullable(getExpression(joinCriteria.booleanExpression()));
                } else if (joinCriteria.USING() != null) {
                    ids = visitIdentifierList(joinCriteria.identifierList())
                            .stream().map(UnboundSlot::quoted)
                            .collect(ImmutableList.toImmutableList());
                }
            } else {
                // keep same with original planner, allow cross/inner join
                if (!joinType.isInnerOrCrossJoin()) {
                    throw new ParseException("on mustn't be empty except for cross/inner join", join);
                }
            }
            if (ids == null) {
                last = new LogicalJoin<>(joinType, ExpressionUtils.EMPTY_CONDITION,
                        condition.map(ExpressionUtils::extractConjunction)
                                .orElse(ExpressionUtils.EMPTY_CONDITION),
                        distributeHint,
                        Optional.empty(),
                        last,
                        plan(join.relationPrimary()), null);
            } else {
                last = new UsingJoin<>(joinType, last,
                        plan(join.relationPrimary()), ImmutableList.of(), ids, distributeHint);

            }
            if (distributeHint.distributeType != DistributeType.NONE
                    && ConnectContext.get().getStatementContext() != null
                    && !ConnectContext.get().getStatementContext().getHints().contains(distributeHint)) {
                ConnectContext.get().getStatementContext().addHint(distributeHint);
            }
        }
        return last;
    }

    private LogicalPlan withHints(LogicalPlan logicalPlan, List<ParserRuleContext> selectHintContexts,
            List<ParserRuleContext> preAggOnHintContexts) {
        if (selectHintContexts.isEmpty() && preAggOnHintContexts.isEmpty()) {
            return logicalPlan;
        }
        LogicalPlan newPlan = logicalPlan;
        if (!selectHintContexts.isEmpty()) {
            Map<String, SelectHint> hints = Maps.newLinkedHashMap();
            for (ParserRuleContext hintContext : selectHintContexts) {
                SelectHintContext selectHintContext = (SelectHintContext) hintContext;
                for (HintStatementContext hintStatement : selectHintContext.hintStatements) {
                    String hintName = hintStatement.hintName.getText().toLowerCase(Locale.ROOT);
                    switch (hintName) {
                        case "set_var":
                            Map<String, Optional<String>> parameters = Maps.newLinkedHashMap();
                            for (HintAssignmentContext kv : hintStatement.parameters) {
                                if (kv.key != null) {
                                    String parameterName = visitIdentifierOrText(kv.key);
                                    Optional<String> value = Optional.empty();
                                    if (kv.constantValue != null) {
                                        Literal literal = (Literal) visit(kv.constantValue);
                                        value = Optional.ofNullable(literal.toLegacyLiteral().getStringValue());
                                    } else if (kv.identifierValue != null) {
                                        // maybe we should throw exception when the identifierValue is quoted identifier
                                        value = Optional.ofNullable(kv.identifierValue.getText());
                                    }
                                    parameters.put(parameterName, value);
                                }
                            }
                            hints.put(hintName, new SelectHintSetVar(hintName, parameters));
                            break;
                        case "leading":
                            List<String> leadingParameters = new ArrayList<>();
                            for (HintAssignmentContext kv : hintStatement.parameters) {
                                if (kv.key != null) {
                                    String parameterName = visitIdentifierOrText(kv.key);
                                    leadingParameters.add(parameterName);
                                }
                            }
                            hints.put(hintName, new SelectHintLeading(hintName, leadingParameters));
                            break;
                        case "ordered":
                            hints.put(hintName, new SelectHintOrdered(hintName));
                            break;
                        case "use_cbo_rule":
                            List<String> useRuleParameters = new ArrayList<>();
                            for (HintAssignmentContext kv : hintStatement.parameters) {
                                if (kv.key != null) {
                                    String parameterName = visitIdentifierOrText(kv.key);
                                    useRuleParameters.add(parameterName);
                                }
                            }
                            hints.put(hintName, new SelectHintUseCboRule(hintName, useRuleParameters, false));
                            break;
                        case "no_use_cbo_rule":
                            List<String> noUseRuleParameters = new ArrayList<>();
                            for (HintAssignmentContext kv : hintStatement.parameters) {
                                String parameterName = visitIdentifierOrText(kv.key);
                                if (kv.key != null) {
                                    noUseRuleParameters.add(parameterName);
                                }
                            }
                            hints.put(hintName, new SelectHintUseCboRule(hintName, noUseRuleParameters, true));
                            break;
                        default:
                            break;
                    }
                }
            }
            newPlan = new LogicalSelectHint<>(hints, newPlan);
        }
        if (!preAggOnHintContexts.isEmpty()) {
            for (ParserRuleContext hintContext : preAggOnHintContexts) {
                if (hintContext instanceof SelectHintContext) {
                    SelectHintContext preAggOnHintContext = (SelectHintContext) hintContext;
                    if (preAggOnHintContext.hintStatement != null
                            && preAggOnHintContext.hintStatement.hintName != null) {
                        String text = preAggOnHintContext.hintStatement.hintName.getText();
                        if (text.equalsIgnoreCase("PREAGGOPEN")) {
                            newPlan = new LogicalPreAggOnHint<>(newPlan);
                            break;
                        }
                    }
                }
            }
        }
        return newPlan;
    }

    @Override
    public String visitBracketDistributeType(BracketDistributeTypeContext ctx) {
        return ctx.identifier().getText();
    }

    @Override
    public String visitCommentDistributeType(CommentDistributeTypeContext ctx) {
        return ctx.identifier().getText();
    }

    @Override
    public List<String> visitBracketRelationHint(BracketRelationHintContext ctx) {
        return ctx.identifier().stream()
                .map(RuleContext::getText)
                .collect(ImmutableList.toImmutableList());
    }

    @Override
    public Object visitCommentRelationHint(CommentRelationHintContext ctx) {
        return ctx.identifier().stream()
                .map(RuleContext::getText)
                .collect(ImmutableList.toImmutableList());
    }

    protected LogicalPlan withProjection(LogicalPlan input, SelectColumnClauseContext selectCtx,
            Optional<AggClauseContext> aggCtx, boolean isDistinct) {
        return ParserUtils.withOrigin(selectCtx, () -> {
            if (aggCtx.isPresent()) {
                if (isDistinct) {
                    return new LogicalProject<>(ImmutableList.of(new UnboundStar(ImmutableList.of())),
                            Collections.emptyList(), isDistinct, input);
                } else {
                    return input;
                }
            } else {
                if (selectCtx.EXCEPT() != null) {
                    List<NamedExpression> expressions = getNamedExpressions(selectCtx.namedExpressionSeq());
                    if (!expressions.stream().allMatch(UnboundSlot.class::isInstance)) {
                        throw new ParseException("only column name is supported in except clause", selectCtx);
                    }
                    UnboundStar star = new UnboundStar(ImmutableList.of());
                    return new LogicalProject<>(ImmutableList.of(star), expressions, isDistinct, input);
                } else {
                    List<NamedExpression> projects = getNamedExpressions(selectCtx.namedExpressionSeq());
                    return new LogicalProject<>(projects, Collections.emptyList(), isDistinct, input);
                }
            }
        });
    }

    private LogicalPlan withRelations(LogicalPlan inputPlan, List<RelationContext> relations) {
        if (relations == null) {
            return inputPlan;
        }
        LogicalPlan left = inputPlan;
        for (RelationContext relation : relations) {
            // build left deep join tree
            LogicalPlan right = withJoinRelations(visitRelation(relation), relation);
            left = (left == null) ? right :
                    new LogicalJoin<>(
                            JoinType.CROSS_JOIN,
                            ExpressionUtils.EMPTY_CONDITION,
                            ExpressionUtils.EMPTY_CONDITION,
                            new DistributeHint(DistributeType.NONE),
                            Optional.empty(),
                            left,
                            right, null);
            // TODO: pivot and lateral view
        }
        return left;
    }

    private LogicalPlan withFilter(LogicalPlan input, Optional<WhereClauseContext> whereCtx) {
        return input.optionalMap(whereCtx, () ->
                new LogicalFilter<>(ExpressionUtils.extractConjunctionToSet(
                        getExpression(whereCtx.get().booleanExpression())), input));
    }

    private LogicalPlan withAggregate(LogicalPlan input, SelectColumnClauseContext selectCtx,
            Optional<AggClauseContext> aggCtx) {
        return input.optionalMap(aggCtx, () -> {
            GroupingElementContext groupingElementContext = aggCtx.get().groupingElement();
            List<NamedExpression> namedExpressions = getNamedExpressions(selectCtx.namedExpressionSeq());
            if (groupingElementContext.GROUPING() != null) {
                ImmutableList.Builder<List<Expression>> groupingSets = ImmutableList.builder();
                for (GroupingSetContext groupingSetContext : groupingElementContext.groupingSet()) {
                    groupingSets.add(visit(groupingSetContext.expression(), Expression.class));
                }
                return new LogicalRepeat<>(groupingSets.build(), namedExpressions, input);
            } else if (groupingElementContext.CUBE() != null) {
                List<Expression> cubeExpressions = visit(groupingElementContext.expression(), Expression.class);
                List<List<Expression>> groupingSets = ExpressionUtils.cubeToGroupingSets(cubeExpressions);
                return new LogicalRepeat<>(groupingSets, namedExpressions, input);
            } else if (groupingElementContext.ROLLUP() != null) {
                List<Expression> rollupExpressions = visit(groupingElementContext.expression(), Expression.class);
                List<List<Expression>> groupingSets = ExpressionUtils.rollupToGroupingSets(rollupExpressions);
                return new LogicalRepeat<>(groupingSets, namedExpressions, input);
            } else {
                List<Expression> groupByExpressions = visit(groupingElementContext.expression(), Expression.class);
                return new LogicalAggregate<>(groupByExpressions, namedExpressions, input);
            }
        });
    }

    private LogicalPlan withHaving(LogicalPlan input, Optional<HavingClauseContext> havingCtx) {
        return input.optionalMap(havingCtx, () -> {
            if (!(input instanceof Aggregate)) {
                throw new ParseException("Having clause should be applied against an aggregation.", havingCtx.get());
            }
            return new LogicalHaving<>(ExpressionUtils.extractConjunctionToSet(
                    getExpression((havingCtx.get().booleanExpression()))), input);
        });
    }

    /**
     * match predicate type and generate different predicates.
     *
     * @param ctx PredicateContext
     * @param valueExpression valueExpression
     * @return Expression
     */
    private Expression withPredicate(Expression valueExpression, PredicateContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            Expression outExpression;
            switch (ctx.kind.getType()) {
                case DorisParser.BETWEEN:
                    Expression lower = getExpression(ctx.lower);
                    Expression upper = getExpression(ctx.upper);
                    if (lower.equals(upper)) {
                        outExpression = new EqualTo(valueExpression, lower);
                    } else {
                        outExpression = new And(
                                new GreaterThanEqual(valueExpression, getExpression(ctx.lower)),
                                new LessThanEqual(valueExpression, getExpression(ctx.upper))
                        );
                    }
                    break;
                case DorisParser.LIKE:
                    if (ctx.ESCAPE() == null) {
                        outExpression = new Like(
                                valueExpression,
                                getExpression(ctx.pattern));
                    } else {
                        outExpression = new Like(
                                valueExpression,
                                getExpression(ctx.pattern),
                                getExpression(ctx.escape));
                    }
                    break;
                case DorisParser.RLIKE:
                case DorisParser.REGEXP:
                    outExpression = new Regexp(
                            valueExpression,
                            getExpression(ctx.pattern)
                    );
                    break;
                case DorisParser.IN:
                    if (ctx.query() == null) {
                        outExpression = new InPredicate(
                                valueExpression,
                                withInList(ctx)
                        );
                    } else {
                        outExpression = new InSubquery(
                                valueExpression,
                                new ListQuery(typedVisit(ctx.query())),
                                ctx.NOT() != null
                        );
                    }
                    break;
                case DorisParser.NULL:
                    outExpression = new IsNull(valueExpression);
                    break;
                case DorisParser.TRUE:
                    outExpression = new Cast(valueExpression,
                            BooleanType.INSTANCE, true);
                    break;
                case DorisParser.FALSE:
                    outExpression = new Not(new Cast(valueExpression,
                            BooleanType.INSTANCE, true));
                    break;
                case DorisParser.MATCH:
                case DorisParser.MATCH_ANY:
                    outExpression = new MatchAny(
                            valueExpression,
                            getExpression(ctx.pattern)
                    );
                    break;
                case DorisParser.MATCH_ALL:
                    outExpression = new MatchAll(
                            valueExpression,
                            getExpression(ctx.pattern)
                    );
                    break;
                case DorisParser.MATCH_PHRASE:
                    outExpression = new MatchPhrase(
                            valueExpression,
                            getExpression(ctx.pattern)
                    );
                    break;
                case DorisParser.MATCH_PHRASE_PREFIX:
                    outExpression = new MatchPhrasePrefix(
                            valueExpression,
                            getExpression(ctx.pattern)
                    );
                    break;
                case DorisParser.MATCH_REGEXP:
                    outExpression = new MatchRegexp(
                            valueExpression,
                            getExpression(ctx.pattern)
                    );
                    break;
                case DorisParser.MATCH_PHRASE_EDGE:
                    outExpression = new MatchPhraseEdge(
                            valueExpression,
                            getExpression(ctx.pattern)
                    );
                    break;
                default:
                    throw new ParseException("Unsupported predicate type: " + ctx.kind.getText(), ctx);
            }
            return ctx.NOT() != null ? new Not(outExpression) : outExpression;
        });
    }

    private List<NamedExpression> getNamedExpressions(NamedExpressionSeqContext namedCtx) {
        return ParserUtils.withOrigin(namedCtx, () -> visit(namedCtx.namedExpression(), NamedExpression.class));
    }

    @Override
    public Expression visitSubqueryExpression(SubqueryExpressionContext subqueryExprCtx) {
        return ParserUtils.withOrigin(subqueryExprCtx, () -> new ScalarSubquery(typedVisit(subqueryExprCtx.query())));
    }

    @Override
    public Expression visitExist(ExistContext context) {
        return ParserUtils.withOrigin(context, () -> new Exists(typedVisit(context.query()), false));
    }

    @Override
    public Expression visitIsnull(IsnullContext context) {
        return ParserUtils.withOrigin(context, () -> new IsNull(typedVisit(context.valueExpression())));
    }

    @Override
    public Expression visitIs_not_null_pred(Is_not_null_predContext context) {
        return ParserUtils.withOrigin(context, () -> new Not(new IsNull(typedVisit(context.valueExpression()))));
    }

    public List<Expression> withInList(PredicateContext ctx) {
        return ctx.expression().stream().map(this::getExpression).collect(ImmutableList.toImmutableList());
    }

    @Override
    public Literal visitDecimalLiteral(DecimalLiteralContext ctx) {
        try {
            if (Config.enable_decimal_conversion) {
                return new DecimalV3Literal(new BigDecimal(ctx.getText()));
            } else {
                return new DecimalLiteral(new BigDecimal(ctx.getText()));
            }
        } catch (Exception e) {
            return new DoubleLiteral(Double.parseDouble(ctx.getText()));
        }
    }

    private String parsePropertyKey(PropertyKeyContext item) {
        if (item.constant() != null) {
            return parseConstant(item.constant());
        }
        return item.getText();
    }

    private String parsePropertyValue(PropertyValueContext item) {
        if (item.constant() != null) {
            return parseConstant(item.constant());
        }
        return item.getText();
    }

    private ExplainLevel parseExplainPlanType(PlanTypeContext planTypeContext) {
        if (planTypeContext == null || planTypeContext.ALL() != null) {
            return ExplainLevel.ALL_PLAN;
        }
        if (planTypeContext.PHYSICAL() != null || planTypeContext.OPTIMIZED() != null) {
            return ExplainLevel.OPTIMIZED_PLAN;
        }
        if (planTypeContext.REWRITTEN() != null || planTypeContext.LOGICAL() != null) {
            return ExplainLevel.REWRITTEN_PLAN;
        }
        if (planTypeContext.ANALYZED() != null) {
            return ExplainLevel.ANALYZED_PLAN;
        }
        if (planTypeContext.PARSED() != null) {
            return ExplainLevel.PARSED_PLAN;
        }
        if (planTypeContext.SHAPE() != null) {
            return ExplainLevel.SHAPE_PLAN;
        }
        if (planTypeContext.MEMO() != null) {
            return ExplainLevel.MEMO_PLAN;
        }
        if (planTypeContext.DISTRIBUTED() != null) {
            return ExplainLevel.DISTRIBUTED_PLAN;
        }
        return ExplainLevel.ALL_PLAN;
    }

    @Override
    public Pair<DataType, Boolean> visitDataTypeWithNullable(DataTypeWithNullableContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> Pair.of(typedVisit(ctx.dataType()), ctx.NOT() == null));
    }

    @Override
    public DataType visitAggStateDataType(AggStateDataTypeContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            List<Pair<DataType, Boolean>> dataTypeWithNullables = ctx.dataTypes.stream()
                    .map(this::visitDataTypeWithNullable)
                    .collect(Collectors.toList());
            List<DataType> dataTypes = dataTypeWithNullables.stream()
                    .map(dt -> dt.first)
                    .collect(ImmutableList.toImmutableList());
            List<Boolean> nullables = dataTypeWithNullables.stream()
                    .map(dt -> dt.second)
                    .collect(ImmutableList.toImmutableList());
            String functionName = ctx.functionNameIdentifier().getText();
            if (!BuiltinAggregateFunctions.INSTANCE.aggFuncNames.contains(functionName)) {
                // TODO use function binder to check function exists
                throw new ParseException("Can not found function '" + functionName + "'", ctx);
            }
            return new AggStateType(functionName, dataTypes, nullables);
        });
    }

    @Override
    public DataType visitPrimitiveDataType(PrimitiveDataTypeContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            String dataType = ctx.primitiveColType().type.getText().toLowerCase(Locale.ROOT);
            if (dataType.equalsIgnoreCase("all")) {
                throw new NotSupportedException("Disable to create table with `ALL` type columns");
            }
            List<String> l = Lists.newArrayList(dataType);
            ctx.INTEGER_VALUE().stream().map(ParseTree::getText).forEach(l::add);
            return DataType.convertPrimitiveFromStrings(l);
        });
    }

    @Override
    public DataType visitVariantPredefinedFields(VariantPredefinedFieldsContext ctx) {
        VariantTypeDefinitionsContext variantDef = ctx.complex;

        List<VariantField> fields = Lists.newArrayList();
        Map<String, String> properties = Maps.newHashMap();
        if (variantDef instanceof VariantWithFieldsAndPropsContext) {
            VariantWithFieldsAndPropsContext withBoth = (VariantWithFieldsAndPropsContext) variantDef;
            fields = visitVariantSubColTypeList(withBoth.variantSubColTypeList());
            properties = Maps.newHashMap(visitPropertyClause(withBoth.properties));
        } else if (variantDef instanceof VariantWithOnlyFieldsContext) {
            VariantWithOnlyFieldsContext withFields = (VariantWithOnlyFieldsContext) variantDef;
            fields = visitVariantSubColTypeList(withFields.variantSubColTypeList());
        } else if (variantDef instanceof VariantWithOnlyPropsContext) {
            VariantWithOnlyPropsContext withProps = (VariantWithOnlyPropsContext) variantDef;
            properties = Maps.newHashMap(visitPropertyClause(withProps.properties));
        } else {
            Preconditions.checkState(variantDef instanceof VariantContext,
                                        "Unsupported variant definition: " + variantDef.getText());
        }

        int variantMaxSubcolumnsCount = ConnectContext.get() == null ? 0 :
                ConnectContext.get().getSessionVariable().getGlobalVariantMaxSubcolumnsCount();
        boolean enableTypedPathsToSparse = ConnectContext.get() == null ? false :
                ConnectContext.get().getSessionVariable().getGlobalEnableTypedPathsToSparse();

        try {
            variantMaxSubcolumnsCount = PropertyAnalyzer
                                        .analyzeVariantMaxSubcolumnsCount(properties, variantMaxSubcolumnsCount);
            enableTypedPathsToSparse = PropertyAnalyzer
                                        .analyzeEnableTypedPathsToSparse(properties, enableTypedPathsToSparse);
        } catch (org.apache.doris.common.AnalysisException e) {
            throw new NotSupportedException(e.getMessage());
        }

        if (!properties.isEmpty()) {
            throw new NotSupportedException("only support for "
                    + PropertyAnalyzer.PROPERTIES_VARIANT_ENABLE_TYPED_PATHS_TO_SPARSE
                    + " and " + PropertyAnalyzer.PROPERTIES_VARIANT_MAX_SUBCOLUMNS_COUNT);
        }

        return new VariantType(fields, variantMaxSubcolumnsCount, enableTypedPathsToSparse);
    }

    @Override
    public List<VariantField> visitVariantSubColTypeList(VariantSubColTypeListContext ctx) {
        return ctx.variantSubColType().stream().map(
                this::visitVariantSubColType).collect(ImmutableList.toImmutableList());
    }

    @Override
    public VariantField visitVariantSubColType(VariantSubColTypeContext ctx) {
        String comment;
        if (ctx.commentSpec() != null) {
            comment = ctx.commentSpec().STRING_LITERAL().getText();
            comment = LogicalPlanBuilderAssistant.escapeBackSlash(comment.substring(1, comment.length() - 1));
        } else {
            comment = "";
        }
        String pattern = ctx.STRING_LITERAL().getText();
        pattern = pattern.substring(1, pattern.length() - 1);
        if (ctx.variantSubColMatchType() != null) {
            return new VariantField(pattern, typedVisit(ctx.dataType()), comment,
                    ctx.variantSubColMatchType().getText());
        }
        return new VariantField(pattern, typedVisit(ctx.dataType()), comment);
    }

    @Override
    public DataType visitComplexDataType(ComplexDataTypeContext ctx) {
        return ParserUtils.withOrigin(ctx, () -> {
            switch (ctx.complex.getType()) {
                case DorisParser.ARRAY:
                    return ArrayType.of(typedVisit(ctx.dataType(0)), true);
                case DorisParser.MAP:
                    return MapType.of(typedVisit(ctx.dataType(0)), typedVisit(ctx.dataType(1)));
                case DorisParser.STRUCT:
                    return new StructType(visitComplexColTypeList(ctx.complexColTypeList()));
                default:
                    throw new AnalysisException("do not support " + ctx.complex.getText() + " type for Nereids");
            }
        });
    }

    @Override
    public List<StructField> visitComplexColTypeList(ComplexColTypeListContext ctx) {
        return ctx.complexColType().stream().map(this::visitComplexColType).collect(ImmutableList.toImmutableList());
    }

    @Override
    public StructField visitComplexColType(ComplexColTypeContext ctx) {
        String comment;
        if (ctx.commentSpec() != null) {
            comment = ctx.commentSpec().STRING_LITERAL().getText();
            comment = LogicalPlanBuilderAssistant.escapeBackSlash(comment.substring(1, comment.length() - 1));
        } else {
            comment = "";
        }
        return new StructField(ctx.identifier().getText(),
                    typedVisit(ctx.dataType()), true, comment);
    }

    private String parseConstant(ConstantContext context) {
        Object constant = visit(context);
        if (constant instanceof Literal && ((Literal) constant).isStringLikeLiteral()) {
            return ((Literal) constant).getStringValue();
        }
        return context.getText();
    }

    @Override
    public Object visitCollate(CollateContext ctx) {
        return visit(ctx.primaryExpression());
    }

    @Override
    public Object visitSample(SampleContext ctx) {
        long seek = ctx.seed == null ? -1L : Long.parseLong(ctx.seed.getText());
        DorisParser.SampleMethodContext sampleContext = ctx.sampleMethod();
        if (sampleContext instanceof SampleByPercentileContext) {
            SampleByPercentileContext sampleByPercentileContext = (SampleByPercentileContext) sampleContext;
            long percent = Long.parseLong(sampleByPercentileContext.INTEGER_VALUE().getText());
            return new TableSample(percent, true, seek);
        }
        SampleByRowsContext sampleByRowsContext = (SampleByRowsContext) sampleContext;
        long rows = Long.parseLong(sampleByRowsContext.INTEGER_VALUE().getText());
        return new TableSample(rows, false, seek);
    }

    @Override
    public Object visitCallProcedure(CallProcedureContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.name);
        FuncNameInfo procedureName = new FuncNameInfo(nameParts);
        List<Expression> arguments = ctx.expression().stream()
                .<Expression>map(this::typedVisit)
                .collect(ImmutableList.toImmutableList());
        UnboundFunction unboundFunction = new UnboundFunction(procedureName.getDbName(), procedureName.getName(),
                true, arguments);
        return new CallCommand(unboundFunction, getOriginSql(ctx));
    }

    @Override
    public LogicalPlan visitCreateProcedure(CreateProcedureContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.name);
        FuncNameInfo procedureName = new FuncNameInfo(nameParts);
        return ParserUtils.withOrigin(ctx, () -> {
            LogicalPlan createProcedurePlan;
            createProcedurePlan = new CreateProcedureCommand(procedureName, getOriginSql(ctx),
                    ctx.REPLACE() != null);
            return createProcedurePlan;
        });
    }

    @Override
    public LogicalPlan visitDropProcedure(DropProcedureContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.name);
        FuncNameInfo procedureName = new FuncNameInfo(nameParts);
        return ParserUtils.withOrigin(ctx, () -> new DropProcedureCommand(procedureName, getOriginSql(ctx)));
    }

    @Override
    public LogicalPlan visitShowProcedureStatus(ShowProcedureStatusContext ctx) {
        Set<Expression> whereExpr = Collections.emptySet();
        if (ctx.whereClause() != null) {
            whereExpr = ExpressionUtils.extractConjunctionToSet(
                    getExpression(ctx.whereClause().booleanExpression()));
        }

        if (ctx.valueExpression() != null) {
            // parser allows only LIKE or WhereClause.
            // Mysql grammar: SHOW PROCEDURE STATUS [LIKE 'pattern' | WHERE expr]
            whereExpr = Sets.newHashSet(new Like(new UnboundSlot("ProcedureName"), getExpression(ctx.pattern)));
        }

        final Set<Expression> whereExprConst = whereExpr;
        return ParserUtils.withOrigin(ctx, () -> new ShowProcedureStatusCommand(whereExprConst));
    }

    @Override
    public LogicalPlan visitShowCreateProcedure(ShowCreateProcedureContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.name);
        FuncNameInfo procedureName = new FuncNameInfo(nameParts);
        return ParserUtils.withOrigin(ctx, () -> new ShowCreateProcedureCommand(procedureName));
    }

    @Override
    public LogicalPlan visitDropCatalogRecycleBin(DropCatalogRecycleBinContext ctx) {
        String idTypeStr = ctx.idType.getText().substring(1, ctx.idType.getText().length() - 1);
        IdType idType = IdType.fromString(idTypeStr);
        long id = Long.parseLong(ctx.id.getText());

        return ParserUtils.withOrigin(ctx, () -> new DropCatalogRecycleBinCommand(idType, id));
    }

    @Override
    public Object visitUnsupported(UnsupportedContext ctx) {
        return UnsupportedCommand.INSTANCE;
    }

    @Override
    public LogicalPlan visitCreateTableLike(CreateTableLikeContext ctx) {
        List<String> nameParts = visitMultipartIdentifier(ctx.name);
        List<String> existedTableNameParts = visitMultipartIdentifier(ctx.existedTable);
        ArrayList<String> rollupNames = Lists.newArrayList();
        boolean withAllRollUp = false;
        if (ctx.WITH() != null && ctx.rollupNames != null) {
            rollupNames = new ArrayList<>(visitIdentifierList(ctx.rollupNames));
        } else if (ctx.WITH() != null && ctx.rollupNames == null) {
            withAllRollUp = true;
        }
        CreateTableLikeInfo info = new CreateTableLikeInfo(ctx.EXISTS() != null,
                ctx.TEMPORARY() != null,
                new TableNameInfo(nameParts), new TableNameInfo(existedTableNameParts),
                rollupNames, withAllRollUp);
        return new CreateTableLikeCommand(info);
    }

    @Override
    public LogicalPlan visitShowConfig(ShowConfigContext ctx) {
        ShowConfigCommand command;
        if (ctx.type.getText().equalsIgnoreCase(NodeType.FRONTEND.name())) {
            command = new ShowConfigCommand(NodeType.FRONTEND);
        } else {
            command = new ShowConfigCommand(NodeType.BACKEND);
        }
        if (ctx.LIKE() != null && ctx.pattern != null) {
            Like like = new Like(new UnboundSlot("ProcedureName"), getExpression(ctx.pattern));
            String pattern = ((Literal) like.child(1)).getStringValue();
            command.setPattern(pattern);
        }
        if (ctx.FROM() != null && ctx.backendId != null) {
            long backendId = Long.parseLong(ctx.backendId.getText());
            command.setBackendId(backendId);
        }
        return command;
    }

    @Override
    public ShowViewCommand visitShowView(ShowViewContext ctx) {
        List<String> tableNameParts = visitMultipartIdentifier(ctx.tableName);
        String databaseName = null;
        if (ctx.database != null) {
            databaseName = stripQuotes(ctx.database.getText());
        }
        return new ShowViewCommand(databaseName, new TableNameInfo(tableNameParts));
    }

    @Override
    public LogicalPlan visitKillQuery(KillQueryContext ctx) {
        String queryId = null;
        int connectionId = -1;
        TerminalNode integerValue = ctx.INTEGER_VALUE();
        if (integerValue != null) {
            connectionId = Integer.valueOf(integerValue.getText());
        } else {
            queryId = stripQuotes(ctx.STRING_LITERAL().getText());
        }
        return new KillQueryCommand(queryId, connectionId);
    }

    @Override
    public LogicalPlan visitKillConnection(DorisParser.KillConnectionContext ctx) {
        int connectionId = Integer.parseInt(ctx.INTEGER_VALUE().getText());
        return new KillConnectionCommand(connectionId);
    }

    @Override
    public LogicalPlan visitDropAnalyzeJob(DorisParser.DropAnalyzeJobContext ctx) {
        long jobId = Long.parseLong(ctx.INTEGER_VALUE().getText());
        return new DropAnalyzeJobCommand(jobId);
    }

    @Override
    public LogicalPlan visitKillAnalyzeJob(DorisParser.KillAnalyzeJobContext ctx) {
        long jobId = Long.parseLong(ctx.jobId.getText());
        return new KillAnalyzeJobCommand(jobId);
    }

    public LogicalPlan visitDropRole(DropRoleContext ctx) {
        return new DropRoleCommand(ctx.name.getText(), ctx.EXISTS() != null);
    }

    @Override
    public LogicalPlan visitCreateIndexAnalyzer(CreateIndexAnalyzerContext ctx) {
        boolean ifNotExists = ctx.IF() != null;
        String policyName = ctx.name.getText();
        Map<String, String> properties = visitPropertyClause(ctx.properties);

        return new CreateIndexAnalyzerCommand(ifNotExists, policyName, properties);
    }

    @Override
    public LogicalPlan visitCreateIndexTokenizer(CreateIndexTokenizerContext ctx) {
        boolean ifNotExists = ctx.IF() != null;
        String policyName = ctx.name.getText();
        Map<String, String> properties = visitPropertyClause(ctx.properties);

        return new CreateIndexTokenizerCommand(ifNotExists, policyName, properties);
    }

    @Override
    public LogicalPlan visitCreateIndexTokenFilter(CreateIndexTokenFilterContext ctx) {
        boolean ifNotExists = ctx.IF() != null;
        String policyName = ctx.name.getText();
        Map<String, String> properties = visitPropertyClause(ctx.properties);

        return new CreateIndexTokenFilterCommand(ifNotExists, policyName, properties);
    }

    @Override
    public LogicalPlan visitDropIndexAnalyzer(DropIndexAnalyzerContext ctx) {
        String policyName = ctx.name.getText();
        boolean ifExists = ctx.IF() != null;

        return new DropIndexAnalyzerCommand(policyName, ifExists);
    }

    @Override
    public LogicalPlan visitDropIndexTokenizer(DropIndexTokenizerContext ctx) {
        String policyName = ctx.name.getText();
        boolean ifExists = ctx.IF() != null;

        return new DropIndexTokenizerCommand(policyName, ifExists);
    }

    @Override
    public LogicalPlan visitDropIndexTokenFilter(DropIndexTokenFilterContext ctx) {
        String policyName = ctx.name.getText();
        boolean ifExists = ctx.IF() != null;

        return new DropIndexTokenFilterCommand(policyName, ifExists);
    }

    @Override
    public LogicalPlan visitShowIndexAnalyzer(ShowIndexAnalyzerContext ctx) {
        return new ShowIndexAnalyzerCommand();
    }

    @Override
    public LogicalPlan visitShowIndexTokenizer(ShowIndexTokenizerContext ctx) {
        return new ShowIndexTokenizerCommand();
    }

    @Override
    public LogicalPlan visitShowIndexTokenFilter(ShowIndexTokenFilterContext ctx) {
        return new ShowIndexTokenFilterCommand();
    }
}