/*
 * 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.avatica;

import org.apache.calcite.avatica.proto.Common;
import org.apache.calcite.avatica.remote.TypedValue;
import org.apache.calcite.avatica.util.FilteredConstants;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.google.protobuf.Descriptors.FieldDescriptor;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;

/**
 * Command handler for getting various metadata. Should be implemented by each
 * driver.
 *
 * <p>Also holds other abstract methods that are not related to metadata
 * that each provider must implement. This is not ideal.</p>
 */
public interface Meta {

  /**
   * Returns a map of static database properties.
   *
   * <p>The provider can omit properties whose value is the same as the
   * default.
   */
  Map<DatabaseProperty, Object> getDatabaseProperties(ConnectionHandle ch);

  /** Per {@link DatabaseMetaData#getTables(String, String, String, String[])}. */
  MetaResultSet getTables(ConnectionHandle ch,
      String catalog,
      Pat schemaPattern,
      Pat tableNamePattern,
      List<String> typeList);

  /** Per {@link DatabaseMetaData#getColumns(String, String, String, String)}. */
  MetaResultSet getColumns(ConnectionHandle ch,
      String catalog,
      Pat schemaPattern,
      Pat tableNamePattern,
      Pat columnNamePattern);

  MetaResultSet getSchemas(ConnectionHandle ch, String catalog, Pat schemaPattern);

  /** Per {@link DatabaseMetaData#getCatalogs()}. */
  MetaResultSet getCatalogs(ConnectionHandle ch);

  /** Per {@link DatabaseMetaData#getTableTypes()}. */
  MetaResultSet getTableTypes(ConnectionHandle ch);

  /** Per {@link DatabaseMetaData#getProcedures(String, String, String)}. */
  MetaResultSet getProcedures(ConnectionHandle ch,
      String catalog,
      Pat schemaPattern,
      Pat procedureNamePattern);

  /** Per {@link DatabaseMetaData#getProcedureColumns(String, String, String, String)}. */
  MetaResultSet getProcedureColumns(ConnectionHandle ch,
      String catalog,
      Pat schemaPattern,
      Pat procedureNamePattern,
      Pat columnNamePattern);

  /** Per {@link DatabaseMetaData#getColumnPrivileges(String, String, String, String)}. */
  MetaResultSet getColumnPrivileges(ConnectionHandle ch,
      String catalog,
      String schema,
      String table,
      Pat columnNamePattern);

  /** Per {@link DatabaseMetaData#getTablePrivileges(String, String, String)}. */
  MetaResultSet getTablePrivileges(ConnectionHandle ch,
      String catalog,
      Pat schemaPattern,
      Pat tableNamePattern);

  /** Per
   * {@link DatabaseMetaData#getBestRowIdentifier(String, String, String, int, boolean)}. */
  MetaResultSet getBestRowIdentifier(ConnectionHandle ch,
      String catalog,
      String schema,
      String table,
      int scope,
      boolean nullable);

  /** Per {@link DatabaseMetaData#getVersionColumns(String, String, String)}. */
  MetaResultSet getVersionColumns(ConnectionHandle ch, String catalog, String schema, String table);

  /** Per {@link DatabaseMetaData#getPrimaryKeys(String, String, String)}. */
  MetaResultSet getPrimaryKeys(ConnectionHandle ch, String catalog, String schema, String table);

  /** Per {@link DatabaseMetaData#getImportedKeys(String, String, String)}. */
  MetaResultSet getImportedKeys(ConnectionHandle ch, String catalog, String schema, String table);

  /** Per {@link DatabaseMetaData#getExportedKeys(String, String, String)}. */
  MetaResultSet getExportedKeys(ConnectionHandle ch, String catalog, String schema, String table);

  /** Per
   * {@link DatabaseMetaData#getCrossReference(String, String, String, String, String, String)}. */
  MetaResultSet getCrossReference(ConnectionHandle ch,
      String parentCatalog,
      String parentSchema,
      String parentTable,
      String foreignCatalog,
      String foreignSchema,
      String foreignTable);

  /** Per {@link DatabaseMetaData#getTypeInfo()}. */
  MetaResultSet getTypeInfo(ConnectionHandle ch);

  /** Per {@link DatabaseMetaData#getIndexInfo(String, String, String, boolean, boolean)}. */
  MetaResultSet getIndexInfo(ConnectionHandle ch, String catalog,
      String schema,
      String table,
      boolean unique,
      boolean approximate);

  /** Per {@link DatabaseMetaData#getUDTs(String, String, String, int[])}. */
  MetaResultSet getUDTs(ConnectionHandle ch,
      String catalog,
      Pat schemaPattern,
      Pat typeNamePattern,
      int[] types);

  /** Per {@link DatabaseMetaData#getSuperTypes(String, String, String)}. */
  MetaResultSet getSuperTypes(ConnectionHandle ch,
      String catalog,
      Pat schemaPattern,
      Pat typeNamePattern);

  /** Per {@link DatabaseMetaData#getSuperTables(String, String, String)}. */
  MetaResultSet getSuperTables(ConnectionHandle ch,
      String catalog,
      Pat schemaPattern,
      Pat tableNamePattern);

  /** Per {@link DatabaseMetaData#getAttributes(String, String, String, String)}. */
  MetaResultSet getAttributes(ConnectionHandle ch,
      String catalog,
      Pat schemaPattern,
      Pat typeNamePattern,
      Pat attributeNamePattern);

  /** Per {@link DatabaseMetaData#getClientInfoProperties()}. */
  MetaResultSet getClientInfoProperties(ConnectionHandle ch);

