maksar-meta_where 1.0.4
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/.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
|