"use strict";
const util = require('util');
const events = require('events');
const types = require('../types');
const utils = require('../utils');
const errors = require('../errors');
const TableMetadata = require('./table-metadata');
const Aggregate = require('./aggregate');
const SchemaFunction = require('./schema-function');
const Index = require('./schema-index');
const MaterializedView = require('./materialized-view');
/**
 * @module metadata/schemaParser
 * @ignore
 */

const _selectAllKeyspacesV1 = "SELECT * FROM system.schema_keyspaces";
const _selectSingleKeyspaceV1 = "SELECT * FROM system.schema_keyspaces where keyspace_name = '%s'";
const _selectAllKeyspacesV2 = "SELECT * FROM system_schema.keyspaces";
const _selectSingleKeyspaceV2 = "SELECT * FROM system_schema.keyspaces where keyspace_name = '%s'";
const _selectTableV1 = "SELECT * FROM system.schema_columnfamilies WHERE keyspace_name='%s' AND columnfamily_name='%s'";
const _selectTableV2 = "SELECT * FROM system_schema.tables WHERE keyspace_name='%s' AND table_name='%s'";
const _selectColumnsV1 = "SELECT * FROM system.schema_columns WHERE keyspace_name='%s' AND columnfamily_name='%s'";
const _selectColumnsV2 = "SELECT * FROM system_schema.columns WHERE keyspace_name='%s' AND table_name='%s'";
const _selectIndexesV2 = "SELECT * FROM system_schema.indexes WHERE keyspace_name='%s' AND table_name='%s'";
const _selectUdtV1 = "SELECT * FROM system.schema_usertypes WHERE keyspace_name='%s' AND type_name='%s'";
const _selectUdtV2 = "SELECT * FROM system_schema.types WHERE keyspace_name='%s' AND type_name='%s'";
const _selectFunctionsV1 = "SELECT * FROM system.schema_functions WHERE keyspace_name = '%s' AND function_name = '%s'";
const _selectFunctionsV2 = "SELECT * FROM system_schema.functions WHERE keyspace_name = '%s' AND function_name = '%s'";
const _selectAggregatesV1 = "SELECT * FROM system.schema_aggregates WHERE keyspace_name = '%s' AND aggregate_name = '%s'";
const _selectAggregatesV2 = "SELECT * FROM system_schema.aggregates WHERE keyspace_name = '%s' AND aggregate_name = '%s'";
const _selectMaterializedViewV2 = "SELECT * FROM system_schema.views WHERE keyspace_name = '%s' AND view_name = '%s'";

const _selectAllVirtualKeyspaces = "SELECT * FROM system_virtual_schema.keyspaces";
const _selectSingleVirtualKeyspace = "SELECT * FROM system_virtual_schema.keyspaces where keyspace_name = '%s'";
const _selectVirtualTable = "SELECT * FROM system_virtual_schema.tables where keyspace_name = '%s' and table_name='%s'";
const _selectVirtualColumns = "SELECT * FROM system_virtual_schema.columns where keyspace_name = '%s' and table_name='%s'";

/**
 * @abstract
 * @param {ClientOptions} options The client options
 * @param {ControlConnection} cc
 * @constructor
 * @ignore
 */
function SchemaParser(options, cc) {
  this.cc = cc;
  this.encodingOptions = options.encoding;
  this.selectTable = null;
  this.selectColumns = null;
  this.selectIndexes = null;
  this.selectUdt = null;
  this.selectAggregates = null;
  this.selectFunctions = null;
  this.supportsVirtual = false;
}

/**
 * @param name
 * @param durableWrites
 * @param strategy
 * @param strategyOptions
 * @param virtual
 * @returns {{name, durableWrites, strategy, strategyOptions, tokenToReplica, udts, tables, functions, aggregates}|null}
 * @protected
 */
SchemaParser.prototype._createKeyspace = function (name, durableWrites, strategy, strategyOptions, virtual) {
  const ksInfo = {
    name: name,
    durableWrites: durableWrites,
    strategy: strategy,
    strategyOptions: strategyOptions,
    tokenToReplica: null,
    virtual: virtual === true,
    udts: {},
    tables: {},
    functions: {},
    aggregates: {},
    views: {}
  };
  ksInfo.tokenToReplica = getTokenToReplicaMapper(strategy, strategyOptions);
  return ksInfo;
};

/**
 * @abstract
 * @param {String} name
 * @param {Function} callback
 */
SchemaParser.prototype.getKeyspace = function (name, callback) {
};

/**
 * @abstract
 * @param {Boolean} waitReconnect
 * @param {Function} callback
 */
SchemaParser.prototype.getKeyspaces = function (waitReconnect, callback) {
};

/**
 * @param {String} keyspaceName
 * @param {String} name
 * @param {Object} cache
 * @param {Boolean} virtual
 * @param {Function} callback
 */
SchemaParser.prototype.getTable = function (keyspaceName, name, cache, virtual, callback) {
  let tableInfo = cache && cache[name];
  if (!tableInfo) {
    tableInfo = new TableMetadata(name);
    if (cache) {
      cache[name] = tableInfo;
    }
  }
  if (tableInfo.loaded) {
    return callback(null, tableInfo);
  }
  tableInfo.once('load', callback);
  if (tableInfo.loading) {
    //It' already queued, it will be emitted
    return;
  }
  // its not cached and not being retrieved
  tableInfo.loading = true;
  let tableRow, columnRows, indexRows;
  const self = this;
  let virtualTable = virtual;
  utils.series([
    function getTableRow(next) {
      const selectTable = virtualTable ? _selectVirtualTable : self.selectTable;
      const query = util.format(selectTable, keyspaceName, name);
      self.cc.query(query, function (err, response) {
        if (err) {
          return next(err);
        }
        tableRow = response.rows[0];
        next();
      });
    },
    function getVirtualTableRow(next) {
      // if we weren't sure if table was virtual or not, query virtual schema.
      if (!tableRow && self.supportsVirtual && virtualTable === undefined) {
        const query = util.format(_selectVirtualTable, keyspaceName, name);
        self.cc.query(query, function (err, response) {
          if (err) {
            // we can't error here as we can't be sure if the node
            // supports virtual tables, in this case it is adequate
            // to act as if there was no matching table.
            return next();
          }
          tableRow = response.rows[0];
          // if we got a result, this is a virtual table
          if (tableRow) {
            virtualTable = true;
          }
          next();
        });
      } else {
        return next();
      }
    },
    function getColumnRows (next) {
      if (!tableRow) {
        return next();
      }
      const selectColumns = virtualTable ? _selectVirtualColumns : self.selectColumns;
      const query = util.format(selectColumns, keyspaceName, name);
      self.cc.query(query, function (err, response) {
        if (err) {
          return next(err);
        }
        columnRows = response.rows;
        next();
      });
    },
    function getIndexes(next) {
      if (!tableRow || !self.selectIndexes || virtualTable) {
        //either the table does not exists or it does not support indexes schema table
        return next();
      }
      const query = util.format(self.selectIndexes, keyspaceName, name);
      self.cc.query(query, function (err, response) {
        if (err) {
          return next(err);
        }
        indexRows = response.rows;
        next();
      });
    }
  ], function afterQuery (err) {
    if (err || !tableRow) {
      tableInfo.loading = false;
      return tableInfo.emit('load', err, null);
    }
    self._parseTableOrView(tableInfo, tableRow, columnRows, indexRows, virtualTable, function (err) {
      tableInfo.loading = false;
      tableInfo.loaded = !err;
      tableInfo.emit('load', err, tableInfo);
    });
  });
};

/**
 * @param {String} keyspaceName
 * @param {String} name
 * @param {Object} cache
 * @param {Function} callback
 */
SchemaParser.prototype.getUdt = function (keyspaceName, name, cache, callback) {
  let udtInfo = cache && cache[name];
  if (!udtInfo) {
    udtInfo = new events.EventEmitter();
    if (cache) {
      cache[name] = udtInfo;
    }
    udtInfo.setMaxListeners(0);
    udtInfo.loading = false;
    udtInfo.name = name;
    udtInfo.fields = null;
  }
  if (udtInfo.fields) {
    return callback(null, udtInfo);
  }
  udtInfo.once('load', callback);
  if (udtInfo.loading) {
    //It' already queued, it will be emitted
    return;
  }
  udtInfo.loading = true;
  //it is not cached, try to query for it
  const query = util.format(this.selectUdt, keyspaceName, name);
  const self = this;
  this.cc.query(query, function (err, response) {
    if (err) {
      return udtInfo.emit('load', err);
    }
    const row = response.rows[0];
    if (!row) {
      udtInfo.loading = false;
      return udtInfo.emit('load', null, null);
    }
    self._parseUdt(udtInfo, row, function (err) {
      udtInfo.loading = false;
      if (err) {
        return udtInfo.emit('load', err);
      }
      return udtInfo.emit('load', null, udtInfo);
    });
  });
};

/**
 * Parses the udt information from the row
 * @param udtInfo
 * @param {Row} row
 * @param {Function} callback Callback to be invoked with the err and {{fields: Array}}|null
 * @abstract
 */
SchemaParser.prototype._parseUdt = function (udtInfo, row, callback) {
};

/**
 * Builds the metadata based on the table and column rows
 * @abstract
 * @param {module:metadata~TableMetadata} tableInfo
 * @param {Row} tableRow
 * @param {Array.<Row>} columnRows
 * @param {Array.<Row>} indexRows
 * @param {Boolean} virtual
 * @param {Function} callback
 * @throws {Error}
 */
SchemaParser.prototype._parseTableOrView = function (tableInfo, tableRow, columnRows, indexRows, virtual, callback) {
};

/**
 * @abstract
 * @param {String} keyspaceName
 * @param {String} name
 * @param {Object} cache
 * @param {Function} callback
 */
SchemaParser.prototype.getMaterializedView = function (keyspaceName, name, cache, callback) {

};

/**
 * @param {String} keyspaceName
 * @param {String} name
 * @param {Boolean} aggregate
 * @param {Object} cache
 * @param {Function} callback
 */
SchemaParser.prototype.getFunctions = function (keyspaceName, name, aggregate, cache, callback) {
  /** @type {String} */
  let query = this.selectFunctions;
  let parser = this._parseFunction.bind(this);
  if (aggregate) {
    query = this.selectAggregates;
    parser = this._parseAggregate.bind(this);
  }
  //if not already loaded
  //get all functions with that name
  //cache it by name and, within name, by signature
  let functionsInfo = cache && cache[name];
  if (!functionsInfo) {
    functionsInfo = new events.EventEmitter();
    if (cache) {
      cache[name] = functionsInfo;
    }
    functionsInfo.setMaxListeners(0);
  }
  if (functionsInfo.values) {
    return callback(null, functionsInfo.values);
  }
  functionsInfo.once('load', callback);
  if (functionsInfo.loading) {
    //It' already queued, it will be emitted
    return;
  }
  functionsInfo.loading = true;
  //it is not cached, try to query for it
  query = util.format(query, keyspaceName, name);
  this.cc.query(query, function (err, response) {
    functionsInfo.loading = false;
    if (err || response.rows.length === 0) {
      return functionsInfo.emit('load', err, null);
    }
    if (response.rows.length > 0) {
      functionsInfo.values = {};
    }
    utils.each(response.rows, function (row, next) {
      parser(row, function (err, func) {
        if (err) {
          return next(err);
        }
        functionsInfo.values['(' + func.signature.join(',') + ')'] = func;
        next();
      });
    }, function (err) {
      if (err) {
        return functionsInfo.emit('load', err);
      }
      functionsInfo.emit('load', null, functionsInfo.values);
    });
  });
};

