diff --git a/browser/sql-parser.js b/browser/sql-parser.js index 4ebe726..3c7f5d6 100644 --- a/browser/sql-parser.js +++ b/browser/sql-parser.js @@ -55,6 +55,27 @@ return this.tokens.push([name, value, this.currentLine]); }; + Lexer.prototype.tokenizeFromStringRegex = function(name, regex, part, lengthPart, output) { + var match, partMatch; + if (part == null) { + part = 0; + } + if (lengthPart == null) { + lengthPart = part; + } + if (output == null) { + output = true; + } + if (!(match = regex.exec(this.chunk))) { + return 0; + } + partMatch = match[part].replace(/''/g, "'"); + if (output) { + this.token(name, partMatch); + } + return match[lengthPart].length; + }; + Lexer.prototype.tokenizeFromRegex = function(name, regex, part, lengthPart, output) { var match, partMatch; if (part == null) { @@ -169,7 +190,7 @@ }; Lexer.prototype.stringToken = function() { - return this.tokenizeFromRegex('STRING', STRING, 1, 0) || this.tokenizeFromRegex('DBLSTRING', DBLSTRING, 1, 0); + return this.tokenizeFromStringRegex('STRING', STRING, 1, 0) || this.tokenizeFromRegex('DBLSTRING', DBLSTRING, 1, 0); }; Lexer.prototype.parensToken = function() { @@ -221,7 +242,7 @@ BOOLEAN = ['TRUE', 'FALSE', 'NULL']; - MATH = ['+', '-']; + MATH = ['+', '-', '||', '&&']; MATH_MULTI = ['/', '*']; @@ -237,7 +258,7 @@ NUMBER = /^[0-9]+(\.[0-9]+)?/; - STRING = /^'([^\\']*(?:\\.[^\\']*)*)'/; + STRING = /^'((?:[^\\']+?|\\.|'')*)'(?!')/; DBLSTRING = /^"([^\\"]*(?:\\.[^\\"]*)*)"/; @@ -886,7 +907,9 @@ if (typeof module !== 'undefined' && require.main === module) { } StringValue.prototype.toString = function() { - return "" + this.quoteType + this.value + this.quoteType; + var escaped; + escaped = this.quoteType === "'" ? this.value.replace(/(^|[^\\])'/g, "$1''") : this.value; + return "" + this.quoteType + escaped + this.quoteType; }; return StringValue; diff --git a/lib/lexer.js b/lib/lexer.js index 3f0ed9c..dffd850 100644 --- a/lib/lexer.js +++ b/lib/lexer.js @@ -50,6 +50,27 @@ return this.tokens.push([name, value, this.currentLine]); }; + Lexer.prototype.tokenizeFromStringRegex = function(name, regex, part, lengthPart, output) { + var match, partMatch; + if (part == null) { + part = 0; + } + if (lengthPart == null) { + lengthPart = part; + } + if (output == null) { + output = true; + } + if (!(match = regex.exec(this.chunk))) { + return 0; + } + partMatch = match[part].replace(/''/g, "'"); + if (output) { + this.token(name, partMatch); + } + return match[lengthPart].length; + }; + Lexer.prototype.tokenizeFromRegex = function(name, regex, part, lengthPart, output) { var match, partMatch; if (part == null) { @@ -164,7 +185,7 @@ }; Lexer.prototype.stringToken = function() { - return this.tokenizeFromRegex('STRING', STRING, 1, 0) || this.tokenizeFromRegex('DBLSTRING', DBLSTRING, 1, 0); + return this.tokenizeFromStringRegex('STRING', STRING, 1, 0) || this.tokenizeFromRegex('DBLSTRING', DBLSTRING, 1, 0); }; Lexer.prototype.parensToken = function() { @@ -216,7 +237,7 @@ BOOLEAN = ['TRUE', 'FALSE', 'NULL']; - MATH = ['+', '-']; + MATH = ['+', '-', '||', '&&']; MATH_MULTI = ['/', '*']; @@ -232,7 +253,7 @@ NUMBER = /^[0-9]+(\.[0-9]+)?/; - STRING = /^'([^\\']*(?:\\.[^\\']*)*)'/; + STRING = /^'((?:[^\\']+?|\\.|'')*)'(?!')/; DBLSTRING = /^"([^\\"]*(?:\\.[^\\"]*)*)"/; diff --git a/lib/nodes.js b/lib/nodes.js index 6bb9bb9..b93ace6 100644 --- a/lib/nodes.js +++ b/lib/nodes.js @@ -154,7 +154,9 @@ } StringValue.prototype.toString = function() { - return "" + this.quoteType + this.value + this.quoteType; + var escaped; + escaped = this.quoteType === "'" ? this.value.replace(/(^|[^\\])'/g, "$1''") : this.value; + return "" + this.quoteType + escaped + this.quoteType; }; return StringValue; diff --git a/src/lexer.coffee b/src/lexer.coffee index 4cb3d8b..249aa91 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -41,6 +41,12 @@ class Lexer token: (name, value) -> @tokens.push([name, value, @currentLine]) + tokenizeFromStringRegex: (name, regex, part=0, lengthPart=part, output=true) -> + return 0 unless match = regex.exec(@chunk) + partMatch = match[part].replace(/''/g, "'") + @token(name, partMatch) if output + return match[lengthPart].length + tokenizeFromRegex: (name, regex, part=0, lengthPart=part, output=true) -> return 0 unless match = regex.exec(@chunk) partMatch = match[part] @@ -112,7 +118,7 @@ class Lexer numberToken: -> @tokenizeFromRegex('NUMBER', NUMBER) parameterToken: -> @tokenizeFromRegex('PARAMETER', PARAMETER) stringToken: -> - @tokenizeFromRegex('STRING', STRING, 1, 0) || + @tokenizeFromStringRegex('STRING', STRING, 1, 0) || @tokenizeFromRegex('DBLSTRING', DBLSTRING, 1, 0) @@ -146,7 +152,7 @@ class Lexer SQL_CONDITIONALS = ['AND', 'OR'] SQL_BETWEENS = ['BETWEEN', 'NOT BETWEEN'] BOOLEAN = ['TRUE', 'FALSE', 'NULL'] - MATH = ['+', '-'] + MATH = ['+', '-', '||', '&&'] MATH_MULTI = ['/', '*'] STAR = /^\*/ SEPARATOR = /^,/ @@ -154,7 +160,7 @@ class Lexer LITERAL = /^`?([a-z_][a-z0-9_]{0,})`?/i PARAMETER = /^\$[0-9]+/ NUMBER = /^[0-9]+(\.[0-9]+)?/ - STRING = /^'([^\\']*(?:\\.[^\\']*)*)'/ + STRING = /^'((?:[^\\']+?|\\.|'')*)'(?!')/ DBLSTRING = /^"([^\\"]*(?:\\.[^\\"]*)*)"/ diff --git a/src/nodes.coffee b/src/nodes.coffee index 119fd43..f5f7eb1 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -55,7 +55,9 @@ exports.LiteralValue = class LiteralValue exports.StringValue = class StringValue constructor: (@value, @quoteType="''") -> null - toString: -> "#{@quoteType}#{@value}#{@quoteType}" + toString: -> + escaped = if @quoteType is "'" then @value.replace /(^|[^\\])'/g, "$1''" else @value + "#{@quoteType}#{escaped}#{@quoteType}" exports.NumberValue = class LiteralValue constructor: (value) -> @value = Number(value) diff --git a/test/grammar.spec.coffee b/test/grammar.spec.coffee index c681f7c..b51ed5f 100644 --- a/test/grammar.spec.coffee +++ b/test/grammar.spec.coffee @@ -292,6 +292,13 @@ describe "SQL Grammar", -> WHERE (`foo` = 'I\\'m') """ + it "parses single quote", -> + parse("select * from a where foo = ''''").toString().should.eql """ + SELECT * + FROM `a` + WHERE (`foo` = '''') + """ + it "allows using double quotes", -> parse('select * from a where foo = "a"').toString().should.eql """ SELECT * @@ -299,6 +306,13 @@ describe "SQL Grammar", -> WHERE (`foo` = "a") """ + it "allows using two single quotes", -> + parse("select * from a where foo = 'I''m'").toString().should.eql """ + SELECT * + FROM `a` + WHERE (`foo` = 'I''m') + """ + it "allows nesting different quote styles", -> parse("""select * from a where foo = "I'm" """).toString().should.eql """ SELECT *