sql_tree 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/LICENSE +20 -0
- data/README.rdoc +25 -0
- data/Rakefile +5 -0
- data/lib/sql_tree.rb +42 -0
- data/lib/sql_tree/node.rb +37 -0
- data/lib/sql_tree/node/expression.rb +240 -0
- data/lib/sql_tree/node/field.rb +50 -0
- data/lib/sql_tree/node/join.rb +50 -0
- data/lib/sql_tree/node/select_expression.rb +45 -0
- data/lib/sql_tree/node/select_query.rb +78 -0
- data/lib/sql_tree/node/source.rb +37 -0
- data/lib/sql_tree/node/table_reference.rb +34 -0
- data/lib/sql_tree/node/value.rb +37 -0
- data/lib/sql_tree/node/variable.rb +35 -0
- data/lib/sql_tree/parser.rb +63 -0
- data/lib/sql_tree/token.rb +131 -0
- data/lib/sql_tree/tokenizer.rb +174 -0
- data/spec/integration/api_spec.rb +5 -0
- data/spec/integration/full_queries_spec.rb +21 -0
- data/spec/lib/matchers.rb +84 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/unit/expression_node_spec.rb +102 -0
- data/spec/unit/leaf_node_spec.rb +84 -0
- data/spec/unit/select_query_spec.rb +52 -0
- data/spec/unit/tokenizer_spec.rb +86 -0
- data/sql_tree.gemspec +27 -0
- data/tasks/github-gem.rake +323 -0
- metadata +92 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
module SQLTree::Node
|
2
|
+
|
3
|
+
class SelectExpression < Base
|
4
|
+
|
5
|
+
attr_accessor :expression, :variable
|
6
|
+
|
7
|
+
def initialize(expression, variable = nil)
|
8
|
+
@expression = expression
|
9
|
+
@variable = variable
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_sql
|
13
|
+
sql = @expression.to_sql
|
14
|
+
sql << " AS " << quote_var(@variable) if @variable
|
15
|
+
return sql
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.parse(tokens)
|
19
|
+
if tokens.peek == SQLTree::Token::MULTIPLY
|
20
|
+
tokens.consume(SQLTree::Token::MULTIPLY)
|
21
|
+
return SQLTree::Node::ALL_FIELDS
|
22
|
+
else
|
23
|
+
expression = SQLTree::Node::Expression.parse(tokens)
|
24
|
+
expr = SQLTree::Node::SelectExpression.new(expression)
|
25
|
+
if tokens.peek == SQLTree::Token::AS
|
26
|
+
tokens.consume(SQLTree::Token::AS)
|
27
|
+
expr.variable = SQLTree::Node::Variable.parse(tokens).name
|
28
|
+
end
|
29
|
+
return expr
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(other)
|
34
|
+
other.expression == self.expression && other.variable == self.variable
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class AllFieldsExpression < Expression
|
39
|
+
def to_sql
|
40
|
+
'*'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
ALL_FIELDS = AllFieldsExpression.new
|
45
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module SQLTree::Node
|
2
|
+
|
3
|
+
class SelectQuery < Base
|
4
|
+
|
5
|
+
attr_accessor :distinct, :select, :from, :where, :group_by, :having, :order_by, :limit
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@distinct = false
|
9
|
+
@select = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_sql
|
13
|
+
raise "At least one SELECT expression is required" if self.select.empty?
|
14
|
+
sql = (self.distinct) ? "SELECT DISTINCT " : "SELECT "
|
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
|
18
|
+
return sql
|
19
|
+
end
|
20
|
+
|
21
|
+
# Uses the provided initialized parser to parse a SELECT query.
|
22
|
+
def self.parse(tokens)
|
23
|
+
select_node = self.new
|
24
|
+
tokens.consume(SQLTree::Token::SELECT)
|
25
|
+
|
26
|
+
if tokens.peek == SQLTree::Token::DISTINCT
|
27
|
+
tokens.consume(SQLTree::Token::DISTINCT)
|
28
|
+
select_node.distinct = true
|
29
|
+
end
|
30
|
+
|
31
|
+
select_node.select = self.parse_select_clause(tokens)
|
32
|
+
select_node.from = self.parse_from_clause(tokens) if tokens.peek == SQLTree::Token::FROM
|
33
|
+
select_node.where = self.parse_where_clause(tokens) if tokens.peek == SQLTree::Token::WHERE
|
34
|
+
|
35
|
+
return select_node
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.parse_select_clause(tokens)
|
39
|
+
expressions = [SQLTree::Node::SelectExpression.parse(tokens)]
|
40
|
+
while tokens.peek == SQLTree::Token::COMMA
|
41
|
+
tokens.consume(SQLTree::Token::COMMA)
|
42
|
+
expressions << SQLTree::Node::SelectExpression.parse(tokens)
|
43
|
+
end
|
44
|
+
return expressions
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.parse_from_clause(tokens)
|
48
|
+
tokens.consume(SQLTree::Token::FROM)
|
49
|
+
sources = [SQLTree::Node::Source.parse(tokens)]
|
50
|
+
while tokens.peek == SQLTree::Token::COMMA
|
51
|
+
tokens.consume(SQLTree::Token::COMMA)
|
52
|
+
sources << SQLTree::Node::Source.parse(tokens)
|
53
|
+
end
|
54
|
+
return sources
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.parse_where_clause(tokens)
|
58
|
+
tokens.consume(SQLTree::Token::WHERE)
|
59
|
+
Expression.parse(tokens)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.parse_group_clause(tokens)
|
63
|
+
# TODO: implement me
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.parse_having_clause(tokens)
|
67
|
+
# TODO: implement me
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.parse_order_clause(tokens)
|
71
|
+
# TODO: implement me
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.parse_limit_clause(tokens)
|
75
|
+
# TODO: implement me
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module SQLTree::Node
|
2
|
+
|
3
|
+
class Source < Base
|
4
|
+
|
5
|
+
attr_accessor :table_reference, :joins
|
6
|
+
|
7
|
+
def initialize(table_reference, joins = [])
|
8
|
+
@table_reference, @joins = table_reference, joins
|
9
|
+
end
|
10
|
+
|
11
|
+
def table
|
12
|
+
table_reference.table
|
13
|
+
end
|
14
|
+
|
15
|
+
def table_alias
|
16
|
+
table_reference.table_alias
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_sql
|
20
|
+
sql = table_reference.to_sql
|
21
|
+
sql << ' ' << joins.map { |j| j.to_sql }.join(' ') if joins.any?
|
22
|
+
return sql
|
23
|
+
end
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
other.table_reference = self.table_reference && other.joins == self.joins
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.parse(tokens)
|
30
|
+
source = self.new(SQLTree::Node::TableReference.parse(tokens))
|
31
|
+
while tokens.peek && tokens.peek.join?
|
32
|
+
source.joins << Join.parse(tokens)
|
33
|
+
end
|
34
|
+
return source
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module SQLTree::Node
|
2
|
+
|
3
|
+
class TableReference < Base
|
4
|
+
|
5
|
+
attr_accessor :table, :table_alias
|
6
|
+
|
7
|
+
def initialize(table, table_alias = nil)
|
8
|
+
@table, @table_alias = table, table_alias
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_sql
|
12
|
+
sql = quote_var(table)
|
13
|
+
sql << " AS " << quote_var(table_alias) if table_alias
|
14
|
+
return sql
|
15
|
+
end
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
other.table = self.table && other.table_alias == self.table_alias
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.parse(tokens)
|
22
|
+
if SQLTree::Token::Variable === tokens.next
|
23
|
+
table_reference = self.new(tokens.current.literal)
|
24
|
+
if tokens.peek == SQLTree::Token::AS || SQLTree::Token::Variable === tokens.peek
|
25
|
+
tokens.consume(SQLTree::Token::AS) if tokens.peek == SQLTree::Token::AS
|
26
|
+
table_reference.table_alias = SQLTree::Node::Variable.parse(tokens).name
|
27
|
+
end
|
28
|
+
return table_reference
|
29
|
+
else
|
30
|
+
raise SQLTree::Parser::UnexpectedToken.new(tokens.current)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module SQLTree::Node
|
2
|
+
|
3
|
+
class Value < Base
|
4
|
+
attr_accessor :value
|
5
|
+
|
6
|
+
def initialize(value)
|
7
|
+
@value = value
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_sql
|
11
|
+
case value
|
12
|
+
when nil then 'NULL'
|
13
|
+
when String then quote_str(@value)
|
14
|
+
else @value.to_s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_tree
|
19
|
+
@value
|
20
|
+
end
|
21
|
+
|
22
|
+
def ==(other)
|
23
|
+
other.kind_of?(self.class) && other.value == self.value
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.parse(tokens)
|
27
|
+
case tokens.next
|
28
|
+
when SQLTree::Token::String, SQLTree::Token::Number
|
29
|
+
SQLTree::Node::Value.new(tokens.current.literal)
|
30
|
+
when SQLTree::Token::NULL
|
31
|
+
SQLTree::Node::Value.new(nil)
|
32
|
+
else
|
33
|
+
raise SQLTree::Parser::UnexpectedToken.new(tokens.current, :literal)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SQLTree::Node
|
2
|
+
|
3
|
+
class Variable < Base
|
4
|
+
|
5
|
+
attr_accessor :name
|
6
|
+
|
7
|
+
def initialize(name)
|
8
|
+
@name = name
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_sql
|
12
|
+
quote_var(@name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_tree
|
16
|
+
@name.to_sym
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
other.name == self.name
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parse(tokens)
|
24
|
+
if SQLTree::Token::Variable === tokens.peek
|
25
|
+
if tokens.peek(2) == SQLTree::Token::DOT
|
26
|
+
SQLTree::Node::Field.parse(tokens)
|
27
|
+
else
|
28
|
+
self.new(tokens.next.literal)
|
29
|
+
end
|
30
|
+
else
|
31
|
+
raise SQLTree::Parser::UnexpectedToken.new(tokens.peek, :variable)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class SQLTree::Parser
|
2
|
+
|
3
|
+
class UnexpectedToken < StandardError
|
4
|
+
|
5
|
+
attr_reader :expected_token, :actual_token
|
6
|
+
|
7
|
+
def initialize(actual_token, expected_token = nil)
|
8
|
+
@expected_token, @actual_token = expected_token, actual_token
|
9
|
+
message = "Unexpected token: found #{actual_token.inspect}"
|
10
|
+
message << ", but expected #{expected_token.inspect}" if expected_token
|
11
|
+
message << '!'
|
12
|
+
super(message)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.parse(sql_string, options = {})
|
17
|
+
self.new(sql_string, options).parse!
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :options
|
21
|
+
|
22
|
+
def initialize(tokens, options)
|
23
|
+
if tokens.kind_of?(String)
|
24
|
+
@tokens = SQLTree::Tokenizer.new.tokenize(tokens)
|
25
|
+
else
|
26
|
+
@tokens = tokens
|
27
|
+
end
|
28
|
+
@options = options
|
29
|
+
end
|
30
|
+
|
31
|
+
def current
|
32
|
+
@current_token
|
33
|
+
end
|
34
|
+
|
35
|
+
def next
|
36
|
+
@current_token = @tokens.shift
|
37
|
+
end
|
38
|
+
|
39
|
+
def consume(*checks)
|
40
|
+
checks.each do |check|
|
41
|
+
raise UnexpectedToken.new(self.current, check) unless check == self.next
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def peek(distance = 1)
|
46
|
+
@tokens[distance - 1]
|
47
|
+
end
|
48
|
+
|
49
|
+
def peek_tokens(amount)
|
50
|
+
@tokens[0, amount]
|
51
|
+
end
|
52
|
+
|
53
|
+
def debug
|
54
|
+
puts @tokens.inspect
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse!
|
58
|
+
case self.peek
|
59
|
+
when SQLTree::Token::SELECT then SQLTree::Node::SelectQuery.parse(self)
|
60
|
+
else raise UnexpectedToken.new(self.peek)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# The <tt>SQLTree::Token</tt> class is the base class for every token
|
2
|
+
# in the SQL language. Actual tokens are represented by a subclass.
|
3
|
+
#
|
4
|
+
# Tokens are produced by the <tt>SQLTree::Tokenizer</tt> from a string
|
5
|
+
# and are consumed by the <tt>SQLTree::Parser</tt> to construct an
|
6
|
+
# abstract syntax tree for the query that is being parsed.
|
7
|
+
class SQLTree::Token
|
8
|
+
|
9
|
+
# For some tokens, the encountered literal value is important
|
10
|
+
# during the parsing phase (e.g. strings and variable names).
|
11
|
+
# Therefore, the literal value encountered that represented the
|
12
|
+
# token in the original SQL query string is stored.
|
13
|
+
attr_accessor :literal
|
14
|
+
|
15
|
+
# Creates a token instance with a given literal representation.
|
16
|
+
#
|
17
|
+
# <tt>literal<tt>:: The literal string value that was encountered
|
18
|
+
# while tokenizing.
|
19
|
+
def initialize(literal)
|
20
|
+
@literal = literal
|
21
|
+
end
|
22
|
+
|
23
|
+
# Compares two tokens. Tokens are considered equal when they are
|
24
|
+
# instances of the same class, i.e. do literal is not used.
|
25
|
+
def ==(other)
|
26
|
+
other.class == self.class
|
27
|
+
end
|
28
|
+
|
29
|
+
def inspect # :nodoc:
|
30
|
+
literal
|
31
|
+
end
|
32
|
+
|
33
|
+
def join?
|
34
|
+
[SQLTree::Token::JOIN, SQLTree::Token::LEFT, SQLTree::Token::RIGHT,
|
35
|
+
SQLTree::Token::INNER, SQLTree::Token::OUTER, SQLTree::Token::NATURAL,
|
36
|
+
SQLTree::Token::FULL].include?(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
###################################################################
|
40
|
+
# DYNAMIC TOKEN TYPES
|
41
|
+
###################################################################
|
42
|
+
|
43
|
+
# The <tt>SQLTree::Token::Value</tt> class is the base class for
|
44
|
+
# every dynamic token. A dynamic token is a token for which the
|
45
|
+
# literal value used remains impoirtant during parsing.
|
46
|
+
class Value < SQLTree::Token
|
47
|
+
|
48
|
+
def inspect # :nodoc:
|
49
|
+
"#<#{self.class.name.split('::').last}:#{literal.inspect}>"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Compares two tokens. For values, the literal encountered value
|
53
|
+
# of the token is also taken into account besides the class.
|
54
|
+
def ==(other)
|
55
|
+
other.class == self.class && @literal == other.literal
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# The <tt>SQLTree::Token::Variable</tt> class represents SQL
|
60
|
+
# variables. The variable name is stored in the literal as string,
|
61
|
+
# without quotes if they were present.
|
62
|
+
class Variable < SQLTree::Token::Value
|
63
|
+
end
|
64
|
+
|
65
|
+
# The <tt>SQLTree::Token::String</tt> class represents strings.
|
66
|
+
# The actual string is stored in the literal as string without quotes.
|
67
|
+
class String < SQLTree::Token::Value
|
68
|
+
end
|
69
|
+
|
70
|
+
# The <tt>SQLTree::Token::Keyword</tt> class represents numbers.
|
71
|
+
# The actual number is stored as an integer or float in the token's
|
72
|
+
# literal.
|
73
|
+
class Number < SQLTree::Token::Value
|
74
|
+
end
|
75
|
+
|
76
|
+
###################################################################
|
77
|
+
# STATIC TOKEN TYPES
|
78
|
+
###################################################################
|
79
|
+
|
80
|
+
# The <tt>SQLTree::Token::Keyword</tt> class represents reserved SQL
|
81
|
+
# keywords. These keywords are used to structure the query. Keywords
|
82
|
+
# are static, i.e. the literal value is not important during the
|
83
|
+
# parsing process.
|
84
|
+
class Keyword < SQLTree::Token
|
85
|
+
def inspect # :nodoc:
|
86
|
+
":#{literal.gsub(/ /, '_').downcase}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# The <tt>SQLTree::Token::Operator</tt> class represents logical and
|
91
|
+
# arithmetic operators in SQL. These tokens are static, i.e. the literal
|
92
|
+
# value is not important during the parsing process.
|
93
|
+
class Operator < SQLTree::Token
|
94
|
+
def inspect # :nodoc:
|
95
|
+
OPERATORS_HASH[literal].inspect
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
###################################################################
|
100
|
+
# STATIC TOKEN CONSTANTS
|
101
|
+
###################################################################
|
102
|
+
|
103
|
+
# Create some static token classes and a single instance of them
|
104
|
+
LPAREN = Class.new(SQLTree::Token).new('(')
|
105
|
+
RPAREN = Class.new(SQLTree::Token).new(')')
|
106
|
+
DOT = Class.new(SQLTree::Token).new('.')
|
107
|
+
COMMA = Class.new(SQLTree::Token).new(',')
|
108
|
+
|
109
|
+
# A list of all the SQL reserverd keywords.
|
110
|
+
KEYWORDS = %w{SELECT FROM WHERE GOUP HAVING ORDER DISTINCT LEFT RIGHT INNER FULL OUTER NATURAL JOIN USING
|
111
|
+
AND OR NOT AS ON IS NULL BY LIKE ILIKE BETWEEN IN}
|
112
|
+
|
113
|
+
# Create a token for all the reserved keywords in SQL
|
114
|
+
KEYWORDS.each { |kw| const_set(kw, Class.new(SQLTree::Token::Keyword).new(kw)) }
|
115
|
+
|
116
|
+
# A list of keywords that aways occur in fixed combinations. Register these as separate keywords.
|
117
|
+
KEYWORD_COMBINATIONS = [%w{IS NOT}, %w{NOT LIKE}, %w{NOT BETWEEN}, %w{NOT ILIKE}]
|
118
|
+
KEYWORD_COMBINATIONS.each { |kw| const_set(kw.join('_'), Class.new(SQLTree::Token::Keyword).new(kw.join(' '))) }
|
119
|
+
|
120
|
+
ARITHMETHIC_OPERATORS_HASH = { '+' => :plus, '-' => :minus, '*' => :multiply, '/' => :divide, '%' => :modulo }
|
121
|
+
COMPARISON_OPERATORS_HASH = { '=' => :eq, '!=' => :ne, '<>' => :ne, '>' => :gt, '<' => :lt, '>=' => :gte, '<=' => :lte }
|
122
|
+
|
123
|
+
# Register a token class and single instance for every token.
|
124
|
+
OPERATORS_HASH = ARITHMETHIC_OPERATORS_HASH.merge(COMPARISON_OPERATORS_HASH)
|
125
|
+
OPERATORS_HASH.each_pair do |literal, symbol|
|
126
|
+
self.const_set(symbol.to_s.upcase, Class.new(SQLTree::Token::Operator).new(literal)) unless self.const_defined?(symbol.to_s.upcase)
|
127
|
+
end
|
128
|
+
|
129
|
+
COMPARISON_OPERATORS = COMPARISON_OPERATORS_HASH.map { |(literal, symbol)| const_get(symbol.to_s.upcase) } +
|
130
|
+
[SQLTree::Token::IN, SQLTree::Token::IS, SQLTree::Token::BETWEEN, SQLTree::Token::LIKE, SQLTree::Token::ILIKE, SQLTree::Token::NOT]
|
131
|
+
end
|