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/.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
|