diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..ef8dbdd --- /dev/null +++ b/.jshintrc @@ -0,0 +1,4 @@ +{ + "node": true, + "expr": true +} diff --git a/.npmignore b/.npmignore deleted file mode 100644 index c065047..0000000 --- a/.npmignore +++ /dev/null @@ -1,9 +0,0 @@ -# Sublime Text projects -*.sublime-project -*.sublime-workspace - -node_modules -tests -example.html -Gruntfile.js -.git* diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 67de7a5..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = function(grunt) { - - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - jshint: { - all: ['Gruntfile.js', 'simpleSqlParser.js', 'tests/tests.js'], - options : { - multistr: true, - sub: true, - }, - }, - qunit: { - all: ['tests/**/*.html'], - }, - watch: { - files: ['**.js'], - tasks: ['default'], - options: { - atBegin: true, - interrupt: true, - }, - }, - }); - - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-contrib-qunit'); - grunt.loadNpmTasks('grunt-contrib-watch'); - - grunt.registerTask('default', ['jshint', 'qunit']); - -}; diff --git a/README.md b/README.md index e771ca9..e7a3d28 100644 --- a/README.md +++ b/README.md @@ -3,59 +3,7 @@ simpleSqlParser Javascript library to parse CRUD (Create Retrieve Update Delete) SQL queries. -## How to use - -### From browser - -Import the JS file in your page: - -```html - -``` - -### From Node.js - -Install the module from [npm](https://npmjs.org/package/simple-sql-parser) using `npm i simple-sql-parser --save`. - -Import the JS module in your app: - -```js -var simpleSqlParser = require('simple-sql-parser'); -``` - -### Parse a query: - -```js -var ast = simpleSqlParser.sql2ast('your SQL query'); -console.log(ast); -``` - -### Create a query from AST: - -```js -var query = simpleSqlParser.ast2sql(ast); -console.log(query); -``` - -*AST means Abstract Syntax Tree.* - -## Examples - -* See `example.html` (open brower's console) -* Have a look to the unit tests `tests/tests.js` to see what's possible - -## Notes - -simpleSqlParser only supports these queries: - -* SELECT -* INSERT -* UPDATE -* DELETE - -simpleSqlParser **is not a full SQL parser!** -It only support few SQL mechanisms and keywords. -Feel free to make a pull request/issue. +**This is a work in progress!** ## How to install dev tools @@ -64,9 +12,13 @@ If you want to contribute, please write tests and respect the coding style. To install dev tools: - install Node.js (http://nodejs.org/) -- install **grunt-cli** globally: `npm i -g grunt-cli` +- install **gulp** globally: `npm i -g gulp` - install dev dependencies: `npm i` -- use `grunt` to check your code! +- use `gulp` to check your code! + +To update dev tools (because `package.json` has changed): + +- install new dev dependencies: `npm i` ## License diff --git a/bower.json b/bower.json deleted file mode 100644 index 20f8d43..0000000 --- a/bower.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "simple-sql-parser", - "main": "simpleSqlParser.js", - "homepage": "https://github.com/dsferruzza/simpleSqlParser", - "authors": [ - "David Sferruzza " - ], - "description": "Javascript library to parse CRUD (Create Retrieve Update Delete) SQL queries.", - "keywords": [ - "sql", - "parse", - "ast" - ], - "license": "MIT", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "tests", - "package.json", - "Gruntfile.js" - ] -} diff --git a/demo.html b/demo.html new file mode 100644 index 0000000..6a67225 --- /dev/null +++ b/demo.html @@ -0,0 +1,27 @@ + + + + + Demo - simpleSqlParser + + +

+
+ +

+

+ +

+

AST

+

