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 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
@@ -1,5 +1,5 @@
1
1
  Dir['tasks/*.rake'].each { |file| load(file) }
2
2
 
3
3
  GithubGem::RakeTasks.new(:gem)
4
-
4
+
5
5
  task :default => [:spec]
@@ -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)
@@ -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
@@ -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 " << from.map { |f| f.to_sql }.join(', ')
17
- sql << " WHERE " << where.to_sql if 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
- # TODO: implement me
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
- # TODO: implement me
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)
@@ -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
@@ -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 = {})
@@ -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)
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  require "#{File.dirname(__FILE__)}/../spec_helper"
2
2
 
3
3
  describe SQLTree, :API do
4
-
4
+
5
5
  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
@@ -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
@@ -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.2"
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."
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sql_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Willem van Bergen