// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2012-2014 Monty Program Ab
// Copyright (c) 2015-2024 MariaDB Corporation Ab
package org.mariadb.jdbc.client.result;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.sql.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.mariadb.jdbc.BasePreparedStatement;
import org.mariadb.jdbc.Connection;
import org.mariadb.jdbc.Statement;
import org.mariadb.jdbc.client.Column;
import org.mariadb.jdbc.client.ColumnDecoder;
import org.mariadb.jdbc.client.Context;
import org.mariadb.jdbc.client.result.rowdecoder.BinaryRowDecoder;
import org.mariadb.jdbc.codec.*;
import org.mariadb.jdbc.plugin.Codec;
import org.mariadb.jdbc.plugin.codec.*;
import org.mariadb.jdbc.util.ParameterList;

/** Updatable result implementation */
public class UpdatableResult extends CompleteResult {
  private static final int STATE_STANDARD = 0;
  private static final int STATE_UPDATE = 1;
  private static final int STATE_UPDATED = 2;
  private static final int STATE_INSERT = 3;
  private static final int STATE_INSERTED = 4;

  private String database;
  private String table;
  private boolean canInsert;
  private boolean canUpdate;
  private String sqlStateError = "HY000";
  private boolean isAutoincrementPk;
  private int savedRowPointer;
  private String changeError;
  private int state = STATE_STANDARD;
  private ParameterList parameters;
  private String[] primaryCols;

  /**
   * Constructor
   *
   * @param stmt statement that initiate this result
   * @param binaryProtocol are rows binary encoded
   * @param maxRows maximum rows
   * @param metadataList columns metadata
   * @param reader packet reader
   * @param context connection context
   * @param resultSetType result-set type
   * @param closeOnCompletion close on completion
   * @param traceEnable must network exchanges be logged
   * @throws IOException if any socket error occurs
   * @throws SQLException for other kind of error
   */
  public UpdatableResult(
      Statement stmt,
      boolean binaryProtocol,
      long maxRows,
      ColumnDecoder[] metadataList,
      org.mariadb.jdbc.client.socket.Reader reader,
      Context context,
      int resultSetType,
      boolean closeOnCompletion,
      boolean traceEnable)
      throws IOException, SQLException {
    super(
        stmt,
        binaryProtocol,
        maxRows,
        metadataList,
        reader,
        context,
        resultSetType,
        closeOnCompletion,
        traceEnable);
    checkIfUpdatable();
    parameters = new ParameterList(metadataList.length);
  }

  private void checkIfUpdatable() throws SQLException {
    isAutoincrementPk = false;
    canInsert = true;
    canUpdate = true;

    // check that resultSet concern one table and database exactly
    database = null;
    table = null;
    for (Column columnDefinition : metadataList) {
      if (columnDefinition.getTable().isEmpty()) {
        cannotUpdateInsertRow(
            "The result-set contains fields without without any database/table information");
        sqlStateError = "0A000";
        return;
      }

      if (database != null && !database.equals(columnDefinition.getSchema())) {
        cannotUpdateInsertRow("The result-set contains more than one database");
        sqlStateError = "0A000";
        return;
      }
      database = columnDefinition.getSchema();

      if (table != null && !table.equals(columnDefinition.getTable())) {
        cannotUpdateInsertRow("The result-set contains fields on different tables");
        sqlStateError = "0A000";
        return;
      }
      table = columnDefinition.getTable();
    }

    // check that listed column contain primary field
    for (Column col : metadataList) {
      if (col.isPrimaryKey()) {
        isAutoincrementPk = col.isAutoIncrement();
        if (isAutoincrementPk) {
          primaryCols = new String[] {col.getColumnName()};
          return;
        }
      }
    }

    // check that table contains a generated primary field
    // to check if insert are still possible
    ResultSet rs =
        statement
            .getConnection()
            .createStatement()
            .executeQuery("SHOW COLUMNS FROM `" + database + "`.`" + table + "`");
    List<String> primaryColumns = new ArrayList<>();
    while (rs.next()) {
      if ("PRI".equals(rs.getString("Key"))) {
        primaryColumns.add(rs.getString("Field"));
        boolean keyPresent = false;
        for (Column col : metadataList) {
          if (rs.getString("Field").equals(col.getColumnName())) {
            keyPresent = true;
          }
        }
        boolean canBeNull = "YES".equals(rs.getString("Null"));
        boolean hasDefault = rs.getString("Default") != null;
        boolean generated = rs.getString("Extra") != null && !rs.getString("Extra").isEmpty();
        isAutoincrementPk =
            rs.getString("Extra") != null && rs.getString("Extra").contains("auto_increment");
        if (!keyPresent && !canBeNull && !hasDefault && !generated) {
          canInsert = false;
          changeError =
              String.format("primary field `%s` is not present in query", rs.getString("Field"));
        }
        if (!keyPresent) {
          canUpdate = false;
          changeError =
              String.format(
                  "Cannot update rows, since primary field %s is not present in query",
                  rs.getString("Field"));
        }
      }
    }

    if (primaryColumns.isEmpty()) {
      canUpdate = false;
      changeError = "Cannot update rows, since no primary field is present in query";
    } else {
      primaryCols = primaryColumns.toArray(new String[0]);
    }
  }

