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.
322 lines
11 KiB
JavaScript
322 lines
11 KiB
JavaScript
/*
|
|
* 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 keyMatches = {
|
|
all: 1,
|
|
none: 0,
|
|
some: -1
|
|
};
|
|
|
|
/**
|
|
* Provides utility methods to choose the correct tables and views that should be included in a statement.
|
|
* @ignore
|
|
*/
|
|
class ObjectSelector {
|
|
/**
|
|
* Gets the table/view that should be used to execute the SELECT query.
|
|
* @param {Client} client
|
|
* @param {ModelMappingInfo} info
|
|
* @param {Boolean} allPKsDefined
|
|
* @param {Array} propertiesInfo
|
|
* @param {Array} fieldsInfo
|
|
* @param {Array<Array<String>>} orderByColumns
|
|
* @return {Promise<String>} A promise that resolves to a table names.
|
|
*/
|
|
static getForSelect(client, info, allPKsDefined, propertiesInfo, fieldsInfo, orderByColumns) {
|
|
return Promise.all(
|
|
info.tables.map(t => {
|
|
if (t.isView) {
|
|
return client.metadata.getMaterializedView(info.keyspace, t.name);
|
|
}
|
|
return client.metadata.getTable(info.keyspace, t.name);
|
|
}))
|
|
.then(tables => {
|
|
for (let i = 0; i < tables.length; i++) {
|
|
const table = tables[i];
|
|
if (table === null) {
|
|
throw new Error(`Table "${info.tables[i].name}" could not be retrieved`);
|
|
}
|
|
|
|
if (keysAreIncluded(table.partitionKeys, propertiesInfo) !== keyMatches.all) {
|
|
// Not all the partition keys are covered
|
|
continue;
|
|
}
|
|
|
|
|
|
if (allPKsDefined) {
|
|
if (keysAreIncluded(table.clusteringKeys, propertiesInfo) !== keyMatches.all) {
|
|
// All clustering keys should be included as allPKsDefined flag is set
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (propertiesInfo.length > table.partitionKeys.length) {
|
|
// Check that the Where clause is composed by partition and clustering keys
|
|
const allPropertiesArePrimaryKeys = propertiesInfo
|
|
.reduce(
|
|
(acc, p) => acc && (
|
|
contains(table.partitionKeys, c => c.name === p.columnName) ||
|
|
contains(table.clusteringKeys, c => c.name === p.columnName)
|
|
),
|
|
true);
|
|
|
|
if (!allPropertiesArePrimaryKeys) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// All fields must be contained
|
|
const containsAllFields = fieldsInfo
|
|
.reduce((acc, p) => acc && table.columnsByName[p.columnName] !== undefined, true);
|
|
|
|
if (!containsAllFields) {
|
|
continue;
|
|
}
|
|
|
|
// CQL:
|
|
// - "ORDER BY" is currently only supported on the clustered columns of the PRIMARY KEY
|
|
// - "ORDER BY" currently only support the ordering of columns following their declared order in
|
|
// the PRIMARY KEY
|
|
//
|
|
// In the mapper, we validate that the ORDER BY columns appear in the same order as in the clustering keys
|
|
const containsAllOrderByColumns = orderByColumns
|
|
.reduce((acc, order, index) => {
|
|
if (!acc) {
|
|
return false;
|
|
}
|
|
|
|
const ck = table.clusteringKeys[index];
|
|
|
|
return ck && ck.name === order[0];
|
|
}, true);
|
|
|
|
if (!containsAllOrderByColumns) {
|
|
continue;
|
|
}
|
|
|
|
return table.name;
|
|
}
|
|
|
|
let message = `No table matches the filter (${allPKsDefined ? 'all PKs have to be specified' : 'PKs'}): [${
|
|
propertiesInfo.map(p => p.columnName)}]`;
|
|
|
|
if (fieldsInfo.length > 0) {
|
|
message += `; fields: [${fieldsInfo.map(p => p.columnName)}]`;
|
|
}
|
|
if (orderByColumns.length > 0) {
|
|
message += `; orderBy: [${orderByColumns.map(item => item[0])}]`;
|
|
}
|
|
|
|
throw new Error(message);
|
|
});
|
|
}
|
|
|
|
/** Returns the name of the first table */
|
|
static getForSelectAll(info) {
|
|
return info.tables[0].name;
|
|
}
|
|
|
|
/**
|
|
* Gets the tables that should be used to execute the INSERT query.
|
|
* @param {Client} client
|
|
* @param {ModelMappingInfo} info
|
|
* @param {Array} propertiesInfo
|
|
* @return {Promise<Array<TableMetadata>>} A promise that resolves to an Array of tables.
|
|
*/
|
|
static getForInsert(client, info, propertiesInfo) {
|
|
return Promise.all(info.tables.filter(t => !t.isView).map(t => client.metadata.getTable(info.keyspace, t.name)))
|
|
.then(tables => {
|
|
const filteredTables = tables
|
|
.filter((table, i) => {
|
|
if (table === null) {
|
|
throw new Error(`Table "${info.tables[i].name}" could not be retrieved`);
|
|
}
|
|
|
|
if (keysAreIncluded(table.partitionKeys, propertiesInfo) !== keyMatches.all) {
|
|
// Not all the partition keys are covered
|
|
return false;
|
|
}
|
|
|
|
const clusteringKeyMatches = keysAreIncluded(table.clusteringKeys, propertiesInfo);
|
|
|
|
// All clustering keys should be included or it can be inserting a static column value
|
|
if (clusteringKeyMatches === keyMatches.all) {
|
|
return true;
|
|
}
|
|
|
|
if (clusteringKeyMatches === keyMatches.some) {
|
|
return false;
|
|
}
|
|
|
|
const staticColumns = staticColumnCount(table);
|
|
return propertiesInfo.length === table.partitionKeys.length + staticColumns && staticColumns > 0;
|
|
});
|
|
|
|
if (filteredTables.length === 0) {
|
|
throw new Error(`No table matches (all PKs have to be specified) fields: [${
|
|
propertiesInfo.map(p => p.columnName)}]`);
|
|
}
|
|
|
|
return filteredTables;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets the tables that should be used to execute the UPDATE query.
|
|
* @param {Client} client
|
|
* @param {ModelMappingInfo} info
|
|
* @param {Array} propertiesInfo
|
|
* @param {Array} when
|
|
* @return {Promise<Array<TableMetadata>>} A promise that resolves to an Array of tables.
|
|
*/
|
|
static getForUpdate(client, info, propertiesInfo, when) {
|
|
return Promise.all(info.tables.filter(t => !t.isView).map(t => client.metadata.getTable(info.keyspace, t.name)))
|
|
.then(tables => {
|
|
const filteredTables = tables
|
|
.filter((table, i) => {
|
|
if (table === null) {
|
|
throw new Error(`Table "${info.tables[i].name}" could not be retrieved`);
|
|
}
|
|
|
|
if (keysAreIncluded(table.partitionKeys, propertiesInfo) !== keyMatches.all) {
|
|
// Not all the partition keys are covered
|
|
return false;
|
|
}
|
|
|
|
const clusteringKeyMatches = keysAreIncluded(table.clusteringKeys, propertiesInfo);
|
|
|
|
// All clustering keys should be included or it can be updating a static column value
|
|
if (clusteringKeyMatches === keyMatches.some) {
|
|
return false;
|
|
}
|
|
|
|
if (clusteringKeyMatches === keyMatches.none && !hasStaticColumn(table)) {
|
|
return false;
|
|
}
|
|
|
|
const applicableColumns = propertiesInfo
|
|
.reduce((acc, p) => acc + (table.columnsByName[p.columnName] !== undefined ? 1 : 0), 0);
|
|
|
|
if (applicableColumns <= table.partitionKeys.length + table.clusteringKeys.length) {
|
|
if (!hasStaticColumn(table) || applicableColumns <= table.partitionKeys.length) {
|
|
// UPDATE statement does not contain columns to SET
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// "when" conditions should be contained in the table
|
|
return when.reduce((acc, p) => acc && table.columnsByName[p.columnName] !== undefined, true);
|
|
});
|
|
|
|
if (filteredTables.length === 0) {
|
|
let message = `No table matches (all PKs and columns to set have to be specified) fields: [${
|
|
propertiesInfo.map(p => p.columnName)}]`;
|
|
|
|
if (when.length > 0) {
|
|
message += `; condition: [${when.map(p => p.columnName)}]`;
|
|
}
|
|
|
|
throw new Error(message);
|
|
}
|
|
|
|
return filteredTables;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets the tables that should be used to execute the DELETE query.
|
|
* @param {Client} client
|
|
* @param {ModelMappingInfo} info
|
|
* @param {Array} propertiesInfo
|
|
* @param {Array} when
|
|
* @return {Promise<Array<TableMetadata>>} A promise that resolves to an Array of tables.
|
|
*/
|
|
static getForDelete(client, info, propertiesInfo, when) {
|
|
return Promise.all(info.tables.filter(t => !t.isView).map(t => client.metadata.getTable(info.keyspace, t.name)))
|
|
.then(tables => {
|
|
const filteredTables = tables
|
|
.filter((table, i) => {
|
|
if (table === null) {
|
|
throw new Error(`Table "${info.tables[i].name}" could not be retrieved`);
|
|
}
|
|
|
|
// All partition and clustering keys from the table should be included in the document
|
|
const keyNames = table.partitionKeys.concat(table.clusteringKeys).map(k => k.name);
|
|
const columns = propertiesInfo.map(p => p.columnName);
|
|
|
|
for (let i = 0; i < keyNames.length; i++) {
|
|
if (columns.indexOf(keyNames[i]) === -1) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// "when" conditions should be contained in the table
|
|
return when.reduce((acc, p) => acc && table.columnsByName[p.columnName] !== undefined, true);
|
|
});
|
|
|
|
if (filteredTables.length === 0) {
|
|
let message = `No table matches (all PKs have to be specified) fields: [${
|
|
propertiesInfo.map(p => p.columnName)}]`;
|
|
|
|
if (when.length > 0) {
|
|
message += `; condition: [${when.map(p => p.columnName)}]`;
|
|
}
|
|
|
|
throw new Error(message);
|
|
}
|
|
|
|
return filteredTables;
|
|
});
|
|
}
|
|
}
|
|
|
|
function contains(arr, fn) {
|
|
return arr.filter(fn).length > 0;
|
|
}
|
|
|
|
/**
|
|
* Returns the amount of matches for a given key
|
|
* @private
|
|
* @param {Array} keys
|
|
* @param {Array} propertiesInfo
|
|
*/
|
|
function keysAreIncluded(keys, propertiesInfo) {
|
|
if (keys.length === 0) {
|
|
return keyMatches.all;
|
|
}
|
|
|
|
// Filtering by name might look slow / ineffective to using hash maps
|
|
// but we expect `keys` and `propertiesInfo` to contain only few items
|
|
const matches = propertiesInfo.reduce((acc, p) => acc + (contains(keys, k => p.columnName === k.name) ? 1 : 0), 0);
|
|
if (matches === 0) {
|
|
return keyMatches.none;
|
|
}
|
|
|
|
return matches === keys.length ? keyMatches.all : keyMatches.some;
|
|
}
|
|
|
|
function hasStaticColumn(table) {
|
|
return staticColumnCount(table) > 0;
|
|
}
|
|
|
|
function staticColumnCount(table) {
|
|
return table.columns.reduce((acc, column) => acc + (column.isStatic ? 1 : 0), 0);
|
|
}
|
|
|
|
module.exports = ObjectSelector;
|