@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
1,009 lines (899 loc) • 37.2 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) {
const 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');
if (
dbSeg.restriction.onlyCount === true ||
dbSeg.restriction.onlyValue === true ||
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;
}
}
}
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)
let dbSeg, isCount, select, 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
addQueryFilter(sqlContext, select, dbSeg);
//add Query $orderby
addQueryOrderBy(sqlContext, select, dbSeg, isCount);
// group by & order by
addGroupByAndOrderByForAggregates(select, dbSeg, isCount);
//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) {
// loop over all properties
for (let 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) {
let expression;
// loop over all orders
for (const treeOrder of tree.orders) {
expression = treeOrder.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) {
let seg1overProp, seg1parentsJoinProp, seg1childsJoinProp;
let seg2overProp, seg2parentsJoinProp, seg2childsJoinProp;
//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 (let 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 (let 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;
for (let 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
const stm = new sql.Select();
let 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()
);
}
}
if (
dbSeg._ExpandedNavigations.length > 0 ||
dbSeg._SelectedNavigations.length > 0
) {
stm.addSelects(dbSeg.getNavPropertiesForSelect());
}
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(),
});
}
const fromSource = dbSeg.getAliasedTableName();
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 addQueryFilter(sqlContext, select, dbSeg) {
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);
}
}
function addQueryOrderBy(sqlContext, select, dbSeg, isCount) {
if (!isCount) {
if (sqlContext.systemQueryParameters.orderby) {
if (
!checkOrderbyProps(
sqlContext.systemQueryParameters.orderby,
dbSeg.entityType.propertiesMap
)
) {
throw new BadRequest('Unknown orderby property');
}
const 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);
}
}
}
function addGroupByAndOrderByForAggregates(select, dbSeg, isCount) {
if (dbSeg.entityType.hasAggregates()) {
select.addGroupBys(
dbSeg.getAllSelectedNonAggregateIncludingJoinPropertiesForGroupBy()
);
if (!isCount) {
select.addSortOrders(
dbSeg.getAllSelectedNonAggregatePropertiesForOrderBy(
null,
true
)
);
}
return;
}
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 (const elem of select.select) {
for (const ordered of orderByProperties) {
if (ordered.getPropertyName() === elem.property) {
elem.alias = ordered.expression.property;
}
}
}
select.addSortOrders(orderByProperties);
}
}
}
}
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');
const dbSeg = sqlContext.dbSegLast;
const isCount = sqlContext.systemQueryParameters.inlinecount;
const 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.
const dbSeg = sqlContext.dbSegLast;
const stmInsert = new sql.Insert();
stmInsert.setTableName({ table: dbSeg.sql.rId });
const selectRow = new sql.Select();
selectRow.addSelects(new sql.SelectFormula(null, '*'));
selectRow.addSelects(new sql.OverFormula('1row'));
const subSelect = exports._masterTableSimpleSelect(sqlContext);
selectRow.setFromBySubQuery(subSelect);
stmInsert.setSubSelect(selectRow);
return stmInsert;
}
function masterTableSelect(sqlContext) {
sqlContext.context.logger.debug('createGetStatements', 'masterTableSelect');
const dbSeg = sqlContext.dbSegLast;
const stmSelect = new sql.Select();
const 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) {
const navigationNames = parentSeg._ExpandedNavigations,
truncateContainer =
sqlContext.context.sql.cleanSessionTruncateContainer,
dropContainer = sqlContext.context.sql.cleanSessionDropContainer;
let truncateStmt;
let dropStmt;
//loop thought expanded tables
for (const navigationName of navigationNames) {
const dbSegExpanded =
parentSeg.getRelevantNavigationSegments()[navigationName];
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) {
const 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) {
const subSelect = exports._expandSimpleSelect(dbSeg, undefined, sqlContext);
const 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) {
const 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'
)
);
const 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(),
});
const onStatements = createOnStatements(dbSeg);
if (onStatements.first.length > 0) {
subSelect.addJoin(
{
table: dbSeg.getOver().object,
alias: dbSeg.getAssocAlias(),
},
onStatements.first
);
}
const 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()) {
const 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) {
const 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) {
const parentsJoinProps = dbSeg.getFrom().joinproperties;
const childJoinProps = dbSeg.getTo().joinproperties;
const overJoinProps = dbSeg.getOver();
const 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) {
const 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')
);
}