/**
 * @abstract
 * @param {Row} row
 * @param {Function} callback
 */
SchemaParser.prototype._parseAggregate = function (row, callback) {
};

/**
 * @abstract
 * @param {Row} row
 * @param {Function} callback
 */
SchemaParser.prototype._parseFunction = function (row, callback) {
};

/** @returns {Map} */
SchemaParser.prototype._asMap = function (obj) {
  if (!obj) {
    return new Map();
  }

  if (this.encodingOptions.map && obj instanceof this.encodingOptions.map) {
    // Its already a Map or a polyfill of a Map
    return obj;
  }

  return new Map(Object.keys(obj).map(k => [ k, obj[k]]));
};

SchemaParser.prototype._mapAsObject = function (map) {
  if (!map) {
    return map;
  }

  if (this.encodingOptions.map && map instanceof this.encodingOptions.map) {
    const result = {};
    map.forEach((value, key) => result[key] = value);
    return result;
  }

  return map;
};

/**
 * Used to parse schema information for Cassandra versions 1.2.x, and 2.x
 * @param {ClientOptions} options The client options
 * @param {ControlConnection} cc
 * @constructor
 * @ignore
 */
function SchemaParserV1(options, cc) {
  SchemaParser.call(this, options, cc);
  this.selectTable = _selectTableV1;
  this.selectColumns = _selectColumnsV1;
  this.selectUdt = _selectUdtV1;
  this.selectAggregates = _selectAggregatesV1;
  this.selectFunctions = _selectFunctionsV1;
}

util.inherits(SchemaParserV1, SchemaParser);

/** @override */
SchemaParserV1.prototype.getKeyspaces = function (waitReconnect, callback) {
  const self = this;
  const keyspaces = {};
  this.cc.query(_selectAllKeyspacesV1, waitReconnect, function (err, result) {
    if (err) {
      return callback(err);
    }
    for (let i = 0; i < result.rows.length; i++) {
      const row = result.rows[i];
      const ksInfo = self._createKeyspace(
        row['keyspace_name'],
        row['durable_writes'],
        row['strategy_class'],
        JSON.parse(row['strategy_options'] || null));
      keyspaces[ksInfo.name] = ksInfo;
    }
    callback(null, keyspaces);
  });
};

/** @override */
SchemaParserV1.prototype.getKeyspace = function (name, callback) {
  const self = this;
  this.cc.query(util.format(_selectSingleKeyspaceV1, name), function (err, result) {
    if (err) {
      return callback(err);
    }
    const row = result.rows[0];
    if (!row) {
      return callback(null, null);
    }
    callback(null, self._createKeyspace(
      row['keyspace_name'],
      row['durable_writes'],
      row['strategy_class'],
      JSON.parse(row['strategy_options'])));
  });
};

/** @override */
SchemaParserV1.prototype._parseTableOrView = function (tableInfo, tableRow, columnRows, indexRows, virtual, callback) {
  let i, c, name, types;
  const encoder = this.cc.getEncoder();
  const columnsKeyed = {};
  let partitionKeys = [];
  let clusteringKeys = [];
  tableInfo.bloomFilterFalsePositiveChance = tableRow['bloom_filter_fp_chance'];
  tableInfo.caching = tableRow['caching'];
  tableInfo.comment = tableRow['comment'];
  tableInfo.compactionClass = tableRow['compaction_strategy_class'];
  tableInfo.compactionOptions = JSON.parse(tableRow['compaction_strategy_options']);
  tableInfo.compression = JSON.parse(tableRow['compression_parameters']);
  tableInfo.gcGraceSeconds = tableRow['gc_grace_seconds'];
  tableInfo.localReadRepairChance = tableRow['local_read_repair_chance'];
  tableInfo.readRepairChance = tableRow['read_repair_chance'];
  tableInfo.populateCacheOnFlush = tableRow['populate_io_cache_on_flush'] || tableInfo.populateCacheOnFlush;
  tableInfo.memtableFlushPeriod = tableRow['memtable_flush_period_in_ms'] || tableInfo.memtableFlushPeriod;
  tableInfo.defaultTtl = tableRow['default_time_to_live'] || tableInfo.defaultTtl;
  tableInfo.speculativeRetry = tableRow['speculative_retry'] || tableInfo.speculativeRetry;
  tableInfo.indexInterval = tableRow['index_interval'] || tableInfo.indexInterval;
  if (typeof tableRow['min_index_interval'] !== 'undefined') {
    //Cassandra 2.1+
    tableInfo.minIndexInterval = tableRow['min_index_interval'] || tableInfo.minIndexInterval;
    tableInfo.maxIndexInterval = tableRow['max_index_interval'] || tableInfo.maxIndexInterval;
  }
  else {
    //set to null
    tableInfo.minIndexInterval = null;
    tableInfo.maxIndexInterval = null;
  }
  if (typeof tableRow['replicate_on_write'] !== 'undefined') {
    //leave the default otherwise
    tableInfo.replicateOnWrite = tableRow['replicate_on_write'];
  }
  tableInfo.columns = [];
  try {
    (function parseColumns() {
      //function context
      for (i = 0; i < columnRows.length; i++) {
        const row = columnRows[i];
        const type = encoder.parseFqTypeName(row['validator']);
        c = {
          name: row['column_name'],
          type: type
        };
        tableInfo.columns.push(c);
        columnsKeyed[c.name] = c;
        switch (row['type']) {
          case 'partition_key':
            partitionKeys.push({c: c, index: (row['component_index'] || 0)});
            break;
          case 'clustering_key':
            clusteringKeys.push({
              c: c,
              index: (row['component_index'] || 0),
              order: c.type.options.reversed ? 'DESC' : 'ASC'
            });
            break;
        }
      }
    })();
    if (partitionKeys.length > 0) {
      tableInfo.partitionKeys = partitionKeys.sort(utils.propCompare('index')).map(function (item) {
        return item.c;
      });
      clusteringKeys.sort(utils.propCompare('index'));
      tableInfo.clusteringKeys = clusteringKeys.map(function (item) {
        return item.c;
      });
      tableInfo.clusteringOrder = clusteringKeys.map(function (item) {
        return item.order;
      });
    }
    //In C* 1.2, keys are not stored on the schema_columns table
    const keysStoredInTableRow = (tableInfo.partitionKeys.length === 0);
    if (keysStoredInTableRow && tableRow['key_aliases']) {
      //In C* 1.2, keys are not stored on the schema_columns table
      partitionKeys = JSON.parse(tableRow['key_aliases']);
      types = encoder.parseKeyTypes(tableRow['key_validator']).types;
      for (i = 0; i < partitionKeys.length; i++) {
        name = partitionKeys[i];
        c = columnsKeyed[name];
        if (!c) {
          c = {
            name: name,
            type: types[i]
          };
          tableInfo.columns.push(c);
        }
        tableInfo.partitionKeys.push(c);
      }
    }
    const comparator = encoder.parseKeyTypes(tableRow['comparator']);
    if (keysStoredInTableRow && tableRow['column_aliases']) {
      clusteringKeys = JSON.parse(tableRow['column_aliases']);
      for (i = 0; i < clusteringKeys.length; i++) {
        name = clusteringKeys[i];
        c = columnsKeyed[name];
        if (!c) {
          c = {
            name: name,
            type: comparator.types[i]
          };
          tableInfo.columns.push(c);
        }
        tableInfo.clusteringKeys.push(c);
        tableInfo.clusteringOrder.push(c.type.options.reversed ? 'DESC' : 'ASC');
      }
    }
    tableInfo.isCompact = !!tableRow['is_dense'];
    if (!tableInfo.isCompact) {
      //is_dense column does not exist in previous versions of Cassandra
      //also, compact pk, ck and val appear as is_dense false
      // clusteringKeys != comparator types - 1
      // or not composite (comparator)
      tableInfo.isCompact = (
        //clustering keys are not marked as composite
        !comparator.isComposite ||
        //only 1 column not part of the partition or clustering keys
        (!comparator.hasCollections && tableInfo.clusteringKeys.length !== comparator.types.length - 1)
      );
    }
    name = tableRow['value_alias'];
    if (tableInfo.isCompact && name && !columnsKeyed[name]) {
      //additional column in C* 1.2 as value_alias
      c = {
        name: name,
        type: encoder.parseFqTypeName(tableRow['default_validator'])
      };
      tableInfo.columns.push(c);
      columnsKeyed[name] = c;
    }
    tableInfo.columnsByName = columnsKeyed;
    tableInfo.indexes = Index.fromColumnRows(columnRows, tableInfo.columnsByName);
  }
  catch (err) {
    return callback(err);
  }
  //All the tableInfo parsing in V1 is sync, it uses a callback because the super defines one
  //to support other versions.
  callback();
};

/** @override */
SchemaParserV1.prototype.getMaterializedView = function (keyspaceName, name, cache, callback) {
  callback(new errors.NotSupportedError('Materialized views are not supported on Cassandra versions below 3.0'));
};

/** @override */
SchemaParserV1.prototype._parseAggregate = function (row, callback) {
  const encoder = this.cc.getEncoder();
  const aggregate = new Aggregate();
  aggregate.name = row['aggregate_name'];
  aggregate.keyspaceName = row['keyspace_name'];
  aggregate.signature = row['signature'] || utils.emptyArray;
  aggregate.stateFunction = row['state_func'];
  aggregate.finalFunction = row['final_func'];
  aggregate.initConditionRaw = row['initcond'];
  try {
    aggregate.argumentTypes = (row['argument_types'] || utils.emptyArray).map(function (name) {
      return encoder.parseFqTypeName(name);
    });
    aggregate.stateType = encoder.parseFqTypeName(row['state_type']);
    const initConditionValue = encoder.decode(aggregate.initConditionRaw, aggregate.stateType);
    if (initConditionValue !== null && typeof initConditionValue !== 'undefined') {
      aggregate.initCondition = initConditionValue.toString();
    }
    aggregate.returnType = encoder.parseFqTypeName(row['return_type']);
  }
  catch (err) {
    return callback(err);
  }
  callback(null, aggregate);
};

/** @override */
SchemaParserV1.prototype._parseFunction = function (row, callback) {
  const encoder = this.cc.getEncoder();
  const func = new SchemaFunction();
  func.name = row['function_name'];
  func.keyspaceName = row['keyspace_name'];
  func.signature = row['signature'] || utils.emptyArray;
  func.argumentNames = row['argument_names'] || utils.emptyArray;
  func.body = row['body'];
  func.calledOnNullInput = row['called_on_null_input'];
  func.language = row['language'];
  try {
    func.argumentTypes = (row['argument_types'] || utils.emptyArray).map(function (name) {
      return encoder.parseFqTypeName(name);
    });
    func.returnType = encoder.parseFqTypeName(row['return_type']);
  }
  catch (err) {
    return callback(err);
  }
  callback(null, func);
};

/** @override */
SchemaParserV1.prototype._parseUdt = function (udtInfo, row, callback) {
  const encoder = this.cc.getEncoder();
  const fieldNames = row['field_names'];
  const fieldTypes = row['field_types'];
  const fields = new Array(fieldNames.length);
  try {
    for (let i = 0; i < fieldNames.length; i++) {
      fields[i] = {
        name: fieldNames[i],
        type: encoder.parseFqTypeName(fieldTypes[i])
      };
    }
  }
  catch (err) {
    return callback(err);
  }
  udtInfo.fields = fields;
  callback(null, udtInfo);
};

/**
 * Used to parse schema information for Cassandra versions 3.x and above
 * @param {ClientOptions} options The client options
 * @param {ControlConnection} cc The control connection to be used
 * @param {Function} udtResolver The function to be used to retrieve the udts.
 * @constructor
 * @ignore
 */