  /** Per {@link DatabaseMetaData#getFunctions(String, String, String)}. */
  MetaResultSet getFunctions(ConnectionHandle ch,
      String catalog,
      Pat schemaPattern,
      Pat functionNamePattern);

  /** Per {@link DatabaseMetaData#getFunctionColumns(String, String, String, String)}. */
  MetaResultSet getFunctionColumns(ConnectionHandle ch,
      String catalog,
      Pat schemaPattern,
      Pat functionNamePattern,
      Pat columnNamePattern);

  /** Per {@link DatabaseMetaData#getPseudoColumns(String, String, String, String)}. */
  MetaResultSet getPseudoColumns(ConnectionHandle ch,
      String catalog,
      Pat schemaPattern,
      Pat tableNamePattern,
      Pat columnNamePattern);

  /** Creates an iterable for a result set.
   *
   * <p>The default implementation just returns {@code iterable}, which it
   * requires to be not null; derived classes may instead choose to execute the
   * relational expression in {@code signature}. */
  Iterable<Object> createIterable(StatementHandle stmt, QueryState state, Signature signature,
      List<TypedValue> parameters, Frame firstFrame);

  /** Prepares a statement.
   *
   * @param ch Connection handle
   * @param sql SQL query
   * @param maxRowCount Negative for no limit (different meaning than JDBC)
   * @return Signature of prepared statement
   */
  StatementHandle prepare(ConnectionHandle ch, String sql, long maxRowCount);

  /** Prepares and executes a statement.
   *
   * @param h Statement handle
   * @param sql SQL query
   * @param maxRowCount Negative for no limit (different meaning than JDBC)
   * @param callback Callback to lock, clear and assign cursor
   *
   * @return Result containing statement ID, and if a query, a result set and
   *     first frame of data
   * @deprecated See {@link #prepareAndExecute(StatementHandle, String, long, int, PrepareCallback)}
   */
  @Deprecated // to be removed before 2.0
  ExecuteResult prepareAndExecute(StatementHandle h, String sql,
      long maxRowCount, PrepareCallback callback) throws NoSuchStatementException;

  /** Prepares and executes a statement.
   *
   * @param h Statement handle
   * @param sql SQL query
   * @param maxRowCount Maximum number of rows for the entire query. Negative for no limit
   *    (different meaning than JDBC).
   * @param maxRowsInFirstFrame Maximum number of rows for the first frame. This value should
   *    always be less than or equal to {@code maxRowCount} as the number of results are guaranteed
   *    to be restricted by {@code maxRowCount} and the underlying database.
   * @param callback Callback to lock, clear and assign cursor
   *
   * @return Result containing statement ID, and if a query, a result set and
   *     first frame of data
   */
  ExecuteResult prepareAndExecute(StatementHandle h, String sql,
      long maxRowCount, int maxRowsInFirstFrame, PrepareCallback callback)
      throws NoSuchStatementException;

  /** Prepares a statement and then executes a number of SQL commands in one pass.
   *
   * @param h Statement handle
   * @param sqlCommands SQL commands to run
   * @return An array of update counts containing one element for each command in the batch.
   */
  ExecuteBatchResult prepareAndExecuteBatch(StatementHandle h, List<String> sqlCommands)
      throws NoSuchStatementException;

  /** Executes a collection of bound parameter values on a prepared statement.
   *
   * @param h Statement handle
   * @param parameterValues A collection of list of typed values, one list per batch
   * @return An array of update counts containing one element for each command in the batch.
   */
  ExecuteBatchResult executeBatch(StatementHandle h, List<List<TypedValue>> parameterValues)
      throws NoSuchStatementException;

  /** Returns a frame of rows.
   *
   * <p>The frame describes whether there may be another frame. If there is not
   * another frame, the current iteration is done when we have finished the
   * rows in the this frame.
   *
   * <p>The default implementation always returns null.
   *
   * @param h Statement handle
   * @param offset Zero-based offset of first row in the requested frame
   * @param fetchMaxRowCount Maximum number of rows to return; negative means
   * no limit
   * @return Frame, or null if there are no more
   */
  Frame fetch(StatementHandle h, long offset, int fetchMaxRowCount) throws
      NoSuchStatementException, MissingResultsException;

  /** Executes a prepared statement.
   *
   * @param h Statement handle
   * @param parameterValues A list of parameter values; may be empty, not null
   * @param maxRowCount Maximum number of rows to return; negative means
   * no limit
   * @return Execute result
   * @deprecated See {@link #execute(StatementHandle, List, int)}
   */
  @Deprecated // to be removed before 2.0
  ExecuteResult execute(StatementHandle h, List<TypedValue> parameterValues,
      long maxRowCount) throws NoSuchStatementException;

  /** Executes a prepared statement.
   *
   * @param h Statement handle
   * @param parameterValues A list of parameter values; may be empty, not null
   * @param maxRowsInFirstFrame Maximum number of rows to return in the Frame.
   * @return Execute result
   */
  ExecuteResult execute(StatementHandle h, List<TypedValue> parameterValues,
      int maxRowsInFirstFrame) throws NoSuchStatementException;

  /** Called during the creation of a statement to allocate a new handle.
   *
   * @param ch Connection handle
   */
  StatementHandle createStatement(ConnectionHandle ch);

  /** Closes a statement.
   *
   * <p>If the statement handle is not known, or is already closed, does
   * nothing.
   *
   * @param h Statement handle
   */
  void closeStatement(StatementHandle h);

