/* Copyright (c) 2012, 2014, Oracle and/or its affiliates. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ "use strict"; var util = require("util"); var BitMask = require(mynode.common.BitMask); var udebug = unified_debug.getLogger("Query.js"); var userContext = require("./UserContext.js"); var keywords = ['param', 'where', 'field', 'execute']; var QueryParameter; var QueryHandler; var QueryEq, QueryNe, QueryLe, QueryLt, QueryGe, QueryGt, QueryBetween, QueryIn, QueryIsNull, QueryIsNotNull; var QueryNot, QueryAnd, QueryOr; /** QueryDomainType function param */ var param = function(name) { return new QueryParameter(this, name); }; /** QueryDomainType function where */ var where = function(predicate) { var mynode = this.mynode_query_domain_type; mynode.predicate = predicate; mynode.queryHandler = new QueryHandler(mynode.dbTableHandler, predicate); mynode.queryType = mynode.queryHandler.queryType; this.prototype = {}; return this; }; /** QueryDomainType function execute */ var execute = function() { var session = this.mynode_query_domain_type.session; var context = new userContext.UserContext(arguments, 2, 2, session, session.sessionFactory); // delegate to context's execute for execution return context.executeQuery(this); }; var queryDomainTypeFunctions = {}; queryDomainTypeFunctions.where = where; queryDomainTypeFunctions.param = param; queryDomainTypeFunctions.execute = execute; /** * QueryField represents a mapped field in a domain object. QueryField is used to build * QueryPredicates by comparing the field to parameters. * @param queryDomainType * @param field * @return */ var QueryField = function(queryDomainType, field) { if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryField', field.fieldName); // this.class = 'QueryField'; // useful for debugging // this.fieldName = field.fieldName; // useful for debugging this.queryDomainType = queryDomainType; this.field = field; }; QueryField.prototype.eq = function(queryParameter) { return new QueryEq(this, queryParameter); }; QueryField.prototype.le = function(queryParameter) { return new QueryLe(this, queryParameter); }; QueryField.prototype.ge = function(queryParameter) { return new QueryGe(this, queryParameter); }; QueryField.prototype.lt = function(queryParameter) { return new QueryLt(this, queryParameter); }; QueryField.prototype.gt = function(queryParameter) { return new QueryGt(this, queryParameter); }; QueryField.prototype.ne = function(queryParameter) { return new QueryNe(this, queryParameter); }; QueryField.prototype.between = function(queryParameter1, queryParameter2) { return new QueryBetween(this, queryParameter1, queryParameter2); }; // 'in' is a keyword so use alternate syntax QueryField.prototype['in'] = function(queryParameter) { return new QueryIn(this, queryParameter); }; QueryField.prototype.isNull = function() { return new QueryIsNull(this); }; QueryField.prototype.isNotNull = function() { return new QueryIsNotNull(this); }; QueryField.prototype.inspect = function() { return this.field.fieldName; }; /** Query Domain Type represents a domain object that can be used to create and execute queries. * It encapsulates the dbTableHandler (obtained from the domain object or table name), * the session (required to execute the query), and the filter which limits the result. * @param session the user Session * @param dbTableHandler the dbTableHandler * @param domainObject true if the query results are domain objects */ var QueryDomainType = function(session, dbTableHandler, domainObject) { udebug.log('QueryDomainType', dbTableHandler.dbTable.name); // avoid most name conflicts: put all implementation artifacts into the property mynode_query_domain_type this.mynode_query_domain_type = {}; this.field = {}; var mynode = this.mynode_query_domain_type; mynode.session = session; mynode.dbTableHandler = dbTableHandler; mynode.domainObject = domainObject; var queryDomainType = this; // initialize the functions (may be overridden below if a field has the name of a keyword) queryDomainType.where = where; queryDomainType.param = param; queryDomainType.execute = execute; var fieldName, queryField; // add a property for each field in the table mapping mynode.dbTableHandler.fieldNumberToFieldMap.forEach(function(field) { fieldName = field.fieldName; queryField = new QueryField(queryDomainType, field); if (keywords.indexOf(fieldName) === -1) { // field name is not a keyword queryDomainType[fieldName] = queryField; } else { if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryDomainType field', fieldName, 'is a keyword.'); // field name is a keyword // allow e.g. qdt.where.id if (fieldName !== 'field') { // if field is a reserved word but not a function, skip setting the function queryDomainType[fieldName] = queryDomainTypeFunctions[fieldName]; queryDomainType[fieldName].eq = QueryField.prototype.eq; queryDomainType[fieldName].field = queryField.field; } // allow e.g. qdt.field.where queryDomainType.field[fieldName] = queryField; } }); }; QueryDomainType.prototype.inspect = function() { var mynode = this.mynode_query_domain_type; return "[[API Query on table: " + mynode.dbTableHandler.dbTable.name + ", type: " + mynode.queryType + ", predicate: " + util.inspect(mynode.predicate) + "]]\n"; }; QueryDomainType.prototype.not = function(queryPredicate) { return new QueryNot(queryPredicate); }; /** * QueryParameter represents a named parameter for a query. The QueryParameter marker is used * as the comparand for QueryField. * @param queryDomainType * @param name * @return */ QueryParameter = function(queryDomainType, name) { if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryParameter', name); this.queryDomainType = queryDomainType; this.name = name; }; QueryParameter.prototype.inspect = function() { return '?' + this.name; }; /****************************************************************************** * SQL VISITOR *****************************************************************************/ var SQLVisitor = function(rootPredicateNode) { this.rootPredicateNode = rootPredicateNode; rootPredicateNode.sql = {}; rootPredicateNode.sql.formalParameters = []; rootPredicateNode.sql.sqlText = 'initialized'; this.parameterIndex = 0; }; /** Handle nodes QueryEq, QueryNe, QueryLt, QueryLe, QueryGt, QueryGe */ SQLVisitor.prototype.visitQueryComparator = function(node) { // set up the sql text in the node var columnName = node.queryField.field.fieldName; node.sql.sqlText = columnName + node.comparator + '?'; // assign ordered list of parameters to the top node this.rootPredicateNode.sql.formalParameters[this.parameterIndex++] = node.parameter; }; /** Handle nodes QueryAnd, QueryOr */ SQLVisitor.prototype.visitQueryNaryPredicate = function(node) { var i; // all n-ary predicates have at least two node.predicates[0].visit(this); // sets up the sql.sqlText in the node node.sql.sqlText = '(' + node.predicates[0].sql.sqlText + ')'; for (i = 1; i < node.predicates.length; ++i) { node.sql.sqlText += node.operator; node.predicates[i].visit(this); node.sql.sqlText += '(' + node.predicates[i].sql.sqlText + ')'; } }; /** Handle nodes QueryNot */ SQLVisitor.prototype.visitQueryUnaryPredicate = function(node) { node.predicates[0].visit(this); // sets up the sql.sqlText in the node node.sql.sqlText = node.operator + '(' + node.predicates[0].sql.sqlText + ')'; }; /** Handle nodes QueryIsNull, QueryIsNotNull */ SQLVisitor.prototype.visitQueryUnaryOperator = function(node) { var columnName = node.queryField.field.fieldName; node.sql.sqlText = columnName + node.operator; }; /** Handle node QueryBetween */ SQLVisitor.prototype.visitQueryBetweenOperator = function(node) { var columnName = node.queryField.field.fieldName; node.sql.sqlText = columnName + ' BETWEEN ? AND ?'; this.rootPredicateNode.sql.formalParameters[this.parameterIndex++] = node.formalParameters[0]; this.rootPredicateNode.sql.formalParameters[this.parameterIndex++] = node.formalParameters[1]; }; /****************************************************************************** * MARKS COLUMN MASKS IN QUERY NODES *****************************************************************************/ function MaskMarkerVisitor() { } /** Set column number in usedColumnMask */ function markUsed(node) { node.usedColumnMask = new BitMask(); node.equalColumnMask = new BitMask(); node.usedColumnMask.set(node.queryField.field.columnNumber); } /** Handle nodes QueryEq, QueryNe, QueryLt, QueryLe, QueryGt, QueryGe */ MaskMarkerVisitor.prototype.visitQueryComparator = function(node) { markUsed(node); if(node.operationCode === 4) { // QueryEq node.equalColumnMask.set(node.queryField.field.columnNumber); } }; /** Nodes Between, IsNotNull, IsNotNull are all handled by markUsed() */ MaskMarkerVisitor.prototype.visitQueryUnaryOperator = markUsed; MaskMarkerVisitor.prototype.visitQueryBetweenOperator = markUsed; /** Handle QueryNot */ MaskMarkerVisitor.prototype.visitQueryUnaryPredicate = function(node) { node.predicates[0].visit(this); node.equalColumnMask = new BitMask(); // Set to zero node.usedColumnMask = node.predicates[0].usedColumnMask; }; /** Handle nodes QueryAnd, QueryOr */ MaskMarkerVisitor.prototype.visitQueryNaryPredicate = function(node) { var i; node.usedColumnMask = new BitMask(); node.equalColumnMask = new BitMask(); for(i = 0 ; i < node.predicates.length ; i++) { node.predicates[i].visit(this); node.usedColumnMask.orWith(node.predicates[i].usedColumnMask); if(this.operationCode === 1) { // QueryAnd node.equalColumnMask.orWith(node.predicates[i].equalColumnMask); } } }; var theMaskMarkerVisitor = new MaskMarkerVisitor(); // Singleton /****************************************************************************** * TOP LEVEL ABSTRACT QUERY PREDICATE *****************************************************************************/ var AbstractQueryPredicate = function() { this.sql = {}; }; AbstractQueryPredicate.prototype.inspect = function() { var str = this.operator + "("; this.predicates.forEach(function(value,index) { if(index) str += " , "; str += value.inspect(); }); str += ")"; return str; }; AbstractQueryPredicate.prototype.and = function(predicate) { // TODO validate parameter return new QueryAnd(this, predicate); }; AbstractQueryPredicate.prototype.andNot = function(predicate) { // TODO validate parameter return new QueryAnd(this, new QueryNot(predicate)); }; AbstractQueryPredicate.prototype.or = function(predicate) { // TODO validate parameter for OR return new QueryOr(this, predicate); }; AbstractQueryPredicate.prototype.orNot = function(predicate) { // TODO validate parameter return new QueryOr(this, new QueryNot(predicate)); }; AbstractQueryPredicate.prototype.not = function() { // TODO validate parameter return new QueryNot(this); }; AbstractQueryPredicate.prototype.getTopLevelPredicates = function() { return [this]; }; AbstractQueryPredicate.prototype.getSQL = function() { var visitor = new SQLVisitor(this); this.visit(visitor); return this.sql; }; /****************************************************************************** * ABSTRACT QUERY N-ARY PREDICATE * AND and OR *****************************************************************************/ var AbstractQueryNaryPredicate = function() { }; AbstractQueryNaryPredicate.prototype = new AbstractQueryPredicate(); AbstractQueryNaryPredicate.prototype.getTopLevelPredicates = function() { return this.predicates; }; AbstractQueryNaryPredicate.prototype.visit = function(visitor) { if (typeof(visitor.visitQueryNaryPredicate) === 'function') { visitor.visitQueryNaryPredicate(this); } }; /****************************************************************************** * ABSTRACT QUERY UNARY PREDICATE * NOT *****************************************************************************/ var AbstractQueryUnaryPredicate = function() { }; AbstractQueryUnaryPredicate.prototype = new AbstractQueryPredicate(); AbstractQueryUnaryPredicate.prototype.visit = function(visitor) { if (typeof(visitor.visitQueryUnaryPredicate) === 'function') { visitor.visitQueryUnaryPredicate(this); } }; /****************************************************************************** * ABSTRACT QUERY COMPARATOR * eq, ne, gt, lt, ge, le *****************************************************************************/ var AbstractQueryComparator = function() { }; /** AbstractQueryComparator inherits AbstractQueryPredicate */ AbstractQueryComparator.prototype = new AbstractQueryPredicate(); AbstractQueryComparator.prototype.inspect = function() { return this.queryField.field.fieldName + this.comparator + this.parameter.inspect(); }; AbstractQueryComparator.prototype.visit = function(visitor) { if (typeof(visitor.visitQueryComparator) === 'function') { visitor.visitQueryComparator(this); } }; /****************************************************************************** * QUERY EQUAL *****************************************************************************/ QueryEq = function(queryField, parameter) { this.comparator = ' = '; this.operationCode = 4; this.queryField = queryField; this.parameter = parameter; }; QueryEq.prototype = new AbstractQueryComparator(); /****************************************************************************** * QUERY LESS THAN OR EQUAL *****************************************************************************/ QueryLe = function(queryField, parameter) { this.comparator = ' <= '; this.operationCode = 0; this.queryField = queryField; this.parameter = parameter; }; QueryLe.prototype = new AbstractQueryComparator(); /****************************************************************************** * QUERY GREATER THAN OR EQUAL *****************************************************************************/ QueryGe = function(queryField, parameter) { this.comparator = ' >= '; this.operationCode = 2; this.queryField = queryField; this.parameter = parameter; }; QueryGe.prototype = new AbstractQueryComparator(); /****************************************************************************** * QUERY LESS THAN *****************************************************************************/ QueryLt = function(queryField, parameter) { this.comparator = ' < '; this.operationCode = 1; this.queryField = queryField; this.parameter = parameter; }; QueryLt.prototype = new AbstractQueryComparator(); /****************************************************************************** * QUERY GREATER THAN *****************************************************************************/ QueryGt = function(queryField, parameter) { this.comparator = ' > '; this.operationCode = 3; this.queryField = queryField; this.parameter = parameter; }; QueryGt.prototype = new AbstractQueryComparator(); /****************************************************************************** * QUERY BETWEEN *****************************************************************************/ QueryBetween = function(queryField, parameter1, parameter2) { this.comparator = ' BETWEEN '; this.queryField = queryField; this.formalParameters = []; this.formalParameters[0] = parameter1; this.formalParameters[1] = parameter2; this.parameter1 = parameter1; this.parameter2 = parameter2; }; QueryBetween.prototype = new AbstractQueryComparator(); QueryBetween.prototype.inspect = function() { return this.queryField.inspect() + ' BETWEEN ' + this.parameter1.inspect() + ' AND ' + this.parameter2.inspect(); }; QueryBetween.prototype.visit = function(visitor) { if (typeof(visitor.visitQueryBetweenOperator) === 'function') { visitor.visitQueryBetweenOperator(this); } }; /****************************************************************************** * QUERY NOT EQUAL *****************************************************************************/ QueryNe = function(queryField, parameter) { this.comparator = ' != '; this.operationCode = 5; this.queryField = queryField; this.parameter = parameter; }; QueryNe.prototype = new AbstractQueryComparator(); /****************************************************************************** * QUERY IN *****************************************************************************/ QueryIn = function(queryField, parameter) { this.comparator = ' IN '; this.queryField = queryField; this.parameter = parameter; }; QueryIn.prototype = new AbstractQueryComparator(); /****************************************************************************** * ABSTRACT QUERY UNARY OPERATOR *****************************************************************************/ var AbstractQueryUnaryOperator = function() { }; AbstractQueryUnaryOperator.prototype = new AbstractQueryPredicate(); AbstractQueryUnaryOperator.prototype.inspect = function() { return util.format(this); // return this.queryField.inspect() + this.comparator + this.parameter.inspect(); }; AbstractQueryUnaryOperator.prototype.visit = function(visitor) { if (typeof(visitor.visitQueryUnaryOperator) === 'function') { visitor.visitQueryUnaryOperator(this); } }; /****************************************************************************** * QUERY IS NULL *****************************************************************************/ QueryIsNull = function(queryField) { this.operator = ' IS NULL'; this.operationCode = 7; this.queryField = queryField; }; QueryIsNull.prototype = new AbstractQueryUnaryOperator(); /****************************************************************************** * QUERY IS NOT NULL *****************************************************************************/ QueryIsNotNull = function(queryField) { this.operator = ' IS NOT NULL'; this.operationCode = 8; this.queryField = queryField; }; QueryIsNotNull.prototype = new AbstractQueryUnaryOperator(); /****************************************************************************** * QUERY AND *****************************************************************************/ QueryAnd = function(left, right) { this.operator = ' AND '; this.operationCode = 1; this.predicates = [left, right]; if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryAnd', this); }; QueryAnd.prototype = new AbstractQueryNaryPredicate(); QueryAnd.prototype.getTopLevelPredicates = function() { return this.predicates; }; /** Override the "and" function to collect all predicates in one variable. */ QueryAnd.prototype.and = function(predicate) { this.predicates.push(predicate); return this; }; /****************************************************************************** * QUERY OR *****************************************************************************/ QueryOr = function(left, right) { this.operator = ' OR '; this.operationCode = 2; this.predicates = [left, right]; if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryOr', this); }; QueryOr.prototype = new AbstractQueryNaryPredicate(); QueryOr.prototype.getTopLevelPredicates = function() { return []; }; /** Override the "or" function to collect all predicates in one variable. */ QueryOr.prototype.or = function(predicate) { this.predicates.push(predicate); return this; }; /****************************************************************************** * QUERY NOT *****************************************************************************/ QueryNot = function(left) { this.operator = ' NOT '; this.operationCode = 3; this.predicates = [left]; if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryNot', this, 'parameter', left); }; QueryNot.prototype = new AbstractQueryUnaryPredicate(); /****************************************************************************** * CANDIDATE INDEX *****************************************************************************/ // CandidateIndex is almost stateless now. // For future consideration: move mask into DbIndexHandler, then eliminate // CandidateIndex completely. var CandidateIndex = function(dbTableHandler, indexNumber) { this.dbIndexHandler = dbTableHandler.dbIndexHandlers[indexNumber]; if(! this.dbIndexHandler) { console.log("indexNumber", typeof(indexNumber)); console.trace("not an index handler"); throw new Error('Query.CandidateIndex indexNumber is not found'); } this.isOrdered = this.dbIndexHandler.dbIndex.isOrdered; this.isUnique = this.dbIndexHandler.dbIndex.isUnique; if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('CandidateIndex for index', this.dbIndexHandler.dbIndex.name, 'isUnique', this.isUnique, 'isOrdered', this.isOrdered); this.indexColumns = this.dbIndexHandler.dbIndex.columnNumbers; var mask = new BitMask(dbTableHandler.dbTable.columns.length); this.indexColumns.forEach(function(columnNumber) { mask.set(columnNumber); }); this.mask = mask; }; CandidateIndex.prototype.isUsable = function(predicate) { var usable; if (this.isUnique) { usable = predicate.equalColumnMask.and(this.mask).isEqualTo(this.mask); } else if(this.isOrdered) { usable = predicate.usedColumnMask.bitIsSet(this.indexColumns[0]); } return usable; }; // This is used in Primary Key & Unique Key queries. // param predicate: query predicate // param parameterValues: the parameters object passed to query.execute() // It returns an array, in key-column order, of the key values from the // parameter object. CandidateIndex.prototype.getKeys = function(predicate, parameterValues) { function getParameterNameForColumn(node, columnNumber) { var i, name; if(node.equalColumnMask.bitIsSet(columnNumber)) { if(node.queryField && node.queryField.field.columnNumber == columnNumber) { return node.parameter.name; } if(node.predicates) { for(i = 0 ; i < node.predicates.length ; i++) { name = getParameterNameForColumn(node, columnNumber); if(name !== null) return name; } } } return null; } var result = []; var candidateIndex = this; this.indexColumns.forEach(function(columnNumber) { result.push(parameterValues[getParameterNameForColumn(predicate, columnNumber)]); }); if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('CandidateIndex.getKeys parameters:', parameterValues, 'key:', result); return result; }; /** Evaluate candidate indexes. Score 1 point for each consecutive key part used plus 1 more point if the column is in QueryEq. */ CandidateIndex.prototype.score = function(predicate) { var score = 0; var point, i; i = 0; do { point = predicate.usedColumnMask.bitIsSet(this.indexColumns[i]); if(point) { score += 1; if(predicate.usedColumnMask.bitIsSet(this.indexColumns[i])) { score += 1; } } i++; } while(point && i < this.indexColumns.length); if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('score', this.dbIndexHandler.dbIndex.name, 'is', score); return score; }; /****************************************************************************** * QUERY HANDLER *****************************************************************************/ /* QueryHandler constructor * IMMEDIATE * * statically analyze the predicate to decide whether: * all primary key fields are specified ==> use primary key lookup; * all unique key fields are specified ==> use unique key lookup; * some (leading) index fields are specified ==> use index scan; * none of the above ==> use table scan * Get the query handler for a given query predicate. * */ var QueryHandler = function(dbTableHandler, predicate) { if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('QueryHandler', util.inspect(predicate)); this.dbTableHandler = dbTableHandler; this.predicate = predicate; var indexes = dbTableHandler.dbTable.indexes; // Mark the usedColumnMask and equalColumnMask in each query node predicate.visit(theMaskMarkerVisitor); // create a CandidateIndex object for each index // if the primary index is usable, choose it var primaryCandidateIndex = new CandidateIndex(dbTableHandler, 0); if(primaryCandidateIndex.isUsable(predicate)) { // we're done! this.candidateIndex = primaryCandidateIndex; this.queryType = 0; // primary key lookup return; } // otherwise, look for a usable unique index var uniqueCandidateIndex, orderedCandidateIndexes = []; var i, index; for (i = 1; i < indexes.length; ++i) { index = indexes[i]; if (index.isUnique) { // create a candidate index for unique index uniqueCandidateIndex = new CandidateIndex(dbTableHandler, i); if (uniqueCandidateIndex.isUsable(predicate)) { this.candidateIndex = uniqueCandidateIndex; this.queryType = 1; // unique key lookup // we're done! return; } } else if (index.isOrdered) { // create an array of candidate indexes for ordered indexes to be evaluated later orderedCandidateIndexes.push(new CandidateIndex(dbTableHandler, i)); } else { throw new Error('FatalInternalException: index is not unique or ordered... so what is it?'); } } // otherwise, look for the best ordered index (largest number of usable query terms) // choose the index with the biggest score var topScore = 0, candidateScore = 0; var bestCandidateIndex = null; orderedCandidateIndexes.forEach(function(candidateIndex) { candidateScore = candidateIndex.score(predicate); if (candidateScore > topScore) { topScore = candidateScore; bestCandidateIndex = candidateIndex; } }); udebug.log("Best score is", topScore); if (topScore > 0) { this.candidateIndex = bestCandidateIndex; this.dbIndexHandler = bestCandidateIndex.dbIndexHandler; this.queryType = 2; // index scan } else { this.queryType = 3; // table scan } }; /** Get key values from candidate indexes and parameters */ QueryHandler.prototype.getKeys = function(parameterValues) { return this.candidateIndex.getKeys(this.predicate, parameterValues); }; exports.QueryDomainType = QueryDomainType; exports.QueryHandler = QueryHandler;