/*
 * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; version 2 of the
 * License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */


#include <errno.h>
#define __STDC_LIMIT_MACROS
#include <stdint.h>
#include <cstdlib>
#include <cstdio>

#include <my_config.h>

#include "base/log.h"
#include "base/string_utilities.h"
#include "base/sqlstring.h"

#include "copytable.h"
#include "converter.h"

#undef min

#include <boost/bind.hpp>
#include <boost/algorithm/string.hpp>

DEFAULT_LOG_DOMAIN("copytable");
#define TMP_TRIGGER_TABLE "wb_tmp_triggers"

#if defined(MYSQL_VERSION_MAJOR) && defined(MYSQL_VERSION_MINOR) && defined(MYSQL_VERSION_PATCH)
#define MYSQL_CHECK_VERSION(major,minor,micro) \
    (MYSQL_VERSION_MAJOR > (major) || \
    (MYSQL_VERSION_MAJOR == (major) && MYSQL_VERSION_MINOR > (minor)) || \
    (MYSQL_VERSION_MAJOR == (major) && MYSQL_VERSION_MINOR == (minor) && MYSQL_VERSION_PATCH >= (micro)))
#endif

static const char *mysql_field_type_to_name(enum enum_field_types type)
{
  switch (type)
  {
    case MYSQL_TYPE_DECIMAL: return "MYSQL_TYPE_DECIMAL";
    case MYSQL_TYPE_TINY: return "MYSQL_TYPE_TINY";
    case MYSQL_TYPE_SHORT: return "MYSQL_TYPE_SHORT";
    case MYSQL_TYPE_LONG: return "MYSQL_TYPE_LONG";
    case MYSQL_TYPE_FLOAT: return "MYSQL_TYPE_FLOAT";
    case MYSQL_TYPE_DOUBLE: return "MYSQL_TYPE_DOUBLE";
    case MYSQL_TYPE_NULL: return "MYSQL_TYPE_NULL";
    case MYSQL_TYPE_TIMESTAMP: return "MYSQL_TYPE_TIMESTAMP";
    case MYSQL_TYPE_LONGLONG: return "MYSQL_TYPE_LONGLONG";
    case MYSQL_TYPE_INT24: return "MYSQL_TYPE_INT24";
    case MYSQL_TYPE_DATE: return "MYSQL_TYPE_DATE";
    case MYSQL_TYPE_TIME: return "MYSQL_TYPE_TIME";
    case MYSQL_TYPE_DATETIME: return "MYSQL_TYPE_DATETIME";
    case MYSQL_TYPE_YEAR: return "MYSQL_TYPE_YEAR";
    case MYSQL_TYPE_NEWDATE: return "MYSQL_TYPE_NEWDATE";
    case MYSQL_TYPE_VARCHAR: return "MYSQL_TYPE_VARCHAR";
    case MYSQL_TYPE_BIT: return "MYSQL_TYPE_BIT";
    case MYSQL_TYPE_NEWDECIMAL: return "MYSQL_TYPE_NEWDECIMAL";
    case MYSQL_TYPE_ENUM: return "MYSQL_TYPE_ENUM";
    case MYSQL_TYPE_SET: return "MYSQL_TYPE_SET";
    case MYSQL_TYPE_TINY_BLOB: return "MYSQL_TYPE_TINY_BLOB";
    case MYSQL_TYPE_MEDIUM_BLOB: return "MYSQL_TYPE_MEDIUM_BLOB";
    case MYSQL_TYPE_LONG_BLOB: return "MYSQL_TYPE_LONG_BLOB";
    case MYSQL_TYPE_BLOB: return "MYSQL_TYPE_BLOB";
    case MYSQL_TYPE_VAR_STRING: return "MYSQL_TYPE_VAR_STRING";
    case MYSQL_TYPE_STRING: return "MYSQL_TYPE_STRING";
    case MYSQL_TYPE_GEOMETRY: return "MYSQL_TYPE_GEOMETRY";
    default:
      return "UNKNOWN";
  }
}

static const char *odbc_type_to_name(SQLSMALLINT type)
{
  switch (type)
  {
    case SQL_CHAR: return "SQL_CHAR";
    case SQL_VARCHAR: return "SQL_VARCHAR";
    case SQL_LONGVARCHAR: return "SQL_LONGVARCHAR";
    case SQL_WCHAR: return "SQL_WCHAR";
    case SQL_WVARCHAR: return "SQL_WVARCHAR";
    case SQL_WLONGVARCHAR: return "SQL_WLONGVARCHAR";
    case SQL_DECIMAL: return "SQL_DECIMAL";
    case SQL_NUMERIC: return "SQL_NUMERIC";
    case SQL_SMALLINT: return "SQL_SMALLINT";
    case SQL_INTEGER: return "SQL_INTEGER";
    case SQL_REAL: return "SQL_REAL";
    case SQL_FLOAT: return "SQL_FLOAT";
    case SQL_DOUBLE: return "SQL_DOUBLE";
    case SQL_BIT: return "SQL_BIT";
    case SQL_TINYINT: return "SQL_TINYINT";
    case SQL_BIGINT: return "SQL_BIGINT";
    case SQL_BINARY: return "SQL_BINARY";
    case SQL_VARBINARY: return "SQL_VARBINARY";
    case SQL_LONGVARBINARY: return "SQL_LONGVARBINARY";
    case SQL_TYPE_DATE: return "SQL_TYPE_DATE";
    case SQL_TYPE_TIME: return "SQL_TYPE_TIME";
    case SQL_TYPE_TIMESTAMP: return "SQL_TYPE_TIMESTAMP";
    case SQL_GUID: return "SQL_GUID";
      //case SQL_TYPE_UTCDATETIME: return "e SQL_TYPE_UTCDATETIME";
      //case SQL_TYPE_UTCTIME: return "e SQL_TYPE_UTCTIME";
    case SQL_INTERVAL_MONTH: return "SQL_INTERVAL_MONTH";
    case SQL_INTERVAL_YEAR: return "SQL_INTERVAL_YEAR";
    case SQL_INTERVAL_YEAR_TO_MONTH: return "SQL_INTERVAL_YEAR_TO_MONTH";
    case SQL_INTERVAL_DAY: return "SQL_INTERVAL_DAY";
    case SQL_INTERVAL_HOUR: return "SQL_INTERVAL_HOUR";
    case SQL_INTERVAL_MINUTE: return "SQL_INTERVAL_MINUTE";
    case SQL_INTERVAL_SECOND: return "SQL_INTERVAL_SECOND";
    case SQL_INTERVAL_DAY_TO_HOUR: return "SQL_INTERVAL_DAY_TO_HOUR";
    case SQL_INTERVAL_DAY_TO_MINUTE: return "SQL_INTERVAL_DAY_TO_MINUTE";
    case SQL_INTERVAL_DAY_TO_SECOND: return "SQL_INTERVAL_DAY_TO_SECOND";
    case SQL_INTERVAL_HOUR_TO_MINUTE: return "SQL_INTERVAL_HOUR_TO_MINUTE";
    case SQL_INTERVAL_HOUR_TO_SECOND: return "SQL_INTERVAL_HOUR_TO_SECOND";
    case SQL_INTERVAL_MINUTE_TO_SECOND: return "SQL_INTERVAL_MINUTE_TO_SECOND";
    default:
      return "UNKNOWN";
  }
}

std::string QueryBuilder::build_query()
{
  std::string q;
  std::string where_cond;
  for (size_t i = 0; i < this->_where.size(); ++i)
  {
    if (i > 0)
      where_cond += " AND ";
    where_cond += base::strfmt("(%s)", this->_where[i].c_str());
  }

  if (this->_schema.empty())
    q = base::strfmt("SELECT %s FROM %s", this->_columns.c_str(), this->_table.c_str());
  else
    q = base::strfmt("SELECT %s FROM %s.%s", this->_columns.c_str(), this->_schema.c_str(), this->_table.c_str());

  if (!where_cond.empty())
    q += base::strfmt(" WHERE %s", + where_cond.c_str());
  if (!this->_orderby.empty())
    q += base::strfmt(" ORDER BY %s", + this->_orderby.c_str());
  if (!this->_limit.empty())
    q += base::strfmt(" LIMIT %s", + this->_limit.c_str());

  return q;
}


std::string ConnectionError::process(SQLRETURN retcode, SQLSMALLINT htype, SQLHANDLE handle)
{
  SQLINTEGER   i = 0;
  SQLINTEGER   native;
  SQLCHAR      state[7];
  SQLCHAR      text[256];
  SQLSMALLINT  len;
  SQLRETURN    ret;
  std::string  output;

  if (retcode == SQL_ERROR || retcode == SQL_SUCCESS_WITH_INFO)
  {
    do
    {
      ret = SQLGetDiagRec(htype, handle, (SQLSMALLINT)++i, state, &native, text,
                          sizeof(text), &len);
      if (SQL_SUCCEEDED(ret))
      {
        char is[32];
        char natives[32];
        snprintf(is, sizeof(is), "%li", (long)i);
        snprintf(natives, sizeof(natives), "%li", (long)native);
        output.append((char*)state).append(":").append(is).append(natives).append(":").append((char*)text).append("\n");
      }
    }
    while (ret == SQL_SUCCESS);
  }
  else
  {
    output = base::strfmt("Error %i", retcode);
  }

  return output;
}



RowBuffer::RowBuffer(boost::shared_ptr<std::vector<ColumnInfo> > columns,
                     boost::function<void (int, const char*, size_t)> send_blob_data,
                     size_t max_packet_size)
: _current_field(0), _send_blob_data(send_blob_data)
{
  for (std::vector<ColumnInfo>::const_iterator col = columns->begin(); col != columns->end(); ++col)
  {
    MYSQL_BIND bind;
    memset(&bind, 0, sizeof(bind));

    bind.buffer_type = col->target_type;
    // Only the PS data types are handled here
    switch (col->target_type)
    {
      case MYSQL_TYPE_TINY:
        bind.buffer_length = sizeof(char);
        break;
      case MYSQL_TYPE_YEAR:
      case MYSQL_TYPE_SHORT:
        bind.buffer_length = sizeof(short);
        break;
      case MYSQL_TYPE_INT24:
      case MYSQL_TYPE_LONG:
        bind.buffer_length = sizeof(int);
        break;
      case MYSQL_TYPE_LONGLONG:
        bind.buffer_length = sizeof(long long int);
        break;
      case MYSQL_TYPE_FLOAT:
        bind.buffer_length = sizeof(float);
        break;
      case MYSQL_TYPE_DOUBLE:
        bind.buffer_length = sizeof(double);
        break;
      case MYSQL_TYPE_TIME:
      case MYSQL_TYPE_DATE:
      case MYSQL_TYPE_NEWDATE:
      case MYSQL_TYPE_DATETIME:
      case MYSQL_TYPE_TIMESTAMP:
        bind.buffer_length = sizeof(MYSQL_TIME);
        break;
      case MYSQL_TYPE_NEWDECIMAL:
      case MYSQL_TYPE_STRING:
      case MYSQL_TYPE_VAR_STRING:
      case MYSQL_TYPE_BIT:
        if (!col->is_long_data)
          bind.buffer_length = (unsigned)col->source_length+1;

        bind.length = (unsigned long*)malloc(sizeof(unsigned long));
        if (!bind.length)
          throw std::runtime_error("Could not allocate memory for row buffer");
        break;
      case MYSQL_TYPE_BLOB:
      case MYSQL_TYPE_GEOMETRY:
        // source_length is not reliable (and returns bogus value for access)
        // so we just use the max_packet_size value
        bind.buffer_length = (unsigned long)std::min(max_packet_size, (size_t)col->source_length + 1);
        bind.length = (unsigned long*)malloc(sizeof(unsigned long));
        if (!bind.length)
          throw std::runtime_error("Could not allocate memory for row buffer");
        break;
      case MYSQL_TYPE_NULL:
        bind.buffer_length = 0;
        break;
      default:
        throw std::logic_error(base::strfmt("Unhandled MySQL type %i for column '%s'", col->target_type, col->target_name.c_str()));
    }
    bind.error = (my_bool*)malloc(sizeof(my_bool));
    if (!bind.error)
      throw std::runtime_error("Could not allocate memory for row buffer");
    if (col->target_type != MYSQL_TYPE_NULL)
    {
      bind.is_null = (my_bool*)malloc(sizeof(my_bool));
      if (!bind.is_null)
      {
        if (bind.length)
        {
          free(bind.length);
          bind.length = NULL;
        }
        throw std::runtime_error("Could not allocate row buffer");
      }
    }
    if (bind.buffer_length > 0)
    {
      bind.buffer = malloc(bind.buffer_length);

      if (!bind.buffer)
      {
        if (bind.error)
        {
          free(bind.error);
          bind.error = NULL;
        }
        {
          free(bind.is_null);
          bind.is_null = NULL;
        }
        throw std::runtime_error(base::strfmt("Could not allocate %lu bytes for row buffer column of %s %s %i",
          bind.buffer_length, col->source_name.c_str(), col->source_type.c_str(), col->target_type));
      }
    }
    else
      bind.buffer = 0;
    bind.is_unsigned = col->is_unsigned;

    push_back(bind);
  }
}

RowBuffer::~RowBuffer()
{
  for (std::vector<MYSQL_BIND>::iterator field = begin(); field != end(); ++field)
  {
    if (field->buffer)
      free(field->buffer);
    if (field->length)
      free(field->length);
    if (field->is_null)
      free(field->is_null);
    if (field->error)
      free(field->error);
  }
}

void RowBuffer::clear()
{
  _current_field = 0;
}