function SchemaParserV2(options, cc, udtResolver) {
  SchemaParser.call(this, options, cc);
  this.udtResolver = udtResolver;
  this.selectTable = _selectTableV2;
  this.selectColumns = _selectColumnsV2;
  this.selectUdt = _selectUdtV2;
  this.selectAggregates = _selectAggregatesV2;
  this.selectFunctions = _selectFunctionsV2;
  this.selectIndexes = _selectIndexesV2;
}

util.inherits(SchemaParserV2, SchemaParser);

/** @override */
SchemaParserV2.prototype.getKeyspaces = function (waitReconnect, callback) {
  const self = this;
  const keyspaces = {};
  this.cc.query(_selectAllKeyspacesV2, waitReconnect, function (err, result) {
    if (err) {
      return callback(err);
    }
    for (let i = 0; i < result.rows.length; i++) {
      const ksInfo = self._parseKeyspace(result.rows[i]);
      keyspaces[ksInfo.name] = ksInfo;
    }
    callback(null, keyspaces);
  });
};

/** @override */
SchemaParserV2.prototype.getKeyspace = function (name, callback) {
  const self = this;
  this.cc.query(util.format(_selectSingleKeyspaceV2, name), function (err, result) {
    if (err) {
      return callback(err);
    }
    const row = result.rows[0];
    if (!row) {
      return callback(null, null);
    }
    callback(null, self._parseKeyspace(row));
  });
};

/** @override */
SchemaParserV2.prototype.getMaterializedView = function (keyspaceName, name, cache, callback) {
  let viewInfo = cache && cache[name];
  if (!viewInfo) {
    viewInfo = new MaterializedView(name);
    if (cache) {
      cache[name] = viewInfo;
    }
  }
  if (viewInfo.loaded) {
    return callback(null, viewInfo);
  }
  viewInfo.once('load', callback);
  if (viewInfo.loading) {
    //It' already queued, it will be emitted
    return;
  }
  viewInfo.loading = true;
  let tableRow, columnRows;
  //it is not cached, try to query for it
  const self = this;
  utils.series([
    function getTableRow(next) {
      const query = util.format(_selectMaterializedViewV2, keyspaceName, name);
      self.cc.query(query, function (err, response) {
        if (err) {
          return next(err);
        }
        tableRow = response.rows[0];
        next();
      });
    },
    function getColumnRows (next) {
      if (!tableRow) {
        return next();
      }
      const query = util.format(self.selectColumns, keyspaceName, name);
      self.cc.query(query, function (err, response) {
        if (err) {
          return next(err);
        }
        columnRows = response.rows;
        next();
      });
    }
  ], function afterQuery (err) {
    viewInfo.loading = false;
    if (err || !tableRow) {
      return viewInfo.emit('load', err, null);
    }
    self._parseTableOrView(viewInfo, tableRow, columnRows, null, false, function (err) {
      viewInfo.loading = false;
      viewInfo.loaded = !err;
      viewInfo.emit('load', err, viewInfo);
    });
  });

};

SchemaParserV2.prototype._parseKeyspace = function (row, virtual) {
  const replication = row['replication'];
  let strategy;
  let strategyOptions;
  if (replication) {
    strategy = replication['class'];
    strategyOptions = {};
    for (const key in replication) {
      if (!replication.hasOwnProperty(key) || key === 'class') {
        continue;
      }
      strategyOptions[key] = replication[key];
    }
  }
  return this._createKeyspace(
    row['keyspace_name'],
    row['durable_writes'],
    strategy,
    strategyOptions,
    virtual);
};

/** @override */
SchemaParserV2.prototype._parseTableOrView = function (tableInfo, tableRow, columnRows, indexRows, virtual, callback) {
  const encoder = this.cc.getEncoder();
  const columnsKeyed = {};
  const partitionKeys = [];
  const clusteringKeys = [];
  const self = this;

  // maps column rows to columnInfo and also populates columnsKeyed
  const columnRowMapper = (row, next) => {
    encoder.parseTypeName(tableRow['keyspace_name'], row['type'], 0, null, self.udtResolver, function (err, type) {
      if (err) {
        return next(err);
      }
      const c = {
        name: row['column_name'],
        type: type,
        isStatic: false
      };
      columnsKeyed[c.name] = c;
      switch (row['kind']) {
        case 'partition_key':
          partitionKeys.push({ c: c, index: (row['position'] || 0)});
          break;
        case 'clustering':
          clusteringKeys.push({ c: c, index: (row['position'] || 0), order: row['clustering_order'] === 'desc' ? 'DESC' : 'ASC'});
          break;
        case 'static':
          c.isStatic = true;
          break;
      }
      next(null, c);
    });
  };

  // is table is virtual, the only relevant information to parse is the columns as the table itself has no configuration.
  if (virtual) {
    tableInfo.virtual = true;
    utils.map(columnRows, columnRowMapper, function (err, columns) {
      if (err) {
        return callback(err);
      }
      tableInfo.columns = columns;
      tableInfo.columnsByName = columnsKeyed;
      tableInfo.partitionKeys = partitionKeys.sort(utils.propCompare('index')).map(function (item) {
        return item.c;
      });
      clusteringKeys.sort(utils.propCompare('index'));
      tableInfo.clusteringKeys = clusteringKeys.map(function (item) {
        return item.c;
      });
      tableInfo.clusteringOrder = clusteringKeys.map(function (item) {
        return item.order;
      });
      callback();
    });
    return;
  }

  const isView = tableInfo instanceof MaterializedView;
  tableInfo.bloomFilterFalsePositiveChance = tableRow['bloom_filter_fp_chance'];
  tableInfo.caching = JSON.stringify(tableRow['caching']);
  tableInfo.comment = tableRow['comment'];

  // Regardless of the encoding options, use always an Object to represent an associative Array
  const compaction = this._asMap(tableRow['compaction']);
  if (compaction) {
    // compactionOptions as an Object<String, String>
    tableInfo.compactionOptions = {};
    tableInfo.compactionClass = compaction.get('class');
    compaction.forEach((value, key) => {
      if (key === 'class') {
        return;
      }
      tableInfo.compactionOptions[key] = compaction.get(key);
    });
  }

  // Convert compression to an Object<String, String>
  tableInfo.compression = this._mapAsObject(tableRow['compression']);

  tableInfo.gcGraceSeconds = tableRow['gc_grace_seconds'];
  tableInfo.localReadRepairChance = tableRow['dclocal_read_repair_chance'];
  tableInfo.readRepairChance = tableRow['read_repair_chance'];
  tableInfo.extensions = this._mapAsObject(tableRow['extensions']);
  tableInfo.crcCheckChance = tableRow['crc_check_chance'];
  tableInfo.memtableFlushPeriod = tableRow['memtable_flush_period_in_ms'] || tableInfo.memtableFlushPeriod;
  tableInfo.defaultTtl = tableRow['default_time_to_live'] || tableInfo.defaultTtl;
  tableInfo.speculativeRetry = tableRow['speculative_retry'] || tableInfo.speculativeRetry;
  tableInfo.minIndexInterval = tableRow['min_index_interval'] || tableInfo.minIndexInterval;
  tableInfo.maxIndexInterval = tableRow['max_index_interval'] || tableInfo.maxIndexInterval;
  if (!isView) {
    const cdc = tableRow['cdc'];
    if (cdc !== undefined) {
      tableInfo.cdc = cdc;
    }
  }

  // Map all columns asynchronously
  utils.map(columnRows, columnRowMapper, (err, columns) => {
    if (err) {
      return callback(err);
    }
    tableInfo.columns = columns;
    tableInfo.columnsByName = columnsKeyed;
    tableInfo.partitionKeys = partitionKeys.sort(utils.propCompare('index')).map(function (item) {
      return item.c;
    });
    clusteringKeys.sort(utils.propCompare('index'));
    tableInfo.clusteringKeys = clusteringKeys.map(function (item) {
      return item.c;
    });
    tableInfo.clusteringOrder = clusteringKeys.map(function (item) {
      return item.order;
    });

    if (isView) {
      tableInfo.tableName = tableRow['base_table_name'];
      tableInfo.whereClause = tableRow['where_clause'];
      tableInfo.includeAllColumns = tableRow['include_all_columns'];
      return callback();
    }

    tableInfo.indexes = this._getIndexes(indexRows);

    // flags can be an instance of Array or Set (real or polyfill)
    let flags = tableRow['flags'];
    if (Array.isArray(flags)) {
      flags = new Set(flags);
    }

    const isDense = flags.has('dense');
    const isSuper = flags.has('super');
    const isCompound = flags.has('compound');

    tableInfo.isCompact = isSuper || isDense || !isCompound;
    //remove the columns related to Thrift
    const isStaticCompact = !isSuper && !isDense && !isCompound;
    if(isStaticCompact) {
      pruneStaticCompactTableColumns(tableInfo);
    }
    else if (isDense) {
      pruneDenseTableColumns(tableInfo);
    }
    callback();
  });
};

SchemaParserV2.prototype._getIndexes = function (indexRows) {
  if (!indexRows || indexRows.length === 0) {
    return utils.emptyArray;
  }

  return indexRows.map((row) => {
    const options = this._mapAsObject(row['options']);
    return new Index(row['index_name'], options['target'], row['kind'], options);
  });
};

/** @override */
SchemaParserV2.prototype._parseAggregate = function (row, callback) {
  const encoder = this.cc.getEncoder();
  const aggregate = new Aggregate();
  aggregate.name = row['aggregate_name'];
  aggregate.keyspaceName = row['keyspace_name'];
  aggregate.signature = row['argument_types'] || utils.emptyArray;
  aggregate.stateFunction = row['state_func'];
  aggregate.finalFunction = row['final_func'];
  aggregate.initConditionRaw = row['initcond'];
  aggregate.initCondition = aggregate.initConditionRaw;
  const self = this;
  utils.series([
    function parseArguments(next) {
      utils.map(row['argument_types'] || utils.emptyArray, function (name, mapNext) {
        encoder.parseTypeName(row['keyspace_name'], name, 0, null, self.udtResolver, mapNext);
      }, function (err, result) {
        aggregate.argumentTypes = result;
        next(err);
      });
    },
    function parseStateType(next) {
      encoder.parseTypeName(row['keyspace_name'], row['state_type'], 0, null, self.udtResolver, function (err, type) {
        aggregate.stateType = type;
        next(err);
      });
    },
    function parseReturnType(next) {
      encoder.parseTypeName(row['keyspace_name'], row['return_type'], 0, null, self.udtResolver, function (err, type) {
        aggregate.returnType = type;
        next(err);
      });
    }
  ], function (err) {
    if (err) {
      return callback(err);
    }
    callback(null, aggregate);
  });
};

/** @override */
SchemaParserV2.prototype._parseFunction = function (row, callback) {
  const encoder = this.cc.getEncoder();
  const func = new SchemaFunction();
  func.name = row['function_name'];
  func.keyspaceName = row['keyspace_name'];
  func.signature = row['argument_types'] || utils.emptyArray;
  func.argumentNames = row['argument_names'] || utils.emptyArray;
  func.body = row['body'];
  func.calledOnNullInput = row['called_on_null_input'];
  func.language = row['language'];
  const self = this;
  utils.series([
    function parseArguments(next) {
      utils.map(row['argument_types'] || utils.emptyArray, function (name, mapNext) {
        encoder.parseTypeName(row['keyspace_name'], name, 0, null, self.udtResolver, mapNext);
      }, function (err, result) {
        func.argumentTypes = result;
        next(err);
      });
    },
    function parseReturnType(next) {
      encoder.parseTypeName(row['keyspace_name'], row['return_type'], 0, null, self.udtResolver, function (err, type) {
        func.returnType = type;
        next(err);
      });
    }
  ], function (err) {
    if (err) {
      return callback(err);
    }
    callback(null, func);
  });
};

