From ec670ebcfc9f8766e1d6a98c3c57fd14e5fc05bd Mon Sep 17 00:00:00 2001 From: Michael Dwyer Date: Thu, 2 Aug 2012 16:29:53 -0500 Subject: [PATCH 1/6] The function DBSelect.where now has an optional third parameter that specifies whether to use OR instead of AND for the current clause. Added the function DBSelect.orWhere to give it parity with ZendDB. Modified the function DBSelect.assemble to take advantage of this feature. --- lib/dbSelect.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/dbSelect.js b/lib/dbSelect.js index 9337bc5..5ccb8b9 100644 --- a/lib/dbSelect.js +++ b/lib/dbSelect.js @@ -88,9 +88,12 @@ DBSelect.prototype.from = function( tableName, fieldsArray ) * @param {String|Number|Array|null} value * @returns {DBSelect} */ -DBSelect.prototype.where = function( whereStr, value ) +DBSelect.prototype.where = function( whereStr, value, orClause ) { - + if (typeof orClause === 'undefined') { + orClause = false; + } + if( ! whereStr ) throw new Error('WHERE String is mandatory !'); @@ -103,12 +106,20 @@ DBSelect.prototype.where = function( whereStr, value ) whereStr = whereStr.replace( /\?/g, value ); } - this._where.push( whereStr ); + this._where.push( { orClause: orClause, string: whereStr } ); return this; }; +/** + * Adds a WHERE clause with an OR instead of AND. + * + * @returns {DBSelect} + */ +DBSelect.prototype.orWhere = function(whereStr, value) { + return this.where(whereStr, value, true); +}; /** * Adds a LIMIT clause. @@ -267,7 +278,12 @@ DBSelect.prototype.assemble = DBSelect.prototype.toString = function() if( this._where.length > 0 ) { this._where = _.unique( this._where ); - sql += ( (completeQuery) ? ' WHERE ' : '' ) + this._where.join(' AND '); + sql += ( (completeQuery) ? ' WHERE ' : '' ) + this._where[0].string; + + for (var i = 1; i < this._where.length; i++) { + sql += ' ' + (this._where[i].orClause ? 'OR' : 'AND') + ' ' + + this._where[i].string; + } } // "GROUP BY" clause : this._group From d7cbcfdd38d6f868fd0f7a2da400158838d32dcb Mon Sep 17 00:00:00 2001 From: Michael Dwyer Date: Fri, 3 Aug 2012 09:43:38 -0500 Subject: [PATCH 2/6] Added unit test for DBSelect.orWhere() functionality. --- test/db-select.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/db-select.js b/test/db-select.js index edfcf89..4847374 100644 --- a/test/db-select.js +++ b/test/db-select.js @@ -191,6 +191,26 @@ var adapterTestSuite = function( adapterName, callback ) } }, + + 'an advanced WHERE clause, with disordered from(), where() and orWhere() calls': { + topic: function() + { + return dbWrapper.getSelect() + .where('enabled=1') + .where( 'id=?', 10 ) + .from('user') + .where( 'first_name=?', 'Dr.') + .where( 'last_name LIKE ?', '%Benton%' ) + .orWhere( 'nickname=?', '"`\'éàèç' ); + }, + + 'assembled Select is OK': function( select ) + { + var user = dbWrapper._adapter.escapeTable('user'); + assert.equal( select.assemble(), 'SELECT '+user+'.* FROM '+user+' WHERE enabled=1 AND id=10 AND first_name=\'Dr.\' AND last_name LIKE \'%Benton%\' OR nickname='+dbWrapper.escape('"`\'éàèç') ); + } + + }, 'a "WHERE clause" only SELECT ': { topic: function() From 8bab6d2f7c6b40f93e686ea85cc802e3d8c5cd69 Mon Sep 17 00:00:00 2001 From: Michael Dwyer Date: Fri, 3 Aug 2012 13:52:16 -0500 Subject: [PATCH 3/6] Added new functions DBSelect.whereGroup(groups) and DBSelect.whereGroupClose(groups). These function will allow the creation of arbitrary parenthetical expressions in the WHERE clause. Each has a parameter specifying how many groups to open or close. It's smart enough to not close groups that aren't open and to automatically close open groups even if you forget to. As an example, this: dbWrapper.getSelect() .from('user') .whereGroupClose() // Erroneous whereGroupClose to be handled .whereGroup(3) // Multiple group start .where('enabled=1') .where( 'id=?', 10 ) .whereGroupClose() .where( 'first_name=?', 'Dr.') .whereGroup() .where( 'last_name LIKE ?', '%Benton%' ) .orWhere( 'nickname=?', 'dude' ) .whereGroupClose(2); // Automatic closing of leftover groups Generates this: "SELECT user.* FROM user WHERE (((enabled=1 AND id=10) AND first_name='Dr.' AND (last_name LIKE '%Benton%' OR nickname='dude')))" --- lib/dbSelect.js | 117 ++++++++++++++++++++++++++++++++++++++++------ test/db-select.js | 47 +++++++++++++++++++ 2 files changed, 149 insertions(+), 15 deletions(-) diff --git a/lib/dbSelect.js b/lib/dbSelect.js index 5ccb8b9..14ab5ed 100644 --- a/lib/dbSelect.js +++ b/lib/dbSelect.js @@ -88,16 +88,19 @@ DBSelect.prototype.from = function( tableName, fieldsArray ) * @param {String|Number|Array|null} value * @returns {DBSelect} */ -DBSelect.prototype.where = function( whereStr, value, orClause ) +DBSelect.prototype.where = function( whereStr, value, clauseType ) { - if (typeof orClause === 'undefined') { - orClause = false; + var pushCount = 1; + + if (typeof clauseType === 'undefined') { + clauseType = 'AND'; } - if( ! whereStr ) - throw new Error('WHERE String is mandatory !'); + if ((clauseType == 'OR' || clauseType == 'AND') && !whereStr) { + throw new Error('WHERE String is mandatory !'); + } - if( value ) { + if( (clauseType == 'OR' || clauseType == 'AND') && whereStr && value ) { if(_.isArray(value)) value = value.map((function (v) { return this._adapter.escape(v); }).bind(this)).join(', '); else @@ -106,7 +109,13 @@ DBSelect.prototype.where = function( whereStr, value, orClause ) whereStr = whereStr.replace( /\?/g, value ); } - this._where.push( { orClause: orClause, string: whereStr } ); + if ((clauseType == '(' || clauseType == ')') && typeof value == 'number') { + pushCount = value; + } + + for (var i = 0; i < pushCount; i++) { + this._where.push( { clauseType: clauseType, string: whereStr } ); + } return this; @@ -115,10 +124,47 @@ DBSelect.prototype.where = function( whereStr, value, orClause ) /** * Adds a WHERE clause with an OR instead of AND. * + * @param {String} whereStr + * @param {String|Number|Array|null} value * @returns {DBSelect} */ DBSelect.prototype.orWhere = function(whereStr, value) { - return this.where(whereStr, value, true); + return this.where(whereStr, value, 'OR'); +}; + +/** + * Adds a WHERE clause grouping start + * + * @param {Number} groupsToOpen + * @returns {DBSelect} + */ +DBSelect.prototype.whereGroup = function(groupsToOpen) { + return this.whereGroupHandler(groupsToOpen, '('); +}; + +/** + * Adds a WHERE clause grouping end + * + * @param {Number} groupsToClose + * @returns {DBSelect} + */ +DBSelect.prototype.whereGroupClose = function(groupsToClose) { + return this.whereGroupHandler(groupsToClose, ')'); +}; + +/** + * Adds WHERE grouping clauses + * + * @param {Number} groups + * @param {String} groupType + * @returns {DBSelect} + */ +DBSelect.prototype.whereGroupHandler = function(groups, groupType) { + if (typeof groups === 'undefined') { + groups = 1; + } + + return this.where(null, groups, groupType); }; /** @@ -275,14 +321,55 @@ DBSelect.prototype.assemble = DBSelect.prototype.toString = function() } // "WHERE" clause : this._where - if( this._where.length > 0 ) - { - this._where = _.unique( this._where ); - sql += ( (completeQuery) ? ' WHERE ' : '' ) + this._where[0].string; + if (this._where.length > 0) { + this._where = _.unique( this._where ); + sql += ( (completeQuery) ? ' WHERE ' : '' ); + + var firstValue = true; + var openGroups = 0; + var whereClause = null; + var whereString = null; + var clauseType = null; + var startGroups = 0; + + for (var i = 0; i < this._where.length; i++) { + whereClause = this._where[i]; + clauseType = whereClause.clauseType; + whereString = whereClause.string; + + switch (clauseType) { + case '(': + startGroups++; + openGroups++; + break; + + case ')': + if (openGroups > 0) { + sql += ')'; + openGroups--; + } + + break; + + case 'AND': + case 'OR': + if (!firstValue) { + sql += ' ' + (clauseType == 'OR' ? 'OR' : 'AND') + ' '; + } else { + firstValue = false; + } + + for (var j = 0; j < startGroups; j++) { + sql += '('; + } + + startGroups = 0; + sql += whereString; + } + } - for (var i = 1; i < this._where.length; i++) { - sql += ' ' + (this._where[i].orClause ? 'OR' : 'AND') + ' ' - + this._where[i].string; + for (var k = 0; k < openGroups; k++) { + sql += ')'; } } diff --git a/test/db-select.js b/test/db-select.js index 4847374..16b66da 100644 --- a/test/db-select.js +++ b/test/db-select.js @@ -211,6 +211,53 @@ var adapterTestSuite = function( adapterName, callback ) } }, + + 'an advanced WHERE clause, with disordered from(), where() and orWhere() calls and parenthetical grouping': { + topic: function() + { + return dbWrapper.getSelect() + .where('enabled=1') + .where( 'id=?', 10 ) + .from('user') + .where( 'first_name=?', 'Dr.') + .whereGroup() + .where( 'last_name LIKE ?', '%Benton%' ) + .orWhere( 'nickname=?', '"`\'éàèç' ); + }, + + 'assembled Select is OK': function( select ) + { + var user = dbWrapper._adapter.escapeTable('user'); + assert.equal( select.assemble(), 'SELECT '+user+'.* FROM '+user+' WHERE enabled=1 AND id=10 AND first_name=\'Dr.\' AND (last_name LIKE \'%Benton%\' OR nickname='+dbWrapper.escape('"`\'éàèç')+')' ); + } + + }, + + 'an advanced WHERE clause, with more parenthetical grouping': { + topic: function() + { + return dbWrapper.getSelect() + .from('user') + .whereGroupClose() // Erroneous whereGroupClose to be handled + .whereGroup(3) // Multiple group start + .where('enabled=1') + .where( 'id=?', 10 ) + .whereGroupClose() + .where( 'first_name=?', 'Dr.') + .whereGroup() + .where( 'last_name LIKE ?', '%Benton%' ) + .orWhere( 'nickname=?', '"`\'éàèç' ) + .whereGroupClose(2); + // Automatic closing of leftover groups + }, + + 'assembled Select is OK': function( select ) + { + var user = dbWrapper._adapter.escapeTable('user'); + assert.equal( select.assemble(), 'SELECT '+user+'.* FROM '+user+' WHERE (((enabled=1 AND id=10) AND first_name=\'Dr.\' AND (last_name LIKE \'%Benton%\' OR nickname='+dbWrapper.escape('"`\'éàèç')+')))' ); + } + + }, 'a "WHERE clause" only SELECT ': { topic: function() From edd925b5efb69b0b249f5ddbebdab02fef2ebcf6 Mon Sep 17 00:00:00 2001 From: Michael Dwyer Date: Wed, 8 Aug 2012 13:57:20 -0500 Subject: [PATCH 4/6] Added documentation about new where features. Added name to package.json under authors section. --- README.md | 15 +++++++++++---- package.json | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index df6ef48..252a1a8 100644 --- a/README.md +++ b/README.md @@ -125,10 +125,17 @@ All these methods returns exactly the sames results, whatever the chosen databas Furthermore, Node-DBI provides a DBSelect class which allows easy and readable SQL "SELECT" Strings building. At the moment, it provides the following methods : * __from( tableName, fieldsArray )__ : adds a table in the FROM clause, and adds its fields to the SELECT - * __where( whereStr, value )__ : adds a WHERE clause ; if "value" is not null, all the "?" occurences of the "whereStr" will be replaced with the safely escaped value - * __limit( nbResults, startIndex )__ : set the LIMIT clause ; "startIndex" param is optionnal - * __order( fieldName, direction )__ : adds a ORDER BY clause ; if "direction" is not set, it will be set to "ASC" - * __join( tableName, joinStr, fieldsArray, joinType )__ : adds a JOIN clause ; if "joinType" is not set, it will be set to "INNER" + * __where( whereStr, value )__: adds a WHERE clause using AND ; if __value__ is not null, all the "?" occurences in __whereStr__ will be replaced with the safely escaped value + * __orWhere( whereStr, value )__ : just like __where__ but adds a WHERE clause using OR + * __whereGroup( num )__ : opens __num__ parenthetical groupings to WHERE clause (ie adds __num__ open parentheses) ; __num__ defaults to 1 + * __whereGroupClose( num )__ : + * closes __num__ parenthetical groupings of WHERE clause (ie adds __num__ closed parentheses) + * __num__ defaults to 1 + * will not close groups that do not exist + * open groups will be closed automatically + * __limit( nbResults, startIndex )__ : set the LIMIT clause ; __startIndex__ param is optionnal + * __order( fieldName, direction )__ : adds a ORDER BY clause ; if __direction__ is not set, it will be set to "ASC" + * __join( tableName, joinStr, fieldsArray, joinType )__ : adds a JOIN clause ; if __joinType__ is not set, it will be set to "INNER" * __distinct()__ : adds a DISTINCT() to the query * __groupyBy( fieldName )__ : adds a GROUPY BY clause * __assemble()__ : converts ou DBSelect object to an SQL SELECT string. diff --git a/package.json b/package.json index 0f0f1e1..11057d1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "authors": [ "Dr. Benton (http://github.com/DrBenton)", "David Schoen ", - "Fabian Bornhofen (http://fabianbornhofen.blogspot.com)" + "Fabian Bornhofen (http://fabianbornhofen.blogspot.com)", + "Michael Dwyer (http://github.com/kalifg)" ], "main": "dbWrapper.js", "directories": { From 0f40601d84a422177094a8dafa6b18342751ee74 Mon Sep 17 00:00:00 2001 From: Michael Dwyer Date: Thu, 9 Aug 2012 15:05:12 -0500 Subject: [PATCH 5/6] Version bump on account of new functionality. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 11057d1..83e8963 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-dbi", "description": "A Database abstraction layer for Node.js, bundled with several DB engines adapters", - "version": "0.5.0", + "version": "0.6.0", "homepage": "https://github.com/DrBenton/Node-DBI", "repository": { "type": "git", From 3b36f87edccd5c475ebd7f519d4144a502a3a219 Mon Sep 17 00:00:00 2001 From: Michael Dwyer Date: Thu, 9 Aug 2012 15:45:16 -0500 Subject: [PATCH 6/6] Fixed bug in DBSelect.where(). --- lib/dbSelect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dbSelect.js b/lib/dbSelect.js index 14ab5ed..20823cd 100644 --- a/lib/dbSelect.js +++ b/lib/dbSelect.js @@ -92,7 +92,7 @@ DBSelect.prototype.where = function( whereStr, value, clauseType ) { var pushCount = 1; - if (typeof clauseType === 'undefined') { + if (!clauseType) { clauseType = 'AND'; }