maksar-meta_where 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/CHANGELOG +90 -0
- data/Gemfile +8 -0
- data/LICENSE +20 -0
- data/README.rdoc +343 -0
- data/Rakefile +11 -0
- data/lib/core_ext/hash.rb +5 -0
- data/lib/core_ext/symbol.rb +39 -0
- data/lib/core_ext/symbol_operators.rb +48 -0
- data/lib/meta_where.rb +51 -0
- data/lib/meta_where/association_reflection.rb +51 -0
- data/lib/meta_where/column.rb +31 -0
- data/lib/meta_where/compound.rb +20 -0
- data/lib/meta_where/condition.rb +32 -0
- data/lib/meta_where/condition_operators.rb +19 -0
- data/lib/meta_where/function.rb +108 -0
- data/lib/meta_where/join_dependency.rb +105 -0
- data/lib/meta_where/join_type.rb +43 -0
- data/lib/meta_where/not.rb +13 -0
- data/lib/meta_where/relation.rb +290 -0
- data/lib/meta_where/utility.rb +51 -0
- data/lib/meta_where/version.rb +3 -0
- data/lib/meta_where/visitors/attribute.rb +58 -0
- data/lib/meta_where/visitors/predicate.rb +149 -0
- data/lib/meta_where/visitors/visitor.rb +52 -0
- data/meta_where.gemspec +48 -0
- data/test/fixtures/companies.yml +17 -0
- data/test/fixtures/company.rb +7 -0
- data/test/fixtures/data_type.rb +3 -0
- data/test/fixtures/data_types.yml +15 -0
- data/test/fixtures/developer.rb +5 -0
- data/test/fixtures/developers.yml +55 -0
- data/test/fixtures/developers_projects.yml +25 -0
- data/test/fixtures/fixed_bid_project.rb +2 -0
- data/test/fixtures/invalid_company.rb +4 -0
- data/test/fixtures/invalid_developer.rb +4 -0
- data/test/fixtures/note.rb +3 -0
- data/test/fixtures/notes.yml +95 -0
- data/test/fixtures/people.yml +14 -0
- data/test/fixtures/person.rb +4 -0
- data/test/fixtures/project.rb +7 -0
- data/test/fixtures/projects.yml +29 -0
- data/test/fixtures/schema.rb +53 -0
- data/test/fixtures/time_and_materials_project.rb +2 -0
- data/test/helper.rb +33 -0
- data/test/test_base.rb +21 -0
- data/test/test_relations.rb +455 -0
- metadata +173 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
module MetaWhere
|
2
|
+
class JoinType
|
3
|
+
attr_reader :name, :join_type, :klass
|
4
|
+
|
5
|
+
def initialize(name, join_type = Arel::Nodes::InnerJoin, klass = nil)
|
6
|
+
@name = name
|
7
|
+
@join_type = join_type
|
8
|
+
@klass = klass
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
self.class == other.class &&
|
13
|
+
name == other.name &&
|
14
|
+
join_type == other.join_type &&
|
15
|
+
klass == other.klass
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method :eql?, :==
|
19
|
+
|
20
|
+
def hash
|
21
|
+
[name, join_type, klass].hash
|
22
|
+
end
|
23
|
+
|
24
|
+
def outer
|
25
|
+
@join_type = Arel::Nodes::OuterJoin
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def inner
|
30
|
+
@join_type = Arel::Nodes::InnerJoin
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def type(klass)
|
35
|
+
@klass = klass
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_sym
|
40
|
+
self
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,290 @@
|
|
1
|
+
module MetaWhere
|
2
|
+
module Relation
|
3
|
+
|
4
|
+
JoinAssociation = ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
|
5
|
+
JoinDependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency
|
6
|
+
|
7
|
+
attr_writer :join_dependency
|
8
|
+
private :join_dependency=
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval do
|
12
|
+
alias_method_chain :reset, :metawhere
|
13
|
+
alias_method_chain :scope_for_create, :metawhere
|
14
|
+
end
|
15
|
+
|
16
|
+
# We have to do this on the singleton to work with Ruby 1.8.7. Not sure why.
|
17
|
+
base.instance_eval do
|
18
|
+
alias_method :&, :merge
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def join_dependency
|
23
|
+
@join_dependency ||= (build_join_dependency(table.from(table), @joins_values) && @join_dependency)
|
24
|
+
end
|
25
|
+
|
26
|
+
def merge(r, association_name = nil)
|
27
|
+
if (r && (association_name || base_class.name != r.klass.base_class.name)) # Merging relations with different base.
|
28
|
+
association_name ||= (default_association = reflect_on_all_associations.detect {|a| a.class_name == r.klass.name}) ?
|
29
|
+
default_association.name : r.table_name.to_sym
|
30
|
+
r = r.clone
|
31
|
+
r.where_values.map! {|w| MetaWhere::Visitors::Predicate.visitables.include?(w.class) ? {association_name => w} : w}
|
32
|
+
r.joins_values.map! {|j| [Symbol, Hash, MetaWhere::JoinType].include?(j.class) ? {association_name => j} : j}
|
33
|
+
self.joins_values += [association_name] if reflect_on_association(association_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
super(r)
|
37
|
+
end
|
38
|
+
|
39
|
+
def reset_with_metawhere
|
40
|
+
@mw_unique_joins = @mw_association_joins = @mw_non_association_joins =
|
41
|
+
@mw_stashed_association_joins = @mw_custom_joins = nil
|
42
|
+
reset_without_metawhere
|
43
|
+
end
|
44
|
+
|
45
|
+
def scope_for_create_with_metawhere
|
46
|
+
@scope_for_create ||= begin
|
47
|
+
@create_with_value || predicates_without_conflicting_equality.inject({}) do |hash, where|
|
48
|
+
if is_equality_predicate?(where)
|
49
|
+
hash[where.left.name] = where.right.respond_to?(:value) ? where.right.value : where.right
|
50
|
+
end
|
51
|
+
|
52
|
+
hash
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def build_where(opts, other = [])
|
58
|
+
if opts.is_a?(String)
|
59
|
+
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
|
60
|
+
else
|
61
|
+
predicates = []
|
62
|
+
[opts, *other].each do |arg|
|
63
|
+
predicates += Array.wrap(
|
64
|
+
case arg
|
65
|
+
when Array
|
66
|
+
@klass.send(:sanitize_sql, arg)
|
67
|
+
when Hash
|
68
|
+
@klass.send(:expand_hash_conditions_for_aggregates, arg)
|
69
|
+
else
|
70
|
+
arg
|
71
|
+
end
|
72
|
+
)
|
73
|
+
end
|
74
|
+
predicates
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def predicates_without_conflicting_equality
|
79
|
+
remove_conflicting_equality_predicates(flatten_predicates(@where_values, predicate_visitor))
|
80
|
+
end
|
81
|
+
|
82
|
+
# Very occasionally, we need to get a visitor for another relation, so it makes sense to factor
|
83
|
+
# these out into a public method despite only being two lines long.
|
84
|
+
def predicate_visitor
|
85
|
+
@predicate_visitor ||= begin
|
86
|
+
visitor = MetaWhere::Visitors::Predicate.new
|
87
|
+
visitor.join_dependency = join_dependency
|
88
|
+
visitor
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def attribute_visitor
|
93
|
+
@attribute_visitor ||= begin
|
94
|
+
visitor = MetaWhere::Visitors::Attribute.new
|
95
|
+
visitor.join_dependency = join_dependency
|
96
|
+
visitor
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Simulate the logic that occurs in ActiveRecord::Relation.to_a
|
101
|
+
#
|
102
|
+
# @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql)
|
103
|
+
#
|
104
|
+
# This will let us get a dump of the SQL that will be run against the DB for debug
|
105
|
+
# purposes without actually running the query.
|
106
|
+
def debug_sql
|
107
|
+
if eager_loading?
|
108
|
+
including = (@eager_load_values + @includes_values).uniq
|
109
|
+
join_dependency = JoinDependency.new(@klass, including, [])
|
110
|
+
construct_relation_for_association_find(join_dependency).to_sql
|
111
|
+
else
|
112
|
+
arel.to_sql
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def construct_limited_ids_condition(relation)
|
117
|
+
visitor = relation.attribute_visitor
|
118
|
+
|
119
|
+
relation.order_values.map! {|o| visitor.can_accept?(o) ? visitor.accept(o).to_sql : o}
|
120
|
+
|
121
|
+
super
|
122
|
+
end
|
123
|
+
|
124
|
+
def build_arel
|
125
|
+
arel = table.from table
|
126
|
+
|
127
|
+
build_join_dependency(arel, @joins_values) unless @joins_values.empty?
|
128
|
+
|
129
|
+
visitor = predicate_visitor
|
130
|
+
|
131
|
+
predicate_wheres = flatten_predicates(@where_values.uniq, visitor)
|
132
|
+
|
133
|
+
collapse_wheres(arel, (predicate_wheres - ['']).uniq)
|
134
|
+
|
135
|
+
arel.having(*flatten_predicates(@having_values, visitor).reject {|h| h.blank?}) unless @having_values.empty?
|
136
|
+
|
137
|
+
arel.take(@limit_value) if @limit_value
|
138
|
+
arel.skip(@offset_value) if @offset_value
|
139
|
+
|
140
|
+
arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
|
141
|
+
|
142
|
+
build_order(arel, attribute_visitor, @order_values) unless @order_values.empty?
|
143
|
+
|
144
|
+
build_select(arel, @select_values.uniq)
|
145
|
+
|
146
|
+
arel.from(@from_value) if @from_value
|
147
|
+
arel.lock(@lock_value) if @lock_value
|
148
|
+
|
149
|
+
arel
|
150
|
+
end
|
151
|
+
|
152
|
+
def select(value = Proc.new)
|
153
|
+
if MetaWhere::Function === value
|
154
|
+
value.table = self.arel_table
|
155
|
+
end
|
156
|
+
|
157
|
+
super
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def is_equality_predicate?(predicate)
|
163
|
+
predicate.class == Arel::Nodes::Equality
|
164
|
+
end
|
165
|
+
|
166
|
+
def build_join_dependency(manager, joins)
|
167
|
+
buckets = joins.group_by do |join|
|
168
|
+
case join
|
169
|
+
when String
|
170
|
+
'string_join'
|
171
|
+
when Hash, Symbol, Array, MetaWhere::JoinType
|
172
|
+
'association_join'
|
173
|
+
when JoinAssociation
|
174
|
+
'stashed_join'
|
175
|
+
when Arel::Nodes::Join
|
176
|
+
'join_node'
|
177
|
+
else
|
178
|
+
raise 'unknown class: %s' % join.class.name
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
association_joins = buckets['association_join'] || []
|
183
|
+
stashed_association_joins = buckets['stashed_join'] || []
|
184
|
+
join_nodes = buckets['join_node'] || []
|
185
|
+
string_joins = (buckets['string_join'] || []).map { |x|
|
186
|
+
x.strip
|
187
|
+
}.uniq
|
188
|
+
|
189
|
+
join_list = custom_join_ast(manager, string_joins)
|
190
|
+
|
191
|
+
# All of this duplication just to add
|
192
|
+
self.join_dependency = JoinDependency.new(
|
193
|
+
@klass,
|
194
|
+
association_joins,
|
195
|
+
join_list
|
196
|
+
)
|
197
|
+
|
198
|
+
join_nodes.each do |join|
|
199
|
+
join_dependency.table_aliases[join.left.name.downcase] = 1
|
200
|
+
end
|
201
|
+
|
202
|
+
join_dependency.graft(*stashed_association_joins)
|
203
|
+
|
204
|
+
@implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
|
205
|
+
|
206
|
+
# FIXME: refactor this to build an AST
|
207
|
+
join_dependency.join_associations.each do |association|
|
208
|
+
association.join_to(manager)
|
209
|
+
end
|
210
|
+
|
211
|
+
manager.join_sources.concat join_nodes.uniq
|
212
|
+
manager.join_sources.concat join_list
|
213
|
+
|
214
|
+
manager
|
215
|
+
end
|
216
|
+
|
217
|
+
def build_order(arel, visitor, orders)
|
218
|
+
order_attributes = orders.map {|o|
|
219
|
+
visitor.can_accept?(o) ? visitor.accept(o, visitor.join_dependency.join_base) : o
|
220
|
+
}.flatten.uniq.reject {|o| o.blank?}
|
221
|
+
order_attributes.present? ? arel.order(*order_attributes) : arel
|
222
|
+
end
|
223
|
+
|
224
|
+
def remove_conflicting_equality_predicates(predicates)
|
225
|
+
predicates.reverse.inject([]) { |ary, w|
|
226
|
+
unless is_equality_predicate?(w) && ary.any? {|p| is_equality_predicate?(p) && p.operand1.name == w.operand1.name}
|
227
|
+
ary << w
|
228
|
+
end
|
229
|
+
ary
|
230
|
+
}.reverse
|
231
|
+
end
|
232
|
+
|
233
|
+
def collapse_wheres(arel, wheres)
|
234
|
+
binaries = wheres.grep(Arel::Nodes::Binary)
|
235
|
+
|
236
|
+
groups = binaries.group_by do |binary|
|
237
|
+
[binary.class, binary.left]
|
238
|
+
end
|
239
|
+
|
240
|
+
groups.each do |_, bins|
|
241
|
+
test = bins.inject(bins.shift) do |memo, expr|
|
242
|
+
memo.or(expr)
|
243
|
+
end
|
244
|
+
arel = arel.where(test)
|
245
|
+
end
|
246
|
+
|
247
|
+
(wheres - binaries).each do |where|
|
248
|
+
where = Arel.sql(where) if String === where
|
249
|
+
arel = arel.where(Arel::Nodes::Grouping.new(where))
|
250
|
+
end
|
251
|
+
arel
|
252
|
+
end
|
253
|
+
|
254
|
+
def flatten_predicates(predicates, visitor)
|
255
|
+
predicates.map {|p|
|
256
|
+
visitor.can_accept?(p) ? visitor.accept(p) : p
|
257
|
+
}.flatten.uniq
|
258
|
+
end
|
259
|
+
#
|
260
|
+
# def unique_joins
|
261
|
+
# @mw_unique_joins ||= @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq
|
262
|
+
# end
|
263
|
+
#
|
264
|
+
# def association_joins
|
265
|
+
# @mw_association_joins ||= unique_joins.select{|j|
|
266
|
+
# [Hash, Array, Symbol, MetaWhere::JoinType].include?(j.class) && !array_of_strings?(j)
|
267
|
+
# }
|
268
|
+
# end
|
269
|
+
#
|
270
|
+
# def string_joins
|
271
|
+
# @mw_string_joins ||= unique_joins.select { |j| j.is_a? String }
|
272
|
+
# end
|
273
|
+
#
|
274
|
+
# def join_nodes
|
275
|
+
# @mw_join_nodes ||= unique_joins.select { |j| j.is_a? Arel::Nodes::Join }
|
276
|
+
# end
|
277
|
+
#
|
278
|
+
# def stashed_association_joins
|
279
|
+
# @mw_stashed_association_joins ||= unique_joins.grep(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)
|
280
|
+
# end
|
281
|
+
#
|
282
|
+
# def non_association_joins
|
283
|
+
# @mw_non_association_joins ||= (unique_joins - association_joins - stashed_association_joins).reject {|j| j.blank?}
|
284
|
+
# end
|
285
|
+
#
|
286
|
+
# def custom_joins
|
287
|
+
# @mw_custom_joins ||= custom_join_ast(@klass.arel_table, non_association_joins)
|
288
|
+
# end
|
289
|
+
end
|
290
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module MetaWhere
|
2
|
+
module Utility
|
3
|
+
private
|
4
|
+
|
5
|
+
def array_of_activerecords(val)
|
6
|
+
val.is_a?(Array) && !val.empty? && val.all? {|v| v.is_a?(ActiveRecord::Base)}
|
7
|
+
end
|
8
|
+
|
9
|
+
def association_from_parent_and_column(parent, column)
|
10
|
+
parent.is_a?(Symbol) ? nil : @join_dependency.send(:find_join_association, column, parent)
|
11
|
+
end
|
12
|
+
|
13
|
+
def attribute_from_column_and_table(column, table)
|
14
|
+
case column
|
15
|
+
when String, Symbol
|
16
|
+
table[column]
|
17
|
+
when MetaWhere::Function
|
18
|
+
column.table = table
|
19
|
+
column.to_sqlliteral
|
20
|
+
else
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def args_for_predicate(value)
|
26
|
+
case value
|
27
|
+
when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation
|
28
|
+
value.to_a.map { |x|
|
29
|
+
x.respond_to?(:quoted_id) ? x.quoted_id : x
|
30
|
+
}
|
31
|
+
when ActiveRecord::Base
|
32
|
+
value.quoted_id
|
33
|
+
else
|
34
|
+
value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def method_from_value(value)
|
39
|
+
case value
|
40
|
+
when Array, Range, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation, Arel::Relation
|
41
|
+
:in
|
42
|
+
else
|
43
|
+
:eq
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def valid_comparison_method?(method)
|
48
|
+
MetaWhere::PREDICATES.map(&:to_s).include?(method.to_s)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'meta_where/visitors/visitor'
|
2
|
+
|
3
|
+
module MetaWhere
|
4
|
+
module Visitors
|
5
|
+
class Attribute < Visitor
|
6
|
+
|
7
|
+
def self.visitables
|
8
|
+
[Hash, Symbol, MetaWhere::Column]
|
9
|
+
end
|
10
|
+
|
11
|
+
def visit_Hash(o, parent)
|
12
|
+
parent = parent.name if parent.is_a? MetaWhere::JoinType
|
13
|
+
table = tables[parent]
|
14
|
+
built_attributes = o.map do |column, value|
|
15
|
+
if value.is_a?(Hash)
|
16
|
+
association = association_from_parent_and_column(parent, column)
|
17
|
+
accept(value, association || column)
|
18
|
+
elsif value.is_a?(Array) && value.all? {|v| can_accept?(v)}
|
19
|
+
association = association_from_parent_and_column(parent, column)
|
20
|
+
value.map {|val| self.accept(val, association || column)}
|
21
|
+
else
|
22
|
+
association = association_from_parent_and_column(parent, column)
|
23
|
+
can_accept?(value) ? self.accept(value, association || column) : value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
built_attributes.flatten
|
28
|
+
end
|
29
|
+
|
30
|
+
def visit_Symbol(o, parent)
|
31
|
+
table = tables[parent]
|
32
|
+
|
33
|
+
unless attribute = table[o]
|
34
|
+
raise ::ActiveRecord::StatementInvalid, "No attribute named `#{o}` exists for table `#{table.name}`"
|
35
|
+
end
|
36
|
+
|
37
|
+
attribute
|
38
|
+
end
|
39
|
+
|
40
|
+
def visit_MetaWhere_Column(o, parent)
|
41
|
+
column_name = o.column.to_s
|
42
|
+
if column_name.include?('.')
|
43
|
+
table_name, column_name = column_name.split('.', 2)
|
44
|
+
table = Arel::Table.new(table_name, :engine => parent.arel_engine)
|
45
|
+
else
|
46
|
+
table = tables[parent]
|
47
|
+
end
|
48
|
+
|
49
|
+
unless attribute = table[column_name]
|
50
|
+
raise ::ActiveRecord::StatementInvalid, "No attribute named `#{column_name}` exists for table `#{table.name}`"
|
51
|
+
end
|
52
|
+
|
53
|
+
attribute.send(o.method)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|