void RowBuffer::prepare_add_string(char* &buffer, size_t &buffer_len, unsigned long *&length)
{
  MYSQL_BIND &bind(at(_current_field));
  if (bind.buffer_type != MYSQL_TYPE_STRING)
    throw std::logic_error(base::strfmt("Type mismatch fetching field %i (should be string, was %s)",
                                        _current_field+1, mysql_field_type_to_name(bind.buffer_type)));

  buffer = (char*)bind.buffer;
  buffer_len = bind.buffer_length;
  length = bind.length;
}

void RowBuffer::prepare_add_float(char* &buffer, size_t &buffer_len)
{
  MYSQL_BIND &bind(at(_current_field));
  if (bind.buffer_type != MYSQL_TYPE_FLOAT)
    throw std::logic_error(base::strfmt("Type mismatch fetching field %i (should be float, was %s)",
                                        _current_field+1, mysql_field_type_to_name(bind.buffer_type)));

  buffer = (char*)bind.buffer;
  buffer_len = bind.buffer_length;
}

void RowBuffer::prepare_add_double(char* &buffer, size_t &buffer_len)
{
  MYSQL_BIND &bind(at(_current_field));
  if (bind.buffer_type != MYSQL_TYPE_DOUBLE)
    throw std::logic_error(base::strfmt("Type mismatch fetching field %i (should be double, was %s)",
                                        _current_field+1, mysql_field_type_to_name(bind.buffer_type)));

  buffer = (char*)bind.buffer;
  buffer_len = bind.buffer_length;
}

void RowBuffer::prepare_add_bigint(char* &buffer, size_t &buffer_len)
{
  MYSQL_BIND &bind(at(_current_field));
  if (bind.buffer_type != MYSQL_TYPE_LONGLONG)
    throw std::logic_error(base::strfmt("Type mismatch fetching field %i (should be bigint, was %s)",
                                        _current_field+1, mysql_field_type_to_name(bind.buffer_type)));

  buffer = (char*)bind.buffer;
  buffer_len = bind.buffer_length;
}

void RowBuffer::prepare_add_long(char* &buffer, size_t &buffer_len)
{
  MYSQL_BIND &bind(at(_current_field));
  if (bind.buffer_type != MYSQL_TYPE_LONG)
    throw std::logic_error(base::strfmt("Type mismatch fetching field %i (should be long, was %s)",
                                        _current_field+1, mysql_field_type_to_name(bind.buffer_type)));

  buffer = (char*)bind.buffer;
  buffer_len = bind.buffer_length;
}

void RowBuffer::prepare_add_short(char* &buffer, size_t &buffer_len)
{
  MYSQL_BIND &bind(at(_current_field));
  if (bind.buffer_type != MYSQL_TYPE_SHORT)
    throw std::logic_error(base::strfmt("Type mismatch fetching field %i (should be short, was %s)",
                                        _current_field+1, mysql_field_type_to_name(bind.buffer_type)));

  buffer = (char*)bind.buffer;
  buffer_len = bind.buffer_length;
}

void RowBuffer::prepare_add_tiny(char* &buffer, size_t &buffer_len)
{
  MYSQL_BIND &bind(at(_current_field));
  if (bind.buffer_type != MYSQL_TYPE_TINY)
    throw std::logic_error(base::strfmt("Type mismatch fetching field %i (should be char, was %s)",
                                        _current_field+1, mysql_field_type_to_name(bind.buffer_type)));

  buffer = (char*)bind.buffer;
  buffer_len = bind.buffer_length;
}

void RowBuffer::prepare_add_time(char* &buffer, size_t &buffer_len)
{
  MYSQL_BIND &bind(at(_current_field));
  if (bind.buffer_type != MYSQL_TYPE_DATETIME &&
      bind.buffer_type != MYSQL_TYPE_TIMESTAMP &&
      bind.buffer_type != MYSQL_TYPE_TIME &&
      bind.buffer_type != MYSQL_TYPE_DATE &&
      bind.buffer_type != MYSQL_TYPE_NEWDATE)
    throw std::logic_error(base::strfmt("Type mismatch fetching field %i (should be time, was %s)",
                                        _current_field+1, mysql_field_type_to_name(bind.buffer_type)));

  buffer = (char*)bind.buffer;
  buffer_len = bind.buffer_length;
}

void RowBuffer::prepare_add_geometry(char* &buffer, size_t &buffer_len, unsigned long *&length)
{
  MYSQL_BIND &bind(at(_current_field));
  if (bind.buffer_type != MYSQL_TYPE_GEOMETRY)
    throw std::logic_error(base::strfmt("Type mismatch fetching field %i (should be geometry, was %s)",
                                        _current_field+1, mysql_field_type_to_name(bind.buffer_type)));

  buffer = (char*)bind.buffer;
  buffer_len = bind.buffer_length;
  length = bind.length;
}

void RowBuffer::finish_field(bool was_null)
{
  *at(_current_field).is_null = was_null;

  _current_field++;
}


bool RowBuffer::check_if_blob()
{
  if (at(_current_field).buffer_type == MYSQL_TYPE_BLOB)
    return true;
  return false;
}


enum enum_field_types RowBuffer::target_type(bool &unsig)
{
  unsig = at(_current_field).is_unsigned != 0;
  return at(_current_field).buffer_type;
}


void RowBuffer::send_blob_data(const char *data, size_t length)
{
  _send_blob_data(_current_field, data, length);
}

// -------------------------------------------------------------------------------------------------

CopyDataSource::CopyDataSource()
: _block_size(0), _max_blob_chunk_size(64*1024), _max_parameter_size(0), _abort_on_oversized_blobs(false),
  _get_field_lengths_from_target(false)
{
}

void CopyDataSource::set_max_blob_chunk_size(size_t size)
{
  _max_blob_chunk_size = size;
}

void CopyDataSource::set_block_size(int bsize)
{
  _block_size = bsize;
}

/*
* get_where_condition : creates where condition for --resume parameter.
* Parameters:
* - pk_columns : vector of PK columns
* - last_pk : vector of last PK value for each of PK column
*
* Remarks : For these columns and values ​​creates a condition to the WHERE clause
*           to skip the rows that have already been copied.
*           For one columns will produce:
*             col1 > val1
*           For two columns:
*             col1 > val1 or (col1 = val1 and col2 > val2)
*           For three columns:
*             col1 > val1 or (col1 = val1 and col2 > val2) or (col1 = val1 and col2 = val2 and col3 > val3)
*           And so on...
*/
std::string CopyDataSource::get_where_condition(const std::vector<std::string> &pk_columns, const std::vector<std::string> &last_pk)
{
  std::string where_cond;
  bool add_and = false;

  for(size_t i = 0; i < pk_columns.size(); ++i)
  {
    add_and = false;
    for(unsigned int j = 0; j < i; ++j)
    {
      add_and = true;
      if(j==0)
        where_cond += " or (";
      else
        where_cond += " and ";
      where_cond += base::strfmt("%s = '%s'", pk_columns[j].c_str(), base::escape_sql_string(last_pk[j]).c_str());
    }
    if(add_and)
      where_cond += " and ";
    where_cond += base::strfmt("%s > '%s'", pk_columns[i].c_str(), base::escape_sql_string(last_pk[i]).c_str());
    if(add_and)
      where_cond += ")";
  }

    return where_cond;
}

// -------------------------------------------------------------------------------------------------

SQLSMALLINT ODBCCopyDataSource::odbc_type_to_c_type(SQLSMALLINT type, bool is_unsigned)
{
  switch (type)
  {
    case SQL_CHAR:
    case SQL_VARCHAR:
    case SQL_LONGVARCHAR:
      return SQL_C_CHAR;
    case SQL_WCHAR:
    case SQL_WVARCHAR:
    case SQL_WLONGVARCHAR:
      // FreeTDS converts the data to utf8
      return _force_utf8_input ? SQL_C_CHAR : SQL_C_WCHAR;
    case SQL_DECIMAL:
    case SQL_NUMERIC:
      return SQL_C_CHAR;
    case SQL_SMALLINT:
      return is_unsigned ? SQL_C_USHORT : SQL_C_SSHORT;
    case SQL_INTEGER:
      return is_unsigned ? SQL_C_ULONG : SQL_C_SLONG;
    case SQL_REAL:
      return SQL_C_FLOAT;
    case SQL_FLOAT:
      return SQL_C_FLOAT;
    case SQL_DOUBLE:
      return SQL_C_DOUBLE;
    case SQL_BIT:
      return SQL_C_BIT;
    case SQL_TINYINT:
      return is_unsigned ? SQL_C_UTINYINT : SQL_C_STINYINT;
    case SQL_BIGINT:
      return is_unsigned ? SQL_C_UBIGINT : SQL_C_SBIGINT;
    case SQL_BINARY:
    case SQL_VARBINARY:
    case SQL_LONGVARBINARY:
      return SQL_C_BINARY;
    case SQL_TYPE_DATE:
      return SQL_C_DATE;
    case SQL_TYPE_TIME:
      return SQL_C_TIME;
    case SQL_TYPE_TIMESTAMP:
      return SQL_C_TIMESTAMP;
    case SQL_GUID:
      return SQL_C_CHAR;
      //case SQL_TYPE_UTCDATETIME:
      //case SQL_TYPE_UTCTIME:
    case SQL_INTERVAL_MONTH:
    case SQL_INTERVAL_YEAR:
    case SQL_INTERVAL_YEAR_TO_MONTH:
    case SQL_INTERVAL_DAY:
    case SQL_INTERVAL_HOUR:
    case SQL_INTERVAL_MINUTE:
    case SQL_INTERVAL_SECOND:
    case SQL_INTERVAL_DAY_TO_HOUR:
    case SQL_INTERVAL_DAY_TO_MINUTE:
    case SQL_INTERVAL_DAY_TO_SECOND:
    case SQL_INTERVAL_HOUR_TO_MINUTE:
    case SQL_INTERVAL_HOUR_TO_SECOND:
    case SQL_INTERVAL_MINUTE_TO_SECOND:
    default:
      // convert to string and make the mysql side also auto-convert
      throw std::runtime_error(base::strfmt("Unhandled type %i", type));
      //// default just convert to string
      //return SQL_C_CHAR;
  }
}


ODBCCopyDataSource::ODBCCopyDataSource(SQLHENV env,
                                       const std::string &connstring,
                                       const std::string &password,
                                       bool force_utf8_input,
                                       const std::string &source_rdbms_type)
: _connstring(connstring), _stmt_ok(false), _source_rdbms_type(source_rdbms_type)
{
  _blob_buffer = NULL;
  _utf8_blob_buffer = NULL;
  _force_utf8_input = force_utf8_input;

  _iconv = iconv_open("UTF-8", "UCS-2LE");
  if (_iconv == (iconv_t)-1)
    throw std::runtime_error("Could not create UCS-2 to UTF-8 conversion context");

  SQLAllocHandle(SQL_HANDLE_DBC, env, &_dbc);

  // 5s timeout
  SQLSetConnectAttr(_dbc, SQL_LOGIN_TIMEOUT, (SQLPOINTER)5, 0);

  bool has_pwd = _connstring.find("PWD=") != std::string::npos;
  if (!has_pwd)
    _connstring.append(";PWD=");
  log_info("Opening ODBC connection to [%s] '%s'\n", _source_rdbms_type.c_str(), (_connstring + (has_pwd ? "" : "XXX")).c_str());
  //log_debug3("connstring %s%s\n", _connstring.c_str(), password.c_str());
  SQLRETURN ret = SQLDriverConnect(_dbc, NULL,
                                   (SQLCHAR*)(_connstring + (has_pwd ? "" : password)).c_str(), SQL_NTS,
                                   NULL, 0, NULL, SQL_DRIVER_COMPLETE);
  if (!SQL_SUCCEEDED(ret))
  {
    log_error("ODBC connection to '%s' failed\n", _connstring.c_str());
    throw ConnectionError("SQLDriverConnect", ret, SQL_HANDLE_DBC, _dbc);
  }
  else
    log_info("ODBC connection to '%s' opened\n", _connstring.c_str());
}


ODBCCopyDataSource::~ODBCCopyDataSource()
{
  if (_blob_buffer)
    free(_blob_buffer);
  if (_utf8_blob_buffer)
    free(_utf8_blob_buffer);
  iconv_close(_iconv);

  if (_stmt_ok)
    SQLFreeHandle(SQL_HANDLE_ENV, _stmt);
  SQLFreeHandle(SQL_HANDLE_DBC, _dbc);
}


void ODBCCopyDataSource::ucs2_to_utf8(char *inbuf, size_t inbuf_len, char *&utf8buf, size_t &utf8buf_len)
{
  size_t converted;
  char *outbuf = (char*)_utf8_blob_buffer;
  size_t outbuf_len = _max_blob_chunk_size;

  // outside Windows, SQLWCHAR is wchar_t, which is not always 2 bytes
  if (sizeof(SQLWCHAR) > sizeof(unsigned short))
  {
    SQLWCHAR *in = (SQLWCHAR*)inbuf;
    unsigned short *out = (unsigned short*)inbuf;
    for (size_t i = 0; i < inbuf_len/sizeof(SQLWCHAR); i++)
      out[i] = in[i];
    inbuf_len = (inbuf_len/sizeof(SQLWCHAR)) * sizeof(unsigned short);
  }
  else if (sizeof(SQLWCHAR) < sizeof(unsigned short))
    throw std::logic_error("Unexpected architecture. sizeof(SQLWCHAR) < sizeof(unsigned short)!");

  //log_debug3("Convert string with %i chars to buffer size %i\n", inbuf_len, outbuf_len);

  // convert data from UCS-2 to utf-8
#ifdef _WIN32
  converted = iconv(_iconv,
                    (const char**)&inbuf, &inbuf_len,
                    (char**)&outbuf, &outbuf_len);
#else
  converted = iconv(_iconv,
                    (char**)&inbuf, &inbuf_len,
                    (char**)&outbuf, &outbuf_len);
#endif
  if (converted == (size_t)-1)
    throw std::logic_error(base::strfmt("Error during charset conversion of wstring: %s", strerror(errno)));

  if (inbuf_len > 0)
    log_warning("%li characters could not be converted to UTF-8 during copy\n",
                (long)inbuf_len);//, (*_columns)[column-1].source_name.c_str());

  utf8buf = _utf8_blob_buffer;
  utf8buf_len = _max_blob_chunk_size - outbuf_len;
}


