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
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Willem van Bergen
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
= SQLTree
|
2
|
+
|
3
|
+
SQLTree is a pure Ruby library to represent SQL queries with a syntax tree
|
4
|
+
for inspection and modification.
|
5
|
+
|
6
|
+
The library can parse an SQL query (a string) to represent the query using
|
7
|
+
a syntax tree, and it can generate an SQL query from a syntax tree. The syntax
|
8
|
+
tree ca be used to inspect to query, or to modify it.
|
9
|
+
|
10
|
+
This library is currently under heavy development. This means that not all
|
11
|
+
SQL constructs are supported yet. For now, only SQL <tt>SELECT</tt> queries
|
12
|
+
are supported.
|
13
|
+
|
14
|
+
== Installation
|
15
|
+
|
16
|
+
The SQLTree library is distributed as a gem on Gemcutter.org. To install:
|
17
|
+
|
18
|
+
gem install sql_tree --source http://gemcutter.org
|
19
|
+
|
20
|
+
== Additional information
|
21
|
+
|
22
|
+
* See the project wiki at http://wiki.github.com/wvanbergen/sql_tree for more
|
23
|
+
information about using this library.
|
24
|
+
* This plugin is written by Willem van Bergen and is MIT licensed (see the
|
25
|
+
LICENSE file).
|
data/Rakefile
ADDED
data/lib/sql_tree.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# The SQLTree module is the basic namespace for the sql_tree gem.
|
2
|
+
#
|
3
|
+
# It contains the shorthand parse method (i.e. <tt>SQLTree[sql_query]</tt>)
|
4
|
+
# and some helper methods that are used by the gem. It also requires the
|
5
|
+
# necessary files for the gem to function properly.
|
6
|
+
module SQLTree
|
7
|
+
|
8
|
+
# Loads constants in the SQLTree namespace using self.load_default_class_file(base, const)
|
9
|
+
# <tt>const</tt>:: The constant that is not yet loaded in the SQLTree namespace. This should be passed as a string or symbol.
|
10
|
+
def self.const_missing(const)
|
11
|
+
load_default_class_file(SQLTree, const)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Loads constants that reside in the SQLTree tree using the constant name
|
15
|
+
# and its base constant to determine the filename.
|
16
|
+
# <tt>base</tt>:: The base constant to load the constant from. This should be Foo when the constant Foo::Bar is being loaded.
|
17
|
+
# <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.
|
18
|
+
def self.load_default_class_file(base, const)
|
19
|
+
require "#{to_underscore("#{base.name}::#{const}")}"
|
20
|
+
base.const_get(const) if base.const_defined?(const)
|
21
|
+
end
|
22
|
+
|
23
|
+
# The <tt>[]</tt> method is a shorthand for the <tt>SQLTree::Parser.parse</tt>
|
24
|
+
# method to parse an SQL query and return a SQL syntax tree.
|
25
|
+
def self.[](query, options = {})
|
26
|
+
SQLTree::Parser.parse(query)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Convert a string/symbol in camelcase (RequestLogAnalyzer::Controller) to underscores (request_log_analyzer/controller)
|
30
|
+
# This function can be used to load the file (using require) in which the given constant is defined.
|
31
|
+
# <tt>str</tt>:: The string to convert in the following format: <tt>ModuleName::ClassName</tt>
|
32
|
+
def self.to_underscore(str)
|
33
|
+
str.to_s.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase
|
34
|
+
end
|
35
|
+
|
36
|
+
# Convert a string/symbol in underscores (<tt>request_log_analyzer/controller</tt>) to camelcase
|
37
|
+
# (<tt>RequestLogAnalyzer::Controller</tt>). This can be used to find the class that is defined in a given filename.
|
38
|
+
# <tt>str</tt>:: The string to convert in the following format: <tt>module_name/class_name</tt>
|
39
|
+
def self.to_camelcase(str)
|
40
|
+
str.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module SQLTree::Node
|
2
|
+
|
3
|
+
def self.const_missing(const)
|
4
|
+
SQLTree.load_default_class_file(SQLTree::Node, const)
|
5
|
+
end
|
6
|
+
|
7
|
+
class Base
|
8
|
+
|
9
|
+
# Pretty prints this instance for inspection
|
10
|
+
def inspect
|
11
|
+
"#{self.class.name}[#{self.to_sql}]"
|
12
|
+
end
|
13
|
+
|
14
|
+
# Quotes a variable name so that it can be safely used within
|
15
|
+
# SQL queries.
|
16
|
+
def quote_var(name)
|
17
|
+
"\"#{name}\""
|
18
|
+
end
|
19
|
+
|
20
|
+
# Quotes a string so that it can be used within an SQL query.
|
21
|
+
def quote_str(str)
|
22
|
+
"'#{str.gsub(/\'/, "''")}'"
|
23
|
+
end
|
24
|
+
|
25
|
+
# This method should be implemented by a subclass.
|
26
|
+
def self.parse(tokens)
|
27
|
+
raise 'Only implemented in subclasses!'
|
28
|
+
end
|
29
|
+
|
30
|
+
# Parses a string, expecting it to be parsable to an instance of
|
31
|
+
# the current class.
|
32
|
+
def self.[](sql, options = {})
|
33
|
+
parser = SQLTree::Parser.new(sql, options)
|
34
|
+
self.parse(parser)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
module SQLTree::Node
|
2
|
+
|
3
|
+
# Base class for all SQL expressions.
|
4
|
+
#
|
5
|
+
# This is an asbtract class and should not be used directly. Use
|
6
|
+
# one of the subclasses instead.
|
7
|
+
class Expression < Base
|
8
|
+
|
9
|
+
def self.parse(tokens)
|
10
|
+
SQLTree::Node::LogicalExpression.parse(tokens)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Parses a single, atomic SQL expression. This can be either:
|
14
|
+
# * a full expression (or set of expressions) within parentheses.
|
15
|
+
# * a logical NOT expression
|
16
|
+
# * an SQL variable
|
17
|
+
# * an SQL function
|
18
|
+
# * a literal SQL value (numeric or string)
|
19
|
+
def self.parse_atomic(tokens)
|
20
|
+
case tokens.peek
|
21
|
+
when SQLTree::Token::LPAREN
|
22
|
+
tokens.consume(SQLTree::Token::LPAREN)
|
23
|
+
expr = SQLTree::Node::Expression.parse(tokens)
|
24
|
+
tokens.consume(SQLTree::Token::RPAREN)
|
25
|
+
expr
|
26
|
+
when SQLTree::Token::NOT
|
27
|
+
SQLTree::Node::LogicalNotExpression.parse(tokens)
|
28
|
+
when SQLTree::Token::Variable
|
29
|
+
if tokens.peek(2) == SQLTree::Token::LPAREN
|
30
|
+
SQLTree::Node::FunctionExpression.parse(tokens)
|
31
|
+
else
|
32
|
+
SQLTree::Node::Variable.parse(tokens)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
SQLTree::Node::Value.parse(tokens)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class LogicalNotExpression < Expression
|
41
|
+
|
42
|
+
attr_accessor :expression
|
43
|
+
|
44
|
+
def initialize(expression)
|
45
|
+
@expression = expression
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_sql
|
49
|
+
"NOT(#{@expression.to_sql})"
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_tree
|
53
|
+
[:not, expression.to_tree]
|
54
|
+
end
|
55
|
+
|
56
|
+
def ==(other)
|
57
|
+
other.kind_of?(self.class) && other.expression == self.expression
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.parse(tokens)
|
61
|
+
tokens.consume(SQLTree::Token::NOT)
|
62
|
+
self.new(SQLTree::Node::Expression.parse(tokens))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class LogicalExpression < Expression
|
67
|
+
attr_accessor :operator, :expressions
|
68
|
+
|
69
|
+
def initialize(operator, expressions)
|
70
|
+
@expressions = expressions
|
71
|
+
@operator = operator.to_s.downcase.to_sym
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_sql
|
75
|
+
"(" + @expressions.map { |e| e.to_sql }.join(" #{@operator.to_s.upcase} ") + ")"
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_tree
|
79
|
+
[@operator] + @expressions.map { |e| e.to_tree }
|
80
|
+
end
|
81
|
+
|
82
|
+
def ==(other)
|
83
|
+
self.operator == other.operator && self.expressions == other.expressions
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.parse(tokens)
|
87
|
+
expr = ComparisonExpression.parse(tokens)
|
88
|
+
while [SQLTree::Token::AND, SQLTree::Token::OR].include?(tokens.peek)
|
89
|
+
expr = SQLTree::Node::LogicalExpression.new(tokens.next.literal, [expr, ComparisonExpression.parse(tokens)])
|
90
|
+
end
|
91
|
+
return expr
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class ComparisonExpression < Expression
|
96
|
+
attr_accessor :lhs, :rhs, :operator
|
97
|
+
|
98
|
+
def initialize(operator, lhs, rhs)
|
99
|
+
@lhs = lhs
|
100
|
+
@rhs = rhs
|
101
|
+
@operator = operator
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_sql
|
105
|
+
"(#{@lhs.to_sql} #{@operator} #{@rhs.to_sql})"
|
106
|
+
end
|
107
|
+
|
108
|
+
def to_tree
|
109
|
+
[SQLTree::Token::OPERATORS_HASH[@operator], @lhs.to_tree, @rhs.to_tree]
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.parse_comparison_operator(tokens)
|
113
|
+
operator_token = tokens.next
|
114
|
+
if SQLTree::Token::IS === operator_token
|
115
|
+
if SQLTree::Token::NOT === tokens.peek
|
116
|
+
tokens.consume(SQLTree::Token::NOT)
|
117
|
+
'IS NOT'
|
118
|
+
else
|
119
|
+
'IS'
|
120
|
+
end
|
121
|
+
elsif SQLTree::Token::NOT === operator_token
|
122
|
+
case tokens.peek
|
123
|
+
when SQLTree::Token::LIKE, SQLTree::Token::ILIKE, SQLTree::Token::BETWEEN, SQLTree::Token::IN
|
124
|
+
"NOT #{tokens.next.literal.upcase}"
|
125
|
+
else
|
126
|
+
raise SQLTree::Parser::UnexpectedToken.new(tokens.peek)
|
127
|
+
end
|
128
|
+
else
|
129
|
+
operator_token.literal
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.parse(tokens)
|
134
|
+
lhs = SQLTree::Node::ArithmeticExpression.parse(tokens)
|
135
|
+
while SQLTree::Token::COMPARISON_OPERATORS.include?(tokens.peek)
|
136
|
+
comparison_operator = parse_comparison_operator(tokens)
|
137
|
+
rhs = ['IN', 'NOT IN'].include?(comparison_operator) ?
|
138
|
+
SQLTree::Node::SetExpression.parse(tokens) :
|
139
|
+
SQLTree::Node::ArithmeticExpression.parse(tokens)
|
140
|
+
|
141
|
+
lhs = self.new(comparison_operator, lhs, rhs)
|
142
|
+
end
|
143
|
+
return lhs
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class SetExpression < Expression
|
148
|
+
attr_accessor :items
|
149
|
+
|
150
|
+
def initialize(items = [])
|
151
|
+
@items = items
|
152
|
+
end
|
153
|
+
|
154
|
+
def to_tree
|
155
|
+
items.map { |i| i.to_tree }
|
156
|
+
end
|
157
|
+
|
158
|
+
def to_sql
|
159
|
+
"(#{items.map {|i| i.to_sql}.join(', ')})"
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.parse(tokens)
|
163
|
+
tokens.consume(SQLTree::Token::LPAREN)
|
164
|
+
items = [SQLTree::Node::Expression.parse(tokens)]
|
165
|
+
while tokens.peek == SQLTree::Token::COMMA
|
166
|
+
tokens.consume(SQLTree::Token::COMMA)
|
167
|
+
items << SQLTree::Node::Expression.parse(tokens)
|
168
|
+
end
|
169
|
+
tokens.consume(SQLTree::Token::RPAREN)
|
170
|
+
|
171
|
+
self.new(items)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
class FunctionExpression < Expression
|
176
|
+
attr_accessor :function, :arguments
|
177
|
+
|
178
|
+
def initialize(function, arguments = [])
|
179
|
+
@function = function
|
180
|
+
@arguments = arguments
|
181
|
+
end
|
182
|
+
|
183
|
+
def to_sql
|
184
|
+
"#{@function}(" + @arguments.map { |e| e.to_sql }.join(', ') + ")"
|
185
|
+
end
|
186
|
+
|
187
|
+
def to_tree
|
188
|
+
[@function.to_sym] + @arguments.map { |e| e.to_tree }
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.parse(tokens)
|
192
|
+
expr = self.new(tokens.next.literal)
|
193
|
+
tokens.consume(SQLTree::Token::LPAREN)
|
194
|
+
until tokens.peek == SQLTree::Token::RPAREN
|
195
|
+
expr.arguments << SQLTree::Node::Expression.parse(tokens)
|
196
|
+
tokens.consume(SQLTree::Token::COMMA) if tokens.peek == SQLTree::Token::COMMA
|
197
|
+
end
|
198
|
+
tokens.consume(SQLTree::Token::RPAREN)
|
199
|
+
return expr
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
class ArithmeticExpression < Expression
|
204
|
+
attr_accessor :lhs, :rhs, :operator
|
205
|
+
|
206
|
+
def initialize(operator, lhs, rhs)
|
207
|
+
@lhs = lhs
|
208
|
+
@rhs = rhs
|
209
|
+
@operator = operator
|
210
|
+
end
|
211
|
+
|
212
|
+
def to_sql
|
213
|
+
"(#{@lhs.to_sql} #{@operator} #{@rhs.to_sql})"
|
214
|
+
end
|
215
|
+
|
216
|
+
def to_tree
|
217
|
+
[SQLTree::Token::OPERATORS_HASH[@operator], @lhs.to_tree, @rhs.to_tree]
|
218
|
+
end
|
219
|
+
|
220
|
+
def self.parse(tokens)
|
221
|
+
self.parse_primary(tokens)
|
222
|
+
end
|
223
|
+
|
224
|
+
def self.parse_primary(tokens)
|
225
|
+
expr = self.parse_secondary(tokens)
|
226
|
+
while [SQLTree::Token::PLUS, SQLTree::Token::MINUS].include?(tokens.peek)
|
227
|
+
expr = self.new(tokens.next.literal, expr, self.parse_secondary(tokens))
|
228
|
+
end
|
229
|
+
return expr
|
230
|
+
end
|
231
|
+
|
232
|
+
def self.parse_secondary(tokens)
|
233
|
+
expr = Expression.parse_atomic(tokens)
|
234
|
+
while [SQLTree::Token::PLUS, SQLTree::Token::MINUS].include?(tokens.peek)
|
235
|
+
expr = self.new(tokens.next.literal, expr, SQLTree::Node::Expression.parse_atomic(tokens))
|
236
|
+
end
|
237
|
+
return expr
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module SQLTree::Node
|
2
|
+
|
3
|
+
class Field < Base
|
4
|
+
|
5
|
+
attr_accessor :name, :table
|
6
|
+
|
7
|
+
def initialize(name, table = nil)
|
8
|
+
@name = name
|
9
|
+
@table = table
|
10
|
+
end
|
11
|
+
|
12
|
+
def quote_var(name)
|
13
|
+
return '*' if name == :all
|
14
|
+
super(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_sql
|
18
|
+
@table.nil? ? quote_var(@name) : quote_var(@table) + '.' + quote_var(@name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_tree
|
22
|
+
to_sql.to_sym
|
23
|
+
end
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
other.name == self.name && other.table == self.table
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.parse(tokens)
|
30
|
+
field_or_table = case tokens.next
|
31
|
+
when SQLTree::Token::MULTIPLY then :all
|
32
|
+
when SQLTree::Token::Variable then tokens.current.literal
|
33
|
+
else raise SQLTree::Parser::UnexpectedToken.new(tokens.current)
|
34
|
+
end
|
35
|
+
|
36
|
+
if tokens.peek == SQLTree::Token::DOT
|
37
|
+
table = field_or_table
|
38
|
+
tokens.consume(SQLTree::Token::DOT)
|
39
|
+
field = case tokens.next
|
40
|
+
when SQLTree::Token::MULTIPLY then :all
|
41
|
+
when SQLTree::Token::Variable then tokens.current.literal
|
42
|
+
else raise SQLTree::Parser::UnexpectedToken.new(tokens.current)
|
43
|
+
end
|
44
|
+
self.new(field, table)
|
45
|
+
else
|
46
|
+
self.new(field_or_table)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module SQLTree::Node
|
2
|
+
|
3
|
+
class Join < Base
|
4
|
+
|
5
|
+
attr_accessor :join_type, :table_reference, :join_expression
|
6
|
+
|
7
|
+
def initialize(values = {})
|
8
|
+
values.each { |key, value| self.send(:"#{key}=", value) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_sql
|
12
|
+
join_sql = join_type ? "#{join_type.to_s.upcase} " : ""
|
13
|
+
join_sql << "JOIN #{table_reference.to_sql} "
|
14
|
+
join_sql << "ON #{join_expression.to_sql}"
|
15
|
+
join_sql
|
16
|
+
end
|
17
|
+
|
18
|
+
def table
|
19
|
+
table_reference.table
|
20
|
+
end
|
21
|
+
|
22
|
+
def table_alias
|
23
|
+
table_reference.table_alias
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.parse(tokens)
|
27
|
+
join = self.new
|
28
|
+
|
29
|
+
if tokens.peek == SQLTree::Token::FULL
|
30
|
+
join.join_type = :outer
|
31
|
+
tokens.consume(SQLTree::Token::FULL, SQLTree::Token::OUTER)
|
32
|
+
elsif [SQLTree::Token::OUTER, SQLTree::Token::INNER, SQLTree::Token::LEFT, SQLTree::Token::RIGHT].include?(tokens.peek)
|
33
|
+
join.join_type = tokens.next.literal.downcase.to_sym
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
tokens.consume(SQLTree::Token::JOIN)
|
38
|
+
join.table_reference = SQLTree::Node::TableReference.parse(tokens)
|
39
|
+
tokens.consume(SQLTree::Token::ON)
|
40
|
+
join.join_expression = SQLTree::Node::Expression.parse(tokens)
|
41
|
+
|
42
|
+
return join
|
43
|
+
end
|
44
|
+
|
45
|
+
def ==(other)
|
46
|
+
other.table = self.table && other.table_alias == self.table_alias &&
|
47
|
+
other.join_type == self.join_type && other.join_expression == self.join_expression
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|