content: /*
   * 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.calcite.sql.validate;
  
  import org.apache.calcite.rel.type.RelDataType;
  import org.apache.calcite.rel.type.RelDataTypeField;
  import org.apache.calcite.rel.type.StructKind;
  import org.apache.calcite.sql.SqlNode;
  import org.apache.calcite.util.Pair;
  import org.apache.calcite.util.Util;
  
  import com.google.common.collect.ImmutableList;
  
  import org.checkerframework.checker.nullness.qual.Nullable;
  
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  
  import static org.apache.calcite.util.Static.RESOURCE;
  
  import static java.util.Objects.requireNonNull;
  
  /**
   * Abstract base for a scope which is defined by a list of child namespaces and
   * which inherits from a parent scope.
   */
  public abstract class ListScope extends DelegatingScope {
    //~ Instance fields --------------------------------------------------------
  
    /**
     * List of child {@link SqlValidatorNamespace} objects and their names.
     */
    public final List<ScopeChild> children = new ArrayList<>();
  
    //~ Constructors -----------------------------------------------------------
  
    protected ListScope(SqlValidatorScope parent) {
      super(parent);
    }
  
    //~ Methods ----------------------------------------------------------------
  
    @Override public void addChild(SqlValidatorNamespace ns, String alias,
        boolean nullable) {
      requireNonNull(alias, "alias");
      children.add(new ScopeChild(children.size(), alias, ns, nullable));
    }
  
    /**
     * Returns an immutable list of child namespaces.
     *
     * @return list of child namespaces
     */
    public List<SqlValidatorNamespace> getChildren() {
      return Util.transform(children, scopeChild -> scopeChild.namespace);
    }
  
    /**
     * Returns an immutable list of child names.
     *
     * @return list of child namespaces
     */
    List<@Nullable String> getChildNames() {
      return Util.transform(children, scopeChild -> scopeChild.name);
    }
  
    /**
     * Whether the ith child namespace produces nullable result.
     *
     * <p>For example, in below query,
     *
     * <blockquote><pre>{@code
     *   SELECT *
     *   FROM EMPS
     *   LEFT OUTER JOIN DEPT
     * }</pre></blockquote>
     *
     * <p>the namespace which corresponding to 'DEPT' is nullable.
     *
     * @param i The child index.
     * @return Whether it's nullable.
     */
    public boolean isChildNullable(int i) {
      return children.get(i).nullable;
    }
  
    private @Nullable ScopeChild findChild(List<String> names,
        SqlNameMatcher nameMatcher) {
      for (ScopeChild child : children) {
        String lastName = Util.last(names);
        if (child.name != null) {
          if (!nameMatcher.matches(child.name, lastName)) {
            // Alias does not match last segment. Don't consider the
            // fully-qualified name. E.g.
            //    SELECT sales.emp.name FROM sales.emp AS otherAlias
            continue;
          }
          if (names.size() == 1) {
            return child;
          }
        }
        // Make sure namespace has been validated.
        validator.validateNamespace(child.namespace, validator.getUnknownType());
  
        // Look up the 2 tables independently, in case one is qualified with
        // catalog & schema and the other is not.
        final SqlValidatorTable table = child.namespace.getTable();
        if (table != null) {
          final ResolvedImpl resolved = new ResolvedImpl();
          resolveTable(names, nameMatcher, Path.EMPTY, resolved);
          if (resolved.count() == 1) {
            Resolve only = resolved.only();
            List<String> qualifiedName = table.getQualifiedName();
            if (only.remainingNames.isEmpty()
                && only.namespace instanceof TableNamespace
                && Objects.equals(qualifiedName, getQualifiedName(only.namespace.getTable()))) {
              return child;
            }
          }
        }
      }
      return null;
    }
  
    private static @Nullable List<String> getQualifiedName(@Nullable SqlValidatorTable table) {
      return table == null ? null : table.getQualifiedName();
    }
  
    @Override public void findAllColumnNames(List<SqlMoniker> result) {
      for (ScopeChild child : children) {
        addColumnNames(child.namespace, result);
      }
      parent.findAllColumnNames(result);
    }
  
    @Override public void findAliases(Collection<SqlMoniker> result) {
      for (ScopeChild child : children) {
        result.add(new SqlMonikerImpl(child.name, SqlMonikerType.TABLE));
      }
      parent.findAliases(result);
    }
  
    @SuppressWarnings("deprecation")
    @Override public Pair<String, SqlValidatorNamespace>
    findQualifyingTableName(final String columnName, SqlNode ctx) {
      final SqlNameMatcher nameMatcher = validator.catalogReader.nameMatcher();
      final Map<String, ScopeChild> map =
          findQualifyingTableNames(columnName, ctx, nameMatcher);
      switch (map.size()) {
      case 0:
        throw validator.newValidationError(ctx,
            RESOURCE.columnNotFound(columnName));
      case 1:
        final Map.Entry<String, ScopeChild> entry =
            map.entrySet().iterator().next();
        return Pair.of(entry.getKey(), entry.getValue().namespace);
      default:
        throw validator.newValidationError(ctx,
            RESOURCE.columnAmbiguous(columnName));
      }
    }
  
    @Override public Map<String, ScopeChild>
    findQualifyingTableNames(String columnName, SqlNode ctx,
        SqlNameMatcher nameMatcher) {
      final Map<String, ScopeChild> map = new HashMap<>();
      for (ScopeChild child : children) {
        final ResolvedImpl resolved = new ResolvedImpl();
        resolve(ImmutableList.of(child.name, columnName), nameMatcher, true,
            resolved);
        if (resolved.count() > 0) {
          map.put(child.name, child);
        }
      }
      switch (map.size()) {
      case 0:
        return parent.findQualifyingTableNames(columnName, ctx, nameMatcher);
      default:
        return map;
      }
    }
  
    @Override public void resolve(List<String> names, SqlNameMatcher nameMatcher,
        boolean deep, Resolved resolved) {
      // First resolve by looking through the child namespaces.
      final ScopeChild child0 = findChild(names, nameMatcher);
      if (child0 != null) {
        final Step path =
            Path.EMPTY.plus(child0.namespace.getRowType(), child0.ordinal,
                child0.name, StructKind.FULLY_QUALIFIED);
        resolved.found(child0.namespace, child0.nullable, this, path,
            ImmutableList.of());
        return;
      }
  
      // Recursively look deeper into the record-valued fields of the namespace,
      // if it allows skipping fields.
      if (deep) {
        for (ScopeChild child : children) {
          // If identifier starts with table alias, remove the alias.
          final List<String> names2 =
              nameMatcher.matches(child.name, names.get(0))
                  ? names.subList(1, names.size())
                  : names;
          resolveInNamespace(child.namespace, child.nullable, names2, nameMatcher,
              Path.EMPTY, resolved);
        }
        if (resolved.count() > 0) {
          return;
        }
      }
  
      // Then call the base class method, which will delegate to the
      // parent scope.
      super.resolve(names, nameMatcher, deep, resolved);
    }
  
    @Override public @Nullable RelDataType resolveColumn(String columnName, SqlNode ctx) {
      final SqlNameMatcher nameMatcher = validator.catalogReader.nameMatcher();
      int found = 0;
      RelDataType type = null;
      for (ScopeChild child : children) {
        SqlValidatorNamespace childNs = child.namespace;
        final RelDataType childRowType = childNs.getRowType();
        final RelDataTypeField field =
            nameMatcher.field(childRowType, columnName);
        if (field != null) {
          found++;
          type = field.getType();
        }
      }
      switch (found) {
      case 0:
        return null;
      case 1:
        return type;
      default:
        throw validator.newValidationError(ctx,
            RESOURCE.columnAmbiguous(columnName));
      }
    }
  
  }
  