SQLRETURN ODBCCopyDataSource::get_wchar_buffer_data(RowBuffer &rowbuffer, int column)
{
  unsigned long *out_length;
  SQLRETURN ret;
  SQLLEN len_or_indicator;
  char* out_buffer;
  size_t out_buffer_len;
  SQLWCHAR tmpbuf[64*1024];

  ret = SQLGetData(_stmt, column, _column_types[column-1], tmpbuf, sizeof(tmpbuf), &len_or_indicator);
  // check if the data fits
  //if (len_or_indicator > out_buffer_len)
  //  ;
  rowbuffer.prepare_add_string(out_buffer, out_buffer_len, out_length);
  if (SQL_SUCCEEDED(ret))
  {
    if (len_or_indicator == SQL_NO_TOTAL)
      throw std::runtime_error(base::strfmt("Got SQL_NO_TOTAL for string size during copy of column %i", column));

    if (len_or_indicator != SQL_NULL_DATA)
    {
      char *inbuf = (char*)tmpbuf;
      size_t inbuf_len = len_or_indicator;
      char *outbuf = (char*)out_buffer;
      size_t outbuf_len = out_buffer_len;
      size_t converted;

      if (sizeof(SQLWCHAR) > sizeof(unsigned short))
      {
        SQLWCHAR *in = (SQLWCHAR*)inbuf;
        unsigned short *out = (unsigned short*)inbuf;
        for (size_t i = 0; i < inbuf_len/sizeof(SQLWCHAR); i++)
          out[i] = in[i];
        inbuf_len = (inbuf_len/sizeof(SQLWCHAR)) * sizeof(unsigned short);
      }
      else if (sizeof(SQLWCHAR) < sizeof(unsigned short))
        throw std::logic_error("Unexpected architecture. sizeof(SQLWCHAR) < sizeof(unsigned short)!");

      //log_debug3("Convert string with %i chars to buffer size %i\n", inbuf_len, outbuf_len);

      // convert data from UCS-2 to utf-8
#ifdef _WIN32
      converted = iconv(_iconv,
                        (const char**)&inbuf, &inbuf_len,
                        (char**)&outbuf, &outbuf_len);
#else
      converted = iconv(_iconv,
                        (char**)&inbuf, &inbuf_len,
                        (char**)&outbuf, &outbuf_len);
#endif
      if (converted == (size_t)-1)
        throw std::logic_error(base::strfmt("Error during charset conversion of wstring: %s", strerror(errno)));

      if (inbuf_len > 0)
        log_warning("%li characters could not be converted to UTF-8 from column %s during copy\n",
                    (long)inbuf_len, (*_columns)[column-1].source_name.c_str());

      *out_length = (unsigned long)(out_buffer_len - outbuf_len);
    }
    rowbuffer.finish_field(len_or_indicator == SQL_NULL_DATA);
  }
  return ret;
}

SQLRETURN ODBCCopyDataSource::get_date_time_data(RowBuffer &rowbuffer, int column, int type)
{
  SQLRETURN ret;
  char* out_buffer;
  SQLLEN len_or_indicator;
  size_t out_buffer_len;
  char out_date[32];

  rowbuffer.prepare_add_time(out_buffer, out_buffer_len);
  ret = SQLGetData(_stmt, column, SQL_C_CHAR, &out_date, sizeof(out_date), &len_or_indicator);
  if (SQL_SUCCEEDED(ret))
  {
    //When driver cannot determine the number of bytes of long data
    //still available to return in an output buffer it return SQL_NO_TOTAL
    if (len_or_indicator == SQL_NO_TOTAL)
      throw std::runtime_error(base::strfmt("Got SQL_NO_TOTAL for string size during copy of column %i", column));

    if (len_or_indicator != SQL_NULL_DATA)
      BaseConverter::convert_date_time(out_date, (MYSQL_TIME*)out_buffer, type);
    else
      ((MYSQL_TIME*)out_buffer)->time_type = MYSQL_TIMESTAMP_NONE;

    rowbuffer.finish_field(len_or_indicator == SQL_NULL_DATA);
  }

  return ret;
}

SQLRETURN ODBCCopyDataSource::get_char_buffer_data(RowBuffer &rowbuffer, int column)
{
  unsigned long *out_length;
  SQLRETURN ret;
  SQLLEN len_or_indicator;
  char* out_buffer;
  size_t out_buffer_len;

  rowbuffer.prepare_add_string(out_buffer, out_buffer_len, out_length);
  ret = SQLGetData(_stmt, column, _column_types[column-1], out_buffer, out_buffer_len, &len_or_indicator);
  // check if the data fits
  //if (len_or_indicator > out_buffer_len)
  //  ;
  if (len_or_indicator == SQL_NO_TOTAL)
    throw std::runtime_error(base::strfmt("Got SQL_NO_TOTAL for string size during copy of column %i", column));

  if (SQL_SUCCEEDED(ret))
  {
    if (len_or_indicator != SQL_NULL_DATA)
      *out_length = (unsigned long)len_or_indicator;
    rowbuffer.finish_field(len_or_indicator == SQL_NULL_DATA);
  }
  return ret;
}


SQLRETURN ODBCCopyDataSource::get_geometry_buffer_data(RowBuffer &rowbuffer, int column)
{
  unsigned long *out_length;
  SQLRETURN ret;
  SQLLEN len_or_indicator;
  char* out_buffer;
  size_t out_buffer_len;
  SQLWCHAR tmpbuf[64*1024];
  ret = SQLGetData(_stmt, column, SQL_C_WCHAR, tmpbuf, sizeof(tmpbuf), &len_or_indicator);

  rowbuffer.prepare_add_geometry(out_buffer, out_buffer_len, out_length);
  if (SQL_SUCCEEDED(ret))
  {
    if (len_or_indicator == SQL_NO_TOTAL)
      throw std::runtime_error(base::strfmt("Got SQL_NO_TOTAL for string size during copy of column %i", column));

    if (len_or_indicator != SQL_NULL_DATA)
    {
      char *inbuf = (char*)tmpbuf;
      size_t inbuf_len = len_or_indicator;
      char *outbuf = out_buffer;
      size_t outbuf_len = out_buffer_len;
      size_t converted;

      if (sizeof(SQLWCHAR) > sizeof(unsigned short))
      {
        SQLWCHAR *in = (SQLWCHAR*)inbuf;
        unsigned short *out = (unsigned short*)inbuf;
        for (size_t i = 0; i < inbuf_len/sizeof(SQLWCHAR); i++)
          out[i] = in[i];
        inbuf_len = (inbuf_len/sizeof(SQLWCHAR)) * sizeof(unsigned short);
      }
      else if (sizeof(SQLWCHAR) < sizeof(unsigned short))
        throw std::logic_error("Unexpected architecture. sizeof(SQLWCHAR) < sizeof(unsigned short)!");

      // convert data from UCS-2 to utf-8
#ifdef _WIN32
      converted = iconv(_iconv,
                        (const char**)&inbuf, &inbuf_len,
                        (char**)&outbuf, &outbuf_len);
#else
      converted = iconv(_iconv,
                        (char**)&inbuf, &inbuf_len,
                        (char**)&outbuf, &outbuf_len);
#endif
      if (converted == (size_t)-1)
        throw std::logic_error(base::strfmt("Error during charset conversion of wstring: %s", strerror(errno)));

      if (inbuf_len > 0)
        log_warning("%lu characters could not be converted to UTF-8 from column %s during copy\n",
                    (unsigned long)inbuf_len, (*_columns)[column-1].source_name.c_str());

      *out_length = (unsigned long)(out_buffer_len - outbuf_len);
    }
    rowbuffer.finish_field(len_or_indicator == SQL_NULL_DATA);
  }
  return ret;
}


size_t ODBCCopyDataSource::count_rows(const std::string &schema, const std::string &table, const std::vector<std::string> &pk_columns,
                                      const CopySpec &spec, const std::vector<std::string> &last_pkeys)
{
  SQLHSTMT stmt;
  SQLRETURN ret;
  if (!SQL_SUCCEEDED(ret = SQLAllocHandle(SQL_HANDLE_STMT, _dbc, &stmt)))
    throw ConnectionError("SQLAllocHandle", ret, SQL_HANDLE_DBC, _dbc);

  std::string q;

  switch (spec.type)
  {
    case CopyAll:
      if (spec.resume && last_pkeys.size())
        q = base::strfmt("SELECT count(*) FROM %s.%s WHERE %s", schema.c_str(), table.c_str(), get_where_condition(pk_columns, last_pkeys).c_str());
      else
        q = base::strfmt("SELECT count(*) FROM %s.%s", schema.c_str(), table.c_str());
      break;
    case CopyRange:
    {
      std::string start_expr, end_expr;
      if (spec.range_end < 0)
        end_expr = "";
      else
        end_expr = base::strfmt("%s <= %lli", spec.range_key.c_str(), spec.range_end);
      start_expr = base::strfmt("%s >= %lli", spec.range_key.c_str(), spec.range_start);
      if (!end_expr.empty())
        q = base::strfmt("SELECT count(*) FROM %s.%s WHERE %s AND %s", schema.c_str(), table.c_str(), start_expr.c_str(), end_expr.c_str());
      else
        q = base::strfmt("SELECT count(*) FROM %s.%s WHERE %s", schema.c_str(), table.c_str(), start_expr.c_str());
      break;
    }
    case CopyCount:
    {
      if (spec.resume && last_pkeys.size())
        q = base::strfmt("SELECT count(*) FROM %s.%s WHERE %s", schema.c_str(), table.c_str(), get_where_condition(pk_columns, last_pkeys).c_str());
      else
        q = base::strfmt("SELECT count(*) FROM %s.%s", schema.c_str(), table.c_str());
      break;
    }
    case CopyWhere:
    {
      q = base::strfmt("SELECT count(*) FROM %s.%s WHERE %s", schema.c_str(), table.c_str(), spec.where_expression.c_str());
      break;
    }
  }
  log_debug("Executing query: %s\n", q.c_str());
  if (!SQL_SUCCEEDED(ret = SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS)))
    throw ConnectionError("SQLExecDirect("+q+")", ret, SQL_HANDLE_STMT, stmt);

  long long count = 0;
  if (SQL_SUCCEEDED(SQLFetch(stmt)))
    SQLGetData(stmt, 1, SQL_C_ULONG, &count, sizeof(count), NULL);

  SQLFreeHandle(SQL_HANDLE_STMT, stmt);

  if ((spec.type == CopyAll || spec.type == CopyWhere) && spec.max_count > 0 && spec.max_count < count)
    count = spec.max_count;

  return count;
}


boost::shared_ptr<std::vector<ColumnInfo> > ODBCCopyDataSource::begin_select_table(const std::string &schema, const std::string &table,
                                                                                   const std::vector<std::string> &pk_columns,
                                                                                   const std::string &select_expression,
                                                                                   const CopySpec &spec, const std::vector<std::string> &last_pkeys)
{
  boost::shared_ptr<std::vector<ColumnInfo> > columns(new std::vector<ColumnInfo>());
  _columns = columns;
  _schema_name = schema;
  _table_name = table;

  _stmt_ok = true;
  SQLRETURN ret;
  if (!SQL_SUCCEEDED(ret = SQLAllocHandle(SQL_HANDLE_STMT, _dbc, &_stmt)))
    throw ConnectionError("SQLAllocHandle", ret, SQL_HANDLE_DBC, _dbc);

  std::string q;

  QueryBuilder select_query;
  select_query.select_columns(select_expression);
  select_query.select_from_table(table, schema);
  select_query.add_orderby(boost::algorithm::join(pk_columns, ", "));

  if (spec.resume && last_pkeys.size())
    select_query.add_where(get_where_condition(pk_columns, last_pkeys));
  if (spec.type == CopyRange)
  {
    select_query.add_where(base::strfmt("%s >= %lli", spec.range_key.c_str(), spec.range_start));
    if (spec.range_end >= 0)
      select_query.add_where(base::strfmt("%s <= %lli", spec.range_key.c_str(), spec.range_end));
  }
  if (spec.type == CopyWhere)
    select_query.add_where(spec.where_expression);

  q = select_query.build_query();

  log_debug("Executing query: %s\n", q.c_str());
  if (!SQL_SUCCEEDED(ret = SQLExecDirect(_stmt, (SQLCHAR*)q.c_str(), SQL_NTS)))
    throw ConnectionError("SQLExecDirect("+q+")", ret, SQL_HANDLE_STMT, _stmt);

  SQLSMALLINT column_count;
  if (!SQL_SUCCEEDED(ret = SQLNumResultCols(_stmt, &column_count)))
    throw ConnectionError("SQLNumResultCols", ret, SQL_HANDLE_STMT, _stmt);
  _column_count = column_count;

  log_debug2("Columns from source table %s.%s (%i):\n", schema.c_str(), table.c_str(), column_count);
  for (int i = 1; i <= column_count; i++)
  {
    ColumnInfo info;
    SQLCHAR columnName[256];
    SQLSMALLINT nameLength;
    SQLSMALLINT dataType;
    SQLULEN columnSize;
    SQLSMALLINT decimalDigits;
    SQLSMALLINT nullablePtr;

    info.is_long_data = false;
    info.is_unsigned = false;

    if (SQL_SUCCEEDED(SQLDescribeCol(_stmt, i,
                                     columnName, sizeof(columnName), &nameLength,
                                     &dataType, &columnSize, &decimalDigits, &nullablePtr)))
    {
      bool is_unsigned = false;
      SQLCHAR typeName[256];
      SQLSMALLINT typeNameLength;

      SQLLEN attrvalue;
      if (SQL_SUCCEEDED(SQLColAttribute(_stmt, i,
                                        SQL_DESC_UNSIGNED,
                                        NULL, 0, NULL,
                                        &attrvalue)))
        is_unsigned = attrvalue == SQL_TRUE;

      if (SQL_SUCCEEDED(SQLColAttribute(_stmt, i,
                                        SQL_DESC_TYPE_NAME,
                                        typeName, sizeof(typeName), &typeNameLength,
                                        NULL)))
        info.source_type = std::string((char*)typeName, typeNameLength);

      info.source_name = (char*)columnName;
      info.source_length = columnSize;
      if (dataType == SQL_WCHAR || dataType == SQL_WLONGVARCHAR || dataType == SQL_WVARCHAR)
        info.source_length *= 4;

      if (dataType == SQL_LONGVARBINARY || dataType == SQL_LONGVARCHAR
          || dataType == SQL_WLONGVARCHAR)
        info.is_long_data = true;

      //TODO find out the MySQL equivalent of the type and fill here.. if its not used, then remove
      //info.mapped_source_type = dataType;
      info.is_unsigned = is_unsigned;

      log_debug2("%i - %s: %s %s (type=%s, len=%lli%s)\n", i, columnName, typeName, is_unsigned ? "UNSIGNED" : "",
                 odbc_type_to_name(dataType), info.source_length, info.is_long_data ? ", long_data" : "");

      columns->push_back(info);

      _column_types.push_back(odbc_type_to_c_type(dataType, is_unsigned));
    }
    else
      throw ConnectionError("SQLDescribeCol", ret, SQL_HANDLE_STMT, _stmt);
  }

  return columns;
}


