sql_tree 0.0.2 → 0.0.3
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/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."
|