/** @override */
SchemaParserV2.prototype._parseUdt = function (udtInfo, row, callback) {
  const encoder = this.cc.getEncoder();
  const fieldTypes = row['field_types'];
  const keyspace = row['keyspace_name'];
  const fields = new Array(fieldTypes.length);
  const self = this;
  utils.forEachOf(row['field_names'], function (name, i, next) {
    encoder.parseTypeName(keyspace, fieldTypes[i], 0, null, self.udtResolver, function (err, type) {
      if (err) {
        return next(err);
      }
      fields[i] = {
        name: name,
        type: type
      };
      next();
    });
  }, function (err) {
    if (err) {
      return callback(err);
    }
    udtInfo.fields = fields;
    callback(null, udtInfo);
  });
};

/**
 * Used to parse schema information for Cassandra versions 4.x and above.
 *
 * This parser similar to [SchemaParserV2] expect it also parses virtual
 * keyspaces.
 *
 * @param {ClientOptions} options The client options
 * @param {ControlConnection} cc The control connection to be used
 * @param {Function} udtResolver The function to be used to retrieve the udts.
 * @constructor
 * @ignore
 */
function SchemaParserV3(options, cc, udtResolver) {
  SchemaParserV2.call(this, options, cc, udtResolver);
  this.supportsVirtual = true;
}

util.inherits(SchemaParserV3, SchemaParserV2);

/** @override */
SchemaParserV3.prototype.getKeyspaces = function (waitReconnect, callback) {
  const self = this;
  const keyspaces = {};
  const queries = [
    { query: _selectAllKeyspacesV2, virtual: false },
    { query: _selectAllVirtualKeyspaces, virtual: true }
  ];
  utils.each(queries, (q, cb) => {
    self.cc.query(q.query, waitReconnect, (err, result) => {
      if (err) {
        // only callback in error for non-virtual query as
        // server reporting C* 4.0 may not actually implement
        // virtual tables.
        if (!q.virtual) {
          return cb(err);
        }
        return cb();
      }
      for (let i = 0; i < result.rows.length; i++) {
        const ksInfo = self._parseKeyspace(result.rows[i], q.virtual);
        keyspaces[ksInfo.name] = ksInfo;
      }
      cb();
    });
  }, (err) => {
    callback(err, keyspaces);
  });
};

/** @override */
SchemaParserV3.prototype.getKeyspace = function (name, callback) {
  this._getKeyspace(_selectSingleKeyspaceV2, name, false, (err, ks) => {
    if (err) {
      return callback(err);
    }
    if (!ks) {
      // if not found, attempt to retrieve as virtual keyspace.
      return this._getKeyspace(_selectSingleVirtualKeyspace, name, true, callback);
    }
    return callback(null, ks);
  });
};

SchemaParserV3.prototype._getKeyspace = function (query, name, virtual, callback) {
  this.cc.query(util.format(query, name), (err, result) => {
    if (err) {
      // only callback in error for non-virtual query as
      // server reporting C* 4.0 may not actually implement
      // virtual tables.
      if (!virtual) {
        return callback(err);
      }
      return callback(null, null);
    }
    const row = result.rows[0];
    if (!row) {
      return callback(null, null);
    }
    callback(null, this._parseKeyspace(row, virtual));
  });
};

/**
 * Upon migration from thrift to CQL, we internally create a pair of surrogate clustering/regular columns
 * for compact static tables. These columns shouldn't be exposed to the user but are currently returned by C*.
 * We also need to remove the static keyword for all other columns in the table.
 * @param {module:metadata~TableMetadata} tableInfo
*/
function pruneStaticCompactTableColumns(tableInfo) {
  let i;
  let c;
  //remove "column1 text" clustering column
  for (i = 0; i < tableInfo.clusteringKeys.length; i++) {
    c = tableInfo.clusteringKeys[i];
    const index = tableInfo.columns.indexOf(c);
    tableInfo.columns.splice(index, 1);
    delete tableInfo.columnsByName[c.name];
  }
  tableInfo.clusteringKeys = utils.emptyArray;
  tableInfo.clusteringOrder = utils.emptyArray;
  //remove regular columns and set the static columns to non-static
  i = tableInfo.columns.length;
  while (i--) {
    c = tableInfo.columns[i];
    if (!c.isStatic && tableInfo.partitionKeys.indexOf(c) === -1) {
      // remove "value blob" regular column
      tableInfo.columns.splice(i, 1);
      delete tableInfo.columnsByName[c.name];
      continue;
    }
    c.isStatic = false;
  }
}

/**
 * Upon migration from thrift to CQL, we internally create a surrogate column "value" of type custom.
 * This column shouldn't be exposed to the user but is currently returned by C*.
 * @param {module:metadata~TableMetadata} tableInfo
 */
function pruneDenseTableColumns(tableInfo) {
  let i = tableInfo.columns.length;
  while (i--) {
    const c = tableInfo.columns[i];
    if (!c.isStatic && c.type.code === types.dataTypes.custom && c.type.info === 'empty') {
      // remove "value blob" regular column
      tableInfo.columns.splice(i, 1);
      delete tableInfo.columnsByName[c.name];
      continue;
    }
    c.isStatic = false;
  }
}

function getTokenToReplicaMapper(strategy, strategyOptions) {
  if (/SimpleStrategy$/.test(strategy)) {
    const rf = parseInt(strategyOptions['replication_factor'], 10);
    if (rf > 1) {
      return getTokenToReplicaSimpleMapper(rf);
    }
  }
  if (/NetworkTopologyStrategy$/.test(strategy)) {
    return getTokenToReplicaNetworkMapper(strategyOptions);
  }
  //default, wrap in an Array
  return (function noStrategy(tokenizer, ring, primaryReplicas) {
    const replicas = {};
    for (const key in primaryReplicas) {
      if (!primaryReplicas.hasOwnProperty(key)) {
        continue;
      }
      replicas[key] = [primaryReplicas[key]];
    }
    return replicas;
  });
}

/**
 * @param {Number} replicationFactor
 * @returns {function}
 */
function getTokenToReplicaSimpleMapper(replicationFactor) {
  return (function tokenSimpleStrategy(tokenizer, ringTokensAsStrings, primaryReplicas) {
    const ringLength = ringTokensAsStrings.length;
    const rf = Math.min(replicationFactor, ringLength);
    const replicas = {};
    for (let i = 0; i < ringLength; i++) {
      const key = ringTokensAsStrings[i];
      const tokenReplicas = [primaryReplicas[key]];
      for (let j = 1; j < ringLength && tokenReplicas.length < rf; j++) {
        let nextReplicaIndex = i + j;
        if (nextReplicaIndex >= ringLength) {
          //circle back
          nextReplicaIndex = nextReplicaIndex % ringLength;
        }
        const nextReplica = primaryReplicas[ringTokensAsStrings[nextReplicaIndex]];
        // In the case of vnodes, consecutive sections of the ring can be assigned to the same host.
        if (tokenReplicas.indexOf(nextReplica) === -1) {
          tokenReplicas.push(nextReplica);
        }
      }
      replicas[key] = tokenReplicas;
    }
    return replicas;
  });
}

/**
 * @param {Object} replicationFactors
 * @returns {Function}
 * @private
 */
function getTokenToReplicaNetworkMapper(replicationFactors) {
  //                A(DC1)
  //
  //           H         B(DC2)
  //                |
  //      G       --+--       C(DC1)
  //                |
  //           F         D(DC2)
  //
  //                E(DC1)
  return (function tokenNetworkStrategy(tokenizer, ringTokensAsStrings, primaryReplicas, datacenters) {
    const replicas = {};
    const ringLength = ringTokensAsStrings.length;

    for (let i = 0; i < ringLength; i++) {
      const key = ringTokensAsStrings[i];
      const tokenReplicas = [];
      const replicasByDc = {};
      const racksPlaced = {};
      const skippedHosts = [];
      for (let j = 0; j < ringLength; j++) {
        let nextReplicaIndex = i + j;
        if (nextReplicaIndex >= ringLength) {
          //circle back
          nextReplicaIndex = nextReplicaIndex % ringLength;
        }
        const h = primaryReplicas[ringTokensAsStrings[nextReplicaIndex]];
        // In the case of vnodes, consecutive sections of the ring can be assigned to the same host.
        if (tokenReplicas.indexOf(h) !== -1) {
          continue;
        }
        const dc = h.datacenter;
        //Check if the next replica belongs to one of the targeted dcs
        let dcRf = parseInt(replicationFactors[dc], 10);
        if (!dcRf) {
          continue;
        }
        dcRf = Math.min(dcRf, datacenters[dc].hostLength);
        let dcReplicas = replicasByDc[dc] || 0;
        //Amount of replicas per dc is greater than rf or the amount of host in the datacenter
        if (dcReplicas >= dcRf) {
          continue;
        }
        let racksPlacedInDc = racksPlaced[dc];
        if (!racksPlacedInDc) {
          racksPlacedInDc = racksPlaced[dc] = new utils.HashSet();
        }
        if (h.rack &&
            racksPlacedInDc.contains(h.rack) &&
            racksPlacedInDc.length < datacenters[dc].racks.length) {
          // We already selected a replica for this rack
          // Skip until replicas in other racks are added
          if (skippedHosts.length < dcRf - dcReplicas) {
            skippedHosts.push(h);
          }
          continue;
        }
        replicasByDc[h.datacenter] = ++dcReplicas;
        tokenReplicas.push(h);
        if (h.rack && racksPlacedInDc.add(h.rack) && racksPlacedInDc.length === datacenters[dc].racks.length) {
          // We finished placing all replicas for all racks in this dc
          // Add the skipped hosts
          replicasByDc[dc] += addSkippedHosts(dcRf, dcReplicas, tokenReplicas, skippedHosts);
        }
        if (isDoneForToken(replicationFactors, datacenters, replicasByDc)) {
          break;
        }
      }
      replicas[key] = tokenReplicas;
    }
    return replicas;
  });
}

/**
 * @returns {Number} The number of skipped hosts added.
 */
function addSkippedHosts(dcRf, dcReplicas, tokenReplicas, skippedHosts) {
  let i;
  for (i = 0; i < dcRf - dcReplicas && i < skippedHosts.length; i++) {
    tokenReplicas.push(skippedHosts[i]);
  }
  return i;
}

function isDoneForToken(replicationFactors, datacenters, replicasByDc) {
  const keys = Object.keys(replicationFactors);
  for (let i = 0; i < keys.length; i++) {
    const dcName = keys[i];
    const dc = datacenters[dcName];
    if (!dc) {
      // A DC is included in the RF but the DC does not exist in the topology
      continue;
    }
    const rf = Math.min(parseInt(replicationFactors[dcName], 10), dc.hostLength);
    if (rf > 0 && (!replicasByDc[dcName] || replicasByDc[dcName] < rf)) {
      return false;
    }
  }
  return true;
}
/**
 * Creates a new instance if the currentInstance is not valid for the
 * provided Cassandra version
 * @param {ClientOptions} options The client options
 * @param {ControlConnection} cc The control connection to be used
 * @param {Function} udtResolver The function to be used to retrieve the udts.
 * @param {Array.<Number>} [version] The cassandra version
 * @param {SchemaParser} [currentInstance] The current instance
 * @returns {SchemaParser}
 */