void ODBCCopyDataSource::end_select_table()
{
  SQLFreeHandle(SQL_HANDLE_STMT, _stmt);
  _column_types.clear();
  _columns.reset();
  _stmt_ok = false;
}

bool ODBCCopyDataSource::fetch_row(RowBuffer &rowbuffer)
{
  if (SQL_SUCCEEDED(SQLFetch(_stmt)))
  {
    for (int i = 1; i <= _column_count; i++)
    {
      SQLRETURN ret = 0;
      SQLLEN len_or_indicator;
      char* out_buffer;
      size_t out_buffer_len;

      //log_debug3("Copy %i from type %i to %i\n", i,
//                 _column_types[i-1],
//                 rowbuffer[i-1].buffer_type);

      // if this column is a blob, handle it as such
      if (rowbuffer.check_if_blob() || (*_columns)[i-1].is_long_data)
      {
        if (!_blob_buffer)
        {
          _blob_buffer = (char*)malloc(_max_blob_chunk_size);
          if (!_blob_buffer)
            throw std::runtime_error(base::strfmt("malloc(%lu) failed for blob transfer buffer", (unsigned long)_max_blob_chunk_size));

          _utf8_blob_buffer = (char*)malloc(_max_blob_chunk_size);
          if (!_utf8_blob_buffer)
            throw std::runtime_error(base::strfmt("malloc(%lu) failed for blob transfer buffer", (unsigned long)_max_blob_chunk_size));
        }

        ret = SQLGetData(_stmt, i,_column_types[i-1], _blob_buffer, _max_blob_chunk_size, &len_or_indicator);

        // Saves the column length, at the first call it is the total column size
        if (len_or_indicator > _max_parameter_size)
        {
          if (_abort_on_oversized_blobs)
            throw std::runtime_error(base::strfmt("oversized blob found in table %s.%s, size: %lli",
                                                  _schema_name.c_str(), _table_name.c_str(),
                                                  (long long)len_or_indicator));
          else
          {
            printf("oversized blob found in table %s.%s, size: %lli",
                   _schema_name.c_str(), _table_name.c_str(),
                   (long long)len_or_indicator);
            rowbuffer.finish_field(true);
            continue;
          }
        }
        else
        {
          while (ret == SQL_SUCCESS_WITH_INFO)
          {
            SQLUSMALLINT  i = 0;
            SQLINTEGER    native;
            SQLCHAR       state[7];
            SQLCHAR       text[256];
            SQLSMALLINT   len;

            ret = SQLGetDiagRec(SQL_HANDLE_STMT, _stmt, ++i, state, &native, text,
                                sizeof(text), &len);

            // This should be done ONLY if no bulk updates
            // are being used
            if (native == 1014 && !_use_bulk_inserts)
              rowbuffer.send_blob_data(_blob_buffer, len_or_indicator);

            // Unrecognized characters were changed to ?? but data was read
            else if (native == 2403)
            {
              log_warning("[%s - %ld]: %s\n", state, (long int)native, text);
              break;
            }

            ret = SQLGetData(_stmt, i, _column_types[i-1], _blob_buffer, _max_blob_chunk_size, &len_or_indicator);
          }

          if (ret == SQL_SUCCESS)
          {
            bool was_null = len_or_indicator == SQL_NULL_DATA;

            if(!was_null)
            {
              char *utf8_data;
              char *final_data = _blob_buffer;
              size_t final_length = len_or_indicator;

              // Convers the data to utf8 if needed
              if (_column_types[i-1] == SQL_C_WCHAR)
              {
                //XXX take care of case where the utf8 data is bigger than _max_blob_chunk_size
                try
                {
                  ucs2_to_utf8(_blob_buffer, len_or_indicator, utf8_data, final_length);
                }
                catch (std::logic_error &)
                {
                  const std::string msg = base::strfmt("ERROR: Could not successfully convert UCS-2 string to UTF-8 "
                      "in table %s.%s (column %s). Original string: \"%s\"",
                      _schema_name.c_str(), _table_name.c_str(), (*_columns)[i-1].source_name.c_str(), std::string(_blob_buffer, len_or_indicator).c_str()
                      );
                  log_error("%s", msg.c_str());
                  throw std::invalid_argument(msg);
                }
                final_data = utf8_data;
              }

              if (_use_bulk_inserts)
              {
                if (rowbuffer[i-1].buffer_length)
                  free(rowbuffer[i-1].buffer);

                *rowbuffer[i - 1].length = (unsigned long)final_length;
                rowbuffer[i - 1].buffer_length = (unsigned long)final_length;
                rowbuffer[i-1].buffer = malloc(final_length);

                memcpy(rowbuffer[i-1].buffer, final_data, final_length);
              }
              else
                rowbuffer.send_blob_data(final_data, final_length);
            }

            rowbuffer.finish_field(was_null);
          }
          else
          {
            rowbuffer.finish_field(true);
            throw ConnectionError("SQLGetData", ret, SQL_HANDLE_STMT, _stmt);
          }
          continue;
        }
      }

      switch (_column_types[i-1])
      {
        case SQL_C_BIT:
          rowbuffer.prepare_add_tiny(out_buffer, out_buffer_len);
          ret = SQLGetData(_stmt, i, SQL_C_STINYINT, out_buffer, out_buffer_len, &len_or_indicator);
          if (SQL_SUCCEEDED(ret))
            rowbuffer.finish_field(len_or_indicator == SQL_NULL_DATA);
          break;
        case SQL_C_FLOAT:
        case SQL_C_DOUBLE:
          if (rowbuffer[i-1].buffer_type == MYSQL_TYPE_FLOAT)
          {
            rowbuffer.prepare_add_float(out_buffer, out_buffer_len);
            ret = SQLGetData(_stmt, i, SQL_C_FLOAT, out_buffer, out_buffer_len, &len_or_indicator);
            if (SQL_SUCCEEDED(ret))
              rowbuffer.finish_field(len_or_indicator == SQL_NULL_DATA);
          }
          else
          {
            rowbuffer.prepare_add_double(out_buffer, out_buffer_len);
            ret = SQLGetData(_stmt, i, SQL_C_DOUBLE, out_buffer, out_buffer_len, &len_or_indicator);
            if (SQL_SUCCEEDED(ret))
              rowbuffer.finish_field(len_or_indicator == SQL_NULL_DATA);
          }
          break;
        case SQL_C_DATE:
          ret = get_date_time_data(rowbuffer, i, MYSQL_TYPE_DATE);
          break;
        case SQL_C_TIME:
          ret = get_date_time_data(rowbuffer, i, MYSQL_TYPE_TIME);
          break;
        case SQL_C_TIMESTAMP:
          ret = get_date_time_data(rowbuffer, i, MYSQL_TYPE_TIMESTAMP);
          break;
        case SQL_C_UBIGINT:
        case SQL_C_SBIGINT:
          rowbuffer.prepare_add_bigint(out_buffer, out_buffer_len);
          ret = SQLGetData(_stmt, i, _column_types[i-1], out_buffer, out_buffer_len, &len_or_indicator);
          if (SQL_SUCCEEDED(ret))
            rowbuffer.finish_field(len_or_indicator == SQL_NULL_DATA);
          break;
        case SQL_C_ULONG:
        case SQL_C_SLONG:
          {
            long tmp_buffer;
            bool unsig;
            enum enum_field_types target_type;
            ret = SQLGetData(_stmt, i, _column_types[i-1], &tmp_buffer, sizeof(tmp_buffer), &len_or_indicator);
            if (SQL_SUCCEEDED(ret))
            {
              switch ((target_type = rowbuffer.target_type(unsig)))
              {
              case MYSQL_TYPE_SHORT:
                rowbuffer.prepare_add_short(out_buffer, out_buffer_len);
                if ((unsig && (tmp_buffer < 0 || tmp_buffer > UINT16_MAX)) || (!unsig && (tmp_buffer > INT16_MAX || tmp_buffer < INT16_MIN)))
                  throw std::logic_error(base::strfmt("Range error fetching field %i (value %li, target is %s)",
                                        i, tmp_buffer, mysql_field_type_to_name(target_type)));
                *(short*)out_buffer = (short)tmp_buffer;
                break;
              case MYSQL_TYPE_TINY:
                rowbuffer.prepare_add_tiny(out_buffer, out_buffer_len);
                if ((unsig && (tmp_buffer < 0 || tmp_buffer > UINT8_MAX)) || (!unsig && (tmp_buffer > INT8_MAX || tmp_buffer < INT8_MIN)))
                  throw std::logic_error(base::strfmt("Range error fetching field %i (value %li, target is %s)",
                                        i, tmp_buffer, mysql_field_type_to_name(target_type)));
                *(char*)out_buffer = (char)tmp_buffer;
                break;
              default:
                rowbuffer.prepare_add_long(out_buffer, out_buffer_len);
                *(long*)out_buffer = tmp_buffer;
                break;
              }
              rowbuffer.finish_field(len_or_indicator == SQL_NULL_DATA);}
            }
            break;
        case SQL_C_USHORT:
        case SQL_C_SSHORT:
          rowbuffer.prepare_add_short(out_buffer, out_buffer_len);
          ret = SQLGetData(_stmt, i, _column_types[i-1], out_buffer, out_buffer_len, &len_or_indicator);
          if (SQL_SUCCEEDED(ret))
            rowbuffer.finish_field(len_or_indicator == SQL_NULL_DATA);
          break;
        case SQL_C_UTINYINT:
        case SQL_C_STINYINT:
          rowbuffer.prepare_add_tiny(out_buffer, out_buffer_len);
          ret = SQLGetData(_stmt, i, _column_types[i-1], out_buffer, out_buffer_len, &len_or_indicator);
          if (SQL_SUCCEEDED(ret))
            rowbuffer.finish_field(len_or_indicator == SQL_NULL_DATA);
          break;
        case SQL_C_WCHAR:
        case SQL_C_CHAR:
          switch (rowbuffer[i-1].buffer_type)
          {
          case MYSQL_TYPE_TIME:
          case MYSQL_TYPE_DATE:
          case MYSQL_TYPE_DATETIME:
          case MYSQL_TYPE_NEWDATE:
            ret = get_date_time_data(rowbuffer, i, rowbuffer[i-1].buffer_type);
            break;
          case MYSQL_TYPE_GEOMETRY:
            ret = get_geometry_buffer_data(rowbuffer, i);
            break;
          default:
            if (_column_types[i-1] == SQL_C_WCHAR)
              ret = get_wchar_buffer_data(rowbuffer, i);
            else
              ret = get_char_buffer_data(rowbuffer, i);
            break;
          }
          break;
        case SQL_C_BINARY:
          {
            bool was_null = true;

            // During the migration process some non standard data types are migrated as strings
            // Those will come as SQL_C_BINARY but will be migrated as NULL for now
            if (rowbuffer[i-1].buffer_type != MYSQL_TYPE_STRING)
            {
              was_null = false;
              ret = get_char_buffer_data(rowbuffer, i);
            }

            rowbuffer.finish_field(was_null);
          }
          break;

        default:
          throw std::logic_error(base::strfmt("Unhandled type %i", _column_types[i-1]));
      }
      if (!SQL_SUCCEEDED(ret))
      {
        rowbuffer.finish_field(true);
        throw ConnectionError("SQLGetData", ret, SQL_HANDLE_STMT, _stmt);
      }
    }
    return true;
  }
  return false;
}


