Skip to content

Commit a4253d1

Browse files
committed
[NEW] supports postgresql-style string escape (e.g. E'foobar')
[NEW] supports postgresql-style interval values (e.g. interval '2 days') [NEW] supports standalone BEGIN and COMMIT statements [NEW] supports postgresql's SET statements (e.g. SET client_min_messages TO 'panic')
1 parent 140ca9d commit a4253d1

File tree

7 files changed

+100
-5
lines changed

7 files changed

+100
-5
lines changed

lib/sql_tree/node/expression.rb

+37-4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ def self.parse_atomic(tokens)
4949
else
5050
Variable.parse(tokens)
5151
end
52+
elsif SQLTree::Token::STRING_ESCAPE == tokens.peek
53+
tokens.consume(SQLTree::Token::STRING_ESCAPE)
54+
Value.parse(tokens)
55+
elsif SQLTree::Token::INTERVAL === tokens.peek
56+
IntervalValue.parse(tokens)
5257
else
5358
Value.parse(tokens)
5459
end
@@ -333,7 +338,35 @@ def self.parse(tokens)
333338
return function_call
334339
end
335340
end
336-
341+
342+
343+
# Represents a postgresql INTERVAL value. Example: interval '2 days'.
344+
#
345+
# The value is the literal text of the interval (e.g. "2 days").
346+
class IntervalValue < SQLTree::Node::Expression
347+
# The actual value this node represents.
348+
leaf :value
349+
350+
def initialize(value) # :nodoc:
351+
@value = value
352+
end
353+
354+
# Generates an SQL representation for this value.
355+
def to_sql(options = {})
356+
"interval " + quote_str(@value)
357+
end
358+
359+
def self.parse(tokens)
360+
tokens.consume(SQLTree::Token::INTERVAL)
361+
if SQLTree::Token::String === tokens.peek
362+
self.new(tokens.next.literal)
363+
else
364+
raise SQLTree::Parser::UnexpectedToken.new(tokens.current, :literal)
365+
end
366+
end
367+
end
368+
369+
337370
# Represents alitreal value in an SQL expression. This node is a leaf node
338371
# and thus has no child nodes.
339372
#
@@ -345,14 +378,14 @@ def self.parse(tokens)
345378
# * an integer or decimal value, which is represented by an appropriate
346379
# <tt>Numeric</tt> instance.
347380
class Value < SQLTree::Node::Expression
348-
381+
349382
# The actual value this node represents.
350383
leaf :value
351384

352385
def initialize(value) # :nodoc:
353386
@value = value
354387
end
355-
388+
356389
# Generates an SQL representation for this value.
357390
#
358391
# This method supports nil, string, numeric, date and time values.
@@ -389,7 +422,7 @@ def self.parse(tokens)
389422
end
390423
end
391424
end
392-
425+
393426
# Represents a variable within an SQL expression. This is a leaf node, so it
394427
# does not have any child nodes. A variale can point to a field of a table or
395428
# to another expression that was declared elsewhere.

lib/sql_tree/parser.rb

+3
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ def parse!
116116
when SQLTree::Token::INSERT then SQLTree::Node::InsertQuery.parse(self)
117117
when SQLTree::Token::DELETE then SQLTree::Node::DeleteQuery.parse(self)
118118
when SQLTree::Token::UPDATE then SQLTree::Node::UpdateQuery.parse(self)
119+
when SQLTree::Token::BEGIN then SQLTree::Node::BeginStatement.parse(self)
120+
when SQLTree::Token::COMMIT then SQLTree::Node::CommitStatement.parse(self)
121+
when SQLTree::Token::SET then SQLTree::Node::SetQuery.parse(self)
119122
else raise UnexpectedToken.new(self.peek)
120123
end
121124
end

lib/sql_tree/token.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,12 @@ def inspect # :nodoc:
140140
RPAREN = Class.new(SQLTree::Token).new(')')
141141
DOT = Class.new(SQLTree::Token).new('.')
142142
COMMA = Class.new(SQLTree::Token).new(',')
143+
STRING_ESCAPE = Class.new(SQLTree::Token).new('E')
143144

144145
# A list of all the SQL reserverd keywords.
145146
KEYWORDS = %w{SELECT FROM WHERE GROUP HAVING ORDER DISTINCT LEFT RIGHT INNER FULL OUTER NATURAL JOIN USING
146-
AND OR NOT AS ON IS NULL BY LIKE ILIKE BETWEEN IN ASC DESC INSERT INTO VALUES DELETE UPDATE SET}
147+
AND OR NOT AS ON IS NULL BY LIKE ILIKE BETWEEN IN ASC DESC INSERT INTO VALUES DELETE UPDATE
148+
SET BEGIN COMMIT TO INTERVAL}
147149

148150
# Create a token for all the reserved keywords in SQL
149151
KEYWORDS.each { |kw| const_set(kw, Class.new(SQLTree::Token::Keyword)) }

