UNPKG

x2node-dbos

Version:
529 lines (443 loc) 16.2 kB
'use strict'; const common = require('x2node-common'); const rsparser = require('x2node-rsparser'); const AbstractDBO = require('./abstract-dbo.js'); const DBOExecutionContext = require('./dbo-execution-context.js'); const ValueExpressionContext = require('./value-expression-context.js'); const propsTreeBuilder = require('./props-tree-builder.js'); const filterBuilder = require('./filter-builder.js'); const orderBuilder = require('./order-builder.js'); const rangeBuilder = require('./range-builder.js'); const queryTreeBuilder = require('./query-tree-builder.js'); // TODO: implement the locks on sub-queries ///////////////////////////////////////////////////////////////////////////////// // COMMANDS ///////////////////////////////////////////////////////////////////////////////// /** * Abstract base command for executing a <code>SELECT</code> query and parsing * its result. * * @private * @memberof module:x2node-dbos * @inner * @implements module:x2node-dbos.DBOCommand * @abstract */ class AbstractFetchCommand { constructor(recordTypes, recordTypeName, query) { this._recordTypes = recordTypes; this._recordTypeName = recordTypeName; this._query = query; } // add command execution to the chain queueUp(promiseChain, ctx) { const parser = rsparser.getResultSetParser( this._recordTypes, this._recordTypeName); const query = this._query; return promiseChain.then(() => new Promise((resolve, reject) => { let sql; try { sql = ctx.replaceParams(query); ctx.log(`executing SQL: ${sql}`); ctx.dbDriver.executeQuery( ctx.connection, sql, { onHeader(fieldNames) { parser.init(fieldNames); }, onRow(row) { parser.feedRow(row); }, onSuccess() { resolve(); }, onError(err) { common.error(`error executing SQL [${sql}]`, err); reject(err); } } ); } catch (err) { common.error(`error executing SQL [${sql || query}]`, err); reject(err); } })).then(() => { this.onComplete(ctx, parser); }); } } /** * Command for executing super-properties fetch. * * @private * @memberof module:x2node-dbos * @inner * @extends module:x2node-dbos~AbstractFetchCommand */ class ExecuteSuperPropertiesFetchCommand extends AbstractFetchCommand { constructor(recordTypes, superTypeName, query) { super(recordTypes, superTypeName, query); } onComplete(ctx, parser) { ctx.addSuperProps(parser); } } /** * Command for executing main records fetch. * * @private * @memberof module:x2node-dbos * @inner * @extends module:x2node-dbos~AbstractFetchCommand */ class ExecuteMainRecordsFetchCommand extends AbstractFetchCommand { constructor(recordTypes, recordTypeName, query) { super(recordTypes, recordTypeName, query); } onComplete(ctx, parser) { ctx.addRecords(parser); } } ///////////////////////////////////////////////////////////////////////////////// // EXECUTION CONTEXT ///////////////////////////////////////////////////////////////////////////////// /** * Operation execution context. * * @private * @memberof module:x2node-dbos * @inner * @extends module:x2node-dbos~DBOExecutionContext */ class FetchDBOExecutionContext extends DBOExecutionContext { constructor(dbo, txOrCon, actor, filterParams) { super(dbo, txOrCon, actor, filterParams); this._superPropsParser = null; this._recordsParser = null; } addSuperProps(parser) { if (this._superPropsParser) this._superPropsParser.merge(parser); else this._superPropsParser = parser; } addRecords(parser) { if (this._recordsParser) this._recordsParser.merge(parser); else this._recordsParser = parser; } getResult() { const res = new Object(); if (this._superPropsParser) { const superRec = this._superPropsParser.records[0]; for (let superPropName in superRec) res[superPropName] = superRec[superPropName]; } else { res.recordTypeName = this._dbo._recordTypeName; } if (this._recordsParser) { res.records = this._recordsParser.records; const refRecs = this._recordsParser.referredRecords; if (Object.keys(refRecs).length > 0) res.referredRecords = refRecs; } return res; } } ///////////////////////////////////////////////////////////////////////////////// // THE DBO ///////////////////////////////////////////////////////////////////////////////// /** * Order specification for the anchor table. * * @private * @constant {module:x2node-dbos~RecordsOrder} */ const ANCHOR_ORDER = { elements: [{ translate() { return 'q.ord'; } }] }; /** * Fetch database operation implementation (SQL <code>SELECT</code> query). * * @memberof module:x2node-dbos * @inner * @extends module:x2node-dbos~AbstractDBO */ class FetchDBO extends AbstractDBO { /** * <strong>Note:</strong> The constructor is not accessible from the client * code. Instances are created using * [DBOFactory]{@link module:x2node-dbos~DBOFactory}. * * @protected * @param {module:x2node-dbos.DBDriver} dbDriver The database driver. * @param {module:x2node-records~RecordTypesLibrary} recordTypes Record types * library. * @param {string} recordTypeName Fetched record type name. * @param {Iterable.<string>} [selectedPropPatterns] Selected record property * patterns, or nothing if records should not be fetched. * @param {Array.<string>} [selectedSuperProps] Selected super-property * names, or nothing if no super-properties need to be fetched. * @param {Array.<Array>} [filterSpec] Optional specification of the filter * to apply to the selected records. * @param {Array.<string>} [orderSpec] Optional order specification to apply * to the selected records. * @param {Array.<number>} [rangeSpec] Optional range specification to apply * to the selected records. * @param {string} [lockType] Lock type. Either "shared" or "exclusive". * @throws {module:x2node-common.X2UsageError} If the record types library is * not suitable for the specified query. * @throws {module:x2node-common.X2SyntaxError} If the provided query * specification is invalid. */ constructor( dbDriver, recordTypes, recordTypeName, selectedPropPatterns, selectedSuperProps, filterSpec, orderSpecs, rangeSpec, lockType) { super(dbDriver, recordTypes); // get the basics const recordTypeDesc = recordTypes.getRecordTypeDesc(recordTypeName); const superTypeName = recordTypeDesc.superRecordTypeName; const recordsPropDesc = recordTypes.getRecordTypeDesc( superTypeName).getPropertyDesc('records'); // create base value expressions context const baseValueExprCtx = new ValueExpressionContext( '', [ recordTypeDesc ]); // involved record types and complexity flag this._involvedRecordTypeNames = new Set(); this._complexity = 0; // build top records filter and the tree of properties used in it const filter = (filterSpec && filterBuilder.buildFilter( recordTypes, baseValueExprCtx, [ ':and', filterSpec ])); const filterPropsTree = ( filter && propsTreeBuilder.buildPropsTreeBranches( recordTypes, recordsPropDesc, 'where', baseValueExprCtx, '', filter.usedPropertyPaths, { noWildcards: true, noAggregates: true, ignoreScopedOrders: true, noScopedFilters: true })[0]); if (filterPropsTree) filterPropsTree.addInvolvedRecordTypesTo( this._involvedRecordTypeNames); // build top records order and the tree of properties used in it const order = (orderSpecs && orderBuilder.buildOrder( baseValueExprCtx, orderSpecs)); const orderPropsTree = ( order && propsTreeBuilder.buildPropsTreeBranches( recordTypes, recordsPropDesc, 'orderBy', baseValueExprCtx, '', order.usedPropertyPaths, { noWildcards: true, noAggregates: true, ignoreScopedOrders: true, noScopedFilters: true })[0]); if (orderPropsTree) orderPropsTree.addInvolvedRecordTypesTo( this._involvedRecordTypeNames); // build the range object const range = (rangeSpec && rangeBuilder.buildRange(rangeSpec)); // the operation commands sequence this._commands = new Array(); // build records queries if (selectedPropPatterns) { // helper functions const combineWithFilter = propsTree => ( filter ? propsTree.combine(filterPropsTree) : propsTree); const combineWithOrder = propsTree => ( order ? propsTree.combine(orderPropsTree) : propsTree); // build and debranch the selected properties tree const selectPropsBranches = propsTreeBuilder.buildPropsTreeBranches( recordTypes, recordsPropDesc, 'select', baseValueExprCtx, null, selectedPropPatterns); for (let branch of selectPropsBranches) branch.addInvolvedRecordTypesTo(this._involvedRecordTypeNames); // check if multiple branches if (selectPropsBranches.length > 1) { // complex this._complexity = 2; // add filter and order properties to the tree const idsPropsTree = combineWithOrder( combineWithFilter(selectPropsBranches[0])); // build ids query tree const idsQueryTree = queryTreeBuilder.forIdsOnlyQuery( dbDriver, recordTypes, idsPropsTree); // add load anchor table command const loadAnchorTable = this._createLoadAnchorTableCommand( idsQueryTree, filter, order, range, (lockType || 'shared')); this._commands.push(loadAnchorTable); // build branch queries selectPropsBranches.forEach(selectPropsBranch => { // build anchored query tree const queryTree = queryTreeBuilder.forAnchoredQuery( dbDriver, recordTypes, 'select', true, selectPropsBranch, loadAnchorTable.anchorTableName); // assemble the SQL and add the fetch command this._commands.push(new ExecuteMainRecordsFetchCommand( recordTypes, recordTypeName, this._assembleSelect(queryTree, null, ANCHOR_ORDER) .toSql() )); }); } else { // single branch // add filter and order properties to the tree const selectPropsTree = combineWithOrder( combineWithFilter(selectPropsBranches[0])); // check if ranged with expanding children if (range && selectPropsTree.hasExpandingChild()) { // complex this._complexity = 2; // build ids query tree const idsQueryTree = queryTreeBuilder.forIdsOnlyQuery( dbDriver, recordTypes, selectPropsTree); // add load anchor table command const loadAnchorTable = this._createLoadAnchorTableCommand( idsQueryTree, filter, order, range, (lockType || 'shared')); this._commands.push(loadAnchorTable); // build anchored query tree const queryTree = queryTreeBuilder.forAnchoredQuery( dbDriver, recordTypes, 'select', true, selectPropsTree, loadAnchorTable.anchorTableName); // assemble the SQL and add the fetch command this._commands.push(new ExecuteMainRecordsFetchCommand( recordTypes, recordTypeName, this._assembleSelect(queryTree, null, ANCHOR_ORDER) .toSql() )); } else { // single branch, no expansion // build query tree const queryTree = queryTreeBuilder.forDirectQuery( dbDriver, recordTypes, 'select', true, selectPropsTree); // assemble the SQL const query = this._assembleSelect(queryTree, filter, order); let querySql = query.toSql(); // consider it complex if it has joins this._complexity = (query.hasJoins() ? 1 : 0); // add range if any if (range) querySql = dbDriver.makeRangedSelect( querySql, range.offset, range.limit); // add locks if any if (lockType) { const exclusiveLockTables = new Array(); const sharedLockTables = new Array(); query.getTablesForLock( lockType, exclusiveLockTables, sharedLockTables); if (query.hasAggregates() && !dbDriver.supportsRowLocksWithAggregates()) { // TODO: can't lock if not supported by DB } else { querySql = dbDriver.makeSelectWithLocks( querySql, exclusiveLockTables, sharedLockTables); } } // add the fetch command this._commands.push(new ExecuteMainRecordsFetchCommand( recordTypes, recordTypeName, querySql)); } } } // build super properties queries if (selectedSuperProps && selectedSuperProps.length > 0) { // complex this._complexity += 2; // build and debranch the selected super-properties tree const superPropsBranches = propsTreeBuilder.buildSuperPropsTreeBranches( recordTypes, recordTypeDesc, selectedSuperProps); for (let branch of superPropsBranches) branch.addInvolvedRecordTypesTo(this._involvedRecordTypeNames); // create filter for the super queries const superFilter = filter && filter.rebase('records'); const superFilterPropsTree = ( superFilter && propsTreeBuilder.buildPropsTreeBranches( recordTypes, recordsPropDesc, 'where', superPropsBranches[0].getValueExpressionContext(), '', superFilter.usedPropertyPaths, { noWildcards: true, noAggregates: true, ignoreScopedOrders: true, noScopedFilters: true })[0]); // build query for each branch superPropsBranches.forEach(superPropsBranch => { // build query tree const queryTree = queryTreeBuilder.forSuperPropsQuery( dbDriver, recordTypes, ( superFilterPropsTree ? superPropsBranch.combine(superFilterPropsTree) : superPropsBranch )); // create filter let combinedFilter; if (queryTree.joinCondition && superFilter) combinedFilter = queryTree.joinCondition.conjoin( superFilter); else if (queryTree.joinCondition) combinedFilter = queryTree.joinCondition; else if (superFilter) combinedFilter = superFilter; // generate the SQL and add the fetch command this._commands.push(new ExecuteSuperPropertiesFetchCommand( recordTypes, superTypeName, this._assembleSelect(queryTree, combinedFilter, null).toSql() )); }); } } /** * Names of all record types involved in the DBO. * * @member {Set.<string>} * @readonly */ get involvedRecordTypeNames() { return this._involvedRecordTypeNames; } /** * Number that characterizes the DBO complexity. A complex DBO involves * multiple database queries (2 or more), a single query with joins and/or * subqueries (1 or more), super-aggregates queries (2 or more). * * @member {number} * @readonly */ get complexity() { return this._complexity; } /** * Fetch DBO execution result object. In addition to the basic result * properties will include a property for each requested super-aggregate. * * @typedef {Object} module:x2node-dbos~FetchDBO~Result * @property {string} recordTypeName Fetched record type name. * @property {Array.<Object>} records Fetched records, or empty array if none * matched. * @property {Object.<string,Object>} [referredRecords] Fetched referred * records by reference. */ /** * Execute the operation. * * @param {(module:x2node-dbos~Transaction|*)} txOrCon The active database * transaction, or database connection object compatible with the database * driver to have the method automatically organize the transaction around * the operation execution. * @param {?module:x2node-common.Actor} actor Actor executing the DBO. * @param {Object.<string,*>} [filterParams] Filter parameters. The keys are * parameter names, the values are parameter values. Note, that no value type * conversion is performed: strings are used as SQL strings, numbers as SQL * numbers, etc. Arrays are expanded into comma-separated lists of element * values. Functions are called without arguments and the results are used as * values. Otherwise, values can be strings, numbers, Booleans and nulls. * @returns {Promise.<module:x2node-dbos~FetchDBO~Result>} The fetch result * promise. * @throws {module:x2node-common.X2UsageError} If provided filter * parameters object is invalid (missing parameter, <code>NaN</code> value or * value of unsupported type). */ execute(txOrCon, actor, filterParams) { return this._executeCommands(new FetchDBOExecutionContext( this, txOrCon, actor, filterParams)); } } // export the class module.exports = FetchDBO;