  /**
   * Opens (creates) a connection. The client allocates its own connection ID which the server is
   * then made aware of through the {@link ConnectionHandle}. The Map {@code info} argument is
   * analogous to the {@link Properties} typically passed to a "normal" JDBC Driver. Avatica
   * specific properties should not be included -- only properties for the underlying driver.
   *
   * @param ch A ConnectionHandle encapsulates information about the connection to be opened
   *    as provided by the client.
   * @param info A Map corresponding to the Properties typically passed to a JDBC Driver.
   */
  void openConnection(ConnectionHandle ch, Map<String, String> info);

  /** Closes a connection */
  void closeConnection(ConnectionHandle ch);

  /**
   * Re-sets the {@link ResultSet} on a Statement. Not a JDBC method.
   *
   * @return True if there are results to fetch after resetting to the given offset. False otherwise
   */
  boolean syncResults(StatementHandle sh, QueryState state, long offset)
      throws NoSuchStatementException;

  /**
   * Makes all changes since the last commit/rollback permanent. Analogous to
   * {@link Connection#commit()}.
   *
   * @param ch A reference to the real JDBC Connection
   */
  void commit(ConnectionHandle ch);

  /**
   * Undoes all changes since the last commit/rollback. Analogous to
   * {@link Connection#rollback()};
   *
   * @param ch A reference to the real JDBC Connection
   */
  void rollback(ConnectionHandle ch);

  /** Synchronizes client and server view of connection properties.
   *
   * <p>Note: this interface is considered "experimental" and may undergo further changes as this
   * functionality is extended to other aspects of state management for
   * {@link java.sql.Connection}, {@link java.sql.Statement}, and {@link java.sql.ResultSet}.</p>
   */
  ConnectionProperties connectionSync(ConnectionHandle ch, ConnectionProperties connProps);

  /** Factory to create instances of {@link Meta}. */
  interface Factory {
    Meta create(List<String> args);
  }

  /** Wrapper to remind API calls that a parameter is a pattern (allows '%' and
   * '_' wildcards, per the JDBC spec) rather than a string to be matched
   * exactly. */
  class Pat {
    public final String s;

    private Pat(String s) {
      this.s = s;
    }

    @Override public String toString() {
      return "Pat[" + s + "]";
    }

    @JsonCreator
    public static Pat of(@JsonProperty("s") String name) {
      return new Pat(name);
    }
  }

