UNPKG

x2node-dbos

Version:
1,599 lines (1,374 loc) 58.9 kB
'use strict'; const propsTreeBuilder = require('./props-tree-builder.js'); const Translatable = require('./translatable.js'); /** * Get id column for the container. * * @private * @param {module:x2node-records~PropertiesContainer} container Properties * container. * @returns {string} Column name. */ function getIdColumn(container) { return container.getPropertyDesc(container.idPropertyName).column; } /** * Get map key column. * * @private * @param {module:x2node-records~PropertyDescriptor} propDesc Map property * descriptor. * @param {module:x2node-records~PropertiesContainer} keyPropContainer Container * where to look for the key property, if applicable. * @returns {string} Column name. */ function getKeyColumn(propDesc, keyPropContainer) { if (propDesc.keyColumn) return propDesc.keyColumn; const keyPropDesc = keyPropContainer.getPropertyDesc( propDesc.keyPropertyName); return keyPropDesc.column; } /** * Create and return an object for the select list element. * * @private * @param {(string|module:x2node-dbos~Translatable|function)} sql The * value, which can be a SQL expression, a value expression object or a SQL * translation function. * @param {string} markup Markup for the result set parser. * @returns {Object} The select list element descriptor. */ function makeSelector(sql, markup) { return { sql: (sql instanceof Translatable ? sql.translate.bind(sql) : sql), markup: markup }; } /** * Create and return an object for the order list element. * * @private * @param {(string|module:x2node-dbos~Translatable|function)} sql The * value, which can be a SQL expression, a value expression object or a SQL * translation function. * @returns {Object} The order list element descriptor. */ function makeOrderElement(sql) { return { sql: (sql instanceof Translatable ? sql.translate.bind(sql) : sql) }; } /** * Aggregate function SQL generators by aggregate function name. * * @private * @enum {function} */ const AGGREGATE_FUNCS = { 'COUNT': function(valueExpr, ctx) { return 'COUNT(' + valueExpr.translate(ctx) + ')'; }, 'SUM': function(valueExpr, ctx) { return ctx.dbDriver.coalesce( 'SUM(' + valueExpr.translate(ctx) + ')', '0'); }, 'MIN': function(valueExpr, ctx) { return 'MIN(' + valueExpr.translate(ctx) + ')'; }, 'MAX': function(valueExpr, ctx) { return 'MAX(' + valueExpr.translate(ctx) + ')'; }, 'AVG': function(valueExpr, ctx) { return 'AVG(' + valueExpr.translate(ctx) + ')'; } }; /** * SQL translation context. * * @protected * @memberof module:x2node-dbos * @inner */ class TranslationContext { /** * Create new context. * * @param {module:x2node-records~RecordTypesLibrary} recordTypes Record types * library. * @param {string} basePath Base path. When the context is asked to resolve a * property path to the corresponding SQL, it prepends the base path to the * property path before performing the lookup. * @param {module:x2node-dbos~QueryTreeNode} queryTree The query tree * being translated. * @param {module:x2node-dbos~FilterParamsHandler} paramsHandler Filter * parameters handler. */ constructor(recordTypes, basePath, queryTree, paramsHandler) { this._recordTypes = recordTypes; this._basePath = basePath; this._basePathPrefix = (basePath.length > 0 ? basePath + '.' : ''); this._queryTree = queryTree; this._dbDriver = queryTree._dbDriver; this._propsSql = queryTree._propsSql; this._propValueColumns = queryTree._propValueColumns; this._rootPropNode = queryTree.rootPropNode; this._paramsHandler = paramsHandler; } /** * Create new context by cloning this one and replacing the base path with * the one provided. * * @param {string} basePath The new base path. * @returns {module:x2node-dbos~TranslationContext} The new context. */ rebase(basePath) { return new TranslationContext( this._recordTypes, basePath, this._queryTree, this._paramsHandler); } /** * Query tree being translated. * * @member {module:x2node-dbos~QueryTreeNode} * @readonly */ get queryTree() { return this._queryTree; } /** * The DB driver being used for the translation. * * @member {module:x2node-dbos.DBDriver} * @readonly */ get dbDriver() { return this._dbDriver; } /** * Translate the specified property path into the corresponding value SQL * expression. Context's base path, if any, is automatically added to the * specified path before looking it up in the query tree's mappings. * * @param {string} propPath Property path. * @returns {string} Property value SQL expression. */ translatePropPath(propPath) { const sql = this._propsSql.get(this._basePathPrefix + propPath); return ((typeof sql) === 'function' ? sql(this) : sql); } /** * Property value column descriptor. * * @protected * @typedef {Object} module:x2node-dbos~TranslationContext~ColumnInfo * @property {string} tableName Table name. * @property {string} tableAlias Table alias. * @property {string} columnName Column name. */ /** * Get information about the database column used to store the specified * property's value. Only properties whose value is stored in a column and * can be updated by updating the column are available through this method, * so properties of scalar value type "object" are never available nor * calculated or dependent reference properties are. * * <p>A set special mappings is made available as well to allow access to * columns that are not directly mapped to named properties. For non-object * collection properties the value column can be found by adding ".$value" to * the property path. For map properties, the key column can be found by * adding ".$key" to the property path. For properties stored in their own * tables (including nested object properties), the parent id column can be * found by adding ".$parentId" to the property path. * * @param {string} propPath Property path. * @returns {module:x2node-dbos~TranslationContext~ColumnInfo} Column * information object. */ getPropValueColumn(propPath) { return this._propValueColumns.get(this._basePathPrefix + propPath); } /** * Filter parameters handler. * * @member {module:x2node-dbos~FilterParamsHandler} * @readonly */ get paramsHandler() { return this._paramsHandler; } /** * Rebase the specified property path by adding the context's base path to * it. * * @param {string} propPath Property path to rebase. * @returns {string} Rebased property path. */ rebasePropPath(propPath) { return this._basePathPrefix + propPath; } /** * Rebase the specified <code>Translatable</code> to the context's base path. * * @param {module:x2node-dbos~Translatable} translatable The translatable. * @returns {module:x2node-dbos~Translatable} Rebased translatable. */ rebaseTranslatable(translatable) { return translatable.rebase(this._basePath); } /** * Build properties tree for a subquery. * * @param {string} colPropPath Path of the collection property being * subqueried. The context automatically adds its base path to it. * @param {Iterable.<string>} propPaths Paths of the properties to include in * the tree. The context automatically adds its base path to all of these. * @param {string} clause The subqiery clause. * @returns {module:x2node-dbos~PropertyTreeNode} The properties tree. */ buildSubqueryPropsTree(colPropPath, propPaths, clause) { let basedPropPaths; if (this._basePathPrefix.length > 0) { basedPropPaths = new Set(); propPaths.forEach(p => { basedPropPaths.add(this.rebasePropPath(p)); }); } else { basedPropPaths = propPaths; } return propsTreeBuilder.buildPropsTreeBranches( this._recordTypes, this._rootPropNode.desc, clause, this._rootPropNode.getValueExpressionContext(), this.rebasePropPath(colPropPath), basedPropPaths, { noWildcards: true, noAggregates: true, ignoreScopedOrders: true, noScopedFilters: true, includeScopeProp: true, ignoreFiltersOn: this.rebasePropPath(colPropPath) } )[0]; } } /** * Parent node property name. * * @private * @constant {Symbol} */ const PARENT_NODE = Symbol('PARENT_NODE'); /** * The query tree node. * * @protected * @memberof module:x2node-dbos * @inner */ class QueryTreeNode { /** * Create new node. Used by the <code>createChildNode</code> method as well * as once directly to create the top tree node. * * @param {module:x2node-dbos.DBDriver} dbDriver The database driver. * @param {module:x2node-records~RecordTypesLibrary} recordTypes Record types * library. * @param {Map.<string,(string|function)>} propsSql Map being populated with * mappings between property paths and corresponding SQL value expressions. * @param {Map.<string,Object>} propValueColumns Map being populated with * mappings between property paths and corresponding value columns info. * @param {Map.<string,module:x2node-dbos~RecordsFilter>} delayedJoinConditions * Map used to delay attaching join conditions until a matching descendant * node is added to the tree. * @param {boolean} singleAxis <code>true</code> to disallow having more than * one expanding child for this node. * @param {module:x2node-dbos~PropertiesTreeNode} propNode Properties tree * node, for which the node was created (the first node that needs to be * included in order to get the property and any of its children). * @param {boolean} collection <code>true</code> if the top table of a * collection property. * @param {string} table The table, for which the node is being created. * @param {string} tableAlias Table alias. * @param {string} keyColumn Name of the column in the node's table that can * be used to join children to. */ constructor( dbDriver, recordTypes, propsSql, propValueColumns, delayedJoinConditions, singleAxis, propNode, collection, table, tableAlias, keyColumn) { this._dbDriver = dbDriver; this._recordTypes = recordTypes; this._propsSql = propsSql; this._propValueColumns = propValueColumns; this._delayedJoinConditions = delayedJoinConditions; this._singleAxis = singleAxis; this._propNode = propNode; this._collection = collection; this._table = table; this._tableAlias = tableAlias; this._keyColumn = keyColumn; this._childTableAliasPrefix = tableAlias; this._nextChildTableAliasDisc = 'a'.charCodeAt(0); this._select = new Array(); this._order = new Array(); this._singleRowChildren = new Array(); this._expandingChildren = new Array(); } /** * Clone the node without adding any of its children. * * @returns {module:x2node-dbos~QueryTreeNode} The node clone. */ cloneWithoutChildren() { return new QueryTreeNode( this._dbDriver, this._recordTypes, new Map(this._propsSql), new Map(this._propValueColumns), new Map(), this._singleAxis, this._propNode, this._collection, this._table, this._tableAlias, this._keyColumn); } /** * Remove the top node of the tree and return the new top, which used to be * the first and only child of this node. * * @returns {module:x2node-dbos~QueryTreeNode} The new top node. */ behead() { const allChildren = this._allChildren; if (allChildren.length > 1) throw new Error('Internal X2 error: more than one neck.'); const newHead = allChildren[0]; delete newHead[PARENT_NODE]; newHead.rootPropNode = this.rootPropNode; newHead._select = this._select.concat(newHead._select); newHead._aggregatedBelow = this._aggregatedBelow; newHead._aggregatedKeySql = this._aggregatedKeySql; newHead._virtual = false; delete newHead._joinByColumn; delete newHead._joinToColumn; return newHead; } /** * Change the table alias and the child table alias prefix for the node. * * @param {string} tableAlias New table alias. * @param {string} [childTableAliasPrefix] New child table alias prefix. If * unspecified, the table alias is used. If empty string, the next child * table will have alias "z" (used for the anchor table node). */ setTableAlias(tableAlias, childTableAliasPrefix) { this._tableAlias = tableAlias; this._childTableAliasPrefix = ( childTableAliasPrefix !== undefined ? childTableAliasPrefix : tableAlias); if (this._childTableAliasPrefix.length === 0) this._nextChildTableAliasDisc = 'z'.charCodeAt(0); } /** * Create child node. * * @param {string} propNode Corresponding properties tree node. * @param {string} table The table, for which the node is being created. * @param {string} keyColumn Name of the column in the node's table that can * be used to join children to. * @param {boolean} many <code>true</code> if node's table is on the "many" * side of the relation. * @param {boolean} virtual <code>true</code> if may select no rows. * @param {string} joinByColumn Name of the column in the node's table used * to join to the parent table. * @param {string} joinToColumn Name of the column in the parent node's table * used for the join. * @param {module:x2node-dbos~RecordsFilter} [joinCondition] Optional * additional condition for the join. If provided, the node is made virtual * regardless of the <code>virtual</code> flag. * @param {module:x2node-dbos~RecordsOrder} [order] Optional additional * ordering specification for the join. * @returns {module:x2node-dbos~QueryTreeNode} The new child node. */ createChildNode( propNode, table, keyColumn, many, virtual, joinByColumn, joinToColumn, joinCondition, order) { // create child node const childNode = new QueryTreeNode( this._dbDriver, this._recordTypes, this._propsSql, this._propValueColumns, this._delayedJoinConditions, this._singleAxis, propNode, many, table, this._childTableAliasPrefix + String.fromCharCode(this._nextChildTableAliasDisc++), keyColumn ); // add join parameters childNode._virtual = (virtual || (joinCondition !== undefined)); childNode._joinByColumn = joinByColumn; childNode._joinToColumn = joinToColumn; // add join condition if any const propPath = propNode.path; const delayedJoinCondition = this._delayedJoinConditions.get(propPath); if (joinCondition && delayedJoinCondition) { this._delayedJoinConditions.delete(propPath); childNode._joinCondition = joinCondition.conjoin( delayedJoinCondition); } else if (delayedJoinCondition) { this._delayedJoinConditions.delete(propPath); childNode._joinCondition = delayedJoinCondition; } else if (joinCondition) { childNode._joinCondition = joinCondition; } // add collection anchor ordering if (this.anchor) { childNode._order.push(makeOrderElement( childNode._tableAlias + '.' + childNode._keyColumn)); } else if (many) { let anchorNode = this; while (anchorNode && !anchorNode._collection) anchorNode = anchorNode[PARENT_NODE]; if (anchorNode && !anchorNode.anchor) childNode._order.push(makeOrderElement( anchorNode._tableAlias + '.' + anchorNode._keyColumn)); } // add scoped order if any if (order) order.elements.forEach(orderElement => { childNode._order.push(makeOrderElement(orderElement)); }); else if (many && propNode.desc.indexColumn) childNode._order.push(makeOrderElement( childNode._tableAlias + '.' + propNode.desc.indexColumn)); // set the child node parent childNode[PARENT_NODE] = this; // add the child to the parent children if (propNode.isExpanding()) { if (this._singleAxis && (this._expandingChildren.length > 1)) throw new Error( 'Internal X2 error: attempt to add more than one expanding' + ' child to a query tree node.'); this._expandingChildren.push(childNode); } else { this._singleRowChildren.push(childNode); } // return the new child node return childNode; } /** * Add element to the select list. * * @param {Object} selector Select list element descriptor. * @returns {Object} Added select list element descriptor. */ addSelect(selector) { this._select.push(selector); return selector; } /** * Mark the node as having its collection child node the first table in the * chain of aggregated tables. * * @param {(string|function)} [keySql] Aggregated map key SQL. * @param {string} colPropPath Aggregated collection property path. * @param {module:x2node-dbos~RecordsFilter} [colFilter] Optional * aggregated collection property filter. */ makeAggregatedBelow(keySql, colPropPath, colFilter) { this._aggregatedBelow = true; this._aggregatedKeySql = keySql; if (colFilter) this._delayedJoinConditions.set(colPropPath, colFilter); } /** * Recursively add child property to this query tree node. * * @param {module:x2node-dbos~PropertyTreeNode} propNode Child property * tree node to add to this query tree node. * @param {Array.<string>} clauses List of clauses to include. The property * is not added if it is not used in one of the listed clauses. * @param {Object} markupCtx Markup context for the property. */ addProperty(propNode, clauses, markupCtx) { // check the clause if (!clauses.some(clause => propNode.isUsedIn(clause))) return; // determine if the property needs to be selected const select = ( propNode.isSelected() && clauses.some(clause => clause === 'select')); // property basics const propDesc = propNode.desc; // create markup context for possible children const markupPrefix = markupCtx.prefix; const childrenMarkupCtx = ( propNode.hasChildren() || (select && !propDesc.isScalar()) ? { prefix: markupPrefix.substring(0, markupPrefix.length - 1) + String.fromCharCode(markupCtx.nextChildMarkupDisc++) + '$', nextChildMarkupDisc: 'a'.charCodeAt(0) } : undefined ); // get reference related data let refTargetDesc, refTargetIdColumn, fetch, reverseRefPropDesc; if (propDesc.isRef()) { refTargetDesc = this._recordTypes.getRecordTypeDesc( propDesc.refTarget); refTargetIdColumn = getIdColumn(refTargetDesc); fetch = ( propNode.hasChildren() && Array.from(propNode.children).some( childPropNode => childPropNode.isSelected()) ); if (propDesc.reverseRefPropertyName) reverseRefPropDesc = refTargetDesc.getPropertyDesc( propDesc.reverseRefPropertyName); } // process property node depending on its type let queryTreeNode, anchorSelector, keyColumn, idIncluded; let valueSelectors = new Array(); switch ( (propDesc.isScalar() ? 'scalar' : ( propDesc.isArray() ? 'array' : 'map')) + ':' + propDesc.scalarValueType ) { case 'scalar:string': case 'scalar:number': case 'scalar:boolean': case 'scalar:datetime': // add the property queryTreeNode = this._addScalarSimpleProperty( propNode, markupPrefix, propDesc.table, propDesc.parentIdColumn, this._keyColumn, null, propDesc.column, false, valueSelectors ); // add value to the select list if (select) valueSelectors.forEach(s => { queryTreeNode.addSelect(s); }); break; case 'scalar:object': // check if stored in a separate table if (propDesc.table) { // create child node for the object table queryTreeNode = this.createChildNode( propNode, propDesc.table, propDesc.parentIdColumn, false, propDesc.optional, propDesc.parentIdColumn, this._keyColumn); // save parent id column mapping this.addPropValueColumn( propNode.path + '.$parentId', queryTreeNode.table, queryTreeNode.tableAlias, propDesc.parentIdColumn); // create anchor selector anchorSelector = makeSelector( queryTreeNode.tableAlias + '.' + propDesc.parentIdColumn, markupPrefix + propDesc.name ); } else { // stored in the same table // create anchor selector anchorSelector = makeSelector( ( propDesc.presenceTest ? ctx => ctx.dbDriver.booleanToNull( propDesc.presenceTest .rebase(propNode.basePath) .translate(ctx)) : this._dbDriver.booleanLiteral(true) ), markupPrefix + propDesc.name ); // add child properties to the same node queryTreeNode = this; } // add anchor selector if (select) queryTreeNode.addSelect(anchorSelector); // add selected child properties for (let p of propNode.children) queryTreeNode.addProperty(p, clauses, childrenMarkupCtx); break; case 'scalar:ref': // check if implicit dependent record reference if (propDesc.implicitDependentRef) { // add referred record type table queryTreeNode = this.createChildNode( propNode, refTargetDesc.table, refTargetIdColumn, false, false, refTargetIdColumn, propDesc.column); } else if (propDesc.reverseRefPropertyName) { // dependent reference // add the property if (reverseRefPropDesc.table) { // add the reference property queryTreeNode = this._addScalarSimpleProperty( propNode, markupPrefix, reverseRefPropDesc.table, reverseRefPropDesc.column, this._keyColumn, reverseRefPropDesc.parentIdColumn, reverseRefPropDesc.parentIdColumn, fetch, valueSelectors ); // add referred record table if used if (propNode.hasChildren()) queryTreeNode = queryTreeNode.createChildNode( propNode, refTargetDesc.table, refTargetIdColumn, false, false, refTargetIdColumn, reverseRefPropDesc.parentIdColumn); } else { // no link table // add the reference property queryTreeNode = this._addScalarSimpleProperty( propNode, markupPrefix, refTargetDesc.table, reverseRefPropDesc.column, this._keyColumn, refTargetIdColumn, refTargetIdColumn, fetch, valueSelectors ); } } else { // direct reference // add the reference property queryTreeNode = this._addScalarSimpleProperty( propNode, markupPrefix, propDesc.table, propDesc.parentIdColumn, this._keyColumn, propDesc.column, propDesc.column, fetch, valueSelectors ); // add referred record table if used if (propNode.hasChildren()) queryTreeNode = queryTreeNode.createChildNode( propNode, refTargetDesc.table, refTargetIdColumn, false, (propDesc.table ? false : propDesc.optional), refTargetIdColumn, propDesc.column); } // add value to the select list if (select) valueSelectors.forEach(s => { queryTreeNode.addSelect(s); }); // add used referred record properties if (propNode.hasChildren()) { for (let p of propNode.children) queryTreeNode.addProperty(p, clauses, childrenMarkupCtx); } break; case 'array:string': case 'array:number': case 'array:boolean': case 'array:datetime': case 'map:string': case 'map:number': case 'map:boolean': case 'map:datetime': // check if aggregate map if (propDesc.isAggregate()) { // create and save value and key mappings const valueSql = AGGREGATE_FUNCS[propDesc.aggregateFunc].bind( null, propDesc.valueExpr.rebase(propNode.basePath)); this.addPropSql(propNode.path, valueSql); this.addPropSql(propNode.path + '.value', valueSql); const keySql = ctx => ( ctx.rebase(propNode.basePath).translatePropPath( propDesc.aggregatedPropPath + '.' + propDesc.keyPropertyName) ); this.addPropSql(propNode.path + '.$key', keySql); // mark the node with aggregation below this.makeAggregatedBelow( keySql, propNode.basePrefix + propDesc.aggregatedPropPath, ( propDesc.filter && propDesc.filter.rebase(propNode.basePath) ) ); // add value to the select list if (select) { this.addSelect(makeSelector( keySql, markupPrefix + propNode.desc.name )); this.addSelect(makeSelector( valueSql, childrenMarkupCtx.prefix + 'value' )); } } else { // not an aggregate map // add value table queryTreeNode = this.createChildNode( propNode, propDesc.table, null, true, propDesc.optional, propDesc.parentIdColumn, this._keyColumn, (select && propDesc.filter && propDesc.filter.rebase( propNode.basePath)), (select && propDesc.order && propDesc.order.rebase( propNode.basePath)) ); // save collection canonical mappings this._saveCollectionCanonicalMappings(queryTreeNode); // save value column and SQL mapping const valSql = queryTreeNode.tableAlias + '.' + propDesc.column; this.addPropSql(propNode.path, valSql); const valPath = propNode.path + '.$value'; this.addPropSql(valPath, valSql); this.addPropValueColumn( valPath, queryTreeNode.table, queryTreeNode.tableAlias, propDesc.column); // add anchor and value selectors if (select) { const anchorSql = queryTreeNode.tableAlias + '.' + ( propDesc.isMap() ? propDesc.keyColumn : propDesc.parentIdColumn); queryTreeNode.addSelect(makeSelector( anchorSql, markupPrefix + propNode.desc.name)); queryTreeNode.addSelect(makeSelector( valSql, childrenMarkupCtx.prefix)); } } break; case 'array:object': case 'map:object': // determine collection element key column keyColumn = ( propDesc.isMap() ? getKeyColumn(propDesc, propDesc.nestedProperties) : getIdColumn(propDesc.nestedProperties) ); // create child node for the objects table queryTreeNode = this.createChildNode( propNode, propDesc.table, ( propDesc.nestedProperties.idPropertyName ? getIdColumn(propDesc.nestedProperties) : keyColumn ), true, propDesc.optional, propDesc.parentIdColumn, this._keyColumn, (select && propDesc.filter && propDesc.filter.rebase( propNode.basePath)), (select && propDesc.order && propDesc.order.rebase( propNode.basePath))); // save collection canonical mappings this._saveCollectionCanonicalMappings(queryTreeNode); // add anchor selector if (select) queryTreeNode.addSelect(makeSelector( queryTreeNode.tableAlias + '.' + keyColumn, markupPrefix + propDesc.name )); // add selected child properties for (let p of propNode.children) queryTreeNode.addProperty(p, clauses, childrenMarkupCtx); break; case 'array:ref': case 'map:ref': // id included by the add collection helper function idIncluded = true; // check if implicit dependent record reference if (propDesc.implicitDependentRef) { // add referred record type table queryTreeNode = this.createChildNode( propNode, refTargetDesc.table, refTargetIdColumn, true, false, refTargetIdColumn, propDesc.column); // add the id property idIncluded = false; } else if (propDesc.reverseRefPropertyName) { // dependent reference // add the property if (reverseRefPropDesc.table) { // add the tables queryTreeNode = this._addRefPropertyViaLinkTable( propNode, select, markupPrefix, (childrenMarkupCtx && childrenMarkupCtx.prefix), reverseRefPropDesc.table, reverseRefPropDesc.column, reverseRefPropDesc.parentIdColumn, refTargetDesc, fetch ); } else { // no link table // add referred record table queryTreeNode = this.createChildNode( propNode, refTargetDesc.table, refTargetIdColumn, true, propDesc.optional, reverseRefPropDesc.column, this._keyColumn, (select && propDesc.filter && propDesc.filter.rebase( propNode.basePath)), (select && propDesc.order && propDesc.order.rebase( propNode.basePath)) ); // create and save value and key mappings const valSql = queryTreeNode.tableAlias + '.' + refTargetIdColumn; this.addPropSql(propNode.path, valSql); this.addPropSql( propNode.path + '.' + refTargetDesc.idPropertyName, valSql ); const keySql = queryTreeNode.tableAlias + '.' + ( propDesc.isMap() ? getKeyColumn(propDesc, refTargetDesc) : refTargetIdColumn ); if (propDesc.isMap()) this.addPropSql(propNode.path + '.$key', keySql); // add value to the select list if neccesary if (select) { queryTreeNode.addSelect(makeSelector( keySql, markupPrefix + propNode.desc.name + (fetch ? ':' : ''))); queryTreeNode.addSelect(makeSelector( valSql, childrenMarkupCtx.prefix + refTargetDesc.idPropertyName)); } } } else { // direct reference (always via link table) // add the tables queryTreeNode = this._addRefPropertyViaLinkTable( propNode, select, markupPrefix, (childrenMarkupCtx && childrenMarkupCtx.prefix), propDesc.table, propDesc.parentIdColumn, propDesc.column, refTargetDesc, fetch ); } // add used referred record properties if (propNode.hasChildren()) { for (let p of propNode.children) if (!idIncluded || !p.desc.isId()) queryTreeNode.addProperty(p, clauses, childrenMarkupCtx); } break; default: // should never happen throw new Error('Internal X2 error: unknown property type.'); } // save type column mapping for polymorphic container if (propDesc.isPolymorphObject()) { const polymorphProps = propDesc.nestedProperties; const typePropDesc = polymorphProps.getPropertyDesc( polymorphProps.typePropertyName); if (typePropDesc.column) { const typePropPath = propNode.path + '.' + polymorphProps.typePropertyName; this.addPropValueColumn( typePropPath, queryTreeNode.table, queryTreeNode.tableAlias, typePropDesc.column); this.addPropSql( typePropPath, queryTreeNode.tableAlias + '.' + typePropDesc.column ); } } } /** * Helper function used by <code>addProperty</code> to add simple scalars. * * @private * @param {module:x2node-dbos~PropertyTreeNode} propNode Scalar simple * value property node. * @param {string} markupPrefix Property's level markup prefix. * @param {string} [valueTable] Value table name if the property is stored in * its own table. * @param {string} [valueTableParentIdColumn] Column in the value table, if * any, that points back to the container table. * @param {string} [parentTableIdColumn] Column in the container table, to * which the value table, if any, points back. * @param {?string} [valueTableKeyColumn] Column in the value table that can * be used to join children to (for fetched references). * @param {string} [valueColumn] Column that contains the property value (for * stored properties). * @param {boolean} fetchedRef <code>true</code> if fetched reference. * @param {Array} valueSelectors Array, to which to add value selectors. * @returns {module:x2node-dbos~QueryTreeNode} The leaf node possibly * added by the method (or this node if no tables were added). */ _addScalarSimpleProperty( propNode, markupPrefix, valueTable, valueTableParentIdColumn, parentTableIdColumn, valueTableKeyColumn, valueColumn, fetchedRef, valueSelectors) { // the leaf node let queryTreeNode; // check if calculated let valueSelector; const propDesc = propNode.desc; if (propDesc.isAggregate()) { // create value selector valueSelector = makeSelector( AGGREGATE_FUNCS[propDesc.aggregateFunc].bind( null, propDesc.valueExpr.rebase(propNode.basePath)), markupPrefix + propDesc.name ); // mark the node with aggregation below this.makeAggregatedBelow( null, propNode.basePrefix + propDesc.aggregatedPropPath, (propDesc.filter && propDesc.filter.rebase(propNode.basePath))); // add value to this query tree node queryTreeNode = this; } else if (propDesc.isCalculated()) { // create value selector valueSelector = makeSelector( propDesc.valueExpr.rebase(propNode.basePath), markupPrefix + propDesc.name ); // add value to this query tree node queryTreeNode = this; } else { // stored value // check if stored in a separate table if (valueTable) { // create child node for the table queryTreeNode = this.createChildNode( propNode, valueTable, valueTableKeyColumn, false, propDesc.optional, valueTableParentIdColumn, parentTableIdColumn); // save parent id column mapping this.addPropValueColumn( propNode.path + '.$parentId', queryTreeNode.table, queryTreeNode.tableAlias, valueTableParentIdColumn); } else { // stored in the same table // add value to this query tree node queryTreeNode = this; } // save value column mapping this.addPropValueColumn( propNode.path, queryTreeNode.table, queryTreeNode.tableAlias, valueColumn); // create value selector valueSelector = makeSelector( queryTreeNode.tableAlias + '.' + valueColumn, markupPrefix + propDesc.name + (fetchedRef ? ':' : '') ); } // save value mapping this.addPropSql(propNode.path, valueSelector.sql); // save value selector valueSelectors.push(valueSelector); // return the leaf node return queryTreeNode; } /** * Helper function used by <code>addProperty</code> to add reference * collection properties that use a link table. * * @private * @param {module:x2node-dbos~PropertyTreeNode} propNode Reference * collection property node. * @param {boolean} select <code>true</code> if the property value is * selected. * @param {string} markupPrefix Property's level markup prefix. * @param {string} [childrenMarkupPrefix] Children markup prefix (only if * selected). * @param {string} linkTable Link table name. * @param {string} linkTableParentIdColumn Column in the link table that * points back to the property table. * @param {string} linkTableTargetIdColumn Column in the link table that * points to the reference target record table. * @param {module:x2node-records~RecordTypeDescriptor} refTargetDesc Referred * record type descriptor. * @param {boolean} fetchedRef <code>true</code> if fetched reference. * @returns {module:x2node-dbos~QueryTreeNode} The leaf node added by the * method. */ _addRefPropertyViaLinkTable( propNode, select, markupPrefix, childrenMarkupPrefix, linkTable, linkTableParentIdColumn, linkTableTargetIdColumn, refTargetDesc, fetchedRef) { // needs referred record table? let queryTreeNode; let valTableName, valTableAlias, valColumn; let keyTableName, keyTableAlias, keyColumn; const propDesc = propNode.desc; if (propNode.hasChildren() || propDesc.keyPropertyName) { // add the link table queryTreeNode = this.createChildNode( propNode, linkTable, linkTableTargetIdColumn, true, propDesc.optional, linkTableParentIdColumn, this._keyColumn, (select && propDesc.filter && propDesc.filter.rebase( propNode.basePath)), (select && propDesc.order && propDesc.order.rebase( propNode.basePath)) ); // save parent id column mapping this.addPropValueColumn( propNode.path + '.$parentId', queryTreeNode.table, queryTreeNode.tableAlias, linkTableParentIdColumn); // check if the key is in the link table if (propDesc.keyColumn) { keyTableName = queryTreeNode.table; keyTableAlias = queryTreeNode.tableAlias; keyColumn = propDesc.keyColumn; } else if (propDesc.isArray()) { keyTableName = queryTreeNode.table; keyTableAlias = queryTreeNode.tableAlias; keyColumn = linkTableTargetIdColumn; } // add referred record table const refTargetIdColumn = getIdColumn(refTargetDesc); queryTreeNode = queryTreeNode.createChildNode( propNode, refTargetDesc.table, refTargetIdColumn, false, false, refTargetIdColumn, linkTableTargetIdColumn ); // create value and key expressions valTableName = queryTreeNode.table; valTableAlias = queryTreeNode.tableAlias; valColumn = refTargetIdColumn; if (!keyTableName) { keyTableName = queryTreeNode.table; keyTableAlias = queryTreeNode.tableAlias; keyColumn = getKeyColumn(propDesc, refTargetDesc); } } else { // only the link table is needed // add the link table queryTreeNode = this.createChildNode( propNode, linkTable, linkTableTargetIdColumn, true, propDesc.optional, linkTableParentIdColumn, this._keyColumn, (select && propDesc.filter && propDesc.filter.rebase( propNode.basePath)), (select && propDesc.order && propDesc.order.rebase( propNode.basePath)) ); // save parent id column mapping this.addPropValueColumn( propNode.path + '.$parentId', queryTreeNode.table, queryTreeNode.tableAlias, linkTableParentIdColumn); // create value and key expressions valTableName = queryTreeNode.table; valTableAlias = queryTreeNode.tableAlias; valColumn = linkTableTargetIdColumn; keyTableName = queryTreeNode.table; keyTableAlias = queryTreeNode.tableAlias; keyColumn = (propDesc.keyColumn || linkTableTargetIdColumn); } // save value column and SQL mappings const valSql = valTableAlias + '.' + valColumn; this.addPropSql(propNode.path, valSql); const valPath = propNode.path + '.$value'; this.addPropSql(valPath, valSql); this.addPropSql( propNode.path + '.' + refTargetDesc.idPropertyName, valSql); this.addPropValueColumn(valPath, valTableName, valTableAlias, valColumn); // save map key column and SQL mappings const keySql = keyTableAlias + '.' + keyColumn; if (propDesc.isMap()) { const keyPath = propNode.path + '.$key'; this.addPropSql(keyPath, keySql); this.addPropValueColumn( keyPath, keyTableName, keyTableAlias, keyColumn); } // save array index column and SQL mapping if (propDesc.isArray() && propDesc.indexColumn) { const indPath = propNode.path + '.$index'; this.addPropSql( indPath, keyTableAlias + '.' + propDesc.indexColumn); this.addPropValueColumn( indPath, keyTableName, keyTableAlias, propDesc.indexColumn); } // add value to the select list if neccesary if (select) { queryTreeNode.addSelect(makeSelector( keySql, markupPrefix + propNode.desc.name + (fetchedRef ? ':' : ''))); queryTreeNode.addSelect(makeSelector( valSql, childrenMarkupPrefix + refTargetDesc.idPropertyName)); } // return the node return queryTreeNode; } /** * Register canonical mappings "$parentId", "$key" and "$index" for a * collection property. * * @private * @param {module:x2node-dbos~QueryTreeNode} queryTreeNode Query tree node * corresponding to the collection property table. */ _saveCollectionCanonicalMappings(queryTreeNode) { const propNode = queryTreeNode._propNode; const propDesc = propNode.desc; // save parent id column mapping this.addPropValueColumn( propNode.path + '.$parentId', queryTreeNode.table, queryTreeNode.tableAlias, propDesc.parentIdColumn); // save map key column and SQL mapping if (propDesc.isMap()) { const keyPath = propNode.path + '.$key'; const keyColumn = getKeyColumn(propDesc, propDesc.nestedProperties); this.addPropSql( keyPath, queryTreeNode.tableAlias + '.' + keyColumn); this.addPropValueColumn( keyPath, queryTreeNode.table, queryTreeNode.tableAlias, keyColumn); } // save array index column and SQL mapping if (propDesc.isArray() && propDesc.indexColumn) { const indPath = propNode.path + '.$index'; this.addPropSql( indPath, queryTreeNode.tableAlias + '.' + propDesc.indexColumn); this.addPropValueColumn( indPath, queryTreeNode.table, queryTreeNode.tableAlias, propDesc.indexColumn); } } /** * Callback for the tree walk methods. * * @callback module:x2node-dbos~QueryTreeNode~walkCallback * @param {module:x2node-dbos~PropertyTreeNode} propNode Property tree node * associated with the query tree node being visited. * @param {module:x2node-dbos~QueryTreeNode~TableDesc} tableDesc Descriptor * of the table associated with the node being visited. * @param {Array.<module:x2node-dbos~QueryTreeNode~TableDesc>} tableChain * Chain of table descrpitors leading down to the node being visited from the * top tree node. When the top tree node is visited, the chain is an empty * array. */ /** * Table descriptor passed to the tree walk methods. * * @typedef {Object} module:x2node-dbos~QueryTreeNode~TableDesc * @property {string} tableName Table name. * @property {string} tableAlias Table alias. * @property {Array.<Object>} selectElements Array of <code>SELECT</code> * clause elements that come from the table. * @property {string} selectElements.valueExpr SQL value expression. * @property {string} selectElements.markup Result set parser markup. * @property {string} [basicJoinCondition] Boolean SQL expression used to * join table to its parent using only the key columns, or * <code>undefined</code> if top query tree node. * @property {string} [joinCondition] Boolean SQL expression used to join the * table to its parent, or <code>undefined</code> if top query tree node. * @property {boolean} outerJoin <code>true</code> if the table is joined to * its parent using an outer join. * @property {boolean} aggregated <code>true</code> if the table is * aggregated. * @property {Array.<string>} [groupByElements] If the table is the first in * the aggregated tables chain joined to it, this is the list of elements for * the <code>GROUP BY</code> clause. If present, the <code>aggregated</code> * flag is also <code>true</code> on the table descriptor. * @property {Array.<string>} [orderByElements] Elements for the * <code>ORDER BY</code> clause. * @property {boolean} referred <code>true</code> if the table belongs to a * referred record type. */ /** * Visit every tree node starting from the top descending to the children. * * @param {module:x2node-dbos~TranslationContext} ctx Translation context. * @param {module:x2node-dbos~QueryTreeNode~walkCallback} callback Callback * function called for every node being visited. * @returns {module:x2node-dbos~QueryTreeNode} This node. */ walk(ctx, callback) { this._walkNode(ctx, false, new Array(), callback); return this; } /** * Visit every tree node in reverse order starting from the leaf nodes and * ascending to the top node (therefore always visited last). * * @param {module:x2node-dbos~TranslationContext} ctx Translation context. * @param {module:x2node-dbos~QueryTreeNode~walkCallback} callback Callback * function called for every node being visited. * @returns {module:x2node-dbos~QueryTreeNode} This node. */ walkReverse(ctx, callback) { this._walkNode(ctx, true, new Array(), callback); return this; } /** * Recursively walk the tree from this node down. * * @private * @param {module:x2node-dbos~TranslationContext} ctx Translation context. * @param {boolean} reverse <code>true</code> for reverse walk order. * @param {Array.<module:x2node-dbos~QueryTreeNode~tableDesc>} tableChain * Table chain leading down to this node. * @param {module:x2node-dbos~QueryTreeNode~walkCallback} callback Node * visitor callback. */ _walkNode(ctx, reverse, tableChain, callback) { const parentTableDesc = ( (tableChain.length > 0) && tableChain[tableChain.length - 1]); const tableDesc = { tableName: this._table, tableAlias: this._tableAlias, selectElements: this._select.map(s => ({ valueExpr: ((typeof s.sql) === 'function' ? s.sql(ctx) : s.sql), markup: s.markup })), basicJoinCondition: this._buildBasicJoinCondition(), joinCondition: this._buildFullJoinCondition(ctx), outerJoin: (parentTableDesc && parentTableDesc.outerJoin) || this._virtual, aggregated: (parentTableDesc && parentTableDesc.aggregated) || this._aggregatedBelow, orderByElements: this._order.map( o => ((typeof o.sql) === 'function' ? o.sql(ctx) : o.sql)), referred: (parentTableDesc && parentTableDesc.referred) || ( parentTableDesc && this._propNode.desc.isRef() && (this._propNode.desc.nestedProperties.table === this._table)) }; if (this._aggregatedBelow) { const groupByChain = new Array(); if (this._aggregatedKeySql) groupByChain.push( (typeof this._aggregatedKeySql) === 'function' ? this._aggregatedKeySql(ctx) : this._aggregatedKeySql ); const addSingleRows = node => { node._singleRowChildren.forEach(cn => { addSingleRows(cn); groupByChain.push(cn._tableAlias + '.' + cn._keyColumn); }); }; for (let n = this; n && !n.anchor; n = n[PARENT_NODE]) { addSingleRows(n); groupByChain.push(n._tableAlias + '.' + n._keyColumn); } tableDesc.groupByElements = groupByChain.reverse(); } if (!reverse) callback(this._propNode, tableDesc, tableChain); tableChain.push(tableDesc); this._allChildren.forEach(childNode => { childNode._walkNode(ctx, reverse, tableChain, callback); }); tableChain.pop(); if (reverse) callback(this._propNode, tableDesc, tableChain); } /** * Find the node corresponding to the specified table alias and call the * provided callback function for it. * * @param {module:x2node-dbos~TranslationContext} ctx Translation context. * @param {string} tableAlias Table alias to find. * @param {module:x2node-dbos~QueryTreeNode~walkCallback} callback Callback * function called for the node matching the specified table alias. * @returns {*} The result of the callback function or <code>undefined</code> * if node was not found and the callback was not called. */ forTableAlias(ctx, tableAlias, callback) { return this._findNodeForTableAlias( ctx, tableAlias, new Array(), callback); } /** * Recursive implementation of the find node by table alias method. * * @private * @param {module:x2node-dbos~TranslationContext} ctx Translation context. * @param {string} tableAlias Table alias to find. * @param {Array.<module:x2node-dbos~QueryTreeNode~TableDesc>} tableChain * Current table chain. * @param {module:x2node-dbos~QueryTreeNode~walkCallback} callback The * callback. * @returns {*} The result of the callback function or * <code>undefined</code>. */ _findNodeForTableAlias(ctx, tableAlias, tableChain, callback) { // check if match if (this._tableAlias === tableAlias) return callback(this._propNode, { tableName: this._table, tableAlias: this._tableAlias, joinCondition: this._buildFullJoinCondition(ctx) }, tableChain); // search children for (let childNode of this._allChildren) { if (tableAlias.startsWith(childNode._tableAlias)) { tableChain.push({ tableName: this._table, tableAlias: this._tableAlias, joinCondition: this._buildFullJoinCondition(ctx) }); return childNode._findNodeForTableAlias( ctx, tableAlias, tableChain, callback); } } // return undefined } /** * Build Boolean SQL expression that can be used to join the table * represented by this node to its parent table. The condition does not take * into account any additional join conditions associated with the node and * only uses the key columns. * * @private * @returns {string} Boolean SQL expression for the join condition. */ _buildBasicJoinCondition() { if (!this[PARENT_NODE]) return undefined; return this._tableAlias + '.' + this._joinByColumn + ' = ' + this[PARENT_NODE]._tableAlias + '.' + this._joinToColumn; } /** * Build Boolean SQL expression that is the complete join condition for the * node to its parent. The condition includes the link to the parent table as * well as the node's scoped join condition, if any. * * @private * @param {module:x2node-dbos~TranslationContext} ctx Translation context. * @returns {string} Boolean SQL expression for the join condition. */ _buildFullJoinCondition(ctx) { if (!this[PARENT_NODE]) return undefined; let joinCondition = this._buildBasicJoinCondition(); if (this._joinCondition) { const joinConditionSql = this._joinCondition.translate(ctx); joinCondition += ' AND ' + ( this._joinCondition.needsParen('AND') ? '(' + joinConditionSql + ')' : joinConditionSql); } return joinCondition; } /** * Find collection attachment node to attach an EXISTS condition subquery. * * @param {string} propPath Collection property path. * @returns {module:x2node-dbos~QueryTreeNode} The attachment node. */ findAttachmentNode(propPath) { if ((this._propNode.path.length > 0) && !propPath.startsWith(this._propNode.path + '.')) return null; let res = this; for (let childNode of this._allChildren) { const node = childNode.findAttachmentNode(propPath); if (node) { res = node; break; } } if (propPath.indexOf( (res._propNode.path.length > 0 ? res._propNode.path.length + 1 : 0), '.') >= 0) throw new Error( 'Internal X2 error: cannot find collection attachment node.'); return res; } /** * All child nodes with the expanding node, if any, at the end of the list. * * @private * @member {Array.<module:x2node-dbos~QueryTreeNode>} * @readonly */ get _allChildren() { return ( this._expandingChildren.length > 0 ? this._singleRowChildren.concat(this._expandingChildren) : this._singleRowChildren ); } /** * Name of the table represented by the node. * * @member {string} * @readonly */ get table() { return this._table; } /** * Alias of the table represented by the node. * * @member {string} * @readonly */ get tableAlias() { return this._tableAlias; } /** * Name of the key (such as a primary key) column in the table represented by * the node. Generally speaking, the column can be used to join child tables * to. For a main record type table, this is the record id column. * * @member {string} * @readonly */ get keyColumn() { return this._keyColumn; } /** * Additional condition used to join the table associated with the node to * its parent, if any. * * @member {module:x2node-dbos~RecordsFilter} * @readonly */ get joinCondition() { return this._joinCon