+
+	
+	
+
+
diff --git a/dist/simpleSqlParser.js b/dist/simpleSqlParser.js
new file mode 100644
index 0000000..53c9607
--- /dev/null
+++ b/dist/simpleSqlParser.js
@@ -0,0 +1,1236 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.simpleSqlParser = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o last.furthest) return result;
+
+    var expected = (result.furthest === last.furthest)
+      ? result.expected.concat(last.expected)
+      : last.expected;
+
+    return {
+      status: result.status,
+      index: result.index,
+      value: result.value,
+      furthest: last.furthest,
+      expected: expected
+    }
+  }
+
+  function assertParser(p) {
+    if (!(p instanceof Parser)) throw new Error('not a parser: '+p);
+  }
+
+  function formatExpected(expected) {
+    if (expected.length === 1) return expected[0];
+
+    return 'one of ' + expected.join(', ')
+  }
+
+  function formatGot(stream, error) {
+    var i = error.index;
+
+    if (i === stream.length) return ', got the end of the stream'
+
+
+    var prefix = (i > 0 ? "'..." : "'");
+    var suffix = (stream.length - i > 12 ? "...'" : "'");
+
+    return ' at character ' + i + ', got ' + prefix + stream.slice(i, i+12) + suffix
+  }
+
+  var formatError = Parsimmon.formatError = function(stream, error) {
+    return 'expected ' + formatExpected(error.expected) + formatGot(stream, error)
+  };
+
+  _.parse = function(stream) {
+    var result = this.skip(eof)._(stream, 0);
+
+    return result.status ? {
+      status: true,
+      value: result.value
+    } : {
+      status: false,
+      index: result.furthest,
+      expected: result.expected
+    };
+  };
+
+  // [Parser a] -> Parser [a]
+  var seq = Parsimmon.seq = function() {
+    var parsers = [].slice.call(arguments);
+    var numParsers = parsers.length;
+
+    return Parser(function(stream, i) {
+      var result;
+      var accum = new Array(numParsers);
+
+      for (var j = 0; j < numParsers; j += 1) {
+        result = mergeReplies(parsers[j]._(stream, i), result);
+        if (!result.status) return result;
+        accum[j] = result.value
+        i = result.index;
+      }
+
+      return mergeReplies(makeSuccess(i, accum), result);
+    });
+  };
+
+
+  var seqMap = Parsimmon.seqMap = function() {
+    var args = [].slice.call(arguments);
+    var mapper = args.pop();
+    return seq.apply(null, args).map(function(results) {
+      return mapper.apply(null, results);
+    });
+  };
+
+  /**
+   * Allows to add custom primitive parsers
+   */
+  var custom = Parsimmon.custom = function(parsingFunction) {
+    return Parser(parsingFunction(makeSuccess, makeFailure));
+  };
+
+  var alt = Parsimmon.alt = function() {
+    var parsers = [].slice.call(arguments);
+    var numParsers = parsers.length;
+    if (numParsers === 0) return fail('zero alternates')
+
+    return Parser(function(stream, i) {
+      var result;
+      for (var j = 0; j < parsers.length; j += 1) {
+        result = mergeReplies(parsers[j]._(stream, i), result);
+        if (result.status) return result;
+      }
+      return result;
+    });
+  };
+
+  // -*- primitive combinators -*- //
+  _.or = function(alternative) {
+    return alt(this, alternative);
+  };
+
+  _.then = function(next) {
+    if (typeof next === 'function') {
+      throw new Error('chaining features of .then are no longer supported, use .chain instead');
+    }
+
+    assertParser(next);
+    return seq(this, next).map(function(results) { return results[1]; });
+  };
+
+  // -*- optimized iterative combinators -*- //
+  // equivalent to:
+  // _.many = function() {
+  //   return this.times(0, Infinity);
+  // };
+  // or, more explicitly:
+  // _.many = function() {
+  //   var self = this;
+  //   return self.then(function(x) {
+  //     return self.many().then(function(xs) {
+  //       return [x].concat(xs);
+  //     });
+  //   }).or(succeed([]));
+  // };
+  _.many = function() {
+    var self = this;
+
+    return Parser(function(stream, i) {
+      var accum = [];
+      var result;
+      var prevResult;
+
+      for (;;) {
+        result = mergeReplies(self._(stream, i), result);
+
+        if (result.status) {
+          i = result.index;
+          accum.push(result.value);
+        }
+        else {
+          return mergeReplies(makeSuccess(i, accum), result);
+        }
+      }
+    });
+  };
+
+  // equivalent to:
+  // _.times = function(min, max) {
+  //   if (arguments.length < 2) max = min;
+  //   var self = this;
+  //   if (min > 0) {
+  //     return self.then(function(x) {
+  //       return self.times(min - 1, max - 1).then(function(xs) {
+  //         return [x].concat(xs);
+  //       });
+  //     });
+  //   }
+  //   else if (max > 0) {
+  //     return self.then(function(x) {
+  //       return self.times(0, max - 1).then(function(xs) {
+  //         return [x].concat(xs);
+  //       });
+  //     }).or(succeed([]));
+  //   }
+  //   else return succeed([]);
+  // };
+  _.times = function(min, max) {
+    if (arguments.length < 2) max = min;
+    var self = this;
+
+    return Parser(function(stream, i) {
+      var accum = [];
+      var start = i;
+      var result;
+      var prevResult;
+
+      for (var times = 0; times < min; times += 1) {
+        result = self._(stream, i);
+        prevResult = mergeReplies(result, prevResult);
+        if (result.status) {
+          i = result.index;
+          accum.push(result.value);
+        }
+        else return prevResult;
+      }
+
+      for (; times < max; times += 1) {
+        result = self._(stream, i);
+        prevResult = mergeReplies(result, prevResult);
+        if (result.status) {
+          i = result.index;
+          accum.push(result.value);
+        }
+        else break;
+      }
+
+      return mergeReplies(makeSuccess(i, accum), prevResult);
+    });
+  };
+
+  // -*- higher-level combinators -*- //
+  _.result = function(res) { return this.map(function(_) { return res; }); };
+  _.atMost = function(n) { return this.times(0, n); };
+  _.atLeast = function(n) {
+    var self = this;
+    return seqMap(this.times(n), this.many(), function(init, rest) {
+      return init.concat(rest);
+    });
+  };
+
+  _.map = function(fn) {
+    var self = this;
+    return Parser(function(stream, i) {
+      var result = self._(stream, i);
+      if (!result.status) return result;
+      return mergeReplies(makeSuccess(result.index, fn(result.value)), result);
+    });
+  };
+
+  _.skip = function(next) {
+    return seq(this, next).map(function(results) { return results[0]; });
+  };
+
+  _.mark = function() {
+    return seqMap(index, this, index, function(start, value, end) {
+      return { start: start, value: value, end: end };
+    });
+  };
+
+  _.desc = function(expected) {
+    var self = this;
+    return Parser(function(stream, i) {
+      var reply = self._(stream, i);
+      if (!reply.status) reply.expected = [expected];
+      return reply;
+    });
+  };
+
+  // -*- primitive parsers -*- //
+  var string = Parsimmon.string = function(str) {
+    var len = str.length;
+    var expected = "'"+str+"'";
+
+    return Parser(function(stream, i) {
+      var head = stream.slice(i, i+len);
+
+      if (head === str) {
+        return makeSuccess(i+len, head);
+      }
+      else {
+        return makeFailure(i, expected);
+      }
+    });
+  };
+
+  var regex = Parsimmon.regex = function(re, group) {
+    var anchored = RegExp('^(?:'+re.source+')', (''+re).slice((''+re).lastIndexOf('/')+1));
+    var expected = '' + re;
+    if (group == null) group = 0;
+
+    return Parser(function(stream, i) {
+      var match = anchored.exec(stream.slice(i));
+
+      if (match) {
+        var fullMatch = match[0];
+        var groupMatch = match[group];
+        if (groupMatch != null) return makeSuccess(i+fullMatch.length, groupMatch);
+      }
+
+      return makeFailure(i, expected);
+    });
+  };
+
+  var succeed = Parsimmon.succeed = function(value) {
+    return Parser(function(stream, i) {
+      return makeSuccess(i, value);
+    });
+  };
+
+  var fail = Parsimmon.fail = function(expected) {
+    return Parser(function(stream, i) { return makeFailure(i, expected); });
+  };
+
+  var letter = Parsimmon.letter = regex(/[a-z]/i).desc('a letter')
+  var letters = Parsimmon.letters = regex(/[a-z]*/i)
+  var digit = Parsimmon.digit = regex(/[0-9]/).desc('a digit');
+  var digits = Parsimmon.digits = regex(/[0-9]*/)
+  var whitespace = Parsimmon.whitespace = regex(/\s+/).desc('whitespace');
+  var optWhitespace = Parsimmon.optWhitespace = regex(/\s*/);
+
+  var any = Parsimmon.any = Parser(function(stream, i) {
+    if (i >= stream.length) return makeFailure(i, 'any character');
+
+    return makeSuccess(i+1, stream.charAt(i));
+  });
+
+  var all = Parsimmon.all = Parser(function(stream, i) {
+    return makeSuccess(stream.length, stream.slice(i));
+  });
+
+  var eof = Parsimmon.eof = Parser(function(stream, i) {
+    if (i < stream.length) return makeFailure(i, 'EOF');
+
+    return makeSuccess(i, null);
+  });
+
+  var test = Parsimmon.test = function(predicate) {
+    return Parser(function(stream, i) {
+      var char = stream.charAt(i);
+      if (i < stream.length && predicate(char)) {
+        return makeSuccess(i+1, char);
+      }
+      else {
+        return makeFailure(i, 'a character matching '+predicate);
+      }
+    });
+  };
+
+  var oneOf = Parsimmon.oneOf = function(str) {
+    return test(function(ch) { return str.indexOf(ch) >= 0; });
+  };
+
+  var noneOf = Parsimmon.noneOf = function(str) {
+    return test(function(ch) { return str.indexOf(ch) < 0; });
+  };
+
+  var takeWhile = Parsimmon.takeWhile = function(predicate) {
+    return Parser(function(stream, i) {
+      var j = i;
+      while (j < stream.length && predicate(stream.charAt(j))) j += 1;
+      return makeSuccess(j, stream.slice(i, j));
+    });
+  };
+
+  var lazy = Parsimmon.lazy = function(desc, f) {
+    if (arguments.length < 2) {
+      f = desc;
+      desc = undefined;
+    }
+
+    var parser = Parser(function(stream, i) {
+      parser._ = f()._;
+      return parser._(stream, i);
+    });
+
+    if (desc) parser = parser.desc(desc)
+
+    return parser;
+  };
+
+  var index = Parsimmon.index = Parser(function(stream, i) {
+    return makeSuccess(i, i);
+  });
+
+  //- fantasyland compat
+
+  //- Monoid (Alternative, really)
+  _.concat = _.or;
+  _.empty = fail('empty')
+
+  //- Applicative
+  _.of = Parser.of = Parsimmon.of = succeed
+
+  _.ap = function(other) {
+    return seqMap(this, other, function(f, x) { return f(x); })
+  };
+
+  //- Monad
+  _.chain = function(f) {
+    var self = this;
+    return Parser(function(stream, i) {
+      var result = self._(stream, i);
+      if (!result.status) return result;
+      var nextParser = f(result.value);
+      return mergeReplies(nextParser._(stream, result.index), result);
+    });
+  };
+
+  return Parser;
+})();
+module.exports = Parsimmon;
+
+},{}],3:[function(require,module,exports){
+module.exports = require('./build/parsimmon.commonjs');
+exports.version = require('./package.json').version;
+
+},{"./build/parsimmon.commonjs":2,"./package.json":4}],4:[function(require,module,exports){
+module.exports={
+  "_args": [
+    [
+      "parsimmon@0.7.0",
+      "/Users/ra/Workspace/ra/simpleSqlParser"
+    ]
+  ],
+  "_from": "parsimmon@0.7.0",
+  "_id": "parsimmon@0.7.0",
+  "_inCache": true,
+  "_installable": true,
+  "_location": "/parsimmon",
+  "_npmUser": {
+    "email": "jjmadkisson@gmail.com",
+    "name": "jayferd"
+  },
+  "_npmVersion": "1.4.14",
+  "_phantomChildren": {},
+  "_requested": {
+    "name": "parsimmon",
+    "raw": "parsimmon@0.7.0",
+    "rawSpec": "0.7.0",
+    "scope": null,
+    "spec": "0.7.0",
+    "type": "version"
+  },
+  "_requiredBy": [
+    "/"
+  ],
+  "_resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-0.7.0.tgz",
+  "_shasum": "652fc7cbade73c5edb42a266ec556c906d82c9fb",
+  "_shrinkwrap": null,
+  "_spec": "parsimmon@0.7.0",
+  "_where": "/Users/ra/Workspace/ra/simpleSqlParser",
+  "author": {
+    "email": "jneen at jneen dot net",
+    "name": "Jeanine Adkisson"
+  },
+  "bugs": {
+    "url": "https://github.com/jneen/parsimmon/issues"
+  },
+  "dependencies": {
+    "pjs": "5.x"
+  },
+  "description": "A monadic LL(infinity) parser combinator library",
+  "devDependencies": {
+    "chai": "1.5.x",
+    "mocha": "1.8.x",
+    "uglify-js": "2.x"
+  },
+  "directories": {},
+  "dist": {
+    "shasum": "652fc7cbade73c5edb42a266ec556c906d82c9fb",
+    "tarball": "https://registry.npmjs.org/parsimmon/-/parsimmon-0.7.0.tgz"
+  },
+  "files": [
+    "Makefile",
+    "build/parsimmon.browser.js",
+    "build/parsimmon.browser.min.js",
+    "build/parsimmon.commonjs.js",
+    "index.js",
+    "package.json",
+    "src",
+    "test"
+  ],
+  "homepage": "https://github.com/jneen/parsimmon",
+  "keywords": [
+    "parse",
+    "parser combinators",
+    "parsing"
+  ],
+  "main": "index.js",
+  "maintainers": [
+    {
+      "name": "jayferd",
+      "email": "jjmadkisson@gmail.com"
+    },
+    {
+      "name": "jneen",
+      "email": "jneen@jneen.net"
+    }
+  ],
+  "name": "parsimmon",
+  "optionalDependencies": {},
+  "readme": "ERROR: No README data found!",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/jneen/parsimmon.git"
+  },
+  "scripts": {
+    "test": "make test"
+  },
+  "version": "0.7.0"
+}
+
+},{}],5:[function(require,module,exports){
+"use strict";
+
+module.exports = function(ast) {
+	if (typeof ast === 'object' && ast.status === true) ast = ast.value;
+	else return false;
+
+	function select(ast) {
+		var result = 'SELECT ';
+		result += ast.select.map(function(item) {
+			return item.expression;
+		}).join(', ');
+		return result;
+	}
+
+	function from(ast) {
+		var result = 'FROM ';
+		result += ast.from.map(function(item) {
+			return item.expression;
+		}).join(', ');
+		return result;
+	}
+
+	function join(ast) {
+		return ast.join.map(function(item) {
+			var result = '';
+			if (item.type === 'inner') result += 'INNER JOIN ';
+			else if (item.type === 'left') result += 'LEFT JOIN ';
+			else if (item.type === 'right') result += 'RIGHT JOIN ';
+			else return '';
+			result += item.table;
+			if (item.alias !== null) result += ' AS ' + item.alias;
+			result += ' ON ';
+			result += item.condition.expression;
+			return result;
+		}).join(' ');
+	}
+
+	function where(ast) {
+		var result = '';
+		if (ast.where !== null) result += 'WHERE ' + ast.where.expression;
+		return result;
+	}
+
+	function group(ast) {
+		var result = '';
+		if (ast.group.length > 0) {
+			result += 'GROUP BY ';
+			result += ast.group.map(function(item) {
+				return item.expression;
+			}).join(', ');
+		}
+		return result;
+	}
+
+	function order(ast) {
+		var result = '';
+		if (ast.order.length > 0) {
+			result += 'ORDER BY ';
+			result += ast.order.map(function(item) {
+				return item.expression;
+			}).join(', ');
+		}
+		return result;
+	}
+
+	function limit(ast) {
+		var result = '';
+		if (ast.limit !== null) {
+			result += 'LIMIT ';
+			if (ast.limit.from !== null) result += ast.limit.from + ', ';
+			result += ast.limit.nb;
+		}
+		return result;
+	}
+
+	function into(ast) {
+		return 'INSERT INTO ' + ast.into.expression;
+	}
+
+	function values(ast) {
+		var result = '';
+		var targets = ast.values.filter(function(item) {
+			return item.target !== null;
+		});
+		if (targets.length > 0) {
+			result += '(';
+			result += targets.map(function(item) {
+				return item.target.expression;
+			}).join(', ');
+			result += ') ';
+		}
+		result += 'VALUES (';
+		result += ast.values.map(function(item) {
+			return item.value;
+		}).join(', ');
+		result += ')';
+		return result;
+	}
+
+	function table(ast) {
+		return 'UPDATE ' + ast.table.expression;
+	}
+
+	function update(ast) {
+		var result = 'SET ';
+		result += ast.values.map(function(item) {
+			return item.target.expression + ' = ' + item.value;
+		}).join(', ');
+		return result;
+	}
+
+	var parts = [];
+	if (ast.type === 'select') {
+		parts.push(select(ast));
+		parts.push(from(ast));
+		parts.push(join(ast));
+		parts.push(where(ast));
+		parts.push(group(ast));
+		parts.push(order(ast));
+		parts.push(limit(ast));
+	}
+	else if (ast.type === 'insert') {
+		parts.push(into(ast));
+		parts.push(values(ast));
+	}
+	else if (ast.type === 'update') {
+		parts.push(table(ast));
+		parts.push(update(ast));
+		parts.push(where(ast));
+	}
+	else if (ast.type === 'delete') {
+		parts.push('DELETE');
+		parts.push(from(ast));
+		parts.push(where(ast));
+	}
+	else return false;
+
+	return parts.filter(function(item) {
+		return item !== '';
+	}).join(' ');
+};
+
+},{}],6:[function(require,module,exports){
+"use strict";
+var Parsimmon = require('parsimmon');
+
+/********************************************************************************************
+	ALIASES
+********************************************************************************************/
+
+var seq = Parsimmon.seq;
+var alt = Parsimmon.alt;
+var regex = Parsimmon.regex;
+var string = Parsimmon.string;
+var optWhitespace = Parsimmon.optWhitespace;
+var whitespace = Parsimmon.whitespace;
+var lazy = Parsimmon.lazy;
+
+
+
+/********************************************************************************************
+	COMMON PATTERNS
+********************************************************************************************/
+
+// Make a parser optionnal
+// "empty" parameter will be returned as result if the optionnal parser can't match
+function opt(parser, empty) {
+	if (typeof empty == 'undefined') empty = [];
+	return parser.or(Parsimmon.succeed(empty));
+}
+
+// Join results of a parser
+function mkString(node) {
+	return node.join('');
+}
+
+// Add an item to an optionnal list and return the final list
+function mergeOptionnalList(node) {
+	node[0].push(node[1]);
+	return node[0];
+}
+
+// Generate a parser that accept a comma-separated list of something
+function optionnalList(parser) {
+	return seq(
+		parser.skip(optWhitespace).skip(string(',')).skip(optWhitespace).many(),
+		parser.skip(optWhitespace)
+	).map(mergeOptionnalList);
+}
+
+// Remove first and last character of a string
+function removeQuotes(string) {
+	return string.replace(/^([`'"])(.*)\1$/, '$2');
+}
+
+// Add the starting and ending char positions of matches of a given parser
+function getPos(parser) {
+	return seq(
+		Parsimmon.index,
+		parser,
+		Parsimmon.index
+	).map(function(node) {
+		var pos = {
+			start: node[0],
+			end: node[2],
+		};
+		if (typeof node[1] == 'object') {
+			var n = node[1];
+			n.position = pos;
+			return n;
+		}
+		else {
+			pos.out = node[1];
+			return pos;
+		}
+	});
+}
+
+
+
+/********************************************************************************************
+	LOW LEVEL PARSERS
+********************************************************************************************/
+
+// The name of a column/table
+var colName = alt(
+	regex(/(?!(FROM|WHERE|GROUP BY|ORDER BY|LIMIT|INNER|LEFT|RIGHT|JOIN|ON|VALUES|SET)\s)[a-z*][a-z0-9_]*/i),
+	regex(/`[^`\\]*(?:\\.[^`\\]*)*`/)
+);
+
+// A string
+var str = alt(
+	regex(/"[^"\\]*(?:\\.[^"\\]*)*"/),
+	regex(/'[^'\\]*(?:\\.[^'\\]*)*'/)
+);
+
+// A function expression
+var func = seq(
+	alt(
+		regex(/[a-zA-Z0-9_]+\(/),
+		string('(')
+		),
+	opt(lazy(function() { return argList; })).map(mkString),
+	string(')')
+).map(mkString);
+
+// A table.column expression
+var tableAndColumn = seq(
+	colName,
+	string('.'),
+	colName
+);
+
+// An operator
+var operator = alt(
+	string('+'),
+	string('-'),
+	string('*'),
+	string('/'),
+	string('&&'),
+	string('&'),
+	string('~'),
+	string('||'),
+	string('|'),
+	string('^'),
+	regex(/XOR/i),
+	string('<=>'),
+	string('='),
+	string('!='),
+	string('>='),
+	string('>>'),
+	string('>'),
+	string('<='),
+	string('<<'),
+	string('<'),
+	regex(/IS NULL/i),
+	regex(/IS NOT/i),
+	regex(/IS NOT NULL/i),
+	regex(/IS/i),
+	regex(/LIKE/i),
+	regex(/NOT LIKE/i),
+	regex(/BETWEEN/i),
+	regex(/NOT BETWEEN/i),
+	string('%'),
+	regex(/MOD/i),
+	regex(/NOT/i),
+	regex(/OR\s/i),	// A space is forced after so this doesn't get mixed up with ORDER BY
+	regex(/AND/i),
+	regex(/IN/i)
+);
+
+// A number
+var number = regex(/[-]?\d+\.?\d*/);
+
+
+
+/********************************************************************************************
+	EXPRESSION PARSERS
+********************************************************************************************/
+
+// List (following IN, for example)
+var list = seq(
+	string('('),
+	optWhitespace,
+	seq(
+		alt(
+			number,
+			str
+		),
+		optWhitespace,
+		opt(string(',')),
+		optWhitespace,
+		opt(
+			alt(
+				number,
+				str
+			)
+		)
+	).map(mkString),
+	optWhitespace,
+	string(')')
+).map(mkString);
+
+// Expression
+var expression = seq(
+	alt(
+		tableAndColumn.map(function(node) {
+			return {
+				expression: node.join(''),
+				table: removeQuotes(node[0]),
+				column: removeQuotes(node[2])
+			};
+		}),
+		func.map(function(node) {
+			return {
+				expression: node,
+				table: null,
+				column: null
+			};
+		}),
+		colName.map(function(node) {
+			return {
+				expression: node,
+				table: null,
+				column: removeQuotes(node)
+			};
+		}),
+		str.map(function(node) {
+			return {
+				expression: node,
+				table: null,
+				column: null
+			};
+		}),
+		number.map(function(node) {
+			return {
+				expression: node,
+				table: null,
+				column: null
+			};
+		}),
+		list.map(function(node) {
+			return {
+				expression: node,
+				table: null,
+				column: null
+			};
+		})
+	),
+	opt(seq(
+		optWhitespace,
+		operator,
+		opt(seq(
+			optWhitespace,
+			lazy(function() { return expression; }).map(function(node) {
+				return node.expression;
+			})
+		).map(mkString), null)
+	).map(mkString), null)
+).map(function(node) {
+	if (node[1] !== null) {
+		node[0] = node[0].expression;
+		return {
+			expression: node.join(''),
+			table: null,
+			column: null
+		};
+	}
+	else return node[0];
+});
+
+// Expression following a SELECT statement
+var colListExpression = seq(
+	expression,
+	opt(	// Alias
+		seq(
+			optWhitespace,
+			opt(regex(/AS\s/i)),
+			alt(colName, str)
+		).map(function(node) {
+			var n = {};
+			n.alias = removeQuotes(node[2]);
+			n.expression = node.join('');
+			return n;
+		}),
+		null
+	)
+).map(function(node) {
+	var n = node[0];
+	n.alias = (node[1] !== null) ? node[1].alias : null;
+	n.expression = n.expression + ((node[1] !== null) ? node[1].expression : '');
+	return n;
+});
+
+// Expression inside a function
+var argListExpression = expression.map(function(node) {
+	return node.expression;
+});
+
+// Expression following a FROM statement
+var tableListExpression = seq(
+	alt(
+		tableAndColumn.map(mkString),
+		colName
+	),
+	opt(	// Alias
+		seq(
+			optWhitespace,
+			opt(regex(/AS\s/i)),
+			alt(colName, str)
+		).map(function(node) {
+			return {
+				alias: removeQuotes(node[2]),
+				expression : node.join(''),
+			};
+		}),
+		null
+	)
+).map(function(node) {
+	var n = {};
+	n.table = node[0];
+	n.alias = (node[1] !== null) ? node[1].alias : null;
+	n.expression = node[0] + ((node[1] !== null) ? node[1].expression : '');
+	return n;
+});
+
+// JOIN expression (including JOIN statements)
+var joinExpression = seq(
+	opt(seq(
+		regex(/INNER|LEFT|RIGHT/i),
+		whitespace
+	).map(function(node) {
+		return node[0].toLowerCase();
+	}), null),
+	regex(/JOIN/i),
+	optWhitespace,
+	getPos(tableListExpression),
+	optWhitespace,
+	regex(/ON/i),
+	optWhitespace,
+	getPos(expression)
+).map(function(node) {
+	var n = {};
+	n.type = node[0] || 'inner';
+	n.table = node[3].table;
+	n.alias = node[3].alias;
+	n.position = node[3].position;
+	n.condition = {
+		expression: node[7].expression,
+		position: node[7].position,
+	};
+	return n;
+});
+
+// Expression following a WHERE statement
+var whereExpression = getPos(expression).map(function(node) {
+	return {
+		expression: node.expression,
+		position: node.position,
+	};
+});
+
+// Expression following an ORDER BY statement
+var orderListExpression = seq(
+	expression,
+	opt(seq(
+		optWhitespace,
+		regex(/ASC|DESC/i)
+	), null)
+).map(function(node) {
+	return {
+		expression: node[0].expression + ((node[1] !== null) ? node[1].join('') : ''),
+		order: (node[1] !== null) ? node[1][1] : 'ASC',
+		table: node[0].table,
+		column: node[0].column,
+	};
+});
+
+// Expression following a LIMIT statement
+var limitExpression = seq(
+	number,
+	opt(seq(
+		optWhitespace,
+		string(','),
+		optWhitespace,
+		number
+	), null)
+).map(function(node) {
+	if (node[1] === null) return {
+		from: null,
+		nb: parseInt(node[0], 10),
+	};
+	else return {
+		from: parseInt(node[0], 10),
+		nb: parseInt(node[1][3], 10),
+	};
+});
+
+// Expression designating a column before VALUES in INSERT query
+var insertColListExpression = alt(
+	tableAndColumn.map(function(node) {
+		return {
+			expression: node.join(''),
+			column: removeQuotes(node[2])
+		};
+	}),
+	colName.map(function(node) {
+		return {
+			expression: node,
+			column: removeQuotes(node)
+		};
+	})
+);
+
+// Expression following a VALUES statement
+var valueExpression = expression.map(function(node) {
+	return node.expression;
+});
+
+// Expression that assign a value to a column
+var assignExpression = seq(
+	insertColListExpression,
+	optWhitespace,
+	string('='),
+	optWhitespace,
+	expression
+).map(function(node) {
+	return {
+		target: node[0],
+		value: node[4].expression,
+	};
+});
+
+
+
+/********************************************************************************************
+	HIGH LEVEL PARSERS
+********************************************************************************************/
+
+// List of arguments inside a function
+var argList = seq(
+	seq(argListExpression, optWhitespace, string(','), optWhitespace).map(mkString).many(),
+	argListExpression.skip(optWhitespace)
+).map(mergeOptionnalList);
+
+// List of expressions following a SELECT statement
+var colList = optionnalList(getPos(colListExpression));
+
+// List of table following a FROM statement
+var tableList = optionnalList(getPos(tableListExpression));
+
+// List of table following an GROUP BY statement
+var groupList = optionnalList(getPos(expression));
+
+// List of table following an ORDER BY statement
+var orderList = optionnalList(getPos(orderListExpression));
+
+// List of joins (including JOIN statements)
+var joinList = optWhitespace.then(joinExpression).skip(optWhitespace).many();
+
+// List of columns before VALUES in INSERT query
+var insertColList = optionnalList(insertColListExpression);
+
+// List of values following a VALUES statement
+var valuesList = optionnalList(valueExpression);
+
+// List of assign expression following a SET statement
+var assignList = optionnalList(assignExpression);
+
+
+
+/********************************************************************************************
+	MAIN PARSERS
+********************************************************************************************/
+
+// SELECT parser
+var selectParser = seq(
+	regex(/SELECT/i).skip(optWhitespace).then(opt(colList)),
+	regex(/FROM/i).skip(optWhitespace).then(opt(tableList)),
+	opt(joinList),
+	opt(regex(/WHERE/i).skip(optWhitespace).then(opt(whereExpression)), null),
+	opt(regex(/\s?GROUP BY/i).skip(optWhitespace).then(opt(groupList))),
+	opt(regex(/\s?ORDER BY/i).skip(optWhitespace).then(opt(orderList))),
+	opt(regex(/\s?LIMIT/i).skip(optWhitespace).then(opt(limitExpression)), null)
+).map(function(node) {
+	return {
+		type: 'select',
+		select: node[0],
+		from: node[1],
+		join: node[2],
+		where: node[3],
+		group: node[4],
+		order: node[5],
+		limit: node[6],
+	};
+});
+
+// INSERT parser
+var insertParser = seq(
+	regex(/INSERT INTO/i).skip(optWhitespace).then(tableListExpression),
+	optWhitespace,
+	opt(
+		seq(
+			string('('),
+			insertColList,
+			string(')')
+		).map(function(node) {
+			return node[1];
+		})
+	),
+	optWhitespace,
+	regex(/VALUES\s?\(/i).skip(optWhitespace).then(valuesList),
+	string(')')
+).map(function(node) {
+	var values = [];
+	var bigger = Math.max(node[2].length, node[4].length);
+
+	for (var i = 0; i < bigger; ++i) {
+		values[i] = {
+			target: node[2][i] || null,
+			value: node[4][i] || null,
+		};
+	}
+
+	return {
+		type: 'insert',
+		into: node[0],
+		values: values,
+	};
+});
+
+// UPDATE parser
+var updateParser = seq(
+	regex(/UPDATE/i).skip(optWhitespace).then(tableListExpression),
+	optWhitespace,
+	regex(/SET/i).skip(optWhitespace).then(assignList),
+	optWhitespace,
+	opt(regex(/WHERE/i).skip(optWhitespace).then(opt(whereExpression)), null)
+).map(function(node) {
+	return {
+		type: 'update',
+		table: node[0],
+		values: node[2],
+		where: node[4],
+	};
+});
+
+// DELETE parser
+var deleteParser = seq(
+	regex(/DELETE FROM/i).skip(optWhitespace).then(opt(tableList)),
+	opt(regex(/WHERE/i).skip(optWhitespace).then(opt(whereExpression)), null)
+).map(function(node) {
+	return {
+		type: 'delete',
+		from: node[0],
+		where: node[1],
+	};
+});
+
+// Main parser
+var p = alt(selectParser, insertParser, updateParser, deleteParser);
+
+
+
+/********************************************************************************************
+	PUBLIC FUNCTIONS
+********************************************************************************************/
+
+module.exports = function(sql) {
+	var result = p.parse(sql);
+	if (result.status === false) result.error = Parsimmon.formatError(sql, result);
+	return result;
+};
+
+},{"parsimmon":3}]},{},[1])(1)
+});
\ No newline at end of file
diff --git a/dist/simpleSqlParser.min.js b/dist/simpleSqlParser.min.js
new file mode 100644
index 0000000..820d5ff
--- /dev/null
+++ b/dist/simpleSqlParser.min.js
@@ -0,0 +1 @@
+!function(n){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.simpleSqlParser=n()}}(function(){return function n(e,t,r){function i(s,o){if(!t[s]){if(!e[s]){var a="function"==typeof require&&require;if(!o&&a)return a(s,!0);if(u)return u(s,!0);var l=new Error("Cannot find module '"+s+"'");throw l.code="MODULE_NOT_FOUND",l}var p=t[s]={exports:{}};e[s][0].call(p.exports,function(n){var t=e[s][1][n];return i(t?t:n)},p,p.exports,n,e,t,r)}return t[s].exports}for(var u="function"==typeof require&&require,s=0;se.furthest)return n;var t=n.furthest===e.furthest?n.expected.concat(e.expected):e.expected;return{status:n.status,index:n.index,value:n.value,furthest:e.furthest,expected:t}}function u(e){if(!(e instanceof n))throw new Error("not a parser: "+e)}function s(n){return 1===n.length?n[0]:"one of "+n.join(", ")}function o(n,e){var t=e.index;if(t===n.length)return", got the end of the stream";var r=t>0?"'...":"'",i=n.length-t>12?"...'":"'";return" at character "+t+", got "+r+n.slice(t,t+12)+i}{var a=n.prototype;r.formatError=function(n,e){return"expected "+s(e.expected)+o(n,e)}}a.parse=function(n){var e=this.skip(d)._(n,0);return e.status?{status:!0,value:e.value}:{status:!1,index:e.furthest,expected:e.expected}};var l=r.seq=function(){var t=[].slice.call(arguments),r=t.length;return n(function(n,u){for(var s,o=new Array(r),a=0;r>a;a+=1){if(s=i(t[a]._(n,u),s),!s.status)return s;o[a]=s.value,u=s.index}return i(e(u,o),s)})},p=r.seqMap=function(){var n=[].slice.call(arguments),e=n.pop();return l.apply(null,n).map(function(n){return e.apply(null,n)})},c=(r.custom=function(r){return n(r(e,t))},r.alt=function(){var e=[].slice.call(arguments),t=e.length;return 0===t?h("zero alternates"):n(function(n,t){for(var r,u=0;up;p+=1){if(o=u._(n,s),a=i(o,a),!o.status)return a;s=o.index,l.push(o.value)}for(;r>p&&(o=u._(n,s),a=i(o,a),o.status);p+=1)s=o.index,l.push(o.value);return i(e(s,l),a)})},a.result=function(n){return this.map(function(e){return n})},a.atMost=function(n){return this.times(0,n)},a.atLeast=function(n){return p(this.times(n),this.many(),function(n,e){return n.concat(e)})},a.map=function(t){var r=this;return n(function(n,u){var s=r._(n,u);return s.status?i(e(s.index,t(s.value)),s):s})},a.skip=function(n){return l(this,n).map(function(n){return n[0]})},a.mark=function(){return p(v,this,v,function(n,e,t){return{start:n,value:e,end:t}})},a.desc=function(e){var t=this;return n(function(n,r){var i=t._(n,r);return i.status||(i.expected=[e]),i})};var f=(r.string=function(r){var i=r.length,u="'"+r+"'";return n(function(n,s){var o=n.slice(s,s+i);return o===r?e(s+i,o):t(s,u)})},r.regex=function(r,i){var u=RegExp("^(?:"+r.source+")",(""+r).slice((""+r).lastIndexOf("/")+1)),s=""+r;return null==i&&(i=0),n(function(n,r){var o=u.exec(n.slice(r));if(o){var a=o[0],l=o[i];if(null!=l)return e(r+a.length,l)}return t(r,s)})}),m=r.succeed=function(t){return n(function(n,r){return e(r,t)})},h=r.fail=function(e){return n(function(n,r){return t(r,e)})},d=(r.letter=f(/[a-z]/i).desc("a letter"),r.letters=f(/[a-z]*/i),r.digit=f(/[0-9]/).desc("a digit"),r.digits=f(/[0-9]*/),r.whitespace=f(/\s+/).desc("whitespace"),r.optWhitespace=f(/\s*/),r.any=n(function(n,r){return r>=n.length?t(r,"any character"):e(r+1,n.charAt(r))}),r.all=n(function(n,t){return e(n.length,n.slice(t))}),r.eof=n(function(n,r){return r=0})},r.noneOf=function(n){return x(function(e){return n.indexOf(e)<0})},r.takeWhile=function(t){return n(function(n,r){for(var i=r;i0&&(e+="GROUP BY ",e+=n.group.map(function(n){return n.expression}).join(", ")),e}function s(n){var e="";return n.order.length>0&&(e+="ORDER BY ",e+=n.order.map(function(n){return n.expression}).join(", ")),e}function o(n){var e="";return null!==n.limit&&(e+="LIMIT ",null!==n.limit.from&&(e+=n.limit.from+", "),e+=n.limit.nb),e}function a(n){return"INSERT INTO "+n.into.expression}function l(n){var e="",t=n.values.filter(function(n){return null!==n.target});return t.length>0&&(e+="(",e+=t.map(function(n){return n.target.expression}).join(", "),e+=") "),e+="VALUES (",e+=n.values.map(function(n){return n.value}).join(", "),e+=")"}function p(n){return"UPDATE "+n.table.expression}function c(n){var e="SET ";return e+=n.values.map(function(n){return n.target.expression+" = "+n.value}).join(", ")}if("object"!=typeof n||n.status!==!0)return!1;n=n.value;var f=[];if("select"===n.type)f.push(e(n)),f.push(t(n)),f.push(r(n)),f.push(i(n)),f.push(u(n)),f.push(s(n)),f.push(o(n));else if("insert"===n.type)f.push(a(n)),f.push(l(n));else if("update"===n.type)f.push(p(n)),f.push(c(n)),f.push(i(n));else{if("delete"!==n.type)return!1;f.push("DELETE"),f.push(t(n)),f.push(i(n))}return f.filter(function(n){return""!==n}).join(" ")}},{}],6:[function(n,e,t){"use strict";function r(n,e){return"undefined"==typeof e&&(e=[]),n.or(l.succeed(e))}function i(n){return n.join("")}function u(n){return n[0].push(n[1]),n[0]}function s(n){return p(n.skip(h).skip(m(",")).skip(h).many(),n.skip(h)).map(u)}function o(n){return n.replace(/^([`'"])(.*)\1$/,"$2")}function a(n){return p(l.index,n,l.index).map(function(n){var e={start:n[0],end:n[2]};if("object"==typeof n[1]){var t=n[1];return t.position=e,t}return e.out=n[1],e})}var l=n("parsimmon"),p=l.seq,c=l.alt,f=l.regex,m=l.string,h=l.optWhitespace,d=l.whitespace,x=l.lazy,v=c(f(/(?!(FROM|WHERE|GROUP BY|ORDER BY|LIMIT|INNER|LEFT|RIGHT|JOIN|ON|VALUES|SET)\s)[a-z*][a-z0-9_]*/i),f(/`[^`\\]*(?:\\.[^`\\]*)*`/)),g=c(f(/"[^"\\]*(?:\\.[^"\\]*)*"/),f(/'[^'\\]*(?:\\.[^'\\]*)*'/)),E=p(c(f(/[a-zA-Z0-9_]+\(/),m("(")),r(x(function(){return D})).map(i),m(")")).map(i),j=p(v,m("."),v),y=c(m("+"),m("-"),m("*"),m("/"),m("&&"),m("&"),m("~"),m("||"),m("|"),m("^"),f(/XOR/i),m("<=>"),m("="),m("!="),m(">="),m(">>"),m(">"),m("<="),m("<<"),m("<"),f(/IS NULL/i),f(/IS NOT/i),f(/IS NOT NULL/i),f(/IS/i),f(/LIKE/i),f(/NOT LIKE/i),f(/BETWEEN/i),f(/NOT BETWEEN/i),m("%"),f(/MOD/i),f(/NOT/i),f(/OR\s/i),f(/AND/i),f(/IN/i)),b=f(/[-]?\d+\.?\d*/),O=p(m("("),h,p(c(b,g),h,r(m(",")),h,r(c(b,g))).map(i),h,m(")")).map(i),k=p(c(j.map(function(n){return{expression:n.join(""),table:o(n[0]),column:o(n[2])}}),E.map(function(n){return{expression:n,table:null,column:null}}),v.map(function(n){return{expression:n,table:null,column:o(n)}}),g.map(function(n){return{expression:n,table:null,column:null}}),b.map(function(n){return{expression:n,table:null,column:null}}),O.map(function(n){return{expression:n,table:null,column:null}})),r(p(h,y,r(p(h,x(function(){return k}).map(function(n){return n.expression})).map(i),null)).map(i),null)).map(function(n){return null!==n[1]?(n[0]=n[0].expression,{expression:n.join(""),table:null,column:null}):n[0]}),_=p(k,r(p(h,r(f(/AS\s/i)),c(v,g)).map(function(n){var e={};return e.alias=o(n[2]),e.expression=n.join(""),e}),null)).map(function(n){var e=n[0];return e.alias=null!==n[1]?n[1].alias:null,e.expression=e.expression+(null!==n[1]?n[1].expression:""),e}),R=k.map(function(n){return n.expression}),I=p(c(j.map(i),v),r(p(h,r(f(/AS\s/i)),c(v,g)).map(function(n){return{alias:o(n[2]),expression:n.join("")}}),null)).map(function(n){var e={};return e.table=n[0],e.alias=null!==n[1]?n[1].alias:null,e.expression=n[0]+(null!==n[1]?n[1].expression:""),e}),N=p(r(p(f(/INNER|LEFT|RIGHT/i),d).map(function(n){return n[0].toLowerCase()}),null),f(/JOIN/i),h,a(I),h,f(/ON/i),h,a(k)).map(function(n){var e={};return e.type=n[0]||"inner",e.table=n[3].table,e.alias=n[3].alias,e.position=n[3].position,e.condition={expression:n[7].expression,position:n[7].position},e}),T=a(k).map(function(n){return{expression:n.expression,position:n.position}}),w=p(k,r(p(h,f(/ASC|DESC/i)),null)).map(function(n){return{expression:n[0].expression+(null!==n[1]?n[1].join(""):""),order:null!==n[1]?n[1][1]:"ASC",table:n[0].table,column:n[0].column}}),L=p(b,r(p(h,m(","),h,b),null)).map(function(n){return null===n[1]?{from:null,nb:parseInt(n[0],10)}:{from:parseInt(n[0],10),nb:parseInt(n[1][3],10)}}),S=c(j.map(function(n){return{expression:n.join(""),column:o(n[2])}}),v.map(function(n){return{expression:n,column:o(n)}})),A=k.map(function(n){return n.expression}),q=p(S,h,m("="),h,k).map(function(n){return{target:n[0],value:n[4].expression}}),D=p(p(R,h,m(","),h).map(i).many(),R.skip(h)).map(u),U=s(a(_)),M=s(a(I)),W=s(a(k)),z=s(a(w)),B=h.then(N).skip(h).many(),C=s(S),F=s(A),P=s(q),H=p(f(/SELECT/i).skip(h).then(r(U)),f(/FROM/i).skip(h).then(r(M)),r(B),r(f(/WHERE/i).skip(h).then(r(T)),null),r(f(/\s?GROUP BY/i).skip(h).then(r(W))),r(f(/\s?ORDER BY/i).skip(h).then(r(z))),r(f(/\s?LIMIT/i).skip(h).then(r(L)),null)).map(function(n){return{type:"select",select:n[0],from:n[1],join:n[2],where:n[3],group:n[4],order:n[5],limit:n[6]}}),G=p(f(/INSERT INTO/i).skip(h).then(I),h,r(p(m("("),C,m(")")).map(function(n){return n[1]})),h,f(/VALUES\s?\(/i).skip(h).then(F),m(")")).map(function(n){for(var e=[],t=Math.max(n[2].length,n[4].length),r=0;t>r;++r)e[r]={target:n[2][r]||null,value:n[4][r]||null};return{type:"insert",into:n[0],values:e}}),J=p(f(/UPDATE/i).skip(h).then(I),h,f(/SET/i).skip(h).then(P),h,r(f(/WHERE/i).skip(h).then(r(T)),null)).map(function(n){return{type:"update",table:n[0],values:n[2],where:n[4]}}),Y=p(f(/DELETE FROM/i).skip(h).then(r(M)),r(f(/WHERE/i).skip(h).then(r(T)),null)).map(function(n){return{type:"delete",from:n[0],where:n[1]}}),V=c(H,G,J,Y);e.exports=function(n){var e=V.parse(n);return e.status===!1&&(e.error=l.formatError(n,e)),e}},{parsimmon:3}]},{},[1])(1)});
\ No newline at end of file
diff --git a/dist/simpleSqlParser.withoutDeps.js b/dist/simpleSqlParser.withoutDeps.js
new file mode 100644
index 0000000..125fb58
--- /dev/null
+++ b/dist/simpleSqlParser.withoutDeps.js
@@ -0,0 +1,705 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.simpleSqlParser = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) {
+			result += 'GROUP BY ';
+			result += ast.group.map(function(item) {
+				return item.expression;
+			}).join(', ');
+		}
+		return result;
+	}
+
+	function order(ast) {
+		var result = '';
+		if (ast.order.length > 0) {
+			result += 'ORDER BY ';
+			result += ast.order.map(function(item) {
+				return item.expression;
+			}).join(', ');
+		}
+		return result;
+	}
+
+	function limit(ast) {
+		var result = '';
+		if (ast.limit !== null) {
+			result += 'LIMIT ';
+			if (ast.limit.from !== null) result += ast.limit.from + ', ';
+			result += ast.limit.nb;
+		}
+		return result;
+	}
+
+	function into(ast) {
+		return 'INSERT INTO ' + ast.into.expression;
+	}
+
+	function values(ast) {
+		var result = '';
+		var targets = ast.values.filter(function(item) {
+			return item.target !== null;
+		});
+		if (targets.length > 0) {
+			result += '(';
+			result += targets.map(function(item) {
+				return item.target.expression;
+			}).join(', ');
+			result += ') ';
+		}
+		result += 'VALUES (';
+		result += ast.values.map(function(item) {
+			return item.value;
+		}).join(', ');
+		result += ')';
+		return result;
+	}
+
+	function table(ast) {
+		return 'UPDATE ' + ast.table.expression;
+	}
+
+	function update(ast) {
+		var result = 'SET ';
+		result += ast.values.map(function(item) {
+			return item.target.expression + ' = ' + item.value;
+		}).join(', ');
+		return result;
+	}
+
+	var parts = [];
+	if (ast.type === 'select') {
+		parts.push(select(ast));
+		parts.push(from(ast));
+		parts.push(join(ast));
+		parts.push(where(ast));
+		parts.push(group(ast));
+		parts.push(order(ast));
+		parts.push(limit(ast));
+	}
+	else if (ast.type === 'insert') {
+		parts.push(into(ast));
+		parts.push(values(ast));
+	}
+	else if (ast.type === 'update') {
+		parts.push(table(ast));
+		parts.push(update(ast));
+		parts.push(where(ast));
+	}
+	else if (ast.type === 'delete') {
+		parts.push('DELETE');
+		parts.push(from(ast));
+		parts.push(where(ast));
+	}
+	else return false;
+
+	return parts.filter(function(item) {
+		return item !== '';
+	}).join(' ');
+};
+
+},{}],3:[function(require,module,exports){
+(function (global){
+"use strict";
+var Parsimmon = (typeof window !== "undefined" ? window.parsimmon : typeof global !== "undefined" ? global.parsimmon : null);
+
+/********************************************************************************************
+	ALIASES
+********************************************************************************************/
+
+var seq = Parsimmon.seq;
+var alt = Parsimmon.alt;
+var regex = Parsimmon.regex;
+var string = Parsimmon.string;
+var optWhitespace = Parsimmon.optWhitespace;
+var whitespace = Parsimmon.whitespace;
+var lazy = Parsimmon.lazy;
+
+
+
+/********************************************************************************************
+	COMMON PATTERNS
+********************************************************************************************/
+
+// Make a parser optionnal
+// "empty" parameter will be returned as result if the optionnal parser can't match
+function opt(parser, empty) {
+	if (typeof empty == 'undefined') empty = [];
+	return parser.or(Parsimmon.succeed(empty));
+}
+
+// Join results of a parser
+function mkString(node) {
+	return node.join('');
+}
+
+// Add an item to an optionnal list and return the final list
+function mergeOptionnalList(node) {
+	node[0].push(node[1]);
+	return node[0];
+}
+
+// Generate a parser that accept a comma-separated list of something
+function optionnalList(parser) {
+	return seq(
+		parser.skip(optWhitespace).skip(string(',')).skip(optWhitespace).many(),
+		parser.skip(optWhitespace)
+	).map(mergeOptionnalList);
+}
+
+// Remove first and last character of a string
+function removeQuotes(string) {
+	return string.replace(/^([`'"])(.*)\1$/, '$2');
+}
+
+// Add the starting and ending char positions of matches of a given parser
+function getPos(parser) {
+	return seq(
+		Parsimmon.index,
+		parser,
+		Parsimmon.index
+	).map(function(node) {
+		var pos = {
+			start: node[0],
+			end: node[2],
+		};
+		if (typeof node[1] == 'object') {
+			var n = node[1];
+			n.position = pos;
+			return n;
+		}
+		else {
+			pos.out = node[1];
+			return pos;
+		}
+	});
+}
+
+
+
+/********************************************************************************************
+	LOW LEVEL PARSERS
+********************************************************************************************/
+
+// The name of a column/table
+var colName = alt(
+	regex(/(?!(FROM|WHERE|GROUP BY|ORDER BY|LIMIT|INNER|LEFT|RIGHT|JOIN|ON|VALUES|SET)\s)[a-z*][a-z0-9_]*/i),
+	regex(/`[^`\\]*(?:\\.[^`\\]*)*`/)
+);
+
+// A string
+var str = alt(
+	regex(/"[^"\\]*(?:\\.[^"\\]*)*"/),
+	regex(/'[^'\\]*(?:\\.[^'\\]*)*'/)
+);
+
+// A function expression
+var func = seq(
+	alt(
+		regex(/[a-zA-Z0-9_]+\(/),
+		string('(')
+		),
+	opt(lazy(function() { return argList; })).map(mkString),
+	string(')')
+).map(mkString);
+
+// A table.column expression
+var tableAndColumn = seq(
+	colName,
+	string('.'),
+	colName
+);
+
+// An operator
+var operator = alt(
+	string('+'),
+	string('-'),
+	string('*'),
+	string('/'),
+	string('&&'),
+	string('&'),
+	string('~'),
+	string('||'),
+	string('|'),
+	string('^'),
+	regex(/XOR/i),
+	string('<=>'),
+	string('='),
+	string('!='),
+	string('>='),
+	string('>>'),
+	string('>'),
+	string('<='),
+	string('<<'),
+	string('<'),
+	regex(/IS NULL/i),
+	regex(/IS NOT/i),
+	regex(/IS NOT NULL/i),
+	regex(/IS/i),
+	regex(/LIKE/i),
+	regex(/NOT LIKE/i),
+	regex(/BETWEEN/i),
+	regex(/NOT BETWEEN/i),
+	string('%'),
+	regex(/MOD/i),
+	regex(/NOT/i),
+	regex(/OR\s/i),	// A space is forced after so this doesn't get mixed up with ORDER BY
+	regex(/AND/i),
+	regex(/IN/i)
+);
+
+// A number
+var number = regex(/[-]?\d+\.?\d*/);
+
+
+
+/********************************************************************************************
+	EXPRESSION PARSERS
+********************************************************************************************/
+
+// List (following IN, for example)
+var list = seq(
+	string('('),
+	optWhitespace,
+	seq(
+		alt(
+			number,
+			str
+		),
+		optWhitespace,
+		opt(string(',')),
+		optWhitespace,
+		opt(
+			alt(
+				number,
+				str
+			)
+		)
+	).map(mkString),
+	optWhitespace,
+	string(')')
+).map(mkString);
+
+// Expression
+var expression = seq(
+	alt(
+		tableAndColumn.map(function(node) {
+			return {
+				expression: node.join(''),
+				table: removeQuotes(node[0]),
+				column: removeQuotes(node[2])
+			};
+		}),
+		func.map(function(node) {
+			return {
+				expression: node,
+				table: null,
+				column: null
+			};
+		}),
+		colName.map(function(node) {
+			return {
+				expression: node,
+				table: null,
+				column: removeQuotes(node)
+			};
+		}),
+		str.map(function(node) {
+			return {
+				expression: node,
+				table: null,
+				column: null
+			};
+		}),
+		number.map(function(node) {
+			return {
+				expression: node,
+				table: null,
+				column: null
+			};
+		}),
+		list.map(function(node) {
+			return {
+				expression: node,
+				table: null,
+				column: null
+			};
+		})
+	),
+	opt(seq(
+		optWhitespace,
+		operator,
+		opt(seq(
+			optWhitespace,
+			lazy(function() { return expression; }).map(function(node) {
+				return node.expression;
+			})
+		).map(mkString), null)
+	).map(mkString), null)
+).map(function(node) {
+	if (node[1] !== null) {
+		node[0] = node[0].expression;
+		return {
+			expression: node.join(''),
+			table: null,
+			column: null
+		};
+	}
+	else return node[0];
+});
+
+// Expression following a SELECT statement
+var colListExpression = seq(
+	expression,
+	opt(	// Alias
+		seq(
+			optWhitespace,
+			opt(regex(/AS\s/i)),
+			alt(colName, str)
+		).map(function(node) {
+			var n = {};
+			n.alias = removeQuotes(node[2]);
+			n.expression = node.join('');
+			return n;
+		}),
+		null
+	)
+).map(function(node) {
+	var n = node[0];
+	n.alias = (node[1] !== null) ? node[1].alias : null;
+	n.expression = n.expression + ((node[1] !== null) ? node[1].expression : '');
+	return n;
+});
+
+// Expression inside a function
+var argListExpression = expression.map(function(node) {
+	return node.expression;
+});
+
+// Expression following a FROM statement
+var tableListExpression = seq(
+	alt(
+		tableAndColumn.map(mkString),
+		colName
+	),
+	opt(	// Alias
+		seq(
+			optWhitespace,
+			opt(regex(/AS\s/i)),
+			alt(colName, str)
+		).map(function(node) {
+			return {
+				alias: removeQuotes(node[2]),
+				expression : node.join(''),
+			};
+		}),
+		null
+	)
+).map(function(node) {
+	var n = {};
+	n.table = node[0];
+	n.alias = (node[1] !== null) ? node[1].alias : null;
+	n.expression = node[0] + ((node[1] !== null) ? node[1].expression : '');
+	return n;
+});
+
+// JOIN expression (including JOIN statements)
+var joinExpression = seq(
+	opt(seq(
+		regex(/INNER|LEFT|RIGHT/i),
+		whitespace
+	).map(function(node) {
+		return node[0].toLowerCase();
+	}), null),
+	regex(/JOIN/i),
+	optWhitespace,
+	getPos(tableListExpression),
+	optWhitespace,
+	regex(/ON/i),
+	optWhitespace,
+	getPos(expression)
+).map(function(node) {
+	var n = {};
+	n.type = node[0] || 'inner';
+	n.table = node[3].table;
+	n.alias = node[3].alias;
+	n.position = node[3].position;
+	n.condition = {
+		expression: node[7].expression,
+		position: node[7].position,
+	};
+	return n;
+});
+
+// Expression following a WHERE statement
+var whereExpression = getPos(expression).map(function(node) {
+	return {
+		expression: node.expression,
+		position: node.position,
+	};
+});
+
+// Expression following an ORDER BY statement
+var orderListExpression = seq(
+	expression,
+	opt(seq(
+		optWhitespace,
+		regex(/ASC|DESC/i)
+	), null)
+).map(function(node) {
+	return {
+		expression: node[0].expression + ((node[1] !== null) ? node[1].join('') : ''),
+		order: (node[1] !== null) ? node[1][1] : 'ASC',
+		table: node[0].table,
+		column: node[0].column,
+	};
+});
+
+// Expression following a LIMIT statement
+var limitExpression = seq(
+	number,
+	opt(seq(
+		optWhitespace,
+		string(','),
+		optWhitespace,
+		number
+	), null)
+).map(function(node) {
+	if (node[1] === null) return {
+		from: null,
+		nb: parseInt(node[0], 10),
+	};
+	else return {
+		from: parseInt(node[0], 10),
+		nb: parseInt(node[1][3], 10),
+	};
+});
+
+// Expression designating a column before VALUES in INSERT query
+var insertColListExpression = alt(
+	tableAndColumn.map(function(node) {
+		return {
+			expression: node.join(''),
+			column: removeQuotes(node[2])
+		};
+	}),
+	colName.map(function(node) {
+		return {
+			expression: node,
+			column: removeQuotes(node)
+		};
+	})
+);
+
+// Expression following a VALUES statement
+var valueExpression = expression.map(function(node) {
+	return node.expression;
+});
+
+// Expression that assign a value to a column
+var assignExpression = seq(
+	insertColListExpression,
+	optWhitespace,
+	string('='),
+	optWhitespace,
+	expression
+).map(function(node) {
+	return {
+		target: node[0],
+		value: node[4].expression,
+	};
+});
+
+
+
+/********************************************************************************************
+	HIGH LEVEL PARSERS
+********************************************************************************************/
+
+// List of arguments inside a function
+var argList = seq(
+	seq(argListExpression, optWhitespace, string(','), optWhitespace).map(mkString).many(),
+	argListExpression.skip(optWhitespace)
+).map(mergeOptionnalList);
+
+// List of expressions following a SELECT statement
+var colList = optionnalList(getPos(colListExpression));
+
+// List of table following a FROM statement
+var tableList = optionnalList(getPos(tableListExpression));
+
+// List of table following an GROUP BY statement
+var groupList = optionnalList(getPos(expression));
+
+// List of table following an ORDER BY statement
+var orderList = optionnalList(getPos(orderListExpression));
+
+// List of joins (including JOIN statements)
+var joinList = optWhitespace.then(joinExpression).skip(optWhitespace).many();
+
+// List of columns before VALUES in INSERT query
+var insertColList = optionnalList(insertColListExpression);
+
+// List of values following a VALUES statement
+var valuesList = optionnalList(valueExpression);
+
+// List of assign expression following a SET statement
+var assignList = optionnalList(assignExpression);
+
+
+
+/********************************************************************************************
+	MAIN PARSERS
+********************************************************************************************/
+
+// SELECT parser
+var selectParser = seq(
+	regex(/SELECT/i).skip(optWhitespace).then(opt(colList)),
+	regex(/FROM/i).skip(optWhitespace).then(opt(tableList)),
+	opt(joinList),
+	opt(regex(/WHERE/i).skip(optWhitespace).then(opt(whereExpression)), null),
+	opt(regex(/\s?GROUP BY/i).skip(optWhitespace).then(opt(groupList))),
+	opt(regex(/\s?ORDER BY/i).skip(optWhitespace).then(opt(orderList))),
+	opt(regex(/\s?LIMIT/i).skip(optWhitespace).then(opt(limitExpression)), null)
+).map(function(node) {
+	return {
+		type: 'select',
+		select: node[0],
+		from: node[1],
+		join: node[2],
+		where: node[3],
+		group: node[4],
+		order: node[5],
+		limit: node[6],
+	};
+});
+
+// INSERT parser
+var insertParser = seq(
+	regex(/INSERT INTO/i).skip(optWhitespace).then(tableListExpression),
+	optWhitespace,
+	opt(
+		seq(
+			string('('),
+			insertColList,
+			string(')')
+		).map(function(node) {
+			return node[1];
+		})
+	),
+	optWhitespace,
+	regex(/VALUES\s?\(/i).skip(optWhitespace).then(valuesList),
+	string(')')
+).map(function(node) {
+	var values = [];
+	var bigger = Math.max(node[2].length, node[4].length);
+
+	for (var i = 0; i < bigger; ++i) {
+		values[i] = {
+			target: node[2][i] || null,
+			value: node[4][i] || null,
+		};
+	}
+
+	return {
+		type: 'insert',
+		into: node[0],
+		values: values,
+	};
+});
+
+// UPDATE parser
+var updateParser = seq(
+	regex(/UPDATE/i).skip(optWhitespace).then(tableListExpression),
+	optWhitespace,
+	regex(/SET/i).skip(optWhitespace).then(assignList),
+	optWhitespace,
+	opt(regex(/WHERE/i).skip(optWhitespace).then(opt(whereExpression)), null)
+).map(function(node) {
+	return {
+		type: 'update',
+		table: node[0],
+		values: node[2],
+		where: node[4],
+	};
+});
+
+// DELETE parser
+var deleteParser = seq(
+	regex(/DELETE FROM/i).skip(optWhitespace).then(opt(tableList)),
+	opt(regex(/WHERE/i).skip(optWhitespace).then(opt(whereExpression)), null)
+).map(function(node) {
+	return {
+		type: 'delete',
+		from: node[0],
+		where: node[1],
+	};
+});
+
+// Main parser
+var p = alt(selectParser, insertParser, updateParser, deleteParser);
+
+
+
+/********************************************************************************************
+	PUBLIC FUNCTIONS
+********************************************************************************************/
+
+module.exports = function(sql) {
+	var result = p.parse(sql);
+	if (result.status === false) result.error = Parsimmon.formatError(sql, result);
+	return result;
+};
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}]},{},[1])(1)
+});
\ No newline at end of file
diff --git a/dist/simpleSqlParser.withoutDeps.min.js b/dist/simpleSqlParser.withoutDeps.min.js
new file mode 100644
index 0000000..9f4a949
--- /dev/null
+++ b/dist/simpleSqlParser.withoutDeps.min.js
@@ -0,0 +1 @@
+!function(n){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.simpleSqlParser=n()}}(function(){return function n(e,t,r){function i(o,s){if(!t[o]){if(!e[o]){var l="function"==typeof require&&require;if(!s&&l)return l(o,!0);if(u)return u(o,!0);var p=new Error("Cannot find module '"+o+"'");throw p.code="MODULE_NOT_FOUND",p}var a=t[o]={exports:{}};e[o][0].call(a.exports,function(n){var t=e[o][1][n];return i(t?t:n)},a,a.exports,n,e,t,r)}return t[o].exports}for(var u="function"==typeof require&&require,o=0;o0&&(e+="GROUP BY ",e+=n.group.map(function(n){return n.expression}).join(", ")),e}function o(n){var e="";return n.order.length>0&&(e+="ORDER BY ",e+=n.order.map(function(n){return n.expression}).join(", ")),e}function s(n){var e="";return null!==n.limit&&(e+="LIMIT ",null!==n.limit.from&&(e+=n.limit.from+", "),e+=n.limit.nb),e}function l(n){return"INSERT INTO "+n.into.expression}function p(n){var e="",t=n.values.filter(function(n){return null!==n.target});return t.length>0&&(e+="(",e+=t.map(function(n){return n.target.expression}).join(", "),e+=") "),e+="VALUES (",e+=n.values.map(function(n){return n.value}).join(", "),e+=")"}function a(n){return"UPDATE "+n.table.expression}function f(n){var e="SET ";return e+=n.values.map(function(n){return n.target.expression+" = "+n.value}).join(", ")}if("object"!=typeof n||n.status!==!0)return!1;n=n.value;var c=[];if("select"===n.type)c.push(e(n)),c.push(t(n)),c.push(r(n)),c.push(i(n)),c.push(u(n)),c.push(o(n)),c.push(s(n));else if("insert"===n.type)c.push(l(n)),c.push(p(n));else if("update"===n.type)c.push(a(n)),c.push(f(n)),c.push(i(n));else{if("delete"!==n.type)return!1;c.push("DELETE"),c.push(t(n)),c.push(i(n))}return c.filter(function(n){return""!==n}).join(" ")}},{}],3:[function(n,e,t){(function(n){"use strict";function t(n,e){return"undefined"==typeof e&&(e=[]),n.or(l.succeed(e))}function r(n){return n.join("")}function i(n){return n[0].push(n[1]),n[0]}function u(n){return p(n.skip(m).skip(c(",")).skip(m).many(),n.skip(m)).map(i)}function o(n){return n.replace(/^([`'"])(.*)\1$/,"$2")}function s(n){return p(l.index,n,l.index).map(function(n){var e={start:n[0],end:n[2]};if("object"==typeof n[1]){var t=n[1];return t.position=e,t}return e.out=n[1],e})}var l="undefined"!=typeof window?window.parsimmon:"undefined"!=typeof n?n.parsimmon:null,p=l.seq,a=l.alt,f=l.regex,c=l.string,m=l.optWhitespace,d=l.whitespace,x=l.lazy,E=a(f(/(?!(FROM|WHERE|GROUP BY|ORDER BY|LIMIT|INNER|LEFT|RIGHT|JOIN|ON|VALUES|SET)\s)[a-z*][a-z0-9_]*/i),f(/`[^`\\]*(?:\\.[^`\\]*)*`/)),h=a(f(/"[^"\\]*(?:\\.[^"\\]*)*"/),f(/'[^'\\]*(?:\\.[^'\\]*)*'/)),v=p(a(f(/[a-zA-Z0-9_]+\(/),c("(")),t(x(function(){return D})).map(r),c(")")).map(r),y=p(E,c("."),E),I=a(c("+"),c("-"),c("*"),c("/"),c("&&"),c("&"),c("~"),c("||"),c("|"),c("^"),f(/XOR/i),c("<=>"),c("="),c("!="),c(">="),c(">>"),c(">"),c("<="),c("<<"),c("<"),f(/IS NULL/i),f(/IS NOT/i),f(/IS NOT NULL/i),f(/IS/i),f(/LIKE/i),f(/NOT LIKE/i),f(/BETWEEN/i),f(/NOT BETWEEN/i),c("%"),f(/MOD/i),f(/NOT/i),f(/OR\s/i),f(/AND/i),f(/IN/i)),N=f(/[-]?\d+\.?\d*/),O=p(c("("),m,p(a(N,h),m,t(c(",")),m,t(a(N,h))).map(r),m,c(")")).map(r),T=p(a(y.map(function(n){return{expression:n.join(""),table:o(n[0]),column:o(n[2])}}),v.map(function(n){return{expression:n,table:null,column:null}}),E.map(function(n){return{expression:n,table:null,column:o(n)}}),h.map(function(n){return{expression:n,table:null,column:null}}),N.map(function(n){return{expression:n,table:null,column:null}}),O.map(function(n){return{expression:n,table:null,column:null}})),t(p(m,I,t(p(m,x(function(){return T}).map(function(n){return n.expression})).map(r),null)).map(r),null)).map(function(n){return null!==n[1]?(n[0]=n[0].expression,{expression:n.join(""),table:null,column:null}):n[0]}),R=p(T,t(p(m,t(f(/AS\s/i)),a(E,h)).map(function(n){var e={};return e.alias=o(n[2]),e.expression=n.join(""),e}),null)).map(function(n){var e=n[0];return e.alias=null!==n[1]?n[1].alias:null,e.expression=e.expression+(null!==n[1]?n[1].expression:""),e}),b=T.map(function(n){return n.expression}),j=p(a(y.map(r),E),t(p(m,t(f(/AS\s/i)),a(E,h)).map(function(n){return{alias:o(n[2]),expression:n.join("")}}),null)).map(function(n){var e={};return e.table=n[0],e.alias=null!==n[1]?n[1].alias:null,e.expression=n[0]+(null!==n[1]?n[1].expression:""),e}),g=p(t(p(f(/INNER|LEFT|RIGHT/i),d).map(function(n){return n[0].toLowerCase()}),null),f(/JOIN/i),m,s(j),m,f(/ON/i),m,s(T)).map(function(n){var e={};return e.type=n[0]||"inner",e.table=n[3].table,e.alias=n[3].alias,e.position=n[3].position,e.condition={expression:n[7].expression,position:n[7].position},e}),w=s(T).map(function(n){return{expression:n.expression,position:n.position}}),L=p(T,t(p(m,f(/ASC|DESC/i)),null)).map(function(n){return{expression:n[0].expression+(null!==n[1]?n[1].join(""):""),order:null!==n[1]?n[1][1]:"ASC",table:n[0].table,column:n[0].column}}),S=p(N,t(p(m,c(","),m,N),null)).map(function(n){return null===n[1]?{from:null,nb:parseInt(n[0],10)}:{from:parseInt(n[0],10),nb:parseInt(n[1][3],10)}}),k=a(y.map(function(n){return{expression:n.join(""),column:o(n[2])}}),E.map(function(n){return{expression:n,column:o(n)}})),q=T.map(function(n){return n.expression}),A=p(k,m,c("="),m,T).map(function(n){return{target:n[0],value:n[4].expression}}),D=p(p(b,m,c(","),m).map(r).many(),b.skip(m)).map(i),U=u(s(R)),M=u(s(j)),B=u(s(T)),F=u(s(L)),H=m.then(g).skip(m).many(),W=u(k),C=u(q),G=u(A),P=p(f(/SELECT/i).skip(m).then(t(U)),f(/FROM/i).skip(m).then(t(M)),t(H),t(f(/WHERE/i).skip(m).then(t(w)),null),t(f(/\s?GROUP BY/i).skip(m).then(t(B))),t(f(/\s?ORDER BY/i).skip(m).then(t(F))),t(f(/\s?LIMIT/i).skip(m).then(t(S)),null)).map(function(n){return{type:"select",select:n[0],from:n[1],join:n[2],where:n[3],group:n[4],order:n[5],limit:n[6]}}),Y=p(f(/INSERT INTO/i).skip(m).then(j),m,t(p(c("("),W,c(")")).map(function(n){return n[1]})),m,f(/VALUES\s?\(/i).skip(m).then(C),c(")")).map(function(n){for(var e=[],t=Math.max(n[2].length,n[4].length),r=0;t>r;++r)e[r]={target:n[2][r]||null,value:n[4][r]||null};return{type:"insert",into:n[0],values:e}}),J=p(f(/UPDATE/i).skip(m).then(j),m,f(/SET/i).skip(m).then(G),m,t(f(/WHERE/i).skip(m).then(t(w)),null)).map(function(n){return{type:"update",table:n[0],values:n[2],where:n[4]}}),z=p(f(/DELETE FROM/i).skip(m).then(t(M)),t(f(/WHERE/i).skip(m).then(t(w)),null)).map(function(n){return{type:"delete",from:n[0],where:n[1]}}),_=a(P,Y,J,z);e.exports=function(n){var e=_.parse(n);return e.status===!1&&(e.error=l.formatError(n,e)),e}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[1])(1)});
\ No newline at end of file
diff --git a/example.html b/example.html
deleted file mode 100644
index 885ddc3..0000000
--- a/example.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-	
-		simpleSqlParser
-		
-	
-	
-		

Check console to see the output

- - - - diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..3808d2b --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,58 @@ +var gulp = require('gulp'); +var jshint = require('gulp-jshint'); +var mocha = require('gulp-mocha'); +var uglify = require('gulp-uglify'); +var rename = require('gulp-rename'); +var source = require('vinyl-source-stream'); +var browserify = require('browserify'); + +gulp.task('lint', function() { + return gulp.src(['*.js', 'src/*.js', 'tests/*.js']) + .pipe(jshint()) + .pipe(jshint.reporter('default')) + .pipe(jshint.reporter('fail')); +}); + +gulp.task('mocha', function() { + return gulp.src('./tests/test*.js') + .pipe(mocha({ + ui: 'qunit', + reporter: 'spec', + })); +}); + +gulp.task('test', ['lint', 'mocha']); + +gulp.task('browserifyWithDeps', function() { + var bundler = browserify({ standalone: 'simpleSqlParser', entries: './index.js' }); + return bundler + .bundle() + .pipe(source('simpleSqlParser.js')) + .pipe(gulp.dest('./dist')); +}); + +gulp.task('browserifyWithoutDeps', function() { + var bundler = browserify({ standalone: 'simpleSqlParser', entries: './index.js' }); + bundler.exclude('Parsimmon'); + bundler.transform('browserify-shim'); + return bundler + .bundle() + .pipe(source('simpleSqlParser.withoutDeps.js')) + .pipe(gulp.dest('./dist')); +}); + +gulp.task('uglifyWithDeps', ['browserifyWithDeps'], function() { + return gulp.src('dist/simpleSqlParser.js') + .pipe(uglify()) + .pipe(rename('simpleSqlParser.min.js')) + .pipe(gulp.dest('./dist')); +}); + +gulp.task('uglifyWithoutDeps', ['browserifyWithoutDeps'], function() { + return gulp.src('dist/simpleSqlParser.withoutDeps.js') + .pipe(uglify()) + .pipe(rename('simpleSqlParser.withoutDeps.min.js')) + .pipe(gulp.dest('./dist')); +}); + +gulp.task('default', ['test', 'uglifyWithDeps', 'uglifyWithoutDeps']); diff --git a/index.js b/index.js new file mode 100644 index 0000000..53b68c9 --- /dev/null +++ b/index.js @@ -0,0 +1,4 @@ +"use strict"; + +module.exports.sql2ast = require('./src/sql2ast.js'); +module.exports.ast2sql = require('./src/ast2sql.js'); diff --git a/package.json b/package.json index 635fab9..342f5d8 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "simple-sql-parser", "description": "Javascript library to parse CRUD (Create Retrieve Update Delete) SQL queries.", - "version": "1.3.0", - "main": "simpleSqlParser.js", + "version": "2.0.0-alpha.1", + "main": "index.js", "keywords": [ "sql", "parse", @@ -13,12 +13,6 @@ "email": "david.sferruzza@gmail.com", "url": "https://github.com/dsferruzza" }, - "contributors": [ - { - "name": "RienNeVaPlus", - "url": "https://github.com/RienNeVaPlus" - } - ], "maintainers": [ { "name": "David Sferruzza", @@ -35,14 +29,21 @@ }, "license": "MIT", "homepage": "https://github.com/dsferruzza/simpleSqlParser", + "dependencies": { + "parsimmon": "0.7.0" + }, "devDependencies": { - "qunitjs": "*", - "grunt": "*", - "grunt-contrib-qunit": "*", - "grunt-contrib-jshint": "*", - "grunt-contrib-watch": "*" + "browserify": "10.2.3", + "browserify-shim": "3.8.7", + "chai": "2.3.0", + "gulp": "3.8.11", + "gulp-jshint": "1.11.0", + "gulp-mocha": "2.1.0", + "gulp-rename": "1.2.2", + "gulp-uglify": "1.2.0", + "vinyl-source-stream": "1.1.0" }, - "scripts": { - "test": "grunt" + "browserify-shim": { + "parsimmon": "global:parsimmon" } } diff --git a/simpleSqlParser.js b/simpleSqlParser.js deleted file mode 100644 index fe6db2a..0000000 --- a/simpleSqlParser.js +++ /dev/null @@ -1,768 +0,0 @@ -(function(exports) { - "use strict"; - - function trim(str) { - if (typeof str == 'string') return str.replace(/^\s+/g,'').replace(/\s+$/g,''); - else return str; - } - - // Split a string using a separator, only if this separator isn't beetween brackets - function protect_split(separator, str) { - var sep = '######'; - - var string = false; - var nb_brackets = 0; - var new_str = ""; - for (var i = 0 ; i < str.length ; i++) { - if (!string && /['"`]/.test(str[i])) string = str[i]; - else if (string && str[i] == string) string = false; - else if (!string && str[i] == '(') nb_brackets ++; - else if (!string && str[i] == ')') nb_brackets --; - - if (str[i] == separator && (nb_brackets > 0 || string)) new_str += sep; - else new_str += str[i]; - } - str = new_str; - - str = str.split(separator); - str = str.map(function (item) { - return trim(item.replace(new RegExp(sep, 'g'), separator)); - }); - - return str; - } - - // Add some # inside a string to avoid it to match a regex/split - function protect(str) { - var result = '#'; - var length = str.length; - for (var i = 0 ; i < length ; i++) result += str[i] + "#"; - return result; - } - - // Restore a string output by protect() to its original state - function unprotect(str) { - var result = ''; - var length = str.length; - for (var i = 1 ; i < length ; i = i + 2) result += str[i]; - return result; - } - - - // Parse a query - // parseCond: (bool) parse conditions in WHERE and JOIN (default true) - exports.sql2ast = function (query, parseCond) { - if (typeof parseCond == 'undefined' || parseCond === null) parseCond = true; - - // Remove semi-colons and keep only the first query - var semi_colon = '###semi-colon###'; - query = query.replace(/[("'`].*;.*[)"'`]/g, function (match) { - return match.replace(/;/g, semi_colon); - }); - var eor = '###EOR###'; - query = query.replace(/;/g, eor); - query = query.split(eor)[0]; - query = query.replace(new RegExp(semi_colon, 'g'), ';'); - - // Define which words can act as separator - var keywords = ['SELECT', 'FROM', 'DELETE FROM', 'INSERT INTO', 'UPDATE', 'JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'ORDER BY', 'GROUP BY', 'HAVING', 'WHERE', 'LIMIT', 'VALUES', 'SET']; - var parts_name = keywords.map(function (item) { - return item + ' '; - }); - parts_name = parts_name.concat(keywords.map(function (item) { - return item + '('; - })); - parts_name = parts_name.concat(parts_name.map(function (item) { - return item.toLowerCase(); - })); - var parts_name_escaped = parts_name.map(function (item) { - return item.replace('(', '[\\(]'); - }); - - // Hide words defined as separator but written inside brackets in the query - query = query.replace(/\((.+?)\)|"(.+?)"|'(.+?)'|`(.+?)`/gi, function (match) { - return match.replace(new RegExp(parts_name_escaped.join('|'), 'gi'), protect); - }); - - // Write the position(s) in query of these separators - var parts_order = []; - function realNameCallback(match, name) { - return name; - } - parts_name.forEach(function (item) { - var pos = 0; - var part; - - do { - part = query.indexOf(item, pos); - if (part != -1) { - var realName = item.replace(/^((\w|\s)+?)\s?\(?$/i, realNameCallback); - parts_order[part] = realName; // Position won't be exact because the use of protect() (above) and unprotect() alter the query string ; but we just need the order :) - pos = part + realName.length; - } - } - while (part != -1); - }); - - // Delete duplicates (caused, for example, by JOIN and INNER JOIN) - var busy_until = 0; - parts_order.forEach(function (item, key) { - if (busy_until > key) delete parts_order[key]; - else { - busy_until = parseInt(key, 10) + item.length; - - // Replace JOIN by INNER JOIN - if (item == 'JOIN') parts_order[key] = 'INNER JOIN'; - } - }); - - // Generate protected word list to reverse the use of protect() - var words = parts_name_escaped.slice(0); - words = words.map(function (item) { - return protect(item); - }); - words = words.join('|'); - - // Split parts - var parts = query.split(new RegExp(parts_name_escaped.join('|'), 'i')); - - // Unhide words precedently hidden with protect() - query = query.replace(/\((.+?)\)|"(.+?)"|'(.+?)'|`(.+?)`/gi, function (match) { - return match.replace(new RegExp(words, 'gi'), unprotect); - }); - parts = parts.map(function (item) { - return item.replace(/\((.+?)\)|"(.+?)"|'(.+?)'|`(.+?)`/gi, function (match) { - return match.replace(new RegExp(words, 'gi'), unprotect); - }); - }); - - // Define analysis functions - var analysis = []; - - analysis['SELECT'] = function (str) { - var result = protect_split(',', str); - result = result.filter(function(item) { - return item !== ''; - }).map(function(item) { - return {name: item}; - }); - return result; - }; - - analysis['SET'] = function (str) { - var result = protect_split(',', str); - result = result.filter(function(item) { - return item !== ''; - }).map(function(item) { - return {expression: item}; - }); - return result; - }; - - analysis['FROM'] = analysis['DELETE FROM'] = analysis['UPDATE'] = function (str) { - var result = str.split(','); - result = result.map(function(item) { - return trim(item); - }); - result.forEach(function(item, key) { - if (item === '') result.splice(key); - }); - result = result.map(function(item) { - var table = item.split(' AS '); - var alias = table[1] || ''; - if (alias.indexOf('"') === 0 && alias.lastIndexOf('"') == alias.length - 1) alias = alias.substring(1, alias.length - 1); - return {table: table[0], as: alias}; - }); - return result; - }; - - analysis['LEFT JOIN'] = analysis['JOIN'] = analysis['INNER JOIN'] = analysis['RIGHT JOIN'] = function (str) { - str = str.split(' ON '); - var table = str[0].split(' AS '); - var result = {}; - result['table'] = trim(table[0]); - result['as'] = trim(table[1]) || ''; - result['cond'] = trim(str[1]); - - return result; - }; - - analysis['WHERE'] = function (str) { - return trim(str); - }; - - analysis['ORDER BY'] = function (str) { - str = str.split(','); - var result = []; - str.forEach(function (item, key) { - var order_by = /([A-Za-z0-9_\.]+)\s*(ASC|DESC){0,1}/gi; - order_by = order_by.exec(item); - if (order_by !== null) { - var tmp = {}; - tmp['column'] = trim(order_by[1]); - tmp['order'] = trim(order_by[2]); - if(order_by[2] === undefined ){ - tmp['order']="ASC"; - } - result.push(tmp); - } - }); - return result; - }; - - analysis['GROUP BY'] = function (str) { - str = str.split(','); - var result = []; - str.forEach(function (item, key) { - var group_by = /([A-Za-z0-9_\.]+)/gi; - group_by = group_by.exec(item); - if (group_by !== null) { - var tmp = {}; - tmp['column'] = trim(group_by[1]); - result.push(tmp); - } - }); - return result; - }; - analysis['LIMIT'] = function (str) { - var limit = /((\d+)\s*,\s*)?(\d+)/gi; - limit = limit.exec(str); - if (typeof limit[2] == 'undefined') limit[2] = 0; - var result = {}; - result['nb'] = parseInt(trim(limit[3]), 10); - result['from'] = parseInt(trim(limit[2]), 10); - return result; - }; - - analysis['INSERT INTO'] = function (str) { - var insert = /([A-Za-z0-9_\.]+)\s*(\(([A-Za-z0-9_\., ]+)\))?/gi; - insert = insert.exec(str); - var result = {}; - result['table'] = trim(insert[1]); - if (typeof insert[3] != 'undefined') { - result['columns'] = insert[3].split(','); - result['columns'] = result['columns'].map(function (item) { - return trim(item); - }); - } - return result; - }; - - analysis['VALUES'] = function (str) { - str = trim(str); - if (str[0] != '(') str = '(' + str; // If query has "VALUES(...)" instead of "VALUES (...)" - var groups = protect_split(',', str); - var result = []; - groups.forEach(function(group) { - group = group.replace(/^\(/g,'').replace(/\)$/g,''); - group = protect_split(',', group); - result.push(group); - }); - return result; - }; - - // TODO: handle HAVING - - // Analyze parts - var result = {}; - var j = 0; - parts_order.forEach(function (item, key) { - item = item.toUpperCase(); - j++; - if (typeof analysis[item] != 'undefined') { - var part_result = analysis[item](parts[j]); - - if (typeof result[item] != 'undefined') { - if (typeof result[item] == 'string' || typeof result[item][0] == 'undefined') { - var tmp = result[item]; - result[item] = []; - result[item].push(tmp); - } - - result[item].push(part_result); - } - else result[item] = part_result; - } - else console.log('Can\'t analyze statement "' + item + '"'); - }); - - // Reorganize joins - if (typeof result['LEFT JOIN'] != 'undefined') { - if (typeof result['JOIN'] == 'undefined') result['JOIN'] = []; - if (typeof result['LEFT JOIN'][0] != 'undefined') { - result['LEFT JOIN'].forEach(function (item) { - item.type = 'left'; - result['JOIN'].push(item); - }); - } - else { - result['LEFT JOIN'].type = 'left'; - result['JOIN'].push(result['LEFT JOIN']); - } - delete result['LEFT JOIN']; - } - if (typeof result['INNER JOIN'] != 'undefined') { - if (typeof result['JOIN'] == 'undefined') result['JOIN'] = []; - if (typeof result['INNER JOIN'][0] != 'undefined') { - result['INNER JOIN'].forEach(function (item) { - item.type = 'inner'; - result['JOIN'].push(item); - }); - } - else { - result['INNER JOIN'].type = 'inner'; - result['JOIN'].push(result['INNER JOIN']); - } - delete result['INNER JOIN']; - } - if (typeof result['RIGHT JOIN'] != 'undefined') { - if (typeof result['JOIN'] == 'undefined') result['JOIN'] = []; - if (typeof result['RIGHT JOIN'][0] != 'undefined') { - result['RIGHT JOIN'].forEach(function (item) { - item.type = 'right'; - result['JOIN'].push(item); - }); - } - else { - result['RIGHT JOIN'].type = 'right'; - result['JOIN'].push(result['RIGHT JOIN']); - } - delete result['RIGHT JOIN']; - } - - // Parse conditions - if (parseCond) { - if (typeof result['WHERE'] == 'string') { - result['WHERE'] = CondParser.parse(result['WHERE']); - } - if (typeof result['JOIN'] != 'undefined') { - result['JOIN'].forEach(function (item, key) { - result['JOIN'][key]['cond'] = CondParser.parse(item['cond']); - }); - } - } - - return result; - }; - - - /* - * LEXER & PARSER FOR SQL CONDITIONS - * Inspired by https://github.com/DmitrySoshnikov/Essentials-of-interpretation - */ - - // Constructor - function CondLexer(source) { - this.source = source; - this.cursor = 0; - this.currentChar = ""; - - this.readNextChar(); - } - - CondLexer.prototype = { - constructor: CondLexer, - - // Read the next character (or return an empty string if cursor is at the end of the source) - readNextChar: function () { - if (typeof this.source != 'string') this.currentChar = ""; - else this.currentChar = this.source[this.cursor++] || ""; - }, - - // Determine the next token - readNextToken: function () { - if (/\w/.test(this.currentChar)) return this.readWord(); - if (/["'`]/.test(this.currentChar)) return this.readString(); - if (/[()]/.test(this.currentChar)) return this.readGroupSymbol(); - if (/[!=<>]/.test(this.currentChar)) return this.readOperator(); - - if (this.currentChar === "") return {type: 'eot', value: ''}; - else { - this.readNextChar(); - return {type: 'empty', value: ''}; - } - }, - - readWord: function () { - var tokenValue = ""; - var nb_brackets = 0; - var string = false; - while (/./.test(this.currentChar)) { - // Check if we are in a string - if (!string && /['"`]/.test(this.currentChar)) string = this.currentChar; - else if (string && this.currentChar == string) string = false; - else { - // Allow spaces inside functions (only if we are not in a string) - if (!string) { - // Token is finished if there is a closing bracket outside a string and with no opening - if (this.currentChar == ')' && nb_brackets <= 0) break; - - if (this.currentChar == '(') nb_brackets++; - else if (this.currentChar == ')') nb_brackets--; - - // Token is finished if there is a operator symbol outside a string - if (/[!=<>]/.test(this.currentChar)) break; - } - - // Token is finished on the first space which is outside a string or a function - if (this.currentChar == ' ' && nb_brackets <= 0) break; - } - - tokenValue += this.currentChar; - this.readNextChar(); - } - - if (/^(AND|OR)$/i.test(tokenValue)) return {type: 'logic', value: tokenValue}; - if (/^(IS|NOT)$/i.test(tokenValue)) return {type: 'operator', value: tokenValue}; - else return {type: 'word', value: tokenValue}; - }, - - readString: function () { - var tokenValue = ""; - var quote = this.currentChar; - - tokenValue += this.currentChar; - this.readNextChar(); - - while (this.currentChar != quote && this.currentChar !== "") { - tokenValue += this.currentChar; - this.readNextChar(); - } - - tokenValue += this.currentChar; - this.readNextChar(); - - // Handle this case : `table`.`column` - if (this.currentChar == '.') { - tokenValue += this.currentChar; - this.readNextChar(); - tokenValue += this.readString().value; - - return {type: 'word', value: tokenValue}; - } - - return {type: 'string', value: tokenValue}; - }, - - readGroupSymbol: function () { - var tokenValue = this.currentChar; - this.readNextChar(); - - return {type: 'group', value: tokenValue}; - }, - - readOperator: function () { - var tokenValue = this.currentChar; - this.readNextChar(); - - if (/[=<>]/.test(this.currentChar)) { - tokenValue += this.currentChar; - this.readNextChar(); - } - - return {type: 'operator', value: tokenValue}; - }, - }; - - // Tokenise a string (only useful for debug) - CondLexer.tokenize = function (source) { - var lexer = new CondLexer(source); - var tokens = []; - do { - var token = lexer.readNextToken(); - if (token.type != 'empty') tokens.push(token); - } - while (lexer.currentChar); - return tokens; - }; - - - // Constructor - function CondParser(source) { - this.lexer = new CondLexer(source); - this.currentToken = ""; - - this.readNextToken(); - } - - CondParser.prototype = { - constructor: CondParser, - - // Read the next token (skip empty tokens) - readNextToken: function () { - this.currentToken = this.lexer.readNextToken(); - while (this.currentToken.type == 'empty') this.currentToken = this.lexer.readNextToken(); - return this.currentToken; - }, - - // Wrapper function ; parse the source - parseExpressionsRecursively: function () { - return this.parseLogicalExpression(); - }, - - // Parse logical expressions (AND/OR) - parseLogicalExpression: function () { - var leftNode = this.parseConditionExpression(); - - while (this.currentToken.type == 'logic') { - var logic = this.currentToken.value; - this.readNextToken(); - - var rightNode = this.parseConditionExpression(); - - // If we are chaining the same logical operator, add nodes to existing object instead of creating another one - if (typeof leftNode.logic != 'undefined' && leftNode.logic == logic && typeof leftNode.terms != 'undefined') leftNode.terms.push(rightNode); - else { - var terms = [leftNode, rightNode]; - leftNode = {'logic': logic, 'terms': terms.slice(0)}; - } - } - - return leftNode; - }, - - // Parse conditions ([word/string] [operator] [word/string]) - parseConditionExpression: function () { - var leftNode = this.parseBaseExpression(); - - if (this.currentToken.type == 'operator') { - var operator = this.currentToken.value; - this.readNextToken(); - - // If there are 2 adjacent operators, join them with a space (exemple: IS NOT) - if (this.currentToken.type == 'operator') { - operator += ' ' + this.currentToken.value; - this.readNextToken(); - } - - var rightNode = this.parseBaseExpression(); - - leftNode = {'operator': operator, 'left': leftNode, 'right': rightNode}; - } - - return leftNode; - }, - - // Parse base items - parseBaseExpression: function () { - var astNode = ""; - - // If this is a word/string, return its value - if (this.currentToken.type == 'word' || this.currentToken.type == 'string') { - astNode = this.currentToken.value; - this.readNextToken(); - } - // If this is a group, skip brackets and parse the inside - else if (this.currentToken.type == 'group') { - this.readNextToken(); - astNode = this.parseExpressionsRecursively(); - this.readNextToken(); - } - - return astNode; - }, - }; - - // Parse a string - CondParser.parse = function (source) { - return new CondParser(source).parseExpressionsRecursively(); - }; - - // Generate the SQL query corresponding to an AST output by sql2ast() - exports.ast2sql = function (ast) { - var result = ''; - - // Define subfunctions - function select(ast) { - if (typeof ast['SELECT'] != 'undefined') { - return 'SELECT ' + ast['SELECT'].map(function(item) { - return item.name; - }).join(', '); - } - else return ''; - } - - function from(ast) { - if (typeof ast['FROM'] != 'undefined') { - var result = ' FROM '; - var tmp = ast['FROM'].map(function (item) { - var str = item.table; - if (item.as !== '') str += ' AS ' + item.as; - return str; - }); - result += tmp.join(', '); - return result; - } - else return ''; - } - - function join(ast) { - if (typeof ast['JOIN'] != 'undefined') { - var result = ''; - ast['JOIN'].forEach(function(item) { - result += ' ' + item.type.toUpperCase() + ' JOIN ' + item.table; - if (item.as !== '') result += ' AS ' + item.as; - result += ' ON ' + cond2sql(item.cond); - }); - return result; - } - else return ''; - } - - function where(ast) { - if (typeof ast['WHERE'] != 'undefined') { - return ' WHERE ' + cond2sql(ast['WHERE']); - } - else return ''; - } - - function order_by(ast) { - if (typeof ast['ORDER BY'] != 'undefined') { - var result = ' ORDER BY '; - var orders = ast['ORDER BY'].map(function (item) { - return item.column + ' ' + item.order; - }); - result += orders.join(', '); - return result; - } - else return ''; - } - - function group_by(ast) { - if (typeof ast['GROUP BY'] != 'undefined') { - var result = ' GROUP BY '; - var groups = ast['GROUP BY'].map(function (item) { - return item.column; - }); - result += groups.join(', '); - return result; - } - else return ''; - } - - function limit(ast) { - if (typeof ast['LIMIT'] != 'undefined' && typeof ast['LIMIT'].nb != 'undefined' && parseInt(ast['LIMIT'].nb, 10) > 0) { - var result = ' LIMIT '; - if (typeof ast['LIMIT'].from != 'undefined' && parseInt(ast['LIMIT'].from, 10) > 1) result += ast['LIMIT'].from + ','; - result += ast['LIMIT'].nb; - return result; - } - else return ''; - } - - function insert_into(ast) { - if (typeof ast['INSERT INTO'] != 'undefined') { - var result = 'INSERT INTO ' + ast['INSERT INTO'].table; - if (typeof ast['INSERT INTO'].columns != 'undefined') { - result += ' ('; - result += ast['INSERT INTO'].columns.join(', '); - result += ')'; - } - return result; - } - else return ''; - } - - function values(ast) { - if (typeof ast['VALUES'] != 'undefined') { - var result = ' VALUES '; - var vals = ast['VALUES'].map(function (item) { - return '(' + item.join(', ') + ')'; - }); - result += vals.join(', '); - return result; - } - else return ''; - } - - function delete_from(ast) { - if (typeof ast['DELETE FROM'] != 'undefined') { - var result = 'DELETE FROM '; - result += ast['DELETE FROM'].map(function (item) { - var str = item.table; - if (item.as !== '') str += ' AS ' + item.as; - return str; - }).join(', '); - return result; - } - else return ''; - } - - function update(ast) { - if (typeof ast['UPDATE'] != 'undefined') { - var result = 'UPDATE '; - result += ast['UPDATE'].map(function (item) { - var str = item.table; - if (item.as !== '') str += ' AS ' + item.as; - return str; - }).join(', '); - return result; - } - else return ''; - } - - function set(ast) { - if (typeof ast['SET'] != 'undefined') { - return ' SET ' + ast['SET'].map(function(item) { - return item.expression; - }).join(', '); - } - else return ''; - } - - - // Check request's type - if (typeof ast['SELECT'] != 'undefined' && typeof ast['FROM'] != 'undefined') { - result = select(ast) + from(ast) + join(ast) + where(ast) + group_by(ast) + order_by(ast) + limit(ast); - } - else if (typeof ast['INSERT INTO'] != 'undefined') { - result = insert_into(ast) + values(ast); - } - else if (typeof ast['UPDATE'] != 'undefined') { - result = update(ast) + set(ast) + where(ast); - } - else if (typeof ast['DELETE FROM'] != 'undefined') { - result = delete_from(ast) + where(ast); - } - else result = null; - - return result; - }; - - // Generate SQL from a condition AST output by sql2ast() or CondParser - function cond2sql(cond, not_first) { - var result = ''; - - // If there is a logical operation - if (typeof cond.logic != 'undefined') { - result = cond.terms.map(function (item) { - return cond2sql(item, true); - }); - result = result.join(' ' + cond.logic + ' '); - if (typeof not_first !== 'undefined') result = '(' + result + ')'; - } - // If there is a condition - else if (typeof cond.left != 'undefined') { - result = cond.left; - if (typeof cond.operator != 'undefined') { - result += ' ' + cond.operator; - if (typeof cond.right != 'undefined') { - result += ' ' + cond.right; - } - } - } - // If there is a boolean - else result = cond; - - return result; - } - - // Export some methods for tests - exports.trim = trim; - exports.protect = protect; - exports.unprotect = unprotect; - exports.protect_split = protect_split; - exports.CondLexer = CondLexer; - exports.CondParser = CondParser; - -}(typeof exports === "undefined" ? (this.simpleSqlParser = {}) : exports)); diff --git a/src/ast2sql.js b/src/ast2sql.js new file mode 100644 index 0000000..d25d2e1 --- /dev/null +++ b/src/ast2sql.js @@ -0,0 +1,141 @@ +"use strict"; + +module.exports = function(ast) { + if (typeof ast === 'object' && ast.status === true) ast = ast.value; + else return false; + + function select(ast) { + var result = 'SELECT '; + result += ast.select.map(function(item) { + return item.expression; + }).join(', '); + return result; + } + + function from(ast) { + var result = 'FROM '; + result += ast.from.map(function(item) { + return item.expression; + }).join(', '); + return result; + } + + function join(ast) { + return ast.join.map(function(item) { + var result = ''; + if (item.type === 'inner') result += 'INNER JOIN '; + else if (item.type === 'left') result += 'LEFT JOIN '; + else if (item.type === 'right') result += 'RIGHT JOIN '; + else return ''; + result += item.table; + if (item.alias !== null) result += ' AS ' + item.alias; + result += ' ON '; + result += item.condition.expression; + return result; + }).join(' '); + } + + function where(ast) { + var result = ''; + if (ast.where !== null) result += 'WHERE ' + ast.where.expression; + return result; + } + + function group(ast) { + var result = ''; + if (ast.group.length > 0) { + result += 'GROUP BY '; + result += ast.group.map(function(item) { + return item.expression; + }).join(', '); + } + return result; + } + + function order(ast) { + var result = ''; + if (ast.order.length > 0) { + result += 'ORDER BY '; + result += ast.order.map(function(item) { + return item.expression; + }).join(', '); + } + return result; + } + + function limit(ast) { + var result = ''; + if (ast.limit !== null) { + result += 'LIMIT '; + if (ast.limit.from !== null) result += ast.limit.from + ', '; + result += ast.limit.nb; + } + return result; + } + + function into(ast) { + return 'INSERT INTO ' + ast.into.expression; + } + + function values(ast) { + var result = ''; + var targets = ast.values.filter(function(item) { + return item.target !== null; + }); + if (targets.length > 0) { + result += '('; + result += targets.map(function(item) { + return item.target.expression; + }).join(', '); + result += ') '; + } + result += 'VALUES ('; + result += ast.values.map(function(item) { + return item.value; + }).join(', '); + result += ')'; + return result; + } + + function table(ast) { + return 'UPDATE ' + ast.table.expression; + } + + function update(ast) { + var result = 'SET '; + result += ast.values.map(function(item) { + return item.target.expression + ' = ' + item.value; + }).join(', '); + return result; + } + + var parts = []; + if (ast.type === 'select') { + parts.push(select(ast)); + parts.push(from(ast)); + parts.push(join(ast)); + parts.push(where(ast)); + parts.push(group(ast)); + parts.push(order(ast)); + parts.push(limit(ast)); + } + else if (ast.type === 'insert') { + parts.push(into(ast)); + parts.push(values(ast)); + } + else if (ast.type === 'update') { + parts.push(table(ast)); + parts.push(update(ast)); + parts.push(where(ast)); + } + else if (ast.type === 'delete') { + parts.push('DELETE'); + parts.push(from(ast)); + parts.push(where(ast)); + } + else return false; + + return parts.filter(function(item) { + return item !== ''; + }).join(' '); +}; diff --git a/src/sql2ast.js b/src/sql2ast.js new file mode 100644 index 0000000..4f4f246 --- /dev/null +++ b/src/sql2ast.js @@ -0,0 +1,550 @@ +"use strict"; +var Parsimmon = require('parsimmon'); + +/******************************************************************************************** + ALIASES +********************************************************************************************/ + +var seq = Parsimmon.seq; +var alt = Parsimmon.alt; +var regex = Parsimmon.regex; +var string = Parsimmon.string; +var optWhitespace = Parsimmon.optWhitespace; +var whitespace = Parsimmon.whitespace; +var lazy = Parsimmon.lazy; + + + +/******************************************************************************************** + COMMON PATTERNS +********************************************************************************************/ + +// Make a parser optionnal +// "empty" parameter will be returned as result if the optionnal parser can't match +function opt(parser, empty) { + if (typeof empty == 'undefined') empty = []; + return parser.or(Parsimmon.succeed(empty)); +} + +// Join results of a parser +function mkString(node) { + return node.join(''); +} + +// Add an item to an optionnal list and return the final list +function mergeOptionnalList(node) { + node[0].push(node[1]); + return node[0]; +} + +// Generate a parser that accept a comma-separated list of something +function optionnalList(parser) { + return seq( + parser.skip(optWhitespace).skip(string(',')).skip(optWhitespace).many(), + parser.skip(optWhitespace) + ).map(mergeOptionnalList); +} + +// Remove first and last character of a string +function removeQuotes(string) { + return string.replace(/^([`'"])(.*)\1$/, '$2'); +} + +// Add the starting and ending char positions of matches of a given parser +function getPos(parser) { + return seq( + Parsimmon.index, + parser, + Parsimmon.index + ).map(function(node) { + var pos = { + start: node[0], + end: node[2], + }; + if (typeof node[1] == 'object') { + var n = node[1]; + n.position = pos; + return n; + } + else { + pos.out = node[1]; + return pos; + } + }); +} + + + +/******************************************************************************************** + LOW LEVEL PARSERS +********************************************************************************************/ + +// The name of a column/table +var colName = alt( + regex(/(?!(FROM|WHERE|GROUP BY|ORDER BY|LIMIT|INNER|LEFT|RIGHT|JOIN|ON|VALUES|SET)\s)[a-z*][a-z0-9_]*/i), + regex(/`[^`\\]*(?:\\.[^`\\]*)*`/) +); + +// A string +var str = alt( + regex(/"[^"\\]*(?:\\.[^"\\]*)*"/), + regex(/'[^'\\]*(?:\\.[^'\\]*)*'/) +); + +// A function expression +var func = seq( + alt( + regex(/[a-zA-Z0-9_]+\(/), + string('(') + ), + opt(lazy(function() { return argList; })).map(mkString), + string(')') +).map(mkString); + +// A table.column expression +var tableAndColumn = seq( + colName, + string('.'), + colName +); + +// An operator +var operator = alt( + string('+'), + string('-'), + string('*'), + string('/'), + string('&&'), + string('&'), + string('~'), + string('||'), + string('|'), + string('^'), + regex(/XOR/i), + string('<=>'), + string('='), + string('!='), + string('>='), + string('>>'), + string('>'), + string('<='), + string('<<'), + string('<'), + regex(/IS NULL/i), + regex(/IS NOT/i), + regex(/IS NOT NULL/i), + regex(/IS/i), + regex(/LIKE/i), + regex(/NOT LIKE/i), + regex(/BETWEEN/i), + regex(/NOT BETWEEN/i), + string('%'), + regex(/MOD/i), + regex(/NOT/i), + regex(/OR\s/i), // A space is forced after so this doesn't get mixed up with ORDER BY + regex(/AND/i), + regex(/IN/i) +); + +// A number +var number = regex(/[-]?\d+\.?\d*/); + + + +/******************************************************************************************** + EXPRESSION PARSERS +********************************************************************************************/ + +// List (following IN, for example) +var list = seq( + string('('), + optWhitespace, + seq( + alt( + number, + str + ), + optWhitespace, + opt(string(',')), + optWhitespace, + opt( + alt( + number, + str + ) + ) + ).map(mkString), + optWhitespace, + string(')') +).map(mkString); + +// Expression +var expression = seq( + alt( + tableAndColumn.map(function(node) { + return { + expression: node.join(''), + table: removeQuotes(node[0]), + column: removeQuotes(node[2]) + }; + }), + func.map(function(node) { + return { + expression: node, + table: null, + column: null + }; + }), + colName.map(function(node) { + return { + expression: node, + table: null, + column: removeQuotes(node) + }; + }), + str.map(function(node) { + return { + expression: node, + table: null, + column: null + }; + }), + number.map(function(node) { + return { + expression: node, + table: null, + column: null + }; + }), + list.map(function(node) { + return { + expression: node, + table: null, + column: null + }; + }) + ), + opt(seq( + optWhitespace, + operator, + opt(seq( + optWhitespace, + lazy(function() { return expression; }).map(function(node) { + return node.expression; + }) + ).map(mkString), null) + ).map(mkString), null) +).map(function(node) { + if (node[1] !== null) { + node[0] = node[0].expression; + return { + expression: node.join(''), + table: null, + column: null + }; + } + else return node[0]; +}); + +// Expression following a SELECT statement +var colListExpression = seq( + expression, + opt( // Alias + seq( + optWhitespace, + opt(regex(/AS\s/i)), + alt(colName, str) + ).map(function(node) { + var n = {}; + n.alias = removeQuotes(node[2]); + n.expression = node.join(''); + return n; + }), + null + ) +).map(function(node) { + var n = node[0]; + n.alias = (node[1] !== null) ? node[1].alias : null; + n.expression = n.expression + ((node[1] !== null) ? node[1].expression : ''); + return n; +}); + +// Expression inside a function +var argListExpression = expression.map(function(node) { + return node.expression; +}); + +// Expression following a FROM statement +var tableListExpression = seq( + alt( + tableAndColumn.map(mkString), + colName + ), + opt( // Alias + seq( + optWhitespace, + opt(regex(/AS\s/i)), + alt(colName, str) + ).map(function(node) { + return { + alias: removeQuotes(node[2]), + expression : node.join(''), + }; + }), + null + ) +).map(function(node) { + var n = {}; + n.table = node[0]; + n.alias = (node[1] !== null) ? node[1].alias : null; + n.expression = node[0] + ((node[1] !== null) ? node[1].expression : ''); + return n; +}); + +// JOIN expression (including JOIN statements) +var joinExpression = seq( + opt(seq( + regex(/INNER|LEFT|RIGHT/i), + whitespace + ).map(function(node) { + return node[0].toLowerCase(); + }), null), + regex(/JOIN/i), + optWhitespace, + getPos(tableListExpression), + optWhitespace, + regex(/ON/i), + optWhitespace, + getPos(expression) +).map(function(node) { + var n = {}; + n.type = node[0] || 'inner'; + n.table = node[3].table; + n.alias = node[3].alias; + n.position = node[3].position; + n.condition = { + expression: node[7].expression, + position: node[7].position, + }; + return n; +}); + +// Expression following a WHERE statement +var whereExpression = getPos(expression).map(function(node) { + return { + expression: node.expression, + position: node.position, + }; +}); + +// Expression following an ORDER BY statement +var orderListExpression = seq( + expression, + opt(seq( + optWhitespace, + regex(/ASC|DESC/i) + ), null) +).map(function(node) { + return { + expression: node[0].expression + ((node[1] !== null) ? node[1].join('') : ''), + order: (node[1] !== null) ? node[1][1] : 'ASC', + table: node[0].table, + column: node[0].column, + }; +}); + +// Expression following a LIMIT statement +var limitExpression = seq( + number, + opt(seq( + optWhitespace, + string(','), + optWhitespace, + number + ), null) +).map(function(node) { + if (node[1] === null) return { + from: null, + nb: parseInt(node[0], 10), + }; + else return { + from: parseInt(node[0], 10), + nb: parseInt(node[1][3], 10), + }; +}); + +// Expression designating a column before VALUES in INSERT query +var insertColListExpression = alt( + tableAndColumn.map(function(node) { + return { + expression: node.join(''), + column: removeQuotes(node[2]) + }; + }), + colName.map(function(node) { + return { + expression: node, + column: removeQuotes(node) + }; + }) +); + +// Expression following a VALUES statement +var valueExpression = expression.map(function(node) { + return node.expression; +}); + +// Expression that assign a value to a column +var assignExpression = seq( + insertColListExpression, + optWhitespace, + string('='), + optWhitespace, + expression +).map(function(node) { + return { + target: node[0], + value: node[4].expression, + }; +}); + + + +/******************************************************************************************** + HIGH LEVEL PARSERS +********************************************************************************************/ + +// List of arguments inside a function +var argList = seq( + seq(argListExpression, optWhitespace, string(','), optWhitespace).map(mkString).many(), + argListExpression.skip(optWhitespace) +).map(mergeOptionnalList); + +// List of expressions following a SELECT statement +var colList = optionnalList(getPos(colListExpression)); + +// List of table following a FROM statement +var tableList = optionnalList(getPos(tableListExpression)); + +// List of table following an GROUP BY statement +var groupList = optionnalList(getPos(expression)); + +// List of table following an ORDER BY statement +var orderList = optionnalList(getPos(orderListExpression)); + +// List of joins (including JOIN statements) +var joinList = optWhitespace.then(joinExpression).skip(optWhitespace).many(); + +// List of columns before VALUES in INSERT query +var insertColList = optionnalList(insertColListExpression); + +// List of values following a VALUES statement +var valuesList = optionnalList(valueExpression); + +// List of assign expression following a SET statement +var assignList = optionnalList(assignExpression); + + + +/******************************************************************************************** + MAIN PARSERS +********************************************************************************************/ + +// SELECT parser +var selectParser = seq( + regex(/SELECT/i).skip(optWhitespace).then(opt(colList)), + regex(/FROM/i).skip(optWhitespace).then(opt(tableList)), + opt(joinList), + opt(regex(/WHERE/i).skip(optWhitespace).then(opt(whereExpression)), null), + opt(regex(/\s?GROUP BY/i).skip(optWhitespace).then(opt(groupList))), + opt(regex(/\s?ORDER BY/i).skip(optWhitespace).then(opt(orderList))), + opt(regex(/\s?LIMIT/i).skip(optWhitespace).then(opt(limitExpression)), null) +).map(function(node) { + return { + type: 'select', + select: node[0], + from: node[1], + join: node[2], + where: node[3], + group: node[4], + order: node[5], + limit: node[6], + }; +}); + +// INSERT parser +var insertParser = seq( + regex(/INSERT INTO/i).skip(optWhitespace).then(tableListExpression), + optWhitespace, + opt( + seq( + string('('), + insertColList, + string(')') + ).map(function(node) { + return node[1]; + }) + ), + optWhitespace, + regex(/VALUES\s?\(/i).skip(optWhitespace).then(valuesList), + string(')') +).map(function(node) { + var values = []; + var bigger = Math.max(node[2].length, node[4].length); + + for (var i = 0; i < bigger; ++i) { + values[i] = { + target: node[2][i] || null, + value: node[4][i] || null, + }; + } + + return { + type: 'insert', + into: node[0], + values: values, + }; +}); + +// UPDATE parser +var updateParser = seq( + regex(/UPDATE/i).skip(optWhitespace).then(tableListExpression), + optWhitespace, + regex(/SET/i).skip(optWhitespace).then(assignList), + optWhitespace, + opt(regex(/WHERE/i).skip(optWhitespace).then(opt(whereExpression)), null) +).map(function(node) { + return { + type: 'update', + table: node[0], + values: node[2], + where: node[4], + }; +}); + +// DELETE parser +var deleteParser = seq( + regex(/DELETE FROM/i).skip(optWhitespace).then(opt(tableList)), + opt(regex(/WHERE/i).skip(optWhitespace).then(opt(whereExpression)), null) +).map(function(node) { + return { + type: 'delete', + from: node[0], + where: node[1], + }; +}); + +// Main parser +var p = alt(selectParser, insertParser, updateParser, deleteParser); + + + +/******************************************************************************************** + PUBLIC FUNCTIONS +********************************************************************************************/ + +module.exports = function(sql) { + var result = p.parse(sql); + if (result.status === false) result.error = Parsimmon.formatError(sql, result); + return result; +}; diff --git a/tests/helpers.js b/tests/helpers.js new file mode 100644 index 0000000..1ac276c --- /dev/null +++ b/tests/helpers.js @@ -0,0 +1,35 @@ +"use strict"; + +var expect = require('chai').expect; +var m = require('../index'); + +function ok(ast) { + return { + status: true, + value: ast + }; +} + +function testAst(comment, query, ast) { + expect(m.sql2ast(query)).to.eql(ok(ast), comment + ': ' + query); +} + +function testBackAndForth(comment, query) { + expect(m.ast2sql(m.sql2ast(query))).to.eql(query, comment + ': ' + query); +} + +function isArray(variable, message) { + expect(variable).to.be.instanceOf(Array, message); +} + +function isObject(variable, message) { + expect(variable).not.to.be.instanceOf(Array, message); +} + +module.exports = { + ok: ok, + testAst: testAst, + testBackAndForth: testBackAndForth, + isArray: isArray, + isObject: isObject, +}; diff --git a/tests/tests-api.js b/tests/tests-api.js new file mode 100644 index 0000000..9df42c3 --- /dev/null +++ b/tests/tests-api.js @@ -0,0 +1,56 @@ +/*global test:true,suite:true*/ +"use strict"; + +var expect = require('chai').expect; +var m = require('../index'); +var h = require('./helpers.js'); + +suite('sql2ast - API'); + +var q = [ + 'SELECT * FROM table', + 'INSERT INTO table VALUES (1, 2, 3)', + 'UPDATE table SET col = "value"', + 'DELETE FROM table', +]; +var ast = q.map(m.sql2ast); + +var types = ['select', 'delete', 'insert', 'update']; + +ast.forEach(function(a) { + test('A basic query should parse', function() { + expect(a.status).to.equal(true, 'Parser must parse valid SQL'); + expect(types.indexOf(a.value.type)).not.to.equal(-1, 'AST must contain a valid type'); + }); + + if (a.value.type === types[0]) { + test('SELECT AST is cool', function() { + h.isArray(a.value.select, "(SELECT) AST must contain a 'select' array"); + h.isArray(a.value.from, "(SELECT) AST must contain a 'from' array"); + h.isArray(a.value.join, "(SELECT) AST must contain a 'join' array"); + h.isObject(a.value.where, "(SELECT) AST must contain a 'where' object"); + h.isArray(a.value.group, "(SELECT) AST must contain a 'group' array"); + h.isArray(a.value.order, "(SELECT) AST must contain a 'order' array"); + h.isObject(a.value.limit, "(SELECT) AST must contain a 'limit' object"); + }); + } + else if (a.value.type === types[1]) { + test('DELETE AST is cool', function() { + h.isArray(a.value.from, "(DELETE) AST must contain a 'from' array"); + h.isObject(a.value.where, "(DELETE) AST must contain a 'where' object"); + }); + } + else if (a.value.type === types[2]) { + test('INSERT AST is cool', function() { + h.isObject(a.value.into, "(INSERT) AST must contain a 'into' object"); + h.isArray(a.value.values, "(INSERT) AST must contain a 'values' array"); + }); + } + else if (a.value.type === types[3]) { + test('UPDATE AST is cool', function() { + h.isObject(a.value.table, "(UPDATE) AST must contain a 'table' object"); + h.isArray(a.value.values, "(UPDATE) AST must contain a 'values' array"); + h.isObject(a.value.where, "(UPDATE) AST must contain a 'where' object"); + }); + } +}); diff --git a/tests/tests-delete.js b/tests/tests-delete.js new file mode 100644 index 0000000..73f8f64 --- /dev/null +++ b/tests/tests-delete.js @@ -0,0 +1,75 @@ +/*global test:true,suite:true*/ +"use strict"; + +var expect = require('chai').expect; +var h = require('./helpers.js'); + +var Delete = [ + { + c: 'Simple delete', + q: 'DELETE FROM table', + a: { + type: 'delete', + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 12, end: 17 } }, + ], + where: null, + }, + }, + { + c: 'Several tables with aliases', + q: 'DELETE FROM table1 AS t1, table2 "t2"', + a: { + type: 'delete', + from: [ + { expression: 'table1 AS t1', table: 'table1', alias: 't1', position: { start: 12, end: 24 } }, + { expression: 'table2 "t2"', table: 'table2', alias: 't2', position: { start: 26, end: 37 } }, + ], + where: null, + }, + }, + { + c: 'Where #1', + q: 'DELETE FROM table WHERE this >= that AND col IS NOT NULL', + a: { + type: 'delete', + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 12, end: 17 } }, + ], + where: { + expression: "this >= that AND col IS NOT NULL", + position: { start: 24, end: 56 }, + }, + }, + }, + { + c: 'Where #2', + q: 'DELETE FROM table WHERE (FUNC(this) = "string") AND (1+5 OR col1)', + a: { + type: 'delete', + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 12, end: 17 } }, + ], + where: { + expression: "(FUNC(this) = \"string\") AND (1+5 OR col1)", + position: { start: 24, end: 65 }, + }, + }, + }, +]; + +suite('sql2ast - delete'); + +Delete.forEach(function(item) { + test(item.c, function() { + h.testAst(item.c, item.q, item.a); + }); +}); + +suite('ast2sql - delete'); + +Delete.forEach(function(item) { + test(item.c, function() { + h.testBackAndForth(item.c, item.q); + }); +}); diff --git a/tests/tests-insert.js b/tests/tests-insert.js new file mode 100644 index 0000000..74a7514 --- /dev/null +++ b/tests/tests-insert.js @@ -0,0 +1,75 @@ +/*global test:true,suite:true*/ +"use strict"; + +var expect = require('chai').expect; +var h = require('./helpers.js'); + +var Insert = [ + { + c: 'Simple insert', + q: 'INSERT INTO table VALUES (1, 2, 3)', + a: { + type: 'insert', + into: { + expression: 'table', + table: 'table', + alias: null, + }, + values: [ + { target: null, value: '1'}, + { target: null, value: '2'}, + { target: null, value: '3'}, + ], + }, + }, + { + c: 'Complex values', + q: 'INSERT INTO table VALUES (1 + 9, FUNC(2, col), "string")', + a: { + type: 'insert', + into: { + expression: 'table', + table: 'table', + alias: null, + }, + values: [ + { target: null, value: '1 + 9'}, + { target: null, value: 'FUNC(2, col)'}, + { target: null, value: '"string"'}, + ], + }, + }, + { + c: 'Insert with columns', + q: 'INSERT INTO table (col1, `col2`, col3) VALUES (1, 2, 3)', + a: { + type: 'insert', + into: { + expression: 'table', + table: 'table', + alias: null, + }, + values: [ + { target: { expression: 'col1', column: 'col1' }, value: '1'}, + { target: { expression: '`col2`', column: 'col2' }, value: '2'}, + { target: { expression: 'col3', column: 'col3' }, value: '3'}, + ], + }, + }, +]; + +suite('sql2ast - insert'); + +Insert.forEach(function(item) { + test(item.c, function() { + h.testAst(item.c, item.q, item.a); + }); +}); + +suite('ast2sql - insert'); + +Insert.forEach(function(item) { + test(item.c, function() { + h.testBackAndForth(item.c, item.q); + }); +}); diff --git a/tests/tests-select.js b/tests/tests-select.js new file mode 100644 index 0000000..a00242d --- /dev/null +++ b/tests/tests-select.js @@ -0,0 +1,480 @@ +/*global test:true,suite:true*/ +"use strict"; + +var expect = require('chai').expect; +var h = require('./helpers.js'); + +var Select = [ + { + c: 'Simple select', + q: 'SELECT * FROM table', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [], + where: null, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Column quotes', + q: 'SELECT col1, `col2` FROM table', + a: { + type: 'select', + select: [ + { expression: 'col1', column: 'col1', table: null, alias: null, position: { start: 7, end: 11 } }, + { expression: '`col2`', column: 'col2', table: null, alias: null, position: { start: 13, end: 19 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 25, end: 30 } }, + ], + join: [], + where: null, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Special words', + q: 'SELECT fromage "from", asymetric AS as FROM table', + a: { + type: 'select', + select: [ + { expression: 'fromage "from"', column: 'fromage', table: null, alias: 'from', position: { start: 7, end: 21 } }, + { expression: 'asymetric AS as', column: 'asymetric', table: null, alias: 'as', position: { start: 23, end: 38 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 44, end: 49 } }, + ], + join: [], + where: null, + group: [], + order: [], + limit: null, + }, + }, + { + c: '"table.column" notation', + q: 'SELECT table.col1, table.`col2`, `table`.col3, `table`.`col4` FROM table', + a: { + type: 'select', + select: [ + { expression: 'table.col1', column: 'col1', table: 'table', alias: null, position: { start: 7, end: 17 } }, + { expression: 'table.`col2`', column: 'col2', table: 'table', alias: null, position: { start: 19, end: 31 } }, + { expression: '`table`.col3', column: 'col3', table: 'table', alias: null, position: { start: 33, end: 45 } }, + { expression: '`table`.`col4`', column: 'col4', table: 'table', alias: null, position: { start: 47, end: 61 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 67, end: 72 } }, + ], + join: [], + where: null, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Strings', + q: 'SELECT "string", "\\"special\\" string" FROM table', + a: { + type: 'select', + select: [ + { expression: '"string"', column: null, table: null, alias: null, position: { start: 7, end: 15 } }, + { expression: '"\\"special\\" string"', column: null, table: null, alias: null, position: { start: 17, end: 37 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 43, end: 48 } }, + ], + join: [], + where: null, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Column alias #1', + q: 'SELECT col1 AS alias, col2 AS "alias" FROM table', + a: { + type: 'select', + select: [ + { expression: 'col1 AS alias', column: 'col1', table: null, alias: 'alias', position: { start: 7, end: 20 } }, + { expression: 'col2 AS "alias"', column: 'col2', table: null, alias: 'alias', position: { start: 22, end: 37 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 43, end: 48 } }, + ], + join: [], + where: null, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Column alias #2', + q: 'SELECT col1 alias, col2 "alias" FROM table', + a: { + type: 'select', + select: [ + { expression: 'col1 alias', column: 'col1', table: null, alias: 'alias', position: { start: 7, end: 17 } }, + { expression: 'col2 "alias"', column: 'col2', table: null, alias: 'alias', position: { start: 19, end: 31 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 37, end: 42 } }, + ], + join: [], + where: null, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Mathematical expressions', + q: 'SELECT 1 + 1, col1*0.7 AS test FROM table', + a: { + type: 'select', + select: [ + { expression: '1 + 1', column: null, table: null, alias: null, position: { start: 7, end: 12 } }, + { expression: 'col1*0.7 AS test', column: null, table: null, alias: 'test', position: { start: 14, end: 30 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 36, end: 41 } }, + ], + join: [], + where: null, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Functions', + q: 'SELECT FUNC(), OTHERFUN(col, FUNC(1/4, -3.05), "string") FROM table', + a: { + type: 'select', + select: [ + { expression: 'FUNC()', column: null, table: null, alias: null, position: { start: 7, end: 13 } }, + { expression: 'OTHERFUN(col, FUNC(1/4, -3.05), "string")', column: null, table: null, alias: null, position: { start: 15, end: 56 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 62, end: 67 } }, + ], + join: [], + where: null, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Table alias', + q: 'SELECT * FROM table AS t, table2 AS "t2"', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table AS t', table: 'table', alias: 't', position: { start: 14, end: 24 } }, + { expression: 'table2 AS "t2"', table: 'table2', alias: 't2', position: { start: 26, end: 40 } }, + ], + join: [], + where: null, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Simple inner join', + q: 'SELECT * FROM table INNER JOIN table2 ON table2.id = id_table2', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [ + { type: 'inner', table: 'table2', alias: null, position: { start: 31, end: 37 }, condition: { expression: 'table2.id = id_table2', position: { start: 41, end: 62 } } }, + ], + where: null, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Several joins', + q: 'SELECT * FROM table INNER JOIN t1 ON t1.id = id_table2 AND t1.bool LEFT JOIN t2 ON t2.id = t1.id_t2 RIGHT JOIN t3 AS table3 ON t3.id = FUNC(t1.stuff)', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [ + { type: 'inner', table: 't1', alias: null, position: { start: 31, end: 33 }, condition: { expression: 't1.id = id_table2 AND t1.bool', position: { start: 37, end: 66 } } }, + { type: 'left', table: 't2', alias: null, position: { start: 77, end: 79 }, condition: { expression: 't2.id = t1.id_t2', position: { start: 83, end: 99 } } }, + { type: 'right', table: 't3', alias: 'table3', position: { start: 111, end: 123 }, condition: { expression: 't3.id = FUNC(t1.stuff)', position: { start: 127, end: 149 } } }, + ], + where: null, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Where #1', + q: 'SELECT * FROM table WHERE this >= that AND col IS NOT NULL', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [], + where: { + expression: "this >= that AND col IS NOT NULL", + position: { start: 26, end: 58 }, + }, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Where #2', + q: 'SELECT * FROM table WHERE (FUNC(this) = "string") AND (1+5 OR col1)', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [], + where: { + expression: "(FUNC(this) = \"string\") AND (1+5 OR col1)", + position: { start: 26, end: 67 }, + }, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Where #3', + q: 'SELECT * FROM table WHERE column IN ("val1", "val2")', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [], + where: { + expression: "column IN (\"val1\", \"val2\")", + position: { start: 26, end: 52 }, + }, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Where #3 with spaces', + q: 'SELECT * FROM table WHERE column IN ( "val1", "val2" )', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [], + where: { + expression: "column IN ( \"val1\", \"val2\" )", + position: { start: 26, end: 54 }, + }, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'Where followed by something else', + q: 'SELECT * FROM table WHERE cond ORDER BY col LIMIT 1', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [], + where: { + expression: "cond", + position: { start: 26, end: 30 }, + }, + group: [], + order: [ + { expression: "col", table: null, column: "col", order: "ASC", position: { start: 40, end: 43 } }, + ], + limit: { from: null, nb: 1 }, + }, + }, + { + c: 'Group by', + q: 'SELECT * FROM table GROUP BY col1, MONTH(col2), table.col3', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [], + where: null, + group: [ + { expression: 'col1', table: null, column: 'col1', position: { start: 29, end: 33 } }, + { expression: 'MONTH(col2)', table: null, column: null, position: { start: 35, end: 46 } }, + { expression: 'table.col3', table: 'table', column: 'col3', position: { start: 48, end: 58 } }, + ], + order: [], + limit: null, + }, + }, + { + c: 'Order by', + q: 'SELECT * FROM table ORDER BY table.col1, col2 DESC, FUNC(col3 + 7) ASC', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [], + where: null, + group: [], + order: [ + { expression: "table.col1", table: "table", column: "col1", order: "ASC", position: { start: 29, end: 39 } }, + { expression: "col2 DESC", table: null, column: "col2", order: "DESC", position: { start: 41, end: 50 } }, + { expression: "FUNC(col3 + 7) ASC", table: null, column: null, order: "ASC", position: { start: 52, end: 70 } }, + ], + limit: null, + }, + }, + { + c: 'Limit #1', + q: 'SELECT * FROM table LIMIT 5', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [], + where: null, + group: [], + order: [], + limit: { from: null, nb: 5 }, + }, + }, + { + c: 'Limit #2', + q: 'SELECT * FROM table LIMIT 1, 2', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [], + where: null, + group: [], + order: [], + limit: { from: 1, nb: 2 }, + }, + }, + { + c: 'Between', + q: 'SELECT * FROM table WHERE col BETWEEN 1 and 5', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [], + where: { expression: 'col BETWEEN 1 and 5', position: { start: 26, end: 45 } }, + group: [], + order: [], + limit: null, + }, + }, + { + c: 'NOT Between', + q: 'SELECT * FROM table WHERE col NOT BETWEEN 1 and 5', + a: { + type: 'select', + select: [ + { expression: '*', column: '*', table: null, alias: null, position: { start: 7, end: 8 } }, + ], + from: [ + { expression: 'table', table: 'table', alias: null, position: { start: 14, end: 19 } }, + ], + join: [], + where: { expression: 'col NOT BETWEEN 1 and 5', position: { start: 26, end: 49 } }, + group: [], + order: [], + limit: null, + }, + }, +]; + +suite('sql2ast - select'); + +Select.forEach(function(item) { + test(item.c, function() { + h.testAst(item.c, item.q, item.a); + }); +}); + +suite('ast2sql - select'); + +Select.forEach(function(item) { + test(item.c, function() { + h.testBackAndForth(item.c, item.q); + }); +}); diff --git a/tests/tests-update.js b/tests/tests-update.js new file mode 100644 index 0000000..56024aa --- /dev/null +++ b/tests/tests-update.js @@ -0,0 +1,77 @@ +/*global test:true,suite:true*/ +"use strict"; + +var expect = require('chai').expect; +var h = require('./helpers.js'); + +var Update = [ + { + c: 'Simple update', + q: 'UPDATE table SET col = "value"', + a: { + type: 'update', + table: { + expression: 'table', + table: 'table', + alias: null, + }, + where: null, + values: [ + { target: { expression: 'col', column: 'col' }, value: '"value"'}, + ], + }, + }, + { + c: 'Several columns', + q: 'UPDATE table SET col = "value", col2 = NULL, table.col3 = col', + a: { + type: 'update', + table: { + expression: 'table', + table: 'table', + alias: null, + }, + where: null, + values: [ + { target: { expression: 'col', column: 'col' }, value: '"value"'}, + { target: { expression: 'col2', column: 'col2' }, value: 'NULL'}, + { target: { expression: 'table.col3', column: 'col3' }, value: 'col'}, + ], + }, + }, + { + c: 'Where #1', + q: 'UPDATE table SET col = "value" WHERE this >= that AND col IS NOT NULL', + a: { + type: 'update', + table: { + expression: 'table', + table: 'table', + alias: null, + }, + where: { + expression: "this >= that AND col IS NOT NULL", + position: { start: 37, end: 69 }, + }, + values: [ + { target: { expression: 'col', column: 'col' }, value: '"value"'}, + ], + }, + }, +]; + +suite('sql2ast - update'); + +Update.forEach(function(item) { + test(item.c, function() { + h.testAst(item.c, item.q, item.a); + }); +}); + +suite('ast2sql - update'); + +Update.forEach(function(item) { + test(item.c, function() { + h.testBackAndForth(item.c, item.q); + }); +}); diff --git a/tests/tests.html b/tests/tests.html deleted file mode 100644 index 212e226..0000000 --- a/tests/tests.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Tests - simpleSqlParser - - - - -
- - - - - diff --git a/tests/tests.js b/tests/tests.js deleted file mode 100644 index 4339c97..0000000 --- a/tests/tests.js +++ /dev/null @@ -1,835 +0,0 @@ -(function () { - "use strict"; - - var m = simpleSqlParser; - - module('sql2ast'); - - test('trim', function () { - expect(9); - - deepEqual(m.trim('test'), 'test'); - deepEqual(m.trim('test '), 'test'); - deepEqual(m.trim(' test'), 'test'); - deepEqual(m.trim(' test '), 'test'); - deepEqual(m.trim('test test'), 'test test'); - deepEqual(m.trim(' test \ - '), 'test'); - - var integer = 5; - var array = [0, 1, 2]; - var object = {test: "test", great_aswer: 42}; - deepEqual(m.trim(integer), integer); - deepEqual(m.trim(array), array); - deepEqual(m.trim(object), object); - }); - - test('protect/unprotect', function () { - expect(9); - - deepEqual(m.protect('test'), '#t#e#s#t#'); - deepEqual(m.protect('#t#e#s#t#'), '###t###e###s###t###'); - - deepEqual(m.unprotect('#t#e#s#t#'), 'test'); - deepEqual(m.unprotect('###t###e###s###t###'), '#t#e#s#t#'); - - var string = "this is a (complex) string, with some special chars like !:@.#ù%$^\ - and also a line break and des caractères français !"; - deepEqual(m.unprotect(m.protect(string)), string); - - deepEqual(m.protect(''), '#'); - deepEqual(m.unprotect('#'), ''); - deepEqual(m.protect('#'), '###'); - deepEqual(m.unprotect('###'), '#'); - }); - - test('protect_split', function () { - expect(10); - - deepEqual(m.protect_split(',', 'test'), ['test']); - deepEqual(m.protect_split(',', 'test(1,2)'), ['test(1,2)']); - deepEqual(m.protect_split(',', 'test1,test2'), ['test1', 'test2']); - deepEqual(m.protect_split(',', 'test1,(test2,test3)'), ['test1', '(test2,test3)']); - deepEqual(m.protect_split(';', 'test1;(test2;test3)'), ['test1', '(test2;test3)']); - deepEqual(m.protect_split(',', 'test1,"test2,test3"'), ['test1', '"test2,test3"']); - deepEqual(m.protect_split(',', 'test1,\'test2,test3\''), ['test1', '\'test2,test3\'']); - deepEqual(m.protect_split(',', 'test1,`test2,test3`'), ['test1', '`test2,test3`']); - deepEqual(m.protect_split(',', 'test1,function(test2,"test3(test4)\'test5\'"),test6()'), ['test1', 'function(test2,"test3(test4)\'test5\'")', 'test6()']); - deepEqual(m.protect_split(',', 'column1, column2, FUNCTION("string )\'\'", column3), column4 AS Test1,column5 AS "Test 2", column6 "Test,3" '), [ - 'column1', - 'column2', - 'FUNCTION("string )\'\'", column3)', - 'column4 AS Test1', - 'column5 AS "Test 2"', - 'column6 "Test,3"', - ]); - }); - - test('condition lexer', function () { - expect(19); - - deepEqual(m.CondLexer.tokenize('column = othercolumn'), [ - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'othercolumn'}, - ]); - - deepEqual(m.CondLexer.tokenize('column=othercolumn'), [ - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'othercolumn'}, - ]); - - deepEqual(m.CondLexer.tokenize('column = \ - othercolumn'), [ - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'othercolumn'}, - ]); - - deepEqual(m.CondLexer.tokenize('table.column = othertable.othercolumn'), [ - {type: 'word', value: 'table.column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'othertable.othercolumn'}, - ]); - - deepEqual(m.CondLexer.tokenize('table.column <= othertable.othercolumn'), [ - {type: 'word', value: 'table.column'}, - {type: 'operator', value: '<='}, - {type: 'word', value: 'othertable.othercolumn'}, - ]); - - deepEqual(m.CondLexer.tokenize('table.column = "string"'), [ - {type: 'word', value: 'table.column'}, - {type: 'operator', value: '='}, - {type: 'string', value: '"string"'}, - ]); - - deepEqual(m.CondLexer.tokenize('table.column = FUNCTION("string")'), [ - {type: 'word', value: 'table.column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'FUNCTION("string")'}, - ]); - - deepEqual(m.CondLexer.tokenize('table.column = FUNCTION("string", columns,"otherstring" ,\ - "string with SQL stuff like = AND OR ()")'), [ - {type: 'word', value: 'table.column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'FUNCTION("string", columns,"otherstring" ,\ - "string with SQL stuff like = AND OR ()")'}, - ]); - - deepEqual(m.CondLexer.tokenize('table.column != "string with SQL stuff like = AND OR ()"'), [ - {type: 'word', value: 'table.column'}, - {type: 'operator', value: '!='}, - {type: 'string', value: '"string with SQL stuff like = AND OR ()"'}, - ]); - - deepEqual(m.CondLexer.tokenize('column = othercolumn AND column < 2'), [ - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'othercolumn'}, - {type: 'logic', value: 'AND'}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '<'}, - {type: 'word', value: '2'}, - ]); - - deepEqual(m.CondLexer.tokenize('column = othercolumn AND column < 2 OR column = "string"'), [ - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'othercolumn'}, - {type: 'logic', value: 'AND'}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '<'}, - {type: 'word', value: '2'}, - {type: 'logic', value: 'OR'}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'string', value: '"string"'}, - ]); - - deepEqual(m.CondLexer.tokenize('column = othercolumn OR column < 2 AND column = "string"'), [ - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'othercolumn'}, - {type: 'logic', value: 'OR'}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '<'}, - {type: 'word', value: '2'}, - {type: 'logic', value: 'AND'}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'string', value: '"string"'}, - ]); - - deepEqual(m.CondLexer.tokenize('(column = othercolumn AND column < 2) OR column = "string"'), [ - {type: 'group', value: '('}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'othercolumn'}, - {type: 'logic', value: 'AND'}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '<'}, - {type: 'word', value: '2'}, - {type: 'group', value: ')'}, - {type: 'logic', value: 'OR'}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'string', value: '"string"'}, - ]); - - deepEqual(m.CondLexer.tokenize('column = othercolumn AND (column < 2 OR column = "string")'), [ - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'othercolumn'}, - {type: 'logic', value: 'AND'}, - {type: 'group', value: '('}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '<'}, - {type: 'word', value: '2'}, - {type: 'logic', value: 'OR'}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'string', value: '"string"'}, - {type: 'group', value: ')'}, - ]); - - deepEqual(m.CondLexer.tokenize('column = othercolumn AND column < 2 AND column = "string"'), [ - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'othercolumn'}, - {type: 'logic', value: 'AND'}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '<'}, - {type: 'word', value: '2'}, - {type: 'logic', value: 'AND'}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'string', value: '"string"'}, - ]); - - deepEqual(m.CondLexer.tokenize('(column = othercolumn)'), [ - {type: 'group', value: '('}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'othercolumn'}, - {type: 'group', value: ')'}, - ]); - - deepEqual(m.CondLexer.tokenize('column = othercolumn AND (column < 2 OR (column = "string" AND table.othercolumn))'), [ - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'word', value: 'othercolumn'}, - {type: 'logic', value: 'AND'}, - {type: 'group', value: '('}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '<'}, - {type: 'word', value: '2'}, - {type: 'logic', value: 'OR'}, - {type: 'group', value: '('}, - {type: 'word', value: 'column'}, - {type: 'operator', value: '='}, - {type: 'string', value: '"string"'}, - {type: 'logic', value: 'AND'}, - {type: 'word', value: 'table.othercolumn'}, - {type: 'group', value: ')'}, - {type: 'group', value: ')'}, - ]); - - deepEqual(m.CondLexer.tokenize('column IS NULL'), [ - {type: 'word', value: 'column'}, - {type: 'operator', value: 'IS'}, - {type: 'word', value: 'NULL'}, - ]); - - deepEqual(m.CondLexer.tokenize('column IS NOT NULL'), [ - {type: 'word', value: 'column'}, - {type: 'operator', value: 'IS'}, - {type: 'operator', value: 'NOT'}, - {type: 'word', value: 'NULL'}, - ]); - }); - - test('condition parser', function () { - expect(20); - - deepEqual(m.CondParser.parse('column = othercolumn'), {left: 'column', operator: '=', right: 'othercolumn'}); - - deepEqual(m.CondParser.parse('column=othercolumn'), {left: 'column', operator: '=', right: 'othercolumn'}); - - deepEqual(m.CondParser.parse('column = \ - othercolumn'), {left: 'column', operator: '=', right: 'othercolumn'}); - - deepEqual(m.CondParser.parse('table.column = othertable.othercolumn'), {left: 'table.column', operator: '=', right: 'othertable.othercolumn'}); - - deepEqual(m.CondParser.parse('table.column <= othertable.othercolumn'), {left: 'table.column', operator: '<=', right: 'othertable.othercolumn'}); - - deepEqual(m.CondParser.parse('table.column = "string"'), {left: 'table.column', operator: '=', right: '"string"'}); - - deepEqual(m.CondParser.parse('table.column = FUNCTION("string")'), {left: 'table.column', operator: '=', right: 'FUNCTION("string")'}); - - deepEqual(m.CondParser.parse('table.column = FUNCTION("string", columns,"otherstring" ,\ - "string with SQL stuff like = AND OR ()")'), {left: 'table.column', operator: '=', right: 'FUNCTION("string", columns,"otherstring" ,\ - "string with SQL stuff like = AND OR ()")'}); - - deepEqual(m.CondParser.parse('table.column != "string with SQL stuff like = AND OR ()"'), {left: 'table.column', operator: '!=', right: '"string with SQL stuff like = AND OR ()"'}); - - deepEqual(m.CondParser.parse('column = othercolumn AND column < 2'), { - logic: 'AND', terms: [ - {left: 'column', operator: '=', right: 'othercolumn'}, - {left: 'column', operator: '<', right: '2'}, - ]}); - - deepEqual(m.CondParser.parse('column = othercolumn AND column < 2 OR column = "string"'), { - logic: 'OR', terms: [ - {logic: 'AND', terms: [ - {left: 'column', operator: '=', right: 'othercolumn'}, - {left: 'column', operator: '<', right: '2'}, - ]}, - {left: 'column', operator: '=', right: '"string"'}, - ]}); - - deepEqual(m.CondParser.parse('column = othercolumn OR column < 2 AND column = "string"'), { - logic: 'AND', terms: [ - {logic: 'OR', terms: [ - {left: 'column', operator: '=', right: 'othercolumn'}, - {left: 'column', operator: '<', right: '2'}, - ]}, - {left: 'column', operator: '=', right: '"string"'}, - ]}); - - deepEqual(m.CondParser.parse('(column = othercolumn AND column < 2) OR column = "string"'), { - logic: 'OR', terms: [ - {logic: 'AND', terms: [ - {left: 'column', operator: '=', right: 'othercolumn'}, - {left: 'column', operator: '<', right: '2'}, - ]}, - {left: 'column', operator: '=', right: '"string"'}, - ]}); - - deepEqual(m.CondParser.parse('column = othercolumn AND (column < 2 OR column = "string")'), { - logic: 'AND', terms: [ - {left: 'column', operator: '=', right: 'othercolumn'}, - {logic: 'OR', terms: [ - {left: 'column', operator: '<', right: '2'}, - {left: 'column', operator: '=', right: '"string"'}, - ]}, - ]}); - - deepEqual(m.CondParser.parse('column = othercolumn AND column < 2 AND column = "string"'), { - logic: 'AND', terms: [ - {left: 'column', operator: '=', right: 'othercolumn'}, - {left: 'column', operator: '<', right: '2'}, - {left: 'column', operator: '=', right: '"string"'}, - ]}); - - deepEqual(m.CondParser.parse('(column = othercolumn)'), {left: 'column', operator: '=', right: 'othercolumn'}); - - deepEqual(m.CondParser.parse('column = othercolumn AND (column < 2 OR (column = "string" AND table.othercolumn))'), { - logic: 'AND', terms: [ - {left: 'column', operator: '=', right: 'othercolumn'}, - {logic: 'OR', terms: [ - {left: 'column', operator: '<', right: '2'}, - {logic: 'AND', terms: [ - {left: 'column', operator: '=', right: '"string"'}, - 'table.othercolumn', - ]}, - ]}, - ]}); - - deepEqual(m.CondParser.parse('(a AND b) OR (c AND d)'), { - logic: 'OR', terms: [ - {logic: 'AND', terms: ['a', 'b']}, - {logic: 'AND', terms: ['c', 'd']}, - ]}); - - deepEqual(m.CondParser.parse('column IS NULL'), {left: 'column', operator: 'IS', right: 'NULL'}); - - deepEqual(m.CondParser.parse('column IS NOT NULL'), {left: 'column', operator: 'IS NOT', right: 'NULL'}); - }); - - test('parse SQL', function() { - expect(30); - var q; - - q = 'SELECT * FROM table'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - }, q); - - q = 'select * from table'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - }, q); - - q = 'SELECT * FROM table;'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - }, q); - - - q = 'SELECT * FROM table AS t'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: 't', - }], - }, q); - - q = 'SELECT * FROM table; SELECT * FROM table2'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - }, q); - - q = 'SELECT column1, column2, FUNCTION("string ()\'\'", column3), column4 AS Test1,column5 AS "Test 2", column6 "Test 3" FROM table'; - deepEqual(m.sql2ast(q), { - 'SELECT': [ - {name: 'column1'}, - {name: 'column2'}, - {name: 'FUNCTION("string ()\'\'", column3)'}, - {name: 'column4 AS Test1'}, - {name: 'column5 AS "Test 2"'}, - {name: 'column6 "Test 3"'}, - ], - 'FROM': [{ - table: 'table', - as: '', - }], - }, q); - - q = 'SELECT FUCTION("SQL syntax like: FROM or LIMIT", \'SQL syntax like: FROM or LIMIT\', `SQL syntax like: FROM or LIMIT`) FROM table'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: 'FUCTION("SQL syntax like: FROM or LIMIT", \'SQL syntax like: FROM or LIMIT\', `SQL syntax like: FROM or LIMIT`)'}], - 'FROM': [{ - table: 'table', - as: '', - }], - }, q); - - q = 'SELECT * FROM table1,table2 AS t2 , table3 AS "t 3"'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [ - {table: 'table1', as: ''}, - {table: 'table2', as: 't2'}, - {table: 'table3', as: 't 3'}, - ], - }, q); - - q = 'SELECT * FROM table LEFT JOIN table2 ON table.id = table2.id_table INNER JOIN table4 AS t4 ON table.id = FUNCTION(table4.id_table, "string()") JOIN table3 ON table.id=table3.id_table'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - 'JOIN': [ - { - type: 'left', - table: 'table2', - as: '', - cond: {left: 'table.id', operator: '=', right: 'table2.id_table'}, - }, - { - type: 'inner', - table: 'table4', - as: 't4', - cond: {left: 'table.id', operator: '=', right: 'FUNCTION(table4.id_table, "string()")'}, - }, - { - type: 'inner', - table: 'table3', - as: '', - cond: {left: 'table.id', operator: '=', right: 'table3.id_table'}, - }, - ], - }, q); - - q = 'SELECT * FROM table LEFT JOIN table10 ON table.id = table10.id RIGHT JOIN table2 ON table.id = table2.id INNER JOIN table3 AS t3 ON table.id = FUNCTION(table4.id_table, "string()") JOIN table4 ON table.id=table4.id'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - 'JOIN': [ - { - type: 'left', - table: 'table10', - as: '', - cond: {left: 'table.id', operator: '=', right: 'table10.id'}, - }, - { - type: 'inner', - table: 'table3', - as: 't3', - cond: {left: 'table.id', operator: '=', right: 'FUNCTION(table4.id_table, "string()")'}, - }, - { - type: 'inner', - table: 'table4', - as: '', - cond: {left: 'table.id', operator: '=', right: 'table4.id'}, - }, - { - type: 'right', - table: 'table2', - as: '', - cond: {left: 'table.id', operator: '=', right: 'table2.id'}, - }, - ], - }, q); - - q = 'SELECT * FROM table LEFT JOIN table2 AS t2 ON table.id = t2.id_table'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - 'JOIN': [ - { - type: 'left', - table: 'table2', - as: 't2', - cond: {left: 'table.id', operator: '=', right: 't2.id_table'}, - }, - ], - }, q); - - // This is an invalid query, but the parser should not crash - q = 'SELECT * FROM table LEFT JOIN table2 table.id = t2.id_table'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - 'JOIN': [ - { - type: 'left', - table: 'table2 table.id = t2.id_table', - as: '', - cond: '', - }, - ], - }, q); - - q = 'SELECT * FROM table WHERE (column1 = "something ()" AND table.column2 != column3) AND (column4 OR column5 IS NOT NULL)'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - 'WHERE': { - logic: 'AND', terms: [ - {left: 'column1', operator: '=', right: '"something ()"'}, - {left: 'table.column2', operator: '!=', right: 'column3'}, - {logic: 'OR', terms: [ - 'column4', - {left: 'column5', operator: 'IS NOT', right: 'NULL'}, - ]}, - ], - }, - }, q); - - q = 'SELECT * FROM table ORDER BY column1 ASC, column2 DESC'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - 'ORDER BY': [ - {column: 'column1', order: 'ASC'}, - {column: 'column2', order: 'DESC'}, - ], - }, q); - - q = 'SELECT * FROM table ORDER BY column1, column2'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - 'ORDER BY': [ - {column: 'column1', order: 'ASC'}, - {column: 'column2', order: 'ASC'}, - ], - }, q); - - q = 'SELECT * FROM table GROUP BY column1, column2'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - 'GROUP BY': [ - {column: 'column1'}, - {column: 'column2'}, - ], - }, q); - - deepEqual(m.sql2ast('SELECT * FROM table LIMIT 5'), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - 'LIMIT': {nb: 5, from: 0}, - }, q); - - q = 'SELECT * FROM table LIMIT 10,20'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: '*'}], - 'FROM': [{ - table: 'table', - as: '', - }], - 'LIMIT': {nb: 20, from: 10}, - }, q); - - q = 'SELECT EXTRACT(MICROSECOND FROM "2003-01-02 10:30:00.00123") FROM table'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: 'EXTRACT(MICROSECOND FROM "2003-01-02 10:30:00.00123")'}], - 'FROM': [{ - table: 'table', - as: '', - }], - }, q); - - q = 'SELECT column1, FROM table, ORDER BY id ASC,'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: 'column1'}], - 'FROM': [{ - table: 'table', - as: '', - }], - 'ORDER BY': [{column: 'id', order: 'ASC'}], - }, q); - - q = 'SELECT column1, FROM table, GROUP BY id,'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: 'column1'}], - 'FROM': [{ - table: 'table', - as: '', - }], - 'GROUP BY': [{column: 'id'}], - }, q); - - q = 'SELECT column1, setup, column2 FROM table'; - deepEqual(m.sql2ast(q), { - 'SELECT': [{name: 'column1'}, {name: 'setup'}, {name: 'column2'}], - 'FROM': [{ - table: 'table', - as: '', - }], - }, q); - - q = 'DELETE FROM table WHERE id = 5'; - deepEqual(m.sql2ast(q), { - 'DELETE FROM': [{ - table: 'table', - as: '', - }], - 'WHERE': {left: 'id', operator: '=', right: '5'}, - }, q); - - q = 'DELETE FROM table WHERE id = 5'; - deepEqual(m.sql2ast(q, false), { - 'DELETE FROM': [{ - table: 'table', - as: '', - }], - 'WHERE': 'id = 5', - }, q); - - q = 'INSERT INTO table (column1, column2) VALUES("test ()", CURDATE())'; - deepEqual(m.sql2ast(q), { - 'INSERT INTO': { - table: 'table', - columns: ['column1', 'column2'], - }, - 'VALUES': [['"test ()"', 'CURDATE()']], - }, q); - - q = 'INSERT INTO table (col_A,col_B,col_C) VALUES (1,2,3)'; - deepEqual(m.sql2ast(q), { - 'INSERT INTO': { - table: 'table', - columns: ['col_A', 'col_B', 'col_C'], - }, - 'VALUES': [['1', '2', '3']], - }, q); - - q = 'INSERT INTO table VALUES (1,2,3), (4,5,6), (7,8,9)'; - deepEqual(m.sql2ast(q), { - 'INSERT INTO': {table: 'table'}, - 'VALUES': [ - ['1', '2', '3'], - ['4', '5', '6'], - ['7', '8', '9'], - ], - }, q); - - q = 'INSERT INTO table (col_A,col_B,col_C) VALUES (1,2,3), (4,5,6), (7,8,9)'; - deepEqual(m.sql2ast(q), { - 'INSERT INTO': { - table: 'table', - columns: ['col_A', 'col_B', 'col_C'], - }, - 'VALUES': [ - ['1', '2', '3'], - ['4', '5', '6'], - ['7', '8', '9'], - ], - }, q); - - q = 'INSERT INTO table VALUES (1,2,3)'; - deepEqual(m.sql2ast(q), { - 'INSERT INTO': {table: 'table'}, - 'VALUES': [['1', '2', '3']], - }, q); - - q = 'UPDATE table SET column1 = "string ()", column2=5,column3=column4, column5 = CURDATE(), column6 = FUNCTION("string ()", column7) WHERE id = 5'; - deepEqual(m.sql2ast(q), { - 'UPDATE': [{ - table: 'table', - as: '', - }], - 'SET': [ - { expression: 'column1 = "string ()"'}, - { expression: 'column2=5'}, - { expression: 'column3=column4'}, - { expression: 'column5 = CURDATE()'}, - { expression: 'column6 = FUNCTION("string ()", column7)'}, - ], - 'WHERE': {left: 'id', operator: '=', right: '5'}, - }, q); - }); - - test('Infinite loop', function() { - m.sql2ast('SELECT * FROM foo where "a'); - ok(true); - }); - - - - - module('ast2sql'); - - test('SELECT query', function() { - expect(15); - var q; - - q = 'SELECT * FROM table'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'SELECT * FROM table'; - deepEqual(m.ast2sql(m.sql2ast('select * from table')), q, q); - - q = 'SELECT * FROM table1, table2'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'SELECT t.column1, ot.column2 FROM table AS t LEFT JOIN othertable AS ot ON t.id = ot.id_table WHERE t.column3 = 5'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'SELECT t.column1, ot.column2 FROM table AS t RIGHT JOIN othertable AS ot ON t.id = ot.id_table WHERE t.column3 = 5'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'SELECT * FROM table AS t LEFT JOIN othertable AS ot ON t.id = ot.id_table LEFT JOIN othertable2 AS ot2 ON t.id = ot2.id_table AND ot2.column INNER JOIN othertable3 AS ot3 ON t.id = ot3.id_table'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'SELECT * FROM table WHERE (column1 = "something ()" AND table.column2 != column3) AND (column4 OR column5 IS NOT NULL)'; - deepEqual(m.ast2sql(m.sql2ast(q)), 'SELECT * FROM table WHERE column1 = "something ()" AND table.column2 != column3 AND (column4 OR column5 IS NOT NULL)', q); - - q = 'SELECT * FROM table ORDER BY a ASC, b DESC, c ASC'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'SELECT * FROM table GROUP BY a, b'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'SELECT * FROM table LIMIT 1'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'SELECT * FROM table LIMIT 10,1'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'SELECT * FROM table LIMIT 1'; - deepEqual(m.ast2sql({'SELECT': [{name: '*'}], 'FROM': [{table: 'table', as: ''}], 'LIMIT': {'nb': '1'}}), q, q); - - q = 'SELECT * FROM table'; - deepEqual(m.ast2sql({'SELECT': [{name: '*'}], 'FROM': [{table: 'table', as: ''}], 'LIMIT': {'nb': ''}}), q, q); - - q = 'SELECT * FROM table LIMIT 1'; - deepEqual(m.ast2sql({'SELECT': [{name: '*'}], 'FROM': [{table: 'table', as: ''}], 'LIMIT': {'from': null, 'nb': '1'}}), q, q); - - q = 'SELECT * FROM table LIMIT 1'; - deepEqual(m.ast2sql({'SELECT': [{name: '*'}], 'FROM': [{table: 'table', as: ''}], 'LIMIT': {'from': '-1', 'nb': '1'}}), q, q); - }); - - test('INSERT query', function() { - expect(4); - var q; - - q = 'INSERT INTO table (col_A, col_B, col_C) VALUES (1, 2, 3)'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'INSERT INTO table VALUES (1, 2, 3), (4, 5, 6), (7, 8, 9)'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'INSERT INTO table (col_A, col_B, col_C) VALUES (1, 2, 3), (4, 5, 6), (7, 8, 9)'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'INSERT INTO table VALUES (1, 2, 3)'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - }); - - test('DELETE query', function() { - expect(2); - var q; - - q = 'DELETE FROM table WHERE id = 5'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'DELETE FROM table1, table2 WHERE id = 5'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - }); - - test('UPDATE query', function() { - expect(3); - var q; - - q = 'UPDATE table SET column = 1'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'UPDATE table1, table2 SET column = 1'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - - q = 'UPDATE table SET column1 = "string ()", column2=5, column3=column4, column5 = CURDATE(), column6 = FUNCTION("string ()", column7) WHERE id = 5'; - deepEqual(m.ast2sql(m.sql2ast(q)), q, q); - }); - -}());