sql_tree 0.1.1 → 0.2.0
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 +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
|