You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1025 lines
33 KiB
JavaScript
1025 lines
33 KiB
JavaScript
2 years ago
|
/*
|
||
|
* Copyright DataStax, Inc.
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
const events = require('events');
|
||
|
const util = require('util');
|
||
|
|
||
|
/**
|
||
|
* Module containing classes and fields related to metadata.
|
||
|
* @module metadata
|
||
|
*/
|
||
|
|
||
|
const t = require('../tokenizer');
|
||
|
const utils = require('../utils');
|
||
|
const errors = require('../errors');
|
||
|
const types = require('../types');
|
||
|
const requests = require('../requests');
|
||
|
const schemaParserFactory = require('./schema-parser');
|
||
|
const promiseUtils = require('../promise-utils');
|
||
|
const { TokenRange } = require('../token');
|
||
|
const { ExecutionOptions } = require('../execution-options');
|
||
|
|
||
|
/**
|
||
|
* @const
|
||
|
* @private
|
||
|
*/
|
||
|
const _selectTraceSession = "SELECT * FROM system_traces.sessions WHERE session_id=%s";
|
||
|
/**
|
||
|
* @const
|
||
|
* @private
|
||
|
*/
|
||
|
const _selectTraceEvents = "SELECT * FROM system_traces.events WHERE session_id=%s";
|
||
|
/**
|
||
|
* @const
|
||
|
* @private
|
||
|
*/
|
||
|
const _selectSchemaVersionPeers = "SELECT schema_version FROM system.peers";
|
||
|
/**
|
||
|
* @const
|
||
|
* @private
|
||
|
*/
|
||
|
const _selectSchemaVersionLocal = "SELECT schema_version FROM system.local";
|
||
|
/**
|
||
|
* @const
|
||
|
* @private
|
||
|
*/
|
||
|
const _traceMaxAttemps = 5;
|
||
|
/**
|
||
|
* @const
|
||
|
* @private
|
||
|
*/
|
||
|
const _traceAttemptDelay = 400;
|
||
|
|
||
|
/**
|
||
|
* Represents cluster and schema information.
|
||
|
* The metadata class acts as a internal state of the driver.
|
||
|
*/
|
||
|
class Metadata {
|
||
|
|
||
|
/**
|
||
|
* Creates a new instance of {@link Metadata}.
|
||
|
* @param {ClientOptions} options
|
||
|
* @param {ControlConnection} controlConnection Control connection used to retrieve information.
|
||
|
*/
|
||
|
constructor(options, controlConnection) {
|
||
|
if (!options) {
|
||
|
throw new errors.ArgumentError('Options are not defined');
|
||
|
}
|
||
|
|
||
|
Object.defineProperty(this, 'options', { value: options, enumerable: false, writable: false });
|
||
|
Object.defineProperty(this, 'controlConnection', { value: controlConnection, enumerable: false, writable: false });
|
||
|
this.keyspaces = {};
|
||
|
this.initialized = false;
|
||
|
this._isDbaas = false;
|
||
|
this._schemaParser = schemaParserFactory.getByVersion(options, controlConnection, this.getUdt.bind(this));
|
||
|
this.log = utils.log;
|
||
|
this._preparedQueries = new PreparedQueries(options.maxPrepared, (...args) => this.log(...args));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the cassandra version
|
||
|
* @internal
|
||
|
* @ignore
|
||
|
* @param {Array.<Number>} version
|
||
|
*/
|
||
|
setCassandraVersion(version) {
|
||
|
this._schemaParser = schemaParserFactory.getByVersion(
|
||
|
this.options, this.controlConnection, this.getUdt.bind(this), version, this._schemaParser);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Determines whether the cluster is provided as a service.
|
||
|
* @returns {boolean} true when the cluster is provided as a service (DataStax Astra), <code>false<code> when it's a
|
||
|
* different deployment (on-prem).
|
||
|
*/
|
||
|
isDbaas() {
|
||
|
return this._isDbaas;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the product type as DBaaS.
|
||
|
* @internal
|
||
|
* @ignore
|
||
|
*/
|
||
|
setProductTypeAsDbaas() {
|
||
|
this._isDbaas = true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @ignore
|
||
|
* @param {String} partitionerName
|
||
|
*/
|
||
|
setPartitioner(partitionerName) {
|
||
|
if (/RandomPartitioner$/.test(partitionerName)) {
|
||
|
return this.tokenizer = new t.RandomTokenizer();
|
||
|
}
|
||
|
if (/ByteOrderedPartitioner$/.test(partitionerName)) {
|
||
|
return this.tokenizer = new t.ByteOrderedTokenizer();
|
||
|
}
|
||
|
return this.tokenizer = new t.Murmur3Tokenizer();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Populates the information regarding primary replica per token, datacenters (+ racks) and sorted token ring.
|
||
|
* @ignore
|
||
|
* @param {HostMap} hosts
|
||
|
*/
|
||
|
buildTokens(hosts) {
|
||
|
if (!this.tokenizer) {
|
||
|
return this.log('error', 'Tokenizer could not be determined');
|
||
|
}
|
||
|
//Get a sorted array of tokens
|
||
|
const allSorted = [];
|
||
|
//Get a map of <token, primaryHost>
|
||
|
const primaryReplicas = {};
|
||
|
//Depending on the amount of tokens, this could be an expensive operation
|
||
|
const hostArray = hosts.values();
|
||
|
const stringify = this.tokenizer.stringify;
|
||
|
const datacenters = {};
|
||
|
hostArray.forEach((h) => {
|
||
|
if (!h.tokens) {
|
||
|
return;
|
||
|
}
|
||
|
h.tokens.forEach((tokenString) => {
|
||
|
const token = this.tokenizer.parse(tokenString);
|
||
|
utils.insertSorted(allSorted, token, (t1, t2) => t1.compare(t2));
|
||
|
primaryReplicas[stringify(token)] = h;
|
||
|
});
|
||
|
let dc = datacenters[h.datacenter];
|
||
|
if (!dc) {
|
||
|
dc = datacenters[h.datacenter] = {
|
||
|
hostLength: 0,
|
||
|
racks: new utils.HashSet()
|
||
|
};
|
||
|
}
|
||
|
dc.hostLength++;
|
||
|
dc.racks.add(h.rack);
|
||
|
});
|
||
|
//Primary replica for given token
|
||
|
this.primaryReplicas = primaryReplicas;
|
||
|
//All the tokens in ring order
|
||
|
this.ring = allSorted;
|
||
|
// Build TokenRanges.
|
||
|
const tokenRanges = new Set();
|
||
|
if (this.ring.length === 1) {
|
||
|
// If there is only one token, return the range ]minToken, minToken]
|
||
|
const min = this.tokenizer.minToken();
|
||
|
tokenRanges.add(new TokenRange(min, min, this.tokenizer));
|
||
|
}
|
||
|
else {
|
||
|
for (let i = 0; i < this.ring.length; i++) {
|
||
|
const start = this.ring[i];
|
||
|
const end = this.ring[(i + 1) % this.ring.length];
|
||
|
tokenRanges.add(new TokenRange(start, end, this.tokenizer));
|
||
|
}
|
||
|
}
|
||
|
this.tokenRanges = tokenRanges;
|
||
|
//Compute string versions as it's potentially expensive and frequently reused later
|
||
|
this.ringTokensAsStrings = new Array(allSorted.length);
|
||
|
for (let i = 0; i < allSorted.length; i++) {
|
||
|
this.ringTokensAsStrings[i] = stringify(allSorted[i]);
|
||
|
}
|
||
|
//Datacenter metadata (host length and racks)
|
||
|
this.datacenters = datacenters;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the keyspace metadata information and updates the internal state of the driver.
|
||
|
* <p>
|
||
|
* If a <code>callback</code> is provided, the callback is invoked when the keyspaces metadata refresh completes.
|
||
|
* Otherwise, it returns a <code>Promise</code>.
|
||
|
* </p>
|
||
|
* @param {String} name Name of the keyspace.
|
||
|
* @param {Function} [callback] Optional callback.
|
||
|
*/
|
||
|
refreshKeyspace(name, callback) {
|
||
|
return promiseUtils.optionalCallback(this._refreshKeyspace(name), callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {String} name
|
||
|
* @private
|
||
|
*/
|
||
|
async _refreshKeyspace(name) {
|
||
|
if (!this.initialized) {
|
||
|
throw this._uninitializedError();
|
||
|
}
|
||
|
this.log('info', util.format('Retrieving keyspace %s metadata', name));
|
||
|
try {
|
||
|
const ksInfo = await this._schemaParser.getKeyspace(name);
|
||
|
if (!ksInfo) {
|
||
|
// the keyspace was dropped
|
||
|
delete this.keyspaces[name];
|
||
|
return null;
|
||
|
}
|
||
|
// Tokens are lazily init on the keyspace, once a replica from that keyspace is retrieved.
|
||
|
this.keyspaces[ksInfo.name] = ksInfo;
|
||
|
return ksInfo;
|
||
|
}
|
||
|
catch (err) {
|
||
|
this.log('error', 'There was an error while trying to retrieve keyspace information', err);
|
||
|
throw err;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the metadata information of all the keyspaces and updates the internal state of the driver.
|
||
|
* <p>
|
||
|
* If a <code>callback</code> is provided, the callback is invoked when the keyspace metadata refresh completes.
|
||
|
* Otherwise, it returns a <code>Promise</code>.
|
||
|
* </p>
|
||
|
* @param {Boolean|Function} [waitReconnect] Determines if it should wait for reconnection in case the control connection is not
|
||
|
* connected at the moment. Default: true.
|
||
|
* @param {Function} [callback] Optional callback.
|
||
|
*/
|
||
|
refreshKeyspaces(waitReconnect, callback) {
|
||
|
if (typeof waitReconnect === 'function' || typeof waitReconnect === 'undefined') {
|
||
|
callback = waitReconnect;
|
||
|
waitReconnect = true;
|
||
|
}
|
||
|
if (!this.initialized) {
|
||
|
const err = this._uninitializedError();
|
||
|
if (callback) {
|
||
|
return callback(err);
|
||
|
}
|
||
|
return Promise.reject(err);
|
||
|
}
|
||
|
return promiseUtils.optionalCallback(this.refreshKeyspacesInternal(waitReconnect), callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Boolean} waitReconnect
|
||
|
* @returns {Promise<Object<string, Object>>}
|
||
|
* @ignore
|
||
|
* @internal
|
||
|
*/
|
||
|
async refreshKeyspacesInternal(waitReconnect) {
|
||
|
this.log('info', 'Retrieving keyspaces metadata');
|
||
|
try {
|
||
|
this.keyspaces = await this._schemaParser.getKeyspaces(waitReconnect);
|
||
|
return this.keyspaces;
|
||
|
}
|
||
|
catch (err) {
|
||
|
this.log('error', 'There was an error while trying to retrieve keyspaces information', err);
|
||
|
throw err;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_getKeyspaceReplicas(keyspace) {
|
||
|
if (!keyspace.replicas) {
|
||
|
//Calculate replicas the first time for the keyspace
|
||
|
keyspace.replicas =
|
||
|
keyspace.tokenToReplica(this.tokenizer, this.ringTokensAsStrings, this.primaryReplicas, this.datacenters);
|
||
|
}
|
||
|
return keyspace.replicas;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the host list representing the replicas that contain the given partition key, token or token range.
|
||
|
* <p>
|
||
|
* It uses the pre-loaded keyspace metadata to retrieve the replicas for a token for a given keyspace.
|
||
|
* When the keyspace metadata has not been loaded, it returns null.
|
||
|
* </p>
|
||
|
* @param {String} keyspaceName
|
||
|
* @param {Buffer|Token|TokenRange} token Can be Buffer (serialized partition key), Token or TokenRange
|
||
|
* @returns {Array}
|
||
|
*/
|
||
|
getReplicas(keyspaceName, token) {
|
||
|
if (!this.ring) {
|
||
|
return null;
|
||
|
}
|
||
|
if (Buffer.isBuffer(token)) {
|
||
|
token = this.tokenizer.hash(token);
|
||
|
}
|
||
|
if (token instanceof TokenRange) {
|
||
|
token = token.end;
|
||
|
}
|
||
|
let keyspace;
|
||
|
if (keyspaceName) {
|
||
|
keyspace = this.keyspaces[keyspaceName];
|
||
|
if (!keyspace) {
|
||
|
// the keyspace was not found, the metadata should be loaded beforehand
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
let i = utils.binarySearch(this.ring, token, (t1, t2) => t1.compare(t2));
|
||
|
if (i < 0) {
|
||
|
i = ~i;
|
||
|
}
|
||
|
if (i >= this.ring.length) {
|
||
|
//it circled back
|
||
|
i = i % this.ring.length;
|
||
|
}
|
||
|
const closestToken = this.ringTokensAsStrings[i];
|
||
|
if (!keyspaceName) {
|
||
|
return [this.primaryReplicas[closestToken]];
|
||
|
}
|
||
|
const replicas = this._getKeyspaceReplicas(keyspace);
|
||
|
return replicas[closestToken];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the token ranges that define data distribution in the ring.
|
||
|
*
|
||
|
* @returns {Set<TokenRange>} The ranges of the ring or empty set if schema metadata is not enabled.
|
||
|
*/
|
||
|
getTokenRanges() {
|
||
|
return this.tokenRanges;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the token ranges that are replicated on the given host, for
|
||
|
* the given keyspace.
|
||
|
*
|
||
|
* @param {String} keyspaceName The name of the keyspace to get ranges for.
|
||
|
* @param {Host} host The host.
|
||
|
* @returns {Set<TokenRange>|null} Ranges for the keyspace on this host or null if keyspace isn't found or hasn't been loaded.
|
||
|
*/
|
||
|
getTokenRangesForHost(keyspaceName, host) {
|
||
|
if (!this.ring) {
|
||
|
return null;
|
||
|
}
|
||
|
let keyspace;
|
||
|
if (keyspaceName) {
|
||
|
keyspace = this.keyspaces[keyspaceName];
|
||
|
if (!keyspace) {
|
||
|
// the keyspace was not found, the metadata should be loaded beforehand
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
// If the ring has only 1 token, just return the ranges as we should only have a single node cluster.
|
||
|
if (this.ring.length === 1) {
|
||
|
return this.getTokenRanges();
|
||
|
}
|
||
|
const replicas = this._getKeyspaceReplicas(keyspace);
|
||
|
const ranges = new Set();
|
||
|
// for each range, find replicas for end token, if replicas include host, add range.
|
||
|
this.tokenRanges.forEach((tokenRange) => {
|
||
|
const replicasForToken = replicas[this.tokenizer.stringify(tokenRange.end)];
|
||
|
if (replicasForToken.indexOf(host) !== -1) {
|
||
|
ranges.add(tokenRange);
|
||
|
}
|
||
|
});
|
||
|
return ranges;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Constructs a Token from the input buffer(s) or string input. If a string is passed in
|
||
|
* it is assumed this matches the token representation reported by cassandra.
|
||
|
* @param {Array<Buffer>|Buffer|String} components
|
||
|
* @returns {Token} constructed token from the input buffer.
|
||
|
*/
|
||
|
newToken(components) {
|
||
|
if (!this.tokenizer) {
|
||
|
throw new Error('Partitioner not established. This should only happen if metadata was disabled or you have not connected yet.');
|
||
|
}
|
||
|
if (Array.isArray(components)) {
|
||
|
return this.tokenizer.hash(Buffer.concat(components));
|
||
|
}
|
||
|
else if (util.isString(components)) {
|
||
|
return this.tokenizer.parse(components);
|
||
|
}
|
||
|
return this.tokenizer.hash(components);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Constructs a TokenRange from the given start and end tokens.
|
||
|
* @param {Token} start
|
||
|
* @param {Token} end
|
||
|
* @returns TokenRange build range spanning from start (exclusive) to end (inclusive).
|
||
|
*/
|
||
|
newTokenRange(start, end) {
|
||
|
if (!this.tokenizer) {
|
||
|
throw new Error('Partitioner not established. This should only happen if metadata was disabled or you have not connected yet.');
|
||
|
}
|
||
|
return new TokenRange(start, end, this.tokenizer);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the metadata information already stored associated to a prepared statement
|
||
|
* @param {String} keyspaceName
|
||
|
* @param {String} query
|
||
|
* @internal
|
||
|
* @ignore
|
||
|
*/
|
||
|
getPreparedInfo(keyspaceName, query) {
|
||
|
return this._preparedQueries.getOrAdd(keyspaceName, query);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clears the internal state related to the prepared statements.
|
||
|
* Following calls to the Client using the prepare flag will re-prepare the statements.
|
||
|
*/
|
||
|
clearPrepared() {
|
||
|
this._preparedQueries.clear();
|
||
|
}
|
||
|
|
||
|
/** @ignore */
|
||
|
getPreparedById(id) {
|
||
|
return this._preparedQueries.getById(id);
|
||
|
}
|
||
|
|
||
|
/** @ignore */
|
||
|
setPreparedById(info) {
|
||
|
return this._preparedQueries.setById(info);
|
||
|
}
|
||
|
|
||
|
/** @ignore */
|
||
|
getAllPrepared() {
|
||
|
return this._preparedQueries.getAll();
|
||
|
}
|
||
|
|
||
|
/** @ignore */
|
||
|
_uninitializedError() {
|
||
|
return new Error('Metadata has not been initialized. This could only happen if you have not connected yet.');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the definition of an user-defined type.
|
||
|
* <p>
|
||
|
* If a <code>callback</code> is provided, the callback is invoked when the metadata retrieval completes.
|
||
|
* Otherwise, it returns a <code>Promise</code>.
|
||
|
* </p>
|
||
|
* <p>
|
||
|
* When trying to retrieve the same UDT definition concurrently, it will query once and invoke all callbacks
|
||
|
* with the retrieved information.
|
||
|
* </p>
|
||
|
* @param {String} keyspaceName Name of the keyspace.
|
||
|
* @param {String} name Name of the UDT.
|
||
|
* @param {Function} [callback] The callback to invoke when retrieval completes.
|
||
|
*/
|
||
|
getUdt(keyspaceName, name, callback) {
|
||
|
return promiseUtils.optionalCallback(this._getUdt(keyspaceName, name), callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {String} keyspaceName
|
||
|
* @param {String} name
|
||
|
* @returns {Promise<Object|null>}
|
||
|
* @private
|
||
|
*/
|
||
|
async _getUdt(keyspaceName, name) {
|
||
|
if (!this.initialized) {
|
||
|
throw this._uninitializedError();
|
||
|
}
|
||
|
let cache;
|
||
|
if (this.options.isMetadataSyncEnabled) {
|
||
|
const keyspace = this.keyspaces[keyspaceName];
|
||
|
if (!keyspace) {
|
||
|
return null;
|
||
|
}
|
||
|
cache = keyspace.udts;
|
||
|
}
|
||
|
return await this._schemaParser.getUdt(keyspaceName, name, cache);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the definition of a table.
|
||
|
* <p>
|
||
|
* If a <code>callback</code> is provided, the callback is invoked when the metadata retrieval completes.
|
||
|
* Otherwise, it returns a <code>Promise</code>.
|
||
|
* </p>
|
||
|
* <p>
|
||
|
* When trying to retrieve the same table definition concurrently, it will query once and invoke all callbacks
|
||
|
* with the retrieved information.
|
||
|
* </p>
|
||
|
* @param {String} keyspaceName Name of the keyspace.
|
||
|
* @param {String} name Name of the Table.
|
||
|
* @param {Function} [callback] The callback with the err as a first parameter and the {@link TableMetadata} as
|
||
|
* second parameter.
|
||
|
*/
|
||
|
getTable(keyspaceName, name, callback) {
|
||
|
return promiseUtils.optionalCallback(this._getTable(keyspaceName, name), callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {String} keyspaceName
|
||
|
* @param {String} name
|
||
|
* @private
|
||
|
*/
|
||
|
async _getTable(keyspaceName, name) {
|
||
|
if (!this.initialized) {
|
||
|
throw this._uninitializedError();
|
||
|
}
|
||
|
let cache;
|
||
|
let virtual;
|
||
|
if (this.options.isMetadataSyncEnabled) {
|
||
|
const keyspace = this.keyspaces[keyspaceName];
|
||
|
if (!keyspace) {
|
||
|
return null;
|
||
|
}
|
||
|
cache = keyspace.tables;
|
||
|
virtual = keyspace.virtual;
|
||
|
}
|
||
|
return await this._schemaParser.getTable(keyspaceName, name, cache, virtual);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the definition of CQL functions for a given name.
|
||
|
* <p>
|
||
|
* If a <code>callback</code> is provided, the callback is invoked when the metadata retrieval completes.
|
||
|
* Otherwise, it returns a <code>Promise</code>.
|
||
|
* </p>
|
||
|
* <p>
|
||
|
* When trying to retrieve the same function definition concurrently, it will query once and invoke all callbacks
|
||
|
* with the retrieved information.
|
||
|
* </p>
|
||
|
* @param {String} keyspaceName Name of the keyspace.
|
||
|
* @param {String} name Name of the Function.
|
||
|
* @param {Function} [callback] The callback with the err as a first parameter and the array of {@link SchemaFunction}
|
||
|
* as second parameter.
|
||
|
*/
|
||
|
getFunctions(keyspaceName, name, callback) {
|
||
|
return promiseUtils.optionalCallback(this._getFunctionsWrapper(keyspaceName, name), callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {String} keyspaceName
|
||
|
* @param {String} name
|
||
|
* @private
|
||
|
*/
|
||
|
async _getFunctionsWrapper(keyspaceName, name) {
|
||
|
if (!keyspaceName || !name) {
|
||
|
throw new errors.ArgumentError('You must provide the keyspace name and cql function name to retrieve the metadata');
|
||
|
}
|
||
|
const functionsMap = await this._getFunctions(keyspaceName, name, false);
|
||
|
return Array.from(functionsMap.values());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets a definition of CQL function for a given name and signature.
|
||
|
* <p>
|
||
|
* If a <code>callback</code> is provided, the callback is invoked when the metadata retrieval completes.
|
||
|
* Otherwise, it returns a <code>Promise</code>.
|
||
|
* </p>
|
||
|
* <p>
|
||
|
* When trying to retrieve the same function definition concurrently, it will query once and invoke all callbacks
|
||
|
* with the retrieved information.
|
||
|
* </p>
|
||
|
* @param {String} keyspaceName Name of the keyspace
|
||
|
* @param {String} name Name of the Function
|
||
|
* @param {Array.<String>|Array.<{code, info}>} signature Array of types of the parameters.
|
||
|
* @param {Function} [callback] The callback with the err as a first parameter and the {@link SchemaFunction} as second
|
||
|
* parameter.
|
||
|
*/
|
||
|
getFunction(keyspaceName, name, signature, callback) {
|
||
|
return promiseUtils.optionalCallback(this._getSingleFunction(keyspaceName, name, signature, false), callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the definition of CQL aggregate for a given name.
|
||
|
* <p>
|
||
|
* If a <code>callback</code> is provided, the callback is invoked when the metadata retrieval completes.
|
||
|
* Otherwise, it returns a <code>Promise</code>.
|
||
|
* </p>
|
||
|
* <p>
|
||
|
* When trying to retrieve the same aggregates definition concurrently, it will query once and invoke all callbacks
|
||
|
* with the retrieved information.
|
||
|
* </p>
|
||
|
* @param {String} keyspaceName Name of the keyspace
|
||
|
* @param {String} name Name of the Function
|
||
|
* @param {Function} [callback] The callback with the err as a first parameter and the array of {@link Aggregate} as
|
||
|
* second parameter.
|
||
|
*/
|
||
|
getAggregates(keyspaceName, name, callback) {
|
||
|
return promiseUtils.optionalCallback(this._getAggregates(keyspaceName, name), callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {String} keyspaceName
|
||
|
* @param {String} name
|
||
|
* @private
|
||
|
*/
|
||
|
async _getAggregates(keyspaceName, name) {
|
||
|
if (!keyspaceName || !name) {
|
||
|
throw new errors.ArgumentError('You must provide the keyspace name and cql aggregate name to retrieve the metadata');
|
||
|
}
|
||
|
const functionsMap = await this._getFunctions(keyspaceName, name, true);
|
||
|
return Array.from(functionsMap.values());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets a definition of CQL aggregate for a given name and signature.
|
||
|
* <p>
|
||
|
* If a <code>callback</code> is provided, the callback is invoked when the metadata retrieval completes.
|
||
|
* Otherwise, it returns a <code>Promise</code>.
|
||
|
* </p>
|
||
|
* <p>
|
||
|
* When trying to retrieve the same aggregate definition concurrently, it will query once and invoke all callbacks
|
||
|
* with the retrieved information.
|
||
|
* </p>
|
||
|
* @param {String} keyspaceName Name of the keyspace
|
||
|
* @param {String} name Name of the aggregate
|
||
|
* @param {Array.<String>|Array.<{code, info}>} signature Array of types of the parameters.
|
||
|
* @param {Function} [callback] The callback with the err as a first parameter and the {@link Aggregate} as second parameter.
|
||
|
*/
|
||
|
getAggregate(keyspaceName, name, signature, callback) {
|
||
|
return promiseUtils.optionalCallback(this._getSingleFunction(keyspaceName, name, signature, true), callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the definition of a CQL materialized view for a given name.
|
||
|
* <p>
|
||
|
* If a <code>callback</code> is provided, the callback is invoked when the metadata retrieval completes.
|
||
|
* Otherwise, it returns a <code>Promise</code>.
|
||
|
* </p>
|
||
|
* <p>
|
||
|
* Note that, unlike the rest of the {@link Metadata} methods, this method does not cache the result for following
|
||
|
* calls, as the current version of the Cassandra native protocol does not support schema change events for
|
||
|
* materialized views. Each call to this method will produce one or more queries to the cluster.
|
||
|
* </p>
|
||
|
* @param {String} keyspaceName Name of the keyspace
|
||
|
* @param {String} name Name of the materialized view
|
||
|
* @param {Function} [callback] The callback with the err as a first parameter and the {@link MaterializedView} as
|
||
|
* second parameter.
|
||
|
*/
|
||
|
getMaterializedView(keyspaceName, name, callback) {
|
||
|
return promiseUtils.optionalCallback(this._getMaterializedView(keyspaceName, name), callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {String} keyspaceName
|
||
|
* @param {String} name
|
||
|
* @returns {Promise<MaterializedView|null>}
|
||
|
* @private
|
||
|
*/
|
||
|
async _getMaterializedView(keyspaceName, name) {
|
||
|
if (!this.initialized) {
|
||
|
throw this._uninitializedError();
|
||
|
}
|
||
|
let cache;
|
||
|
if (this.options.isMetadataSyncEnabled) {
|
||
|
const keyspace = this.keyspaces[keyspaceName];
|
||
|
if (!keyspace) {
|
||
|
return null;
|
||
|
}
|
||
|
cache = keyspace.views;
|
||
|
}
|
||
|
return await this._schemaParser.getMaterializedView(keyspaceName, name, cache);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets a map of cql function definitions or aggregates based on signature.
|
||
|
* @param {String} keyspaceName
|
||
|
* @param {String} name Name of the function or aggregate
|
||
|
* @param {Boolean} aggregate
|
||
|
* @returns {Promise<Map>}
|
||
|
* @private
|
||
|
*/
|
||
|
async _getFunctions(keyspaceName, name, aggregate) {
|
||
|
if (!this.initialized) {
|
||
|
throw this._uninitializedError();
|
||
|
}
|
||
|
let cache;
|
||
|
if (this.options.isMetadataSyncEnabled) {
|
||
|
const keyspace = this.keyspaces[keyspaceName];
|
||
|
if (!keyspace) {
|
||
|
return new Map();
|
||
|
}
|
||
|
cache = aggregate ? keyspace.aggregates : keyspace.functions;
|
||
|
}
|
||
|
return await this._schemaParser.getFunctions(keyspaceName, name, aggregate, cache);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets a single cql function or aggregate definition
|
||
|
* @param {String} keyspaceName
|
||
|
* @param {String} name
|
||
|
* @param {Array} signature
|
||
|
* @param {Boolean} aggregate
|
||
|
* @returns {Promise<SchemaFunction|Aggregate|null>}
|
||
|
* @private
|
||
|
*/
|
||
|
async _getSingleFunction(keyspaceName, name, signature, aggregate) {
|
||
|
if (!keyspaceName || !name) {
|
||
|
throw new errors.ArgumentError('You must provide the keyspace name and cql function name to retrieve the metadata');
|
||
|
}
|
||
|
if (!Array.isArray(signature)) {
|
||
|
throw new errors.ArgumentError('Signature must be an array of types');
|
||
|
}
|
||
|
signature = signature.map(item => {
|
||
|
if (typeof item === 'string') {
|
||
|
return item;
|
||
|
}
|
||
|
return types.getDataTypeNameByCode(item);
|
||
|
});
|
||
|
const functionsMap = await this._getFunctions(keyspaceName, name, aggregate);
|
||
|
return functionsMap.get(signature.join(',')) || null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the trace session generated by Cassandra when query tracing is enabled for the
|
||
|
* query. The trace itself is stored in Cassandra in the <code>sessions</code> and
|
||
|
* <code>events</code> table in the <code>system_traces</code> keyspace and can be
|
||
|
* retrieve manually using the trace identifier.
|
||
|
* <p>
|
||
|
* If a <code>callback</code> is provided, the callback is invoked when the metadata retrieval completes.
|
||
|
* Otherwise, it returns a <code>Promise</code>.
|
||
|
* </p>
|
||
|
* @param {Uuid} traceId Identifier of the trace session.
|
||
|
* @param {Number} [consistency] The consistency level to obtain the trace.
|
||
|
* @param {Function} [callback] The callback with the err as first parameter and the query trace as second parameter.
|
||
|
*/
|
||
|
getTrace(traceId, consistency, callback) {
|
||
|
if (!callback && typeof consistency === 'function') {
|
||
|
// Both callback and consistency are optional parameters
|
||
|
// In this case, the second parameter is the callback
|
||
|
callback = consistency;
|
||
|
consistency = null;
|
||
|
}
|
||
|
|
||
|
return promiseUtils.optionalCallback(this._getTrace(traceId, consistency), callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Uuid} traceId
|
||
|
* @param {Number} consistency
|
||
|
* @returns {Promise<Object>}
|
||
|
* @private
|
||
|
*/
|
||
|
async _getTrace(traceId, consistency) {
|
||
|
if (!this.initialized) {
|
||
|
throw this._uninitializedError();
|
||
|
}
|
||
|
|
||
|
let trace;
|
||
|
let attempts = 0;
|
||
|
const info = ExecutionOptions.empty();
|
||
|
info.getConsistency = () => consistency;
|
||
|
|
||
|
const sessionRequest = new requests.QueryRequest(util.format(_selectTraceSession, traceId), null, info);
|
||
|
const eventsRequest = new requests.QueryRequest(util.format(_selectTraceEvents, traceId), null, info);
|
||
|
|
||
|
while (!trace && (attempts++ < _traceMaxAttemps)) {
|
||
|
const sessionResponse = await this.controlConnection.query(sessionRequest);
|
||
|
const sessionRow = sessionResponse.rows[0];
|
||
|
|
||
|
if (!sessionRow || typeof sessionRow['duration'] !== 'number') {
|
||
|
await promiseUtils.delay(_traceAttemptDelay);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
trace = {
|
||
|
requestType: sessionRow['request'],
|
||
|
coordinator: sessionRow['coordinator'],
|
||
|
parameters: sessionRow['parameters'],
|
||
|
startedAt: sessionRow['started_at'],
|
||
|
duration: sessionRow['duration'],
|
||
|
clientAddress: sessionRow['client'],
|
||
|
events: null
|
||
|
};
|
||
|
|
||
|
const eventsResponse = await this.controlConnection.query(eventsRequest);
|
||
|
trace.events = eventsResponse.rows.map(row => ({
|
||
|
id: row['event_id'],
|
||
|
activity: row['activity'],
|
||
|
source: row['source'],
|
||
|
elapsed: row['source_elapsed'],
|
||
|
thread: row['thread']
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
if (!trace) {
|
||
|
throw new Error(`Trace ${traceId.toString()} could not fully retrieved after ${_traceMaxAttemps} attempts`);
|
||
|
}
|
||
|
|
||
|
return trace;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks whether hosts that are currently up agree on the schema definition.
|
||
|
* <p>
|
||
|
* This method performs a one-time check only, without any form of retry; therefore
|
||
|
* <code>protocolOptions.maxSchemaAgreementWaitSeconds</code> setting does not apply in this case.
|
||
|
* </p>
|
||
|
* @param {Function} [callback] A function that is invoked with a value
|
||
|
* <code>true</code> when all hosts agree on the schema and <code>false</code> when there is no agreement or when
|
||
|
* the check could not be performed (for example, if the control connection is down).
|
||
|
* @returns {Promise} Returns a <code>Promise</code> when a callback is not provided. The promise resolves to
|
||
|
* <code>true</code> when all hosts agree on the schema and <code>false</code> when there is no agreement or when
|
||
|
* the check could not be performed (for example, if the control connection is down).
|
||
|
*/
|
||
|
checkSchemaAgreement(callback) {
|
||
|
return promiseUtils.optionalCallback(this._checkSchemaAgreement(), callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Async-only version of check schema agreement.
|
||
|
* @private
|
||
|
*/
|
||
|
async _checkSchemaAgreement() {
|
||
|
const connection = this.controlConnection.connection;
|
||
|
if (!connection) {
|
||
|
return false;
|
||
|
}
|
||
|
try {
|
||
|
return await this.compareSchemaVersions(connection);
|
||
|
}
|
||
|
catch (err) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Uses the metadata to fill the user provided parameter hints
|
||
|
* @param {String} keyspace
|
||
|
* @param {Array} hints
|
||
|
* @internal
|
||
|
* @ignore
|
||
|
*/
|
||
|
async adaptUserHints(keyspace, hints) {
|
||
|
if (!Array.isArray(hints)) {
|
||
|
return;
|
||
|
}
|
||
|
const udts = [];
|
||
|
// Check for udts and get the metadata
|
||
|
for (let i = 0; i < hints.length; i++) {
|
||
|
const hint = hints[i];
|
||
|
if (typeof hint !== 'string') {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const type = types.dataTypes.getByName(hint);
|
||
|
this._checkUdtTypes(udts, type, keyspace);
|
||
|
hints[i] = type;
|
||
|
}
|
||
|
|
||
|
for (const type of udts) {
|
||
|
const udtInfo = await this.getUdt(type.info.keyspace, type.info.name);
|
||
|
if (!udtInfo) {
|
||
|
throw new TypeError('User defined type not found: ' + type.info.keyspace + '.' + type.info.name);
|
||
|
}
|
||
|
type.info = udtInfo;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Array} udts
|
||
|
* @param {{code, info}} type
|
||
|
* @param {string} keyspace
|
||
|
* @private
|
||
|
*/
|
||
|
_checkUdtTypes(udts, type, keyspace) {
|
||
|
if (type.code === types.dataTypes.udt) {
|
||
|
const udtName = type.info.split('.');
|
||
|
type.info = {
|
||
|
keyspace: udtName[0],
|
||
|
name: udtName[1]
|
||
|
};
|
||
|
if (!type.info.name) {
|
||
|
if (!keyspace) {
|
||
|
throw new TypeError('No keyspace specified for udt: ' + udtName.join('.'));
|
||
|
}
|
||
|
//use the provided keyspace
|
||
|
type.info.name = type.info.keyspace;
|
||
|
type.info.keyspace = keyspace;
|
||
|
}
|
||
|
udts.push(type);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!type.info) {
|
||
|
return;
|
||
|
}
|
||
|
if (type.code === types.dataTypes.list || type.code === types.dataTypes.set) {
|
||
|
return this._checkUdtTypes(udts, type.info, keyspace);
|
||
|
}
|
||
|
if (type.code === types.dataTypes.map) {
|
||
|
this._checkUdtTypes(udts, type.info[0], keyspace);
|
||
|
this._checkUdtTypes(udts, type.info[1], keyspace);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Uses the provided connection to query the schema versions and compare them.
|
||
|
* @param {Connection} connection
|
||
|
* @internal
|
||
|
* @ignore
|
||
|
*/
|
||
|
async compareSchemaVersions(connection) {
|
||
|
const versions = new Set();
|
||
|
const response1 = await connection.send(new requests.QueryRequest(_selectSchemaVersionLocal), null);
|
||
|
if (response1 && response1.rows && response1.rows.length === 1) {
|
||
|
versions.add(response1.rows[0]['schema_version'].toString());
|
||
|
}
|
||
|
const response2 = await connection.send(new requests.QueryRequest(_selectSchemaVersionPeers), null);
|
||
|
if (response2 && response2.rows) {
|
||
|
for (const row of response2.rows) {
|
||
|
const value = row['schema_version'];
|
||
|
if (!value) {
|
||
|
continue;
|
||
|
}
|
||
|
versions.add(value.toString());
|
||
|
}
|
||
|
}
|
||
|
return versions.size === 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Allows to store prepared queries and retrieval by query or query id.
|
||
|
* @ignore
|
||
|
*/
|
||
|
class PreparedQueries {
|
||
|
|
||
|
/**
|
||
|
* @param {Number} maxPrepared
|
||
|
* @param {Function} logger
|
||
|
*/
|
||
|
constructor(maxPrepared, logger) {
|
||
|
this.length = 0;
|
||
|
this._maxPrepared = maxPrepared;
|
||
|
this._mapByKey = new Map();
|
||
|
this._mapById = new Map();
|
||
|
this._logger = logger;
|
||
|
}
|
||
|
|
||
|
_getKey(keyspace, query) {
|
||
|
return (keyspace || '') + query;
|
||
|
}
|
||
|
|
||
|
getOrAdd(keyspace, query) {
|
||
|
const key = this._getKey(keyspace, query);
|
||
|
let info = this._mapByKey.get(key);
|
||
|
if (info) {
|
||
|
return info;
|
||
|
}
|
||
|
|
||
|
this._validateOverflow();
|
||
|
|
||
|
info = new events.EventEmitter();
|
||
|
info.setMaxListeners(0);
|
||
|
info.query = query;
|
||
|
// The keyspace in which it was prepared
|
||
|
info.keyspace = keyspace;
|
||
|
this._mapByKey.set(key, info);
|
||
|
this.length++;
|
||
|
return info;
|
||
|
}
|
||
|
|
||
|
_validateOverflow() {
|
||
|
if (this.length < this._maxPrepared) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const toRemove = [];
|
||
|
this._logger('warning',
|
||
|
'Prepared statements exceeded maximum. This could be caused by preparing queries that contain parameters');
|
||
|
|
||
|
const toRemoveLength = this.length - this._maxPrepared + 1;
|
||
|
|
||
|
for (const [key, info] of this._mapByKey) {
|
||
|
if (!info.queryId) {
|
||
|
// Only remove queries that contain queryId
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const length = toRemove.push([key, info]);
|
||
|
if (length >= toRemoveLength) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for (const [key, info] of toRemove) {
|
||
|
this._mapByKey.delete(key);
|
||
|
this._mapById.delete(info.queryId.toString('hex'));
|
||
|
this.length--;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
setById(info) {
|
||
|
this._mapById.set(info.queryId.toString('hex'), info);
|
||
|
}
|
||
|
|
||
|
getById(id) {
|
||
|
return this._mapById.get(id.toString('hex'));
|
||
|
}
|
||
|
|
||
|
clear() {
|
||
|
this._mapByKey = new Map();
|
||
|
this._mapById = new Map();
|
||
|
this.length = 0;
|
||
|
}
|
||
|
|
||
|
getAll() {
|
||
|
return Array.from(this._mapByKey.values()).filter(info => !!info.queryId);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = Metadata;
|