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.

446 lines
14 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 vm = require('vm');
const qModule = require('./q');
const QueryOperator = qModule.QueryOperator;
const QueryAssignment = qModule.QueryAssignment;
const types = require('../types');
const dataTypes = types.dataTypes;
const vmFileName = 'gen-param-getter.js';
/**
* Provides methods to generate a query and parameter handlers.
* @ignore
*/
class QueryGenerator {
/**
* Gets the SELECT query given the doc.
* @param {String} tableName
* @param {String} keyspace
* @param {Array} propertiesInfo
* @param {Array} fieldsInfo
* @param {Array} orderByColumns
* @param {Number|undefined} limit
* @return {string}
*/
static getSelect(tableName, keyspace, propertiesInfo, fieldsInfo, orderByColumns, limit) {
let query = 'SELECT ';
query += fieldsInfo.length > 0 ? fieldsInfo.map(p => p.columnName).join(', ') : '*';
query += ` FROM ${keyspace}.${tableName}`;
if (propertiesInfo.length > 0) {
query += ' WHERE ';
query += QueryGenerator._getConditionWithOperators(propertiesInfo);
}
if (orderByColumns.length > 0) {
query += ' ORDER BY ';
query += orderByColumns.map(order => order[0] + ' ' + order[1]).join(', ');
}
if (typeof limit === 'number') {
query += ' LIMIT ?';
}
return query;
}
static selectParamsGetter(propertiesInfo, limit) {
let scriptText = '(function getParametersSelect(doc, docInfo, mappingInfo) {\n';
scriptText += ' return [';
scriptText += QueryGenerator._valueGetterExpression(propertiesInfo);
if (typeof limit === 'number') {
if (propertiesInfo.length > 0) {
scriptText += ', ';
}
scriptText += `docInfo['limit']`;
}
// Finish return statement
scriptText += '];\n})';
const script = new vm.Script(scriptText, { filename: vmFileName });
return script.runInThisContext();
}
/**
* Gets the INSERT query and function to obtain the parameters, given the doc.
* @param {TableMetadata} table
* @param {String} keyspace
* @param {Array} propertiesInfo
* @param {Object} docInfo
* @param {Boolean|undefined} ifNotExists
* @return {{query: String, paramsGetter: Function, isIdempotent: Boolean}}
*/
static getInsert(table, keyspace, propertiesInfo, docInfo, ifNotExists) {
const ttl = docInfo && docInfo.ttl;
// Not all columns are contained in the table
const filteredPropertiesInfo = propertiesInfo
.filter(pInfo => table.columnsByName[pInfo.columnName] !== undefined);
return ({
query: QueryGenerator._getInsertQuery(table.name, keyspace, filteredPropertiesInfo, ifNotExists, ttl),
paramsGetter: QueryGenerator._insertParamsGetter(filteredPropertiesInfo, docInfo),
isIdempotent: !ifNotExists
});
}
/**
* Gets the query for an insert statement.
* @param {String} tableName
* @param {String} keyspace
* @param {Array} propertiesInfo
* @param {Boolean} ifNotExists
* @param {Number|undefined} ttl
* @return {String}
*/
static _getInsertQuery(tableName, keyspace, propertiesInfo, ifNotExists, ttl) {
let query = `INSERT INTO ${keyspace}.${tableName} (`;
query += propertiesInfo.map(pInfo => pInfo.columnName).join(', ');
query += ') VALUES (';
query += propertiesInfo.map(() => '?').join(', ');
query += ')';
if (ifNotExists === true) {
query += ' IF NOT EXISTS';
}
if (typeof ttl === 'number') {
query += ' USING TTL ?';
}
return query;
}
static _insertParamsGetter(propertiesInfo, docInfo) {
let scriptText = '(function getParametersInsert(doc, docInfo, mappingInfo) {\n';
scriptText += ' return [';
scriptText += QueryGenerator._valueGetterExpression(propertiesInfo);
if (docInfo && typeof docInfo.ttl === 'number') {
scriptText += `, docInfo['ttl']`;
}
// Finish return statement
scriptText += '];\n})';
const script = new vm.Script(scriptText, { filename: vmFileName });
return script.runInThisContext();
}
/**
* Gets the UPDATE query and function to obtain the parameters, given the doc.
* @param {TableMetadata} table
* @param {String} keyspace
* @param {Array} propertiesInfo
* @param {Object} docInfo
* @param {Array} when
* @param {Boolean|undefined} ifExists
* @return {{query: String, paramsGetter: Function, isIdempotent: Boolean, isCounter}}
*/
static getUpdate(table, keyspace, propertiesInfo, docInfo, when, ifExists) {
const ttl = docInfo && docInfo.ttl;
const primaryKeys = new Set(table.partitionKeys.concat(table.clusteringKeys).map(c => c.name));
let isIdempotent = true;
let isCounter = false;
// Not all columns are contained in the table
const filteredPropertiesInfo = propertiesInfo.filter(pInfo => {
const column = table.columnsByName[pInfo.columnName];
if (column === undefined) {
return false;
}
if (column.type.code === dataTypes.list && pInfo.value instanceof QueryAssignment) {
// Its not idempotent when list append/prepend
isIdempotent = false;
} else if (column.type.code === dataTypes.counter) {
// Any update on a counter table is not idempotent
isIdempotent = false;
isCounter = true;
}
return true;
});
return {
query: QueryGenerator._getUpdateQuery(
table.name, keyspace, primaryKeys, filteredPropertiesInfo, when, ifExists, ttl),
isIdempotent: isIdempotent && when.length === 0 && !ifExists,
paramsGetter: QueryGenerator._updateParamsGetter(primaryKeys, filteredPropertiesInfo, when, ttl),
isCounter
};
}
/**
* Gets the query for an UPDATE statement.
* @param {String} tableName
* @param {String} keyspace
* @param {Set} primaryKeys
* @param {Array} propertiesInfo
* @param {Object} when
* @param {Boolean} ifExists
* @param {Number|undefined} ttl
*/
static _getUpdateQuery(tableName, keyspace, primaryKeys, propertiesInfo, when, ifExists, ttl) {
let query = `UPDATE ${keyspace}.${tableName} `;
if (typeof ttl === 'number') {
query += 'USING TTL ? ';
}
query += 'SET ';
query += propertiesInfo
.filter(p => !primaryKeys.has(p.columnName))
.map(p => {
if (p.value instanceof QueryAssignment) {
if (p.value.inverted) {
// e.g: prepend "col1 = ? + col1"
return `${p.columnName} = ? ${p.value.sign} ${p.columnName}`;
}
// e.g: increment "col1 = col1 + ?"
return `${p.columnName} = ${p.columnName} ${p.value.sign} ?`;
}
return p.columnName + ' = ?';
})
.join(', ');
query += ' WHERE ';
query += propertiesInfo.filter(p => primaryKeys.has(p.columnName)).map(p => p.columnName + ' = ?').join(' AND ');
if (ifExists === true) {
query += ' IF EXISTS';
}
else if (when.length > 0) {
query += ' IF ' + QueryGenerator._getConditionWithOperators(when);
}
return query;
}
/**
* Returns a function to obtain the parameter values from a doc for an UPDATE statement.
* @param {Set} primaryKeys
* @param {Array} propertiesInfo
* @param {Array} when
* @param {Number|undefined} ttl
* @returns {Function}
*/
static _updateParamsGetter(primaryKeys, propertiesInfo, when, ttl) {
let scriptText = '(function getParametersUpdate(doc, docInfo, mappingInfo) {\n';
scriptText += ' return [';
if (typeof ttl === 'number') {
scriptText += `docInfo['ttl'], `;
}
// Assignment clause
scriptText += QueryGenerator._assignmentGetterExpression(propertiesInfo.filter(p => !primaryKeys.has(p.columnName)));
scriptText += ', ';
// Where clause
scriptText += QueryGenerator._valueGetterExpression(propertiesInfo.filter(p => primaryKeys.has(p.columnName)));
// Condition clause
if (when.length > 0) {
scriptText += ', ' + QueryGenerator._valueGetterExpression(when, 'docInfo.when');
}
// Finish return statement
scriptText += '];\n})';
const script = new vm.Script(scriptText, { filename: vmFileName });
return script.runInThisContext();
}
/**
* Gets the DELETE query and function to obtain the parameters, given the doc.
* @param {TableMetadata} table
* @param {String} keyspace
* @param {Array} propertiesInfo
* @param {Object} docInfo
* @param {Array} when
* @param {Boolean|undefined} ifExists
* @return {{query: String, paramsGetter: Function, isIdempotent}}
*/
static getDelete(table, keyspace, propertiesInfo, docInfo, when, ifExists) {
const deleteOnlyColumns = docInfo && docInfo.deleteOnlyColumns;
const primaryKeys = new Set(table.partitionKeys.concat(table.clusteringKeys).map(c => c.name));
const filteredPropertiesInfo = propertiesInfo
.filter(pInfo => table.columnsByName[pInfo.columnName] !== undefined);
return ({
query: QueryGenerator._getDeleteQuery(
table.name, keyspace, primaryKeys, filteredPropertiesInfo, when, ifExists, deleteOnlyColumns),
paramsGetter: QueryGenerator._deleteParamsGetter(primaryKeys, filteredPropertiesInfo, when),
isIdempotent: when.length === 0 && !ifExists
});
}
/**
* Gets the query for an UPDATE statement.
* @param {String} tableName
* @param {String} keyspace
* @param {Set} primaryKeys
* @param {Array} propertiesInfo
* @param {Array} when
* @param {Boolean} ifExists
* @param {Boolean} deleteOnlyColumns
* @private
* @return {String}
*/
static _getDeleteQuery(tableName, keyspace, primaryKeys, propertiesInfo, when, ifExists, deleteOnlyColumns) {
let query = 'DELETE';
if (deleteOnlyColumns) {
const columnsToDelete = propertiesInfo.filter(p => !primaryKeys.has(p.columnName))
.map(p => p.columnName)
.join(', ');
if (columnsToDelete !== '') {
query += ' ' + columnsToDelete;
}
}
query += ` FROM ${keyspace}.${tableName} WHERE `;
query += propertiesInfo.filter(p => primaryKeys.has(p.columnName)).map(p => p.columnName + ' = ?').join(' AND ');
if (ifExists === true) {
query += ' IF EXISTS';
}
else if (when.length > 0) {
query += ' IF ' + QueryGenerator._getConditionWithOperators(when);
}
return query;
}
/**
* Returns a function to obtain the parameter values from a doc for an UPDATE statement.
* @param {Set} primaryKeys
* @param {Array} propertiesInfo
* @param {Array} when
* @returns {Function}
*/
static _deleteParamsGetter(primaryKeys, propertiesInfo, when) {
let scriptText = '(function getParametersDelete(doc, docInfo, mappingInfo) {\n';
scriptText += ' return [';
// Where clause
scriptText += QueryGenerator._valueGetterExpression(propertiesInfo.filter(p => primaryKeys.has(p.columnName)));
// Condition clause
if (when.length > 0) {
scriptText += ', ' + QueryGenerator._valueGetterExpression(when, 'docInfo.when');
}
// Finish return statement
scriptText += '];\n})';
const script = new vm.Script(scriptText, { filename: vmFileName });
return script.runInThisContext();
}
/**
* Gets a string containing the doc properties to get.
* @param {Array} propertiesInfo
* @param {String} [objectName='doc']
* @return {string}
* @private
*/
static _valueGetterExpression(propertiesInfo, objectName) {
objectName = objectName || 'doc';
return propertiesInfo
.map(p =>
QueryGenerator._valueGetterSingle(`${objectName}['${p.propertyName}']`, p.propertyName, p.value, p.fromModel))
.join(', ');
}
static _valueGetterSingle(prefix, propName, value, fromModelFn) {
let valueGetter = prefix;
if (value instanceof QueryOperator) {
if (value.hasChildValues) {
return `${QueryGenerator._valueGetterSingle(`${prefix}.value[0]`, propName, value.value[0], fromModelFn)}` +
`, ${QueryGenerator._valueGetterSingle(`${prefix}.value[1]`, propName, value.value[1], fromModelFn)}`;
}
valueGetter = `${prefix}.value`;
if (value.isInOperator && fromModelFn) {
// Transform each individual value
return `${valueGetter}.map(v => ${QueryGenerator._getMappingFunctionCall(propName, 'v')})`;
}
}
return !fromModelFn ? valueGetter : QueryGenerator._getMappingFunctionCall(propName, valueGetter);
}
/**
* Gets a string containing the doc properties to SET, considering QueryAssignment instances.
* @param {Array} propertiesInfo
* @param {String} [prefix='doc']
* @return {string}
* @private
*/
static _assignmentGetterExpression(propertiesInfo, prefix) {
prefix = prefix || 'doc';
return propertiesInfo
.map(p => {
const valueGetter = `${prefix}['${p.propertyName}']${p.value instanceof QueryAssignment ? '.value' : ''}`;
if (p.fromModel) {
return QueryGenerator._getMappingFunctionCall(p.propertyName, valueGetter);
}
return valueGetter;
})
.join(', ');
}
static _getConditionWithOperators(propertiesInfo) {
return propertiesInfo
.map(p => QueryGenerator._getSingleCondition(p.columnName, p.value))
.join(' AND ');
}
static _getMappingFunctionCall(propName, valueGetter) {
return `mappingInfo.getFromModelFn('${propName}')(${valueGetter})`;
}
static _getSingleCondition(columnName, value) {
if (value instanceof QueryOperator) {
if (value.hasChildValues) {
return `${QueryGenerator._getSingleCondition(columnName, value.value[0])}` +
` ${value.key} ${QueryGenerator._getSingleCondition(columnName, value.value[1])}`;
}
return `${columnName} ${value.key} ?`;
}
return `${columnName} = ?`;
}
}
module.exports = QueryGenerator;