content: /*
   * Copyright (C) 2016-2023 ActionTech.
   * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher.
   */
  
  package com.actiontech.dble.plan.node;
  
  import com.actiontech.dble.config.ErrorCode;
  import com.actiontech.dble.plan.NamedField;
  import com.actiontech.dble.plan.Order;
  import com.actiontech.dble.plan.common.context.NameResolutionContext;
  import com.actiontech.dble.plan.common.context.ReferContext;
  import com.actiontech.dble.plan.common.exception.MySQLOutPutException;
  import com.actiontech.dble.plan.common.item.Item;
  import com.actiontech.dble.plan.common.item.ItemField;
  import com.actiontech.dble.plan.common.item.function.ItemFunc;
  import com.actiontech.dble.plan.common.item.function.sumfunc.ItemFuncGroupConcat;
  import com.actiontech.dble.plan.common.item.function.sumfunc.ItemSum;
  import com.actiontech.dble.plan.common.item.subquery.ItemSubQuery;
  import com.actiontech.dble.route.parser.druid.RouteTableConfigInfo;
  import com.actiontech.dble.singleton.TraceManager;
  import com.actiontech.dble.util.CollectionUtil;
  import com.alibaba.druid.sql.ast.SQLOrderingSpecification;
  import com.alibaba.druid.sql.ast.statement.SQLSelectStatement;
  
  import java.util.*;
  
  public abstract class PlanNode {
  
      /**
       * select subQuery list
       */
      public List<ItemSubQuery> getSubQueries() {
          return subQueries;
      }
  
      public enum PlanNodeType {
          NONAME, TABLE, JOIN, MERGE, QUERY, JOIN_INNER, MANAGER_TABLE, MODIFY
      }
  
      public abstract PlanNodeType type();
  
      protected String sql;
      protected SQLSelectStatement ast;
  
      private boolean isDistinct = false;
  
      /**
       * select columns
       */
      List<Item> columnsSelected = new ArrayList<>();
  
      /**
       * orderBy,Notice:field order
       */
      List<Order> orderBys = new LinkedList<>();
  
      /**
       * group By,Notice:field order
       */
      protected List<Order> groups = new LinkedList<>();
  
      /**
       * having condition
       */
      Item havingFilter;
      /**
       * parent node ,eg:subquery may use parent's info
       * http://dev.mysql.com/doc/refman/5.0/en/correlated-subqueries.html
       */
      private PlanNode parent;
  
      /**
       * children NODE
       */
      protected List<PlanNode> children = new ArrayList<>();
  
      /**
       * LIMIT FROM
       */
      private long limitFrom = -1;
  
      /**
       * LIMIT TO
       */
      private long limitTo = -1;
  
      /**
       * filter in where
       */
      Item whereFilter = null;
  
      /**
       * alias for table node ,used for named subNode when join.
       */
      protected String alias;
      /**
       * is this node is subQuery
       */
      private boolean withSubQuery;
      /**
       * is this node contains subQuery
       */
      private boolean containsSubQuery;
      private boolean correlatedSubQuery;
  
      private boolean existView = false;
  
      private HashSet<ItemSum> sumFuncs = new HashSet<>();
  
      private List<ItemSubQuery> subQueries = new ArrayList<>();
  
      List<TableNode> referedTableNodes = new ArrayList<>();
  
      // inner field -> child field
      Map<NamedField, NamedField> innerFields = new LinkedHashMap<>();
  
      Map<NamedField, Item> outerFields = new LinkedHashMap<>();
  
      NameResolutionContext nameContext;
  
      protected ReferContext referContext;
  
      protected boolean keepFieldSchema = true;
      private boolean singleRoute = false;
  
      protected PlanNode() {
          nameContext = new NameResolutionContext();
          referContext = new ReferContext();
          nameContext.setPlanNode(this);
          referContext.setPlanNode(this);
      }
  
      /**
       * map :childnode->iselectable
       */
      private Map<PlanNode, List<Item>> columnsReferredCache = new LinkedHashMap<>();
  
  
      private List<Item> columnsReferList = new ArrayList<>();
  
      private boolean existUnPushDownGroup = false;
  
      /**
       * is global table?
       * ex. TableNode: isGlobaled when tablename is global tablename
       * MergeNode: always false;
       * QueryNode: true <-->subchild is true
       * JoinNode: all children has only one unglobal table at most
       */
      private Set<String> noshardNode = null;
  
      // unGlobalTableCount for this node(contains children)
      int unGlobalTableCount = 0;
  
      private List<Item> nestLoopFilters = null;
  
      private PlanNode nestLoopDependNode;
  
      private List<PlanNode> nestLoopDependOnNodeList = null;
  
      public PlanNode getNestLoopDependNode() {
          return nestLoopDependNode;
      }
  
      public void setNestLoopDependNode(PlanNode nestLoopDependNode) {
          this.nestLoopDependNode = nestLoopDependNode;
      }
  
      public List<PlanNode> getNestLoopDependOnNodeList() {
          return nestLoopDependOnNodeList;
      }
  
      public void setNestLoopDependOnNodeList(List<PlanNode> nestLoopDependOnNodeList) {
          this.nestLoopDependOnNodeList = nestLoopDependOnNodeList;
      }
  
      public boolean haveDependOnNode() {
          return !CollectionUtil.isEmpty(nestLoopDependOnNodeList) && !nestLoopDependOnNodeList.isEmpty();
      }
  
      public abstract String getPureName();
  
      public abstract String getPureSchema();
  
      /* height of node */
      public abstract int getHeight();
  
      final String getCombinedName() {
          if (this.getAlias() != null) {
              return this.getAlias();
          }
          return this.getPureName();
      }
  
      public PlanNode getChild() {
          if (children.isEmpty())
              return null;
          else
              return children.get(0);
      }
  
      public void addChild(PlanNode childNode) {
          if (childNode != null) {
              childNode.setParent(this);
          }
          this.children.add(childNode);
      }
  
      public PlanNode select(List<Item> columnSelected) {
          this.columnsSelected = columnSelected;
          return this;
      }
  
      public PlanNode groupBy(Item c, SQLOrderingSpecification sortOrder) {
          Order order = new Order(c, sortOrder);
          this.groups.add(order);
          return this;
      }
  
      public PlanNode orderBy(Item c, SQLOrderingSpecification sortOrder) {
          if (sortOrder == null) {
              sortOrder = SQLOrderingSpecification.ASC;
          }
          Order order = new Order(c, sortOrder);
          if (!this.orderBys.contains(order)) {
              this.orderBys.add(order);
          }
          return this;
      }
  
      public abstract RouteTableConfigInfo findFieldSourceFromIndex(int index) throws Exception;
  
      public void setUpFields() {
          TraceManager.TraceObject traceObject = TraceManager.threadTrace("set-up-field-for-sql");
          try {
              sumFuncs.clear();
              setUpInnerFields();
              setUpSelects();
              setUpWhere();
              setUpGroupBy();
              setUpHaving();
              setUpOrderBy();
          } finally {
              TraceManager.finishSpan(traceObject);
          }
      }
  
      // column refered start
      public void setUpRefers(boolean isPushDownNode) {
          sumFuncs.clear();
          referContext.setPushDownNode(isPushDownNode);
          // select
          for (Item sel : columnsSelected) {
              setUpItemRefer(sel);
          }
          if (type() == PlanNodeType.JOIN) {
              JoinNode jn = (JoinNode) this;
              if (!isPushDownNode) {
                  for (Item bf : jn.getJoinFilter())
                      setUpItemRefer(bf);
                  setUpFilterRefer(jn.getOtherJoinOnFilter());
              }
          }
          // where, pushdown node does 't need where
          if (!isPushDownNode) {
              setUpFilterRefer(whereFilter);
          }
          // group by
          for (Order groupBy : groups) {
              setUpItemRefer(groupBy.getItem());
          }
          // having
          setUpFilterRefer(havingFilter);
          // order by
          for (Order orderBy : orderBys) {
              setUpItemRefer(orderBy.getItem());
          }
          // make list
          for (List<Item> selSet : columnsReferredCache.values()) {
              columnsReferList.addAll(selSet);
          }
      }
  
      // ==================== helper method =================
  
      public abstract PlanNode copy();
  
      protected final void copySelfTo(PlanNode to) {
          to.setAlias(this.alias);
          to.setDistinct(this.isDistinct);
          for (Item selected : this.getColumnsSelected()) {
              Item copySel = selected.cloneStruct();
              copySel.setItemName(selected.getItemName());
              copySel.setCharsetIndex(selected.getCharsetIndex());
              to.columnsSelected.add(copySel);
          }
          for (Order groupBy : this.getGroupBys()) {
              to.groups.add(groupBy.copy());
          }
          for (Order orderBy : this.getOrderBys()) {
              to.orderBys.add(orderBy.copy());
          }
          to.whereFilter = this.whereFilter == null ? null : this.whereFilter.cloneStruct();
          to.havingFilter = this.havingFilter == null ? null : havingFilter.cloneStruct();
          to.setLimitFrom(this.limitFrom);
          to.setLimitTo(this.limitTo);
          to.setSql(this.getSql());
          to.setWithSubQuery(withSubQuery);
          to.setContainsSubQuery(containsSubQuery);
          to.setCorrelatedSubQuery(correlatedSubQuery);
          to.setUnGlobalTableCount(unGlobalTableCount);
          if (noshardNode != null) {
              Set<String> noShardNode = new HashSet<>(noshardNode);
              to.setNoshardNode(noShardNode);
          }
          to.getSubQueries().addAll(subQueries);
          to.setKeepFieldSchema(keepFieldSchema);
      }
  
      protected void setUpInnerFields() {
          innerFields.clear();
          for (PlanNode child : children) {
              child.setUpFields();
              for (NamedField coutField : child.outerFields.keySet()) {
                  String tmpFieldSchema = null;
                  if (child.isKeepFieldSchema()) {
                      tmpFieldSchema = child.type() == PlanNodeType.TABLE ? ((TableNode) child).getSchema() : coutField.getSchema();
                  }
                  String tmpFieldTable = child.getAlias() == null ? coutField.getTable() : child.getAlias();
                  String tmpFieldName = coutField.getName();
                  NamedField tmpField = new NamedField(tmpFieldSchema, tmpFieldTable, tmpFieldName, coutField.planNode, coutField.isNatualShow());
                  tmpField.setCharsetIndex(coutField.getCharsetIndex());
                  if (innerFields.containsKey(tmpField) && getParent() != null)
                      throw new MySQLOutPutException(ErrorCode.ER_DUP_FIELDNAME, "42S21", "Duplicate column name '" + tmpFieldName + "'");
                  innerFields.put(tmpField, coutField);
              }
          }
      }
  
      protected void setUpSelects() {
          if (columnsSelected.isEmpty()) {
              columnsSelected.add(new ItemField(null, null, "*"));
          }
          boolean withWild = false;
          for (Item sel : columnsSelected) {
              if (sel.isWild())
                  withWild = true;
          }
          if (withWild)
              dealStarColumn();
          outerFields.clear();
          nameContext.setFindInSelect(false);
          nameContext.setSelectFirst(false);
          if (parent instanceof MergeNode) {
              if (parent.getChildren().get(1) != null && parent.getChildren().get(1) == this) {
                  List<Item> aliasList = parent.getChildren().get(0).getColumnsSelected();
                  if (aliasList.size() != columnsSelected.size()) {
                      throw new MySQLOutPutException(ErrorCode.ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT, "21000",
                              "The used SELECT statements have a different number of columns");
                  }
                  for (int i = 0; i < columnsSelected.size(); i++) {
                      Item sel = columnsSelected.get(i);
                      Item beforeUnion = aliasList.get(i);
                      sel.setAlias(beforeUnion.getAlias() == null ? beforeUnion.getItemName() : beforeUnion.getAlias());
                  }
              }
          }
  
          for (Item sel : columnsSelected) {
              setUpItem(sel);
              if (sel instanceof ItemFuncGroupConcat) {
                  ((ItemFuncGroupConcat) sel).fixOrders(nameContext, referContext);
              }
              NamedField field = makeOutNamedField(sel);
              if (outerFields.containsKey(field) && isDuplicateField(this))
                  throw new MySQLOutPutException(ErrorCode.ER_OPTIMIZER, "", "duplicate field");
              outerFields.put(field, sel);
          }
      }
  
      protected boolean isDuplicateField(PlanNode node) {
          if (node.parent == null) {
              return false;
          } else if (node.parent.type() == PlanNodeType.MERGE) {
              return isDuplicateField(node.parent);
          } else {
              return true;
          }
      }
  
      private void setUpWhere() {
          nameContext.setFindInSelect(false);
          nameContext.setSelectFirst(false);
          whereFilter = setUpItem(whereFilter);
      }
  
      private void setUpGroupBy() {
          nameContext.setFindInSelect(true);
          nameContext.setSelectFirst(false);
          for (Order order : groups) {
              Item item = order.getItem();
              if (item.type() == Item.ItemType.INT_ITEM) {
                  int index = item.valInt().intValue();
                  if (index >= 1 && index <= getColumnsSelected().size())
                      order.setItem(getColumnsSelected().get(index - 1));
                  else
                      throw new MySQLOutPutException(ErrorCode.ER_OPTIMIZER, "",
                              "Unknown column '" + index + "' in group statement");
              } else {
                  order.setItem(setUpItem(item));
              }
          }
      }
  
      private void setUpHaving() {
          nameContext.setFindInSelect(true);
          nameContext.setSelectFirst(true);
          havingFilter = setUpItem(havingFilter);
      }
  
      private void setUpOrderBy() {
          nameContext.setFindInSelect(true);
          nameContext.setSelectFirst(true);
          for (Order order : orderBys) {
              Item item = order.getItem();
              if (item.type() == Item.ItemType.INT_ITEM) {
                  int index = item.valInt().intValue();
                  if (index >= 1 && index <= getColumnsSelected().size())
                      order.setItem(getColumnsSelected().get(index - 1));
                  else
                      throw new MySQLOutPutException(ErrorCode.ER_OPTIMIZER, "",
                              "Unknown column '" + index + "' in order statement");
              } else {
                  order.setItem(setUpItem(item));
              }
          }
      }
  
      protected void dealSingleStarColumn(List<Item> newSels) {
          for (NamedField field : innerFields.keySet()) {
              ItemField col = new ItemField(field.getSchema(), field.getTable(), field.getName(), field.getCharsetIndex());
              newSels.add(col);
          }
      }
  
      protected void dealStarColumn() {
          List<Item> newSels = new ArrayList<>();
          for (Item selItem : columnsSelected) {
              if (selItem.isWild()) {
                  ItemField wildField = (ItemField) selItem;
                  if (wildField.getTableName() == null || wildField.getTableName().length() == 0) {
                      dealSingleStarColumn(newSels);
                  } else {
                      String selTable = wildField.getTableName();
                      boolean found = false;
                      for (NamedField field : innerFields.keySet()) {
                          if (selTable.equals(field.getTable())) {
                              ItemField col = new ItemField(field.getSchema(), field.getTable(), field.getName(), field.getCharsetIndex());
                              newSels.add(col);
                              found = true;
                          } else if (found) {
                              // a.* ->a.id,a.id1,b.id ,break when find b.id
                              break;
                          }
                      }
                      if (!found) {
                          throw new MySQLOutPutException(ErrorCode.ER_OPTIMIZER, "",
                                  "child table " + selTable + " not exist!");
                      }
                  }
              } else {
                  newSels.add(selItem);
              }
          }
          columnsSelected = newSels;
      }
  
      protected NamedField makeOutNamedField(Item sel) {
          String tmpSchema = null;
          if (keepFieldSchema) {
              tmpSchema = sel.getDbName();
              if (sel.basicConstItem()) {
                  tmpSchema = getPureSchema();
              }
          }
          String tmpFieldTable = sel.getTableName();
          String tmpFieldName = sel.getItemName();
          if (alias != null)
              tmpFieldTable = alias;
          if (tmpFieldTable == null)// maybe function
              tmpFieldTable = getPureName();
          if (sel.getAlias() != null)
              tmpFieldName = sel.getAlias();
          boolean isShow = sel instanceof ItemField ? ((ItemField) sel).isNatualShow() : true;
          return new NamedField(tmpSchema, tmpFieldTable, tmpFieldName, this, isShow);
      }
  
      Item setUpItem(Item sel) {
          if (sel == null)
              return null;
          return sel.fixFields(nameContext);
      }
  
      private void setUpFilterRefer(Item filter) {
          if (filter != null) {
              if (filter instanceof ItemFunc) {
                  filter.setPushDownName(null);
                  for (Item item : filter.arguments()) {
                      setUpFilterRefer(item);
                  }
              } else {
                  setUpItemRefer(filter);
              }
          }
      }
  
      private void setUpItemRefer(Item sel) {
          if (sel != null)
              sel.fixRefer(referContext);
      }
  
      // --------------------------getter&setter---------------------------
  
      public long getLimitFrom() {
          return limitFrom;
      }
  
      /**
       * aggregate function list
       */
      public HashSet<ItemSum> getSumFuncs() {
          return sumFuncs;
      }
  
      public void setSumFuncs(HashSet<ItemSum> sumFuncs) {
          this.sumFuncs = sumFuncs;
      }
  
      public PlanNode setLimitFrom(long from) {
          this.limitFrom = from;
          return this;
      }
  
      public PlanNode setLimitTo(long to) {
          this.limitTo = to;
          return this;
      }
  
      public PlanNode setGroupBys(List<Order> groupBys) {
          this.groups = groupBys;
          return this;
      }
  
      public PlanNode setColumnsSelected(List<Item> columns) {
          this.columnsSelected = columns;
          return this;
      }
  
      /**
       * @param existView the exsitView to set
       */
      public void setExistView(boolean existView) {
          this.existView = existView;
      }
  
      public long getLimitTo() {
          return limitTo;
      }
  
      public PlanNode limit(long i, long j) {
          this.setLimitFrom(i);
          this.setLimitTo(j);
          return this;
      }
  
      public Item getWhereFilter() {
          return whereFilter;
      }
  
      public PlanNode query(Item where) {
          this.whereFilter = where;
          return this;
      }
  
      public String getAlias() {
          return alias;
      }
  
      public List<Order> getGroupBys() {
          return this.groups;
      }
  
      public List<Order> getOrderBys() {
          return orderBys;
      }
  
      public void setOrderBys(List<Order> orderBys) {
          this.orderBys = orderBys;
      }
  
      public List<PlanNode> getChildren() {
          return this.children;
      }
  
      public List<Item> getColumnsSelected() {
          return columnsSelected;
      }
  
      public List<Item> getColumnsReferedByChild(PlanNode tn) {
          return this.columnsReferredCache.get(tn);
      }
  
      public void addSelToReferedMap(PlanNode tn, Item sel) {
          // the same ReferedMap's selects have the same columnname
          this.columnsReferredCache.computeIfAbsent(tn, k -> new ArrayList<>());
          this.columnsReferredCache.get(tn).add(sel);
      }
  
      public List<Item> getColumnsRefered() {
          return this.columnsReferList;
      }
  
      /**
       * setAlias for table
       */
      public PlanNode setAlias(String aliasName) {
          if (aliasName != null && aliasName.charAt(0) == '`') {
              aliasName = aliasName.substring(1, aliasName.length() - 1);
          }
          this.alias = aliasName;
          return this;
      }
  
      public boolean isWithSubQuery() {
          return withSubQuery;
      }
  
      public void setWithSubQuery(boolean withSubQuery) {
          this.withSubQuery = withSubQuery;
      }
  
      public boolean isContainsSubQuery() {
          return containsSubQuery;
      }
  
      public void setContainsSubQuery(boolean containsSubQuery) {
          this.containsSubQuery = containsSubQuery;
      }
  
  
      public boolean isCorrelatedSubQuery() {
          return correlatedSubQuery;
      }
  
      public void setCorrelatedSubQuery(boolean correlatedSubQuery) {
          this.correlatedSubQuery = correlatedSubQuery;
      }
  
      public PlanNode having(Item having) {
          this.havingFilter = having;
          return this;
      }
  
      public Item getHavingFilter() {
          return this.havingFilter;
      }
  
      public void setWhereFilter(Item whereFilter) {
          this.whereFilter = whereFilter;
      }
  
  
      public int getUnGlobalTableCount() {
          return unGlobalTableCount;
      }
  
      public void setUnGlobalTableCount(int unGlobalTableCount) {
          this.unGlobalTableCount = unGlobalTableCount;
      }
  
      public Set<String> getNoshardNode() {
          return noshardNode;
      }
  
      public void setNoshardNode(Set<String> noshardNode) {
          this.noshardNode = noshardNode;
      }
  
      /* get tablenode list */
      public List<TableNode> getReferedTableNodes() {
          return referedTableNodes;
      }
  
      public PlanNode getParent() {
          return parent;
      }
  
      public void setParent(PlanNode parent) {
          this.parent = parent;
          if (parent != null) {
              parent.referedTableNodes.addAll(referedTableNodes);
              parent.existView |= this.existView;
          }
      }
  
      /**
       * @return the innerFields
       */
      public Map<NamedField, NamedField> getInnerFields() {
          return innerFields;
      }
  
      /**
       * @return the outerFields
       */
      public Map<NamedField, Item> getOuterFields() {
          return outerFields;
      }
  
      /**
       * @return the exsitView
       */
      public boolean isExistView() {
          return existView;
      }
  
      public boolean existUnPushDownGroup() {
          return existUnPushDownGroup;
      }
  
      public void setExistUnPushDownGroup(boolean existUnPushDownGroup) {
          this.existUnPushDownGroup = existUnPushDownGroup;
      }
  
      /**
       * @return the isDistinct
       */
      public boolean isDistinct() {
          return isDistinct;
      }
  
      /**
       * @param distinct the isDistinct to set
       */
      public void setDistinct(boolean distinct) {
          this.isDistinct = distinct;
      }
  
      public String getSql() {
          return sql;
      }
  
      public void setSql(String sql) {
          this.sql = sql;
      }
  
      public SQLSelectStatement getAst() {
          return ast;
      }
  
      public void setAst(SQLSelectStatement ast) {
          this.ast = ast;
      }
  
      /**
       * @return the strategyFilters
       */
      public List<Item> getNestLoopFilters() {
          return nestLoopFilters;
      }
  
      /**
       * @param nestLoopFilters the strategyFilters to set
       */
      public void setNestLoopFilters(List<Item> nestLoopFilters) {
          this.nestLoopFilters = nestLoopFilters;
      }
  
      @Override
      public final String toString() {
          return this.toString(0);
      }
  
      /**
       * show plan in tree
       *
       * @param level level
       * @return String
       */
      public abstract String toString(int level);
  
      public void setKeepFieldSchema(boolean keepFieldSchema) {
          this.keepFieldSchema = keepFieldSchema;
      }
  
      public boolean isKeepFieldSchema() {
          return keepFieldSchema;
      }
  
  
      public boolean isSingleRoute() {
          return singleRoute;
      }
  
      public void setSingleRoute(boolean singleRoute) {
          this.singleRoute = singleRoute;
      }
  
  }
  