wvanbergen-sql_tree 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.rdoc ADDED
@@ -0,0 +1,6 @@
1
+ === SQLTree
2
+
3
+ SQLTree is a pure Ruby library to represent SQL queries as a tree of nodes.
4
+
5
+ The library can parse an SQL query (a string) to represent the query using
6
+ a tree, and it can generate an SQL query from a constructed tree.
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ Dir['tasks/*.rake'].each { |file| load(file) }
2
+
3
+ task :default => [:spec]
data/lib/sql_tree.rb ADDED
@@ -0,0 +1,14 @@
1
+ module SQLTree
2
+
3
+ end
4
+
5
+ require 'sql_tree/token'
6
+ require 'sql_tree/tokenizer'
7
+ require 'sql_tree/parser'
8
+
9
+ require 'sql_tree/node'
10
+ require 'sql_tree/node/select_query'
11
+ require 'sql_tree/node/expression'
12
+ require 'sql_tree/node/leafs'
13
+
14
+ require 'sql_tree/generator'
@@ -0,0 +1,3 @@
1
+ module SQLTree::Generator
2
+
3
+ end
@@ -0,0 +1,22 @@
1
+ class SQLTree::Node
2
+
3
+ def inspect
4
+ "#{self.class.name}[#{self.to_sql}]"
5
+ end
6
+
7
+ def quote_var(name)
8
+ "\"#{name}\""
9
+ end
10
+
11
+ def quote_str(str)
12
+ "'#{str.gsub(/\'/, "''")}'"
13
+ end
14
+
15
+ def self.[](arg)
16
+ case arg
17
+ when Symbol; Variable.new(arg.to_s)
18
+ else; Value.new(arg)
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,214 @@
1
+ class SQLTree::Parser
2
+
3
+ def current_token
4
+ @current_token
5
+ end
6
+
7
+ def next_token
8
+ @current_token = @tokens.shift
9
+ end
10
+
11
+ def consume(check)
12
+ error(current_token) unless check == next_token
13
+ end
14
+
15
+ def peek_token(distance = 1)
16
+ @tokens[distance - 1]
17
+ end
18
+
19
+ def error(token)
20
+ raise "Unexpected token: #{token.inspect}"
21
+ end
22
+
23
+ def debug
24
+ p @tokens.inspect
25
+ end
26
+
27
+ def parse(tokens, options = {:as => :query})
28
+ if tokens.kind_of?(String)
29
+ tokenizer = SQLTree::Tokenizer.new
30
+ @tokens = tokenizer.tokenize(tokens)
31
+ else
32
+ @tokens = tokens
33
+ end
34
+
35
+ send("parse_#{options[:as]}".to_sym)
36
+ end
37
+
38
+ def parse_query
39
+ case peek_token
40
+ when SQLTree::Token::SELECT; parse_select_query
41
+ else raise "Could not parse query"
42
+ end
43
+ end
44
+
45
+ def parse_select_query
46
+ select_node = SQLTree::Node::SelectQuery.new
47
+ consume(SQLTree::Token::SELECT)
48
+
49
+ if peek_token == SQLTree::Token::DISTINCT
50
+ consume(SQLTree::Token::DISTINCT)
51
+ select_node.distinct = true
52
+ end
53
+
54
+ select_node.select = parse_select_clause
55
+ select_node.from = parse_from_clause if peek_token == SQLTree::Token::FROM
56
+ select_node.where = parse_where_clause if peek_token == SQLTree::Token::WHERE
57
+
58
+ return select_node
59
+ end
60
+
61
+ def parse_select_clause
62
+ expressions = [parse_select_expression]
63
+ while peek_token == SQLTree::Token::COMMA
64
+ consume(SQLTree::Token::COMMA)
65
+ expressions << parse_select_expression
66
+ end
67
+ return expressions
68
+ end
69
+
70
+ def parse_select_expression
71
+ if peek_token == SQLTree::Token::MULTIPLY
72
+ consume(SQLTree::Token::MULTIPLY)
73
+ return SQLTree::Node::ALL_FIELDS
74
+ else
75
+ expr = SQLTree::Node::SelectExpression.new(parse_expression)
76
+ if peek_token == SQLTree::Token::AS
77
+ consume(SQLTree::Token::AS)
78
+ expr.variable = parse_variable_name
79
+ end
80
+ return expr
81
+ end
82
+ end
83
+
84
+ def parse_single_expression
85
+ if SQLTree::Token::LPAREN === peek_token(1)
86
+ consume(SQLTree::Token::LPAREN)
87
+ expr = parse_expression
88
+ consume(SQLTree::Token::RPAREN)
89
+ return expr
90
+ elsif SQLTree::Token::Variable === peek_token(1) && peek_token(2) == SQLTree::Token::LPAREN
91
+ return parse_function_call
92
+ elsif SQLTree::Token::Variable === peek_token(1)
93
+ return parse_variable
94
+ else
95
+ return parse_value
96
+ end
97
+ end
98
+
99
+ def parse_expression
100
+ parse_logical_expression
101
+ end
102
+
103
+ def parse_secondary_arithmetic_expression
104
+ expr = parse_single_expression
105
+ while [SQLTree::Token::PLUS, SQLTree::Token::MINUS].include?(peek_token)
106
+ expr = SQLTree::Node::ArithmeticExpression.new(next_token.literal, expr, parse_single_expression)
107
+ end
108
+ return expr
109
+ end
110
+
111
+
112
+ def parse_primary_arithmetic_expression
113
+ expr = parse_secondary_arithmetic_expression
114
+ while [SQLTree::Token::PLUS, SQLTree::Token::MINUS].include?(peek_token)
115
+ expr = SQLTree::Node::ArithmeticExpression.new(next_token.literal, expr, parse_secondary_arithmetic_expression)
116
+ end
117
+ return expr
118
+ end
119
+
120
+ def parse_comparison_expression
121
+ expr = parse_primary_arithmetic_expression
122
+ while [SQLTree::Token::EQ, SQLTree::Token::NE, SQLTree::Token::GT, SQLTree::Token::GTE, SQLTree::Token::LT, SQLTree::Token::LTE].include?(peek_token)
123
+ expr = SQLTree::Node::ComparisonExpression.new(next_token.literal, expr, parse_primary_arithmetic_expression)
124
+ end
125
+ return expr
126
+ end
127
+
128
+ def parse_logical_expression
129
+ expr = parse_comparison_expression
130
+ while [SQLTree::Token::AND, SQLTree::Token::OR].include?(peek_token)
131
+ expr = SQLTree::Node::LogicalExpression.new(next_token.literal, [expr, parse_comparison_expression])
132
+ end
133
+ return expr
134
+ end
135
+
136
+ def parse_logical_not_expression
137
+
138
+ end
139
+
140
+ def parse_function_call
141
+
142
+ expr = SQLTree::Node::FunctionExpression.new(next_token.literal)
143
+ consume(SQLTree::Token::LPAREN)
144
+ until peek_token == SQLTree::Token::RPAREN
145
+ expr.arguments << parse_expression
146
+ consume(SQLTree::Token::COMMA) if peek_token == SQLTree::Token::COMMA
147
+ end
148
+ consume(SQLTree::Token::RPAREN)
149
+ return expr
150
+ end
151
+
152
+ def parse_from_clause
153
+ consume(SQLTree::Token::FROM)
154
+ from_expressions = [parse_from_expression]
155
+ while peek_token == SQLTree::Token::COMMA
156
+ consume(SQLTree::Token::COMMA)
157
+ from_expressions << parse_from_expression
158
+ end
159
+
160
+ return from_expressions
161
+ end
162
+
163
+ def parse_from_expression
164
+ from_expression = case peek_token
165
+ when SQLTree::Token::Variable; parse_table_import
166
+ else; error(peek_token)
167
+ end
168
+ return from_expression
169
+ end
170
+
171
+ def parse_table_import
172
+ table_import = SQLTree::Node::TableImport.new(next_token.literal)
173
+ if peek_token == SQLTree::Token::AS || SQLTree::Token::Variable === peek_token
174
+ consume(SQLTree::Token::AS) if peek_token == SQLTree::Token::AS
175
+ table_import.variable = parse_variable
176
+ end
177
+ return table_import
178
+ end
179
+
180
+ def parse_field
181
+ lhs = next_token
182
+ lhs = (lhs == SQLTree::Token::MULTIPLY) ? :all : lhs.literal
183
+
184
+ if peek_token == SQLTree::Token::DOT
185
+ consume(SQLTree::Token::DOT)
186
+ rhs = next_token
187
+ rhs = (rhs == SQLTree::Token::MULTIPLY) ? :all : rhs.literal
188
+ SQLTree::Node::Field.new(rhs, lhs)
189
+ else
190
+ SQLTree::Node::Field.new(lhs)
191
+ end
192
+ end
193
+
194
+ def parse_variable_name
195
+ return SQLTree::Node::Variable.new(next_token.literal)
196
+ end
197
+
198
+ def parse_variable
199
+ if peek_token(2) == SQLTree::Token::DOT
200
+ parse_field
201
+ else
202
+ parse_variable_name
203
+ end
204
+ end
205
+
206
+ def parse_value
207
+ SQLTree::Node::Value.new(next_token.literal)
208
+ end
209
+
210
+
211
+ def parse_where_clause
212
+ end
213
+
214
+ end
@@ -0,0 +1,63 @@
1
+ class SQLTree::Token
2
+
3
+ attr_accessor :literal
4
+
5
+ def initialize(literal)
6
+ @literal = literal
7
+ end
8
+
9
+ def ==(other)
10
+ other.class == self.class && @literal == other.literal
11
+ end
12
+
13
+ alias :eql? :==
14
+
15
+ def inspect
16
+ literal
17
+ end
18
+
19
+ # Token types
20
+
21
+ class Value < SQLTree::Token
22
+ end
23
+
24
+ class Variable < SQLTree::Token::Value
25
+ end
26
+
27
+ class String < SQLTree::Token::Value
28
+ end
29
+
30
+ class Number < SQLTree::Token::Value
31
+ end
32
+
33
+ class Keyword < SQLTree::Token
34
+ def inspect
35
+ ":#{literal.downcase}"
36
+ end
37
+ end
38
+
39
+ class Operator < SQLTree::Token
40
+ def inspect
41
+ OPERATORS[literal].inspect
42
+ end
43
+ end
44
+
45
+ LPAREN = Class.new(SQLTree::Token).new('(')
46
+ RPAREN = Class.new(SQLTree::Token).new(')')
47
+ DOT = Class.new(SQLTree::Token).new('.')
48
+ COMMA = Class.new(SQLTree::Token).new(',')
49
+
50
+ KEYWORDS = %w{select from where group having order distinct left right inner outer join and or not as}
51
+ KEYWORDS.each do |kw|
52
+ self.const_set(kw.upcase, Class.new(SQLTree::Token::Keyword).new(kw.upcase))
53
+ end
54
+
55
+ ARITHMETHIC_OPERATORS = { '+' => :plus, '-' => :minus, '*' => :multiply, '/' => :divide, '%' => :modulo }
56
+ LOGICAL_OPERATORS = { '=' => :eq, '!=' => :ne, '<>' => :ne, '>' => :gt, '<' => :lt, '>=' => :gte, '<=' => :lte }
57
+
58
+ OPERATORS = ARITHMETHIC_OPERATORS.merge(LOGICAL_OPERATORS)
59
+ OPERATORS.each do |literal, symbol|
60
+ self.const_set(symbol.to_s.upcase, Class.new(SQLTree::Token::Operator).new(literal)) unless self.const_defined?(symbol.to_s.upcase)
61
+ end
62
+
63
+ end
@@ -0,0 +1,99 @@
1
+ class SQLTree::Tokenizer
2
+
3
+ include Enumerable
4
+
5
+ def tokenize(string)
6
+ @string = string
7
+ @current_char_pos = -1
8
+ to_a
9
+ end
10
+
11
+ def current_char
12
+ @current_char
13
+ end
14
+
15
+ def peek_char(amount = 1)
16
+ @string[@current_char_pos + amount, 1]
17
+ end
18
+
19
+ def next_char
20
+ @current_char_pos += 1
21
+ @current_char = @string[@current_char_pos, 1]
22
+ end
23
+
24
+ OPERATOR_CHARS = /\=|<|>|!|\-|\+|\/|\*|\%/
25
+
26
+ def each_token(&block)
27
+ while next_char
28
+ case current_char
29
+ when /^\s?$/; # whitespace, go to next token
30
+ when '('; yield(SQLTree::Token::LPAREN)
31
+ when ')'; yield(SQLTree::Token::RPAREN)
32
+ when '.'; yield(SQLTree::Token::DOT)
33
+ when '.'; yield(SQLTree::Token::COMMA)
34
+ when /\d/; tokenize_number(&block)
35
+ when "'"; tokenize_quoted_string(&block)
36
+ when OPERATOR_CHARS; tokenize_operator(&block)
37
+ when /\w/; tokenize_literal(&block)
38
+ when '"'; tokenize_quoted_literal(&block) # TODO: allow MySQL quoting mode
39
+ end
40
+ end
41
+ end
42
+
43
+ alias :each :each_token
44
+
45
+ def tokenize_literal(&block)
46
+ literal = current_char
47
+ literal << next_char while /[\w]/ =~ peek_char
48
+
49
+ if SQLTree::Token::KEYWORDS.include?(literal.downcase)
50
+ yield(SQLTree::Token.const_get(literal.upcase))
51
+ else
52
+ yield(SQLTree::Token::Variable.new(literal))
53
+ end
54
+ end
55
+
56
+ def tokenize_number(&block)
57
+ number = current_char
58
+ dot_encountered = false
59
+ while /\d/ =~ peek_char || (peek_char == '.' && !dot_encountered)
60
+ dot_encountered = true if peek_char == '.'
61
+ number << next_char
62
+ end
63
+
64
+ if dot_encountered
65
+ yield(SQLTree::Token::Number.new(number.to_f))
66
+ else
67
+ yield(SQLTree::Token::Number.new(number.to_i))
68
+ end
69
+ end
70
+
71
+ def tokenize_quoted_string(&block)
72
+ string = ''
73
+ until next_char.nil? || current_char == "'"
74
+ string << (current_char == "\\" ? next_char : current_char)
75
+ end
76
+ yield(SQLTree::Token::String.new(string))
77
+ end
78
+
79
+ def tokenize_quoted_literal(&block)
80
+ literal = ''
81
+ until next_char.nil? || current_char == '"' # TODO: allow MySQL quoting mode
82
+ literal << (current_char == "\\" ? next_char : current_char)
83
+ end
84
+ yield(SQLTree::Token::Variable.new(literal))
85
+ end
86
+
87
+
88
+ def tokenize_operator(&block)
89
+ operator = current_char
90
+ if operator == '-' && /[\d\.]/ =~ peek_char
91
+ tokenize_number(&block)
92
+ else
93
+ operator << next_char if SQLTree::Token::OPERATORS.has_key?(operator + peek_char)
94
+ yield(SQLTree::Token.const_get(SQLTree::Token::OPERATORS[operator].to_s.upcase))
95
+ end
96
+ end
97
+
98
+
99
+ end
@@ -0,0 +1,5 @@
1
+ require "#{File.dirname(__FILE__)}/../spec_helper"
2
+
3
+ describe SQLTree, :API do
4
+
5
+ end
@@ -0,0 +1,80 @@
1
+ class ParseAs
2
+
3
+ def initialize(expected_tree)
4
+ @expected_tree = expected_tree
5
+ end
6
+
7
+ def matches?(found_tree)
8
+ @found_tree = found_tree.to_tree
9
+ return @found_tree.eql?(@expected_tree)
10
+ end
11
+
12
+ def description
13
+ "expected to parse to #{@expected_tree.inspect}"
14
+ end
15
+
16
+ def failure_message
17
+ " #{@expected_tree.inspect} expected, but found #{@found_tree.inspect}"
18
+ end
19
+
20
+ def negative_failure_message
21
+ " expected not to be tokenized to #{@expected_tree.inspect}"
22
+ end
23
+ end
24
+
25
+ def parse_as(tree)
26
+ ParseAs.new(tree)
27
+ end
28
+
29
+ class TokenizeTo
30
+
31
+ def initialize(expected_tokens)
32
+ @expected_tokens = expected_tokens.map do |t|
33
+ case t
34
+ when SQLTree::Token; t
35
+ when String; SQLTree::Token::String.new(t)
36
+ when Integer; SQLTree::Token::Number.new(t)
37
+ when Float; SQLTree::Token::Number.new(t)
38
+ when Symbol; SQLTree::Token.const_get(t.to_s.upcase)
39
+ end
40
+ end
41
+ end
42
+
43
+ def matches?(found_tokens)
44
+ @found_tokens = found_tokens
45
+ return @found_tokens.eql?(@expected_tokens)
46
+ end
47
+
48
+ def description
49
+ "expected to tokenized to #{@expected_tokens.inspect}"
50
+ end
51
+
52
+ def failure_message
53
+ " #{@expected_tokens.inspect} expected, but found #{@found_tokens.inspect}"
54
+ end
55
+
56
+ def negative_failure_message
57
+ " expected not to be tokenized to #{@expected_tokens.inspect}"
58
+ end
59
+
60
+ end
61
+
62
+ def tokenize_to(*expected_tokens)
63
+ TokenizeTo.new(expected_tokens)
64
+ end
65
+
66
+ def sql_var(name)
67
+ SQLTree::Token::Variable.new(name.to_s)
68
+ end
69
+
70
+ def dot
71
+ SQLTree::Token::DOT
72
+ end
73
+
74
+ def lparen
75
+ SQLTree::Token::LPAREN
76
+ end
77
+
78
+ def rparen
79
+ SQLTree::Token::RPAREN
80
+ end
@@ -0,0 +1,11 @@
1
+ $:.reject! { |e| e.include? 'TextMate' }
2
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'rubygems'
5
+ require 'spec'
6
+ require 'sql_tree'
7
+
8
+ module SQLTree::Spec
9
+ end
10
+
11
+ require "#{File.dirname(__FILE__)}/lib/matchers"
@@ -0,0 +1,81 @@
1
+ require "#{File.dirname(__FILE__)}/../spec_helper"
2
+
3
+ describe SQLTree::Parser do
4
+
5
+ before(:all) do
6
+ @parser = SQLTree::Parser.new
7
+ end
8
+
9
+ context :expressions do
10
+
11
+ it "should parse a variable" do
12
+ @parser.parse("field", :as => :expression).should eql(SQLTree::Node[:field])
13
+ end
14
+
15
+ it "should parse a table field" do
16
+ @parser.parse('tbl.field', :as => :expression).should eql(SQLTree::Node::Field[:tbl, :field])
17
+ end
18
+
19
+
20
+ it "should parse a number" do
21
+ @parser.parse("1.0", :as => :expression).should eql(SQLTree::Node[1.0])
22
+ end
23
+
24
+ it "should parse a string" do
25
+ @parser.parse("'str'", :as => :expression).should eql(SQLTree::Node['str'])
26
+ end
27
+
28
+ it "should parse a function call without arguments" do
29
+ @parser.parse("NOW()", :as => :expression).should parse_as([:NOW])
30
+ end
31
+
32
+ it "should parse a function call with a single numeric argument" do
33
+ @parser.parse("MD5('abc')", :as => :expression).should parse_as([:MD5, 'abc'])
34
+ end
35
+
36
+ it "should parse a function call with multiple arguments" do
37
+ @parser.parse("CONCAT('Mr. ', last_name)", :as => :expression).should parse_as([:CONCAT, 'Mr. ', :last_name])
38
+ end
39
+
40
+ it "should parse a simple arithmetic operator" do
41
+ @parser.parse("1 + 2", :as => :expression).should parse_as([:plus, 1, 2])
42
+ end
43
+
44
+ it "should parse a simple comparison" do
45
+ @parser.parse("1 < 2", :as => :expression).should parse_as([:lt, 1, 2])
46
+ end
47
+
48
+ it "should parse a simple subexpressions with parenthesis on the start" do
49
+ @parser.parse("(1 + 1) = 2", :as => :expression).should parse_as([:eq, [:plus, 1, 1], 2])
50
+ end
51
+
52
+ it "should parse a simple subexpressions with parenthesis on the end" do
53
+ @parser.parse("1 + (1 = 2)", :as => :expression).should parse_as([:plus, 1, [:eq, 1, 2]])
54
+ end
55
+
56
+ it "should parse arithmetic operations before comparisons" do
57
+ @parser.parse("1 + 1 = 2", :as => :expression).should parse_as([:eq, [:plus, 1, 1], 2])
58
+ end
59
+
60
+ it "should parse arithmetic operations before comparisons" do
61
+ @parser.parse("1 + 2 - 3", :as => :expression).should parse_as([:minus, [:plus, 1, 2], 3])
62
+ end
63
+
64
+
65
+ it "should parse arithmetic operations before comparisons" do
66
+ @parser.parse("a and b", :as => :expression).should parse_as([:and, :a, :b])
67
+ end
68
+
69
+ it "should parse arithmetic operations before comparisons" do
70
+ @parser.parse("a and b and c", :as => :expression).should parse_as([:and, [:and, :a, :b], :c])
71
+ end
72
+
73
+ it "should parse arithmetic operations before comparisons" do
74
+ @parser.parse("a and b or c", :as => :expression).should parse_as([:or, [:and, :a, :b], :c])
75
+ end
76
+
77
+
78
+ end
79
+
80
+
81
+ end
@@ -0,0 +1,76 @@
1
+ require "#{File.dirname(__FILE__)}/../spec_helper"
2
+
3
+ describe SQLTree::Tokenizer do
4
+
5
+ before(:all) do
6
+ @tokenizer = SQLTree::Tokenizer.new
7
+ end
8
+
9
+ context "recognizing single tokens" do
10
+ it "should tokenize SQL query keywords" do
11
+ @tokenizer.tokenize('WHERE').should tokenize_to(:where)
12
+ end
13
+
14
+ it "should tokenize expression keywords" do
15
+ @tokenizer.tokenize('and').should tokenize_to(:and)
16
+ end
17
+
18
+ it "should tokenize muliple keywords" do
19
+ @tokenizer.tokenize('SELECT DISTINCT').should tokenize_to(:select, :distinct)
20
+ end
21
+
22
+ it "should ignore excessive whitespace" do
23
+ @tokenizer.tokenize("\tSELECT DISTINCT \r\r").should tokenize_to(:select, :distinct)
24
+ end
25
+
26
+ it "should tokenize variables" do
27
+ @tokenizer.tokenize("var").should tokenize_to(sql_var('var'))
28
+ end
29
+
30
+ it "should tokenize quoted variables" do
31
+ @tokenizer.tokenize('"var"').should tokenize_to(sql_var('var'))
32
+ end
33
+
34
+ it "should tokenize strings" do
35
+ @tokenizer.tokenize("'hello' ' world '").should tokenize_to('hello', ' world ')
36
+ end
37
+
38
+ it "should tokenize numbers" do
39
+ @tokenizer.tokenize("1 -2 3.14 -4.0").should tokenize_to(1, -2, 3.14, -4.0)
40
+ end
41
+
42
+ it "should tokenize logical operators" do
43
+ @tokenizer.tokenize("< = <> >=").should tokenize_to(:lt, :eq, :ne, :gte)
44
+ end
45
+
46
+ it "should tokenize arithmetic operators" do
47
+ @tokenizer.tokenize("+ - / * %").should tokenize_to(:plus, :minus, :divide, :multiply, :modulo)
48
+ end
49
+
50
+ it "should tokenize parentheses" do
51
+ @tokenizer.tokenize("(a)").should tokenize_to(lparen, sql_var('a'), rparen)
52
+ end
53
+
54
+ it "should tokenize dots" do
55
+ @tokenizer.tokenize('a."b"').should tokenize_to(sql_var('a'), dot, sql_var('b'))
56
+ end
57
+
58
+ it "should tokenize commas" do
59
+ @tokenizer.tokenize('a,"b"').should tokenize_to(sql_var('a'), comma, sql_var('b'))
60
+ end
61
+
62
+
63
+ end
64
+
65
+ context "combined tokens" do
66
+ it "should tokenize a full sql_query" do
67
+ @tokenizer.tokenize("SELECT a.* FROM a_table AS a WHERE a.id > 1").should tokenize_to(
68
+ :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)
69
+ end
70
+
71
+ it "should tokenize a function call" do
72
+ @tokenizer.tokenize("MD5('test')").should tokenize_to(sql_var('MD5'), lparen, 'test', rparen)
73
+ end
74
+ end
75
+
76
+ end
@@ -0,0 +1,250 @@
1
+ require 'rubygems'
2
+ require 'rubyforge'
3
+ require 'rake'
4
+ require 'rake/tasklib'
5
+ require 'date'
6
+
7
+ module Rake
8
+
9
+ class GithubGem < TaskLib
10
+
11
+ attr_accessor :name
12
+ attr_accessor :specification
13
+
14
+ def self.define_tasks!
15
+ gem_task_builder = Rake::GithubGem.new
16
+ gem_task_builder.register_all_tasks!
17
+ end
18
+
19
+
20
+ def initialize
21
+ reload_gemspec!
22
+ end
23
+
24
+ def register_all_tasks!
25
+ namespace(:gem) do
26
+ desc "Updates the file lists for this gem"
27
+ task(:manifest) { manifest_task }
28
+
29
+ desc "Releases a new version of #{@name}"
30
+ task(:build => [:manifest]) { build_task }
31
+
32
+
33
+ release_dependencies = [:check_clean_master_branch, :version, :build, :create_tag]
34
+ release_dependencies.push 'doc:publish' if has_rdoc?
35
+ release_dependencies.unshift 'test' if has_tests?
36
+ release_dependencies.unshift 'spec' if has_specs?
37
+
38
+ desc "Releases a new version of #{@name}"
39
+ task(:release => release_dependencies) { release_task }
40
+
41
+ # helper task for releasing
42
+ task(:check_clean_master_branch) { verify_clean_status('master') }
43
+ task(:check_version) { verify_version(ENV['VERSION'] || @specification.version) }
44
+ task(:version => [:check_version]) { set_gem_version! }
45
+ task(:create_tag) { create_version_tag! }
46
+ end
47
+
48
+ # Register RDoc tasks
49
+ if has_rdoc?
50
+ require 'rake/rdoctask'
51
+
52
+ namespace(:doc) do
53
+ desc 'Generate documentation for request-log-analyzer'
54
+ Rake::RDocTask.new(:compile) do |rdoc|
55
+ rdoc.rdoc_dir = 'doc'
56
+ rdoc.title = @name
57
+ rdoc.options += @specification.rdoc_options
58
+ rdoc.rdoc_files.include(@specification.extra_rdoc_files)
59
+ rdoc.rdoc_files.include('lib/**/*.rb')
60
+ end
61
+
62
+ desc "Publish RDoc files for #{@name} to Github"
63
+ task(:publish => :compile) do
64
+ sh 'git checkout gh-pages'
65
+ sh 'git pull origin gh-pages'
66
+ sh 'cp -rf doc/* .'
67
+ sh "git commit -am \"Publishing newest RDoc documentation for #{@name}\""
68
+ sh "git push origin gh-pages"
69
+ sh "git checkout master"
70
+ end
71
+ end
72
+ end
73
+
74
+ # Setup :spec task if RSpec files exist
75
+ if has_specs?
76
+ require 'spec/rake/spectask'
77
+
78
+ desc "Run all specs for #{@name}"
79
+ Spec::Rake::SpecTask.new(:spec) do |t|
80
+ t.spec_files = FileList['spec/**/*_spec.rb']
81
+ end
82
+ end
83
+
84
+ # Setup :test task if unit test files exist
85
+ if has_tests?
86
+ require 'rake/testtask'
87
+
88
+ desc "Run all unit tests for #{@name}"
89
+ Rake::TestTask.new(:test) do |t|
90
+ t.pattern = 'test/**/*_test.rb'
91
+ t.verbose = true
92
+ t.libs << 'test'
93
+ end
94
+ end
95
+ end
96
+
97
+ protected
98
+
99
+ def has_rdoc?
100
+ @specification.has_rdoc
101
+ end
102
+
103
+ def has_specs?
104
+ Dir['spec/**/*_spec.rb'].any?
105
+ end
106
+
107
+ def has_tests?
108
+ Dir['test/**/*_test.rb'].any?
109
+ end
110
+
111
+ def reload_gemspec!
112
+ raise "No gemspec file found!" if gemspec_file.nil?
113
+ spec = File.read(gemspec_file)
114
+ @specification = eval(spec)
115
+ @name = specification.name
116
+ end
117
+
118
+ def run_command(command)
119
+ lines = []
120
+ IO.popen(command) { |f| lines = f.readlines }
121
+ return lines
122
+ end
123
+
124
+ def git_modified?(file)
125
+ return !run_command('git status').detect { |line| Regexp.new(Regexp.quote(file)) =~ line }.nil?
126
+ end
127
+
128
+ def git_commit_file(file, message, branch = nil)
129
+ verify_current_branch(branch) unless branch.nil?
130
+ if git_modified?(file)
131
+ sh "git add #{file}"
132
+ sh "git commit -m \"#{message}\""
133
+ else
134
+ raise "#{file} is not modified and cannot be committed!"
135
+ end
136
+ end
137
+
138
+ def git_create_tag(tag_name, message)
139
+ sh "git tag -a \"#{tag_name}\" -m \"#{message}\""
140
+ end
141
+
142
+ def git_push(remote = 'origin', branch = 'master', options = [])
143
+ verify_clean_status(branch)
144
+ options_str = options.map { |o| "--#{o}"}.join(' ')
145
+ sh "git push #{options_str} #{remote} #{branch}"
146
+ end
147
+
148
+ def gemspec_version=(new_version)
149
+ spec = File.read(gemspec_file)
150
+ spec.gsub!(/^(\s*s\.version\s*=\s*)('|")(.+)('|")(\s*)$/) { "#{$1}'#{new_version}'#{$5}" }
151
+ spec.gsub!(/^(\s*s\.date\s*=\s*)('|")(.+)('|")(\s*)$/) { "#{$1}'#{Date.today.strftime('%Y-%m-%d')}'#{$5}" }
152
+ File.open(gemspec_file, 'w') { |f| f << spec }
153
+ reload_gemspec!
154
+ end
155
+
156
+ def gemspec_date=(new_date)
157
+ spec = File.read(gemspec_file)
158
+ spec.gsub!(/^(\s*s\.date\s*=\s*)('|")(.+)('|")(\s*)$/) { "#{$1}'#{new_date.strftime('%Y-%m-%d')}'#{$5}" }
159
+ File.open(gemspec_file, 'w') { |f| f << spec }
160
+ reload_gemspec!
161
+ end
162
+
163
+ def gemspec_file
164
+ @gemspec_file ||= Dir['*.gemspec'].first
165
+ end
166
+
167
+ def verify_current_branch(branch)
168
+ run_command('git branch').detect { |line| /^\* (.+)/ =~ line }
169
+ raise "You are currently not working in the master branch!" unless branch == $1
170
+ end
171
+
172
+ def verify_clean_status(on_branch = nil)
173
+ sh "git fetch"
174
+ lines = run_command('git status')
175
+ raise "You don't have the most recent version available. Run git pull first." if /^\# Your branch is behind/ =~ lines[1]
176
+ raise "You are currently not working in the #{on_branch} branch!" unless on_branch.nil? || (/^\# On branch (.+)/ =~ lines.first && $1 == on_branch)
177
+ raise "Your master branch contains modifications!" unless /^nothing to commit \(working directory clean\)/ =~ lines.last
178
+ end
179
+
180
+ def verify_version(new_version)
181
+ newest_version = run_command('git tag').map { |tag| tag.split(name + '-').last }.compact.map { |v| Gem::Version.new(v) }.max
182
+ raise "This version number (#{new_version}) is not higher than the highest tagged version (#{newest_version})" if !newest_version.nil? && newest_version >= Gem::Version.new(new_version.to_s)
183
+ end
184
+
185
+ def set_gem_version!
186
+ # update gemspec file
187
+ self.gemspec_version = ENV['VERSION'] if Gem::Version.correct?(ENV['VERSION'])
188
+ self.gemspec_date = Date.today
189
+ end
190
+
191
+ def manifest_task
192
+ verify_current_branch('master')
193
+
194
+ list = Dir['**/*'].sort
195
+ list -= [gemspec_file]
196
+
197
+ if File.exist?('.gitignore')
198
+ File.read('.gitignore').each_line do |glob|
199
+ glob = glob.chomp.sub(/^\//, '')
200
+ list -= Dir[glob]
201
+ list -= Dir["#{glob}/**/*"] if File.directory?(glob) and !File.symlink?(glob)
202
+ end
203
+ end
204
+
205
+ # update the spec file
206
+ spec = File.read(gemspec_file)
207
+ spec.gsub! /^(\s* s.(test_)?files \s* = \s* )( \[ [^\]]* \] | %w\( [^)]* \) )/mx do
208
+ assignment = $1
209
+ bunch = $2 ? list.grep(/^(test.*_test\.rb|spec.*_spec.rb)$/) : list
210
+ '%s%%w(%s)' % [assignment, bunch.join(' ')]
211
+ end
212
+
213
+ File.open(gemspec_file, 'w') { |f| f << spec }
214
+ reload_gemspec!
215
+ end
216
+
217
+ def build_task
218
+ sh "gem build #{gemspec_file}"
219
+ Dir.mkdir('pkg') unless File.exist?('pkg')
220
+ sh "mv #{name}-#{specification.version}.gem pkg/#{name}-#{specification.version}.gem"
221
+ end
222
+
223
+ def install_task
224
+ raise "#{name} .gem file not found" unless File.exist?("pkg/#{name}-#{specification.version}.gem")
225
+ sh "gem install pkg/#{name}-#{specification.version}.gem"
226
+ end
227
+
228
+ def uninstall_task
229
+ raise "#{name} .gem file not found" unless File.exist?("pkg/#{name}-#{specification.version}.gem")
230
+ sh "gem uninstall #{name}"
231
+ end
232
+
233
+ def create_version_tag!
234
+ # commit the gemspec file
235
+ git_commit_file(gemspec_file, "Updated #{gemspec_file} for release of version #{@specification.version}") if git_modified?(gemspec_file)
236
+
237
+ # create tag and push changes
238
+ git_create_tag("#{@name}-#{@specification.version}", "Tagged version #{@specification.version}")
239
+ git_push('origin', 'master', [:tags])
240
+ end
241
+
242
+ def release_task
243
+ puts
244
+ puts '------------------------------------------------------------'
245
+ puts "Released #{@name} - version #{@specification.version}"
246
+ end
247
+ end
248
+ end
249
+
250
+ Rake::GithubGem.define_tasks!
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wvanbergen-sql_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Willem van Bergen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-08-02 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: To make it easier to build and manipulate SQL queries, sql_tree can parse an SQL query to represent it as a tree of nodes and can generate an SQL query given a tree as input
17
+ email: willem@vanbergen.org
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ files:
25
+ - README.rdoc
26
+ - Rakefile
27
+ - lib
28
+ - lib/sql_tree
29
+ - lib/sql_tree.rb
30
+ - lib/sql_tree/generator.rb
31
+ - lib/sql_tree/node
32
+ - lib/sql_tree/node.rb
33
+ - lib/sql_tree/parser.rb
34
+ - lib/sql_tree/token.rb
35
+ - lib/sql_tree/tokenizer.rb
36
+ - spec
37
+ - spec/integration
38
+ - spec/integration/api_spec.rb
39
+ - spec/lib
40
+ - spec/lib/matchers.rb
41
+ - spec/spec_helper.rb
42
+ - spec/unit
43
+ - spec/unit/parser_spec.rb
44
+ - spec/unit/tokenizer_spec.rb
45
+ - tasks
46
+ - tasks/github-gem.rake
47
+ has_rdoc: true
48
+ homepage: http://wiki.github.com/wvanbergen/sql_tree
49
+ licenses:
50
+ post_install_message:
51
+ rdoc_options:
52
+ - --title
53
+ - sql_tree
54
+ - --main
55
+ - README.rdoc
56
+ - --line-numbers
57
+ - --inline-source
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.5
76
+ signing_key:
77
+ specification_version: 2
78
+ summary: A pure Ruby library to represent SQL queries as trees of nodes.
79
+ test_files:
80
+ - spec/integration/api_spec.rb
81
+ - spec/unit/parser_spec.rb
82
+ - spec/unit/tokenizer_spec.rb