@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
823 lines (674 loc) • 34.1 kB
JavaScript
'use strict';
const sql = require('./sqlStatement');
const edm = require('./../utils/typedObjects');
const sqlTools = require('./sqlTools');
const EntityType = require('../model/entityType.js');
const addETagToSelect = sql._addETagToSelect;
const utils = require('../utils/utils');
const BadRequest = require('../utils/errors/http/badRequest');
exports._masterTableSimpleSelect = masterTableSimpleSelect;
exports._expandSimpleSelect = expandSimpleSelect;
exports._expandInsert = expandInsert;
exports._masterTableInsert = masterTableInsert;
//Code
/**
* Create all SQL statements required for GET requests
* The SQL statements are stored directly in context.sql and dbSegments.sql.stmContainer.
* @param context
* @param asyncDone
* @returns {*}
*/
exports.createGetSqlStatements = function (context, asyncDone) {
var sqlContext = context.sql = {
netId: context.uniqueNetworkRequestID,
reqId: context.uniqueRequestID,
rId: 1,
dbSegLast: context.oData.dbSegmentLast,
systemQueryParameters: context.oData.systemQueryParameters,
context: context
};
context.logger.silly('createGetStatements', 'createGetSqlStatements');
sqlContext.dbSegLast.sql.stmContainer = new sql.GetContainer();
sqlContext.rId = 1;
context.sql.cleanSessionTruncateContainer = [];
context.sql.cleanSessionDropContainer = [];
masterTable(sqlContext);
expandedTables(sqlContext, sqlContext.dbSegLast, '', []);
context.sql.container = sqlTools.collectSqls(context, sqlContext);
return asyncDone(null, context);
};
/**
* Create SQL statements for the master table (=end of resource path)
* @param sqlContext
*/
function masterTable(sqlContext) {
let dbSeg = sqlContext.dbSegLast;
sqlContext.context.logger.debug('createGetStatements', 'masterTable');
//dbSeg.sql.stmContainer = new sql.GetContainer();
if (dbSeg.restriction.onlyCount === true) {
//no expand, avoid temporary tables
dbSeg.sql.stmContainer.select = exports._masterTableSimpleSelect(sqlContext);
dbSeg.sql.stmContainer.select.setAddCalcViewHint();
dbSeg.sql.stmContainer.select.setHints(sqlContext.context.gModel.getHints());
} else if (dbSeg.restriction.onlyValue === true) {
//no expand, avoid temporary tables
dbSeg.sql.stmContainer.select = exports._masterTableSimpleSelect(sqlContext);
dbSeg.sql.stmContainer.select.setAddCalcViewHint();
dbSeg.sql.stmContainer.select.setHints(sqlContext.context.gModel.getHints());
} else if (dbSeg._ExpandedNavigations.length === 0) {
//no expand, avoid temporary tables
dbSeg.sql.stmContainer.select = exports._masterTableSimpleSelect(sqlContext);
dbSeg.sql.stmContainer.select.setAddCalcViewHint();
dbSeg.sql.stmContainer.select.setHints(sqlContext.context.gModel.getHints());
} else {
dbSeg.sql.rId = sqlTools.rIdToTableName(sqlContext.reqId, sqlContext.rId++);
//create statement for master table
dbSeg.sql.stmContainer.createTmp = masterTableCreate(sqlContext, dbSeg.sql.rId);
//create insert statement for master table
dbSeg.sql.stmContainer.insertTmp = masterTableInsert(sqlContext);
//create select statement for master table
dbSeg.sql.stmContainer.selectTmp = masterTableSelect(sqlContext);
dbSeg.sql.stmContainer.selectTmp.setAddCalcViewHint();
dbSeg.sql.stmContainer.selectTmp.setHints(sqlContext.context.gModel.getHints());
if (sqlContext.context.db && sqlContext.context.db.isExternalHandledConnection === true) {
// We only truncate and delete temp tables when db connection is handled external
// otherwise we will self disconnect and temp tables will be deleted automatically
// build the truncate statement for created temporary table;
const truncateStmt = sql.buildTableStatement(sql.Truncate, sqlContext, dbSeg.sql.stmContainer.createTmp.table);
const truncateContainer = sqlContext.context.sql.cleanSessionTruncateContainer;
truncateContainer.push(truncateStmt);
sqlContext.context.networkContext.cleanSessionTruncateContainer.push(truncateStmt);
// build the drop statement for created temporary table;
const dropStmt = sql.buildTableStatement(sql.Drop, sqlContext, dbSeg.sql.stmContainer.createTmp.table);
const dropContainer = sqlContext.context.sql.cleanSessionDropContainer;
dropContainer.push(dropStmt);
sqlContext.context.networkContext.cleanSessionDropContainer.push(dropStmt);
}
}
}
/**
* Load the limit information from the xsodata file
* @param sqlContext
* @param {string} name Name of the limit setting (e.g. 'max_records')
* @returns {*}
*/
function getLimit(sqlContext, name) {
if (!sqlContext) {return null;}
const context = sqlContext.context;
if (!context) {return null;}
const model = context.gModel;
if (model && model._settings && model._settings.limits && model._settings.limits[name]) {
const limit = model._settings.limits[name];
return Number.parseInt(limit);
} else {
if (name === 'max_expanded_records') {
return 10000;
} else {
return 1000;
}
}
return null;
}
function masterTableSimpleSelect(sqlContext, countFallback) {
// SAPINFO A simple odata GET request without and expand is processed without using temporary tables.
// Count fallback it true if a SELECT is created which is used to obtain the max count for all records, even
// if the result set is empty. (If the result is not empty there would be a count column)
var dbSeg, isCount, select, orderBy, selectCount;
sqlContext.context.logger.debug('createGetStatements', 'masterTableSimpleSelect');
dbSeg = sqlContext.dbSegLast;
isCount = dbSeg.restriction.onlyCount || countFallback;
//create select statement
select = simpleSelectJoins(dbSeg, sqlContext);
//add Query $filter
if (sqlContext.systemQueryParameters.filter) {
if (!checkConditionsProps(sqlContext.systemQueryParameters.filter, dbSeg.entityType.propertiesMap)) {
throw new BadRequest('Unknown filter property');
}
// Add WHERE statement from uri
if (sqlContext.systemQueryParameters.filter.setAlias) {
//filters like '$filter=1' have no alias because there are just literals
sqlContext.systemQueryParameters.filter.setAlias(dbSeg.getAlias());
}
select.addWhereAnded(sqlContext.systemQueryParameters.filter);
}
//add Query $orderby
if (!isCount) {
if (sqlContext.systemQueryParameters.orderby) {
if (!checkOrderbyProps(sqlContext.systemQueryParameters.orderby, dbSeg.entityType.propertiesMap)) {
throw new BadRequest('Unknown orderby property');
}
orderBy = sqlContext.systemQueryParameters.orderby;
if (orderBy.setAlias) { // Add WHERE statement from uri
orderBy.setAlias(dbSeg.getAlias());
}
if (orderBy.applyConverter) {
orderBy.applyConverter(replaceEdmPropertiesWithSqlProperties.bind(null, dbSeg));
}
select.addOrderBy(orderBy);
}
}
// group by & order by
if (dbSeg.entityType.hasAggregates()) {
select.addGroupBys(dbSeg.getAllSelectedNonAggregateIncludingJoinPropertiesForGroupBy());
if (!isCount) {
select.addSortOrders(dbSeg.getAllSelectedNonAggregatePropertiesForOrderBy(null, true));
}
} else {
if (!isCount) {
if (!(dbSeg.entityType.kind === EntityType.entityKind.calculationView &&
dbSeg.entityType._entityType.parameters &&
dbSeg.entityType._entityType.parameters.viaKey === true)) {
select.addSortOrders(dbSeg.getKeyProperties0123ForOrderBy(true));
} else {
let orderByProperties = dbSeg.getKeyProperties0123ForOrderByCalcView(true);
for (let i= 0; i < select.select.length; i++) {
for (let j= 0; j < orderByProperties.length; j++) {
if (orderByProperties[j].getPropertyName() === select.select[i].property) {
select.select[i].alias = orderByProperties[j].expression.property;
}
}
}
select.addSortOrders(orderByProperties);
}
}
}
//add $skip --> Offset
if (sqlContext.systemQueryParameters.skip && !countFallback) {
select.addOffset(sqlContext.systemQueryParameters.skip);
}
//add $top --> Limit
if (!isCount) {
const maxRecords = getLimit(sqlContext, 'max_records');
const top = sqlContext.systemQueryParameters.top;
if (maxRecords) {
select.setCustomData('maxRecords', maxRecords);
if (top >= 0) {
if (top > maxRecords) {
// Value of $top is ok. If there are only max_records records these are returned.
// If there are max_records+1 or more records an error will be thrown
select.addLimit(maxRecords + 1);
} else {
select.addLimit(top);
}
} else {
// Add a max_records+1 limit. If there are only max_records records these are returned.
// If there are max_records+1 or more records an error will be thrown
select.addLimit(maxRecords + 1);
}
} else {
if (top >= 0) {
select.addLimit(top);
}
}
}
if (!countFallback) {
if (sqlContext.systemQueryParameters.inlinecount) {
select.addFallbackStatement(masterTableSimpleSelect(sqlContext, true));
}
}
if (isCount) {
//wrap select
selectCount = new sql.Select();
selectCount.setFromBySubQuery(select);
selectCount.addSelects([new sql.SelectFormula(null, 'count(*)', 'c')]);
} else if (!sqlContext.systemQueryParameters.expand && !countFallback &&
utils.isETagRequired(sqlContext.context, dbSeg)) {
addETagToSelect(dbSeg, select);
}
return selectCount || select;
/**
* scan the tree of conditions, and check that every given property exist in the corresponding entityType
* @param {object} tree - the conditions tree. It consists of an object with further objects, arrays or properties
* @param {object} props - the propertiesMap of the given entityType
* @return {boolean}
* */
function checkConditionsProps(tree, props) {
var objpr;
// loop over all properties
for (objpr in tree) {
if (tree.hasOwnProperty(objpr)) {
// if the property is an object itself, then recursively call the check
if (typeof tree[objpr] === 'object') {
if (!checkConditionsProps(tree[objpr], props)) {
return false;
}
} else if (objpr === 'property' && !props.hasOwnProperty(tree[objpr])) {
return false;
}
}
}
return true;
}
/**
* scan the array of $orderby orders, and check that every given property exist in the corresponding entityType
* @param {object} tree - the conditions tree. It consists of an object with the array of orders
* @param {object} props - the propertiesMap of the given entityType
* @return {boolean}
* */
function checkOrderbyProps(tree, props) {
var expression, i;
// loop over all orders
for (i = 0; i < tree.orders.length; i++) {
expression = tree.orders[i].expression;
// loop over all properties
if (!checkConditionsProps(expression, props)) {
return false;
}
}
return true;
}
/**
* handle the MxN properties in the associative 'over' table
* */
function handleKeyPropertiesInOverTable(inner, dbSeg) {
var seg1overProp, seg1parentsJoinProp, seg1childsJoinProp;
var seg2overProp, seg2parentsJoinProp, seg2childsJoinProp;
var i;
//create join property list from the first end ('dependent' | 'principal')
seg1overProp = (dbSeg.isPrincipal() ? 'dependent' : 'principal'); // seg1 is on the contrary to dbSeg (that referes to dbSegmentLast
seg1parentsJoinProp = dbSeg.getFrom().joinproperties;
seg1childsJoinProp = dbSeg.getOver()[seg1overProp];
for (i = 0; i < seg1parentsJoinProp.length; i++) {
inner.addWhereAnded(new edm.BinaryOperator(edm.EQ,
/* no dbType required as the binary operator compares to properties, not property and value */
new edm.Property(seg1parentsJoinProp[i], dbSeg.previousDBSegment.getAlias()),
/* no dbType required as the binary operator compares to properties, not property and value */
new edm.Property(seg1childsJoinProp[i], dbSeg.previousDBSegment.getAssocAlias()),
true /*no OData like null handling*/
));
}
//create join property list from the second end
seg2overProp = (dbSeg.isPrincipal() ? 'principal' : 'dependent');
seg2parentsJoinProp = dbSeg.getOver()[seg2overProp];
seg2childsJoinProp = dbSeg.getTo().joinproperties;
for (i = 0; i < seg2parentsJoinProp.length; i++) {
inner.addWhereAnded(new edm.BinaryOperator(edm.EQ,
/* no dbType required as the binary operator compares to properties, not property and value */
new edm.Property(seg2childsJoinProp[i], dbSeg.getAlias()),
/* no dbType required as the binary operator compares to properties, not property and value */
new edm.Property(seg2parentsJoinProp[i], dbSeg.previousDBSegment.getAssocAlias()),
true /*no OData like null handling*/
));
}
}
/**
* handle the 1xN properties in the corresponding table
* */
function handleKeyProperties(inner, dbSeg) {
//create join property list
const parentsJoinProp = dbSeg.getFrom().joinproperties;
const childsJoinProp = dbSeg.getTo().joinproperties;
var i;
for (i = 0; i < parentsJoinProp.length; i++) {
inner.addWhereAnded(new edm.BinaryOperator(edm.EQ,
/* no dbType required as the binary operator compares to properties, not property and value */
new edm.Property(parentsJoinProp[i], dbSeg.previousDBSegment.getAlias()),
/* no dbType required as the binary operator compares to properties, not property and value */
new edm.Property(childsJoinProp[i], dbSeg.getAlias()),
true /*no OData like null handling*/
));
}
}
/**
* Creates a sql command to select data to which the resource path points to, all segments of the resource path with their dbSeg equivalents are joined
* Sample: select "FromEntity1"."KEY_1" "0", "FromEntity1"."KEY_1", "FromEntity1"."INTEGER_1", "FromEntity1"."NVARCHAR_1" from "xsodata.test.tables::calc_nav__entity_from" as "FromEntity1"
* @param dbSeg
* @param sqlContext
* @returns {Select}
*/
function simpleSelectJoins(dbSeg, sqlContext) {
sqlContext.context.logger.debug('createGetStatements', 'simpleSelectJoins');
//create select statement
var stm = new sql.Select();
var inner = null;
if (dbSeg.previousDBSegment) { //end of recursion
inner = simpleSelectJoins(dbSeg.previousDBSegment, sqlContext);
if (dbSeg.getOver()) {
handleKeyPropertiesInOverTable(inner, dbSeg);
} else {
handleKeyProperties(inner, dbSeg);
}
}
//SELECT
if (dbSeg.nextDBSegment) {
stm.addSelects([new sql.SelectFormula(null, '1')]);
} else {
if (dbSeg.entityType.hasGeneratedKey()) {
stm.addSelects(dbSeg.getAllSelectedPropertiesConsideringAggregates());
} else {
if (dbSeg.entityType.kind === EntityType.entityKind.calculationView && dbSeg.entityType._entityType.parameters && dbSeg.entityType._entityType.parameters.viaKey === true) {
// Adding properties from getKeyProperties0123ForSelectAs0123 is not needed since
// if (in future) an association via input parameter is used then the input parameter values must be added via ? into the sql command
// that's because calcview don't return their input parameter in the result set
let tmp = dbSeg.getPropertiesForSelectCollectInputParameters();
// since the input parameters may not be returned from the calcview the parameters are merged after the select
// from "calcview2" into the result. // Q: What does that mean: "calcview2"?
stm.byPassInputs = tmp.input;
stm.addSelects(tmp.selects);
stm.addSelects(dbSeg.getKeyPropertiesNotSelectedForSelect(undefined, dbSeg.entityType.inputParameters));
// keep aliased key properties
let selectProperties = dbSeg.getKeyProperties0123ForSelectOnCalcViewAs0123();
dbSeg.setAliasedKeyPropertiesOnCalcView(selectProperties);
} else {
stm.addSelect(dbSeg.getKeyProperties0123ForSelectAs0123());
stm.addSelects(dbSeg.getPropertiesForSelect());
stm.addSelects(dbSeg.getKeyPropertiesNotSelectedForSelect());
}
//}
//stm.addSelects(dbSeg.getAllSelectedPropertiesConsideringAggregates());
}
if (dbSeg._ExpandedNavigations.length > 0 || dbSeg._SelectedNavigations.length > 0) {
stm.addSelects(dbSeg.getNavPropertiesForSelect());
}
//stm.addSelects(dbSeg.getQNonKeyProperties());
if (sqlContext.systemQueryParameters.inlinecount && !countFallback) {
stm.addSelects(new sql.SelectFormula(null, 'count(*) over () as ', '0count'));
}
}
//FROM
if (dbSeg.nextDBSegment && dbSeg.nextDBSegment.getOver()) {
stm.addFrom({
schema: dbSeg._DB_Schema,
table: dbSeg.nextDBSegment.getOver().object,
alias: dbSeg.getAssocAlias()
});
}
var fromSource = dbSeg.getAliasedTableName();
//fromSource.parameters = dbSeg.inputParams;
stm.addFrom(fromSource, dbSeg.getInputParameters());
if (inner) {
//WHERE set Exists first
stm.addWhereAnded(new sql.Exists(inner));
}
//WHERE set Keys next
stm.addWhereKeyValuePairs(dbSeg.getQKeyWithValues());
return stm;
}
}
function replaceEdmPropertiesWithSqlProperties(dbSeg, expression) {
if (expression instanceof edm.Property) {
return new edm.Parenthesis(dbSeg.getPropertyAsSelectProperty(expression.property));
}
return expression;
}
function masterTableCreate(sqlContext, rId) {
sqlContext.context.logger.debug('createGetStatements', 'masterTableCreate');
var dbSeg = sqlContext.dbSegLast;
var isCount = sqlContext.systemQueryParameters.inlinecount;
var stmCreate = new sql.Create();
stmCreate.setModifiers(['local', 'temporary', dbSeg.entityType.tableStoreType]);
stmCreate.setTableName(rId);
if (dbSeg.entityType.hasGeneratedKey()) {
stmCreate.addProperties(dbSeg.getPropertiesForCreate());
} else {
stmCreate.addProperties(dbSeg.getKeyProperties0123ForCreate());
stmCreate.addProperties(dbSeg.getPropertiesForCreate());
stmCreate.addProperties(dbSeg.getKeyPropertiesNotSelectedForCreate()); // SKDO for uri building
}
if (dbSeg._ExpandedNavigations.length > 0 || dbSeg._SelectedNavigations.length > 0) {
stmCreate.addProperties(dbSeg.getNavPropertiesForCreate());
}
if (isCount) {
stmCreate.addProperty(new sql.CreateProperty('0count', 'BIGINT'));
}
stmCreate.addProperty(new sql.CreateProperty('1row', 'BIGINT'));
return stmCreate;
}
function masterTableInsert(sqlContext) {
sqlContext.context.logger.debug('createGetStatements', 'masterTableInsert');
//SystemQuery option order: $format, $inlinecount, $filter, $orderby, $skiptoken, $skip, $top, $expand.
var dbSeg = sqlContext.dbSegLast;
var stmInsert = new sql.Insert();
stmInsert.setTableName({ table: dbSeg.sql.rId });
var selectRow = new sql.Select();
selectRow.addSelects(new sql.SelectFormula(null, '*'));
selectRow.addSelects(new sql.OverFormula('1row'));
var subSelect = exports._masterTableSimpleSelect(sqlContext);
selectRow.setFromBySubQuery(subSelect);
stmInsert.setSubSelect(selectRow);
return stmInsert;
}
function masterTableSelect(sqlContext) {
sqlContext.context.logger.debug('createGetStatements', 'masterTableSelect');
var dbSeg = sqlContext.dbSegLast;
var stmSelect = new sql.Select();
var isCount = sqlContext.systemQueryParameters.inlinecount;
addSelectsForSelectFromTmp(stmSelect, dbSeg);
stmSelect.addSelects([new sql.SelectProperty(null, '1row')]);
if (isCount) {
stmSelect.addSelects([new sql.SelectProperty(null, '0count')]);
} else if (utils.isETagRequired(sqlContext.context, dbSeg)) {
addETagToSelect(dbSeg, stmSelect, dbSeg.sql.rId);
}
// If at least one expand is used, then a tmp table is used to collect the
// master tables records first. Into this tmp table only max_records entries
// are inserted (because a limit is added to the inserts subquery, e.g.
// insert into "#r0_1" (
// select *, row_number() over() "1row" from (
// select "a_key1"."KEY" "0", "a_key1"."KEY", "a_key1"."INFO", "a_key1"."NOKEY", count(*) over () as "0count"
// from "xsodata.test.tables::massdata_a_key" as "a_key1"
// order by "0" limit ?)) <--- added limit
// ). Since there is no direct select execution of the 2nd inner select, because the tmp table is read with a
// direct select on the tmp table, the maxRecords must be added to this direct select.
const maxRecords = getLimit(sqlContext, 'max_records');
stmSelect.setCustomData('maxRecords', maxRecords);
addFromAndSortOrderForSelectFromTmp(stmSelect, dbSeg.sql.rId);
return stmSelect;
}
function expandedTables(sqlContext, parentSeg, path) {
var navigationNames = parentSeg._ExpandedNavigations, truncateStmt, dropStmt,
truncateContainer = sqlContext.context.sql.cleanSessionTruncateContainer,
dropContainer = sqlContext.context.sql.cleanSessionDropContainer;
//loop thought expanded tables
for (var i = 0; i < navigationNames.length; i++) {
var dbSegExpanded = parentSeg.getRelevantNavigationSegments()[navigationNames[i]];
dbSegExpanded.sql.stmContainer = new sql.GetContainer();
//check if it makes sense to create a tmp table (this is done only if there are further
//expands on the already expanded table.
if (dbSegExpanded._ExpandedNavigations.length > 0) {
dbSegExpanded.sql.rId = sqlTools.rIdToTableName(sqlContext.reqId, sqlContext.rId++);
dbSegExpanded.sql.path = path + '/' + dbSegExpanded.getAlias();
//Sql for creating tmp table
dbSegExpanded.sql.stmContainer.createTmp = expandCreate(dbSegExpanded);
//Sql for filling tmp table
dbSegExpanded.sql.stmContainer.insertTmp = exports._expandInsert(dbSegExpanded, sqlContext);
//Sql for reading tmp table
dbSegExpanded.sql.stmContainer.selectTmp = expandSelect(dbSegExpanded, sqlContext);
if (sqlContext.context.db &&
sqlContext.context.db.isExternalHandledConnection === true) {
// We only truncate and delete temp tables when db connection is handled external
// otherwise we will self disconnect and temp tables will be deleted automatically
// build the truncate statement for created temporary table;
truncateStmt = sql.buildTableStatement(sql.Truncate, sqlContext, dbSegExpanded.sql.stmContainer.createTmp.table);
truncateContainer.push(truncateStmt);
sqlContext.context.networkContext.cleanSessionTruncateContainer.push(truncateStmt);
// build the drop statement for created temporary table;
dropStmt = sql.buildTableStatement(sql.Drop, sqlContext, dbSegExpanded.sql.stmContainer.createTmp.table);
dropContainer.push(dropStmt);
sqlContext.context.networkContext.cleanSessionDropContainer.push(dropStmt);
}
} else {
//no, makes not sense, just do a normal select
dbSegExpanded.sql.stmContainer.select = exports._expandSimpleSelect(dbSegExpanded, true, sqlContext);
}
//recurse
expandedTables(sqlContext, dbSegExpanded, path + '/' + dbSegExpanded.getAlias());
}
}
function expandCreate(dbSeg) {
var stmCreate = new sql.Create();
stmCreate.setModifiers(['local', 'temporary', dbSeg.entityType.tableStoreType]);
stmCreate.setTableName(dbSeg.sql.rId);
stmCreate.addProperty(new sql.CreateProperty('0row', 'BIGINT'));
stmCreate.addProperty(new sql.CreateProperty('1row', 'BIGINT'));
if (dbSeg.entityType.hasGeneratedKey()) {
stmCreate.addProperties(dbSeg.getPropertiesForCreate());
} else {
stmCreate.addProperties(dbSeg.getKeyProperties0123ForCreate());
stmCreate.addProperties(dbSeg.getPropertiesForCreate());
stmCreate.addProperties(dbSeg.getKeyPropertiesNotSelectedForCreate());
}
if (dbSeg._ExpandedNavigations.length > 0 || dbSeg._SelectedNavigations.length > 0) {
stmCreate.addUniqueProperties(dbSeg.getNavPropertiesForCreate());
}
return stmCreate;
}
function expandInsert(dbSeg, sqlContext) {
var subSelect = exports._expandSimpleSelect(dbSeg, undefined, sqlContext);
var stmInsert = new sql.Insert();
stmInsert.setTableName({ table: dbSeg.sql.rId });
stmInsert.setSubSelect(subSelect);
return stmInsert;
}
/**
* Creates SELECT statement for the entities, which should be expanded.
* @param {object} dbSeg - DBSegment representing the entity type of the entities, which should be expanded
* @param {boolean} [addEtag] - flag indicating whether ETag generation expression should be added to the created
* SELECT statement
* @param {object} [sqlContext] - SQL context
* @returns {Select}
*/
function expandSimpleSelect(dbSeg, addEtag, sqlContext) {
var subSelect = new sql.Select();
//SELECT
//this adds the 1row index from the parent table.
subSelect.addSelects(new sql.SelectProperty(dbSeg.previousDBSegment.getAlias(), '1row', null, '0row'));
var over = createOverWithOrderBys(dbSeg); //this ensure the
subSelect.addSelects(over);
//Over with 1row is not required here because this dbSeg has no expands
subSelect.addSelect(new sql.SelectFormula(dbSeg._Alias, '*', null));
if (addEtag && utils.isETagRequired(sqlContext.context, dbSeg)) {
addETagToSelect(dbSeg, subSelect);
}
//FROM
subSelect.setFrom({
schema: null,
table: dbSeg.previousDBSegment.sql.rId,
alias: dbSeg.previousDBSegment.getAlias()
});
var onStatements = createOnStatements(dbSeg);
if (onStatements.first.length > 0) {
subSelect.addJoin({
table: dbSeg.getOver().object,
alias: dbSeg.getAssocAlias()
}, onStatements.first);
}
var joinSelect = new sql.Select();
if (dbSeg.entityType.hasGeneratedKey()) {
joinSelect.addSelects(dbSeg.getAllSelectedIncludingJoinPropertiesConsideringAggregates());
} else {
joinSelect.addSelects(dbSeg.getQKeyPropertiesWith0123AliasForSelect());
joinSelect.addSelects(dbSeg.getPropertiesForSelect());
joinSelect.addSelects(dbSeg.getKeyPropertiesNotSelectedForSelect());
joinSelect.addUniqueSelects(dbSeg.getNonKeyJoinProperties());
}
if (dbSeg._ExpandedNavigations.length > 0 || dbSeg._SelectedNavigations.length > 0) {
joinSelect.addUniqueSelects(dbSeg.getNavPropertiesForSelect());
}
joinSelect.setFrom(dbSeg.getAliasedTableName());
// group bys
if (dbSeg.entityType.hasAggregates()) {
var groupBys = dbSeg.getAllSelectedNonAggregateIncludingJoinPropertiesForGroupBy();
joinSelect.addGroupBys(groupBys);
}
subSelect.addJoinTable(joinSelect, onStatements.second, dbSeg._Alias);
/* no dbType required as property is used only for SortOrder */
subSelect.addSortOrder(new edm.SortOrder(new edm.Property('1row'), 'ASC'));
const maxExpandedRecords = getLimit(sqlContext, 'max_expanded_records');
if (maxExpandedRecords) {
subSelect.setCustomData('maxExpandedRecords', maxExpandedRecords);
// Add a maxExpandedRecords+1 limit. If there are only maxExpandedRecords records these are returned.
// If there are maxExpandedRecords+1 or more records an error will be thrown
// With OData V2 its not possible to restrict the number of expanded records
subSelect.addLimit(maxExpandedRecords + 1);
}
return subSelect;
}
function createOverWithOrderBys(dbSeg) {
var over = new sql.OverFormula('1row'); //this ensure the
/* no dbType required as property is used only for SortOrder */
over.addSortOrder(new edm.SortOrder(new edm.Property('1row', dbSeg.previousDBSegment.getAlias()), 'ASC'));
if (dbSeg.entityType.hasGeneratedKey()) {
over.addSortOrders(dbSeg.getAllSelectedNonAggregatePropertiesForOrderBy(null, true));
} else {
over.addSortOrders(dbSeg.getKeyProperties0123ForOrderBy());
}
return over;
}
function createOnStatements(dbSeg) {
var parentsJoinProps = dbSeg.getFrom().joinproperties;
var childJoinProps = dbSeg.getTo().joinproperties;
var overJoinProps = dbSeg.getOver();
var onStatements = {
first: [],
second: []
};
if (overJoinProps) {
return parentsJoinProps.reduce(createOnStatementWithOver, onStatements);
} else {
return parentsJoinProps.reduce(createOnStatementWithoutOver, onStatements);
}
function createOnStatementWithOver(onStatements, parentsJoinProp, index) {
onStatements.first.push(createOnStatement(
parentsJoinProp, dbSeg.previousDBSegment.getAlias(),
overJoinProps.principal[index], dbSeg.getAssocAlias()
));
onStatements.second.push(createOnStatement(
childJoinProps[index], dbSeg.getAlias(),
overJoinProps.dependent[index], dbSeg.getAssocAlias()
));
return onStatements;
}
function createOnStatementWithoutOver(onStatements, parentsJoinProp, index) {
onStatements.second.push(createOnStatement(
parentsJoinProp, dbSeg.previousDBSegment.getAlias(),
childJoinProps[index], dbSeg.getAlias()
));
return onStatements;
}
function createOnStatement(firstProp, firstAlias, secondProp, secondAlias) {
return new edm.BinaryOperator(edm.EQ,
/* no dbType required as the binary operator compares to properties, not property and value */
new edm.Property(firstProp, firstAlias),
/* no dbType required as the binary operator compares to properties, not property and value */
new edm.Property(secondProp, secondAlias),
true /*no OData like null handling*/
);
}
}
function expandSelect(dbSeg, sqlContext) {
var subSelect = new sql.Select();
subSelect.addSelects([new sql.SelectProperty(null, '0row')]);
subSelect.addSelects([new sql.SelectProperty(null, '1row')]);
addSelectsForSelectFromTmp(subSelect, dbSeg);
if (utils.isETagRequired(sqlContext.context, dbSeg)) {
addETagToSelect(dbSeg, subSelect, dbSeg.sql.rId);
}
addFromAndSortOrderForSelectFromTmp(subSelect, dbSeg.sql.rId);
const maxExpandedRecords = getLimit(sqlContext, 'max_expanded_records');
if (maxExpandedRecords) {
subSelect.setCustomData('maxExpandedRecords', maxExpandedRecords);
// Add a maxExpandedRecords+1 limit. If there are only maxExpandedRecords records these are returned.
// If there are maxExpandedRecords+1 or more records an error will be thrown
// With OData V2 its not possible to restrict the number of expanded records
subSelect.addLimit(maxExpandedRecords + 1);
}
return subSelect;
}
function addSelectsForSelectFromTmp(select, dbSeg) {
if (dbSeg.entityType.hasGeneratedKey()) {
select.addSelects(dbSeg.getPropertiesForSelect(true));
} else {
select.addSelects(dbSeg.getKeyProperties0123ForSelect(true));
select.addSelects(dbSeg.getPropertiesForSelect(true));
select.addSelects(dbSeg.getKeyPropertiesNotSelectedForSelect(true)); // SKDO for uri building
}
if (dbSeg._ExpandedNavigations.length > 0 || dbSeg._SelectedNavigations.length > 0) {
select.addSelects(dbSeg.getNavPropertiesForSelect(true));
}
}
function addFromAndSortOrderForSelectFromTmp(select, rId) {
select.setFrom({ schema: null, table: rId, alias: null });
/* no dbType required as property is used only for SortOrder */
select.addSortOrder(new edm.SortOrder(new edm.Property('1row', null), 'ASC'));
}