lib/sql_tree/tokenizer.rb

+12
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def each_token(&block) # :yields: SQLTree::Token
9494
when ','; handle_token(SQLTree::Token::COMMA, &block)
9595
when /\d/; tokenize_number(&block)
9696
when "'"; tokenize_quoted_string(&block)
97+
when 'E'; tokenize_possible_escaped_string(&block)
9798
when /\w/; tokenize_keyword(&block)
9899
when OPERATOR_CHARS; tokenize_operator(&block)
99100
when SQLTree.identifier_quote_char; tokenize_quoted_identifier(&block)
@@ -106,6 +107,17 @@ def each_token(&block) # :yields: SQLTree::Token
106107

107108
alias :each :each_token
108109

110+
# Tokenizes a something that could be a 'postgresql'-styled string (e.g.
111+
# E'foobar'). If the very next char isn't a single quote, then it falls back
112+
# to tokenizing a keyword.
113+
def tokenize_possible_escaped_string(&block)
114+
if peek_char == "'"
115+
handle_token(SQLTree::Token::STRING_ESCAPE, &block)
116+
else
117+
tokenize_keyword(&block)
118+
end
119+
end
120+
109121
# Tokenizes a eyword in the code. This can either be a reserved SQL keyword
110122
# or a variable. This method will yield variables directly. Keywords will be
111123
# yielded with a delay, because they may need to be combined with other

spec/integration/parse_and_generate_spec.rb

+19
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,23 @@
8080
SQLTree['UPDATE table SET field1 = 123 WHERE id = 17'].to_sql.should ==
8181
'UPDATE "table" SET "field1" = 123 WHERE ("id" = 17)'
8282
end
83+
84+
it "should parse and generate a SET query" do
85+
SQLTree["SET client_min_messages TO 'panic'"].to_sql.should ==
86+
"SET \"client_min_messages\" TO 'panic'"
87+
end
88+
89+
it "should parse and generate a BEGIN statement" do
90+
SQLTree["BEGIN"].to_sql.should == "BEGIN"
91+
end
92+
93+
it "should parse and generate a COMMIT statement" do
94+
SQLTree["COMMIT"].to_sql.should == "COMMIT"
95+
end
96+
97+
# TODO: make this work
98+
# it "should parse and generate a SELECT query with a function that takes star as arg" do
99+
# SQLTree["SELECT count(*) FROM jobs"].to_sql.should ==
100+
# "SELECT count(*) FROM jobs"
101+
# end
83102
end

spec/unit/expression_node_spec.rb

+10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@
1919
function.arguments.should == [SQLTree::Node::Expression::Value.new('string')]
2020
end
2121

22+
it "shoud parse an escaped string correctly" do
23+
string = SQLTree::Node::Expression["E'string'"]
24+
string.should == SQLTree::Node::Expression::Value.new("string")
25+
end
26+
27+
it "should parse a postgresql interval expression correctly" do
28+
interval = SQLTree::Node::Expression["interval '2 hours'"]
29+
interval.value.should == "2 hours"
30+
end
31+
2232
it "should parse a logical OR expression correctly" do
2333
logical = SQLTree::Node::Expression["'this' OR 'that"]
2434
logical.operator.should == 'OR'

spec/unit/tokenizer_spec.rb

+16
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
SQLTree::Tokenizer.tokenize('and').should tokenize_to(:and)
1212
end
1313

14+
it "should tokenize a begin SQL keyword" do
15+
SQLTree::Tokenizer.tokenize('BEGIN').should tokenize_to(:begin)
16+
end
17+
1418
it "should tokenize muliple separate keywords" do
1519
SQLTree::Tokenizer.tokenize('SELECT DISTINCT').should tokenize_to(:select, :distinct)
1620
end
@@ -58,6 +62,14 @@
5862
it "should tokenize commas" do
5963
SQLTree::Tokenizer.tokenize('a , "b"').should tokenize_to(sql_var('a'), comma, sql_var('b'))
6064
end
65+
66+
it "should tokenize postgresql string escape token" do
67+
SQLTree::Tokenizer.tokenize("E'foo'").should tokenize_to(:string_escape, "foo")
68+
end
69+
70+
it "should tokenize postgresql interval statements" do
71+
SQLTree::Tokenizer.tokenize("interval '2 days'").should tokenize_to(:interval, "2 days")
72+
end
6173
end
6274

6375
# # Combined tokens are disabled for now;
@@ -77,6 +89,10 @@
7789
it "should tokenize a function call" do
7890
SQLTree::Tokenizer.tokenize("MD5('test')").should tokenize_to(sql_var('MD5'), lparen, 'test', rparen)
7991
end
92+
93+
it "should tokenize a posgresql SET call" do
94+
SQLTree::Tokenizer.tokenize("SET client_min_messages TO 'panic'").should tokenize_to(:set, sql_var('client_min_messages'), :to, 'panic')
95+
end
8096
end
8197

8298
end

0 commit comments

Comments
 (0)