MySQLCopyDataSource::MySQLCopyDataSource(const std::string &hostname, int port,
                    const std::string &username, const std::string &password,
                    const std::string &socket, bool use_cleartext_plugin)
  : _select_stmt(NULL), _has_long_data(false)
{
  std::string host = hostname;
  mysql_init(&_mysql);

  if (port > 0)
  {
    // Forces usage of TCP connection if indicated on the connection
    // settings (a port is specified)
    int proto = MYSQL_PROTOCOL_TCP;
    mysql_options(&_mysql, MYSQL_OPT_PROTOCOL, &proto);

    log_info("Connecting to MySQL server at %s:%i with user %s\n",
           hostname.c_str(), port, username.c_str());
  }
  else
  {
    // Socket file/Named pipe connections

    #if defined(WIN32)
    // Default local host connection in windows are done through shared memory
    // using "." forces using named pipe
    host=".";
    #else
    // Default local host connections in Linux are done through socket files
    host="localhost";
    #endif

    log_info("Connecting to MySQL server using socket %s with user %s\n",
           socket.c_str(), username.c_str());
  }


#if defined(MYSQL_VERSION_MAJOR) && defined(MYSQL_VERSION_MINOR) && defined(MYSQL_VERSION_PATCH)
#if MYSQL_CHECK_VERSION(5,5,27)
  my_bool use_cleartext = use_cleartext_plugin;
  mysql_options(&_mysql, MYSQL_ENABLE_CLEARTEXT_PLUGIN, &use_cleartext);
#else
  if (use_cleartext_plugin)
    log_warning("Trying to use the ClearText plugin, but it's not supported by libmysqlclient\n");
#endif
#endif

  if (!mysql_real_connect(&_mysql, host.c_str(), username.c_str(), password.c_str(), NULL, port, socket.c_str(),
                          CLIENT_COMPRESS))
  {
    log_error("Failed opening connection to MySQL: %s\n", mysql_error(&_mysql));
    throw ConnectionError("mysql_real_connect", &_mysql);
  }
  log_info("Connection to MySQL opened\n");

  std::string q = "SET NAMES 'utf8'";
  if (mysql_real_query(&_mysql, q.data(), (unsigned long)q.length()) != 0)
    throw ConnectionError(q, &_mysql);
}

size_t MySQLCopyDataSource::count_rows(const std::string &schema, const std::string &table, const std::vector<std::string> &pk_columns,
                                       const CopySpec &spec, const std::vector<std::string> &last_pkeys)
{
  std::string q = base::strfmt("USE %s", schema.c_str());

  if (mysql_query(&_mysql, q.data()) < 0)
    throw ConnectionError("mysql_query("+q+")", &_mysql);

  switch (spec.type)
  {
    case CopyAll:
      if (spec.resume && last_pkeys.size())
        q = base::strfmt("SELECT count(*) FROM %s WHERE %s", table.c_str(), get_where_condition(pk_columns, last_pkeys).c_str());
      else
        q = base::strfmt("SELECT count(*) FROM %s", table.c_str());
      break;
    case CopyRange:
    {
      std::string start_expr, end_expr;
      if (spec.range_end < 0)
        end_expr = "";
      else
        end_expr = base::strfmt("%s <= %lli", spec.range_key.c_str(), spec.range_end);
      start_expr = base::strfmt("%s >= %lli", spec.range_key.c_str(), spec.range_start);
      if (!end_expr.empty())
        q = base::strfmt("SELECT count(*) FROM %s WHERE %s AND %s", table.c_str(), start_expr.c_str(), end_expr.c_str());
      else
        q = base::strfmt("SELECT count(*) FROM %s WHERE %s", table.c_str(), start_expr.c_str());
      break;
    }
    case CopyCount:
    {
      if (spec.resume && last_pkeys.size())
        q = base::strfmt("SELECT count(*) FROM %s WHERE %s LIMIT %lli", table.c_str(), get_where_condition(pk_columns, last_pkeys).c_str(), spec.row_count);
      else
        q = base::strfmt("SELECT count(*) FROM %s LIMIT %lli", table.c_str(), spec.row_count);
      break;
    }
    case CopyWhere:
    {
      q = base::strfmt("SELECT count(*) FROM %s WHERE %s", table.c_str(), spec.where_expression.c_str());
      break;
    }
  }

  if (mysql_query(&_mysql, q.data()) != 0)
    throw ConnectionError("mysql_query("+q+")", &_mysql);

  MYSQL_RES *result;
  if ((result = mysql_use_result(&_mysql)) == NULL)
    throw ConnectionError("MySQL query", &_mysql);


  // Retrieves the row count...
  MYSQL_ROW row = mysql_fetch_row(result);

  long long count = 0;
  if (row)
    count = atol(row[0]);

  mysql_free_result(result);

  if ((spec.type == CopyAll || spec.type == CopyWhere) && spec.max_count > 0 && spec.max_count < count)
      count = spec.max_count;

  return count;
}

boost::shared_ptr<std::vector<ColumnInfo> > MySQLCopyDataSource::begin_select_table(const std::string &schema, const std::string &table,
                                                                                    const std::vector<std::string> &pk_columns,
                                                                                    const std::string &select_expression,
                                                                                    const CopySpec &spec, const std::vector<std::string> &last_pkeys)
{
  boost::shared_ptr<std::vector<ColumnInfo> > columns(new std::vector<ColumnInfo>());

  _schema_name = schema;
  _table_name = table;

  std::string q = base::strfmt("USE %s", schema.c_str());

  if (mysql_query(&_mysql, q.data()) < 0)
    throw ConnectionError("mysql_query("+q+")", &_mysql);

  QueryBuilder select_query;
  select_query.select_columns(select_expression);
  select_query.select_from_table(table);
  select_query.add_orderby(boost::algorithm::join(pk_columns, ", "));

  if (spec.type == CopyCount || spec.max_count > 0)
    select_query.add_limit(base::strfmt("%lli", spec.row_count));
  if (spec.resume && last_pkeys.size())
    select_query.add_where(get_where_condition(pk_columns, last_pkeys));
  if (spec.type == CopyRange)
  {
    select_query.add_where(base::strfmt("%s >= %lli", spec.range_key.c_str(), spec.range_start));
    if (spec.range_end >= 0)
      select_query.add_where(base::strfmt("%s <= %lli", spec.range_key.c_str(), spec.range_end));
  }
  if (spec.type == CopyWhere)
    select_query.add_where(spec.where_expression);

  q = select_query.build_query();

  log_debug("Executing query: %s\n", q.c_str());
  MYSQL_STMT *stmt = mysql_stmt_init(&_mysql);
  if (stmt)
  {
    if (mysql_stmt_prepare(stmt, q.data(), (unsigned long)q.length()) == 0)
    {
      _select_stmt = stmt;

      MYSQL_RES *result = mysql_stmt_result_metadata(_select_stmt);

      if (result)
      {
        int column_count = mysql_num_fields(result);

        log_debug2("Columns from source table %s.%s (%i):\n", schema.c_str(), table.c_str(), column_count);

        MYSQL_FIELD *fields = mysql_fetch_fields(result);

        for (int i = 0; i < column_count; i++)
        {
          ColumnInfo info;

          info.source_name = fields[i].name;
          info.source_type = mysql_field_type_to_name(fields[i].type);
          info.source_length = fields[i].length;
          info.is_unsigned = false;
          info.is_long_data = false;

          info.is_long_data = fields[i].type == MYSQL_TYPE_TINY_BLOB ||
                              fields[i].type == MYSQL_TYPE_MEDIUM_BLOB ||
                              fields[i].type == MYSQL_TYPE_BLOB;

          if(info.is_long_data)
            _has_long_data = true;

          log_debug2("%i - %s: %s\n", i + 1, info.source_name.c_str(), info.source_type.c_str());
          columns->push_back(info);
        }

        if (mysql_stmt_execute(_select_stmt) != 0)
          throw ConnectionError("mysql_stmt_execute", &_mysql);
      }
      else
        throw ConnectionError("mysql_stmt_result_metadata", &_mysql);
    }
    else
    {
      if(mysql_stmt_close(stmt))
        throw ConnectionError("mysql_stmt_close", &_mysql);

      throw ConnectionError("mysql_stmt_prepare", &_mysql);
    }
  }
  else
    throw ConnectionError("mysql_stmt_init", &_mysql);

  return columns;
}

void MySQLCopyDataSource::end_select_table()
{
  if (_select_stmt)
  {
    if (mysql_stmt_close(_select_stmt))
      throw ConnectionError("mysql_stmt_close", &_mysql);
    else
      _select_stmt = NULL;
  }
}

bool MySQLCopyDataSource::fetch_row(RowBuffer &rowbuffer)
{
  bool ret_val = true;

  if (mysql_stmt_bind_result(_select_stmt, &(rowbuffer[0])) != 0)
    throw ConnectionError(base::strfmt("mysql_stmt_bind_result: %s", mysql_stmt_error(_select_stmt)), &_mysql);

  int errcode = mysql_stmt_fetch(_select_stmt);

  if (errcode != 0)
  {
    if (errcode == MYSQL_DATA_TRUNCATED /*&& _has_long_data*/)
    {
      for (size_t index = 0; index < rowbuffer.size(); index++)
      {
        if (*rowbuffer[index].error)
        {
          if (rowbuffer[index].buffer_type == MYSQL_TYPE_TINY_BLOB ||
            rowbuffer[index].buffer_type == MYSQL_TYPE_MEDIUM_BLOB ||
            rowbuffer[index].buffer_type == MYSQL_TYPE_LONG_BLOB ||
            rowbuffer[index].buffer_type == MYSQL_TYPE_BLOB ||
            rowbuffer[index].buffer_type == MYSQL_TYPE_STRING ||
            rowbuffer[index].buffer_type == MYSQL_TYPE_GEOMETRY)
          {
            if (rowbuffer[index].buffer_length)
              free(rowbuffer[index].buffer);

            rowbuffer[index].buffer_length = *rowbuffer[index].length;

            if (_max_parameter_size >= 0 && rowbuffer[index].buffer_length > (unsigned long long)_max_parameter_size)
            {
              if (_abort_on_oversized_blobs)
                throw std::runtime_error(base::strfmt("oversized blob found in table %s.%s, size: %lli",
                _schema_name.c_str(), _table_name.c_str(),
                (long long)rowbuffer[index].buffer_length));
              else
              {
                printf("oversized blob found in table %s.%s, size: %lli",
                  _schema_name.c_str(), _table_name.c_str(),
                  (long long)rowbuffer[index].buffer_length);
                *rowbuffer[index].is_null = true;
                continue;
              }
            }
            else
            {
              rowbuffer[index].buffer = malloc(rowbuffer[index].buffer_length);

              mysql_stmt_fetch_column(_select_stmt, &rowbuffer[index], (unsigned int)index, 0);
            }
          }
          else
            ret_val = false;
        }
      }
    }
    else
      ret_val = false;

    // BLOBs will be sent ONLY when not doing bulk inserts
    if (ret_val && _has_long_data && !_use_bulk_inserts)
    {
      // Sends the BLOBS...
      rowbuffer.clear();
      for (size_t index = 0; index < rowbuffer.size(); index++)
      {
        if (rowbuffer.check_if_blob())
          rowbuffer.send_blob_data((const char*)rowbuffer[index].buffer, rowbuffer[index].buffer_length);

        // Advances the current field pointer insied row buffer
        rowbuffer.finish_field((*rowbuffer[index].is_null) == 1);
      }
    }
  }

  return ret_val;
}

MySQLCopyDataSource::~MySQLCopyDataSource()
{
  if (_select_stmt)
    mysql_stmt_close(_select_stmt);
  mysql_close(&_mysql);
}

// -------------------------------------------------------------------------------------------------

void MySQLCopyDataTarget::init()
{
  /*
   As of MySQL 5.1.57, the max_long_data_size system variable controls the maximum size of parameter
   values that can be sent with mysql_stmt_send_long_data(). If this variable not set at server startup,
   the default is the value of the max_allowed_packet system variable. max_long_data_size is deprecated.
   In MySQL 5.6, it is removed and the maximum parameter size is controlled by max_allowed_packet.
   */
  get_server_version();

  // find out the max packet size taken by the connection
   get_server_value("max_allowed_packet", _max_allowed_packet);
   log_debug("Detected max_allowed_packet=%lu\n", _max_allowed_packet);

   if (_major_version == 5 &&
      ((_minor_version > 1 && _minor_version < 6) || (_minor_version == 1 && _build_version >= 57)))
   {
     // find out the max parameter size on a prepared statement
      get_server_value("max_long_data_size", _max_long_data_size);
      log_debug("Detected max_long_data_size=%lu\n", _max_long_data_size);
   }
   else
     _max_long_data_size = _max_allowed_packet;

  std::string q = "SET NAMES 'utf8'";
  if (mysql_real_query(&_mysql, q.data(), (unsigned long)q.length()) != 0)
    throw ConnectionError(q, &_mysql);

  // the source data will come in a charset that's not utf-8, so we let the server do the conversion
  if (!_incoming_data_charset.empty())
  {
    log_info("Setting charset for source data to %s\n", _incoming_data_charset.c_str());
    q = base::sqlstring("SET character_set_client=?", 0) << _incoming_data_charset;
    if (mysql_real_query(&_mysql, q.data(), (unsigned long)q.length()) != 0)
      throw ConnectionError(q, &_mysql);
  }

  q = "SET FOREIGN_KEY_CHECKS=0";
  if (mysql_real_query(&_mysql, q.data(), (unsigned long)q.length()) != 0)
    throw ConnectionError(q, &_mysql);

  // some DBs (like MS Access) have sequence/auto-increment values start at 0
  // by default, mysql will change that to 1, so when the actual row numbered 1 comes it will be duplicated
  if (mysql_query(&_mysql, "SET SESSION SQL_MODE=CONCAT('NO_AUTO_VALUE_ON_ZERO,', @@SQL_MODE)") != 0)
  {
    log_warning("Error changing sql_mode: %s\n", mysql_error(&_mysql));
  }
}

