sql_tree 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|