  /** Database property.
   *
   * <p>Values exist for methods, such as
   * {@link DatabaseMetaData#getSQLKeywords()}, which always return the same
   * value at all times and across connections.
   *
   * @see #getDatabaseProperties(Meta.ConnectionHandle)
   */
  enum DatabaseProperty {
    /** Database property containing the value of
     * {@link DatabaseMetaData#getNumericFunctions()}. */
    GET_NUMERIC_FUNCTIONS(""),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getStringFunctions()}. */
    GET_STRING_FUNCTIONS(""),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getSystemFunctions()}. */
    GET_SYSTEM_FUNCTIONS(""),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getTimeDateFunctions()}. */
    GET_TIME_DATE_FUNCTIONS(""),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getSQLKeywords()}. */
    GET_S_Q_L_KEYWORDS(""),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getDefaultTransactionIsolation()}. */
    GET_DEFAULT_TRANSACTION_ISOLATION(Connection.TRANSACTION_NONE),

    /** Database property which is the Avatica version */
    AVATICA_VERSION(FilteredConstants.VERSION),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getDriverVersion()}. */
    GET_DRIVER_VERSION(""),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getDriverMinorVersion()}. */
    GET_DRIVER_MINOR_VERSION(-1),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getDriverMajorVersion()}. */
    GET_DRIVER_MAJOR_VERSION(-1),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getDriverName()}. */
    GET_DRIVER_NAME(""),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getDatabaseMinorVersion()}. */
    GET_DATABASE_MINOR_VERSION(-1),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getDatabaseMajorVersion()}. */
    GET_DATABASE_MAJOR_VERSION(-1),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getDatabaseProductName()}. */
    GET_DATABASE_PRODUCT_NAME(""),

    /** Database property containing the value of
     * {@link DatabaseMetaData#getDatabaseProductVersion()}. */
    GET_DATABASE_PRODUCT_VERSION("");

    public final Class<?> type;
    public final Object defaultValue;
    public final Method method;
    public final boolean isJdbc;

    <T> DatabaseProperty(T defaultValue) {
      this.defaultValue = defaultValue;
      final String methodName = AvaticaUtils.toCamelCase(name());
      Method localMethod = null;
      try {
        localMethod = DatabaseMetaData.class.getMethod(methodName);
      } catch (NoSuchMethodException e) {
        // Pass, localMethod stays null.
      }

      if (null == localMethod) {
        this.method = null;
        this.type = null;
        this.isJdbc = false;
      } else {
        this.method = localMethod;
        this.type = AvaticaUtils.box(method.getReturnType());
        this.isJdbc = true;
      }

      // It's either: 1) not a JDBC method, 2) has no default value,
      // 3) the defaultValue is of the expected type
      assert !isJdbc || defaultValue == null || defaultValue.getClass() == type;
    }

    /** Returns a value of this property, using the default value if the map
     * does not contain an explicit value. */
    public <T> T getProp(Meta meta, ConnectionHandle ch, Class<T> aClass) {
      return getProp(meta.getDatabaseProperties(ch), aClass);
    }

    /** Returns a value of this property, using the default value if the map
     * does not contain an explicit value. */
    public <T> T getProp(Map<DatabaseProperty, Object> map, Class<T> aClass) {
      assert aClass == type;
      Object v = map.get(this);
      if (v == null) {
        v = defaultValue;
      }
      return aClass.cast(v);
    }

    public static DatabaseProperty fromProto(Common.DatabaseProperty proto) {
      return DatabaseProperty.valueOf(proto.getName());
    }

    public Common.DatabaseProperty toProto() {
      return Common.DatabaseProperty.newBuilder().setName(name()).build();
    }
  }

  /** Response from execute.
   *
   * <p>Typically a query will have a result set and rowCount = -1;
   * a DML statement will have a rowCount and no result sets.
   */
  class ExecuteResult {
    public final List<MetaResultSet> resultSets;

    public ExecuteResult(List<MetaResultSet> resultSets) {
      this.resultSets = resultSets;
    }
  }

  /**
   * Response from a collection of SQL commands or parameter values in a single batch.
   */
  class ExecuteBatchResult {
    public final long[] updateCounts;

    public ExecuteBatchResult(long[] updateCounts) {
      this.updateCounts = Objects.requireNonNull(updateCounts);
    }
  }

  /** Meta data from which a result set can be constructed.
   *
   * <p>If {@code updateCount} is not -1, the result is just a count. A result
   * set cannot be constructed. */
  class MetaResultSet {
    public final String connectionId;
    public final int statementId;
    public final boolean ownStatement;
    public final Frame firstFrame;
    public final Signature signature;
    public final long updateCount;

    @Deprecated // to be removed before 2.0
    protected MetaResultSet(String connectionId, int statementId,
        boolean ownStatement, Signature signature, Frame firstFrame,
        int updateCount) {
      this(connectionId, statementId, ownStatement, signature, firstFrame,
          (long) updateCount);
    }

    protected MetaResultSet(String connectionId, int statementId,
        boolean ownStatement, Signature signature, Frame firstFrame,
        long updateCount) {
      this.signature = signature;
      this.connectionId = connectionId;
      this.statementId = statementId;
      this.ownStatement = ownStatement;
      this.firstFrame = firstFrame; // may be null even if signature is not null
      this.updateCount = updateCount;
    }

    public static MetaResultSet create(String connectionId, int statementId,
        boolean ownStatement, Signature signature, Frame firstFrame) {
      return new MetaResultSet(connectionId, statementId, ownStatement,
          Objects.requireNonNull(signature), firstFrame, -1L);
    }

    public static MetaResultSet count(String connectionId, int statementId,
        long updateCount) {
      assert updateCount >= 0
          : "Meta.count(" + connectionId + ", " + statementId + ", "
          + updateCount + ")";
      return new MetaResultSet(connectionId, statementId, false, null, null,
          updateCount);
    }
  }

  /** Information necessary to convert an {@link Iterable} into a
   * {@link org.apache.calcite.avatica.util.Cursor}. */
  final class CursorFactory {
    private static final FieldDescriptor CLASS_NAME_DESCRIPTOR = Common.CursorFactory.
        getDescriptor().findFieldByNumber(Common.CursorFactory.CLASS_NAME_FIELD_NUMBER);

    public final Style style;
    public final Class clazz;
    @JsonIgnore
    public final List<Field> fields;
    public final List<String> fieldNames;

    private CursorFactory(Style style, Class clazz, List<Field> fields,
        List<String> fieldNames) {
      assert (fieldNames != null)
          == (style == Style.RECORD_PROJECTION || style == Style.MAP);
      assert (fields != null) == (style == Style.RECORD_PROJECTION);
      this.style = Objects.requireNonNull(style);
      this.clazz = clazz;
      this.fields = fields;
      this.fieldNames = fieldNames;
    }

    @JsonCreator
    public static CursorFactory create(@JsonProperty("style") Style style,
        @JsonProperty("clazz") Class clazz,
        @JsonProperty("fieldNames") List<String> fieldNames) {
      switch (style) {
      case OBJECT:
        return OBJECT;
      case ARRAY:
        return ARRAY;
      case LIST:
        return LIST;
      case RECORD:
        return record(clazz);
      case RECORD_PROJECTION:
        return record(clazz, null, fieldNames);
      case MAP:
        return map(fieldNames);
      default:
        throw new AssertionError("unknown style: " + style);
      }
    }

    public static final CursorFactory OBJECT =
        new CursorFactory(Style.OBJECT, null, null, null);

    public static final CursorFactory ARRAY =
        new CursorFactory(Style.ARRAY, null, null, null);

    public static final CursorFactory LIST =
        new CursorFactory(Style.LIST, null, null, null);

    public static CursorFactory record(Class resultClazz) {
      return new CursorFactory(Style.RECORD, resultClazz, null, null);
    }

    public static CursorFactory record(Class resultClass, List<Field> fields,
        List<String> fieldNames) {
      if (fields == null) {
        fields = new ArrayList<>();
        for (String fieldName : fieldNames) {
          try {
            fields.add(resultClass.getField(fieldName));
          } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
          }
        }
      }
      return new CursorFactory(Style.RECORD_PROJECTION, resultClass, fields,
          fieldNames);
    }

    public static CursorFactory map(List<String> fieldNames) {
      return new CursorFactory(Style.MAP, null, null, fieldNames);
    }

    public static CursorFactory deduce(List<ColumnMetaData> columns,
        Class resultClazz) {
      if (columns.size() == 1) {
        return OBJECT;
      }
      if (resultClazz == null) {
        return ARRAY;
      }
      if (resultClazz.isArray()) {
        return ARRAY;
      }
      if (List.class.isAssignableFrom(resultClazz)) {
        return LIST;
      }
      return record(resultClazz);
    }

    public Common.CursorFactory toProto() {
      Common.CursorFactory.Builder builder = Common.CursorFactory.newBuilder();

      if (null != clazz) {
        builder.setClassName(clazz.getName());
      }
      builder.setStyle(style.toProto());
      if (null != fieldNames) {
        builder.addAllFieldNames(fieldNames);
      }

      return builder.build();
    }

    public static CursorFactory fromProto(Common.CursorFactory proto) {
      // Reconstruct CursorFactory
      Class<?> clz = null;

      if (proto.hasField(CLASS_NAME_DESCRIPTOR)) {
        try {
          clz = Class.forName(proto.getClassName());
        } catch (ClassNotFoundException e) {
          throw new RuntimeException(e);
        }
      }

      return CursorFactory.create(Style.fromProto(proto.getStyle()), clz,
          proto.getFieldNamesList());
    }

    @Override public int hashCode() {
      return Objects.hash(clazz, fieldNames, fields, style);
    }

    @Override public boolean equals(Object o) {
      return o == this
          || o instanceof CursorFactory
          && Objects.equals(clazz, ((CursorFactory) o).clazz)
          && Objects.equals(fieldNames, ((CursorFactory) o).fieldNames)
          && Objects.equals(fields, ((CursorFactory) o).fields)
          && style == ((CursorFactory) o).style;
    }
  }

  /** How logical fields are represented in the objects returned by the
   * iterator. */
  enum Style {
    OBJECT,
    RECORD,
    RECORD_PROJECTION,
    ARRAY,
    LIST,
    MAP;

    public Common.CursorFactory.Style toProto() {
      return Common.CursorFactory.Style.valueOf(name());
    }

    public static Style fromProto(Common.CursorFactory.Style proto) {
      return Style.valueOf(proto.name());
    }
  }

  /** Result of preparing a statement. */
  public class Signature {
    private static final FieldDescriptor SQL_DESCRIPTOR = Common.Signature
        .getDescriptor().findFieldByNumber(Common.Signature.SQL_FIELD_NUMBER);
    private static final FieldDescriptor CURSOR_FACTORY_DESCRIPTOR = Common.Signature
        .getDescriptor().findFieldByNumber(Common.Signature.CURSOR_FACTORY_FIELD_NUMBER);

    public final List<ColumnMetaData> columns;
    public final String sql;
    public final List<AvaticaParameter> parameters;
    public final transient Map<String, Object> internalParameters;
    public final CursorFactory cursorFactory;

    public final Meta.StatementType statementType;

    /** Creates a Signature. */
    public Signature(List<ColumnMetaData> columns,
        String sql,
        List<AvaticaParameter> parameters,
        Map<String, Object> internalParameters,
        CursorFactory cursorFactory,
        Meta.StatementType statementType) {
      this.columns = columns;
      this.sql = sql;
      this.parameters = parameters;
      this.internalParameters = internalParameters;
      this.cursorFactory = cursorFactory;
      this.statementType = statementType;
    }

    /** Used by Jackson to create a Signature by de-serializing JSON. */
    @JsonCreator
    public static Signature create(
        @JsonProperty("columns") List<ColumnMetaData> columns,
        @JsonProperty("sql") String sql,
        @JsonProperty("parameters") List<AvaticaParameter> parameters,
        @JsonProperty("cursorFactory") CursorFactory cursorFactory,
        @JsonProperty("statementType") Meta.StatementType statementType) {
      return new Signature(columns, sql, parameters,
          Collections.<String, Object>emptyMap(), cursorFactory, statementType);
    }

    /** Returns a copy of this Signature, substituting given CursorFactory. */
    public Signature setCursorFactory(CursorFactory cursorFactory) {
      return new Signature(columns, sql, parameters, internalParameters,
          cursorFactory, statementType);
    }

    /** Creates a copy of this Signature with null lists and maps converted to
     * empty. */
    public Signature sanitize() {
      if (columns == null || parameters == null || internalParameters == null
          || statementType == null) {
        return new Signature(sanitize(columns), sql, sanitize(parameters),
            sanitize(internalParameters), cursorFactory,
            Meta.StatementType.SELECT);
      }
      return this;
    }

    private <E> List<E> sanitize(List<E> list) {
      return list == null ? Collections.<E>emptyList() : list;
    }

    private <K, V> Map<K, V> sanitize(Map<K, V> map) {
      return map == null ? Collections.<K, V>emptyMap() : map;
    }

    public Common.Signature toProto() {
      Common.Signature.Builder builder = Common.Signature.newBuilder();

      if (null != sql) {
        builder.setSql(sql);
      }

      if (null != cursorFactory) {
        builder.setCursorFactory(cursorFactory.toProto());
      }

      if (null != columns) {
        for (ColumnMetaData column : columns) {
          builder.addColumns(column.toProto());
        }
      }

      if (null != parameters) {
        for (AvaticaParameter parameter : parameters) {
          builder.addParameters(parameter.toProto());
        }
      }

      return builder.build();
    }

    public static Signature fromProto(Common.Signature protoSignature) {
      List<ColumnMetaData> metadata = new ArrayList<>(protoSignature.getColumnsCount());
      for (Common.ColumnMetaData protoMetadata : protoSignature.getColumnsList()) {
        metadata.add(ColumnMetaData.fromProto(protoMetadata));
      }

      List<AvaticaParameter> parameters = new ArrayList<>(protoSignature.getParametersCount());
      for (Common.AvaticaParameter protoParam : protoSignature.getParametersList()) {
        parameters.add(AvaticaParameter.fromProto(protoParam));
      }

      String sql = null;
      if (protoSignature.hasField(SQL_DESCRIPTOR)) {
        sql = protoSignature.getSql();
      }

      CursorFactory cursorFactory = null;
      if (protoSignature.hasField(CURSOR_FACTORY_DESCRIPTOR)) {
        cursorFactory = CursorFactory.fromProto(protoSignature.getCursorFactory());
      }
      final Meta.StatementType statementType =
          Meta.StatementType.fromProto(protoSignature.getStatementType());

      return Signature.create(metadata, sql, parameters, cursorFactory, statementType);
    }

    @Override public int hashCode() {
      return Objects.hash(columns, cursorFactory, parameters, sql);
    }

    @Override public boolean equals(Object o) {
      return o == this
          || o instanceof Signature
          && Objects.equals(columns, ((Signature) o).columns)
          && Objects.equals(cursorFactory, ((Signature) o).cursorFactory)
          && Objects.equals(parameters, ((Signature) o).parameters)
          && Objects.equals(sql, ((Signature) o).sql);
    }
  }

  /** A collection of rows. */
  class Frame {
    private static final FieldDescriptor HAS_ARRAY_VALUE_DESCRIPTOR = Common.ColumnValue
        .getDescriptor().findFieldByNumber(Common.ColumnValue.HAS_ARRAY_VALUE_FIELD_NUMBER);
    private static final FieldDescriptor SCALAR_VALUE_DESCRIPTOR = Common.ColumnValue
        .getDescriptor().findFieldByNumber(Common.ColumnValue.SCALAR_VALUE_FIELD_NUMBER);
    /** Frame that has zero rows and is the last frame. */
    public static final Frame EMPTY =
        new Frame(0, true, Collections.emptyList());

    /** Frame that has zero rows but may have another frame. */
    public static final Frame MORE =
        new Frame(0, false, Collections.emptyList());

    /** Zero-based offset of first row. */
    public final long offset;
    /** Whether this is definitely the last frame of rows.
     * If true, there are no more rows.
     * If false, there may or may not be more rows. */
    public final boolean done;
    /** The rows. */
    public final Iterable<Object> rows;

    public Frame(long offset, boolean done, Iterable<Object> rows) {
      this.offset = offset;
      this.done = done;
      this.rows = rows;
    }

    @JsonCreator
    public static Frame create(@JsonProperty("offset") long offset,
        @JsonProperty("done") boolean done,
        @JsonProperty("rows") List<Object> rows) {
      if (offset == 0 && done && rows.isEmpty()) {
        return EMPTY;
      }
      return new Frame(offset, done, rows);
    }

    public Common.Frame toProto() {
      Common.Frame.Builder builder = Common.Frame.newBuilder();

      builder.setDone(done).setOffset(offset);

      for (Object row : this.rows) {
        if (null == row) {
          // Does this need to be persisted for some reason?
          continue;
        }

        final Common.Row.Builder rowBuilder = Common.Row.newBuilder();
        if (row instanceof Object[]) {
          // If only Object[] was also Iterable.
          for (Object element : (Object[]) row) {
            parseColumn(rowBuilder, element);
          }
        } else if (row instanceof Iterable) {
          for (Object element : (Iterable<?>) row) {
            parseColumn(rowBuilder, element);
          }
        } else {
          // Can a "row" be a primitive? A struct? Only an Array?
          throw new RuntimeException("Only arrays are supported");
        }

        // Collect all rows
        builder.addRows(rowBuilder.build());
      }

      return builder.build();
    }

    static void parseColumn(Common.Row.Builder rowBuilder, Object column) {
      final Common.ColumnValue.Builder columnBuilder = Common.ColumnValue.newBuilder();

      if (column instanceof List) {
        columnBuilder.setHasArrayValue(true);
        List<?> list = (List<?>) column;
        // Add each element in the list/array to the column's value
        for (Object listItem : list) {
          final Common.TypedValue scalarListItem = serializeScalar(listItem);
          columnBuilder.addArrayValue(scalarListItem);
          // Add the deprecated 'value' repeated attribute for backwards compat
          columnBuilder.addValue(scalarListItem);
        }
      } else {
        // The default value, but still explicit.
        columnBuilder.setHasArrayValue(false);
        // Only one value for this column, a scalar.
        final Common.TypedValue scalarVal = serializeScalar(column);
        columnBuilder.setScalarValue(scalarVal);
        // Add the deprecated 'value' repeated attribute for backwards compat
        columnBuilder.addValue(scalarVal);
      }

      // Add value to row
      rowBuilder.addValue(columnBuilder.build());
    }

    static Common.TypedValue serializeScalar(Object element) {
      final Common.TypedValue.Builder valueBuilder = Common.TypedValue.newBuilder();

      // Let TypedValue handle the serialization for us.
      TypedValue.toProto(valueBuilder, element);

      return valueBuilder.build();
    }

    public static Frame fromProto(Common.Frame proto) {
      List<Object> parsedRows = new ArrayList<>(proto.getRowsCount());
      for (Common.Row protoRow : proto.getRowsList()) {
        ArrayList<Object> row = new ArrayList<>(protoRow.getValueCount());
        for (Common.ColumnValue protoColumn : protoRow.getValueList()) {
          final Object value;
          if (!isNewStyleColumn(protoColumn)) {
            // Backward compatibility
            value = parseOldStyleColumn(protoColumn);
          } else {
            // Current style parsing (separate scalar and array values)
            value = parseColumn(protoColumn);
          }

          row.add(value);
        }

        parsedRows.add(row);
      }

      return new Frame(proto.getOffset(), proto.getDone(), parsedRows);
    }

    /**
     * Determines whether this message contains the new attributes in the
     * message. We can't directly test for the negative because our
     * {@code hasField} trick does not work on repeated fields.
     *
     * @param column The protobuf column object
     * @return True if the message is the new style, false otherwise.
     */
    static boolean isNewStyleColumn(Common.ColumnValue column) {
      return column.hasField(HAS_ARRAY_VALUE_DESCRIPTOR)
          || column.hasField(SCALAR_VALUE_DESCRIPTOR);
    }

    /**
     * For Calcite 1.5, we made the mistake of using array length to determine when the value for a
     * column is a scalar or an array. This method performs the old parsing for backwards
     * compatibility.
     *
     * @param column The protobuf ColumnValue object
     * @return The parsed value for this column
     */
    static Object parseOldStyleColumn(Common.ColumnValue column) {
      if (column.getValueCount() > 1) {
        List<Object> array = new ArrayList<>(column.getValueCount());
        for (Common.TypedValue columnValue : column.getValueList()) {
          array.add(deserializeScalarValue(columnValue));
        }
        return array;
      } else {
        return deserializeScalarValue(column.getValue(0));
      }
    }

    /**
     * Parses the value for a ColumnValue using the separated array and scalar attributes.
     *
     * @param column The protobuf ColumnValue object
     * @return The parse value for this column
     */
    static Object parseColumn(Common.ColumnValue column) {
      // Verify that we have one or the other (scalar or array)
      validateColumnValue(column);

      if (!column.hasField(SCALAR_VALUE_DESCRIPTOR)) {
        // The column in this row is an Array (has multiple values)
        List<Object> array = new ArrayList<>(column.getArrayValueCount());
        for (Common.TypedValue arrayValue : column.getArrayValueList()) {
          // Duplicative because of the ColumnValue/TypedValue difference.
          if (Common.Rep.ARRAY == arrayValue.getType()) {
            // Each element in this Array is an Array.
            array.add(parseArray(arrayValue));
          } else {
            // The array element is a scalar.
            array.add(deserializeScalarValue(arrayValue));
          }
        }
        return array;
      } else {
        // Scalar
        return deserializeScalarValue(column.getScalarValue());
      }
    }

    /**
     * Recursively parses a TypedValue while it is an array.
     */
    static Object parseArray(Common.TypedValue array) {
      List<Object> convertedArray = new ArrayList<>(array.getArrayValueCount());
      for (Common.TypedValue arrayElement : array.getArrayValueList()) {
        if (Common.Rep.ARRAY == arrayElement.getType()) {
          // Recurse
          convertedArray.add(parseArray(arrayElement));
        } else {
          // The component type of this array is a scalar.
          convertedArray.add(deserializeScalarValue(arrayElement));
        }
      }
      return convertedArray;
    }

    /**
     * Verifies that a ColumnValue has only a scalar or array value, not both and not neither.
     *
     * @param column The protobuf ColumnValue object
     * @throws IllegalArgumentException When the above condition is not met
     */
    static void validateColumnValue(Common.ColumnValue column) {
      final boolean hasScalar = column.hasField(SCALAR_VALUE_DESCRIPTOR);
      final boolean hasArrayValue = column.getHasArrayValue();

      // These should always be different
      if (hasScalar == hasArrayValue) {
        throw new IllegalArgumentException("A column must have a scalar or array value, not "
            + (hasScalar ? "both" : "neither"));
      }
    }

    static Object deserializeScalarValue(Common.TypedValue protoElement) {
      // ByteString is a single case where TypedValue is representing the data differently
      // (in its "local" form) than Frame does. We need to unwrap the Base64 encoding.
      if (Common.Rep.BYTE_STRING == protoElement.getType()) {
        // Protobuf is sending native bytes (not b64) across the wire. B64 bytes is only for
        // TypedValue's benefit
        return protoElement.getBytesValue().toByteArray();
      }
      // Again, let TypedValue deserialize things for us.
      return TypedValue.fromProto(protoElement).value;
    }

    @Override public int hashCode() {
      return Objects.hash(done, offset, rows);
    }

    @Override public boolean equals(Object o) {
      return o == this
          || o instanceof Frame
          && equalRows(rows, ((Frame) o).rows)
          && offset == ((Frame) o).offset
          && done == ((Frame) o).done;
    }

    private static boolean equalRows(Iterable<Object> rows, Iterable<Object> otherRows) {
      if (null == rows) {
        if (null != otherRows) {
          return false;
        }
      } else {
        Iterator<Object> iter1 = rows.iterator();
        Iterator<Object> iter2 = otherRows.iterator();
        while (iter1.hasNext() && iter2.hasNext()) {
          Object obj1 = iter1.next();
          Object obj2 = iter2.next();

          // Can't just call equals on an array
          if (obj1 instanceof Object[]) {
            if (obj2 instanceof Object[]) {
              // Compare array and array
              if (!Arrays.equals((Object[]) obj1, (Object[]) obj2)) {
                return false;
              }
            } else if (obj2 instanceof List) {
              // compare array and list
              @SuppressWarnings("unchecked")
              List<Object> obj2List = (List<Object>) obj2;
              if (!Arrays.equals((Object[]) obj1, obj2List.toArray())) {
                return false;
              }
            } else {
              // compare array and something that isn't an array will always fail
              return false;
            }
          } else if (obj1 instanceof List) {
            if (obj2 instanceof Object[]) {
              // Compare list and array
              @SuppressWarnings("unchecked")
              List<Object> obj1List = (List<Object>) obj1;
              if (!Arrays.equals(obj1List.toArray(), (Object[]) obj2)) {
                return false;
              }
            } else if (!obj1.equals(obj2)) {
              // compare list and something else, let it fall to equals()
              return false;
            }
          } else if (!obj1.equals(obj2)) {
            // Not an array, leave it to equals()
            return false;
          }
        }

        // More elements in one of the iterables
        if (iter1.hasNext() || iter2.hasNext()) {
          return false;
        }
      }

      return true;
    }
  }

  /** Connection handle. */
  public class ConnectionHandle {
    public final String id;

    @Override public String toString() {
      return id;
    }

    @JsonCreator
    public ConnectionHandle(@JsonProperty("id") String id) {
      this.id = id;
    }
  }

  /** Statement handle. */
  // Visible for testing
  public class StatementHandle {
    private static final FieldDescriptor SIGNATURE_DESCRIPTOR = Common.StatementHandle
        .getDescriptor().findFieldByNumber(Common.StatementHandle.SIGNATURE_FIELD_NUMBER);
    public final String connectionId;
    public final int id;

    // not final because LocalService#apply(PrepareRequest)
    /** Only present for PreparedStatement handles, null otherwise. */
    public Signature signature;

    @Override public String toString() {
      return connectionId + "::" + Integer.toString(id);
    }

    @JsonCreator
    public StatementHandle(
        @JsonProperty("connectionId") String connectionId,
        @JsonProperty("id") int id,
        @JsonProperty("signature") Signature signature) {
      this.connectionId = connectionId;
      this.id = id;
      this.signature = signature;
    }

    public Common.StatementHandle toProto() {
      Common.StatementHandle.Builder builder = Common.StatementHandle.newBuilder()
          .setConnectionId(connectionId).setId(id);
      if (null != signature) {
        builder.setSignature(signature.toProto());
      }
      return builder.build();
    }

    public static StatementHandle fromProto(Common.StatementHandle protoHandle) {
      // Signature is optional in the update path for executes.
      Signature signature = null;
      if (protoHandle.hasField(SIGNATURE_DESCRIPTOR)) {
        signature = Signature.fromProto(protoHandle.getSignature());
      }
      return new StatementHandle(protoHandle.getConnectionId(), protoHandle.getId(), signature);
    }

    @Override public int hashCode() {
      return Objects.hash(connectionId, id, signature);
    }

    @Override public boolean equals(Object o) {
      return o == this
          || o instanceof StatementHandle
          && Objects.equals(connectionId, ((StatementHandle) o).connectionId)
          && Objects.equals(signature, ((StatementHandle) o).signature)
          && id == ((StatementHandle) o).id;
    }
  }

  /** A pojo containing various client-settable {@link java.sql.Connection} properties.
   *
   * <p>{@code java.lang} types are used here so that {@code null} can be used to indicate
   * a value has no been set.</p>
   *
   * <p>Note: this interface is considered "experimental" and may undergo further changes as this
   * functionality is extended to other aspects of state management for
   * {@link java.sql.Connection}, {@link java.sql.Statement}, and {@link java.sql.ResultSet}.</p>
   */
  @JsonTypeInfo(
      use = JsonTypeInfo.Id.NAME,
      property = "connProps",
      defaultImpl = ConnectionPropertiesImpl.class)
  @JsonSubTypes({
      @JsonSubTypes.Type(value = ConnectionPropertiesImpl.class, name = "connPropsImpl")
  })
  interface ConnectionProperties {

    /** Overwrite fields in {@code this} with any non-null fields in {@code that}
     *
     * @return {@code this}
     */
    ConnectionProperties merge(ConnectionProperties that);

    /** @return {@code true} when no properies have been set, {@code false} otherwise. */
    @JsonIgnore
    boolean isEmpty();

    /** Set {@code autoCommit} status.
     *
     * @return {@code this}
     */
    ConnectionProperties setAutoCommit(boolean val);

    Boolean isAutoCommit();

    /** Set {@code readOnly} status.
     *
     * @return {@code this}
     */
    ConnectionProperties setReadOnly(boolean val);

    Boolean isReadOnly();

    /** Set {@code transactionIsolation} status.
     *
     * @return {@code this}
     */
    ConnectionProperties setTransactionIsolation(int val);

    Integer getTransactionIsolation();

    /** Set {@code catalog}.
     *
     * @return {@code this}
     */
    ConnectionProperties setCatalog(String val);

    String getCatalog();

    /** Set {@code schema}.
     *
     * @return {@code this}
     */
    ConnectionProperties setSchema(String val);

    String getSchema();

    Common.ConnectionProperties toProto();
  }

  /** API to put a result set into a statement, being careful to enforce
   * thread-safety and not to overwrite existing open result sets. */
  interface PrepareCallback {
    Object getMonitor();
    void clear() throws SQLException;
    void assign(Signature signature, Frame firstFrame, long updateCount)
        throws SQLException;
    void execute() throws SQLException;
  }

  /** Type of statement. */
  enum StatementType {
    SELECT, INSERT, UPDATE, DELETE, UPSERT, MERGE, OTHER_DML, IS_DML,
    CREATE, DROP, ALTER, OTHER_DDL, CALL;

    public boolean canUpdate() {
      switch(this) {
      case INSERT:
        return true;
      case IS_DML:
        return true;
      default:
        return false;
      }
    }

    public Common.StatementType toProto() {
      return Common.StatementType.valueOf(name());
    }

    public static StatementType fromProto(Common.StatementType proto) {
      return StatementType.valueOf(proto.name());
    }
  }
}

// End Meta.java