std::vector<std::string> MySQLCopyDataTarget::get_last_pkeys(const std::vector<std::string> &pk_columns, const std::string &schema, const std::string &table)
{
  std::vector<std::string> ret;
  std::string order_by_cond;

  for (size_t i = 0; i < pk_columns.size(); ++i)
  {
    order_by_cond += base::strfmt("%s DESC", pk_columns[i].c_str());
    if (i < pk_columns.size() - 1)
      order_by_cond += ",";
  }

  const std::string q = base::strfmt("SELECT %s FROM %s.%s ORDER BY %s LIMIT 0,1", boost::algorithm::join(pk_columns, ", ").c_str(), schema.c_str(), table.c_str(), order_by_cond.c_str());
  if (mysql_query(&_mysql, q.data()) != 0)
      throw ConnectionError("mysql_query(" + q + ")", &_mysql);

  MYSQL_RES *result;
  if ((result = mysql_use_result(&_mysql)) == NULL)
    throw ConnectionError("mysql_use_result", &_mysql);


  MYSQL_ROW row = mysql_fetch_row(result);

  if (row)
    for (size_t i = 0; i < pk_columns.size(); ++i)
      ret.push_back(row[i]);

  mysql_free_result(result);

  std::string column_value;
  for (size_t i = 0; i < ret.size(); ++i)
  {
    column_value += base::strfmt("%s: %s", pk_columns[i].c_str(), ret[i].c_str());
    if (i < pk_columns.size() - 1)
      column_value += ",";
  }
  log_info("Resuming copy of table %s.%s. Starting on record with keys: %s\n", schema.c_str(), table.c_str(), column_value.c_str());

  return ret;
}


MYSQL_RES * MySQLCopyDataTarget::get_server_value(const std::string& variable)
{
  std::string q = "SHOW VARIABLES LIKE '" + variable + "'";
  if (mysql_real_query(&_mysql, q.data(), (unsigned long)q.length()) < 0)
    throw ConnectionError(q, &_mysql);

  MYSQL_RES *result;
  if ((result = mysql_use_result(&_mysql)) == NULL)
    throw ConnectionError("MySQL query", &_mysql);

  return result;
}


void MySQLCopyDataTarget::get_server_value(const std::string& variable, std::string &value)
{
  MYSQL_RES *result = get_server_value(variable);

  MYSQL_ROW row = mysql_fetch_row(result);

  if (row)
    value = row[1];

  mysql_free_result(result);
}

void MySQLCopyDataTarget::get_server_value(const std::string& variable, unsigned long &value)
{
  MYSQL_RES *result = get_server_value(variable);

  MYSQL_ROW row = mysql_fetch_row(result);

  if (row)
  {
    unsigned long *lengths = mysql_fetch_lengths(result);
    sscanf(std::string(row[1], lengths[1]).c_str(), "%lu", &value);
  }

  mysql_free_result(result);
}

void MySQLCopyDataTarget::get_server_version()
{
  std::string version;

  get_server_value("version", version);

  std::vector<std::string> parsed_version = base::split(version, ".");

  _major_version = base::atoi<int>(parsed_version[0], 0);
  if (parsed_version.size() > 1)
    _minor_version = base::atoi<int>(parsed_version[1], 0);

  if (parsed_version.size() > 2)
    _build_version = base::atoi<int>(parsed_version[2], 0);

  log_debug("Detected server version=%s\n", version.c_str());
}

bool MySQLCopyDataTarget::is_mysql_version_at_least(const int _major, const int _minor, const int _build)
{
  return _major_version > _major
           || (_major_version == _major && _minor_version > _minor)
           || (_major_version == _major && _minor_version == _minor && _build_version >= _build);
}

std::string MySQLCopyDataTarget::ps_query()
{
  std::string q("INSERT INTO ");
  //q.append(base::sqlstring("!.!", 0) << _schema << _table).append(" (");
  q.append(base::strfmt("%s.%s", _schema.c_str(), _table.c_str())).append(" (");
  for (std::vector<ColumnInfo>::const_iterator iter = _columns->begin(); iter != _columns->end(); ++iter)
  {
    if (iter != _columns->begin())
      q.append(", ").append(base::sqlstring("!", 0) << iter->target_name);
    else
      q.append(base::sqlstring("!", 0) << iter->target_name);
  }

  q.append(") VALUES ");

  // On Prepared statemnts a sample record with the wildcards needs to be set
  // On bulk inserts the real records will be appended later
  if (!_use_bulk_inserts)
  {
    q.append("(");
    for (std::vector<ColumnInfo>::const_iterator iter = _columns->begin(); iter != _columns->end(); ++iter)
    {
      if (iter != _columns->begin())
        q.append(", ?");
      else
        q.append("?");
    }
    q.append(")");
  }
  return q;
}


enum enum_field_types MySQLCopyDataTarget::field_type_to_ps_param_type(enum enum_field_types ftype)
{
  // convert the resultset types to the PS param types
  switch (ftype)
  {
    case MYSQL_TYPE_DECIMAL:
      ftype = MYSQL_TYPE_STRING;
      break;
    case MYSQL_TYPE_TINY:
      ftype = MYSQL_TYPE_TINY;
      break;
    case MYSQL_TYPE_SHORT:
      ftype = MYSQL_TYPE_SHORT;
      break;
    case MYSQL_TYPE_LONG:
      ftype = MYSQL_TYPE_LONG;
      break;
    case MYSQL_TYPE_FLOAT:
      ftype = MYSQL_TYPE_FLOAT;
      break;
    case MYSQL_TYPE_DOUBLE:
      ftype = MYSQL_TYPE_DOUBLE;
      break;
    case MYSQL_TYPE_NULL:
      ftype = MYSQL_TYPE_NULL;
      break;
    case MYSQL_TYPE_TIMESTAMP:
      ftype = MYSQL_TYPE_TIMESTAMP;
      break;
    case MYSQL_TYPE_LONGLONG:
      ftype = MYSQL_TYPE_LONGLONG;
      break;
    case MYSQL_TYPE_INT24:
      ftype = MYSQL_TYPE_LONG;
      break;
    case MYSQL_TYPE_DATE:
      ftype = MYSQL_TYPE_DATE;
      break;
    case MYSQL_TYPE_TIME:
      ftype = MYSQL_TYPE_TIME;
      break;
    case MYSQL_TYPE_DATETIME:
      ftype = MYSQL_TYPE_DATETIME;
      break;
    case MYSQL_TYPE_YEAR:
      ftype = MYSQL_TYPE_SHORT;
      break;
    case MYSQL_TYPE_NEWDATE:
      ftype = MYSQL_TYPE_DATE;
      break;
    case MYSQL_TYPE_VARCHAR:
      ftype = MYSQL_TYPE_STRING;
      break;
    case MYSQL_TYPE_BIT:
      ftype = MYSQL_TYPE_BIT;
      break;
    case MYSQL_TYPE_NEWDECIMAL:
      ftype = MYSQL_TYPE_STRING;
      break;
    case MYSQL_TYPE_ENUM:
    case MYSQL_TYPE_SET:
      ftype = MYSQL_TYPE_STRING;
      break;
    case MYSQL_TYPE_TINY_BLOB:
    case MYSQL_TYPE_MEDIUM_BLOB:
    case MYSQL_TYPE_LONG_BLOB:
    case MYSQL_TYPE_BLOB:
      ftype = MYSQL_TYPE_BLOB;
      break;
    case MYSQL_TYPE_VAR_STRING:
    case MYSQL_TYPE_STRING:
      ftype = MYSQL_TYPE_STRING;
      break;
    case MYSQL_TYPE_GEOMETRY:
      if (_source_rdbms_type == "Mysql")
        ftype = MYSQL_TYPE_BLOB;
      else
        ftype = MYSQL_TYPE_GEOMETRY;
      break;
    default:
      break;
  }
  return ftype;
}

MySQLCopyDataTarget::MySQLCopyDataTarget(const std::string &hostname, int port,
                    const std::string &username, const std::string &password,
                    const std::string &socket, bool use_cleartext_plugin, const std::string &app_name,
                    const std::string &incoming_charset, const std::string &source_rdbms_type)
: _insert_stmt(NULL), _max_allowed_packet(1000000), _max_long_data_size(1000000),// 1M default
  _row_buffer(NULL), _major_version(0), _minor_version(0), _build_version(0), _use_bulk_inserts(true),
  _bulk_insert_batch(0), _source_rdbms_type(source_rdbms_type)
{
  std::string host = hostname;
  _truncate = false;

  _incoming_data_charset = incoming_charset;
  if (base::tolower(_incoming_data_charset) == "cp1252" || base::tolower(_incoming_data_charset) == "windows-1252")
    _incoming_data_charset = "latin1";

  mysql_init(&_mysql);
#if defined(MYSQL_VERSION_MAJOR) && defined(MYSQL_VERSION_MINOR) && defined(MYSQL_VERSION_PATCH)
#if MYSQL_CHECK_VERSION(5,6,6)
  if (is_mysql_version_at_least(5,6,6))
    mysql_options4(&_mysql, MYSQL_OPT_CONNECT_ATTR_ADD, "program_name", app_name.c_str());
#endif
#endif

  // _bulk_insert_record is used to prepare a single record string, the connection
  // is needed to escape binary data properly
  _bulk_insert_record.set_connection(&_mysql);

  if (port > 0)
  {
    // Forces usage of TCP connection if indicated on the connection
    // settings (a port is specified)
    int proto = MYSQL_PROTOCOL_TCP;
    mysql_options(&_mysql, MYSQL_OPT_PROTOCOL, &proto);

    log_info("Connecting to MySQL server at %s:%i with user %s\n",
           hostname.c_str(), port, username.c_str());
  }
  else
  {
    // Socket file/Named pipe connections

    #if defined(WIN32)
    // Default local host connection in windows are done through shared memory
    // using "." forces using named pipe
    host=".";
    #else
    // Default local host connections in Linux are done through socket files
    host="localhost";
    #endif

    log_info("Connecting to MySQL server using socket %s with user %s\n",
           socket.c_str(), username.c_str());
  }

#if defined(MYSQL_VERSION_MAJOR) && defined(MYSQL_VERSION_MINOR) && defined(MYSQL_VERSION_PATCH)
#if MYSQL_CHECK_VERSION(5,5,27)
  my_bool use_cleartext = use_cleartext_plugin;
  mysql_options(&_mysql, MYSQL_ENABLE_CLEARTEXT_PLUGIN, &use_cleartext);
#else
  if (use_cleartext_plugin)
    log_warning("Trying to use the ClearText plugin, but it's not supported by libmysqlclient\n");
#endif
#endif


  if (!mysql_real_connect(&_mysql, hostname.c_str(), username.c_str(), password.c_str(), NULL, port, socket.c_str(),
                          CLIENT_COMPRESS))
  {
    log_error("Failed opening connection to MySQL: %s\n", mysql_error(&_mysql));
    throw ConnectionError("mysql_real_connect", &_mysql);
  }
  log_info("Connection to MySQL opened\n");

  init();
}

MySQLCopyDataTarget::~MySQLCopyDataTarget()
{
  delete _row_buffer;
  if (_insert_stmt)
    mysql_stmt_close(_insert_stmt);
  mysql_close(&_mysql);
}


void MySQLCopyDataTarget::set_truncate(bool flag)
{
  _truncate = flag;
}

void MySQLCopyDataTarget::set_target_table(const std::string &schema, const std::string &table,
                                           boost::shared_ptr<std::vector<ColumnInfo> > columns)
{
  _schema = schema;
  _table = table;
  _columns = columns;

  // create a PS and prepare it, which will get us the metadata we need without
  // actually executing the query
  std::string q = base::strfmt("SELECT * FROM %s.%s", schema.c_str(), table.c_str()); //base::sqlstring("SELECT * FROM !.!", 0) << schema << table;
  MYSQL_STMT *stmt = mysql_stmt_init(&_mysql);
  if (stmt)
  {
    if (mysql_stmt_prepare(stmt, q.data(), (unsigned long)q.length()) == 0)
    {
      MYSQL_RES *meta = mysql_stmt_result_metadata(stmt);
      if (meta)
      {
        int column_count = mysql_num_fields(meta);
        if (column_count == (int)columns->size())
        {
          MYSQL_FIELD *fields = mysql_fetch_fields(meta);
          log_debug2("Columns from target table %s.%s (%i):\n", schema.c_str(), table.c_str(), column_count);
          for (int i= 0; i < column_count; i++)
          {
            (*columns)[i].target_name = std::string(fields[i].name, fields[i].name_length);
            (*columns)[i].target_type = field_type_to_ps_param_type(fields[i].type);
            // We can't trust source drivers to report signed/unsigned values, so we take that from the target MySQL table:
            (*columns)[i].is_unsigned = (fields[i].flags & UNSIGNED_FLAG) != 0;
            if (_get_field_lengths_from_target)
                (*columns)[i].source_length = fields[i].length;
            log_debug2("%i - %s: %s\n", i + 1, (*columns)[i].target_name.c_str(),
                       mysql_field_type_to_name((*columns)[i].target_type));
          }
        }
        else
        {
          mysql_free_result(meta);
          mysql_stmt_close(stmt);
          throw std::runtime_error(base::strfmt("Table %s.%s has wrong number of columns in target DB (%i, expected %i)",
                                                schema.c_str(), table.c_str(), column_count, (int)columns->size()));
        }
        mysql_free_result(meta);
      }
      else
      {
        ConnectionError err("mysql_stmt_result_metadata", stmt);
        mysql_stmt_close(stmt);
        throw err;
      }
    }
    else
    {
      ConnectionError err("mysql_stmt_prepare", stmt);
      mysql_stmt_close(stmt);
      throw err;
    }
    mysql_stmt_close(stmt);
  }
  else
    throw ConnectionError("mysql_stmt_init", &_mysql);

  if (_truncate)
  {
    log_info("Truncating table %s.%s\n", schema.c_str(), table.c_str());
    if (mysql_query(&_mysql, base::strfmt("TRUNCATE %s.%s", schema.c_str(), table.c_str()).c_str()) != 0)
      log_warning("Error executing TRUNCATE %s.%s: %s\n",
                  schema.c_str(), table.c_str(), mysql_error(&_mysql));
  }

  // TODO: Bulk inserts should be disabled when a single record can be bigger than the max_packet_size
  _use_bulk_inserts = true;
  if (_use_bulk_inserts)
  {
    _bulk_insert_buffer.reset(_max_allowed_packet);
    _bulk_insert_record.reset(_max_allowed_packet);
  }
}