function getByVersion(options, cc, udtResolver, version, currentInstance) {
  let parserConstructor = SchemaParserV1;
  if (version && version[0] === 3) {
    parserConstructor = SchemaParserV2;
  } else if (version && version[0] >= 4) {
    parserConstructor = SchemaParserV3;
  }
  if (!currentInstance || !(currentInstance instanceof parserConstructor)){
    return new parserConstructor(options, cc, udtResolver);
  }
  return currentInstance;
}

exports.getByVersion = getByVersion;


"use strict";
const events = require('events');
const util = require('util');

const utils = require('./utils.js');
const errors = require('./errors.js');
const types = require('./types');
const ProfileManager = require('./execution-profile').ProfileManager;
const requests = require('./requests');
const clientOptions = require('./client-options');
const ClientState = require('./metadata/client-state');
const description = require('../package.json').description;
const version = require('../package.json').version;
const DefaultExecutionOptions = require('./execution-options').DefaultExecutionOptions;

// Allow injection of the following modules
/* eslint-disable prefer-const */
let ControlConnection = require('./control-connection');
let RequestHandler = require('./request-handler');
let PrepareHandler = require('./prepare-handler');
/* eslint-enable prefer-const */

/**
 * Max amount of pools being warmup in parallel, when warmup is enabled
 * @private
 */
const warmupLimit = 32;

/**
 * Client options
 * @typedef {Object} ClientOptions
 * @property {Array.<string>} contactPoints
 * Array of addresses or host names of the nodes to add as contact points.
 *

 *  Contact points are addresses of Cassandra nodes that the driver uses to discover the cluster topology.
 *

 *

 *  Only one contact point is required (the driver will retrieve the address of the other nodes automatically),
 *  but it is usually a good idea to provide more than one contact point, because if that single contact point is
 *  unavailable, the driver will not be able to initialize correctly.
 *

 * @property {String} [localDataCenter] The local data center to use.
 *

 *   If using DCAwareRoundRobinPolicy (default), this option is required and only hosts from this data center are
 *   connected to and used in query plans.
 *

 * @property {String} keyspace The logged keyspace for all the connections created within the {@link Client} instance.
 * @property {Number} refreshSchemaDelay The default window size in milliseconds used to debounce node list and schema
 * refresh metadata requests. Default: 1000.
 * @property {Boolean} isMetadataSyncEnabled Determines whether client-side schema metadata retrieval and update is
 * enabled.
 *
Setting this value to false will cause keyspace information not to be automatically loaded, affecting
 * replica calculation per token in the different keyspaces. When disabling metadata synchronization, use
 * [Metadata.refreshKeyspaces()]{@link module:metadata~Metadata#refreshKeyspaces} to keep keyspace information up to
 * date or token-awareness will not work correctly.

 * Default: true.
 * @property {Boolean} prepareOnAllHosts Determines if the driver should prepare queries on all hosts in the cluster.
 * Default: true.
 * @property {Boolean} rePrepareOnUp Determines if the driver should re-prepare all cached prepared queries on a
 * host when it marks it back up.
 * Default: true.
 * @property {Number} maxPrepared Determines the maximum amount of different prepared queries before evicting items
 * from the internal cache. Reaching a high threshold hints that the queries are not being reused, like when
 * hard-coding parameter values inside the queries.
 * Default: 500.
 * @property {Object} policies
 * @property {LoadBalancingPolicy} policies.loadBalancing The load balancing policy instance to be used to determine
 * the coordinator per query.
 * @property {RetryPolicy} policies.retry The retry policy.
 * @property {ReconnectionPolicy} policies.reconnection The reconnection policy to be used.
 * @property {AddressTranslator} policies.addressResolution The address resolution policy.
 * @property {SpeculativeExecutionPolicy} policies.speculativeExecution The SpeculativeExecutionPolicy
 * instance to be used to determine if the client should send speculative queries when the selected host takes more
 * time than expected.
 *

 *   Default: [NoSpeculativeExecutionPolicy]{@link
  *   module:policies/speculativeExecution~NoSpeculativeExecutionPolicy}
 *

 * @property {TimestampGenerator} policies.timestampGeneration The client-side
 * [query timestamp generator]{@link module:policies/timestampGeneration~TimestampGenerator}.
 *

 *   Default: [MonotonicTimestampGenerator]{@link module:policies/timestampGeneration~MonotonicTimestampGenerator}
 *
 *

 *
Use null to disable client-side timestamp generation.

 * @property {QueryOptions} queryOptions Default options for all queries.
 * @property {Object} pooling Pooling options.
 * @property {Number} pooling.heartBeatInterval The amount of idle time in milliseconds that has to pass before the
 * driver issues a request on an active connection to avoid idle time disconnections. Default: 30000.
 * @property {Object} pooling.coreConnectionsPerHost Associative array containing amount of connections per host
 * distance.
 * @property {Number} pooling.maxRequestsPerConnection The maximum number of requests per connection. The default
 * value is:
 *

 *
For modern protocol versions (v3 and above): 2048

 *
For older protocol versions (v1 and v2): 128

 *

 * @property {Boolean} pooling.warmup Determines if all connections to hosts in the local datacenter must be opened on
 * connect. Default: true.
 * @property {Object} protocolOptions
 * @property {Number} protocolOptions.port The port to use to connect to the Cassandra host. If not set through this
 * method, the default port (9042) will be used instead.
 * @property {Number} protocolOptions.maxSchemaAgreementWaitSeconds The maximum time in seconds to wait for schema
 * agreement between nodes before returning from a DDL query. Default: 10.
 * @property {Number} protocolOptions.maxVersion When set, it limits the maximum protocol version used to connect to
 * the nodes.
 * Useful for using the driver against a cluster that contains nodes with different major/minor versions of Cassandra.
 * @property {Boolean} protocolOptions.noCompact When set to true, enables the NO_COMPACT startup option.
 *

 * When this option is supplied SELECT, UPDATE, DELETE, and BATCH
 * statements on COMPACT STORAGE tables function in "compatibility" mode which allows seeing these tables
 * as if they were "regular" CQL tables.
 *

 *

 * This option only effects interactions with interactions with tables using COMPACT STORAGE and is only
 * supported by C* 3.0.16+, 3.11.2+, 4.0+ and DSE 6.0+.
 *

 * @property {Object} socketOptions
 * @property {Number} socketOptions.connectTimeout Connection timeout in milliseconds. Default: 5000.
 * @property {Number} socketOptions.defunctReadTimeoutThreshold Determines the amount of requests that simultaneously
 * have to timeout before closing the connection. Default: 64.
 * @property {Boolean} socketOptions.keepAlive Whether to enable TCP keep-alive on the socket. Default: true.
 * @property {Number} socketOptions.keepAliveDelay TCP keep-alive delay in milliseconds. Default: 0.
 * @property {Number} socketOptions.readTimeout Per-host read timeout in milliseconds.
 *

 *   Please note that this is not the maximum time a call to {@link Client#execute} may have to wait;
 *   this is the maximum time that call will wait for one particular Cassandra host, but other hosts will be tried if
 *   one of them timeout. In other words, a {@link Client#execute} call may theoretically wait up to
 *   readTimeout * number_of_cassandra_hosts (though the total number of hosts tried for a given query also
 *   depends on the LoadBalancingPolicy in use).
 *
When setting this value, keep in mind the following:

 *

 *
the timeout settings used on the Cassandra side (*_request_timeout_in_ms in cassandra.yaml) should be taken
 *   into account when picking a value for this read timeout. You should pick a value a couple of seconds greater than
 *   the Cassandra timeout settings.
 *

 *

 *     the read timeout is only approximate and only control the timeout to one Cassandra host, not the full query.
 *

 *

 * Setting a value of 0 disables read timeouts. Default: 12000.
 * @property {Boolean} socketOptions.tcpNoDelay When set to true, it disables the Nagle algorithm. Default: true.
 * @property {Number} socketOptions.coalescingThreshold Buffer length in bytes use by the write queue before flushing
 * the frames. Default: 8000.
 * @property {AuthProvider} authProvider Provider to be used to authenticate to an auth-enabled cluster.
 * @property {RequestTracker} requestTracker The instance of RequestTracker used to monitor or log requests executed
 * with this instance.
 * @property {Object} sslOptions Client-to-node ssl options. When set the driver will use the secure layer.
 * You can specify cert, ca, ... options named after the Node.js tls.connect() options.
 *

 *   It uses the same default values as Node.js tls.connect() except for rejectUnauthorized
 *   which is set to false by default (for historical reasons). This setting is likely to change
 *   in upcoming versions to enable validation by default.
 *

 * @property {Object} encoding
 * @property {Function} encoding.map Map constructor to use for Cassandra map<k,v> type encoding and decoding.
 * If not set, it will default to Javascript Object with map keys as property names.
 * @property {Function} encoding.set Set constructor to use for Cassandra set<k> type encoding and decoding.
 * If not set, it will default to Javascript Array.
 * @property {Boolean} encoding.copyBuffer Determines if the network buffer should be copied for buffer based data
 * types (blob, uuid, timeuuid and inet).
 *

 *   Setting it to true will cause that the network buffer is copied for each row value of those types,
 *   causing additional allocations but freeing the network buffer to be reused.
 *   Setting it to true is a good choice for cases where the Row and ResultSet returned by the queries are long-lived
 *   objects.
 *

 *

 *  Setting it to false will cause less overhead and the reference of the network buffer to be maintained until the row
 *  / result set are de-referenced.
 *  Default: true.
 *

 * @property {Boolean} encoding.useUndefinedAsUnset Valid for Cassandra 2.2 and above. Determines that, if a parameter
 * is set to
 * undefined it should be encoded as unset.
 *

 *  By default, ECMAScript undefined is encoded as null in the driver. Cassandra 2.2
 *  introduced the concept of unset.
 *  At driver level, you can set a parameter to unset using the field types.unset. Setting this flag to
 *  true allows you to use ECMAScript undefined as Cassandra unset.
 *

 *

 *   Default: true.
 *

 * @property {Boolean} encoding.useBigIntAsLong Use [BigInt ECMAScript type](https://tc39.github.io/proposal-bigint/)
 * to represent CQL bigint and counter data types.
 * @property {Boolean} encoding.useBigIntAsVarint Use [BigInt ECMAScript type](https://tc39.github.io/proposal-bigint/)
 * to represent CQL varint data type.
 * @property {Array.<ExecutionProfile>} profiles The array of [execution profiles]{@link ExecutionProfile}.
 * @property {Function} promiseFactory Function to be used to create a Promise from a
 * callback-style function.
 *

 *   Promise libraries often provide different methods to create a promise. For example, you can use Bluebird's
 *   Promise.fromCallback() method.
 *

 *

 *   By default, the driver will use the
 *   [Promise constructor]{@link https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise}.
 *

 */

