sql_tree 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -1
- data/README.rdoc +6 -5
- data/lib/sql_tree.rb +11 -3
- data/lib/sql_tree/node.rb +75 -4
- data/lib/sql_tree/node/delete_query.rb +19 -6
- data/lib/sql_tree/node/expression.rb +431 -163
- data/lib/sql_tree/node/insert_query.rb +13 -17
- data/lib/sql_tree/node/join.rb +8 -12
- data/lib/sql_tree/node/ordering.rb +4 -3
- data/lib/sql_tree/node/select_declaration.rb +62 -0
- data/lib/sql_tree/node/select_query.rb +27 -46
- data/lib/sql_tree/node/source.rb +5 -8
- data/lib/sql_tree/node/table_reference.rb +7 -10
- data/lib/sql_tree/node/update_query.rb +73 -12
- data/lib/sql_tree/parser.rb +70 -14
- data/lib/sql_tree/token.rb +48 -22
- data/lib/sql_tree/tokenizer.rb +23 -16
- data/spec/integration/parse_and_generate_spec.rb +8 -0
- data/spec/lib/matchers.rb +3 -2
- data/spec/unit/delete_query_spec.rb +2 -2
- data/spec/unit/expression_node_spec.rb +24 -23
- data/spec/unit/insert_query_spec.rb +8 -8
- data/spec/unit/leaf_node_spec.rb +17 -17
- data/spec/unit/select_query_spec.rb +4 -4
- data/spec/unit/tokenizer_spec.rb +17 -21
- data/spec/unit/update_query_spec.rb +6 -6
- data/sql_tree.gemspec +3 -3
- metadata +4 -8
- data/lib/sql_tree/node/assignment.rb +0 -22
- data/lib/sql_tree/node/field.rb +0 -49
- data/lib/sql_tree/node/select_expression.rb +0 -45
- data/lib/sql_tree/node/value.rb +0 -33
- data/lib/sql_tree/node/variable.rb +0 -31
data/lib/sql_tree/parser.rb
CHANGED
@@ -1,10 +1,23 @@
|
|
1
|
+
# The <tt>SQLTree::Parser</tt> class is used to construct a syntax tree
|
2
|
+
# using <tt>SQLTree::Node</tt> instances from a tree of tokens.
|
3
|
+
#
|
4
|
+
# This class does only kickstart the parsing process and manages the
|
5
|
+
# token stream that is being parsed. The actual parsing of the nodes
|
6
|
+
# occurs in the <tt>parse</tt> class method of the different node classes.
|
1
7
|
class SQLTree::Parser
|
2
8
|
|
9
|
+
# The <tt>SQLTree::Parser::UnexpectedToken</tt> exception is thrown
|
10
|
+
# when the parser meets a token that it not expect.
|
11
|
+
#
|
12
|
+
# This exceptions usually means that an SQL syntax error has been found,
|
13
|
+
# however it can also mean that the SQL construct that is being used is
|
14
|
+
# not (yet) supported by this library. Please create an issue on Github
|
15
|
+
# if the latter is the case.
|
3
16
|
class UnexpectedToken < StandardError
|
4
17
|
|
5
18
|
attr_reader :expected_token, :actual_token
|
6
19
|
|
7
|
-
def initialize(actual_token, expected_token = nil)
|
20
|
+
def initialize(actual_token, expected_token = nil) # :nodoc:
|
8
21
|
@expected_token, @actual_token = expected_token, actual_token
|
9
22
|
message = "Unexpected token: found #{actual_token.inspect}"
|
10
23
|
message << ", but expected #{expected_token.inspect}" if expected_token
|
@@ -12,48 +25,91 @@ class SQLTree::Parser
|
|
12
25
|
super(message)
|
13
26
|
end
|
14
27
|
end
|
15
|
-
|
28
|
+
|
29
|
+
# Kickstarts the parser by creating a new instance with the provided
|
30
|
+
# string, and calling the <tt>parse!</tt> method on this instance.
|
31
|
+
#
|
32
|
+
# Do not use this method directly, but use the <tt>SQLTree.[]</tt>
|
33
|
+
# method instead to parse SQL strings.
|
34
|
+
#
|
35
|
+
# <tt>sql_string</tt>:: The string to parse
|
36
|
+
# <tt>options</tt>:: Options to pass to the parser
|
16
37
|
def self.parse(sql_string, options = {})
|
17
38
|
self.new(sql_string, options).parse!
|
18
39
|
end
|
19
40
|
|
41
|
+
# Hash for parser options.
|
20
42
|
attr_reader :options
|
21
43
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
44
|
+
# Initializes the parser instance.
|
45
|
+
# <tt>tokens</tt>:: The stream of tokens to turn into a syntax tree. If a
|
46
|
+
# string is given, it is tokenized automatically.
|
47
|
+
# <tt>options</tt>:: An optional hash of parser options.
|
48
|
+
def initialize(tokens, options = {})
|
49
|
+
@tokens = tokens.kind_of?(String) ? SQLTree::Tokenizer.tokenize(tokens) : tokens
|
28
50
|
@options = options
|
29
51
|
end
|
30
52
|
|
53
|
+
# Returns the current token that is being parsed.
|
31
54
|
def current
|
32
55
|
@current_token
|
33
56
|
end
|
34
57
|
|
58
|
+
# Returns the next token on the token queue, and moves the token queue
|
59
|
+
# one position forward. This will update the result of the
|
60
|
+
# <tt>SQLTree::Parser#current</tt> method.
|
35
61
|
def next
|
36
62
|
@current_token = @tokens.shift
|
37
63
|
end
|
38
64
|
|
39
|
-
|
65
|
+
# Consumes the current token(s), which will make the parser continue to the
|
66
|
+
# next token (see <tt>SQLTree::Parser#next</tt>).
|
67
|
+
#
|
68
|
+
# This method will also check if the consumed token is of the expected type.
|
69
|
+
# It will raise a <tt>SQLTree::Parser::UnexpectedToken</tt> exception if the
|
70
|
+
# consumed token is not of the expected type
|
71
|
+
#
|
72
|
+
# <tt>*checks</tt>:: a list of token types to consume.
|
73
|
+
def consume(*checks) # :raises: SQLTree::Parser::UnexpectedToken
|
40
74
|
checks.each do |check|
|
41
|
-
raise UnexpectedToken.new(self.current, check) unless check
|
75
|
+
raise UnexpectedToken.new(self.current, check) unless check === self.next
|
42
76
|
end
|
43
77
|
end
|
44
78
|
|
45
|
-
|
46
|
-
|
79
|
+
# Looks at the next token on the token queue without consuming it.
|
80
|
+
#
|
81
|
+
# The token queue will not be adjusted, will is the case when using
|
82
|
+
# <tt>SQLTree::Parser#next</tt>.
|
83
|
+
#
|
84
|
+
# <tt>lookahead</tt>:: the number of positions to look ahead. Defaults to 1.
|
85
|
+
def peek(lookahead = 1)
|
86
|
+
@tokens[lookahead - 1]
|
47
87
|
end
|
48
88
|
|
49
|
-
|
50
|
-
|
89
|
+
# Peeks multiple tokens at the same time.
|
90
|
+
#
|
91
|
+
# This method will return an array of the requested number of tokens,
|
92
|
+
# except for when the token stream is nearing its end. In this case, the
|
93
|
+
# number of tokens returned can be less than requested.
|
94
|
+
# <tt>lookahead_amount</tt>:: the amount of tokens to return from the
|
95
|
+
# front of the token queue.
|
96
|
+
def peek_multiple(lookahead_amount)
|
97
|
+
@tokens[0, lookahead_amount]
|
51
98
|
end
|
52
99
|
|
100
|
+
# Prints the current list of tokens to $stdout for inspection.
|
53
101
|
def debug
|
54
102
|
puts @tokens.inspect
|
55
103
|
end
|
56
104
|
|
105
|
+
# Parser a complete SQL query into a tree of <tt>SQLTree::Node</tt> instances.
|
106
|
+
#
|
107
|
+
# Currently, SELECT, INSERT, UPDATE and DELETE queries are supported for
|
108
|
+
# the most part. This emthod should not be called directly, but is called
|
109
|
+
# by the <tt>SQLTree.[]</tt> method, e.g.:
|
110
|
+
#
|
111
|
+
# tree = SQLTree['SELECT * FROM table WHERE 1=1']
|
112
|
+
#
|
57
113
|
def parse!
|
58
114
|
case self.peek
|
59
115
|
when SQLTree::Token::SELECT then SQLTree::Node::SelectQuery.parse(self)
|
data/lib/sql_tree/token.rb
CHANGED
@@ -30,24 +30,50 @@ class SQLTree::Token
|
|
30
30
|
literal
|
31
31
|
end
|
32
32
|
|
33
|
+
def not?
|
34
|
+
SQLTree::Token::NOT === self
|
35
|
+
end
|
36
|
+
|
37
|
+
def optional_not_prefix?
|
38
|
+
[SQLTree::Token::LIKE, SQLTree::Token::ILIKE, SQLTree::Token::IN].include?(self.class)
|
39
|
+
end
|
40
|
+
|
41
|
+
def optional_not_suffix?
|
42
|
+
[SQLTree::Token::IS].include?(self.class)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns true if this is a JOIN token?
|
33
46
|
def join?
|
34
47
|
[SQLTree::Token::JOIN, SQLTree::Token::LEFT, SQLTree::Token::RIGHT,
|
35
48
|
SQLTree::Token::INNER, SQLTree::Token::OUTER, SQLTree::Token::NATURAL,
|
36
|
-
SQLTree::Token::FULL].include?(self)
|
49
|
+
SQLTree::Token::FULL].include?(self.class)
|
50
|
+
end
|
51
|
+
|
52
|
+
def prefix_operator?
|
53
|
+
SQLTree::Node::Expression::PrefixOperator::TOKENS.include?(self.class)
|
54
|
+
end
|
55
|
+
|
56
|
+
def variable?
|
57
|
+
self.kind_of?(SQLTree::Token::Identifier)
|
58
|
+
end
|
59
|
+
|
60
|
+
def value?
|
61
|
+
self.kind_of?(SQLTree::Token::LiteralValue)
|
37
62
|
end
|
38
63
|
|
64
|
+
# Returns true if this is an order direction token.
|
39
65
|
def direction?
|
40
|
-
[SQLTree::Token::ASC, SQLTree::Token::DESC].include?(self)
|
66
|
+
[SQLTree::Token::ASC, SQLTree::Token::DESC].include?(self.class)
|
41
67
|
end
|
42
68
|
|
43
69
|
###################################################################
|
44
70
|
# DYNAMIC TOKEN TYPES
|
45
71
|
###################################################################
|
46
72
|
|
47
|
-
# The <tt>SQLTree::Token::
|
73
|
+
# The <tt>SQLTree::Token::DynamicToken</tt> class is the base class for
|
48
74
|
# every dynamic token. A dynamic token is a token for which the
|
49
75
|
# literal value used remains impoirtant during parsing.
|
50
|
-
class
|
76
|
+
class DynamicToken < SQLTree::Token
|
51
77
|
|
52
78
|
def inspect # :nodoc:
|
53
79
|
"#<#{self.class.name.split('::').last}:#{literal.inspect}>"
|
@@ -59,22 +85,27 @@ class SQLTree::Token
|
|
59
85
|
other.class == self.class && @literal == other.literal
|
60
86
|
end
|
61
87
|
end
|
62
|
-
|
63
|
-
# The <tt>SQLTree::Token::
|
88
|
+
|
89
|
+
# The <tt>SQLTree::Token::Identifier</tt> class represents SQL
|
64
90
|
# variables. The variable name is stored in the literal as string,
|
65
91
|
# without quotes if they were present.
|
66
|
-
class
|
92
|
+
class Identifier < DynamicToken
|
93
|
+
end
|
94
|
+
|
95
|
+
# The <tt>SQLTree::Token::LiteralVakue</tt> class represents literal values
|
96
|
+
# like strings and numbers that occur in SQL.
|
97
|
+
class LiteralValue < DynamicToken
|
67
98
|
end
|
68
99
|
|
69
100
|
# The <tt>SQLTree::Token::String</tt> class represents strings.
|
70
101
|
# The actual string is stored in the literal as string without quotes.
|
71
|
-
class String <
|
102
|
+
class String < LiteralValue
|
72
103
|
end
|
73
104
|
|
74
105
|
# The <tt>SQLTree::Token::Keyword</tt> class represents numbers.
|
75
106
|
# The actual number is stored as an integer or float in the token's
|
76
107
|
# literal.
|
77
|
-
class Number <
|
108
|
+
class Number < LiteralValue
|
78
109
|
end
|
79
110
|
|
80
111
|
###################################################################
|
@@ -115,21 +146,16 @@ class SQLTree::Token
|
|
115
146
|
AND OR NOT AS ON IS NULL BY LIKE ILIKE BETWEEN IN ASC DESC INSERT INTO VALUES DELETE UPDATE SET}
|
116
147
|
|
117
148
|
# Create a token for all the reserved keywords in SQL
|
118
|
-
KEYWORDS.each { |kw| const_set(kw, Class.new(SQLTree::Token::Keyword)
|
149
|
+
KEYWORDS.each { |kw| const_set(kw, Class.new(SQLTree::Token::Keyword)) }
|
119
150
|
|
120
|
-
|
121
|
-
|
122
|
-
|
151
|
+
OPERATORS_HASH = { '+' => :plus, '-' => :minus, '*' => :multiply,
|
152
|
+
'/' => :divide, '%' => :modulo, '||' => :concat, '|' => :binary_or,
|
153
|
+
'&' => :binary_and, '<<' => :lshift, '>>' => :rshift, '=' => :eq,
|
154
|
+
'!=' => :ne, '<>' => :ne, '>' => :gt, '<' => :lt, '>=' => :gte,
|
155
|
+
'<=' => :lte, '==' => :eq }
|
123
156
|
|
124
|
-
|
125
|
-
COMPARISON_OPERATORS_HASH = { '=' => :eq, '!=' => :ne, '<>' => :ne, '>' => :gt, '<' => :lt, '>=' => :gte, '<=' => :lte }
|
126
|
-
|
127
|
-
# Register a token class and single instance for every token.
|
128
|
-
OPERATORS_HASH = ARITHMETHIC_OPERATORS_HASH.merge(COMPARISON_OPERATORS_HASH)
|
157
|
+
# Register a token class and single instance for every operator.
|
129
158
|
OPERATORS_HASH.each_pair do |literal, symbol|
|
130
|
-
self.const_set(symbol.to_s.upcase, Class.new(SQLTree::Token::Operator)
|
159
|
+
self.const_set(symbol.to_s.upcase, Class.new(SQLTree::Token::Operator)) unless self.const_defined?(symbol.to_s.upcase)
|
131
160
|
end
|
132
|
-
|
133
|
-
COMPARISON_OPERATORS = COMPARISON_OPERATORS_HASH.map { |(literal, symbol)| const_get(symbol.to_s.upcase) } +
|
134
|
-
[SQLTree::Token::IN, SQLTree::Token::IS, SQLTree::Token::BETWEEN, SQLTree::Token::LIKE, SQLTree::Token::ILIKE, SQLTree::Token::NOT]
|
135
161
|
end
|
data/lib/sql_tree/tokenizer.rb
CHANGED
@@ -14,19 +14,24 @@ class SQLTree::Tokenizer
|
|
14
14
|
|
15
15
|
include Enumerable
|
16
16
|
|
17
|
+
# Returns an array of tokens for the given string.
|
18
|
+
# <tt>string</tt>:: the string to tokenize
|
19
|
+
def self.tokenize(string)
|
20
|
+
self.new(string).tokens
|
21
|
+
end
|
22
|
+
|
17
23
|
# The keyword queue, on which kywords are placed before they are yielded
|
18
24
|
# to the parser, to enable keyword combining (e.g. NOT LIKE)
|
19
25
|
attr_reader :keyword_queue
|
20
26
|
|
21
|
-
def initialize # :nodoc:
|
27
|
+
def initialize(string) # :nodoc:
|
28
|
+
@string = string
|
29
|
+
@current_char_pos = -1
|
22
30
|
@keyword_queue = []
|
23
31
|
end
|
24
32
|
|
25
|
-
#
|
26
|
-
|
27
|
-
def tokenize(string)
|
28
|
-
@string = string
|
29
|
-
@current_char_pos = -1
|
33
|
+
# Tokeinzes the string and returns all tokens as an array
|
34
|
+
def tokens
|
30
35
|
self.entries
|
31
36
|
end
|
32
37
|
|
@@ -79,6 +84,7 @@ class SQLTree::Tokenizer
|
|
79
84
|
# This method is aliased to <tt>:each</tt> to make the Enumerable
|
80
85
|
# methods work on this method.
|
81
86
|
def each_token(&block) # :yields: SQLTree::Token
|
87
|
+
|
82
88
|
while next_char
|
83
89
|
case current_char
|
84
90
|
when /^\s?$/; # whitespace, go to next character
|
@@ -88,9 +94,9 @@ class SQLTree::Tokenizer
|
|
88
94
|
when ','; handle_token(SQLTree::Token::COMMA, &block)
|
89
95
|
when /\d/; tokenize_number(&block)
|
90
96
|
when "'"; tokenize_quoted_string(&block)
|
91
|
-
when OPERATOR_CHARS; tokenize_operator(&block)
|
92
97
|
when /\w/; tokenize_keyword(&block)
|
93
|
-
when
|
98
|
+
when OPERATOR_CHARS; tokenize_operator(&block)
|
99
|
+
when SQLTree.identifier_quote_char; tokenize_quoted_identifier(&block)
|
94
100
|
end
|
95
101
|
end
|
96
102
|
|
@@ -109,9 +115,9 @@ class SQLTree::Tokenizer
|
|
109
115
|
literal << next_char while /[\w]/ =~ peek_char
|
110
116
|
|
111
117
|
if SQLTree::Token::KEYWORDS.include?(literal.upcase)
|
112
|
-
handle_token(SQLTree::Token.const_get(literal.upcase), &block)
|
118
|
+
handle_token(SQLTree::Token.const_get(literal.upcase).new(literal), &block)
|
113
119
|
else
|
114
|
-
handle_token(SQLTree::Token::
|
120
|
+
handle_token(SQLTree::Token::Identifier.new(literal), &block)
|
115
121
|
end
|
116
122
|
end
|
117
123
|
|
@@ -145,20 +151,20 @@ class SQLTree::Tokenizer
|
|
145
151
|
end
|
146
152
|
|
147
153
|
# Tokenize a quoted variable from the SQL stream. This method will
|
148
|
-
# yield an SQLTree::Token::
|
154
|
+
# yield an SQLTree::Token::Identifier when to closing quote is found.
|
149
155
|
#
|
150
156
|
# The actual quote character that is used depends on the DBMS. For now,
|
151
157
|
# only the more standard double quote is accepted.
|
152
|
-
def
|
158
|
+
def tokenize_quoted_identifier(&block) # :yields: SQLTree::Token::Identifier
|
153
159
|
variable = ''
|
154
|
-
until next_char.nil? || current_char ==
|
160
|
+
until next_char.nil? || current_char == SQLTree.identifier_quote_char # TODO: allow MySQL quoting mode
|
155
161
|
variable << (current_char == "\\" ? next_char : current_char)
|
156
162
|
end
|
157
|
-
handle_token(SQLTree::Token::
|
163
|
+
handle_token(SQLTree::Token::Identifier.new(variable), &block)
|
158
164
|
end
|
159
165
|
|
160
166
|
# A regular expression that matches all operator characters.
|
161
|
-
OPERATOR_CHARS =
|
167
|
+
OPERATOR_CHARS = /\=|<|>|!|\-|\+|\/|\*|\%|\||\&/
|
162
168
|
|
163
169
|
# Tokenizes an operator in the SQL stream. This method will yield the
|
164
170
|
# operator token when the last character of the token is encountered.
|
@@ -168,7 +174,8 @@ class SQLTree::Tokenizer
|
|
168
174
|
tokenize_number(&block)
|
169
175
|
else
|
170
176
|
operator << next_char if SQLTree::Token::OPERATORS_HASH.has_key?(operator + peek_char)
|
171
|
-
|
177
|
+
operator_class = SQLTree::Token.const_get(SQLTree::Token::OPERATORS_HASH[operator].to_s.upcase)
|
178
|
+
handle_token(operator_class.new(operator), &block)
|
172
179
|
end
|
173
180
|
end
|
174
181
|
end
|
@@ -2,9 +2,17 @@ require "#{File.dirname(__FILE__)}/../spec_helper"
|
|
2
2
|
|
3
3
|
describe SQLTree, 'parsing and generating SQL' do
|
4
4
|
|
5
|
+
before(:each) { SQLTree.identifier_quote_char = '"' }
|
6
|
+
|
5
7
|
it "should parse an generate q query without FROM" do
|
6
8
|
SQLTree['SELECT 1'].to_sql.should == 'SELECT 1'
|
7
9
|
end
|
10
|
+
|
11
|
+
it "should parse and generate MySQL type identifier quotes" do
|
12
|
+
SQLTree.identifier_quote_char = "`"
|
13
|
+
SQLTree['SELECT `field` FROM `table`'].to_sql.should ==
|
14
|
+
'SELECT `field` FROM `table`'
|
15
|
+
end
|
8
16
|
|
9
17
|
it "should parse and generate SQL fo a simple list query" do
|
10
18
|
SQLTree["SELECT * FROM table"].to_sql.should == 'SELECT * FROM "table"'
|
data/spec/lib/matchers.rb
CHANGED
@@ -14,7 +14,8 @@ class TokenizeTo
|
|
14
14
|
|
15
15
|
def matches?(found_tokens)
|
16
16
|
@found_tokens = found_tokens
|
17
|
-
return @found_tokens == @expected_tokens
|
17
|
+
return @found_tokens.length == @expected_tokens.length &&
|
18
|
+
@found_tokens.zip(@expected_tokens).all? { |(f, e)| e === f }
|
18
19
|
end
|
19
20
|
|
20
21
|
def description
|
@@ -36,7 +37,7 @@ def tokenize_to(*expected_tokens)
|
|
36
37
|
end
|
37
38
|
|
38
39
|
def sql_var(name)
|
39
|
-
SQLTree::Token::
|
40
|
+
SQLTree::Token::Identifier.new(name.to_s)
|
40
41
|
end
|
41
42
|
|
42
43
|
def dot
|
@@ -4,13 +4,13 @@ describe SQLTree::Node::DeleteQuery do
|
|
4
4
|
|
5
5
|
it "should parse a delete query without WHERE clause correctly" do
|
6
6
|
delete = SQLTree::Node::DeleteQuery["DELETE FROM table"]
|
7
|
-
delete.table.should ==
|
7
|
+
delete.table.should == SQLTree::Node::TableReference.new("table")
|
8
8
|
delete.where.should be_nil
|
9
9
|
end
|
10
10
|
|
11
11
|
it "should parse a delete query without WHERE clause correctly" do
|
12
12
|
delete = SQLTree::Node::DeleteQuery["DELETE FROM table WHERE 1 = 1"]
|
13
|
-
delete.table.should ==
|
13
|
+
delete.table.should == SQLTree::Node::TableReference.new("table")
|
14
14
|
delete.where.should be_kind_of(SQLTree::Node::Expression)
|
15
15
|
end
|
16
16
|
end
|
@@ -4,7 +4,7 @@ describe SQLTree::Node::Expression do
|
|
4
4
|
|
5
5
|
describe '.parse' do
|
6
6
|
it "shoud parse a value correctly" do
|
7
|
-
SQLTree::Node::Expression['123'].should == SQLTree::Node::Value.new(123)
|
7
|
+
SQLTree::Node::Expression['123'].should == SQLTree::Node::Expression::Value.new(123)
|
8
8
|
end
|
9
9
|
|
10
10
|
it "shoud parse a function call without arguments correctly" do
|
@@ -16,19 +16,21 @@ describe SQLTree::Node::Expression do
|
|
16
16
|
it "shoud parse a function call with arguments correctly" do
|
17
17
|
function = SQLTree::Node::Expression["MD5('string')"]
|
18
18
|
function.function.should == 'MD5'
|
19
|
-
function.arguments.should == [SQLTree::Node::Value.new('string')]
|
19
|
+
function.arguments.should == [SQLTree::Node::Expression::Value.new('string')]
|
20
20
|
end
|
21
21
|
|
22
22
|
it "should parse a logical OR expression correctly" do
|
23
23
|
logical = SQLTree::Node::Expression["'this' OR 'that"]
|
24
|
-
logical.operator.should
|
25
|
-
logical.
|
24
|
+
logical.operator.should == 'OR'
|
25
|
+
logical.lhs.should == SQLTree::Node::Expression::Value.new('this')
|
26
|
+
logical.rhs.should == SQLTree::Node::Expression::Value.new('that')
|
26
27
|
end
|
27
28
|
|
28
29
|
it "should parse a logical AND expression correctly" do
|
29
30
|
logical = SQLTree::Node::Expression['1 AND 2']
|
30
|
-
logical.operator.should ==
|
31
|
-
logical.
|
31
|
+
logical.operator.should == 'AND'
|
32
|
+
logical.lhs.should == SQLTree::Node::Expression::Value.new(1)
|
33
|
+
logical.rhs.should == SQLTree::Node::Expression::Value.new(2)
|
32
34
|
end
|
33
35
|
|
34
36
|
it "should nest a logical AND expression correctly" do
|
@@ -42,61 +44,60 @@ describe SQLTree::Node::Expression do
|
|
42
44
|
end
|
43
45
|
|
44
46
|
it "should parse a NOT expression without parenteheses correctly" do
|
45
|
-
SQLTree::Node::Expression['NOT 1'].should == SQLTree::Node::
|
47
|
+
SQLTree::Node::Expression['NOT 1'].should == SQLTree::Node::Expression::PrefixOperator.new(:operator => 'NOT', :rhs => SQLTree::Node::Expression::Value.new(1))
|
46
48
|
end
|
47
49
|
|
48
50
|
it "should parse a NOT expression without parenteheses correctly" do
|
49
|
-
SQLTree::Node::Expression['NOT(1)'].should == SQLTree::Node::
|
51
|
+
SQLTree::Node::Expression['NOT(1)'].should == SQLTree::Node::Expression::PrefixOperator.new(:operator => 'NOT', :rhs => SQLTree::Node::Expression::Value.new(1))
|
50
52
|
end
|
51
53
|
|
52
54
|
it "should parse a comparison expression correctly" do
|
53
55
|
comparison = SQLTree::Node::Expression['1 < 2']
|
54
56
|
comparison.operator.should == '<'
|
55
|
-
comparison.lhs.should == SQLTree::Node::Value.new(1)
|
56
|
-
comparison.rhs.should == SQLTree::Node::Value.new(2)
|
57
|
+
comparison.lhs.should == SQLTree::Node::Expression::Value.new(1)
|
58
|
+
comparison.rhs.should == SQLTree::Node::Expression::Value.new(2)
|
57
59
|
end
|
58
60
|
|
59
61
|
it "should parse an IS NULL expression corectly" do
|
60
62
|
comparison = SQLTree::Node::Expression['field IS NULL']
|
61
63
|
comparison.operator.should == 'IS'
|
62
|
-
comparison.lhs.should == SQLTree::Node::Variable.new('field')
|
63
|
-
comparison.rhs.should == SQLTree::Node::Value.new(nil)
|
64
|
+
comparison.lhs.should == SQLTree::Node::Expression::Variable.new('field')
|
65
|
+
comparison.rhs.should == SQLTree::Node::Expression::Value.new(nil)
|
64
66
|
end
|
65
67
|
|
66
68
|
it "should parse an IS NOT NULL expression corectly" do
|
67
69
|
comparison = SQLTree::Node::Expression['field IS NOT NULL']
|
68
70
|
comparison.operator.should == 'IS NOT'
|
69
|
-
comparison.lhs.should == SQLTree::Node::Variable.new('field')
|
70
|
-
comparison.rhs.should == SQLTree::Node::Value.new(nil)
|
71
|
+
comparison.lhs.should == SQLTree::Node::Expression::Variable.new('field')
|
72
|
+
comparison.rhs.should == SQLTree::Node::Expression::Value.new(nil)
|
71
73
|
end
|
72
74
|
|
73
75
|
it "should parse a LIKE expression corectly" do
|
74
76
|
comparison = SQLTree::Node::Expression["field LIKE '%search%"]
|
75
77
|
comparison.operator.should == 'LIKE'
|
76
|
-
comparison.lhs.should == SQLTree::Node::Variable.new('field')
|
77
|
-
comparison.rhs.should == SQLTree::Node::Value.new('%search%')
|
78
|
+
comparison.lhs.should == SQLTree::Node::Expression::Variable.new('field')
|
79
|
+
comparison.rhs.should == SQLTree::Node::Expression::Value.new('%search%')
|
78
80
|
end
|
79
81
|
|
80
82
|
it "should parse a NOT ILIKE expression corectly" do
|
81
83
|
comparison = SQLTree::Node::Expression["field NOT ILIKE '%search%"]
|
82
84
|
comparison.operator.should == 'NOT ILIKE'
|
83
|
-
comparison.lhs.should == SQLTree::Node::Variable.new('field')
|
84
|
-
comparison.rhs.should == SQLTree::Node::Value.new('%search%')
|
85
|
+
comparison.lhs.should == SQLTree::Node::Expression::Variable.new('field')
|
86
|
+
comparison.rhs.should == SQLTree::Node::Expression::Value.new('%search%')
|
85
87
|
end
|
86
88
|
|
87
89
|
it "should parse an IN expression correctly" do
|
88
90
|
comparison = SQLTree::Node::Expression["field IN (1,2,3,4)"]
|
89
91
|
comparison.operator.should == 'IN'
|
90
|
-
comparison.lhs.should == SQLTree::Node::Variable.new('field')
|
91
|
-
comparison.rhs.should be_kind_of(SQLTree::Node::
|
92
|
+
comparison.lhs.should == SQLTree::Node::Expression::Variable.new('field')
|
93
|
+
comparison.rhs.should be_kind_of(SQLTree::Node::Expression::List)
|
92
94
|
end
|
93
95
|
|
94
96
|
it "should parse a NOT IN expression correctly" do
|
95
97
|
comparison = SQLTree::Node::Expression["field NOT IN (1>2, 3+6, 99)"]
|
96
98
|
comparison.operator.should == 'NOT IN'
|
97
|
-
comparison.lhs.should == SQLTree::Node::Variable.new('field')
|
98
|
-
comparison.rhs.should be_kind_of(SQLTree::Node::
|
99
|
+
comparison.lhs.should == SQLTree::Node::Expression::Variable.new('field')
|
100
|
+
comparison.rhs.should be_kind_of(SQLTree::Node::Expression::List)
|
99
101
|
end
|
100
|
-
|
101
102
|
end
|
102
103
|
end
|