UNPKG

@sap/xsodata

Version:

Expose data from a HANA database as OData V2 service with help of .xsodata files.

1,690 lines (1,415 loc) 46.4 kB
'use strict'; var edm = require('./../utils/typedObjects'); var cloneContextWithNoAlias = edm.cloneContextWithNoAlias; var InternalError = require('./../utils/errors/internalError'); var util = require('util'); /** * Create a new SqlBuildHanaContext * The buildContext is used as parameter for the toSqlHana functions * The buildContext stores whether null values are supported or not and * @param context * @constructor */ exports.SqlBuildHanaContext = function (context) { if (context) { this.oDataNullSupport = context.gModel.isNullSupported(); var dbSeg = context.oData.dbSegmentLast; this.isNav = function (string) { return dbSeg._SelectedNavigations.indexOf(string) > -1; }; this.isHanaCloudDb = getHanaCloudDbSetting(context); } else { this.oDataNullSupport = false; this.isNav = false; this.isHanaCloudDb = true; // use default Hana Cloud - Q: Is this needed? In a clone (without context, the original context should have this setting) } this.inOrderBy = false; this.table = null; this.noAlias = null; // only used in a cloned SqlBuildHanaContext to avoid using aliases in orderby clauses }; function getHanaCloudDbSetting(context) { // 06/2022: All used SQL from Hana Cloud DB work also on Hana Service DB, // i.e. take that as default; but that may change in the future! let isHanaCloudDb = true; if (context.batchContext && context.batchContext.parentContext && context.batchContext.parentContext.db && context.batchContext.parentContext.db.isHanaCloudDb !== undefined) { isHanaCloudDb = context.batchContext.parentContext.db.isHanaCloudDb; // batch request context } else if (context.db && context.db.isHanaCloudDb !== undefined) { isHanaCloudDb = context.db.isHanaCloudDb; // regular request context } return isHanaCloudDb; } exports.SqlBuildHanaContext.prototype.clone = function () { var c = new exports.SqlBuildHanaContext(); c.oDataNullSupport = this.oDataNullSupport; c.isNav = this.isNav; c.table = this.table; // used in Property.prototype.toSqlHana c.noAlias = this.noAlias; c.inOrderBy = this.inOrderBy; c.isHanaCloudDb = this.isHanaCloudDb; // keep this setting in any clone!!! return c; }; const extractStms = (stmDbset) => { if (!stmDbset) { return []; } return stmDbset.map(x => x.stm); }; exports.PostContainer = class PostContainer { constructor() { this.createTmp = null; this.insertTmp = null; this.insertReal = null; this.select = null; } }; exports.GetContainer = class GetContainer { constructor() { this.createTmp = null; this.insertTmp = null; this.selectTmp = null; this.select = null; //if there is no $expand or $count is set or $value is set } }; exports.GetContainerAll = class GetContainerAll { constructor() { this.createTmp = []; this.insertTmp = []; this.selectTmp = []; //if there is no $expand or $count is set or $value is set this.select = []; } getCreateTmpStms() { return extractStms(this.createTmp); } getInsertTmpStms() { return extractStms(this.insertTmp); } getSelectTmpStms() { return extractStms(this.selectTmp); } getSelectStms() { return extractStms(this.select); } }; exports.PutContainer = class PutContainer { constructor() { this.createTmpOld = null; this.createTmp = null; this.insertTmpOld = null; this.insertTmp = null; this.updateReal = null; } }; exports.DeleteContainer = class DeleteContainer { constructor() { this.createTmpDel = null; this.insertTmpDel = null; this.delete = null; } }; /** * @Class Eq * @constructor * @param left * @param right */ /* function Eq(left, right) { this.left = left; this.right = right; } exports.Eq = Eq; Eq.prototype.toSqlHana = function (context,parameter) { var ret = ''; ret += '(' + this.left.toSqlHana(context,parameter) + ' = ' + this.right.toSqlHana(context,parameter) + ')'; return ret; };*/ /** * @Class Value * @constructor * @param value */ function Value(value) { this.value = value; } exports.Value = Value; Value.prototype.toSqlHana = function (context, parameter) { parameter.push(this.value); return '?'; }; /** * @class Exists * @constructor * @param select */ function Exists(select) { this.exists = select; } exports.Exists = Exists; Exists.prototype.toSqlHana = function (context, parameter) { var ret = ''; ret += 'exists (' + this.exists.toSqlHana(context, parameter) + ')'; return ret; }; /** * @class Property * @constructor * @param table * @param property * @param propertyType * @param alias */ function SelectProperty(table, property, propertyType, alias) { this.table = table; this.property = property; this.propertyType = propertyType; this.alias = alias; } exports.SelectProperty = SelectProperty; SelectProperty.prototype.toSqlHana = function (context, parameter, hint) { var sql = []; var conversionFunction; if (!context.inOrderBy) { conversionFunction = getConversionFunction(this.propertyType); } if (conversionFunction) { sql.push(conversionFunction, "("); } if (!(hint && hint.withoutTable) && this.table) { sql.push('"', this.table, '".'); } sql.push('"', this.property, '"'); if (conversionFunction) { sql.push(")"); } if (!context.noAlias) { if (conversionFunction || this.alias) { sql.push(' "', (this.alias || this.property), '"'); } } return sql.join(""); }; /** * Returns name of the SQL data type conversion function, which should be used in the SQL SELECT statement to retrieve * property with the specified <code>propertyType</code>. * * @param {string} propertyType - type of the property, for which the data type conversion function has to be determined * @returns {string} name of the data type conversion function or <code>undefined</code>, when no type conversion * should be applied. */ function getConversionFunction(propertyType) { if (propertyType === "DECIMAL") { return 'to_char'; } } function AggregateSelectProperty(aggregation, table, property, aggregateAlias) { this.aggregation = aggregation; this.aggregateAlias = aggregateAlias; this.selectProperty = new SelectProperty(table, property); this.property = this.selectProperty.property; this.table = this.selectProperty.table; } exports.AggregateSelectProperty = AggregateSelectProperty; AggregateSelectProperty.prototype.toSqlHana = function (context, parameters) { var sql = this.aggregation + '(' + this.selectProperty.toSqlHana(context, parameters) + ')'; if (this.aggregateAlias) { sql += ' "' + this.aggregateAlias + '"'; } return sql; }; function ParameterSelectProperty(table, property, alias, value) { SelectProperty.call(this, table, property, null, alias); this.value = value; } ParameterSelectProperty.prototype.toSqlHana = function (context, parameter, hint) { var _alias = this.alias || this.property; /* if(!_alias){ if (!withoutTable && this.table) { _alias = this.table + '.'; }else{ _alias = ''; } _alias = this.property; }*/ if (this.value) { this.value.toSqlHana(context, parameter, hint); return util.format('? "%s"', _alias); } parameter.push(null); return util.format('? "%s"', _alias); }; exports.ParameterSelectProperty = ParameterSelectProperty; function CreateProperty(property, type) { this.property = property; this.type = type; } exports.CreateProperty = CreateProperty; CreateProperty.prototype.toSqlHana = function (/*context, parameter*/) { return '"' + this.property + '" ' + this.type; }; /** * @class SelectFormula * @constructor * @param {string} table * @param {string} formula * @param {string} alias */ function SelectFormula(table, formula, alias) { this.table = table; this.formula = formula; this.alias = alias; } exports.SelectFormula = SelectFormula; SelectFormula.prototype.toSqlHana = function () { var ret = ''; if (this.table) { ret += '"' + this.table + '".'; } ret += this.formula; if (this.alias) { ret += ' "' + this.alias + '"'; } return ret; }; /** * @class SelectFormula * @constructor * @param {string=} alias */ function OverFormula(alias) { this.alias = alias; this.orders = []; } exports.OverFormula = OverFormula; OverFormula.prototype.addSortOrder = function (order) { this.orders.push(order); }; OverFormula.prototype.addSortOrders = function (orders) { for (var i = 0; i < orders.length; i++) { this.orders.push(orders[i]); } }; OverFormula.prototype.toSqlHana = function (context, parameter) { var clonedContext = null; var sql = ''; sql += 'row_number() over('; var i; if (this.orders.length > 0) { clonedContext = cloneContextWithNoAlias(context); clonedContext.inOrderBy = true; sql += '\norder by '; for (i = 0; i < this.orders.length; i++) { sql += (i === 0 ? '' : ', '); sql += this.orders[i].toSqlHana(clonedContext, parameter); } sql += ''; } sql += ') '; sql += '"' + this.alias + '"'; return sql; }; /** * @class Create * @constructor */ function Create() { this.modifiers = []; this.table = null; this.as = null; this.postModifiers = null; this.properties = []; this.propertieMap = {}; } exports.Create = Create; Create.prototype.addProperties = function (properties) { for (let property of properties) { if (!this.propertieMap[property.property]) { this.properties.push(property); this.propertieMap[property.property] = property; } } }; Create.prototype.addUniqueProperties = function (properties) { addUniqueSqlStatementsToList(properties, this.properties); return this; }; Create.prototype.addProperty = function (property) { this.properties.push(property); }; /** * Set modifiers * SQL: create <modifiers> table ... as * @param {Array.<String>} modifiers */ Create.prototype.setModifiers = function (modifiers) { this.modifiers = modifiers; }; /** * Set post modifiers * SQL: create ... table ... as ... <postModifiers> * @param {Array.<String>} postModifiers */ Create.prototype.setPostModifiers = function (postModifiers) { this.postModifiers = postModifiers; }; /** * Set Table * SQL: create ... table <table> as * @param table */ Create.prototype.setTableName = function (table) { this.table = table; }; /** * Set As * SQL: create ... table ... as (<as>) * @param as */ Create.prototype.setAs = function (as) { this.as = as; }; Create.prototype.toSqlHana = function (context, parameter) { var i; var sql = ''; sql += 'create '; for (i = 0; i < this.modifiers.length; i++) { sql += ' ' + this.modifiers[i]; } sql += ' table '; sql += '"' + this.table + '" '; if (this.as) { sql += '\nas ( '; sql += this.as.toSqlHana(context, parameter); sql += ') '; } if (this.properties.length > 0) { sql += '('; for (i = 0; i < this.properties.length; i++) { sql += (i === 0 ? '' : ', '); sql += this.properties[i].toSqlHana(context, parameter); } sql += ')'; } if (this.postModifiers) { for (i = 0; i < this.postModifiers.length; i++) { sql += ' ' + this.postModifiers[i]; } } return sql; }; /** * @class Insert * @constructor */ function Insert() { this.intoTable = null; this.subSelect = null; this.nv = []; } exports.Insert = Insert; /** * Set Table * SQL: insert into <table> * @param {{table: String}} table */ Insert.prototype.setTableName = function (table) { this.intoTable = table; }; /** * Set subSelect * SQL: insert into ... select <subselect> * @param {exports.Select} subSelect of type "Select" */ Insert.prototype.setSubSelect = function (subSelect) { this.subSelect = subSelect; }; Insert.prototype.toSqlHana = function (context, parameter) { var sql = ''; sql += 'insert into '; if (this.intoTable.table.substr(0, 1) === '#') { sql += ' "' + this.intoTable.table + '" '; if (this.intoTable.alias) { sql += ' as "' + this.intoTable.alias + '" '; } } else { if (this.intoTable.schema) { sql += '"' + this.intoTable.schema + '".'; } sql += '"' + this.intoTable.table + '"'; if (this.intoTable.alias) { sql += ' as "' + this.intoTable.alias + '" '; } } var i; var tmp = ''; //sql += this.table + ' '; //collect names if (this.nv.length > 0) { for (i = 0; i < this.nv.length; i++) { tmp += (tmp.length > 0 ? ', ' : ''); tmp += this.nv[i].name.toSqlHana(context, parameter, { withoutTable: true, useDbType: this.nv[i].dbType }); // OK } sql += '(' + tmp + ')'; } if (this.subSelect) { sql += ' ('; sql += ' ' + this.subSelect.toSqlHana(context, parameter); sql += ') '; } else { sql += ' values '; tmp = ''; for (i = 0; i < this.nv.length; i++) { tmp += (tmp.length > 0 ? ', ' : ''); if (this.nv[i].overwriteValue !== undefined) { if (this.nv[i].overwriteValue && this.nv[i].overwriteValue.toSqlHana) { tmp += this.nv[i].overwriteValue.toSqlHana(context, parameter, { withoutTable: true, useDbType: this.nv[i].dbType }); } else { tmp += '?'; parameter.push(this.nv[i].overwriteValue); } } else if (this.nv[i].formula !== undefined) { tmp += ' ' + this.nv[i].formula; } else if (this.nv[i].sequence) { tmp += ' "' + this.nv[i].sequence + '".NEXTVAL '; } else { if (this.nv[i].value && this.nv[i].value.toSqlHana) { tmp += this.nv[i].value.toSqlHana(context, parameter, { withoutTable: true, useDbType: this.nv[i].dbType }); // Ok } else { //console.log('--------------'+JSON.stringify(this.nv[i].value,null,4)); tmp += '?'; parameter.push(this.nv[i].value); } } } sql += '(' + tmp + ')'; //collect values } return sql; }; /** * Add array of properties to be inserted * @param {(exports.Property)} names */ Insert.prototype.addNames = function (names) { for (var i = 0; i < names.length; i++) { var n = names[i]; this.nv.push({ name: n, value: undefined //null would be a valid OData value }); } }; Insert.prototype.setValue = function (name, value, dbType) { for (var i = 0; i < this.nv.length; i++) { var nv = this.nv[i]; if (nv.name.property === name) { nv.value = value; if (dbType) { nv.dbType = dbType; } } } }; /** * @class Select * @constructor */ function Select() { this.select = []; this.from = { schema: null, table: null, alias: null }; this.froms = []; this.joins = []; this.whereAnded = []; this.orders = []; this.groupBys = []; this.limit = null; this.offset = null; this.fallbackStatement = null; this.addCalcViewHint = false; this.customData = {}; // restriction on the count of selected records } exports.Select = Select; Select.prototype.setCustomData = function (name, value) { this.customData[name] = value; }; Select.prototype.getCustomData = function (name) { return this.customData[name]; }; Select.prototype.addJoin = function (table, on) { this.joins.push({ table: table, on: on }); }; Select.prototype.addJoinTable = function (select, on, alias) { this.joins.push({ tableSelect: select, alias: alias, on: on }); }; Select.prototype.toSqlHana = function (context, parameter) { var i = 0; var clonedContext = null; var sql = ''; sql += 'select '; for (i = 0; i < this.select.length; i++) { sql += (i === 0 ? '' : ', '); sql += this.select[i].toSqlHana(context, parameter); } // note: $orderby properties for CV that are NOT in select list result in SQL error // but: That was never supported by this lib! sql += '\nfrom '; sql += this.froms.map(function (from) { if (from.subQuery) { return '(' + from.subQuery.toSqlHana(context, parameter) + ')'; } var fromString = ''; if (from.table.substr(0, 1) !== '#' && from.schema) { fromString += '"' + from.schema + '".'; } fromString += '"' + from.table + '"'; if (from.placeholders) { var sql_placeholders = from.placeholders.map(function (ph) { return ph.toSqlHana(context, parameter); }); fromString += '('; fromString += sql_placeholders.join(','); fromString += ')'; } if (from.alias) { fromString += ' as "' + from.alias + '"'; } return fromString; }).join(', '); this.joins.forEach(function (join) { if (join.table) { var table = join.table; sql += ' join '; if (table.schema) { sql += '"' + table.schema + '".'; } sql += '"' + table.table + '"'; if (table.alias) { sql += ' as "' + table.alias + '"'; } if (join.on.length > 0) { sql += ' on '; for (i = 0; i < join.on.length; i++) { sql += (i === 0 ? '' : ' and '); sql += join.on[i].toSqlHana(context, parameter); } } } else if (join.tableSelect) { sql += ' join ('; sql += join.tableSelect.toSqlHana(context, parameter); sql += ' )'; if (join.alias) { sql += ' "' + join.alias + '" '; } if (join.on.length > 0) { sql += ' on '; for (i = 0; i < join.on.length; i++) { sql += (i === 0 ? '' : ' and '); sql += join.on[i].toSqlHana(context, parameter); } } } }); if (this.whereAnded.length > 0) { sql += '\nwhere '; for (i = 0; i < this.whereAnded.length; i++) { sql += (i === 0 ? '' : ' and '); sql += this.whereAnded[i].toSqlHana(context, parameter); } } if (this.groupBys.length > 0) { sql += '\ngroup by '; sql += this.groupBys.map(function (groupBy) { return groupBy.toSqlHana(context, parameter); }).join(', '); sql += ' '; } if (this.orders.length > 0) { clonedContext = cloneContextWithNoAlias(context); clonedContext.inOrderBy = true; sql += '\norder by '; for (i = 0; i < this.orders.length; i++) { sql += (i === 0 ? '' : ', '); sql += this.orders[i].toSqlHana(clonedContext, parameter); } sql += ''; } if (this.limit !== null && this.limit >= 0 && this.offset) { sql += ' limit ? offset ?'; parameter.push(this.limit); parameter.push(this.offset); } else if (this.offset) { sql += ' limit null offset ?'; parameter.push(this.offset); } else if (this.limit !== null && this.limit >= 0) { sql += ' limit ?'; parameter.push(this.limit); } var hints = this.hints; if (hints) { if (hints.length > 0) { sql += ' with hint (' + hints.join(',') + ')'; } } else { if (this.addCalcViewHint) { // Disable calc view unfolding by default for older Hana/Calcview-Engine versions to avoid backward compatibility issues. sql += ' with hint (NO_CALC_VIEW_UNFOLDING)';//due to fix in calcview engine } } return sql; }; /** * Sets an indicator to add with "hint (NO_CALC_VIEW_UNFOLDING)" to the sql statment. */ Select.prototype.setAddCalcViewHint = function () { this.addCalcViewHint = true; }; /** * Sets a list of hints */ Select.prototype.setHints = function (hints) { this.hints = hints; }; /** * Set from clause * SQL: select ... from <from> * @param {{schema: string, table: string, alias: string}} from qualified tablename or */ Select.prototype.setFrom = function (from) { return this.addFrom(from); }; Select.prototype.addFrom = function (from, inputParams) { function addPlaceholder(kv) { function isStringArray(value) { const defaultResult = { "isStringArray": false, "stringArray": [] }; // at least one string must be present in string array if (value[0] !== '\'' || value[value.length - 1] !== '\'') { return defaultResult; } //let localValue = value.substr(1, value.length - 2); // surrounding quotes from URI already removed by 'toSqlHana' let localValue = value; let stringArray = []; let currString = ""; // simple state machine: // 0 = initial // 1 = start-quote // 2 = end-quote // 3 = in-string // 4 = comma let state = 0; let ch = ''; for (let i = 0; i <= localValue.length - 1; i++) { ch = localValue[i]; switch (state) { case 0: if (ch !== '\'') { return defaultResult; } state = 1; currString = ""; break; case 1: if (ch === '\'') { state = 2; currString = ""; } else { currString = currString + ch; state = 3; } break; case 2: if (ch === ',' && state === 2) { state = 4; } else { return defaultResult; } break; case 3: if (ch === '\'' && ((i < localValue.length - 1 && localValue[i + 1] === ',')) || (i === localValue.length - 1)) { stringArray.push(currString); currString = ""; state = 2; } else { currString = currString + ch; } break; case 4: if (ch === '\'' && state === 4) { state = 1; } else { return defaultResult; } break; } } return { "isStringArray": (state === 2), "stringArray": stringArray }; } from.placeholders = from.placeholders || []; from.placeholders.push({ key: kv.key, value: kv.value, toSqlHana: function (context, params) { var sql = util.format('placeholder."$$%s$$" => ?', kv.key); // placeholder."$$%s$$ == only for calc-views (not regular SQL) var localParams = []; var value; if (kv.value instanceof edm.EdmString) { //Edm.String kv.value.toSqlHana(context, localParams); value = localParams[0]; let result = isStringArray(value); if (!result.isStringArray) { value = value.replace(/\\/g, "\\\\"); value = value.replace(/'/g, "\\'"); } else { let newStringArray = result.stringArray.map( element => { element = element.replace(/\\/g, "\\\\"); element = element.replace(/'/g, "\\'"); return `'${element}'`; }); value = newStringArray.join(','); } params.push(value); // e.g. "'a string'" or string-array } else if (kv.value instanceof edm.Number) { kv.value.toSqlHana(context, localParams); if (typeof localParams[0] === "string") { //Edm.Decimal(SMALLDECIMAL,DECIMAL), params.push(localParams[0]); //e.g. "1.2" } else { //Edm.Byte(TINYINT), Edm.Int16(SMALLINT), Edm.Int32(INTEGER), Edm.Int64(BIGINT), //Edm.Single(REAL,FLOAT), Edm.Double(DOUBLE) params.push(localParams[0].toString()); //e.g. "1" } } else if (kv.value instanceof edm.Binary) { value = kv.value.value; value = value.substring(2, value.length - 1); params.push(value); //params.push(kv.value.value); } else if (kv.value instanceof edm.DateTime) { //Edm.DateTime(DATE,SECONDDATE,TIMESTAMP) kv.value.toSqlHana(context, localParams); params.push(localParams[0]); //e.g. "'1753-01-01'","'1753-01-01'","'1753-01-01'" } else if (kv.value instanceof edm.Time) { //Edm.Time(TIME) kv.value.toSqlHana(context, localParams); params.push(localParams[0]); //e.g. "'00:00:00'" } else { throw new InternalError('Navigation to calc view expected after an Input path segment 3'); } return sql; } } ); } if (inputParams) { inputParams.forEach(addPlaceholder); } this.froms.push(from); return this; } ; /** * Set from clause * SQL: select ... from <from> * @param {exports.Select} from qualified tablename or * @param {string} [alias] qualified tablename or */ Select.prototype.setFromBySubQuery = function (from, alias) { this.froms.push({ subQuery: from, alias: alias }); return this; }; /** * Set array of selected properties * @param {(exports.Property|exports.SelectFormula)[]} select */ Select.prototype.setSelect = function (select) { this.select = select; }; /** * Add array of selected properties * @param {(exports.Property|exports.SelectFormula)[]} select */ Select.prototype.addSelect = function (select) { this.select = this.select.concat(select); return this; }; Select.prototype.addSelects = function (newSelects) { if (!Array.isArray(newSelects)) { newSelects = [newSelects]; } for (let newSelect of newSelects) { let add = true; if (newSelect.property) { for (let select of this.select) { if (newSelect.table === select.table && newSelect.property === select.property && newSelect.alias === select.alias) { add = false; } } } if (add) { this.select.push(newSelect); } } return this; }; Select.prototype.addUniqueSelects = function (selects) { addUniqueSqlStatementsToList(selects, this.select); return this; }; /** * * @param {{key : exports.Property, value: string}[]} keyValuePairs */ Select.prototype.addWhereKeyValuePairs = function (keyValuePairs) { for (var i = 0; i < keyValuePairs.length; i++) { var kv = keyValuePairs[i]; this.whereAnded.push(new edm.BinaryOperator(edm.EQ, kv.key, kv.value, true /*strict*/)); } return this; }; /** * @param {(edm.BinaryOperator|Object)} expression */ Select.prototype.addWhereAnded = function (expression) { this.whereAnded.push(expression); return this; }; /** * @param {edm.SortOrder[]} orders */ Select.prototype.addSortOrders = function (orders) { for (var i = 0; i < orders.length; i++) { this.orders.push(orders[i]); } return this; }; Select.prototype.addGroupBys = function (groupBys) { this.groupBys = this.groupBys.concat(groupBys); return this; }; Select.prototype.addSortOrder = function (order) { this.orders.push(order); return this; }; /** * * @param {edm.OrderBy} orderBy */ Select.prototype.addOrderBy = function (orderBy) { for (var i = 0; i < orderBy.orders.length; i++) { this.orders.push(orderBy.orders[i]); } return this; }; /** * * @param {edm.OrderBy} offset */ Select.prototype.addOffset = function (offset) { this.offset = offset; return this; }; /** * * @param {edm.OrderBy} limit */ Select.prototype.addLimit = function (limit) { this.limit = limit; return this; }; Select.prototype.toString = function () { return this.toSqlHana({}, []); }; Select.prototype.addFallbackStatement = function (statement) { this.fallbackStatement = statement; }; Select.prototype.getFallbackStatement = function () { return this.fallbackStatement; }; function Update() { this.update = { schema: null, table: null, alias: null }; this.from = { schema: null, table: null, alias: null }; this.copyFromTo = [];//list of SelectProperty this.whereAnded = []; } exports.Update = Update; /** * Set from clause * SQL: select ... from <from> * @param {{schema: string, table: string, alias: string}} table from qualified tablename */ Update.prototype.setTable = function (table) { this.update = table; }; Update.prototype.setFrom = function (from) { this.from = from; }; Update.prototype.addSetCopyProperties = function (properties, fromAlias) { var max = properties.length; var p; for (var i = 0; i < max; i++) { p = { from: new SelectProperty(fromAlias, properties[i].property, null), to: properties[i] }; this.copyFromTo.push(p); } }; Update.prototype.addWhereKeyValuePairs = function (keyValuePairs) { for (var i = 0; i < keyValuePairs.length; i++) { var kv = keyValuePairs[i]; this.whereAnded.push(new edm.BinaryOperator(edm.EQ, kv.key, kv.value, true /*strict*/)); } }; Update.prototype.toSqlHana = function (context, parameter) { // 'context.isHanaCloudDb': is set in SQL context (hence 'context' here) to member 'isHanaCloudDb'; // check creation of 'new SqlBuildHanaContext' above let isHanaCloudDb = (context.isHanaCloudDb) ? context.isHanaCloudDb : true; // Hana Cloud as default return (isHanaCloudDb) ? this.getHANACloudUpdateSql(context, parameter) : this.getHANAServiceUpdateSql(context, parameter); }; Update.prototype.getHANACloudUpdateSql = function(context, parameter) { var sql = ''; sql += 'MERGE INTO '; if (this.update.table.substr(0, 1) === '#') { sql += ' "' + this.update.table + '" '; if (this.update.alias) { sql += ' "' + this.update.alias + '"'; } } else { if (this.update.schema) { sql += '"' + this.update.schema + '".'; } sql += '"' + this.update.table + '"'; if (this.update.alias) { sql += ' "' + this.update.alias + '"'; } } sql += ' USING '; if (this.from.table.substr(0, 1) === '#') { sql += ' "' + this.from.table + '" '; if (this.from.alias) { sql += ' "' + this.from.alias + '"'; } } else { if (this.from.schema) { sql += '"' + this.from.schema + '".'; } sql += '"' + this.from.table + '"'; if (this.from.alias) { sql += ' "' + this.from.alias + '"'; } } if (this.whereAnded.length > 0) { sql += ' ON '; for (let i = 0; i < this.whereAnded.length; i++) { sql += (i === 0 ? '' : ' and '); sql += '('; sql += this.whereAnded[i].toSqlHana(context, parameter); sql += ')'; } } sql += ' WHEN MATCHED THEN UPDATE '; sql += ' SET '; for (let i = 0; i < this.copyFromTo.length; i++) { var cft = this.copyFromTo[i]; sql += (i === 0 ? '' : ', '); sql += cft.to.toSqlHana(context, parameter, { withoutTable: true }); sql += '='; sql += cft.from.toSqlHana(context, parameter); } return sql; }; Update.prototype.getHANAServiceUpdateSql = function(context, parameter) { var i; var sql = ''; sql += 'update '; if (this.update.table.substr(0, 1) === '#') { sql += ' "' + this.update.table + '" '; if (this.update.alias) { sql += ' "' + this.update.alias + '"'; } } else { if (this.update.schema) { sql += '"' + this.update.schema + '".'; } sql += '"' + this.update.table + '"'; if (this.update.alias) { sql += ' "' + this.update.alias + '"'; } } sql += ' set '; for (i = 0; i < this.copyFromTo.length; i++) { var cft = this.copyFromTo[i]; sql += (i === 0 ? '' : ', '); sql += cft.to.toSqlHana(context, parameter, { withoutTable: true }); sql += '='; sql += cft.from.toSqlHana(context, parameter); } // from <origin table> <origin alias> , <temp table> <temp alias> sql += '\nfrom '; if (this.update.schema) { sql += '"' + this.update.schema + '".'; } sql += ' "' + this.update.table + '" '; if (this.update.alias) { sql += ' "' + this.update.alias + '"'; } if (this.from.table.substr(0, 1) === '#') { sql += ' , '; sql += ' "' + this.from.table + '" '; if (this.from.alias) { sql += ' "' + this.from.alias + '"'; } } else { if (this.from.schema) { sql += '"' + this.from.schema + '".'; } sql += '"' + this.from.table + '"'; if (this.from.alias) { sql += ' "' + this.from.alias + '"'; } } if (this.whereAnded.length > 0) { sql += '\n where '; for (i = 0; i < this.whereAnded.length; i++) { sql += (i === 0 ? '' : ' and '); sql += '('; sql += this.whereAnded[i].toSqlHana(context, parameter); sql += ')'; } } return sql; }; /** * @class Upsert * @constructor */ function Upsert() { this.upsert = { schema: null, table: null, alias: null }; this.subSelect = null; this.nv = []; this.whereAnded = []; } exports.Upsert = Upsert; Upsert.prototype.setTable = function (table) { this.upsert = table; }; Upsert.prototype.setSubSelect = function (subSelect) { this.subSelect = subSelect; }; Upsert.prototype.addNames = function (names) { for (var i = 0; i < names.length; i++) { var n = names[i]; this.nv.push({ name: n, value: undefined //null would be a valid OData value }); } }; Upsert.prototype.setValue = function (name, value) { for (var i = 0; i < this.nv.length; i++) { var nv = this.nv[i]; if (nv.name.property === name) { nv.value = value; } } }; Upsert.prototype.addWhereKeyValuePairs = function (keyValuePairs) { for (var i = 0; i < keyValuePairs.length; i++) { var kv = keyValuePairs[i]; this.whereAnded.push(new edm.BinaryOperator(edm.EQ, kv.key, kv.value, true /*strict*/)); } }; Upsert.prototype.toSqlHana = function (context, parameter) { var i; var sql = ''; sql += 'upsert '; if (this.upsert.table.substr(0, 1) === '#') { sql += ' "' + this.upsert.table + '" '; if (this.upsert.alias) { sql += ' "' + this.upsert.alias + '"'; } } else { if (this.upsert.schema) { sql += '"' + this.upsert.schema + '".'; } sql += '"' + this.upsert.table + '"'; if (this.upsert.alias) { sql += ' "' + this.upsert.alias + '"'; } } var tmp = ''; if (this.subSelect) { //sql += ' ('; sql += ' ' + this.subSelect.toSqlHana(context, parameter); //sql += ') '; } else { sql += ' values '; tmp = ''; for (i = 0; i < this.nv.length; i++) { tmp += (tmp.length > 0 ? ', ' : ''); if (this.nv[i].value && this.nv[i].value.toSqlHana) { var pleaceholder = []; this.nv[i].value.toSqlHana(context, pleaceholder, { withoutTable: true }); tmp += this.nv[i].value.toSqlHana(context, parameter, { withoutTable: true }); } else { tmp += '?'; parameter.push(this.nv[i].value); } } sql += '(' + tmp + ')'; //collect values } if (this.whereAnded.length > 0) { sql += '\n where '; for (i = 0; i < this.whereAnded.length; i++) { sql += (i === 0 ? '' : ' and '); sql += '('; sql += this.whereAnded[i].toSqlHana(context, parameter); sql += ')'; } } return sql; }; /*------------------------------*/ /** * Creates following structure on given target object: * * target.table: { * name: nameParameter, * schema: schemaParameter, *} * *@param {Object} target The target object to create structure on *@param {String} tableName The table name *@param {String} [schema] The name of the schema *@throws {Error} throws an error if tableName was not provided *@private */ function initializeDDLObject(target, tableName, schema) { if (!tableName) { throw new Error("Can not create object without 'tableName' parameter"); } target.table = { name: tableName, schema: schema }; } /** * Creates a simple prefixed sql like '<prefix> TABLE "schema"."table"'. * *@param {Object} options Options object with following structure: * {prefix:..., schema: ..., table: ...} where schema is optional *@private */ function buildSimplePrefixedSql(options) { var sql = options.prefix + " TABLE "; if (options.schema) { sql += '"' + options.schema + '".'; } sql += '"' + options.table + '"'; return sql; } /** * Creates a drop table sql statement object. * *@param {string} tableName The table name to drop *@param {string} [schema] The name of the schema */ function Drop(tableName, schema) { initializeDDLObject(this, tableName, schema); } exports.Drop = Drop; /** * Creates the correspondig hana drop sql statement string which is represented by this object. * *@returns {string} The generated sql string */ Drop.prototype.toSqlHana = function () { return buildSimplePrefixedSql({ prefix: "DROP", schema: this.table.schema, table: this.table.name }); }; /** * Creates a truncate table sql statement object. * *@param {string} tableName The table name to truncate *@param {string} [schema] The name of the schema */ function Truncate(tableName, schema) { initializeDDLObject(this, tableName, schema); } exports.Truncate = Truncate; /** * Creates the correspondig hana truncate sql statement string which is represented by this object. * *@returns {string} The generated sql string */ Truncate.prototype.toSqlHana = function () { return buildSimplePrefixedSql({ prefix: "TRUNCATE", schema: this.table.schema, table: this.table.name }); }; /*------------------------------*/ function Delete() { this.from = { schema: null, table: null, alias: null }; this.whereAnded = []; } exports.Delete = Delete; Delete.prototype.setFrom = function (from) { this.from = from; }; Delete.prototype.addWhereKeyValuePairs = function (keyValuePairs) { for (var i = 0; i < keyValuePairs.length; i++) { var kv = keyValuePairs[i]; this.whereAnded.push(new edm.BinaryOperator(edm.EQ, kv.key, kv.value, true /*strict*/)); } }; Delete.prototype.toSqlHana = function (context, parameter) { var i = 0; var sql = ''; sql += 'delete '; sql += 'from '; if (this.from.table.substr(0, 1) === '#') { sql += '' + this.from.table + ''; if (this.from.alias) { sql += ' as "' + this.from.alias + '"'; } } else { if (this.from.schema) { sql += '"' + this.from.schema + '".'; } sql += '"' + this.from.table + '"'; if (this.from.alias) { sql += ' as "' + this.from.alias + '"'; } } if (this.whereAnded.length > 0) { sql += '\n where '; for (i = 0; i < this.whereAnded.length; i++) { sql += (i === 0 ? '' : ' and '); sql += this.whereAnded[i].toSqlHana(context, parameter); } } return sql; }; function addUniqueSqlStatementsToList(newStatements, list) { newStatements = [].concat(newStatements); newStatements.forEach(function (newStatement) { if (list.every(notEqual.bind(null, newStatement))) { list.push(newStatement); } }); function notEqual(prop, current) { return prop.toSqlHana({}, []) !== current.toSqlHana({}, []); } } exports._addETagToSelect = addETagToSelect; /** * Adds ETag generation expression to the SQL SELECT statement as an pseudo "__etag" column in the result set. * @param {object} dbSegment - DBSegment, which represents entity type of the entity, for which ETag should be generated * @param {object} selectStmt - SQL SELECT statement, where the ETag expression should be added to the SELECT part * @param {string} [tableAlias] - DB table alias. The parameter should be specified only if the alias of the table, * which is used in the SELECT statement is different than the Alias property of the DBSegment (i.e. default alias). */ function addETagToSelect(dbSegment, selectStmt, tableAlias) { var concurrentProperties = dbSegment.entityType.getConcurrentProperties(), hashFunctionArguments, eTagExpression; tableAlias = tableAlias || dbSegment.getAlias(); hashFunctionArguments = concurrentProperties.map(function (property) { var dbColumnName = util.format('"%s"."%s"', tableAlias, property); return util.format("ifnull(to_binary(%s), to_binary('')), case when %s is null then '01' else '00' end", dbColumnName, dbColumnName); }); eTagExpression = "to_varchar(hash_sha256(" + hashFunctionArguments.join(",") + "))"; selectStmt.addSelects(new SelectFormula(null, eTagExpression, "__etag")); } exports.createSelectStmtForETag = createSelectStmtForETag; /** * Creates SELECT statement to generate ETag for the entity. * * @param {object} dbSegment - DBSegment, which represents the entity type of the entity, for which ETag should be * generated. * @returns {Select} */ function createSelectStmtForETag(dbSegment) { var selectStmt = new Select(); addETagToSelect(dbSegment, selectStmt); selectStmt.addFrom(dbSegment.getAliasedTableName()); selectStmt.addWhereKeyValuePairs(dbSegment.getQKeyWithValues()); return selectStmt; } /** * Builds table statement object. The type of object created depends on the type parameter. * Currently supported is Truncate, Drop. * Examples: * <code> * var sql = require("lib/.../sqlStatement"); * var truncate = sql.buildTableStatement(sql.Truncate, sqlContext, "yourTableName"); * var drop = sql.buildTableStatement(sql.Drop, sqlContext, "yourTableName"); * </code> * *@param type {Function} The type of statement you wnat to create *@param sqlContext {Object} The current sql context *@param tableName {String} The name of the table to operate on *@param schema {String} The schema of the table to operate on *@returns {Object} Object representation of sql statement */ function buildTableStatement(type, sqlContext, tableName, schema) { sqlContext.context.logger.debug('sqlStatement', 'buildTableStatement'); if (type === exports.Truncate) { return new exports.Truncate(tableName, schema); } if (type === exports.Drop) { return new exports.Drop(tableName, schema); } return null; } exports.buildTableStatement = buildTableStatement; /** * Object representing the DB table column in the SQL query (except of the SELECT queries, where SelectProperty class * should be used). * @param {string} table - DB table name * @param {string} property - DB column name * @param {?string=} alias - alias, which should be used for the DB column in the SQL query * @constructor */ function TableColumn(table, property, alias) { this.table = table; this.property = property; this.alias = alias; } exports.TableColumn = TableColumn; TableColumn.prototype.toSqlHana = function (context, parameter, hint) { var sql = []; if (!(hint && hint.withoutTable) && this.table) { sql.push('"', this.table, '".'); } sql.push('"', this.property, '"'); if (this.alias) { sql.push(' "', this.alias, '"'); } return sql.join(""); };