/**
 * Query options
 * @typedef {Object} QueryOptions
 * @property {Boolean} [autoPage] Determines if the driver must retrieve the following result pages automatically.
 *

 *   This setting is only considered by the [Client#eachRow()]{@link Client#eachRow} method. For more information,
 *   check the
 *   [paging results documentation]{@link http://docs.datastax.com/en/developer/nodejs-driver/latest/features/paging/}.
 *

 * @property {Boolean} [captureStackTrace] Determines if the stack trace before the query execution should be
 * maintained.
 *

 *   Useful for debugging purposes, it should be set to false under production environment as it adds an
 *   unnecessary overhead to each execution.
 *

 * Default: false.
 * @property {Number} [consistency] [Consistency level]{@link module:types~consistencies}. Default: localOne.
 * @property {Object} [customPayload] Key-value payload to be passed to the server. On the Cassandra side,
 * implementations of QueryHandler can use this data.
 * @property {String|ExecutionProfile} [executionProfile] Name or instance of the [profile]{@link ExecutionProfile} to
 * be used for this execution. If not set, it will the use "default" execution profile.
 * @property {Number} [fetchSize] Amount of rows to retrieve per page.
 * @property {Array|Array<Array>} [hints] Type hints for parameters given in the query, ordered as for the parameters.
 *
For batch queries, an array of such arrays, ordered as with the queries in the batch.

 * @property {Host} [host] The host that should handle the query.
 *

 *   Use of this option is heavily discouraged and should only be used in the following cases:
 *

 *

 *

 *     Querying node-local tables, such as tables in the system and system_views
 *     keyspaces.
 *

 *

 *     Applying a series of schema changes, where it may be advantageous to execute schema changes in sequence on the
 *     same node.
 *

 *

 *

 *   Configuring a specific host causes the configured
 *   [LoadBalancingPolicy]{@link module:policies/loadBalancing~LoadBalancingPolicy} to be completely bypassed.
 *   However, if the load balancing policy dictates that the host is at a
 *   [distance of ignored]{@link module:types~distance} or there is no active connectivity to the host, the request will
 *   fail with a [NoHostAvailableError]{@link module:errors~NoHostAvailableError}.
 *

 * @property {Boolean} [isIdempotent] Defines whether the query can be applied multiple times without changing the result
 * beyond the initial application.
 *

 *   The query execution idempotence can be used at [RetryPolicy]{@link module:policies/retry~RetryPolicy} level to
 *   determine if an statement can be retried in case of request error or write timeout.
 *

 *
Default: false.

 * @property {String} [keyspace] Specifies the keyspace for the query. Used for routing within the driver, this
 * property is suitable when the query operates on a different keyspace than the current {@link Client#keyspace}.
 *

 *   This property should only be set manually by the user when the query operates on a different keyspace than
 *   the current {@link Client#keyspace} and using either batch or non-prepared query executions.
 *

 * @property {Boolean} [logged] Determines if the batch should be written to the batchlog. Only valid for
 * [Client#batch()]{@link Client#batch}, it will be ignored by other methods. Default: true.
 * @property {Boolean} [counter] Determines if its a counter batch. Only valid for
 * [Client#batch()]{@link Client#batch}, it will be ignored by other methods. Default: false.
 * @property {Buffer|String} [pageState] Buffer or string token representing the paging state.
 *
Useful for manual paging, if provided, the query will be executed starting from a given paging state.

 * @property {Boolean} [prepare] Determines if the query must be executed as a prepared statement.
 * @property {Number} [readTimeout] When defined, it overrides the default read timeout
 * (socketOptions.readTimeout) in milliseconds for this execution per coordinator.
 *

 *   Suitable for statements for which the coordinator may allow a longer server-side timeout, for example aggregation
 *   queries.
 *

 *

 *   A value of 0 disables client side read timeout for the execution. Default: undefined.
 *

 * @property {RetryPolicy} [retry] Retry policy for the query.
 *

 *   This property can be used to specify a different [retry policy]{@link module:policies/retry} to the one specified
 *   in the {@link ClientOptions}.policies.
 *

 * @property {Array} [routingIndexes] Index of the parameters that are part of the partition key to determine
 * the routing.
 * @property {Buffer|Array} [routingKey] Partition key(s) to determine which coordinator should be used for the query.
 * @property {Array} [routingNames] Array of the parameters names that are part of the partition key to determine the
 * routing. Only valid for non-prepared requests, it's recommended that you use the prepare flag instead.
 * @property {Number} [serialConsistency] Serial consistency is the consistency level for the serial phase of
 * conditional updates.
 * This option will be ignored for anything else that a conditional update/insert.
 * @property {Number|Long} [timestamp] The default timestamp for the query in microseconds from the unix epoch
 * (00:00:00, January 1st, 1970).
 *
If provided, this will replace the server side assigned timestamp as default timestamp.

 *
Use [generateTimestamp()]{@link module:types~generateTimestamp} utility method to generate a valid timestamp
 * based on a Date and microseconds parts.

 * @property {Boolean} [traceQuery] Enable query tracing for the execution. Use query tracing to diagnose performance
 * problems related to query executions. Default: false.
 *
To retrieve trace, you can call [Metadata.getTrace()]{@link module:metadata~Metadata#getTrace} method.

 */

/**
 * Creates a new instance of {@link Client}.
 * @classdesc
 * A Client holds connections to a Cassandra cluster, allowing it to be queried.
 * Each Client instance maintains multiple connections to the cluster nodes,
 * provides [policies]{@link module:policies} to choose which node to use for each query,
 * and handles [retries]{@link module:policies/retry} for failed query (when it makes sense), etc...
 *

 * Client instances are designed to be long-lived and usually a single instance is enough
 * per application. As a given Client can only be "logged" into one keyspace at
 * a time (where the "logged" keyspace is the one used by query if the query doesn't
 * explicitly use a fully qualified table name), it can make sense to create one
 * client per keyspace used. This is however not necessary to query multiple keyspaces
 * since it is always possible to use a single session with fully qualified table name
 * in queries.
 *

 * @extends EventEmitter
 * @param {ClientOptions} options The options for this instance.
 * @example
Creating a new client instance

 * const client = new Client({ contactPoints: ['192.168.1.100'], localDataCenter: 'datacenter1' });
 * client.connect(function (err) {
 *   if (err) return console.error(err);
 *   console.log('Connected to cluster with %d host(s): %j', client.hosts.length, client.hosts.keys());
 * });
 * @example
Executing a query

 * // calling #execute() can be made without previously calling #connect(), as internally
 * // it will ensure it's connected before attempting to execute the query
 * client.execute('SELECT key FROM system.local', function (err, result) {
 *   if (err) return console.error(err);
 *   const row = result.first();
 *   console.log(row['key']);
 * });
 * @example
Executing a query with promise-based API

 * const result = await client.execute('SELECT key FROM system.local');
 * const row = result.first();
 * console.log(row['key']);
 * @constructor
 */
function Client(options) {
  events.EventEmitter.call(this);
  this.options = clientOptions.extend({ logEmitter: this.emit.bind(this) }, options);
  Object.defineProperty(this, 'profileManager', { value: new ProfileManager(this.options) });
  Object.defineProperty(this, 'controlConnection', {
    value: new ControlConnection(this.options, this.profileManager), writable: true }
  );
  //Unlimited amount of listeners for internal event queues by default
  this.setMaxListeners(0);
  this.connected = false;
  this.isShuttingDown = false;
  /**
   * Gets the name of the active keyspace.
   * @type {String}
   */
  this.keyspace = options.keyspace;
  /**
   * Gets the schema and cluster metadata information.
   * @type {Metadata}
   */
  this.metadata = this.controlConnection.metadata;
  /**
   * Gets an associative array of cluster hosts.
   * @type {HostMap}
   */
  this.hosts = this.controlConnection.hosts;

  /**
   * The [ClientMetrics]{@link module:metrics~ClientMetrics} instance used to expose measurements of its internal
   * behavior and of the server as seen from the driver side.
   *
By default, a [DefaultMetrics]{@link module:metrics~DefaultMetrics} instance is used.

   * @type {ClientMetrics}
   */
  this.metrics = this.options.metrics;
}

util.inherits(Client, events.EventEmitter);

/**
 * Emitted when a new host is added to the cluster.
 *

 *
{@link Host} The host being added.

 *

 * @event Client#hostAdd
 */
/**
 * Emitted when a host is removed from the cluster
 *

 *
{@link Host} The host being removed.

 *

 * @event Client#hostRemove
 */
/**
 * Emitted when a host in the cluster changed status from down to up.
 *

 *
{@link Host host} The host that changed the status.

 *

 * @event Client#hostUp
 */
/**
 * Emitted when a host in the cluster changed status from up to down.
 *

 *
{@link Host host} The host that changed the status.

 *

 * @event Client#hostDown
 */

/**
 * Tries to connect to one of the [contactPoints]{@link ClientOptions} and discovers the rest the nodes of the cluster.
 *

 *   If a callback is provided, it will invoke the callback when the client is connected. Otherwise,
 *   it will return a Promise.
 *

 *

 *   If the {@link Client} is already connected, it invokes callback immediately (when provided) or the promise is
 *   fulfilled .
 *

 * @example
Callback-based execution

 * client.connect(function (err) {
 *   if (err) return console.error(err);
 *   console.log('Connected to cluster with %d host(s): %j', client.hosts.length, client.hosts.keys());
 * });
 * @example
Promise-based execution

 * await client.connect();
 * @param {function} [callback] The callback is invoked when the pool is connected it failed to connect.
 */
Client.prototype.connect = function (callback) {
  return utils.promiseWrapper.call(this, this.options, callback, this._connectCb);
};

/**
 * @param {Function} callback
 * @private
 */
Client.prototype._connectCb = function (callback) {
  if (this.connected) {
    return callback();
  }
  if (this.isShuttingDown) {
    //it is being shutdown, don't allow further calls to connect()
    return callback(new errors.NoHostAvailableError(null, 'Connecting after shutdown is not supported'));
  }
  this.once('connected', callback);
  if (this.connecting) {
    //the listener to connect was added, move on
    return;
  }
  this.connecting = true;
  const self = this;
  this.log('info', util.format("Connecting to cluster using '%s' version %s", description, version));
  utils.series([
    function initControlConnection(next) {
      self.controlConnection.init(next);
    },
    function initLoadBalancingPolicy(next) {
      self.hosts = self.controlConnection.hosts;
      self.profileManager.init(self, self.hosts, next);
    },
    function setKeyspace(next) {
      if (!self.keyspace) {
        return next();
      }
      RequestHandler.setKeyspace(self, next);
    },
    function setPoolOptionsAndWarmup(next) {
      clientOptions.setProtocolDependentDefaults(self.options, self.controlConnection.protocolVersion);

      if (!self.options.pooling.warmup) {
        return next();
      }
      self._warmup(next);
    }
  ], function connectFinished(err) {
    if (err) {
      // We should close the pools (if any) and reset the state to allow successive calls to connect()
      return self.controlConnection.reset(function () {
        self.connected = false;
        self.connecting = false;
        self.emit('connected', err);
      });
    }
    self._setHostListeners();
    // Set the distance of the control connection host relatively to this instance
    self.profileManager.getDistance(self.controlConnection.host);
    self.connected = true;
    self.connecting = false;
    self.emit('connected');
  });
};

/**
 * Executes a query on an available connection.
 *

 *   If a callback is provided, it will invoke the callback when the execution completes. Otherwise,
 *   it will return a Promise.
 *

 *
The query can be prepared (recommended) or not depending on {@link QueryOptions}.prepare flag.

 *

 *   Some executions failures can be handled transparently by the driver, according to the
 *   [RetryPolicy]{@link module:policies/retry~RetryPolicy} defined at {@link ClientOptions} or {@link QueryOptions}
 *   level.
 *

 * @param {String} query The query to execute.
 * @param {Array|Object} [params] Array of parameter values or an associative array (object) containing parameter names
 * as keys and its value.
 * @param {QueryOptions} [options] The query options for the execution.
 * @param {ResultCallback} [callback] Executes callback(err, result) when execution completed. When not defined, the
 * method will return a promise.
 * @example
Callback-based API

 * const query = 'SELECT name, email FROM users WHERE id = ?';
 * client.execute(query, [ id ], { prepare: true }, function (err, result) {
 *   assert.ifError(err);
 *   const row = result.first();
 *   console.log('%s: %s', row.name, row.email);
 * });
 * @example
Promise-based API, using async/await

 * const query = 'SELECT name, email FROM users WHERE id = ?';
 * const result = await client.execute(query, [ id ], { prepare: true });
 * const row = result.first();
 * console.log('%s: %s', row.name, row.email);
 * @see {@link ExecutionProfile} to reuse a set of options across different query executions.
 */