void MySQLCopyDataTarget::send_long_data(int column, const char *data, size_t length)
{
  if (mysql_stmt_send_long_data(_insert_stmt, column, data, (unsigned long)length))
  {
    std::string error = base::strfmt("Error sending long data: %s", mysql_stmt_error(_insert_stmt));
    throw std::logic_error(error);
  }
}

void MySQLCopyDataTarget::begin_inserts()
{
  MYSQL_STMT *stmt;

  // Initialize variables for non prepared insert statement
  _bulk_insert_query = ps_query();
  _init_bulk_insert = true;
  _bulk_record_count = 0;

  // The RowBuffer is used by the CopyDataSources to store in it the data read from the
  // database, once the data is loaded in it, it is used for both bulk inserts
  // and prepared statements
  if (_row_buffer)
    delete _row_buffer;

  _row_buffer = new RowBuffer(_columns, boost::bind(&MySQLCopyDataTarget::send_long_data, this, _1, _2, _3), _max_allowed_packet);

  if (!_use_bulk_inserts)
  {
    stmt = mysql_stmt_init(&_mysql);
    if (!stmt)
      throw ConnectionError("mysql_stmt_init", &_mysql);

    if (mysql_stmt_prepare(stmt, _bulk_insert_query.data(), (unsigned long)_bulk_insert_query.length()) != 0)
    {
      mysql_stmt_close(stmt);
      throw ConnectionError("mysql_stmt_prepare", stmt);
    }
    if (mysql_stmt_param_count(stmt) != _columns->size())
    {
      mysql_stmt_close(stmt);
      throw std::logic_error("Unexpected parameter count for PS returned by MySQL");
    }

    if (mysql_stmt_bind_param(stmt, &(*_row_buffer)[0]) != 0)
      throw ConnectionError("mysql_stmt_bind_param", stmt);

    _insert_stmt = stmt;
  }
}

int MySQLCopyDataTarget::end_inserts(bool flush)
{
  int ret_val = 0;

  // When doing bulk inserts it is possible that some records are still pending on the
  // _bulk_insert_buffer or _bulk_insert_record so they need to be inserted
  if (_use_bulk_inserts)
  {
    if (flush)
    {
      if (_bulk_insert_buffer.length)
        ret_val = do_insert(true);
      else if (_bulk_insert_record.length)
      {
        _init_bulk_insert = true;
        ret_val = do_insert(true);
      }
    }
  }
  else
  {
    if (_insert_stmt)
      mysql_stmt_close(_insert_stmt);
    _insert_stmt = NULL;
  }

  return ret_val;
}

int MySQLCopyDataTarget::do_insert(bool final)
{
  int ret_val = 0;

  if (_use_bulk_inserts)
  {
    bool add_comma = true;

    if (_init_bulk_insert)
    {
      add_comma = false;
      _init_bulk_insert = false;

      _bulk_insert_buffer.append(_bulk_insert_query.c_str(), _bulk_insert_query.length());

      if (_bulk_insert_record.length)
      {
        _bulk_insert_buffer.append(_bulk_insert_record.buffer, _bulk_insert_record.length);
        _bulk_insert_record.reset(_max_allowed_packet);
        _bulk_record_count++;
        add_comma = true;
      }
    }

    // This will be disabled if still have records but they still fit into the buffer
    bool do_insert = true;

    // If it is not the last insert (there still pending records)
    // Then continues with the formatting
    if (!final)
    {
      // Formats the next record into _bulk_insert_record
      if (format_bulk_record())
      {
        // Next record + 1 as the comma also counts
        if (_bulk_insert_buffer.space_left() >= (_bulk_insert_record.length + ( add_comma? 1:0)))
        {
          if (add_comma)
            _bulk_insert_buffer.append(",", 1);

          _bulk_insert_buffer.append(_bulk_insert_record.buffer, _bulk_insert_record.length);
          _bulk_insert_record.reset(_max_allowed_packet);
          _bulk_record_count++;

          // Forces the insert when the max number of records has been reached
          do_insert = _bulk_record_count == _bulk_insert_batch;
        }
      }
      else
      {
        throw std::runtime_error("Found record bigger than max_allowed_packet");
      }
    }

    if (do_insert)
    {
      ret_val = _bulk_record_count;
      _init_bulk_insert = true;
      if (mysql_real_query(&_mysql, _bulk_insert_buffer.buffer, (unsigned long)_bulk_insert_buffer.length) != 0)
      {
        _bulk_insert_buffer.buffer[_bulk_insert_buffer.length] = 0;
        log_info("Statement execution failed: %s:\n%s\n", mysql_error(&_mysql), _bulk_insert_buffer.buffer);

        throw ConnectionError("Inserting Data", &_mysql);
      }
      _bulk_insert_buffer.reset(_max_allowed_packet);
      _bulk_record_count = 0;
    }
  }
  else
  {
    if (mysql_stmt_execute(_insert_stmt) != 0)
      throw ConnectionError("mysql_stmt_execute", _insert_stmt);

    ret_val = 1;
  }

  return ret_val;
}

bool MySQLCopyDataTarget::format_bulk_record()
{
  bool ret_val = true;
  _bulk_insert_record.append("(", 1);

  for(size_t index = 0; ret_val && index < _row_buffer->size() - 1; index++)
  {
    ret_val = append_bulk_column(index);
    _bulk_insert_record.append(",", 1);
  }

  if (ret_val)
  {
    ret_val = append_bulk_column(_row_buffer->size() - 1);

    if (ret_val)
      ret_val = _bulk_insert_record.append(")", 1);
  }

  return ret_val;
}

bool MySQLCopyDataTarget::append_bulk_column(size_t col_index)
{
  std::string data;
  bool ret_val = true;

  if (*(*_row_buffer)[col_index].is_null)
    ret_val = _bulk_insert_record.append("NULL", 4);
  else
  {
    switch((*_row_buffer)[col_index].buffer_type)
    {
    case MYSQL_TYPE_NULL:
      ret_val = _bulk_insert_record.append("NULL", 4);
      break;
    case MYSQL_TYPE_TINY:
      if ((*_row_buffer)[col_index].is_unsigned)
      {
        unsigned char *val_char = (unsigned char *)(*_row_buffer)[col_index].buffer;
        data = base::strfmt("%u", *val_char);
      }
      else
      {
        char *val_char = (char *)(*_row_buffer)[col_index].buffer;
        data = base::strfmt("%d", *val_char);
      }
      ret_val = _bulk_insert_record.append(data.data(), data.length());
      break;
    case MYSQL_TYPE_SHORT:
    case MYSQL_TYPE_YEAR:
      if ((*_row_buffer)[col_index].is_unsigned)
      {
        unsigned short *val_short = (unsigned short *)(*_row_buffer)[col_index].buffer;
        data = base::strfmt("%u", *val_short);
      }
      else
      {
        short *val_short = (short *)(*_row_buffer)[col_index].buffer;
        data = base::strfmt("%d", *val_short);
      }
      ret_val = _bulk_insert_record.append(data.data(), data.length());
      break;
    case MYSQL_TYPE_INT24:
    case MYSQL_TYPE_LONG:
      if ((*_row_buffer)[col_index].is_unsigned)
      {
        unsigned int *val_int = (unsigned int *)(*_row_buffer)[col_index].buffer;
        data = base::strfmt("%u", *val_int);
      }
      else
      {
        int *val_int = (int *)(*_row_buffer)[col_index].buffer;
        data = base::strfmt("%i", *val_int);
      }
      ret_val = _bulk_insert_record.append(data.data(), data.length());
      break;
    case MYSQL_TYPE_LONGLONG:
      if ((*_row_buffer)[col_index].is_unsigned)
      {
        unsigned long long int *val_llint = (unsigned long long int*)(*_row_buffer)[col_index].buffer;
        data = base::strfmt("%llu", *val_llint);
      }
      else
      {
        long long int *val_llint = (long long int *)(*_row_buffer)[col_index].buffer;
        data = base::strfmt("%lli", *val_llint);
      }
      ret_val = _bulk_insert_record.append(data.data(), data.length());
      break;
    case MYSQL_TYPE_FLOAT:
      {
        float *val_float = (float*)(*_row_buffer)[col_index].buffer;
        data = base::strfmt("%f", *val_float);
        ret_val = _bulk_insert_record.append(data.data(), data.length());
      }
      break;
    case MYSQL_TYPE_DOUBLE:
      {
        double *val_double = (double*)(*_row_buffer)[col_index].buffer;
        data = base::strfmt("%f", *val_double);
        ret_val = _bulk_insert_record.append(data.data(), data.length());
      }
      break;
    case MYSQL_TYPE_BIT:
    {
      // As managed as string, an additional byte is added to the length, so
      // we remove that here to know the real legth in bytes
      std::div_t length= std::div((*_row_buffer)[col_index].buffer_length - 1, 8);

      if (length.rem)
        ++length.quot;

      unsigned long long uval = 0;
      unsigned int shift = 0;

      for (int index = 1; index <= length.quot; index++ )
      {
        uval += (((unsigned char*)(*_row_buffer)[col_index].buffer)[length.quot - index]) << shift;
        shift += 8;
      }

      std::string numeric_value = base::strfmt("%llu", uval);

      ret_val = _bulk_insert_record.append(numeric_value.data(), numeric_value.length());
      break;
    }
    case MYSQL_TYPE_DECIMAL:
    case MYSQL_TYPE_NEWDECIMAL:
      ret_val = _bulk_insert_record.append_escaped((char*)(*_row_buffer)[col_index].buffer, *(*_row_buffer)[col_index].length);
      break;
    case MYSQL_TYPE_VAR_STRING:
    case MYSQL_TYPE_VARCHAR:
    case MYSQL_TYPE_STRING:
    case MYSQL_TYPE_ENUM:
    case MYSQL_TYPE_SET:
      _bulk_insert_record.append("'", 1);
      ret_val = _bulk_insert_record.append_escaped((char*)(*_row_buffer)[col_index].buffer, *(*_row_buffer)[col_index].length);
      _bulk_insert_record.append("'", 1);
      break;
    case MYSQL_TYPE_TIME:
    case MYSQL_TYPE_DATE:
    case MYSQL_TYPE_NEWDATE:
    case MYSQL_TYPE_DATETIME:
    case MYSQL_TYPE_TIMESTAMP:
      {
        MYSQL_TIME *ts = (MYSQL_TIME*)(*_row_buffer)[col_index].buffer;
        switch(ts->time_type)
        {
        case MYSQL_TIMESTAMP_DATETIME:
          if (_major_version >= 6
              || (_major_version == 5 && _minor_version >= 7)
              || (_major_version == 5 && _minor_version == 6 && _build_version >= 4))
            data = base::strfmt("'%04d-%02d-%02d %02d:%02d:%02d.%06lu'",
                         ts->year, ts->month, ts->day,
                         ts->hour, ts->minute, ts->second,
                         ts->second_part);
          else
            data = base::strfmt("'%04d-%02d-%02d %02d:%02d:%02d'",
                                   ts->year, ts->month, ts->day,
                                   ts->hour, ts->minute, ts->second);
          break;
        case MYSQL_TIMESTAMP_DATE:
          data = base::strfmt("'%04d-%02d-%02d'",
                         ts->year, ts->month, ts->day);
          break;
        case MYSQL_TIMESTAMP_TIME:
          if (_major_version >= 6
                        || (_major_version == 5 && _minor_version >= 7)
                        || (_major_version == 5 && _minor_version == 6 && _build_version >= 4))
            data = base::strfmt("'%02d:%02d:%02d.%06lu'",
                                ts->hour, ts->minute, ts->second, ts->second_part);
          else
            data = base::strfmt("'%02d:%02d:%02d'",
                                            ts->hour, ts->minute, ts->second);
          break;
        default:
          data = "''";
          break;
        }

        ret_val = _bulk_insert_record.append(data.data(), data.length());
      }
      break;
    case MYSQL_TYPE_BLOB:
    case MYSQL_TYPE_TINY_BLOB:
    case MYSQL_TYPE_MEDIUM_BLOB:
    case MYSQL_TYPE_LONG_BLOB:
      _bulk_insert_record.append("'", 1);
      ret_val = _bulk_insert_record.append_escaped((char*)(*_row_buffer)[col_index].buffer, *(*_row_buffer)[col_index].length);
      _bulk_insert_record.append("'", 1);
      break;

#if MYSQL_VERSION_ID > 50600
      case MYSQL_TYPE_TIMESTAMP2:
      case MYSQL_TYPE_DATETIME2:
      case MYSQL_TYPE_TIME2:
#endif
        // TODO: implement handling
        break;
      case MYSQL_TYPE_GEOMETRY:
        _bulk_insert_record.append("GeomFromText('");
        ret_val = _bulk_insert_record.append_escaped((char*)(*_row_buffer)[col_index].buffer, *(*_row_buffer)[col_index].length);
        _bulk_insert_record.append("')");
        break;
    }
  }

  return ret_val;
}

RowBuffer &MySQLCopyDataTarget::row_buffer()
{
  return *_row_buffer;
}

long long MySQLCopyDataTarget::get_max_value(const std::string &key)
{
  std::string q = base::sqlstring("SELECT max(!) FROM !.!", 0) << key << _schema << _table;
  mysql_query(&_mysql, q.c_str());
  return 0;
}


void MySQLCopyDataTarget::get_triggers_for_schema(const std::string &schema, std::map<std::string, std::string>& triggers)
{
  // Now pulls the trigger names
  log_debug("Retrieving trigger list\n");
  std::string get_trigger_list = base::sqlstring("SHOW TRIGGERS FROM !", 0) << schema;
  if (mysql_query(&_mysql, get_trigger_list.data()) != 0)
    throw ConnectionError("Querying Trigger List", &_mysql);

  MYSQL_RES *result;
  if ((result = mysql_use_result(&_mysql)) == NULL)
    throw ConnectionError("Getting Trigger List", &_mysql);

  // Gets the trigger names so they can be backed up
  MYSQL_ROW row;
  while ((row = mysql_fetch_row(result)))
    triggers[row[0]]="";

  mysql_free_result(result);
}

