/* * 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 util = require('util'); const events = require('events'); const types = require('../types'); const utils = require('../utils'); const errors = require('../errors'); const promiseUtils = require('../promise-utils'); const TableMetadata = require('./table-metadata'); const Aggregate = require('./aggregate'); const SchemaFunction = require('./schema-function'); const Index = require('./schema-index'); const MaterializedView = require('./materialized-view'); const { format } = util; /** * @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 */ class SchemaParser { constructor(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}} * @protected */ _createKeyspace(name, durableWrites, strategy, strategyOptions, virtual) { return { name, durableWrites, strategy, strategyOptions, virtual: virtual === true, udts: {}, tables: {}, functions: {}, aggregates: {}, views: {}, tokenToReplica: getTokenToReplicaMapper(strategy, strategyOptions), graphEngine: undefined }; } /** * @abstract * @param {String} name * @returns {Promise} */ getKeyspace(name) { } /** * @abstract * @param {Boolean} waitReconnect * @returns {Promise>} */ getKeyspaces(waitReconnect) { } /** * @param {String} keyspaceName * @param {String} name * @param {Object} cache * @param {Boolean} virtual * @returns {Promise} */ async getTable(keyspaceName, name, cache, virtual) { let tableInfo = cache && cache[name]; if (!tableInfo) { tableInfo = new TableMetadata(name); if (cache) { cache[name] = tableInfo; } } if (tableInfo.loaded) { return tableInfo; } if (tableInfo.loading) { // Wait for it to emit return promiseUtils.fromEvent(tableInfo, 'load'); } try { // its not cached and not being retrieved tableInfo.loading = true; let indexRows; let virtualTable = virtual; const selectTable = virtualTable ? _selectVirtualTable : this.selectTable; const query = util.format(selectTable, keyspaceName, name); let tableRow = await this._getFirstRow(query); // if we weren't sure if table was virtual or not, query virtual schema. if (!tableRow && this.supportsVirtual && virtualTable === undefined) { const query = util.format(_selectVirtualTable, keyspaceName, name); try { tableRow = await this._getFirstRow(query); } catch (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. } if (tableRow) { // We are fetching a virtual table virtualTable = true; } } if (!tableRow) { tableInfo.loading = false; tableInfo.emit('load', null, null); return null; } const selectColumns = virtualTable ? _selectVirtualColumns : this.selectColumns; const columnRows = await this._getRows(util.format(selectColumns, keyspaceName, name)); if (this.selectIndexes && !virtualTable) { indexRows = await this._getRows(util.format(this.selectIndexes, keyspaceName, name)); } await this._parseTableOrView(tableInfo, tableRow, columnRows, indexRows, virtualTable); tableInfo.loaded = true; tableInfo.emit('load', null, tableInfo); return tableInfo; } catch (err) { tableInfo.emit('load', err, null); throw err; } finally { tableInfo.loading = false; } } async _getFirstRow(query) { const rows = await this._getRows(query); return rows[0]; } async _getRows(query) { const response = await this.cc.query(query); return response.rows; } /** * @param {String} keyspaceName * @param {String} name * @param {Object} cache * @returns {Promise} */ async getUdt(keyspaceName, name, cache) { 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.keyspace = keyspaceName; udtInfo.fields = null; } if (udtInfo.fields) { return udtInfo; } if (udtInfo.loading) { return promiseUtils.fromEvent(udtInfo, 'load'); } udtInfo.loading = true; const query = format(this.selectUdt, keyspaceName, name); try { const row = await this._getFirstRow(query); if (!row) { udtInfo.loading = false; udtInfo.emit('load', null, null); return null; } await this._parseUdt(udtInfo, row); udtInfo.emit('load', null, udtInfo); return udtInfo; } catch (err) { udtInfo.emit('load', err); throw err; } finally { udtInfo.loading = false; } } /** * Parses the udt information from the row * @param udtInfo * @param {Row} row * @returns {Promise} * @abstract */ _parseUdt(udtInfo, row) { } /** * Builds the metadata based on the table and column rows * @abstract * @param {module:metadata~TableMetadata} tableInfo * @param {Row} tableRow * @param {Array.} columnRows * @param {Array.} indexRows * @param {Boolean} virtual * @returns {Promise} * @throws {Error} */ async _parseTableOrView(tableInfo, tableRow, columnRows, indexRows, virtual) { } /** * @abstract * @param {String} keyspaceName * @param {String} name * @param {Object} cache * @returns {Promise} */ getMaterializedView(keyspaceName, name, cache) { } /** * @param {String} keyspaceName * @param {String} name * @param {Boolean} aggregate * @param {Object} cache * @returns {Promise} */ async getFunctions(keyspaceName, name, aggregate, cache) { /** @type {String} */ let query = this.selectFunctions; let parser = row => this._parseFunction(row); if (aggregate) { query = this.selectAggregates; parser = row => this._parseAggregate(row); } // if it's 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 functionsInfo.values; } if (functionsInfo.loading) { return promiseUtils.fromEvent(functionsInfo, 'load'); } functionsInfo.loading = true; try { const rows = await this._getRows(format(query, keyspaceName, name)); const funcs = await Promise.all(rows.map(parser)); const result = new Map(); if (rows.length > 0) { // Cache positive hits functionsInfo.values = result; } funcs.forEach(f => functionsInfo.values.set(f.signature.join(','), f)); functionsInfo.emit('load', null, result); return result; } catch (err) { functionsInfo.emit('load', err); throw err; } finally { functionsInfo.loading = false; } } /** * @abstract * @param {Row} row * @returns {Promise} */ _parseAggregate(row) { } /** * @abstract * @param {Row} row * @returns {Promise} */ _parseFunction(row) { } /** @returns {Map} */ _asMap(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]])); } _mapAsObject(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 * @ignore */ class SchemaParserV1 extends SchemaParser { /** * @param {ClientOptions} options * @param {ControlConnection} cc */ constructor(options, cc) { super(options, cc); this.selectTable = _selectTableV1; this.selectColumns = _selectColumnsV1; this.selectUdt = _selectUdtV1; this.selectAggregates = _selectAggregatesV1; this.selectFunctions = _selectFunctionsV1; } async getKeyspaces(waitReconnect) { const keyspaces = {}; const result = await this.cc.query(_selectAllKeyspacesV1, waitReconnect); for (let i = 0; i < result.rows.length; i++) { const row = result.rows[i]; const ksInfo = this._createKeyspace(row['keyspace_name'], row['durable_writes'], row['strategy_class'], JSON.parse(row['strategy_options'] || null)); keyspaces[ksInfo.name] = ksInfo; } return keyspaces; } async getKeyspace(name) { const row = await this._getFirstRow(format(_selectSingleKeyspaceV1, name)); if (!row) { return null; } return this._createKeyspace(row['keyspace_name'], row['durable_writes'], row['strategy_class'], JSON.parse(row['strategy_options'])); } // eslint-disable-next-line require-await async _parseTableOrView(tableInfo, tableRow, columnRows, indexRows, virtual) { // All the tableInfo parsing in V1 is sync, it uses a async function because the super class defines one // to support other versions. let 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 = []; for (let i = 0; i < columnRows.length; i++) { const row = columnRows[i]; const type = encoder.parseFqTypeName(row['validator']); c = { name: row['column_name'], type: type, isStatic: false }; 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; case 'static': // C* 2.0.6+ supports static columns c.isStatic = true; break; } } if (partitionKeys.length > 0) { tableInfo.partitionKeys = partitionKeys.sort(utils.propCompare('index')).map(item => item.c); clusteringKeys.sort(utils.propCompare('index')); tableInfo.clusteringKeys = clusteringKeys.map(item => item.c); tableInfo.clusteringOrder = clusteringKeys.map(item => 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 (let 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 (let 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); } getMaterializedView(keyspaceName, name, cache) { return Promise.reject(new errors.NotSupportedError('Materialized views are not supported on Cassandra versions below 3.0')); } // eslint-disable-next-line require-await async _parseAggregate(row) { 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']; aggregate.argumentTypes = (row['argument_types'] || utils.emptyArray).map(name => 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']); return aggregate; } // eslint-disable-next-line require-await async _parseFunction(row) { 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']; func.argumentTypes = (row['argument_types'] || utils.emptyArray).map(name => encoder.parseFqTypeName(name)); func.returnType = encoder.parseFqTypeName(row['return_type']); return func; } // eslint-disable-next-line require-await async _parseUdt(udtInfo, row) { const encoder = this.cc.getEncoder(); const fieldNames = row['field_names']; const fieldTypes = row['field_types']; const fields = new Array(fieldNames.length); for (let i = 0; i < fieldNames.length; i++) { fields[i] = { name: fieldNames[i], type: encoder.parseFqTypeName(fieldTypes[i]) }; } udtInfo.fields = fields; return 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. * @ignore */ class SchemaParserV2 extends SchemaParser { /** * @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(options, cc, udtResolver) { super(options, cc); this.udtResolver = udtResolver; this.selectTable = _selectTableV2; this.selectColumns = _selectColumnsV2; this.selectUdt = _selectUdtV2; this.selectAggregates = _selectAggregatesV2; this.selectFunctions = _selectFunctionsV2; this.selectIndexes = _selectIndexesV2; } async getKeyspaces(waitReconnect) { const keyspaces = {}; const result = await this.cc.query(_selectAllKeyspacesV2, waitReconnect); for (let i = 0; i < result.rows.length; i++) { const ksInfo = this._parseKeyspace(result.rows[i]); keyspaces[ksInfo.name] = ksInfo; } return keyspaces; } async getKeyspace(name) { const row = await this._getFirstRow(format(_selectSingleKeyspaceV2, name)); if (!row) { return null; } return this._parseKeyspace(row); } async getMaterializedView(keyspaceName, name, cache) { let viewInfo = cache && cache[name]; if (!viewInfo) { viewInfo = new MaterializedView(name); if (cache) { cache[name] = viewInfo; } } if (viewInfo.loaded) { return viewInfo; } if (viewInfo.loading) { return promiseUtils.fromEvent(viewInfo, 'load'); } viewInfo.loading = true; try { const tableRow = await this._getFirstRow(format(_selectMaterializedViewV2, keyspaceName, name)); if (!tableRow) { viewInfo.emit('load', null, null); viewInfo.loading = false; return null; } const columnRows = await this._getRows(format(this.selectColumns, keyspaceName, name)); await this._parseTableOrView(viewInfo, tableRow, columnRows, null, false); viewInfo.loaded = true; viewInfo.emit('load', null, viewInfo); return viewInfo; } catch (err) { viewInfo.emit('load', err); throw err; } finally { viewInfo.loading = false; } } _parseKeyspace(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]; } } const ks = this._createKeyspace(row['keyspace_name'], row['durable_writes'], strategy, strategyOptions, virtual); ks.graphEngine = row['graph_engine']; return ks; } async _parseTableOrView(tableInfo, tableRow, columnRows, indexRows, virtual) { const encoder = this.cc.getEncoder(); const columnsKeyed = {}; const partitionKeys = []; const clusteringKeys = []; tableInfo.columns = await Promise.all(columnRows.map(async (row) => { const type = await encoder.parseTypeName(tableRow['keyspace_name'], row['type'], 0, null, this.udtResolver); const c = { name: row['column_name'], type: type, isStatic: false }; columnsKeyed[c.name] = c; switch (row['kind']) { case 'partition_key': partitionKeys.push({ c, index: (row['position'] || 0) }); break; case 'clustering': clusteringKeys.push({ c, index: (row['position'] || 0), order: row['clustering_order'] === 'desc' ? 'DESC' : 'ASC' }); break; case 'static': c.isStatic = true; break; } return c; })); tableInfo.columnsByName = columnsKeyed; tableInfo.partitionKeys = partitionKeys.sort(utils.propCompare('index')).map(item => item.c); clusteringKeys.sort(utils.propCompare('index')); tableInfo.clusteringKeys = clusteringKeys.map(item => item.c); tableInfo.clusteringOrder = clusteringKeys.map(item => item.order); if (virtual) { // When table is virtual, the only relevant information to parse are the columns // as the table itself has no configuration tableInfo.virtual = true; 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 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 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; tableInfo.nodesync = tableRow['nodesync'] || tableInfo.nodesync; if (!isView) { const cdc = tableRow['cdc']; if (cdc !== undefined) { tableInfo.cdc = cdc; } } if (isView) { tableInfo.tableName = tableRow['base_table_name']; tableInfo.whereClause = tableRow['where_clause']; tableInfo.includeAllColumns = tableRow['include_all_columns']; return; } 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); } } _getIndexes(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); }); } async _parseAggregate(row) { 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; aggregate.deterministic = row['deterministic'] || false; aggregate.argumentTypes = await Promise.all(aggregate.signature.map(name => encoder.parseTypeName(row['keyspace_name'], name, 0, null, this.udtResolver))); aggregate.stateType = await encoder.parseTypeName(row['keyspace_name'], row['state_type'], 0, null, this.udtResolver); aggregate.returnType = await encoder.parseTypeName(row['keyspace_name'], row['return_type'], 0, null, this.udtResolver); return aggregate; } async _parseFunction(row) { 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']; func.deterministic = row['deterministic'] || false; func.monotonic = row['monotonic'] || false; func.monotonicOn = row['monotonic_on'] || utils.emptyArray; func.argumentTypes = await Promise.all(func.signature.map(name => encoder.parseTypeName(row['keyspace_name'], name, 0, null, this.udtResolver))); func.returnType = await encoder.parseTypeName(row['keyspace_name'], row['return_type'], 0, null, this.udtResolver); return func; } async _parseUdt(udtInfo, row) { const encoder = this.cc.getEncoder(); const fieldTypes = row['field_types']; const keyspace = row['keyspace_name']; udtInfo.fields = await Promise.all(row['field_names'].map(async (name, i) => { const type = await encoder.parseTypeName(keyspace, fieldTypes[i], 0, null, this.udtResolver); return { name, type }; })); return udtInfo; } } /** * Used to parse schema information for Cassandra versions 4.x and above. * * This parser similar to [SchemaParserV2] expect it also parses virtual * keyspaces. * @ignore */ class SchemaParserV3 extends SchemaParserV2 { /** * @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(options, cc, udtResolver) { super(options, cc, udtResolver); this.supportsVirtual = true; } async getKeyspaces(waitReconnect) { const keyspaces = {}; const queries = [ { query: _selectAllKeyspacesV2, virtual: false }, { query: _selectAllVirtualKeyspaces, virtual: true } ]; await Promise.all(queries.map(async (q) => { let result = null; try { result = await this.cc.query(q.query, waitReconnect); } catch (err) { if (q.virtual) { // Only throw error for non-virtual query as // server reporting C* 4.0 may not actually implement // virtual tables. return; } throw err; } for (let i = 0; i < result.rows.length; i++) { const ksInfo = this._parseKeyspace(result.rows[i], q.virtual); keyspaces[ksInfo.name] = ksInfo; } })); return keyspaces; } async getKeyspace(name) { const ks = await this._getKeyspace(_selectSingleKeyspaceV2, name, false); if (!ks) { // if not found, attempt to retrieve as virtual keyspace. return this._getKeyspace(_selectSingleVirtualKeyspace, name, true); } return ks; } async _getKeyspace(query, name, virtual) { try { const row = await this._getFirstRow(format(query, name)); if (!row) { return null; } return this._parseKeyspace(row, virtual); } catch (err) { if (virtual) { // only throw error for non-virtual query as // server reporting C* 4.0 may not actually implement // virtual tables. return null; } throw err; } } } /** * 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.} [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; exports.isDoneForToken = isDoneForToken;