UNPKG

x2node-dbos

Version:
208 lines (182 loc) 5.67 kB
'use strict'; /** * A fully assembled SQL <code>SELECT</code> query. * * @protected * @memberof module:x2node-dbos * @inner */ class SelectQuery { /** * Assemble new query. * * @private * @param {module:x2node-dbos~QueryTreeNode} queryTree Query tree. * @param {module:x2node-dbos~TranslationContext} ctx Translation context. * @param {?module:x2node-dbos~RecordsFilter} [filter] Filter to generate * the <code>WHERE</code> clause. * @param {?module:x2node-dbos~RecordsOrder} [order] Order to generate the * <code>ORDER BY</code> clause. */ constructor(queryTree, ctx, filter, order) { // set up initial query clauses this._select = new Array(); this._from = null; this._where = (filter ? filter.translate(ctx) : null); this._groupBy = null; this._orderBy = new Array(); if (order) for (let orderElement of order.elements) this._orderBy.push(orderElement.translate(ctx)); // initial has aggregates and has joins flags this._hasAggregates = false; this._hasJoins = (filter ? filter.hasCollectionTests() : false); // proper and referred tables collections this._properTables = new Array(); this._referredTables = new Array(); // process the query tree queryTree.walk(ctx, (propNode, tableDesc, tableChain) => { // mark query as having aggregates if aggregated if (tableDesc.aggregated) this._hasAggregates = true; // add SELECT clause elements tableDesc.selectElements.forEach(s => { this._select.push({ valueExpr: s.valueExpr, label: ctx.dbDriver.safeLabel(s.markup) }); }); // add node to the FROM chain const isTopNode = (tableChain.length === 0); if (isTopNode) { this._from = tableDesc.tableName + ' AS ' + tableDesc.tableAlias; } else { this._hasJoins = true; this._from += (tableDesc.outerJoin ? ' LEFT OUTER' : ' INNER') + ' JOIN ' + tableDesc.tableName + ' AS ' + tableDesc.tableAlias + ' ON ' + tableDesc.joinCondition; } // add node's table to the appropriate tables group for locking const tableInfo = { tableName: tableDesc.tableName, tableAlias: tableDesc.tableAlias, }; if (tableDesc.referred) this._referredTables.push(tableInfo); else this._properTables.push(tableInfo); // add groupping let topAggregate = false; if (tableDesc.groupByElements) { topAggregate = true; this._groupBy = ( this._orderBy[0] === 'q.ord' ? [ 'q.ord' ].concat(tableDesc.groupByElements) : tableDesc.groupByElements ); } // add order if (!tableDesc.aggregated || topAggregate) tableDesc.orderByElements.forEach(o => { this._orderBy.push(o); }); }); // weed out repeats in the order const seen = new Set(); this._orderBy = this._orderBy.filter(o => { const v = o.match(/^(.+?)(?:\s+(?:asc|desc))?$/i)[1]; return (seen.has(v) ? false : (seen.add(v), true)); }); } /** * Get tables used by the query in two groups appropriate for the specified * locking. * * @param {?string} lockType Lock type: "exclusive" or "shared". * @param {Array.<Object>} exclusiveLockTables Array, to which to add tables * for exclusive locking. * @param {Array.<Object>} sharedLockTables Array, to which to add tables for * shared locking. */ getTablesForLock(lockType, exclusiveLockTables, sharedLockTables) { let merged; switch (lockType) { case 'exclusive': this._properTables.forEach(t => { exclusiveLockTables.push(t); }); this._referredTables.forEach(t => { sharedLockTables.push(t); }); break; case 'shared': merged = new Map(); this._properTables.forEach(t => { merged.set(t.tableAlias, t); }); this._referredTables.forEach(t => { merged.set(t.tableAlias, t); }); for (let t of merged.values()) sharedLockTables.push(t); } } /** * Get SQL value expression for the top record id returned by the query. * * @returns {string} SQL value expression. */ getIdValueExpr() { return this._select[0].valueExpr; } /** * Tell if the query has any aggregates in it. * * @returns {boolean} <code>true</code> if has aggregates. */ hasAggregates() { return this._hasAggregates; } /** * Tell if the query has joins (or subqueries). * * @returns {boolean} <code>true</code> if has joins, <code>false</code> if * single table. */ hasJoins() { return this._hasJoins; } /** * Get the query SQL. * * @param {boolean} [stumpOnly] If <code>true</code>, the select list of the * returned query is replaced with "{*}" (for further substitution). * @returns {string} The query SQL. */ toSql(stumpOnly) { return 'SELECT ' + ( stumpOnly ? '{*}' : this._select.map( s => `${s.valueExpr} AS ${s.label}`).join(', ') ) + ' FROM ' + this._from + ( this._where ? ' WHERE ' + this._where : '' ) + ( this._groupBy && (this._groupBy.length > 0) ? ' GROUP BY ' + this._groupBy.join(', ') : '' ) + ( this._orderBy.length > 0 ? ' ORDER BY ' + this._orderBy.join(', ') : '' ); } } /** * Assemble <code>SELECT</code> query. * * @protected * @param {module:x2node-dbos~QueryTreeNode} queryTree Query tree. * @param {module:x2node-dbos~TranslationContext} ctx Translation context. * @param {?module:x2node-dbos~RecordsFilter} [filter] Filter to generate the * <code>WHERE</code> clause. * @param {?module:x2node-dbos~RecordsOrder} [order] Order to generate the * <code>ORDER BY</code> clause. * @returns {module:x2node-dbos~SelectQuery} The query. */ exports.assembleSelect = function(queryTree, ctx, filter, order) { return new SelectQuery(queryTree, ctx, filter, order); };