bool MySQLCopyDataTarget::get_trigger_definitions_for_schema(const std::string &schema, std::map<std::string, std::string>& triggers)
{
  bool success = true;
  std::map<std::string, std::string>::iterator index, end = triggers.end();

  // Pulls every trigger unless an error is found
  for (index = triggers.begin(); success && index != end; index++)
  {
    bool trigger_pulled = false;

    log_debug("Retrieving trigger definition for: %s\n", index->first.c_str());

    std::string get_trigger = base::sqlstring("SHOW CREATE TRIGGER !.!", 0) << schema << index->first;
    if (mysql_query(&_mysql, get_trigger.data()) == 0)
    {
      MYSQL_RES *result;
      if ((result = mysql_use_result(&_mysql)) != NULL)
      {
        MYSQL_ROW row;
        row = mysql_fetch_row(result);

        if (row)
        {
          triggers[index->first] = row[2];
          trigger_pulled = true;
        }

        mysql_free_result(result);
      }
    }

    // Updates the exit flag
    success = trigger_pulled;
  }

  // Will return TRUE only if all the triggers were successfully pulled
  return success;
}

void MySQLCopyDataTarget::backup_triggers_for_schema(const std::string &schema)
{
  bool created_table = false;
  std::string tmp_trigger_table(TMP_TRIGGER_TABLE);
  std::map <std::string, std::string> triggers;
  std::map<std::string, std::string>::iterator index, end;

  // Ensures there are triggers to backup
  get_triggers_for_schema(schema, triggers);

  if (!triggers.empty())
  {
    log_info("Disabling triggers for schema '%s'...", schema.c_str());

    // Creates the trigger backup table
    log_debug("\nCreating temporary trigger table: %s\n", tmp_trigger_table.c_str());
    std::string create_trigger_backup_table = base::sqlstring("CREATE TABLE !.! ( ! VARCHAR(100) NOT NULL, ! MEDIUMTEXT, PRIMARY KEY (!))", 0) << schema << TMP_TRIGGER_TABLE << "trigger_name" << "trigger_sql" << "trigger_name";

    if (mysql_query(&_mysql, create_trigger_backup_table.data()) != 0)
    {
      // If the backup table already exists then it is ok to continue
      if (mysql_errno(&_mysql) != 1050)
        throw ConnectionError("Unable to create trigger backup table", &_mysql);
      else
        log_info("The trigger backup table already existed on %s\n", schema.c_str());
    }
    else
      created_table = true;

    end = triggers.end();

    if (get_trigger_definitions_for_schema(schema, triggers))
    {
      // Backups the triggers
      for (index = triggers.begin(); index != end; index++)
      {
        if (!index->second.empty())
        {
          log_debug("Backing up trigger definition for: %s\n", index->first.c_str());
          std::string insert_trigger = base::sqlstring("INSERT INTO !.! VALUES (?,?)", 0) << schema << TMP_TRIGGER_TABLE << index->first << index->second;

          if (mysql_query(&_mysql, insert_trigger.data()) != 0)
          {
            // An error inserting the trigger because already exists is OK
            // It may be there from a previous run
            if (mysql_errno(&_mysql) != 1062)
            {
              // If the error is different and the backup table was created
              // on this run, it will be dropped
              if (created_table)
                drop_trigger_backups(schema);

              throw ConnectionError("Backing Up Trigger", &_mysql);
            }
            else
              log_info("The trigger %s was already in the backup\n", index->first.c_str());
          }
        }
      }

      // Now proceeds to remove the triggers
      for (index = triggers.begin(); index != end; index++)
      {
        log_debug("Dropping trigger: %s\n", index->first.c_str());
        std::string drop_trigger = base::sqlstring("DROP TRIGGER !.!", 0) << schema << index->first;
        if (mysql_query(&_mysql, drop_trigger.data()) != 0)
        {
          // If an error occurs and the backup table was created on this
          // run, the table is dropped
          if (created_table)
            drop_trigger_backups(schema);

          throw ConnectionError("Dropping Trigger", &_mysql);
        }
      }
    }

    log_info("Successfully backed up %lu triggers.\n", (unsigned long)triggers.size());
  }
}

void MySQLCopyDataTarget::drop_trigger_backups(const std::string& schema)
{
  log_debug("Deleting trigger backups\n");

  std::string drop_trigger_table = base::sqlstring("DROP TABLE !.!", 0) << schema << TMP_TRIGGER_TABLE;

  if (mysql_query(&_mysql, drop_trigger_table.data()) != 0)
    throw ConnectionError("Dropping trigger backups", &_mysql);
}

void MySQLCopyDataTarget::backup_triggers(std::set<std::string> &schemas)
{
  std::set <std::string>::const_iterator index;
  std::set <std::string>::const_iterator end = schemas.end();

  for (index = schemas.begin(); index!= end; index++)
    backup_triggers_for_schema (base::unquote_identifier(*index));
}

void MySQLCopyDataTarget::restore_triggers(std::set<std::string> &schemas)
{
  std::set <std::string>::const_iterator index;
  std::set <std::string>::const_iterator end = schemas.end();

  for (index = schemas.begin(); index!= end; index++)
  {
    std::vector <std::string> trigger_name;
    std::vector <std::string> trigger_sql;
    std::string a_schema = base::unquote_identifier(*index);

    log_info("Re-enabling triggers for schema '%s'\n", a_schema.c_str());
    log_debug("Retrieving trigger definitions\n");
    std::string get_trigger_definition = base::sqlstring("SELECT * FROM !.!", 0) << a_schema << TMP_TRIGGER_TABLE;

    if (mysql_query(&_mysql, get_trigger_definition.data()) != 0)
    {
      // It is ok if the table does not exist, it indicates there are
      // no triggers to restore
      if (mysql_errno(&_mysql) != 1146)
        throw ConnectionError("Querying Trigger Definitions", &_mysql);
      else
        log_info("No triggers found for '%s'\n", a_schema.c_str());
    }
    else
    {
      MYSQL_RES *result;
      if ((result = mysql_use_result(&_mysql)) == NULL)
        throw ConnectionError("Getting Trigger Definitions", &_mysql);

      // Gets the trigger names so they can be backed up
      MYSQL_ROW row;
      while ((row = mysql_fetch_row(result)))
      {
        trigger_name.push_back(row[0]);
        trigger_sql.push_back(row[1]);
      }

      mysql_free_result(result);

      std::vector <std::string>::const_iterator trigger_index;

      // Restores the triggers
      log_debug("Configuring active schema\n");
      std::string select_database = base::sqlstring("USE !", 0) << a_schema;
      if (mysql_query(&_mysql, select_database.data()) != 0)
        throw ConnectionError("Selecting Database", &_mysql);

      int restored = 0;
      for (size_t trigger_index = 0; trigger_index != trigger_sql.size(); trigger_index++)
      {
        log_debug("Restoring trigger %s\n", trigger_name.at(trigger_index).c_str());

        std::string trigger_def(trigger_sql.at(trigger_index));
        if (mysql_query(&_mysql, trigger_def.c_str()) != 0)
        {
          // It is ok having an error for duplicated triggers
          if (mysql_errno(&_mysql) != 1235)
            throw ConnectionError("Restoring trigger", &_mysql);
          else
            log_info("Trigger %s already existed, skipping...\n", trigger_name.at(trigger_index).c_str());
        }
        else
          restored++;
      }

      // Drops the trigger backup table
      drop_trigger_backups(a_schema);

      log_info("Trigger Restore: %d succeeded, %lu failed\n", restored, (unsigned long)(trigger_sql.size() - restored));
    }
  }
}


TaskQueue::TaskQueue()
{}

void TaskQueue::add_task(const TableParam& task)
{
  base::MutexLock  lock(_task_mutex);
  _tasks.push_back(task);
}

bool TaskQueue::get_task(TableParam& task)
{
  bool ret_val = false;

  base::MutexLock  lock(_task_mutex);

  if (_tasks.size() > 0 )
  {
    ret_val = true;
    task = _tasks.front();
    _tasks.erase(_tasks.begin());
  }

  return ret_val;
}

CopyDataTask::CopyDataTask(const std::string name, CopyDataSource*psource, MySQLCopyDataTarget* ptarget, TaskQueue* ptasks, bool show_progress):
_source(psource),
_target(ptarget)
{
  _name = name;
  _tasks = ptasks;
  _show_progress = show_progress;

  _thread = base::create_thread(&CopyDataTask::thread_func, this);
}

gpointer CopyDataTask::thread_func(gpointer data)
{
  CopyDataTask* self = (CopyDataTask*)data;

  TableParam tparam;

  while (self->_tasks->get_task(tparam))
  {
    self->copy_table(tparam);
  }

  return NULL;
}

void CopyDataTask::copy_table(const TableParam &task)
{
  boost::shared_ptr<std::vector<ColumnInfo> > columns;

  long long i = 0, total = 0;
  int inserted_records;

  time_t start = time(NULL);
  try
  {
    std::vector<std::string> last_pkeys;
    if (task.copy_spec.resume)
      last_pkeys = _target->get_last_pkeys(task.target_pk_columns, task.target_schema, task.target_table);
    total = _source->count_rows(task.source_schema, task.source_table, task.source_pk_columns, task.copy_spec, last_pkeys);
    columns = _source->begin_select_table(task.source_schema, task.source_table, task.source_pk_columns, task.select_expression, task.copy_spec, last_pkeys);

    printf("BEGIN:%s.%s:Copying %li columns of %lli rows from table %s.%s\n",
           task.target_schema.c_str(), task.target_table.c_str(),
           (long)columns->size(), total,
           task.source_schema.c_str(), task.source_table.c_str());
    fflush(stdout);

    _target->set_get_field_lengths_from_target(_source->get_get_field_lengths_from_target());

    _target->set_target_table(task.target_schema, task.target_table, columns);

    _source->set_bulk_inserts(_target->bulk_inserts());

    _target->begin_inserts();
    while (_source->fetch_row(_target->row_buffer()))
    {
      inserted_records = _target->do_insert();
      i += inserted_records;

      if (_show_progress && inserted_records)
        report_progress(task.target_schema, task.target_table, i, total);

      _target->row_buffer().clear();

      if ((task.copy_spec.type == CopyCount && i >= task.copy_spec.row_count) ||
          (task.copy_spec.max_count > 0 && i >= task.copy_spec.max_count))
        break;
    }

    inserted_records = _target->end_inserts();
    i += inserted_records;

    if (_show_progress && inserted_records)
    {
      report_progress(task.target_schema, task.target_table, i, total);
    }

    _source->end_select_table();
  }
  catch (std::exception &e)
  {
    printf("ERROR:%s.%s:%s\n",
           task.target_schema.c_str(), task.target_table.c_str(), e.what());
    fflush(stdout);
    _target->end_inserts(false);
    _source->end_select_table();
  }

  time_t end = time(NULL);
  if (i != total)
    printf("ERROR:%s.%s:Failed copying %lli rows\n",
               task.target_schema.c_str(), task.target_table.c_str(), total-i);
  else
    printf("END:%s.%s:Finished copying %lli rows in %im%02is\n",
           task.target_schema.c_str(), task.target_table.c_str(), i,
           (int)((end-start) / 60), (int)((end-start) % 60));
  fflush(stdout);
}

void CopyDataTask::report_progress(const std::string &schema, const std::string &table, long long current, long long total)
{
  printf("PROGRESS:%s.%s:%lli:%lli\n", schema.c_str(), table.c_str(), current, total);
  fflush(stdout);
}


CopyDataTask::~CopyDataTask()
{
}

void MySQLCopyDataTarget::InsertBuffer::reset(size_t size)
{
  length = 0;
  last_insert_length = 0;

  if (buffer)
  {
    if (size == this->size)
      return;
    free(buffer);
  }
  this->size = size;
  buffer = (char*)malloc(size);
  if (!buffer)
    throw std::runtime_error(base::strfmt("Not enough memory to allocate insert buffer of size %li", (long)size));
}

void MySQLCopyDataTarget::InsertBuffer::end_insert()
{
  last_insert_length = length;
}

bool MySQLCopyDataTarget::InsertBuffer::append(const char *data, size_t dlength)
{
  if (dlength > space_left())
    return false;
  memcpy(buffer+length, data, dlength);
  length += dlength;
  return true;
}

bool MySQLCopyDataTarget::InsertBuffer::append(const char *data)
{
  return append(data, strlen(data));
}

bool MySQLCopyDataTarget::InsertBuffer::append_escaped(const char *data, size_t dlength)
{
  // We need to check for the worst case scenario where all the
  // characters are escaped
  if ((dlength * 2) > space_left())
    return false;

  // This function is used to create a legal SQL string that you can use in an SQL statement
  // This is needed because the escaping depends on the character set in use by the server
  #if defined(MYSQL_VERSION_MAJOR) && defined(MYSQL_VERSION_MINOR) && defined(MYSQL_VERSION_PATCH)
  #if MYSQL_CHECK_VERSION(5,7,6)
    if (is_mysql_version_at_least(5,7,6))
      length += mysql_real_escape_string_quote(_mysql, buffer + length, data, (unsigned long)dlength);
    else
      length += mysql_real_escape_string(_mysql, buffer + length, data, (unsigned long)dlength);
  #else
    length += mysql_real_escape_string(_mysql, buffer + length, data, (unsigned long)dlength);
  #endif
  #endif

  return true;
}

size_t MySQLCopyDataTarget::InsertBuffer::space_left()
{
  return size - length;
}