Client.prototype.execute = function (query, params, options, callback) {
  // set default argument values for optional parameters
  callback = callback || (options ? options : params);
  if (typeof callback === 'function') {
    params = typeof params !== 'function' ? params : null;
  }
  return utils.promiseWrapper.call(this, this.options, callback, function handler(cb) {
    let execOptions;
    try {
      execOptions = DefaultExecutionOptions.create(options, this);
    }
    catch (e) {
      return cb(e);
    }

    this._innerExecute(query, params, execOptions, cb);
  });
};

/**
 * Executes the query and calls rowCallback for each row as soon as they are received. Calls final callback after all
 * rows have been sent, or when there is an error.
 *

 *   The query can be prepared (recommended) or not depending on {@link QueryOptions}.prepare flag. Retries on multiple
 *   hosts if needed.
 *

 * @param {String} query The query to execute
 * @param {Array|Object} [params] Array of parameter values or an associative array (object) containing parameter names
 * as keys and its value.
 * @param {QueryOptions} [options]
 * @param {function} rowCallback Executes rowCallback(n, row) per each row received, where n is the row
 * index and row is the current Row.
 * @param {function} [callback] Executes callback(err, result) after all rows have been received.
 *

 *   When dealing with paged results, [ResultSet#nextPage()]{@link module:types~ResultSet#nextPage} method can be used
 *   to retrieve the following page. In that case, rowCallback() will be again called for each row and
 *   the final callback will be invoked when all rows in the following page has been retrieved.
 *

 * @example
Using per-row callback and arrow functions

 * client.eachRow(query, params, { prepare: true }, (n, row) => console.log(n, row), err => console.error(err));
 * @example
Overloads

 * client.eachRow(query, rowCallback);
 * client.eachRow(query, params, rowCallback);
 * client.eachRow(query, params, options, rowCallback);
 * client.eachRow(query, params, rowCallback, callback);
 * client.eachRow(query, params, options, rowCallback, callback);
 */
Client.prototype.eachRow = function (query, params, options, rowCallback, callback) {
  if (!callback && rowCallback && typeof options === 'function') {
    callback = utils.validateFn(rowCallback, 'rowCallback');
    rowCallback = options;
  } else {
    callback = callback || utils.noop;
    rowCallback = utils.validateFn(rowCallback || options || params, 'rowCallback');
  }

  params = typeof params !== 'function' ? params : null;

  let execOptions;
  try {
    execOptions = DefaultExecutionOptions.create(options, this, rowCallback);
  }
  catch (e) {
    return callback(e);
  }

  const self = this;
  let rowLength = 0;

  function nextPage() {
    self._innerExecute(query, params, execOptions, pageCallback);
  }

  function pageCallback (err, result) {
    if (err) {
      return callback(err);
    }
    // Next requests in case paging (auto or explicit) is used
    rowLength += result.rowLength;

    if (result.rawPageState !== undefined) {
      // Use new page state as next request page state
      execOptions.setPageState(result.rawPageState);
      if (execOptions.isAutoPage()) {
        // Issue next request for the next page
        return nextPage();
      }
      // Allows for explicit (manual) paging, in case the caller needs it
      result.nextPage = nextPage;
    }

    // Finished auto-paging
    result.rowLength = rowLength;
    callback(null, result);
  }

  this._innerExecute(query, params, execOptions, pageCallback);
};

/**
 * Executes the query and pushes the rows to the result stream
 *  as soon as they received.
 * Calls callback after all rows have been sent, or when there is an error.
 *

 * The stream is a [Readable Streams2]{@link http://nodejs.org/api/stream.html#stream_class_stream_readable} object
 *  that contains the raw bytes of the field value.
 *  It can be piped downstream and provides automatic pause/resume logic (it buffers when not read).
 *

 *

 *   The query can be prepared (recommended) or not depending on {@link QueryOptions}.prepare flag. Retries on multiple
 *   hosts if needed.
 *

 * @param {String} query The query to prepare and execute
 * @param {Array|Object} [params] Array of parameter values or an associative array (object) containing parameter names
 * as keys and its value
 * @param {QueryOptions} [options]
 * @param {function} [callback], executes callback(err) after all rows have been received or if there is an error
 * @returns {types.ResultStream}
 */
Client.prototype.stream = function (query, params, options, callback) {
  callback = callback || utils.noop;
  // NOTE: the nodejs stream maintains yet another internal buffer
  // we rely on the default stream implementation to keep memory
  // usage reasonable.
  const resultStream = new types.ResultStream({ objectMode: 1 });
  function onFinish(err, result) {
    if (err) {
      resultStream.emit('error', err);
    }
    if (result && result.nextPage ) {
      // allows for throttling as per the
      // default nodejs stream implementation
      resultStream._valve(function pageValve() {
        try {
          result.nextPage();
        }
        catch( ex ) {
          resultStream.emit('error', ex );
        }
      });
      return;
    }
    // Explicitly dropping the valve (closure)
    resultStream._valve(null);
    resultStream.add(null);
    callback(err);
  }
  let sync = true;
  this.eachRow(query, params, options, function rowCallback(n, row) {
    resultStream.add(row);
  }, function eachRowFinished(err, result) {
    if (sync) {
      // Prevent sync callback
      return setImmediate(function eachRowFinishedImmediate() {
        onFinish(err, result);
      });
    }
    onFinish(err, result);
  });
  sync = false;
  return resultStream;
};

/**
 * Executes batch of queries on an available connection to a host.
 *

 *   If a callback is provided, it will invoke the callback when the execution completes. Otherwise,
 *   it will return a Promise.
 *

 * @param {Array.<string>|Array.<{query, params}>} queries The queries to execute as an Array of strings or as an array
 * of object containing the query and params
 * @param {QueryOptions} [options]
 * @param {ResultCallback} [callback] Executes callback(err, result) when the batch was executed
 */
Client.prototype.batch = function (queries, options, callback) {
  callback = callback || options;
  return utils.promiseWrapper.call(this, this.options, callback, function handler(cb) {
    this._batchCb(queries, options, cb);
  });
};

/**
 * @param {Array.<string>|Array.<{query, params}>}queries
 * @param {QueryOptions} options
 * @param {ResultCallback} callback
 * @private
 */
Client.prototype._batchCb = function (queries, options, callback) {
  if (!Array.isArray(queries)) {
    // We should throw (not callback) for an unexpected type
    throw new errors.ArgumentError('Queries should be an Array');
  }
  if (queries.length === 0) {
    return callback(new errors.ArgumentError('Queries array can not be empty'));
  }

  let execOptions;
  try {
    execOptions = DefaultExecutionOptions.create(options, this);
  } catch (e) {
    return callback(e);
  }

  let queryItems;
  let request;

  utils.series([
    next => this.connect(next),
    next => {
      if (execOptions.isPrepared()) {
        return PrepareHandler.getPreparedMultiple(
          this, execOptions.getLoadBalancingPolicy(), queries, this.keyspace, function(err, result) {
            queryItems = result;
            next(err);
          });
      }
      queryItems = new Array(queries.length);
      for (let i = 0; i < queries.length; i++) {
        const item = queries[i];
        if (!item) {
          return next(new errors.ArgumentError(util.format('Invalid query at index %d', i)));
        }
        const query = typeof item === 'string' ? item : item.query;
        if (!query) {
          return next(errors.ArgumentError(util.format('Invalid query at index %d', i)));
        }
        queryItems[i] = { query: query, params: item.params };
      }
      next();
    },
    next => this._createBatchRequest(queryItems, execOptions, (err, r) => {
      request = r;
      next(err);
    }),
    next => RequestHandler.send(request, execOptions, this, next)
  ], callback);
};

/**
 * Gets the host list representing the replicas that contain such partition.
 * @param {String} keyspace
 * @param {Buffer} token
 * @returns {Array}
 */
Client.prototype.getReplicas = function (keyspace, token) {
  return this.metadata.getReplicas(keyspace, token);
};

/**
 * Gets a snapshot containing information on the connections pools held by this Client at the current time.
 *

 *   The information provided in the returned object only represents the state at the moment this method was called and
 *   it's not maintained in sync with the driver metadata.
 *

 * @return module:metadata~ClientState
 */
Client.prototype.getState = function () {
  return ClientState.from(this);
};

Client.prototype.log = utils.log;

/**
 * Closes all connections to all hosts.
 *

 *   If a callback is provided, it will invoke the callback when the client is disconnected. Otherwise,
 *   it will return a Promise.
 *

 * @param {Function} [callback] Optional callback to be invoked when finished closing all connections.
 */
Client.prototype.shutdown = function (callback) {
  return utils.promiseWrapper.call(this, this.options, callback, this._shutdownCb);
};

/**
 * @param {Function} callback
 * @private
 */
Client.prototype._shutdownCb = function (callback) {
  const self = this;
  function doShutdown() {
    self.connected = false;
    self.isShuttingDown = true;
    const hosts = self.hosts.values();
    // Shutdown the ControlConnection before shutting down the pools
    self.controlConnection.shutdown();
    self.options.policies.speculativeExecution.shutdown();
    if (self.options.requestTracker) {
      self.options.requestTracker.shutdown();
    }
    // go through all the host and shut down their pools
    utils.each(hosts, (h, next) => h.shutdown(false, next), callback);
  }
  this.log('info', 'Shutting down');
  callback = callback || utils.noop;
  if (!this.hosts || !this.connected) {
    // not initialized
    this.connected = false;
    return callback();
  }
  if (this.connecting) {
    this.log('warning', 'Shutting down while connecting');
    // wait until finish connecting for easier troubleshooting
    return this.once('connected', doShutdown);
  }
  doShutdown();
};

/**
 * Waits until that the schema version in all nodes is the same or the waiting time passed.
 * @param {Connection} connection
 * @param {Function} callback
 * @ignore
 */
Client.prototype._waitForSchemaAgreement = function (connection, callback) {
  if (this.hosts.length === 1) {
    return setImmediate(() => callback(null, true));
  }

  const start = process.hrtime();
  const maxWaitSeconds = this.options.protocolOptions.maxSchemaAgreementWaitSeconds;

  this.log('info', 'Waiting for schema agreement');

  let versionsMatch;

  utils.whilst(
    () => !versionsMatch && process.hrtime(start)[0] < maxWaitSeconds,
    next => {
      this.metadata.compareSchemaVersions(connection, (err, agreement) => {
        if (err) {
          return next(err);
        }

        versionsMatch = agreement;

        if (versionsMatch) {
          this.log('info', 'Schema versions match');
          return next();
        }

        // Let some time pass before the next check
        setTimeout(next, 500);
      });
    },
    (err) => callback(err, versionsMatch));
};

/**
 * Waits for schema agreements and schedules schema metadata refresh.
 * @param {Connection} connection
 * @param event
 * @param {Function} callback
 * @ignore
 * @internal
 */
Client.prototype.handleSchemaAgreementAndRefresh = function (connection, event, callback) {
  this._waitForSchemaAgreement(connection, (err, agreement) => {
    if (err) {
      //we issue a warning but we continue with the normal flow
      this.log('warning', 'There was an error while waiting for the schema agreement between nodes', err);
    }
    if (!this.options.isMetadataSyncEnabled) {
      return callback(agreement);
    }

    // schedule metadata refresh immediately and the callback will be invoked once it was refreshed
    this.controlConnection.handleSchemaChange(event, true, (err) => {
      if (err) {
        this.log('warning', 'There was an error while handling schema change', err);
      }
      callback(agreement);
    });
  });
};

