sql_tree 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.infinity_test +8 -0
- data/Gemfile +2 -0
- data/LICENSE +1 -1
- data/README.rdoc +5 -3
- data/Rakefile +1 -3
- data/lib/sql_tree.rb +7 -29
- data/lib/sql_tree/node.rb +19 -7
- data/lib/sql_tree/node/begin_statement.rb +12 -0
- data/lib/sql_tree/node/commit_statement.rb +12 -0
- data/lib/sql_tree/node/expression.rb +42 -9
- data/lib/sql_tree/node/join.rb +8 -0
- data/lib/sql_tree/node/rollback_statement.rb +12 -0
- data/lib/sql_tree/node/select_declaration.rb +68 -2
- data/lib/sql_tree/node/set_query.rb +38 -0
- data/lib/sql_tree/parser.rb +8 -4
- data/lib/sql_tree/token.rb +3 -1
- data/lib/sql_tree/tokenizer.rb +12 -0
- data/spec/{lib → helpers}/matchers.rb +4 -4
- data/spec/integration/api_spec.rb +1 -1
- data/spec/integration/parse_and_generate_spec.rb +28 -1
- data/spec/spec_helper.rb +5 -21
- data/spec/unit/control_statements_spec.rb +17 -0
- data/spec/unit/delete_query_spec.rb +1 -1
- data/spec/unit/expression_node_spec.rb +39 -1
- data/spec/unit/insert_query_spec.rb +1 -1
- data/spec/unit/leaf_node_spec.rb +1 -1
- data/spec/unit/select_query_spec.rb +5 -1
- data/spec/unit/set_query_spec.rb +11 -0
- data/spec/unit/tokenizer_spec.rb +17 -1
- data/spec/unit/update_query_spec.rb +3 -3
- data/sql_tree.gemspec +7 -4
- data/tasks/{github-gem.rake → github-gem.rb} +104 -62
- metadata +82 -38
data/.gitignore
CHANGED
data/.infinity_test
ADDED
data/Gemfile
ADDED
data/LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -12,9 +12,11 @@ yet stable and not all SQL constructs are implemented yet.
|
|
12
12
|
|
13
13
|
== Installation
|
14
14
|
|
15
|
-
The SQLTree library is distributed as a gem
|
15
|
+
The SQLTree library is distributed as a gem. To install:
|
16
16
|
|
17
|
-
gem install sql_tree
|
17
|
+
gem install sql_tree
|
18
|
+
|
19
|
+
Or, add <tt>gem 'sql_tree'</tt> to your project's Gemfile and run <tt>bundle install</tt>.
|
18
20
|
|
19
21
|
== Usage
|
20
22
|
|
@@ -33,5 +35,5 @@ This library is written by Willem van Bergen and is MIT licensed (see the
|
|
33
35
|
LICENSE file).
|
34
36
|
|
35
37
|
* Full RDoc API documentation can be found at http://rdoc.info/projects/wvanbergen/sql_tree
|
36
|
-
* See the project wiki at http://
|
38
|
+
* See the project wiki at http://github.com/wvanbergen/sql_tree/wiki for more
|
37
39
|
information about using this library.
|
data/Rakefile
CHANGED
data/lib/sql_tree.rb
CHANGED
@@ -5,6 +5,8 @@
|
|
5
5
|
# necessary files for the gem to function properly.
|
6
6
|
module SQLTree
|
7
7
|
|
8
|
+
VERSION = "0.2.0"
|
9
|
+
|
8
10
|
class << self
|
9
11
|
# The character to quote variable names with.
|
10
12
|
attr_accessor :identifier_quote_char
|
@@ -13,38 +15,14 @@ module SQLTree
|
|
13
15
|
# Set default quote characters
|
14
16
|
self.identifier_quote_char = '"'
|
15
17
|
|
16
|
-
# Loads constants in the SQLTree namespace using self.load_default_class_file(base, const)
|
17
|
-
# <tt>const</tt>:: The constant that is not yet loaded in the SQLTree namespace. This should be passed as a string or symbol.
|
18
|
-
def self.const_missing(const)
|
19
|
-
load_default_class_file(SQLTree, const)
|
20
|
-
end
|
21
|
-
|
22
|
-
# Loads constants that reside in the SQLTree tree using the constant name
|
23
|
-
# and its base constant to determine the filename.
|
24
|
-
# <tt>base</tt>:: The base constant to load the constant from. This should be Foo when the constant Foo::Bar is being loaded.
|
25
|
-
# <tt>const</tt>:: The constant to load from the base constant as a string or symbol. This should be 'Bar' or :Bar when the constant Foo::Bar is being loaded.
|
26
|
-
def self.load_default_class_file(base, const)
|
27
|
-
require "#{to_underscore("#{base.name}::#{const}")}"
|
28
|
-
base.const_get(const) if base.const_defined?(const)
|
29
|
-
end
|
30
|
-
|
31
18
|
# The <tt>[]</tt> method is a shorthand for the <tt>SQLTree::Parser.parse</tt>
|
32
19
|
# method to parse an SQL query and return a SQL syntax tree.
|
33
20
|
def self.[](query, options = {})
|
34
21
|
SQLTree::Parser.parse(query)
|
35
22
|
end
|
36
|
-
|
37
|
-
# Convert a string/symbol in camelcase (SQLTree::Parser) to underscores (sql_tree/parser)
|
38
|
-
# This function can be used to load the file (using require) in which the given constant is defined.
|
39
|
-
# <tt>str</tt>:: The string to convert in the following format: <tt>ModuleName::ClassName</tt>
|
40
|
-
def self.to_underscore(str)
|
41
|
-
str.to_s.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase
|
42
|
-
end
|
43
|
-
|
44
|
-
# Convert a string/symbol in underscores (<tt>sql_tree/parser</tt>) to camelcase
|
45
|
-
# (<tt>SqlTree::Parser</tt>). This can be used to find the class that is defined in a given filename.
|
46
|
-
# <tt>str</tt>:: The string to convert in the following format: <tt>module_name/class_name</tt>
|
47
|
-
def self.to_camelcase(str)
|
48
|
-
str.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
49
|
-
end
|
50
23
|
end
|
24
|
+
|
25
|
+
require 'sql_tree/token'
|
26
|
+
require 'sql_tree/tokenizer'
|
27
|
+
require 'sql_tree/node'
|
28
|
+
require 'sql_tree/parser'
|
data/lib/sql_tree/node.rb
CHANGED
@@ -1,11 +1,5 @@
|
|
1
1
|
module SQLTree::Node
|
2
2
|
|
3
|
-
# Auto-loades files for Node subclasses that reside in the
|
4
|
-
# node directory, based on the classname.
|
5
|
-
def self.const_missing(const)
|
6
|
-
SQLTree.load_default_class_file(SQLTree::Node, const)
|
7
|
-
end
|
8
|
-
|
9
3
|
# The SQLTree::Node::Base class is the superclass for all node
|
10
4
|
# types that are used to represent SQL queries.
|
11
5
|
#
|
@@ -105,4 +99,22 @@ module SQLTree::Node
|
|
105
99
|
self.parse(parser)
|
106
100
|
end
|
107
101
|
end
|
108
|
-
end
|
102
|
+
end
|
103
|
+
|
104
|
+
require 'sql_tree/node/expression'
|
105
|
+
|
106
|
+
require 'sql_tree/node/select_query'
|
107
|
+
require 'sql_tree/node/select_declaration'
|
108
|
+
require 'sql_tree/node/table_reference'
|
109
|
+
require 'sql_tree/node/source'
|
110
|
+
require 'sql_tree/node/join'
|
111
|
+
require 'sql_tree/node/ordering'
|
112
|
+
|
113
|
+
require 'sql_tree/node/insert_query'
|
114
|
+
require 'sql_tree/node/update_query'
|
115
|
+
require 'sql_tree/node/delete_query'
|
116
|
+
require 'sql_tree/node/set_query'
|
117
|
+
|
118
|
+
require 'sql_tree/node/begin_statement'
|
119
|
+
require 'sql_tree/node/commit_statement'
|
120
|
+
require 'sql_tree/node/rollback_statement'
|
@@ -49,6 +49,11 @@ module SQLTree::Node
|
|
49
49
|
else
|
50
50
|
Variable.parse(tokens)
|
51
51
|
end
|
52
|
+
elsif SQLTree::Token::STRING_ESCAPE == tokens.peek
|
53
|
+
tokens.consume(SQLTree::Token::STRING_ESCAPE)
|
54
|
+
Value.parse(tokens)
|
55
|
+
elsif SQLTree::Token::INTERVAL === tokens.peek
|
56
|
+
IntervalValue.parse(tokens)
|
52
57
|
else
|
53
58
|
Value.parse(tokens)
|
54
59
|
end
|
@@ -333,7 +338,35 @@ module SQLTree::Node
|
|
333
338
|
return function_call
|
334
339
|
end
|
335
340
|
end
|
336
|
-
|
341
|
+
|
342
|
+
|
343
|
+
# Represents a postgresql INTERVAL value. Example: interval '2 days'.
|
344
|
+
#
|
345
|
+
# The value is the literal text of the interval (e.g. "2 days").
|
346
|
+
class IntervalValue < SQLTree::Node::Expression
|
347
|
+
# The actual value this node represents.
|
348
|
+
leaf :value
|
349
|
+
|
350
|
+
def initialize(value) # :nodoc:
|
351
|
+
@value = value
|
352
|
+
end
|
353
|
+
|
354
|
+
# Generates an SQL representation for this value.
|
355
|
+
def to_sql(options = {})
|
356
|
+
"interval " + quote_str(@value)
|
357
|
+
end
|
358
|
+
|
359
|
+
def self.parse(tokens)
|
360
|
+
tokens.consume(SQLTree::Token::INTERVAL)
|
361
|
+
if SQLTree::Token::String === tokens.peek
|
362
|
+
self.new(tokens.next.literal)
|
363
|
+
else
|
364
|
+
raise SQLTree::Parser::UnexpectedToken.new(tokens.current, :literal)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
|
337
370
|
# Represents alitreal value in an SQL expression. This node is a leaf node
|
338
371
|
# and thus has no child nodes.
|
339
372
|
#
|
@@ -345,14 +378,14 @@ module SQLTree::Node
|
|
345
378
|
# * an integer or decimal value, which is represented by an appropriate
|
346
379
|
# <tt>Numeric</tt> instance.
|
347
380
|
class Value < SQLTree::Node::Expression
|
348
|
-
|
381
|
+
|
349
382
|
# The actual value this node represents.
|
350
383
|
leaf :value
|
351
384
|
|
352
385
|
def initialize(value) # :nodoc:
|
353
386
|
@value = value
|
354
387
|
end
|
355
|
-
|
388
|
+
|
356
389
|
# Generates an SQL representation for this value.
|
357
390
|
#
|
358
391
|
# This method supports nil, string, numeric, date and time values.
|
@@ -361,11 +394,11 @@ module SQLTree::Node
|
|
361
394
|
# within an SQL query
|
362
395
|
def to_sql(options = {})
|
363
396
|
case value
|
364
|
-
when nil
|
365
|
-
when String
|
366
|
-
when Numeric
|
367
|
-
when Date
|
368
|
-
when DateTime, Time
|
397
|
+
when nil; 'NULL'
|
398
|
+
when String; quote_str(@value)
|
399
|
+
when Numeric; @value.to_s
|
400
|
+
when Date; @value.strftime("'%Y-%m-%d'")
|
401
|
+
when DateTime, Time; @value.strftime("'%Y-%m-%d %H:%M:%S'")
|
369
402
|
else raise "Don't know how te represent this value in SQL!"
|
370
403
|
end
|
371
404
|
end
|
@@ -389,7 +422,7 @@ module SQLTree::Node
|
|
389
422
|
end
|
390
423
|
end
|
391
424
|
end
|
392
|
-
|
425
|
+
|
393
426
|
# Represents a variable within an SQL expression. This is a leaf node, so it
|
394
427
|
# does not have any child nodes. A variale can point to a field of a table or
|
395
428
|
# to another expression that was declared elsewhere.
|
data/lib/sql_tree/node/join.rb
CHANGED
@@ -3,15 +3,18 @@ module SQLTree::Node
|
|
3
3
|
class Join < Base
|
4
4
|
|
5
5
|
leaf :join_type
|
6
|
+
leaf :is_outer
|
6
7
|
child :table_reference
|
7
8
|
child :join_expression
|
8
9
|
|
9
10
|
def initialize(values = {})
|
11
|
+
self.is_outer = false
|
10
12
|
values.each { |key, value| self.send(:"#{key}=", value) }
|
11
13
|
end
|
12
14
|
|
13
15
|
def to_sql(options = {})
|
14
16
|
join_sql = join_type ? "#{join_type.to_s.upcase} " : ""
|
17
|
+
join_sql << "OUTER " if is_outer
|
15
18
|
join_sql << "JOIN #{table_reference.to_sql(options)} "
|
16
19
|
join_sql << "ON #{join_expression.to_sql(options)}"
|
17
20
|
join_sql
|
@@ -35,6 +38,11 @@ module SQLTree::Node
|
|
35
38
|
join.join_type = tokens.next.literal.downcase.to_sym
|
36
39
|
end
|
37
40
|
|
41
|
+
if [:right, :left].include?(join.join_type) && tokens.peek.class == SQLTree::Token::OUTER
|
42
|
+
join.is_outer = true
|
43
|
+
tokens.consume(SQLTree::Token::OUTER)
|
44
|
+
end
|
45
|
+
|
38
46
|
tokens.consume(SQLTree::Token::JOIN)
|
39
47
|
join.table_reference = SQLTree::Node::TableReference.parse(tokens)
|
40
48
|
tokens.consume(SQLTree::Token::ON)
|
@@ -1,5 +1,17 @@
|
|
1
1
|
module SQLTree::Node
|
2
2
|
|
3
|
+
class SelectExpression < Base
|
4
|
+
|
5
|
+
def self.parse(tokens)
|
6
|
+
case tokens.peek
|
7
|
+
when SQLTree::Token::COUNT
|
8
|
+
SQLTree::Node::CountAggregrate.parse(tokens)
|
9
|
+
else
|
10
|
+
SQLTree::Node::Expression.parse(tokens)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
3
15
|
class SelectDeclaration < Base
|
4
16
|
|
5
17
|
child :expression
|
@@ -30,8 +42,7 @@ module SQLTree::Node
|
|
30
42
|
|
31
43
|
else
|
32
44
|
|
33
|
-
|
34
|
-
expr = self.new(:expression => expression)
|
45
|
+
expr = self.new(:expression => SQLTree::Node::SelectExpression.parse(tokens))
|
35
46
|
if SQLTree::Token::AS === tokens.peek
|
36
47
|
tokens.consume(SQLTree::Token::AS)
|
37
48
|
if SQLTree::Token::Identifier === tokens.peek
|
@@ -44,6 +55,61 @@ module SQLTree::Node
|
|
44
55
|
end
|
45
56
|
end
|
46
57
|
end
|
58
|
+
|
59
|
+
class CountAggregrate < Base
|
60
|
+
leaf :distinct
|
61
|
+
child :expression
|
62
|
+
|
63
|
+
def to_sql(options = {})
|
64
|
+
sql = "COUNT("
|
65
|
+
sql << "DISTINCT " if distinct
|
66
|
+
sql << expression.to_sql(options)
|
67
|
+
sql << ')'
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.parse(tokens)
|
71
|
+
count_aggregate = self.new(:distinct => false)
|
72
|
+
|
73
|
+
tokens.consume(SQLTree::Token::COUNT)
|
74
|
+
tokens.consume(SQLTree::Token::LPAREN)
|
75
|
+
|
76
|
+
# Handle DISTINCT
|
77
|
+
distinct_parens = false
|
78
|
+
if SQLTree::Token::DISTINCT === tokens.peek
|
79
|
+
tokens.consume(SQLTree::Token::DISTINCT)
|
80
|
+
count_aggregate.distinct = true
|
81
|
+
if SQLTree::Token::LPAREN === tokens.peek
|
82
|
+
tokens.consume(SQLTree::Token::LPAREN)
|
83
|
+
distinct_parens = true
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
if SQLTree::Token::MULTIPLY === tokens.peek
|
88
|
+
|
89
|
+
# "COUNT(*)"
|
90
|
+
tokens.consume(SQLTree::Token::MULTIPLY)
|
91
|
+
count_aggregate.expression = SQLTree::Node::ALL_FIELDS
|
92
|
+
|
93
|
+
elsif SQLTree::Token::Identifier === tokens.peek(1) &&
|
94
|
+
SQLTree::Token::DOT === tokens.peek(2) &&
|
95
|
+
SQLTree::Token::MULTIPLY === tokens.peek(3)
|
96
|
+
|
97
|
+
# "COUNT(table.*)"
|
98
|
+
table = tokens.next.literal
|
99
|
+
tokens.consume(SQLTree::Token::DOT, SQLTree::Token::MULTIPLY)
|
100
|
+
count_aggregate.expression = SQLTree::Node::AllFieldsDeclaration.new(table)
|
101
|
+
|
102
|
+
else
|
103
|
+
|
104
|
+
count_aggregate.expression = SQLTree::Node::Expression.parse(tokens)
|
105
|
+
end
|
106
|
+
|
107
|
+
tokens.consume(SQLTree::Token::RPAREN) if distinct_parens
|
108
|
+
tokens.consume(SQLTree::Token::RPAREN)
|
109
|
+
|
110
|
+
return count_aggregate
|
111
|
+
end
|
112
|
+
end
|
47
113
|
|
48
114
|
class AllFieldsDeclaration < Base
|
49
115
|
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module SQLTree::Node
|
2
|
+
|
3
|
+
# The <tt>DeleteQuery</tt> node represents an SQL DELETE query.
|
4
|
+
#
|
5
|
+
# This node has two children: <tt>table</tt> and <tt>where</tt>.
|
6
|
+
class SetQuery < Base
|
7
|
+
|
8
|
+
# The variable (<tt>SQLTree::Node::Field</tt>) that is being set.
|
9
|
+
child :variable
|
10
|
+
|
11
|
+
# The <tt>SQLTree::Node::Expression</tt> value that the variable is being
|
12
|
+
# set to.
|
13
|
+
child :value
|
14
|
+
|
15
|
+
# Initializes a new DeleteQuery instance.
|
16
|
+
def initialize(variable, value)
|
17
|
+
@variable, @value = variable, value
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generates an SQL DELETE query from this node.
|
21
|
+
def to_sql(options = {})
|
22
|
+
sql = "SET #{variable.to_sql(options)}"
|
23
|
+
sql << " TO #{value.to_sql(options)}"
|
24
|
+
sql
|
25
|
+
end
|
26
|
+
|
27
|
+
# Parses a SET query from a stream of tokens.
|
28
|
+
# <tt>tokens</tt>:: The token stream to parse from, which is an instance
|
29
|
+
# of <tt> SQLTree::Parser</tt>.
|
30
|
+
def self.parse(tokens)
|
31
|
+
tokens.consume(SQLTree::Token::SET)
|
32
|
+
variable = SQLTree::Node::Expression::Field.parse(tokens)
|
33
|
+
tokens.consume(SQLTree::Token::TO)
|
34
|
+
value = SQLTree::Node::Expression::Value.parse(tokens)
|
35
|
+
return self.new(variable, value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/sql_tree/parser.rb
CHANGED
@@ -112,10 +112,14 @@ class SQLTree::Parser
|
|
112
112
|
#
|
113
113
|
def parse!
|
114
114
|
case self.peek
|
115
|
-
when SQLTree::Token::SELECT
|
116
|
-
when SQLTree::Token::INSERT
|
117
|
-
when SQLTree::Token::DELETE
|
118
|
-
when SQLTree::Token::UPDATE
|
115
|
+
when SQLTree::Token::SELECT; SQLTree::Node::SelectQuery.parse(self)
|
116
|
+
when SQLTree::Token::INSERT; SQLTree::Node::InsertQuery.parse(self)
|
117
|
+
when SQLTree::Token::DELETE; SQLTree::Node::DeleteQuery.parse(self)
|
118
|
+
when SQLTree::Token::UPDATE; SQLTree::Node::UpdateQuery.parse(self)
|
119
|
+
when SQLTree::Token::BEGIN; SQLTree::Node::BeginStatement.parse(self)
|
120
|
+
when SQLTree::Token::COMMIT; SQLTree::Node::CommitStatement.parse(self)
|
121
|
+
when SQLTree::Token::ROLLBACK; SQLTree::Node::RollbackStatement.parse(self)
|
122
|
+
when SQLTree::Token::SET; SQLTree::Node::SetQuery.parse(self)
|
119
123
|
else raise UnexpectedToken.new(self.peek)
|
120
124
|
end
|
121
125
|
end
|