/* * 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;