/**
 * Connects and handles the execution of prepared and simple statements. All parameters are mandatory.
 * @param {string} query
 * @param {Array} params
 * @param {ExecutionOptions} execOptions
 * @param {Function} callback
 * @private
 */
Client.prototype._innerExecute = function (query, params, execOptions, callback) {
  const version = this.controlConnection.protocolVersion;

  if (!execOptions.isPrepared() && params && !util.isArray(params) && !types.protocolVersion.supportsNamedParameters(version)) {
    // Only Cassandra 2.1 and above supports named parameters
    return callback(
      new errors.ArgumentError('Named parameters for simple statements are not supported, use prepare flag'));
  }

  let request;
  utils.series([
    next => this.connect(next),
    next => {
      if (!execOptions.isPrepared()) {
        return this._createQueryRequest(query, execOptions, params, (err, r) => {
          request = r;
          next(err);
        });
      }

      const lbp = execOptions.getLoadBalancingPolicy();
      PrepareHandler.getPrepared(this, lbp, query, this.keyspace, (err, queryId, meta) => {
        if (err) {
          return next(err);
        }
        this._createExecuteRequest(query, queryId, execOptions, params, meta, (err, r) => {
          request = r;
          next(err);
        });
      });
    },
    next => RequestHandler.send(request, execOptions, this, next)
  ], callback);
};

/**
 * Sets the listeners for the nodes.
 * @private
 */
Client.prototype._setHostListeners = function () {
  function getHostUpListener(emitter, h) {
    return () => emitter.emit('hostUp', h);
  }
  function getHostDownListener(emitter, h) {
    return () => emitter.emit('hostDown', h);
  }
  const self = this;
  //Add status listeners when new nodes are added and emit hostAdd
  this.hosts.on('add', function hostAddedListener(h) {
    h.on('up', getHostUpListener(self, h));
    h.on('down', getHostDownListener(self, h));
    self.emit('hostAdd', h);
  });
  //Remove all listeners and emit hostRemove
  this.hosts.on('remove', function hostRemovedListener(h) {
    h.removeAllListeners();
    self.emit('hostRemove', h);
  });
  //Add status listeners for existing hosts
  this.hosts.forEach(function (h) {
    h.on('up', getHostUpListener(self, h));
    h.on('down', getHostDownListener(self, h));
  });
};

Client.prototype._warmup = function (callback) {
  const self = this;
  const hosts = this.hosts.values();
  utils.timesLimit(hosts.length, warmupLimit, function warmupEachCallback(i, next) {
    const h = hosts[i];
    const distance = self.profileManager.getDistance(h);
    if (distance !== types.distance.local) {
      //do not warmup pool for remote or ignored hosts
      return next();
    }
    h.warmupPool(function (err) {
      if (err) {
        //An error while trying to create a connection
        //To 1 host is not an issue, warn the user and move on
        self.log('warning', util.format('Connection pool to host %s could not be created: %s', h.address, err));
      }
      next();
    });
  }, callback);
};

/**
 * @returns {Encoder}
 * @private
 */
Client.prototype._getEncoder = function () {
  const encoder = this.controlConnection.getEncoder();
  if (!encoder) {
    throw new errors.DriverInternalError('Encoder is not defined');
  }
  return encoder;
};

/**
 * Returns a BatchRequest instance and fills the routing key information in the provided options.
 * @private
 */
Client.prototype._createBatchRequest = function (queryItems, info, callback) {
  const firstQuery = queryItems[0];
  if (!firstQuery.meta) {
    return callback(null, new requests.BatchRequest(queryItems, info));
  }

  this._setRoutingInfo(info, firstQuery.params, firstQuery.meta, function (err) {
    if (err) {
      return callback(err);
    }
    callback(null, new requests.BatchRequest(queryItems, info));
  });
};

/**
 * Returns an ExecuteRequest instance and fills the routing key information in the provided options.
 * @private
 */
Client.prototype._createExecuteRequest = function(query, queryId, info, params, meta, callback) {
  try {
    params = utils.adaptNamedParamsPrepared(params, meta.columns);
  }
  catch (err) {
    return callback(err);
  }

  this._setRoutingInfo(info, params, meta, err => {
    if (err) {
      return callback(err);
    }
    callback(null, new requests.ExecuteRequest(query, queryId, params, info, meta));
  });
};

/**
 * Returns a QueryRequest instance and fills the routing key information in the provided options.
 * @private
 */
Client.prototype._createQueryRequest = function (query, execOptions, params, callback) {
  this.metadata.adaptUserHints(this.keyspace, execOptions.getHints(), (err) => {
    if (err) {
      return callback(err);
    }

    let paramsInfo;
    try {
      paramsInfo = utils.adaptNamedParamsWithHints(params, execOptions);
      this._getEncoder().setRoutingKeyFromUser(paramsInfo.params, execOptions, paramsInfo.keyIndexes);
    } catch (err) {
      return callback(err);
    }

    callback(null, new requests.QueryRequest(query, paramsInfo.params, execOptions, paramsInfo.namedParameters));
  });
};

/**
 * Sets the routing key based on the parameter values or the provided routing key components.
 * @param {ExecutionOptions} execOptions
 * @param {Array} params
 * @param meta
 * @param {Function} callback
 * @private
 */
Client.prototype._setRoutingInfo = function (execOptions, params, meta, callback) {
  const self = this;

  /** Wrapper function as encoding a routing key could throw a TypeError */
  function encodeRoutingKey(fromUser) {
    const encoder = self._getEncoder();
    try {
      if (fromUser) {
        encoder.setRoutingKeyFromUser(params, execOptions);
      } else {
        encoder.setRoutingKeyFromMeta(meta, params, execOptions);
      }
    }
    catch (err) {
      return callback(err);
    }
    callback();
  }

  if (!execOptions.getKeyspace() && meta.keyspace) {
    execOptions.setKeyspace(meta.keyspace);
  }
  if (execOptions.getRoutingKey()) {
    // Routing information provided by the user
    return encodeRoutingKey(true);
  }
  if (Array.isArray(meta.partitionKeys)) {
    // The partition keys are provided as part of the metadata for modern protocol versions
    execOptions.setRoutingIndexes(meta.partitionKeys);
    return encodeRoutingKey();
  }

  // Older versions of the protocol (v3 and below) don't provide routing information
  this.metadata.getTable(meta.keyspace, meta.table, (err, tableInfo) => {
    if (err) {
      this.log('warning', util.format('Table %s.%s metadata could not be retrieved', meta.keyspace, meta.table));
      return callback();
    }
    if (!tableInfo) {
      // The schema data is not there, maybe it is being recreated, avoid setting the routing information
      return callback();
    }
    execOptions.setRoutingIndexes(tableInfo.partitionKeys.map(c => meta.columnsByName[c.name]));
    // Skip parsing metadata next time
    meta.partitionKeys = execOptions.getRoutingIndexes();
    encodeRoutingKey();
  });
};

/**
 * Callback used by execution methods.
 * @callback ResultCallback
 * @param {Error} err Error occurred in the execution of the query.
 * @param {ResultSet} [result] Result of the execution of the query.
 */

module.exports = Client;


"use strict";

const async = require('async');
const exec = require('child_process').exec;
const fs = require('fs');
const path = require('path');

/**
 * This script is used to check that the samples run correctly.
 * It is not a valid example, see README.md and subdirectories for more information.
 */

/** List all js files in the directory */
function getJsFiles(dir, fileArray) {
  const files = fs.readdirSync(dir);
  fileArray = fileArray || [];
  files.forEach(function(file) {
    if (file === 'node_modules') {
      return;
    }
    if (fs.statSync(dir + file).isDirectory()) {
      getJsFiles(dir + file + '/', fileArray);
      return;
    }
    if (file.substring(file.length-3, file.length) !== '.js') {
      return;
    }
    fileArray.push(dir+file);
  });
  return fileArray;
}

if (+process.versions.node.split('.')[0] < 8) {
  console.log('Examples were not executed as they were designed to run against Node.js 8+');
  return;
}

const runnerFileName = path.basename(module.filename);
let counter = 0;
let failures = 0;

async.eachSeries(getJsFiles(path.dirname(module.filename) + path.sep), function (file, next) {
  if (file.indexOf(runnerFileName) >= 0) {
    return next();
  }

  let timedOut = false;
  const timeout = setTimeout(function() {
    console.log("%s timed out after 10s", file);
    counter++;
    failures++;
    next();
  }, 10000);

  exec('node ' + file, function (err) {
    if(timedOut) {
      return;
    }
    counter++;
    clearTimeout(timeout);
    process.stdout.write('.');
    if (err) {
      console.log('Failed %s', file);
      console.error(err);
      failures++;
    }
    next();
  });
}, function (err) {
  if (err) {
    console.error(err);
  }
  console.log('\n%d/%d examples executed successfully', (counter-failures), counter);
  process.exit(failures);
});


"use strict";
const cassandra = require('cassandra-driver');
const executeConcurrent = cassandra.concurrent.executeConcurrent;
const Uuid = cassandra.types.Uuid;

const client = new cassandra.Client({ contactPoints: ['127.0.0.1'], localDataCenter: 'datacenter1' });

/**
 * Inserts multiple rows in a table from an Array using the built in method executeConcurrent(),
 * limiting the amount of parallel requests.
 */
async function example() {
  await client.connect();
  await client.execute(`CREATE KEYSPACE IF NOT EXISTS examples
                        WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1' }`);
  await client.execute(`USE examples`);
  await client.execute(`CREATE TABLE IF NOT EXISTS tbl_sample_kv (id uuid, value text, PRIMARY KEY (id))`);

  // The maximum amount of async executions that are going to be launched in parallel
  // at any given time
  const concurrencyLevel = 32;

  // Use an Array with 10000 different values
  const values = Array.from(new Array(10000).keys()).map(x => [ Uuid.random(), x.toString() ]);

  try {

    const query = 'INSERT INTO tbl_sample_kv (id, value) VALUES (?, ?)';
    await executeConcurrent(client, query, values);

    console.log(`Finished executing ${values.length} queries with a concurrency level of ${concurrencyLevel}.`);

  } finally {
    client.shutdown();
  }
}

example();


"use strict";

/**
 * Creates a new SchemaFunction.
 * @classdesc Describes a CQL function.
 * @alias module:metadata~SchemaFunction
 * @constructor
 */
function SchemaFunction() {
  /**
   * Name of the cql function.
   * @type {String}
   */
  this.name = null;
  /**
   * Name of the keyspace where the cql function is declared.
   */
  this.keyspaceName = null;
  /**
   * Signature of the function.
   * @type {Array.<String>}
   */
  this.signature = null;
  /**
   * List of the function argument names.
   * @type {Array.<String>}
   */
  this.argumentNames = null;
  /**
   * List of the function argument types.
   * @type {Array.<{code, info}>}
   */
  this.argumentTypes = null;
  /**
   * Body of the function.
   * @type {String}
   */
  this.body = null;
  /**
   * Determines if the function is called when the input is null.
   * @type {Boolean}
   */
  this.calledOnNullInput = null;
  /**
   * Name of the programming language, for example: java, javascript, ...
   * @type {String}
   */
  this.language = null;
  /**
   * Type of the return value.
   * @type {{code: number, info: (Object|Array|null)}}
   */
  this.returnType = null;
}

module.exports = SchemaFunction;