  private void cannotUpdateInsertRow(String reason) {
    changeError = reason;
    canUpdate = false;
    canInsert = false;
  }

  private void checkUpdatable(int position) throws SQLException {
    if (position <= 0 || position > metadataList.length) {
      throw exceptionFactory.create("No such column: " + position, "22023");
    }

    if (state == STATE_STANDARD || state == STATE_UPDATED) {
      state = STATE_UPDATE;
    }
    if (state == STATE_UPDATE) {
      if (rowPointer <= BEFORE_FIRST_POS) {
        throw new SQLDataException("Current position is before the first row", "22023");
      }
      if (rowPointer >= dataSize) {
        throw new SQLDataException("Current position is after the last row", "22023");
      }
      if (!canUpdate) {
        throw exceptionFactory.create("ResultSet cannot be updated. " + changeError, sqlStateError);
      }
    }
  }

  @Override
  public boolean rowUpdated() {
    return state == STATE_UPDATED;
  }

  @Override
  public boolean rowInserted() {
    return state == STATE_INSERTED;
  }

  @Override
  public boolean rowDeleted() {
    return false;
  }

  @Override
  public void updateNull(int columnIndex) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, Parameter.NULL_PARAMETER);
  }

  @Override
  public void updateBoolean(int columnIndex, boolean x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(BooleanCodec.INSTANCE, x));
  }

  @Override
  public void updateByte(int columnIndex, byte x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(ByteCodec.INSTANCE, x));
  }

  @Override
  public void updateShort(int columnIndex, short x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(ShortCodec.INSTANCE, x));
  }

  @Override
  public void updateInt(int columnIndex, int x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(IntCodec.INSTANCE, x));
  }

  @Override
  public void updateLong(int columnIndex, long x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(LongCodec.INSTANCE, x));
  }

  @Override
  public void updateFloat(int columnIndex, float x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(FloatCodec.INSTANCE, x));
  }

  @Override
  public void updateDouble(int columnIndex, double x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(DoubleCodec.INSTANCE, x));
  }

  @Override
  public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(BigDecimalCodec.INSTANCE, x));
  }

  @Override
  public void updateString(int columnIndex, String x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(StringCodec.INSTANCE, x));
  }

  @Override
  public void updateBytes(int columnIndex, byte[] x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(ByteArrayCodec.INSTANCE, x));
  }

  @Override
  public void updateDate(int columnIndex, Date x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(DateCodec.INSTANCE, x));
  }

  @Override
  public void updateTime(int columnIndex, Time x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(TimeCodec.INSTANCE, x));
  }

  @Override
  public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(TimestampCodec.INSTANCE, x));
  }

  @Override
  public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, (long) length));
  }

  @Override
  public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, (long) length));
  }

  @Override
  public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, x, (long) length));
  }

  @Override
  public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException {
    updateInternalObject(columnIndex, x, (long) scaleOrLength);
  }

  @Override
  public void updateObject(int columnIndex, Object x) throws SQLException {
    updateInternalObject(columnIndex, x, null);
  }

  @Override
  public void updateNull(String columnLabel) throws SQLException {
    updateNull(findColumn(columnLabel));
  }

  @Override
  public void updateBoolean(String columnLabel, boolean x) throws SQLException {
    updateBoolean(findColumn(columnLabel), x);
  }

  @Override
  public void updateByte(String columnLabel, byte x) throws SQLException {
    updateByte(findColumn(columnLabel), x);
  }

  @Override
  public void updateShort(String columnLabel, short x) throws SQLException {
    updateShort(findColumn(columnLabel), x);
  }

  @Override
  public void updateInt(String columnLabel, int x) throws SQLException {
    updateInt(findColumn(columnLabel), x);
  }

  @Override
  public void updateLong(String columnLabel, long x) throws SQLException {
    updateLong(findColumn(columnLabel), x);
  }

  @Override
  public void updateFloat(String columnLabel, float x) throws SQLException {
    updateFloat(findColumn(columnLabel), x);
  }

  @Override
  public void updateDouble(String columnLabel, double x) throws SQLException {
    updateDouble(findColumn(columnLabel), x);
  }

  @Override
  public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException {
    updateBigDecimal(findColumn(columnLabel), x);
  }

  @Override
  public void updateString(String columnLabel, String x) throws SQLException {
    updateString(findColumn(columnLabel), x);
  }

  @Override
  public void updateBytes(String columnLabel, byte[] x) throws SQLException {
    updateBytes(findColumn(columnLabel), x);
  }

  @Override
  public void updateDate(String columnLabel, Date x) throws SQLException {
    updateDate(findColumn(columnLabel), x);
  }

  @Override
  public void updateTime(String columnLabel, Time x) throws SQLException {
    updateTime(findColumn(columnLabel), x);
  }

  @Override
  public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException {
    updateTimestamp(findColumn(columnLabel), x);
  }

  @Override
  public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException {
    updateAsciiStream(findColumn(columnLabel), x, length);
  }

  @Override
  public void updateBinaryStream(String columnLabel, InputStream x, int length)
      throws SQLException {
    updateBinaryStream(findColumn(columnLabel), x, length);
  }

  @Override
  public void updateCharacterStream(String columnLabel, Reader reader, int length)
      throws SQLException {
    updateCharacterStream(findColumn(columnLabel), reader, length);
  }

  @Override
  public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException {
    updateObject(findColumn(columnLabel), x, scaleOrLength);
  }

  @Override
  public void updateObject(String columnLabel, Object x) throws SQLException {
    updateObject(findColumn(columnLabel), x);
  }

  @Override
  public void insertRow() throws SQLException {
    if (state == STATE_INSERT || state == STATE_INSERTED) {

      // Create query will all field with WHERE clause contain primary field.
      // if field are not updated, value DEFAULT will be set
      // (if field has no default, then insert will throw an exception that will be return to
      // user)

      String insertSql = buildInsertQuery();
      try (PreparedStatement insertPreparedStatement =
          ((Connection) statement.getConnection())
              .prepareInternal(
                  insertSql,
                  Statement.RETURN_GENERATED_KEYS,
                  ResultSet.TYPE_FORWARD_ONLY,
                  ResultSet.CONCUR_READ_ONLY,
                  rowDecoder instanceof BinaryRowDecoder)) {

        int paramPos = 0;
        for (int pos = 0; pos < metadataList.length; pos++) {
          Column colInfo = metadataList[pos];
          org.mariadb.jdbc.client.util.Parameter param =
              parameters.size() > pos ? parameters.get(pos) : null;
          if (param != null) {
            ((BasePreparedStatement) insertPreparedStatement).setParameter(paramPos++, param);
          } else if (!Arrays.asList(primaryCols).contains(colInfo.getColumnName())
              && !colInfo.hasDefault()) {
            ((BasePreparedStatement) insertPreparedStatement)
                .setParameter(paramPos++, Parameter.NULL_PARAMETER);
          }
        }
        ResultSet insertRs = insertPreparedStatement.executeQuery();
        if (context.getVersion().isMariaDBServer()
            && context.getVersion().versionGreaterOrEqual(10, 5, 1)) {
          if (insertRs.next()) {
            byte[] rowByte = ((Result) insertRs).getCurrentRowData();
            addRowData(rowByte);
          }
        } else if (isAutoincrementPk) {
          // primary is auto_increment (only one field)
          ResultSet rsKey = insertPreparedStatement.getGeneratedKeys();
          if (rsKey.next()) {
            try (PreparedStatement refreshPreparedStatement = prepareRefreshStmt()) {
              refreshPreparedStatement.setObject(1, rsKey.getObject(1));
              Result rs = (Result) refreshPreparedStatement.executeQuery();
              // update row data only if not deleted externally
              if (rs.next()) {
                addRowData(rs.getCurrentRowData());
              }
            }
          }
        } else {
          addRowData(refreshRawData());
        }
      }
      parameters = new ParameterList(parameters.size());
      state = STATE_INSERTED;
    }
  }

  /**
   * Build insert query
   *
   * @return insert sql
   * @throws SQLException exception
   */
  private String buildInsertQuery() throws SQLException {
    StringBuilder insertSql = new StringBuilder("INSERT `" + database + "`.`" + table + "` ( ");
    StringBuilder valueClause = new StringBuilder();
    StringBuilder returningClause = new StringBuilder();

    boolean firstParam = true;

    for (int pos = 0; pos < metadataList.length; pos++) {
      Column colInfo = metadataList[pos];

      if (pos != 0) {
        returningClause.append(", ");
      }
      returningClause.append("`").append(colInfo.getColumnName()).append("`");

      org.mariadb.jdbc.client.util.Parameter param =
          parameters.size() > pos ? parameters.get(pos) : null;
      if (param != null) {
        if (!firstParam) {
          insertSql.append(",");
          valueClause.append(", ");
        }
        insertSql.append("`").append(colInfo.getColumnName()).append("`");
        valueClause.append("?");
        firstParam = false;
      } else {
        if (Arrays.asList(primaryCols).contains(colInfo.getColumnName())) {
          boolean isAutoIncrement =
              colInfo.isAutoIncrement() || (primaryCols.length == 1 && isAutoincrementPk);
          if (isAutoIncrement || colInfo.hasDefault()) {
            if (!isAutoIncrement
                && (!context.getVersion().isMariaDBServer()
                    || !context.getVersion().versionGreaterOrEqual(10, 5, 1))) {
              // driver cannot know generated default value like uuid().
              // but for server 10.5+, will use RETURNING to know primary key
              throw exceptionFactory.create(
                  String.format(
                      "Cannot call insertRow() not setting value for primary key %s "
                          + "with default value before server 10.5",
                      colInfo.getColumnName()));
            }
          } else {
            throw exceptionFactory.create(
                String.format(
                    "Cannot call insertRow() not setting value for primary key %s",
                    colInfo.getColumnName()));
          }
        } else if (!colInfo.hasDefault()) {
          if (!firstParam) {
            insertSql.append(",");
            valueClause.append(", ");
          }
          firstParam = false;
          insertSql.append("`").append(colInfo.getColumnName()).append("`");
          valueClause.append("?");
        }
      }
    }
    insertSql.append(") VALUES (").append(valueClause).append(")");
    if (context.getVersion().isMariaDBServer()
        && context.getVersion().versionGreaterOrEqual(10, 5, 1)) {
      insertSql.append(" RETURNING ").append(returningClause);
    }
    return insertSql.toString();
  }

  private String refreshStmt() {
    // Construct SELECT query according to column metadata, with WHERE part containing primary
    // fields
    StringBuilder selectSql = new StringBuilder("SELECT ");
    StringBuilder whereClause = new StringBuilder(" WHERE ");

    boolean firstPrimary = true;
    for (int pos = 0; pos < metadataList.length; pos++) {
      Column colInfo = metadataList[pos];
      if (pos != 0) {
        selectSql.append(",");
      }
      selectSql.append("`").append(colInfo.getColumnName()).append("`");

      if (Arrays.asList(primaryCols).contains(colInfo.getColumnName())) {
        if (!firstPrimary) {
          whereClause.append("AND ");
        }
        firstPrimary = false;
        whereClause.append("`").append(colInfo.getColumnName()).append("` = ? ");
      }
    }
    selectSql
        .append(" FROM `")
        .append(database)
        .append("`.`")
        .append(table)
        .append("`")
        .append(whereClause);
    return selectSql.toString();
  }

  private PreparedStatement prepareRefreshStmt() throws SQLException {
    // row's raw bytes must be encoded according to current resultSet type
    // so use Server or Client PrepareStatement accordingly
    return ((Connection) statement.getConnection())
        .prepareInternal(
            refreshStmt(),
            Statement.RETURN_GENERATED_KEYS,
            ResultSet.TYPE_FORWARD_ONLY,
            ResultSet.CONCUR_READ_ONLY,
            rowDecoder instanceof BinaryRowDecoder);
  }

  private byte[] refreshRawData() throws SQLException {
    int fieldsPrimaryIndex = 0;
    try (PreparedStatement refreshPreparedStatement = prepareRefreshStmt()) {

      for (int pos = 0; pos < metadataList.length; pos++) {
        Column colInfo = metadataList[pos];
        if (Arrays.asList(primaryCols).contains(colInfo.getColumnName())) {
          if ((state != STATE_STANDARD) && parameters.size() > pos && parameters.get(pos) != null) {
            // Row has just been updated using updateRow() methods.
            // updateRow might have changed primary key, so must use the new value.
            org.mariadb.jdbc.client.util.Parameter value = parameters.get(pos);
            ((BasePreparedStatement) refreshPreparedStatement)
                .setParameter(fieldsPrimaryIndex++, value);
          } else {
            refreshPreparedStatement.setObject(++fieldsPrimaryIndex, getObject(pos + 1));
          }
        }
      }

      Result rs = (Result) refreshPreparedStatement.executeQuery();
      rs.next();
      return rs.getCurrentRowData();
    }
  }

  private String updateQuery() {
    StringBuilder updateSql = new StringBuilder("UPDATE `" + database + "`.`" + table + "` SET ");
    StringBuilder whereClause = new StringBuilder(" WHERE ");

    boolean firstUpdate = true;

    for (int pos = 0; pos < primaryCols.length; pos++) {
      String key = primaryCols[pos];
      if (pos != 0) {
        whereClause.append("AND ");
      }
      whereClause.append("`").append(key).append("` = ? ");
    }

    for (int pos = 0; pos < metadataList.length; pos++) {
      Column colInfo = metadataList[pos];

      if (parameters.size() > pos && parameters.get(pos) != null) {
        if (!firstUpdate) {
          updateSql.append(",");
        }
        firstUpdate = false;
        updateSql.append("`").append(colInfo.getColumnName()).append("` = ? ");
      }
    }
    if (firstUpdate) return null;
    return updateSql.append(whereClause).toString();
  }

  @Override
  public void updateRow() throws SQLException {

    if (state == STATE_INSERT) {
      throw exceptionFactory.create("Cannot call updateRow() when inserting a new row");
    }

    if (rowPointer < 0) {
      throw exceptionFactory.create("Current position is before the first row", "22023");
    }

    if (rowPointer >= dataSize) {
      throw exceptionFactory.create("Current position is after the last row", "22023");
    }

    if (state == STATE_UPDATE || state == STATE_UPDATED) {

      // state is STATE_UPDATE, meaning that at least one field is modified, update query can be
      // run.
      // Construct UPDATE query according to modified field only
      String updateQuery = updateQuery();
      if (updateQuery != null) {
        try (PreparedStatement preparedStatement =
            ((Connection) statement.getConnection())
                .prepareInternal(
                    updateQuery,
                    Statement.RETURN_GENERATED_KEYS,
                    ResultSet.TYPE_FORWARD_ONLY,
                    ResultSet.CONCUR_READ_ONLY,
                    rowDecoder instanceof BinaryRowDecoder)) {

          int fieldsIndex = 0;
          for (int pos = 0; pos < metadataList.length; pos++) {
            if (parameters.size() > pos) {
              org.mariadb.jdbc.client.util.Parameter param = parameters.get(pos);
              if (param != null) {
                ((BasePreparedStatement) preparedStatement).setParameter(fieldsIndex++, param);
              }
            }
          }

          for (int pos = 0; pos < metadataList.length; pos++) {
            Column colInfo = metadataList[pos];
            if (Arrays.asList(primaryCols).contains(colInfo.getColumnName())) {
              preparedStatement.setObject(++fieldsIndex, getObject(pos + 1));
            }
          }

          preparedStatement.execute();
        }
        refreshRow();
      }
      parameters = new ParameterList(parameters.size());
      state = STATE_UPDATED;
    }
  }

  @Override
  public void deleteRow() throws SQLException {

    if (state == STATE_INSERT) {
      throw exceptionFactory.create("Cannot call deleteRow() when inserting a new row");
    }
    if (!canUpdate) {
      throw exceptionFactory.create("ResultSet cannot be updated. " + changeError, sqlStateError);
    }
    if (rowPointer < 0) {
      throw new SQLDataException("Current position is before the first row", "22023");
    }
    if (rowPointer >= dataSize) {
      throw new SQLDataException("Current position is after the last row", "22023");
    }

    // Create query with WHERE clause contain primary field.
    StringBuilder deleteSql =
        new StringBuilder("DELETE FROM `" + database + "`.`" + table + "` WHERE ");
    boolean firstPrimary = true;
    for (Column colInfo : metadataList) {
      if (Arrays.asList(primaryCols).contains(colInfo.getColumnName())) {
        if (!firstPrimary) {
          deleteSql.append("AND ");
        }
        firstPrimary = false;
        deleteSql.append("`").append(colInfo.getColumnName()).append("` = ? ");
      }
    }

    try (PreparedStatement deletePreparedStatement =
        ((Connection) statement.getConnection())
            .prepareInternal(
                deleteSql.toString(),
                Statement.RETURN_GENERATED_KEYS,
                ResultSet.TYPE_FORWARD_ONLY,
                ResultSet.CONCUR_READ_ONLY,
                false)) {

      int fieldsPrimaryIndex = 1;
      for (int pos = 0; pos < metadataList.length; pos++) {
        Column colInfo = metadataList[pos];
        if (Arrays.asList(primaryCols).contains(colInfo.getColumnName())) {
          deletePreparedStatement.setObject(fieldsPrimaryIndex++, getObject(pos + 1));
        }
      }

      deletePreparedStatement.executeUpdate();

      // remove data
      System.arraycopy(data, rowPointer + 1, data, rowPointer, dataSize - 1 - rowPointer);
      data[dataSize - 1] = null;
      dataSize--;
      previous();
    }
  }

  @Override
  public void refreshRow() throws SQLException {
    if (state == STATE_INSERT) {
      throw exceptionFactory.create("Cannot call refreshRow() when inserting a new row");
    }
    if (rowPointer < 0) {
      throw exceptionFactory.create("Current position is before the first row", "22023");
    }
    if (rowPointer >= data.length) {
      throw exceptionFactory.create("Current position is after the last row", "22023");
    }
    if (canUpdate) {
      updateRowData(refreshRawData());
    }
  }

  @Override
  public void cancelRowUpdates() {
    parameters = new ParameterList(parameters.size());
    state = STATE_STANDARD;
  }

  @Override
  public void moveToInsertRow() throws SQLException {
    if (!canInsert) {
      throw exceptionFactory.create("No row can be inserted. " + changeError, sqlStateError);
    }
    parameters = new ParameterList(parameters.size());
    state = STATE_INSERT;
    savedRowPointer = rowPointer;
  }

  @Override
  public void moveToCurrentRow() {
    state = STATE_STANDARD;
    resetToRowPointer();
  }

  @Override
  public void updateBlob(int columnIndex, Blob x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(BlobCodec.INSTANCE, x));
  }

  @Override
  public void updateBlob(String columnLabel, Blob x) throws SQLException {
    updateBlob(findColumn(columnLabel), x);
  }

  @Override
  public void updateClob(int columnIndex, Clob x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(ClobCodec.INSTANCE, x));
  }

  @Override
  public void updateClob(String columnLabel, Clob x) throws SQLException {
    updateClob(findColumn(columnLabel), x);
  }

  @Override
  public void updateNString(int columnIndex, String nString) throws SQLException {
    updateString(columnIndex, nString);
  }

  @Override
  public void updateNString(String columnLabel, String nString) throws SQLException {
    updateString(columnLabel, nString);
  }

  @Override
  public void updateNClob(int columnIndex, NClob nClob) throws SQLException {
    updateClob(columnIndex, nClob);
  }

  @Override
  public void updateNClob(String columnLabel, NClob nClob) throws SQLException {
    updateClob(columnLabel, nClob);
  }

  @Override
  public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException {
    updateCharacterStream(columnIndex, x, length);
  }

  @Override
  public void updateNCharacterStream(String columnLabel, Reader reader, long length)
      throws SQLException {
    updateCharacterStream(columnLabel, reader, length);
  }

  @Override
  public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, length));
  }

  @Override
  public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, length));
  }

  @Override
  public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, x, length));
  }

  @Override
  public void updateAsciiStream(String columnLabel, InputStream x, long length)
      throws SQLException {
    updateAsciiStream(findColumn(columnLabel), x, length);
  }

  @Override
  public void updateBinaryStream(String columnLabel, InputStream x, long length)
      throws SQLException {
    updateBinaryStream(findColumn(columnLabel), x, length);
  }

  @Override
  public void updateCharacterStream(String columnLabel, Reader reader, long length)
      throws SQLException {
    updateCharacterStream(findColumn(columnLabel), reader, length);
  }

  @Override
  public void updateBlob(int columnIndex, InputStream x, long length) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x, length));
  }

  @Override
  public void updateBlob(String columnLabel, InputStream inputStream, long length)
      throws SQLException {
    updateBlob(findColumn(columnLabel), inputStream, length);
  }

  @Override
  public void updateClob(int columnIndex, Reader x, long length) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, x, length));
  }

  @Override
  public void updateClob(String columnLabel, Reader reader, long length) throws SQLException {
    updateClob(findColumn(columnLabel), reader, length);
  }

  @Override
  public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException {
    updateClob(columnIndex, reader, length);
  }

  @Override
  public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException {
    updateClob(columnLabel, reader, length);
  }

  @Override
  public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException {
    updateCharacterStream(columnIndex, x);
  }

  @Override
  public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException {
    updateCharacterStream(columnLabel, reader);
  }

  @Override
  public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x));
  }

  @Override
  public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x));
  }

  @Override
  public void updateCharacterStream(int columnIndex, Reader x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, x));
  }

  @Override
  public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException {
    updateAsciiStream(findColumn(columnLabel), x);
  }

  @Override
  public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException {
    updateBinaryStream(findColumn(columnLabel), x);
  }

  @Override
  public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException {
    updateCharacterStream(findColumn(columnLabel), reader);
  }

  @Override
  public void updateBlob(int columnIndex, InputStream x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(StreamCodec.INSTANCE, x));
  }

  @Override
  public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException {
    updateBlob(findColumn(columnLabel), inputStream);
  }

  @Override
  public void updateClob(int columnIndex, Reader x) throws SQLException {
    checkUpdatable(columnIndex);
    parameters.set(columnIndex - 1, new Parameter<>(ReaderCodec.INSTANCE, x));
  }

  @Override
  public void updateClob(String columnLabel, Reader reader) throws SQLException {
    updateClob(findColumn(columnLabel), reader);
  }

  @Override
  public void updateNClob(int columnIndex, Reader reader) throws SQLException {
    updateClob(columnIndex, reader);
  }

  @Override
  public void updateNClob(String columnLabel, Reader reader) throws SQLException {
    updateClob(columnLabel, reader);
  }

  @Override
  public void updateObject(int columnIndex, Object x, SQLType targetSqlType, int scaleOrLength)
      throws SQLException {
    updateInternalObject(columnIndex, x, (long) scaleOrLength);
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private void updateInternalObject(int columnIndex, Object x, Long scaleOrLength)
      throws SQLException {
    checkUpdatable(columnIndex);
    if (x == null) {
      parameters.set(columnIndex - 1, Parameter.NULL_PARAMETER);
      return;
    }

    for (Codec<?> codec : context.getConf().codecs()) {
      if (codec.canEncode(x)) {
        Parameter p = new Parameter(codec, x, scaleOrLength);
        parameters.set(columnIndex - 1, p);
        return;
      }
    }

    throw new SQLException(String.format("Type %s not supported type", x.getClass().getName()));
  }

  @Override
  public void updateObject(String columnLabel, Object x, SQLType targetSqlType, int scaleOrLength)
      throws SQLException {
    updateObject(findColumn(columnLabel), x, targetSqlType, scaleOrLength);
  }

  @Override
  public void updateObject(int columnIndex, Object x, SQLType targetSqlType) throws SQLException {
    updateInternalObject(columnIndex, x, null);
  }

  @Override
  public void updateObject(String columnLabel, Object x, SQLType targetSqlType)
      throws SQLException {
    updateObject(findColumn(columnLabel), x, targetSqlType);
  }

  @Override
  public int getConcurrency() {
    return CONCUR_UPDATABLE;
  }

  private void resetToRowPointer() {
    rowPointer = savedRowPointer;
    if (rowPointer != BEFORE_FIRST_POS && rowPointer < dataSize - 1) {
      setRow(data[rowPointer]);
    } else {
      // all data are reads and pointer is after last
      setNullRowBuf();
    }
    savedRowPointer = -1;
  }

  @Override
  public void beforeFirst() throws SQLException {
    if (state == STATE_INSERT) {
      resetToRowPointer();
    }
    state = STATE_STANDARD;
    super.beforeFirst();
  }

  @Override
  public boolean first() throws SQLException {
    if (state == STATE_INSERT) {
      resetToRowPointer();
    }
    state = STATE_STANDARD;
    return super.first();
  }

  @Override
  public boolean last() throws SQLException {
    if (state == STATE_INSERT) {
      resetToRowPointer();
    }
    state = STATE_STANDARD;
    return super.last();
  }

  @Override
  public void afterLast() throws SQLException {
    if (state == STATE_INSERT) {
      resetToRowPointer();
    }
    state = STATE_STANDARD;
    super.afterLast();
  }

  @Override
  public boolean absolute(int row) throws SQLException {
    if (state == STATE_INSERT) {
      resetToRowPointer();
    }
    state = STATE_STANDARD;
    return super.absolute(row);
  }

  @Override
  public boolean relative(int rows) throws SQLException {
    if (state == STATE_INSERT) {
      resetToRowPointer();
    }
    state = STATE_STANDARD;
    return super.relative(rows);
  }

  @Override
  public boolean next() throws SQLException {
    if (state == STATE_INSERT) {
      resetToRowPointer();
    }
    state = STATE_STANDARD;
    return super.next();
  }

  @Override
  public boolean previous() throws SQLException {
    if (state == STATE_INSERT) {
      resetToRowPointer();
    }
    state = STATE_STANDARD;
    return super.previous();
  }
}
