sql_tree 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +12 -1
- data/Rakefile +1 -1
- data/lib/sql_tree/node/expression.rb +31 -31
- data/lib/sql_tree/node/field.rb +3 -3
- data/lib/sql_tree/node/join.rb +2 -2
- data/lib/sql_tree/node/ordering.rb +5 -5
- data/lib/sql_tree/node/select_expression.rb +9 -9
- data/lib/sql_tree/node/select_query.rb +27 -13
- data/lib/sql_tree/node/source.rb +6 -6
- data/lib/sql_tree/node/table_reference.rb +2 -2
- data/lib/sql_tree/node/value.rb +5 -5
- data/lib/sql_tree/node/variable.rb +3 -3
- data/lib/sql_tree/node.rb +5 -5
- data/lib/sql_tree/parser.rb +9 -9
- data/lib/sql_tree/token.rb +18 -18
- data/lib/sql_tree/tokenizer.rb +15 -15
- data/lib/sql_tree.rb +6 -6
- data/spec/integration/api_spec.rb +1 -1
- data/spec/integration/full_queries_spec.rb +12 -7
- data/spec/lib/matchers.rb +7 -7
- data/spec/spec_helper.rb +3 -3
- data/spec/unit/expression_node_spec.rb +8 -8
- data/spec/unit/leaf_node_spec.rb +3 -3
- data/spec/unit/select_query_spec.rb +9 -9
- data/spec/unit/tokenizer_spec.rb +17 -17
- data/sql_tree.gemspec +1 -1
- metadata +1 -1
data/README.rdoc
CHANGED
@@ -16,10 +16,21 @@ are supported.
|
|
16
16
|
The SQLTree library is distributed as a gem on Gemcutter.org. To install:
|
17
17
|
|
18
18
|
gem install sql_tree --source http://gemcutter.org
|
19
|
+
|
20
|
+
== Usage
|
21
|
+
|
22
|
+
Consider the following example:
|
23
|
+
|
24
|
+
require 'sql_tree'
|
25
|
+
tree = SQLTree["SELECT * FROM table WHERE field = 'value'"]
|
26
|
+
where = SQLTree::Node::Expression["username = 'user' AND password = MD5('$secret')"]
|
27
|
+
tree.where = where # replace WHERE expression
|
28
|
+
puts tree.to_sql
|
29
|
+
# "SELECT * FROM "table" WHERE (("username" = 'user') AND ("password" = MD5('$secret')))"
|
19
30
|
|
20
31
|
== Additional information
|
21
32
|
|
22
33
|
* See the project wiki at http://wiki.github.com/wvanbergen/sql_tree for more
|
23
34
|
information about using this library.
|
24
|
-
* This plugin is written by Willem van Bergen and is MIT licensed (see the
|
35
|
+
* This plugin is written by Willem van Bergen and is MIT licensed (see the
|
25
36
|
LICENSE file).
|
data/Rakefile
CHANGED
@@ -5,11 +5,11 @@ module SQLTree::Node
|
|
5
5
|
# This is an asbtract class and should not be used directly. Use
|
6
6
|
# one of the subclasses instead.
|
7
7
|
class Expression < Base
|
8
|
-
|
8
|
+
|
9
9
|
def self.parse(tokens)
|
10
10
|
SQLTree::Node::LogicalExpression.parse(tokens)
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
# Parses a single, atomic SQL expression. This can be either:
|
14
14
|
# * a full expression (or set of expressions) within parentheses.
|
15
15
|
# * a logical NOT expression
|
@@ -21,7 +21,7 @@ module SQLTree::Node
|
|
21
21
|
when SQLTree::Token::LPAREN
|
22
22
|
tokens.consume(SQLTree::Token::LPAREN)
|
23
23
|
expr = SQLTree::Node::Expression.parse(tokens)
|
24
|
-
tokens.consume(SQLTree::Token::RPAREN)
|
24
|
+
tokens.consume(SQLTree::Token::RPAREN)
|
25
25
|
expr
|
26
26
|
when SQLTree::Token::NOT
|
27
27
|
SQLTree::Node::LogicalNotExpression.parse(tokens)
|
@@ -36,23 +36,23 @@ module SQLTree::Node
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
40
|
class LogicalNotExpression < Expression
|
41
|
-
|
41
|
+
|
42
42
|
attr_accessor :expression
|
43
|
-
|
43
|
+
|
44
44
|
def initialize(expression)
|
45
45
|
@expression = expression
|
46
46
|
end
|
47
|
-
|
47
|
+
|
48
48
|
def to_sql
|
49
49
|
"NOT(#{@expression.to_sql})"
|
50
50
|
end
|
51
|
-
|
51
|
+
|
52
52
|
def ==(other)
|
53
53
|
other.kind_of?(self.class) && other.expression == self.expression
|
54
54
|
end
|
55
|
-
|
55
|
+
|
56
56
|
def self.parse(tokens)
|
57
57
|
tokens.consume(SQLTree::Token::NOT)
|
58
58
|
self.new(SQLTree::Node::Expression.parse(tokens))
|
@@ -74,29 +74,29 @@ module SQLTree::Node
|
|
74
74
|
def ==(other)
|
75
75
|
self.operator == other.operator && self.expressions == other.expressions
|
76
76
|
end
|
77
|
-
|
77
|
+
|
78
78
|
def self.parse(tokens)
|
79
79
|
expr = ComparisonExpression.parse(tokens)
|
80
80
|
while [SQLTree::Token::AND, SQLTree::Token::OR].include?(tokens.peek)
|
81
81
|
expr = SQLTree::Node::LogicalExpression.new(tokens.next.literal, [expr, ComparisonExpression.parse(tokens)])
|
82
|
-
end
|
82
|
+
end
|
83
83
|
return expr
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
87
87
|
class ComparisonExpression < Expression
|
88
88
|
attr_accessor :lhs, :rhs, :operator
|
89
|
-
|
89
|
+
|
90
90
|
def initialize(operator, lhs, rhs)
|
91
91
|
@lhs = lhs
|
92
92
|
@rhs = rhs
|
93
93
|
@operator = operator
|
94
94
|
end
|
95
|
-
|
95
|
+
|
96
96
|
def to_sql
|
97
97
|
"(#{@lhs.to_sql} #{@operator} #{@rhs.to_sql})"
|
98
98
|
end
|
99
|
-
|
99
|
+
|
100
100
|
def self.parse_comparison_operator(tokens)
|
101
101
|
operator_token = tokens.next
|
102
102
|
if SQLTree::Token::IS === operator_token
|
@@ -117,7 +117,7 @@ module SQLTree::Node
|
|
117
117
|
operator_token.literal
|
118
118
|
end
|
119
119
|
end
|
120
|
-
|
120
|
+
|
121
121
|
def self.parse(tokens)
|
122
122
|
lhs = SQLTree::Node::ArithmeticExpression.parse(tokens)
|
123
123
|
while SQLTree::Token::COMPARISON_OPERATORS.include?(tokens.peek)
|
@@ -125,16 +125,16 @@ module SQLTree::Node
|
|
125
125
|
rhs = ['IN', 'NOT IN'].include?(comparison_operator) ?
|
126
126
|
SQLTree::Node::SetExpression.parse(tokens) :
|
127
127
|
SQLTree::Node::ArithmeticExpression.parse(tokens)
|
128
|
-
|
128
|
+
|
129
129
|
lhs = self.new(comparison_operator, lhs, rhs)
|
130
130
|
end
|
131
131
|
return lhs
|
132
132
|
end
|
133
133
|
end
|
134
|
-
|
134
|
+
|
135
135
|
class SetExpression < Expression
|
136
136
|
attr_accessor :items
|
137
|
-
|
137
|
+
|
138
138
|
def initialize(items = [])
|
139
139
|
@items = items
|
140
140
|
end
|
@@ -142,7 +142,7 @@ module SQLTree::Node
|
|
142
142
|
def to_sql
|
143
143
|
"(#{items.map {|i| i.to_sql}.join(', ')})"
|
144
144
|
end
|
145
|
-
|
145
|
+
|
146
146
|
def self.parse(tokens)
|
147
147
|
tokens.consume(SQLTree::Token::LPAREN)
|
148
148
|
items = [SQLTree::Node::Expression.parse(tokens)]
|
@@ -151,23 +151,23 @@ module SQLTree::Node
|
|
151
151
|
items << SQLTree::Node::Expression.parse(tokens)
|
152
152
|
end
|
153
153
|
tokens.consume(SQLTree::Token::RPAREN)
|
154
|
-
|
154
|
+
|
155
155
|
self.new(items)
|
156
156
|
end
|
157
157
|
end
|
158
|
-
|
158
|
+
|
159
159
|
class FunctionExpression < Expression
|
160
160
|
attr_accessor :function, :arguments
|
161
|
-
|
161
|
+
|
162
162
|
def initialize(function, arguments = [])
|
163
163
|
@function = function
|
164
164
|
@arguments = arguments
|
165
165
|
end
|
166
|
-
|
166
|
+
|
167
167
|
def to_sql
|
168
168
|
"#{@function}(" + @arguments.map { |e| e.to_sql }.join(', ') + ")"
|
169
169
|
end
|
170
|
-
|
170
|
+
|
171
171
|
def self.parse(tokens)
|
172
172
|
expr = self.new(tokens.next.literal)
|
173
173
|
tokens.consume(SQLTree::Token::LPAREN)
|
@@ -176,27 +176,27 @@ module SQLTree::Node
|
|
176
176
|
tokens.consume(SQLTree::Token::COMMA) if tokens.peek == SQLTree::Token::COMMA
|
177
177
|
end
|
178
178
|
tokens.consume(SQLTree::Token::RPAREN)
|
179
|
-
return expr
|
179
|
+
return expr
|
180
180
|
end
|
181
181
|
end
|
182
|
-
|
182
|
+
|
183
183
|
class ArithmeticExpression < Expression
|
184
184
|
attr_accessor :lhs, :rhs, :operator
|
185
|
-
|
185
|
+
|
186
186
|
def initialize(operator, lhs, rhs)
|
187
187
|
@lhs = lhs
|
188
188
|
@rhs = rhs
|
189
189
|
@operator = operator
|
190
190
|
end
|
191
|
-
|
191
|
+
|
192
192
|
def to_sql
|
193
193
|
"(#{@lhs.to_sql} #{@operator} #{@rhs.to_sql})"
|
194
194
|
end
|
195
|
-
|
195
|
+
|
196
196
|
def self.parse(tokens)
|
197
197
|
self.parse_primary(tokens)
|
198
198
|
end
|
199
|
-
|
199
|
+
|
200
200
|
def self.parse_primary(tokens)
|
201
201
|
expr = self.parse_secondary(tokens)
|
202
202
|
while [SQLTree::Token::PLUS, SQLTree::Token::MINUS].include?(tokens.peek)
|
@@ -204,7 +204,7 @@ module SQLTree::Node
|
|
204
204
|
end
|
205
205
|
return expr
|
206
206
|
end
|
207
|
-
|
207
|
+
|
208
208
|
def self.parse_secondary(tokens)
|
209
209
|
expr = Expression.parse_atomic(tokens)
|
210
210
|
while [SQLTree::Token::PLUS, SQLTree::Token::MINUS].include?(tokens.peek)
|
data/lib/sql_tree/node/field.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module SQLTree::Node
|
2
|
-
|
2
|
+
|
3
3
|
class Field < Base
|
4
4
|
|
5
5
|
attr_accessor :name, :table
|
@@ -20,11 +20,11 @@ module SQLTree::Node
|
|
20
20
|
def to_sql
|
21
21
|
@table.nil? ? quote_var(@name) : quote_var(@table) + '.' + quote_var(@name)
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
def ==(other)
|
25
25
|
other.name == self.name && other.table == self.table
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
def self.parse(tokens)
|
29
29
|
field_or_table = case tokens.next
|
30
30
|
when SQLTree::Token::MULTIPLY then :all
|
data/lib/sql_tree/node/join.rb
CHANGED
@@ -15,7 +15,7 @@ module SQLTree::Node
|
|
15
15
|
join_sql
|
16
16
|
end
|
17
17
|
|
18
|
-
def table
|
18
|
+
def table
|
19
19
|
table_reference.table
|
20
20
|
end
|
21
21
|
|
@@ -43,7 +43,7 @@ module SQLTree::Node
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def ==(other)
|
46
|
-
other.table = self.table && other.table_alias == self.table_alias &&
|
46
|
+
other.table = self.table && other.table_alias == self.table_alias &&
|
47
47
|
other.join_type == self.join_type && other.join_expression == self.join_expression
|
48
48
|
end
|
49
49
|
end
|
@@ -1,19 +1,19 @@
|
|
1
1
|
module SQLTree::Node
|
2
|
-
|
2
|
+
|
3
3
|
class Ordering < Base
|
4
|
-
|
4
|
+
|
5
5
|
attr_accessor :expression, :direction
|
6
|
-
|
6
|
+
|
7
7
|
def initialize(expression, direction = nil)
|
8
8
|
@expression, @direction = expression, direction
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
def to_sql
|
12
12
|
sql = expression.to_sql
|
13
13
|
sql << " #{direction.to_s.upcase}" if direction
|
14
14
|
sql
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def self.parse(tokens)
|
18
18
|
ordering = self.new(SQLTree::Node::Expression.parse(tokens))
|
19
19
|
if tokens.peek && tokens.peek.direction?
|
@@ -1,20 +1,20 @@
|
|
1
1
|
module SQLTree::Node
|
2
|
-
|
2
|
+
|
3
3
|
class SelectExpression < Base
|
4
|
-
|
4
|
+
|
5
5
|
attr_accessor :expression, :variable
|
6
|
-
|
6
|
+
|
7
7
|
def initialize(expression, variable = nil)
|
8
8
|
@expression = expression
|
9
9
|
@variable = variable
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
def to_sql
|
13
13
|
sql = @expression.to_sql
|
14
14
|
sql << " AS " << quote_var(@variable) if @variable
|
15
15
|
return sql
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
def self.parse(tokens)
|
19
19
|
if tokens.peek == SQLTree::Token::MULTIPLY
|
20
20
|
tokens.consume(SQLTree::Token::MULTIPLY)
|
@@ -29,17 +29,17 @@ module SQLTree::Node
|
|
29
29
|
return expr
|
30
30
|
end
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
def ==(other)
|
34
34
|
other.expression == self.expression && other.variable == self.variable
|
35
|
-
end
|
35
|
+
end
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
class AllFieldsExpression < Expression
|
39
39
|
def to_sql
|
40
40
|
'*'
|
41
41
|
end
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
ALL_FIELDS = AllFieldsExpression.new
|
45
45
|
end
|
@@ -1,24 +1,26 @@
|
|
1
1
|
module SQLTree::Node
|
2
|
-
|
2
|
+
|
3
3
|
class SelectQuery < Base
|
4
|
-
|
4
|
+
|
5
5
|
attr_accessor :distinct, :select, :from, :where, :group_by, :having, :order_by, :limit
|
6
|
-
|
6
|
+
|
7
7
|
def initialize
|
8
8
|
@distinct = false
|
9
9
|
@select = []
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
def to_sql
|
13
13
|
raise "At least one SELECT expression is required" if self.select.empty?
|
14
14
|
sql = (self.distinct) ? "SELECT DISTINCT " : "SELECT "
|
15
15
|
sql << select.map { |s| s.to_sql }.join(', ')
|
16
|
-
sql << " FROM "
|
17
|
-
sql << " WHERE "
|
16
|
+
sql << " FROM " << from.map { |f| f.to_sql }.join(', ')
|
17
|
+
sql << " WHERE " << where.to_sql if where
|
18
|
+
sql << " GROUP BY " << group_by.map { |g| g.to_sql }.join(', ') if group_by
|
18
19
|
sql << " ORDER BY " << order_by.map { |o| o.to_sql }.join(', ') if order_by
|
20
|
+
sql << " HAVING " << having.to_sql if having
|
19
21
|
return sql
|
20
22
|
end
|
21
|
-
|
23
|
+
|
22
24
|
# Uses the provided initialized parser to parse a SELECT query.
|
23
25
|
def self.parse(tokens)
|
24
26
|
select_node = self.new
|
@@ -32,10 +34,14 @@ module SQLTree::Node
|
|
32
34
|
select_node.select = self.parse_select_clause(tokens)
|
33
35
|
select_node.from = self.parse_from_clause(tokens) if tokens.peek == SQLTree::Token::FROM
|
34
36
|
select_node.where = self.parse_where_clause(tokens) if tokens.peek == SQLTree::Token::WHERE
|
37
|
+
if tokens.peek == SQLTree::Token::GROUP
|
38
|
+
select_node.group_by = self.parse_group_clause(tokens)
|
39
|
+
select_node.having = self.parse_having_clause(tokens) if tokens.peek == SQLTree::Token::HAVING
|
40
|
+
end
|
35
41
|
select_node.order_by = self.parse_order_clause(tokens) if tokens.peek == SQLTree::Token::ORDER
|
36
42
|
return select_node
|
37
43
|
end
|
38
|
-
|
44
|
+
|
39
45
|
def self.parse_select_clause(tokens)
|
40
46
|
expressions = [SQLTree::Node::SelectExpression.parse(tokens)]
|
41
47
|
while tokens.peek == SQLTree::Token::COMMA
|
@@ -44,7 +50,7 @@ module SQLTree::Node
|
|
44
50
|
end
|
45
51
|
return expressions
|
46
52
|
end
|
47
|
-
|
53
|
+
|
48
54
|
def self.parse_from_clause(tokens)
|
49
55
|
tokens.consume(SQLTree::Token::FROM)
|
50
56
|
sources = [SQLTree::Node::Source.parse(tokens)]
|
@@ -54,20 +60,28 @@ module SQLTree::Node
|
|
54
60
|
end
|
55
61
|
return sources
|
56
62
|
end
|
57
|
-
|
63
|
+
|
58
64
|
def self.parse_where_clause(tokens)
|
59
65
|
tokens.consume(SQLTree::Token::WHERE)
|
60
66
|
Expression.parse(tokens)
|
61
67
|
end
|
62
68
|
|
63
69
|
def self.parse_group_clause(tokens)
|
64
|
-
|
70
|
+
tokens.consume(SQLTree::Token::GROUP)
|
71
|
+
tokens.consume(SQLTree::Token::BY)
|
72
|
+
exprs = [SQLTree::Node::Expression.parse(tokens)]
|
73
|
+
while tokens.peek == SQLTree::Token::COMMA
|
74
|
+
tokens.consume(SQLTree::Token::COMMA)
|
75
|
+
exprs << SQLTree::Node::Expression.parse(tokens)
|
76
|
+
end
|
77
|
+
return exprs
|
65
78
|
end
|
66
79
|
|
67
80
|
def self.parse_having_clause(tokens)
|
68
|
-
|
81
|
+
tokens.consume(SQLTree::Token::HAVING)
|
82
|
+
SQLTree::Node::Expression.parse(tokens)
|
69
83
|
end
|
70
|
-
|
84
|
+
|
71
85
|
def self.parse_order_clause(tokens)
|
72
86
|
tokens.consume(SQLTree::Token::ORDER)
|
73
87
|
tokens.consume(SQLTree::Token::BY)
|
data/lib/sql_tree/node/source.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
module SQLTree::Node
|
2
|
-
|
2
|
+
|
3
3
|
class Source < Base
|
4
|
-
|
4
|
+
|
5
5
|
attr_accessor :table_reference, :joins
|
6
|
-
|
6
|
+
|
7
7
|
def initialize(table_reference, joins = [])
|
8
8
|
@table_reference, @joins = table_reference, joins
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
def table
|
12
12
|
table_reference.table
|
13
13
|
end
|
@@ -15,7 +15,7 @@ module SQLTree::Node
|
|
15
15
|
def table_alias
|
16
16
|
table_reference.table_alias
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
def to_sql
|
20
20
|
sql = table_reference.to_sql
|
21
21
|
sql << ' ' << joins.map { |j| j.to_sql }.join(' ') if joins.any?
|
@@ -33,5 +33,5 @@ module SQLTree::Node
|
|
33
33
|
end
|
34
34
|
return source
|
35
35
|
end
|
36
|
-
end
|
36
|
+
end
|
37
37
|
end
|
@@ -17,7 +17,7 @@ module SQLTree::Node
|
|
17
17
|
def ==(other)
|
18
18
|
other.table = self.table && other.table_alias == self.table_alias
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
21
|
def self.parse(tokens)
|
22
22
|
if SQLTree::Token::Variable === tokens.next
|
23
23
|
table_reference = self.new(tokens.current.literal)
|
@@ -26,7 +26,7 @@ module SQLTree::Node
|
|
26
26
|
table_reference.table_alias = SQLTree::Node::Variable.parse(tokens).name
|
27
27
|
end
|
28
28
|
return table_reference
|
29
|
-
else
|
29
|
+
else
|
30
30
|
raise SQLTree::Parser::UnexpectedToken.new(tokens.current)
|
31
31
|
end
|
32
32
|
end
|
data/lib/sql_tree/node/value.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
module SQLTree::Node
|
2
|
-
|
2
|
+
|
3
3
|
class Value < Base
|
4
4
|
attr_accessor :value
|
5
5
|
|
6
6
|
def initialize(value)
|
7
7
|
@value = value
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
def to_sql
|
11
11
|
case value
|
12
12
|
when nil then 'NULL'
|
@@ -14,11 +14,11 @@ module SQLTree::Node
|
|
14
14
|
else @value.to_s
|
15
15
|
end
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
def ==(other)
|
19
19
|
other.kind_of?(self.class) && other.value == self.value
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def self.parse(tokens)
|
23
23
|
case tokens.next
|
24
24
|
when SQLTree::Token::String, SQLTree::Token::Number
|
@@ -27,7 +27,7 @@ module SQLTree::Node
|
|
27
27
|
SQLTree::Node::Value.new(nil)
|
28
28
|
else
|
29
29
|
raise SQLTree::Parser::UnexpectedToken.new(tokens.current, :literal)
|
30
|
-
end
|
30
|
+
end
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module SQLTree::Node
|
2
|
-
|
2
|
+
|
3
3
|
class Variable < Base
|
4
4
|
|
5
5
|
attr_accessor :name
|
@@ -7,11 +7,11 @@ module SQLTree::Node
|
|
7
7
|
def initialize(name)
|
8
8
|
@name = name
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
def to_sql
|
12
12
|
quote_var(@name)
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def ==(other)
|
16
16
|
other.name == self.name
|
17
17
|
end
|
data/lib/sql_tree/node.rb
CHANGED
@@ -10,23 +10,23 @@ module SQLTree::Node
|
|
10
10
|
def inspect
|
11
11
|
"#{self.class.name}[#{self.to_sql}]"
|
12
12
|
end
|
13
|
-
|
14
|
-
# Quotes a variable name so that it can be safely used within
|
13
|
+
|
14
|
+
# Quotes a variable name so that it can be safely used within
|
15
15
|
# SQL queries.
|
16
16
|
def quote_var(name)
|
17
17
|
"\"#{name}\""
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
# Quotes a string so that it can be used within an SQL query.
|
21
21
|
def quote_str(str)
|
22
22
|
"'#{str.gsub(/\'/, "''")}'"
|
23
23
|
end
|
24
|
-
|
24
|
+
|
25
25
|
# This method should be implemented by a subclass.
|
26
26
|
def self.parse(tokens)
|
27
27
|
raise 'Only implemented in subclasses!'
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
# Parses a string, expecting it to be parsable to an instance of
|
31
31
|
# the current class.
|
32
32
|
def self.[](sql, options = {})
|
data/lib/sql_tree/parser.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
class SQLTree::Parser
|
2
|
-
|
2
|
+
|
3
3
|
class UnexpectedToken < StandardError
|
4
|
-
|
4
|
+
|
5
5
|
attr_reader :expected_token, :actual_token
|
6
|
-
|
6
|
+
|
7
7
|
def initialize(actual_token, expected_token = nil)
|
8
8
|
@expected_token, @actual_token = expected_token, actual_token
|
9
9
|
message = "Unexpected token: found #{actual_token.inspect}"
|
@@ -11,7 +11,7 @@ class SQLTree::Parser
|
|
11
11
|
message << '!'
|
12
12
|
super(message)
|
13
13
|
end
|
14
|
-
end
|
14
|
+
end
|
15
15
|
|
16
16
|
def self.parse(sql_string, options = {})
|
17
17
|
self.new(sql_string, options).parse!
|
@@ -31,21 +31,21 @@ class SQLTree::Parser
|
|
31
31
|
def current
|
32
32
|
@current_token
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
def next
|
36
36
|
@current_token = @tokens.shift
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
def consume(*checks)
|
40
40
|
checks.each do |check|
|
41
41
|
raise UnexpectedToken.new(self.current, check) unless check == self.next
|
42
42
|
end
|
43
43
|
end
|
44
|
-
|
44
|
+
|
45
45
|
def peek(distance = 1)
|
46
46
|
@tokens[distance - 1]
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
def peek_tokens(amount)
|
50
50
|
@tokens[0, amount]
|
51
51
|
end
|
@@ -53,7 +53,7 @@ class SQLTree::Parser
|
|
53
53
|
def debug
|
54
54
|
puts @tokens.inspect
|
55
55
|
end
|
56
|
-
|
56
|
+
|
57
57
|
def parse!
|
58
58
|
case self.peek
|
59
59
|
when SQLTree::Token::SELECT then SQLTree::Node::SelectQuery.parse(self)
|
data/lib/sql_tree/token.rb
CHANGED
@@ -8,60 +8,60 @@ class SQLTree::Token
|
|
8
8
|
|
9
9
|
# For some tokens, the encountered literal value is important
|
10
10
|
# during the parsing phase (e.g. strings and variable names).
|
11
|
-
# Therefore, the literal value encountered that represented the
|
11
|
+
# Therefore, the literal value encountered that represented the
|
12
12
|
# token in the original SQL query string is stored.
|
13
13
|
attr_accessor :literal
|
14
14
|
|
15
15
|
# Creates a token instance with a given literal representation.
|
16
16
|
#
|
17
|
-
# <tt>literal<tt>:: The literal string value that was encountered
|
17
|
+
# <tt>literal<tt>:: The literal string value that was encountered
|
18
18
|
# while tokenizing.
|
19
19
|
def initialize(literal)
|
20
20
|
@literal = literal
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
# Compares two tokens. Tokens are considered equal when they are
|
24
24
|
# instances of the same class, i.e. do literal is not used.
|
25
25
|
def ==(other)
|
26
26
|
other.class == self.class
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
def inspect # :nodoc:
|
30
30
|
literal
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
def join?
|
34
|
-
[SQLTree::Token::JOIN, SQLTree::Token::LEFT, SQLTree::Token::RIGHT,
|
35
|
-
SQLTree::Token::INNER, SQLTree::Token::OUTER, SQLTree::Token::NATURAL,
|
34
|
+
[SQLTree::Token::JOIN, SQLTree::Token::LEFT, SQLTree::Token::RIGHT,
|
35
|
+
SQLTree::Token::INNER, SQLTree::Token::OUTER, SQLTree::Token::NATURAL,
|
36
36
|
SQLTree::Token::FULL].include?(self)
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
def direction?
|
40
40
|
[SQLTree::Token::ASC, SQLTree::Token::DESC].include?(self)
|
41
41
|
end
|
42
|
-
|
42
|
+
|
43
43
|
###################################################################
|
44
44
|
# DYNAMIC TOKEN TYPES
|
45
45
|
###################################################################
|
46
46
|
|
47
47
|
# The <tt>SQLTree::Token::Value</tt> class is the base class for
|
48
|
-
# every dynamic token. A dynamic token is a token for which the
|
48
|
+
# every dynamic token. A dynamic token is a token for which the
|
49
49
|
# literal value used remains impoirtant during parsing.
|
50
50
|
class Value < SQLTree::Token
|
51
|
-
|
51
|
+
|
52
52
|
def inspect # :nodoc:
|
53
53
|
"#<#{self.class.name.split('::').last}:#{literal.inspect}>"
|
54
54
|
end
|
55
|
-
|
56
|
-
# Compares two tokens. For values, the literal encountered value
|
55
|
+
|
56
|
+
# Compares two tokens. For values, the literal encountered value
|
57
57
|
# of the token is also taken into account besides the class.
|
58
58
|
def ==(other)
|
59
59
|
other.class == self.class && @literal == other.literal
|
60
60
|
end
|
61
61
|
end
|
62
|
-
|
62
|
+
|
63
63
|
# The <tt>SQLTree::Token::Variable</tt> class represents SQL
|
64
|
-
# variables. The variable name is stored in the literal as string,
|
64
|
+
# variables. The variable name is stored in the literal as string,
|
65
65
|
# without quotes if they were present.
|
66
66
|
class Variable < SQLTree::Token::Value
|
67
67
|
end
|
@@ -76,7 +76,7 @@ class SQLTree::Token
|
|
76
76
|
# literal.
|
77
77
|
class Number < SQLTree::Token::Value
|
78
78
|
end
|
79
|
-
|
79
|
+
|
80
80
|
###################################################################
|
81
81
|
# STATIC TOKEN TYPES
|
82
82
|
###################################################################
|
@@ -111,7 +111,7 @@ class SQLTree::Token
|
|
111
111
|
COMMA = Class.new(SQLTree::Token).new(',')
|
112
112
|
|
113
113
|
# A list of all the SQL reserverd keywords.
|
114
|
-
KEYWORDS = %w{SELECT FROM WHERE GROUP HAVING ORDER DISTINCT LEFT RIGHT INNER FULL OUTER NATURAL JOIN USING
|
114
|
+
KEYWORDS = %w{SELECT FROM WHERE GROUP HAVING ORDER DISTINCT LEFT RIGHT INNER FULL OUTER NATURAL JOIN USING
|
115
115
|
AND OR NOT AS ON IS NULL BY LIKE ILIKE BETWEEN IN ASC DESC}
|
116
116
|
|
117
117
|
# Create a token for all the reserved keywords in SQL
|
@@ -129,7 +129,7 @@ class SQLTree::Token
|
|
129
129
|
OPERATORS_HASH.each_pair do |literal, symbol|
|
130
130
|
self.const_set(symbol.to_s.upcase, Class.new(SQLTree::Token::Operator).new(literal)) unless self.const_defined?(symbol.to_s.upcase)
|
131
131
|
end
|
132
|
-
|
132
|
+
|
133
133
|
COMPARISON_OPERATORS = COMPARISON_OPERATORS_HASH.map { |(literal, symbol)| const_get(symbol.to_s.upcase) } +
|
134
134
|
[SQLTree::Token::IN, SQLTree::Token::IS, SQLTree::Token::BETWEEN, SQLTree::Token::LIKE, SQLTree::Token::ILIKE, SQLTree::Token::NOT]
|
135
135
|
end
|
data/lib/sql_tree/tokenizer.rb
CHANGED
@@ -11,17 +11,17 @@
|
|
11
11
|
# the <tt>each_token</tt> (aliased to <tt>each</tt>) will yield every
|
12
12
|
# token one by one.
|
13
13
|
class SQLTree::Tokenizer
|
14
|
-
|
14
|
+
|
15
15
|
include Enumerable
|
16
|
-
|
16
|
+
|
17
17
|
# The keyword queue, on which kywords are placed before they are yielded
|
18
18
|
# to the parser, to enable keyword combining (e.g. NOT LIKE)
|
19
19
|
attr_reader :keyword_queue
|
20
|
-
|
20
|
+
|
21
21
|
def initialize # :nodoc:
|
22
22
|
@keyword_queue = []
|
23
23
|
end
|
24
|
-
|
24
|
+
|
25
25
|
# Returns an array of tokens for the given string.
|
26
26
|
# <tt>string</tt>:: the string to tokenize
|
27
27
|
def tokenize(string)
|
@@ -29,7 +29,7 @@ class SQLTree::Tokenizer
|
|
29
29
|
@current_char_pos = -1
|
30
30
|
self.entries
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
# Returns the current character that is being tokenized
|
34
34
|
def current_char
|
35
35
|
@current_char
|
@@ -51,7 +51,7 @@ class SQLTree::Tokenizer
|
|
51
51
|
|
52
52
|
# Combines several tokens to a single token if possible, and
|
53
53
|
# yields teh result, or yields every single token if they cannot
|
54
|
-
# be combined.
|
54
|
+
# be combined.
|
55
55
|
# <tt>token</tt>:: the token to yield or combine
|
56
56
|
# <tt>block</tt>:: the block to yield tokens and combined tokens to.
|
57
57
|
def handle_token(token, &block) # :yields: SQLTree::Token
|
@@ -62,7 +62,7 @@ class SQLTree::Tokenizer
|
|
62
62
|
block.call(token)
|
63
63
|
end
|
64
64
|
end
|
65
|
-
|
65
|
+
|
66
66
|
# This method ensures that every keyword currently in the queue is
|
67
67
|
# yielded. This method get called by <tt>handle_token</tt> when it
|
68
68
|
# knows for sure that the keywords on the queue cannot be combined
|
@@ -73,10 +73,10 @@ class SQLTree::Tokenizer
|
|
73
73
|
end
|
74
74
|
|
75
75
|
# Iterator method that yields each token that is encountered in the
|
76
|
-
# SQL stream. These tokens are passed to the SQL parser to construct
|
76
|
+
# SQL stream. These tokens are passed to the SQL parser to construct
|
77
77
|
# a syntax tree for the SQL query.
|
78
78
|
#
|
79
|
-
# This method is aliased to <tt>:each</tt> to make the Enumerable
|
79
|
+
# This method is aliased to <tt>:each</tt> to make the Enumerable
|
80
80
|
# methods work on this method.
|
81
81
|
def each_token(&block) # :yields: SQLTree::Token
|
82
82
|
while next_char
|
@@ -93,13 +93,13 @@ class SQLTree::Tokenizer
|
|
93
93
|
when '"'; tokenize_quoted_variable(&block) # TODO: allow MySQL quoting mode
|
94
94
|
end
|
95
95
|
end
|
96
|
-
|
96
|
+
|
97
97
|
# Make sure to yield any tokens that are still stashed on the queue.
|
98
98
|
empty_keyword_queue!(&block)
|
99
99
|
end
|
100
|
-
|
100
|
+
|
101
101
|
alias :each :each_token
|
102
|
-
|
102
|
+
|
103
103
|
# Tokenizes a eyword in the code. This can either be a reserved SQL keyword
|
104
104
|
# or a variable. This method will yield variables directly. Keywords will be
|
105
105
|
# yielded with a delay, because they may need to be combined with other
|
@@ -107,7 +107,7 @@ class SQLTree::Tokenizer
|
|
107
107
|
def tokenize_keyword(&block) # :yields: SQLTree::Token
|
108
108
|
literal = current_char
|
109
109
|
literal << next_char while /[\w]/ =~ peek_char
|
110
|
-
|
110
|
+
|
111
111
|
if SQLTree::Token::KEYWORDS.include?(literal.upcase)
|
112
112
|
handle_token(SQLTree::Token.const_get(literal.upcase), &block)
|
113
113
|
else
|
@@ -125,7 +125,7 @@ class SQLTree::Tokenizer
|
|
125
125
|
dot_encountered = true if peek_char == '.'
|
126
126
|
number << next_char
|
127
127
|
end
|
128
|
-
|
128
|
+
|
129
129
|
if dot_encountered
|
130
130
|
handle_token(SQLTree::Token::Number.new(number.to_f), &block)
|
131
131
|
else
|
@@ -160,7 +160,7 @@ class SQLTree::Tokenizer
|
|
160
160
|
# A regular expression that matches all operator characters.
|
161
161
|
OPERATOR_CHARS = /\=|<|>|!|\-|\+|\/|\*|\%/
|
162
162
|
|
163
|
-
# Tokenizes an operator in the SQL stream. This method will yield the
|
163
|
+
# Tokenizes an operator in the SQL stream. This method will yield the
|
164
164
|
# operator token when the last character of the token is encountered.
|
165
165
|
def tokenize_operator(&block) # :yields: SQLTree::Token
|
166
166
|
operator = current_char
|
data/lib/sql_tree.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# The SQLTree module is the basic namespace for the sql_tree gem.
|
2
2
|
#
|
3
3
|
# It contains the shorthand parse method (i.e. <tt>SQLTree[sql_query]</tt>)
|
4
|
-
# and some helper methods that are used by the gem. It also requires the
|
4
|
+
# and some helper methods that are used by the gem. It also requires the
|
5
5
|
# necessary files for the gem to function properly.
|
6
6
|
module SQLTree
|
7
|
-
|
7
|
+
|
8
8
|
# Loads constants in the SQLTree namespace using self.load_default_class_file(base, const)
|
9
9
|
# <tt>const</tt>:: The constant that is not yet loaded in the SQLTree namespace. This should be passed as a string or symbol.
|
10
10
|
def self.const_missing(const)
|
@@ -18,14 +18,14 @@ module SQLTree
|
|
18
18
|
def self.load_default_class_file(base, const)
|
19
19
|
require "#{to_underscore("#{base.name}::#{const}")}"
|
20
20
|
base.const_get(const) if base.const_defined?(const)
|
21
|
-
end
|
22
|
-
|
21
|
+
end
|
22
|
+
|
23
23
|
# The <tt>[]</tt> method is a shorthand for the <tt>SQLTree::Parser.parse</tt>
|
24
24
|
# method to parse an SQL query and return a SQL syntax tree.
|
25
25
|
def self.[](query, options = {})
|
26
26
|
SQLTree::Parser.parse(query)
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
# Convert a string/symbol in camelcase (RequestLogAnalyzer::Controller) to underscores (request_log_analyzer/controller)
|
30
30
|
# This function can be used to load the file (using require) in which the given constant is defined.
|
31
31
|
# <tt>str</tt>:: The string to convert in the following format: <tt>ModuleName::ClassName</tt>
|
@@ -38,5 +38,5 @@ module SQLTree
|
|
38
38
|
# <tt>str</tt>:: The string to convert in the following format: <tt>module_name/class_name</tt>
|
39
39
|
def self.to_camelcase(str)
|
40
40
|
str.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
41
|
-
end
|
41
|
+
end
|
42
42
|
end
|
@@ -5,32 +5,37 @@ describe SQLTree, 'parsing and generating SQL' do
|
|
5
5
|
it "should parse and generate SQL fo a simple list query" do
|
6
6
|
SQLTree["SELECT * FROM table"].to_sql.should == 'SELECT * FROM "table"'
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
it "should parse and generate the DISTINCT keyword" do
|
10
10
|
SQLTree["SELECT DISTINCT * FROM table"].to_sql.should == 'SELECT DISTINCT * FROM "table"'
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
it 'should parse and generate table aliases' do
|
14
14
|
SQLTree["SELECT a.* FROM table AS a"].to_sql.should == 'SELECT "a".* FROM "table" AS "a"'
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
it "should parse and generate an ORDER BY clause" do
|
18
|
-
SQLTree["SELECT * FROM table ORDER BY field1, field2"].to_sql.should ==
|
18
|
+
SQLTree["SELECT * FROM table ORDER BY field1, field2"].to_sql.should ==
|
19
19
|
'SELECT * FROM "table" ORDER BY "field1", "field2"'
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
it "should parse and generate an expression in the SELECT clause" do
|
23
23
|
SQLTree['SELECT MD5( a) AS a, b > 0 AS test FROM table'].to_sql.should ==
|
24
24
|
'SELECT MD5("a") AS "a", ("b" > 0) AS "test" FROM "table"'
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
it "should parse and generate a complex FROM clause" do
|
28
28
|
SQLTree['SELECT * FROM a LEFT JOIN b ON ( a.id = b.a_id), c AS d'].to_sql.should ==
|
29
29
|
'SELECT * FROM "a" LEFT JOIN "b" ON ("a"."id" = "b"."a_id"), "c" AS "d"'
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
it "should parse and generate a WHERE clause" do
|
33
33
|
SQLTree['SELECT * FROM t WHERE ( field > 4 OR NOW() > timestamp) AND other_field IS NOT NULL'].to_sql.should ==
|
34
34
|
'SELECT * FROM "t" WHERE ((("field" > 4) OR (NOW() > "timestamp")) AND ("other_field" IS NOT NULL))'
|
35
35
|
end
|
36
|
+
|
37
|
+
it "should parse and generate a GROUP BY and HAVING clause" do
|
38
|
+
SQLTree['SELECT SUM( field1 ) FROM t GROUP BY field1, MD5( field2 ) HAVING SUM( field1 ) > 10'].to_sql.should ==
|
39
|
+
'SELECT SUM("field1") FROM "t" GROUP BY "field1", MD5("field2") HAVING (SUM("field1") > 10)'
|
40
|
+
end
|
36
41
|
end
|
data/spec/lib/matchers.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
class TokenizeTo
|
2
|
-
|
2
|
+
|
3
3
|
def initialize(expected_tokens)
|
4
4
|
@expected_tokens = expected_tokens.map do |t|
|
5
5
|
case t
|
@@ -11,24 +11,24 @@ class TokenizeTo
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def matches?(found_tokens)
|
16
16
|
@found_tokens = found_tokens
|
17
17
|
return @found_tokens == @expected_tokens
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
def description
|
21
21
|
"expected to tokenized to #{@expected_tokens.inspect}"
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
def failure_message
|
25
25
|
" #{@expected_tokens.inspect} expected, but found #{@found_tokens.inspect}"
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
def negative_failure_message
|
29
29
|
" expected not to be tokenized to #{@expected_tokens.inspect}"
|
30
|
-
end
|
31
|
-
|
30
|
+
end
|
31
|
+
|
32
32
|
end
|
33
33
|
|
34
34
|
def tokenize_to(*expected_tokens)
|
data/spec/spec_helper.rb
CHANGED
@@ -11,16 +11,16 @@ module SQLTree::Spec
|
|
11
11
|
SQLTree::Node.const_get(const)
|
12
12
|
end
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
module TokenLoader
|
16
16
|
def self.const_missing(const)
|
17
17
|
SQLTree::Token.const_get(const)
|
18
18
|
end
|
19
|
-
end
|
19
|
+
end
|
20
20
|
end
|
21
21
|
|
22
22
|
Spec::Runner.configure do |config|
|
23
|
-
|
23
|
+
|
24
24
|
end
|
25
25
|
|
26
26
|
require "#{File.dirname(__FILE__)}/lib/matchers"
|
@@ -35,12 +35,12 @@ describe SQLTree::Node::Expression do
|
|
35
35
|
logical = SQLTree::Node::Expression['1 AND 2 AND 3']
|
36
36
|
logical.should == SQLTree::Node::Expression['(1 AND 2) AND 3']
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
it "should nest expressions correctly when parentheses are used" do
|
40
40
|
logical = SQLTree::Node::Expression['1 AND (2 AND 3)']
|
41
41
|
logical.should_not == SQLTree::Node::Expression['(1 AND 2) AND 3']
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
it "should parse a NOT expression without parenteheses correctly" do
|
45
45
|
SQLTree::Node::Expression['NOT 1'].should == SQLTree::Node::LogicalNotExpression.new(SQLTree::Node::Value.new(1))
|
46
46
|
end
|
@@ -55,21 +55,21 @@ describe SQLTree::Node::Expression do
|
|
55
55
|
comparison.lhs.should == SQLTree::Node::Value.new(1)
|
56
56
|
comparison.rhs.should == SQLTree::Node::Value.new(2)
|
57
57
|
end
|
58
|
-
|
58
|
+
|
59
59
|
it "should parse an IS NULL expression corectly" do
|
60
60
|
comparison = SQLTree::Node::Expression['field IS NULL']
|
61
61
|
comparison.operator.should == 'IS'
|
62
62
|
comparison.lhs.should == SQLTree::Node::Variable.new('field')
|
63
63
|
comparison.rhs.should == SQLTree::Node::Value.new(nil)
|
64
64
|
end
|
65
|
-
|
65
|
+
|
66
66
|
it "should parse an IS NOT NULL expression corectly" do
|
67
67
|
comparison = SQLTree::Node::Expression['field IS NOT NULL']
|
68
68
|
comparison.operator.should == 'IS NOT'
|
69
69
|
comparison.lhs.should == SQLTree::Node::Variable.new('field')
|
70
70
|
comparison.rhs.should == SQLTree::Node::Value.new(nil)
|
71
71
|
end
|
72
|
-
|
72
|
+
|
73
73
|
it "should parse a LIKE expression corectly" do
|
74
74
|
comparison = SQLTree::Node::Expression["field LIKE '%search%"]
|
75
75
|
comparison.operator.should == 'LIKE'
|
@@ -83,20 +83,20 @@ describe SQLTree::Node::Expression do
|
|
83
83
|
comparison.lhs.should == SQLTree::Node::Variable.new('field')
|
84
84
|
comparison.rhs.should == SQLTree::Node::Value.new('%search%')
|
85
85
|
end
|
86
|
-
|
86
|
+
|
87
87
|
it "should parse an IN expression correctly" do
|
88
88
|
comparison = SQLTree::Node::Expression["field IN (1,2,3,4)"]
|
89
89
|
comparison.operator.should == 'IN'
|
90
90
|
comparison.lhs.should == SQLTree::Node::Variable.new('field')
|
91
91
|
comparison.rhs.should be_kind_of(SQLTree::Node::SetExpression)
|
92
92
|
end
|
93
|
-
|
93
|
+
|
94
94
|
it "should parse a NOT IN expression correctly" do
|
95
95
|
comparison = SQLTree::Node::Expression["field NOT IN (1>2, 3+6, 99)"]
|
96
96
|
comparison.operator.should == 'NOT IN'
|
97
97
|
comparison.lhs.should == SQLTree::Node::Variable.new('field')
|
98
98
|
comparison.rhs.should be_kind_of(SQLTree::Node::SetExpression)
|
99
99
|
end
|
100
|
-
|
100
|
+
|
101
101
|
end
|
102
102
|
end
|
data/spec/unit/leaf_node_spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require "#{File.dirname(__FILE__)}/../spec_helper"
|
2
2
|
|
3
3
|
describe SQLTree::Node::Value do
|
4
|
-
|
4
|
+
|
5
5
|
describe '.parse' do
|
6
6
|
it "should not parse a field name" do
|
7
7
|
lambda { SQLTree::Node::Value['field_name'] }.should raise_error(SQLTree::Parser::UnexpectedToken)
|
@@ -14,11 +14,11 @@ describe SQLTree::Node::Value do
|
|
14
14
|
it "should parse a string correctly" do
|
15
15
|
SQLTree::Node::Value["'123'"].value.should == '123'
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
it "should parse a NULL value correctly" do
|
19
19
|
SQLTree::Node::Value['NULL'].value.should == nil
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -29,23 +29,23 @@ describe SQLTree::Node::Join do
|
|
29
29
|
it "should parse a join table" do
|
30
30
|
SQLTree::Node::Join['LEFT JOIN table ON other.field = table.field'].table.should == 'table'
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
it "should parse the join type" do
|
34
34
|
SQLTree::Node::Join['LEFT JOIN table ON other.field = table.field'].join_type.should == :left
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
37
|
it "should parse the join expression" do
|
38
38
|
SQLTree::Node::Join['LEFT JOIN table ON other.field = table.field'].join_expression.should be_kind_of(SQLTree::Node::Expression)
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
it "should not parse a table alias" do
|
42
42
|
SQLTree::Node::Join['LEFT JOIN table ON other.field = table.field'].table_alias.should be_nil
|
43
|
-
end
|
44
|
-
|
43
|
+
end
|
44
|
+
|
45
45
|
it "should parse a table alias with AS" do
|
46
46
|
SQLTree::Node::Join['LEFT JOIN table AS t ON other.field = table.field'].table_alias.should == 't'
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
it "should parse a table alias without AS" do
|
50
50
|
SQLTree::Node::Join['LEFT JOIN table t ON other.field = table.field'].table_alias.should == 't'
|
51
51
|
end
|
@@ -58,20 +58,20 @@ describe SQLTree::Node::Ordering do
|
|
58
58
|
ordering.expression.name.should == 'field'
|
59
59
|
ordering.direction.should == :asc
|
60
60
|
end
|
61
|
-
|
61
|
+
|
62
62
|
it "should parse an ordering without direction" do
|
63
63
|
ordering = SQLTree::Node::Ordering["table.field"]
|
64
64
|
ordering.expression.table.should == 'table'
|
65
65
|
ordering.expression.name.should == 'field'
|
66
66
|
ordering.direction.should be_nil
|
67
67
|
end
|
68
|
-
|
68
|
+
|
69
69
|
it "should parse an ordering without direction" do
|
70
70
|
ordering = SQLTree::Node::Ordering["MD5(3 + 6) DESC"]
|
71
71
|
ordering.expression.should be_kind_of(SQLTree::Node::FunctionExpression)
|
72
72
|
ordering.direction.should == :desc
|
73
73
|
end
|
74
|
-
|
74
|
+
|
75
75
|
it "shoulde parse multiple orderings" do
|
76
76
|
tree = SQLTree['SELECT * FROM table ORDER BY field1 ASC, field2 DESC']
|
77
77
|
tree.order_by.should have(2).items
|
data/spec/unit/tokenizer_spec.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
require "#{File.dirname(__FILE__)}/../spec_helper"
|
2
2
|
|
3
3
|
describe SQLTree::Tokenizer do
|
4
|
-
|
4
|
+
|
5
5
|
before(:all) do
|
6
6
|
@tokenizer = SQLTree::Tokenizer.new
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
context "recognizing single tokens" do
|
10
10
|
it "should tokenize SQL query keywords" do
|
11
11
|
@tokenizer.tokenize('WHERE').should tokenize_to(:where)
|
@@ -14,7 +14,7 @@ describe SQLTree::Tokenizer do
|
|
14
14
|
it "should tokenize expression keywords" do
|
15
15
|
@tokenizer.tokenize('and').should tokenize_to(:and)
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
it "should tokenize muliple separate keywords" do
|
19
19
|
@tokenizer.tokenize('SELECT DISTINCT').should tokenize_to(:select, :distinct)
|
20
20
|
end
|
@@ -38,49 +38,49 @@ describe SQLTree::Tokenizer do
|
|
38
38
|
it "should tokenize strings" do
|
39
39
|
@tokenizer.tokenize("'hello' ' world '").should tokenize_to('hello', ' world ')
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
it "should tokenize numbers" do
|
43
43
|
@tokenizer.tokenize("1 -2 3.14 -4.0").should tokenize_to(1, -2, 3.14, -4.0)
|
44
|
-
end
|
44
|
+
end
|
45
45
|
|
46
46
|
it "should tokenize logical operators" do
|
47
47
|
@tokenizer.tokenize("< = <> >=").should tokenize_to(:lt, :eq, :ne, :gte)
|
48
48
|
end
|
49
|
-
|
49
|
+
|
50
50
|
it "should tokenize arithmetic operators" do
|
51
51
|
@tokenizer.tokenize("+ - / * %").should tokenize_to(:plus, :minus, :divide, :multiply, :modulo)
|
52
|
-
end
|
53
|
-
|
52
|
+
end
|
53
|
+
|
54
54
|
it "should tokenize parentheses" do
|
55
55
|
@tokenizer.tokenize("(a)").should tokenize_to(lparen, sql_var('a'), rparen)
|
56
|
-
end
|
57
|
-
|
56
|
+
end
|
57
|
+
|
58
58
|
it "should tokenize dots" do
|
59
59
|
@tokenizer.tokenize('a."b"').should tokenize_to(sql_var('a'), dot, sql_var('b'))
|
60
|
-
end
|
61
|
-
|
60
|
+
end
|
61
|
+
|
62
62
|
it "should tokenize commas" do
|
63
63
|
@tokenizer.tokenize('a , "b"').should tokenize_to(sql_var('a'), comma, sql_var('b'))
|
64
64
|
end
|
65
65
|
end
|
66
|
-
|
67
|
-
# # Combined tokens are disabled for now;
|
66
|
+
|
67
|
+
# # Combined tokens are disabled for now;
|
68
68
|
# # Combination is currently done in the parsing phase.
|
69
69
|
# context "combining double keywords" do
|
70
70
|
# it "should tokenize double keywords" do
|
71
71
|
# @tokenizer.tokenize('NOT LIKE').should tokenize_to(:not_like)
|
72
72
|
# end
|
73
73
|
# end
|
74
|
-
|
74
|
+
|
75
75
|
context "when tokenizing full queries or query fragments" do
|
76
76
|
it "should tokenize a full SQL query" do
|
77
77
|
@tokenizer.tokenize("SELECT a.* FROM a_table AS a WHERE a.id > 1").should tokenize_to(
|
78
78
|
:select, sql_var('a'), dot, :multiply, :from, sql_var('a_table'), :as, sql_var('a'), :where, sql_var('a'), dot, sql_var('id'), :gt, 1)
|
79
79
|
end
|
80
|
-
|
80
|
+
|
81
81
|
it "should tokenize a function call" do
|
82
82
|
@tokenizer.tokenize("MD5('test')").should tokenize_to(sql_var('MD5'), lparen, 'test', rparen)
|
83
83
|
end
|
84
84
|
end
|
85
|
-
|
85
|
+
|
86
86
|
end
|
data/sql_tree.gemspec
CHANGED
@@ -3,7 +3,7 @@ Gem::Specification.new do |s|
|
|
3
3
|
|
4
4
|
# Do not modify the version and date values by hand, because this will
|
5
5
|
# automatically by them gem release script.
|
6
|
-
s.version = "0.0.
|
6
|
+
s.version = "0.0.3"
|
7
7
|
s.date = "2009-10-09"
|
8
8
|
|
9
9
|
s.summary = "A pure Ruby library to represent SQL queries with a syntax tree for inspection and modification."
|