From 06ea44ac8f6e5fecc7df9f4c7857e22e5ae4df45 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Mon, 27 Oct 2014 10:35:16 -0400 Subject: [PATCH] Fix joining methods --- lib/query-builder.js | 72 ++++++++++++++++++------------ lib/query-parser.js | 87 +++++++++++++++++++++++++++++++++++++ tests/query-builder-base.js | 84 +++++++++++------------------------ 3 files changed, 157 insertions(+), 86 deletions(-) create mode 100644 lib/query-parser.js diff --git a/lib/query-builder.js b/lib/query-builder.js index 7506552..288be97 100755 --- a/lib/query-builder.js +++ b/lib/query-builder.js @@ -23,6 +23,8 @@ var QueryBuilder = function(driver, adapter) { // That 'new' keyword is annoying if ( ! (this instanceof QueryBuilder)) return new QueryBuilder(driver, adapter); + var parser = require('./query-parser')(driver); + /** * "Private" methods * @@ -70,9 +72,8 @@ var QueryBuilder = function(driver, adapter) { switch(type) { case "insert": - var paramCount = state.setArrayKeys.length; - var params = []; - params.fill('?', 0, paramCount); + var params = new Array(state.setArrayKeys.length); + params.fill('?'); sql = "INSERT INTO " + table + " ("; sql += state.setArrayKeys.join(','); @@ -146,34 +147,38 @@ var QueryBuilder = function(driver, adapter) { * * @private */ - mixedSet: function(/* varName, valType, key, [val] */) { - var args = getArgs('varName:string, valType:string, key:string|object, [val]:string|number|boolean', arguments); + mixedSet: function(/* $varName, $valType, $key, [$val] */) { + var args = getArgs('$varName:string, $valType:string, $key:object|string|number, [$val]', arguments); var obj = {}; - if (helpers.isScalar(args.key) && !helpers.isUndefined(args.val) && !helpers.isNull(args.val)) + if (helpers.isScalar(args.$key) && !helpers.isUndefined(args.$val) && !helpers.isNull(args.$val)) { - obj[args.key] = args.val; + obj[args.$key] = args.$val; + } + else if ( ! helpers.isScalar(args.$key)) + { + obj = args.$key; } else { - obj = args.key; + throw new Error("Invalid arguments passed"); } Object.keys(obj).forEach(function(k) { // If a single value for the return - if (['key','value'].indexOf(args.valType) !== -1) + if (['key','value'].indexOf(args.$valType) !== -1) { - var pushVal = (args.valType === 'key') ? k : obj[k]; - state[args.varName].push(pushVal); + var pushVal = (args.$valType === 'key') ? k : obj[k]; + state[args.$varName].push(pushVal); } else { - state[args.varName][k] = obj[k]; + state[args.$varName][k] = obj[k]; } }); - return state[args.varName]; + return state[args.$varName]; }, whereMixedSet: function(/*key, val*/) { var args = getArgs('key:string|object, [val]', arguments); @@ -561,12 +566,12 @@ var QueryBuilder = function(driver, adapter) { * @param {String} [val] - The value if using a scalar key * @return this */ - this.set = function(/* key, [val] */) { - var args = getArgs('key:string|object, [val]:string', arguments); + this.set = function(/* $key, [$val] */) { + var args = getArgs('$key, [$val]', arguments); // Set the appropriate state variables - _p.mixedSet('setArrayKeys', 'key', args.key, args.val); - _p.mixedSet('values', 'value', args.key, args.val); + _p.mixedSet('setArrayKeys', 'key', args.$key, args.$val); + _p.mixedSet('values', 'value', args.$key, args.$val); // Use the keys of the array to make the insert/update string // and escape the field names @@ -582,16 +587,26 @@ var QueryBuilder = function(driver, adapter) { /** * Add a join clause to the query * - * @param {String} joinOn - The table you are joining - * @param {String} [cond='='] - The join condition, eg. =,<,>,<>,!=,etc. - * @param {String} joinTo - The value of the condition you are joining on, whether another table's field, or a literal value + * @param {String} table - The table you are joining + * @param {String} cond - The join condition. * @param {String} [type='inner'] - The type of join, which defaults to inner * @return this */ - this.join = function(/* joinOn, [cond='='], joinTo, [type='inner']*/) { - var args = getArgs('joinOn:string, [cond]:string, joinTo:string, [type]:string', arguments); - args.cond = args.cond || '='; - args.type = args.type || "inner"; + this.join = function(table, cond, type) { + type = type || "inner"; + + // Prefix/quote table name + var table = table.split(' ').map(helpers.stringTrim); + table[0] = driver.quoteTable(table[0]); + table = table.map(driver.quoteIdentifiers); + table = table.join(' '); + + // Parse out the join condition + var parsedCondition = parser.compileJoin(cond); + var condition = table + ' ON ' + parsedCondition; + + // Append the join condition to the query map + _p.appendMap("\n" + type.toUpperCase() + ' JOIN ', condition, 'join'); return this; }; @@ -743,13 +758,14 @@ var QueryBuilder = function(driver, adapter) { * @param {Function} callback - Callback for handling response from the database * @return void */ - this.insert = function(table, data, callback) { - if (data) { - this.set(data); + this.insert = function(/* table, data, callback */) { + var args = getArgs('table:string, [data]:object, callback:function', arguments); + if (args.data) { + this.set(args.data); } // Run the query - _p.run('insert', table, callback); + _p.run('insert', args.table, args.callback); }; /** diff --git a/lib/query-parser.js b/lib/query-parser.js new file mode 100644 index 0000000..6dad42a --- /dev/null +++ b/lib/query-parser.js @@ -0,0 +1,87 @@ +'use strict'; + +var helpers = require('./helpers'); + +var matchPatterns = { + 'function': /([a-zA-Z0-9_]+\((.*?)\))/i, + identifier: /([a-zA-Z0-9_\-]+\.?)+/ig, + operator: /\=|AND|&&?|~|\|\|?|\^|\/|>=?|<=?|-|%|OR|\+|NOT|\!=?|<>|XOR/i +}; + +// Full pattern for determining ordering of the pieces +matchPatterns.combined = new RegExp(matchPatterns['function'].source + "+|" + + matchPatterns.identifier.source + + '|(' + matchPatterns.operator.source + ')+', 'ig'); + +var filterMatches = function(array) { + var output = []; + + // Return non-array matches + if (helpers.isScalar(array) || helpers.isNull(array) || helpers.isUndefined(array)) return output; + + array.forEach(function(item) { + if ( ! helpers.isUndefined(item)) + { + output.push(item); + } + }); + + return output; +}; + +var parseJoin = function(sql) { + var matches = {}; + var output = {}; + + // Get clause components + matches['function'] = sql.match(matchPatterns['function']); + matches.identifiers = sql.match(matchPatterns.identifier); + matches.operators = sql.match(matchPatterns.operator); + + // Get everything at once for ordering + matches.combined = sql.match(matchPatterns.combined); + + // Flatten the matches to increase relevance + Object.keys(matches).forEach(function(key) { + output[key] = filterMatches(matches[key]); + }); + + return output; +}; + +// -------------------------------------------------------------------------- + +/** + * @constructor + * @param {Driver} - The driver object for the database in use + * @module query-parser + */ +var QueryParser = function(driver) { + + // That 'new' keyword is annoying + if ( ! (this instanceof QueryParser)) return new QueryParser(driver); + + /** + * Return the output of the parsing of the join condition + * + * @param {String} condition - The join condition to evalate + * @return {String} - The parsed/escaped join condition + */ + this.compileJoin = function(condition) { + var parts = parseJoin(condition); + var count = parts.identifiers.length; + var i; + + // Quote the identifiers + parts.combined.forEach(function(part, i) { + if (parts.identifiers.indexOf(part) !== -1 && ! helpers.isNumber(part)) + { + parts.combined[i] = driver.quoteIdentifiers(part); + } + }); + + return parts.combined.join(''); + }; +}; + +module.exports = QueryParser; diff --git a/tests/query-builder-base.js b/tests/query-builder-base.js index c9a16a2..30c9794 100644 --- a/tests/query-builder-base.js +++ b/tests/query-builder-base.js @@ -26,29 +26,19 @@ module.exports = (function() { .from('create_test') .groupBy('id') .get(base.testCallback.bind(null, test)); - - }, 'Basic select all get': function(test) { base.qb.get('create_test', base.testCallback.bind(null, test)); - - }, 'Basic select all with from': function(test) { base.qb.from('create_test') .get(base.testCallback.bind(null, test)); - - }, 'Get with limit': function(test) { base.qb.get('create_test', 2, base.testCallback.bind(null, test)); - - }, 'Get with limit and offset': function(test) { base.qb.get('create_test', 2, 1, base.testCallback.bind(null, test)); - - }, 'Test get with having': function(test) { base.qb.select('id') @@ -57,8 +47,6 @@ module.exports = (function() { .having({'id >':1}) .having('id !=', 3) .get(base.testCallback.bind(null, test)); - - }, "Test get with 'orHaving'": function(test) { base.qb.select('id') @@ -67,8 +55,6 @@ module.exports = (function() { .having({'id >':1}) .orHaving('id !=', 3) .get(base.testCallback.bind(null, test)); - - } }, // ! Select tests @@ -88,22 +74,16 @@ module.exports = (function() { base.qb.from('create_test') .orderBy('id, key') .get(base.testCallback.bind(null, test)); - - }, 'Select get': function(test) { base.qb.select('id, key as k, val') .get('create_test', 2, 1, base.testCallback.bind(null, test)); - - }, 'Select from get': function(test) { base.qb.select('id, key as k, val') .from('create_test ct') .where('id >', 1) .get(base.testCallback.bind(null, test)); - - }, 'Select from limit get': function(test) { base.qb.select('id, key as k, val') @@ -111,8 +91,6 @@ module.exports = (function() { .where('id >', 1) .limit(3) .get(base.testCallback.bind(null, test)); - - } }, // ! Grouping tests @@ -126,8 +104,6 @@ module.exports = (function() { .groupEnd() .limit(2, 1) .get(base.testCallback.bind(null, test)); - - }, 'Using or grouping method': function(test) { base.qb.select('id, key as k, val') @@ -141,8 +117,6 @@ module.exports = (function() { .groupEnd() .limit(2, 1) .get(base.testCallback.bind(null, test)); - - }, 'Using or not grouping method': function(test) { base.qb.select('id, key as k, val') @@ -156,8 +130,6 @@ module.exports = (function() { .groupEnd() .limit(2, 1) .get(base.testCallback.bind(null, test)); - - } }, // ! Where in tests @@ -166,32 +138,24 @@ module.exports = (function() { base.qb.from('create_test') .whereIn('id', [0, 6, 56, 563, 341]) .get(base.testCallback.bind(null, test)); - - }, 'Or Where in': function(test) { base.qb.from('create_test') .where('key', 'false') .orWhereIn('id', [0, 6, 56, 563, 341]) .get(base.testCallback.bind(null, test)); - - }, 'Where Not in': function(test) { base.qb.from('create_test') .where('key', 'false') .whereNotIn('id', [0, 6, 56, 563, 341]) .get(base.testCallback.bind(null, test)); - - }, 'Or Where Not in': function(test) { base.qb.from('create_test') .where('key', 'false') .orWhereNotIn('id', [0, 6, 56, 563, 341]) .get(base.testCallback.bind(null, test)); - - } }, // ! Query modifier testss @@ -205,8 +169,6 @@ module.exports = (function() { .orderBy('k', "ASC") .limit(5, 2) .get(base.testCallback.bind(null, test)); - - }, 'Group by': function(test) { base.qb.select('id, key as k, val') @@ -219,8 +181,6 @@ module.exports = (function() { .orderBy('k', "ASC") .limit(5, 2) .get(base.testCallback.bind(null, test)); - - }, 'Or Where': function(test) { base.qb.select('id, key as k, val') @@ -229,62 +189,70 @@ module.exports = (function() { .orWhere('key >', 0) .limit(2, 1) .get(base.testCallback.bind(null, test)); - - }, 'Like' : function(test) { base.qb.from('create_test') .like('key', 'og') .get(base.testCallback.bind(null, test)); - - }, 'Or Like': function(test) { base.qb.from('create_test') .like('key', 'og') .orLike('key', 'val') .get(base.testCallback.bind(null, test)); - - }, 'Not Like': function(test) { base.qb.from('create_test') .like('key', 'og', 'before') .notLike('key', 'val') .get(base.testCallback.bind(null, test)); - - }, 'Or Not Like': function(test) { base.qb.from('create_test') .like('key', 'og', 'before') .orNotLike('key', 'val') .get(base.testCallback.bind(null, test)); - - }, 'Like Before': function(test) { base.qb.from('create_test') .like('key', 'og', 'before') .get(base.testCallback.bind(null, test)); - - }, 'Like After': function(test) { base.qb.from('create_test') .like('key', 'og', 'after') .get(base.testCallback.bind(null, test)); - - - }/*, + }, 'Basic Join': function(test) { base.qb.from('create_test ct') - .join('create_join cj', 'cj.id', '=', 'ct.id', 'left') + .join('create_join cj', 'cj.id=ct.id') .get(base.testCallback.bind(null, test)); - }*/ + }, + 'Left Join': function(test) { + base.qb.from('create_test ct') + .join('create_join cj', 'cj.id=ct.id', 'left') + .get(base.testCallback.bind(null, test)); + }, + 'InnerJoin': function(test) { + base.qb.from('create_test ct') + .join('create_join cj', 'cj.id=ct.id', 'inner') + .get(base.testCallback.bind(null, test)); + } }, 'DB update tests' : { - + /*'Test Insert': function(test) { + base.qb.set('id', 98) + .set('key', 84) + .set('val', 120) + .insert('create_test', base.testCallback.bind(null, test)); + }, + 'Test Insert Object': function(test) { + base.qb.insert('create_test', { + id: 587, + key: 1, + val: 2 + }, base.testCallback.bind(null, test)); + }*/ }, 'Compiled query tests' : {