maksar-meta_where 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/CHANGELOG +90 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +343 -0
  7. data/Rakefile +11 -0
  8. data/lib/core_ext/hash.rb +5 -0
  9. data/lib/core_ext/symbol.rb +39 -0
  10. data/lib/core_ext/symbol_operators.rb +48 -0
  11. data/lib/meta_where.rb +51 -0
  12. data/lib/meta_where/association_reflection.rb +51 -0
  13. data/lib/meta_where/column.rb +31 -0
  14. data/lib/meta_where/compound.rb +20 -0
  15. data/lib/meta_where/condition.rb +32 -0
  16. data/lib/meta_where/condition_operators.rb +19 -0
  17. data/lib/meta_where/function.rb +108 -0
  18. data/lib/meta_where/join_dependency.rb +105 -0
  19. data/lib/meta_where/join_type.rb +43 -0
  20. data/lib/meta_where/not.rb +13 -0
  21. data/lib/meta_where/relation.rb +290 -0
  22. data/lib/meta_where/utility.rb +51 -0
  23. data/lib/meta_where/version.rb +3 -0
  24. data/lib/meta_where/visitors/attribute.rb +58 -0
  25. data/lib/meta_where/visitors/predicate.rb +149 -0
  26. data/lib/meta_where/visitors/visitor.rb +52 -0
  27. data/meta_where.gemspec +48 -0
  28. data/test/fixtures/companies.yml +17 -0
  29. data/test/fixtures/company.rb +7 -0
  30. data/test/fixtures/data_type.rb +3 -0
  31. data/test/fixtures/data_types.yml +15 -0
  32. data/test/fixtures/developer.rb +5 -0
  33. data/test/fixtures/developers.yml +55 -0
  34. data/test/fixtures/developers_projects.yml +25 -0
  35. data/test/fixtures/fixed_bid_project.rb +2 -0
  36. data/test/fixtures/invalid_company.rb +4 -0
  37. data/test/fixtures/invalid_developer.rb +4 -0
  38. data/test/fixtures/note.rb +3 -0
  39. data/test/fixtures/notes.yml +95 -0
  40. data/test/fixtures/people.yml +14 -0
  41. data/test/fixtures/person.rb +4 -0
  42. data/test/fixtures/project.rb +7 -0
  43. data/test/fixtures/projects.yml +29 -0
  44. data/test/fixtures/schema.rb +53 -0
  45. data/test/fixtures/time_and_materials_project.rb +2 -0
  46. data/test/helper.rb +33 -0
  47. data/test/test_base.rb +21 -0
  48. data/test/test_relations.rb +455 -0
  49. 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,13 @@
1
+ require 'meta_where/condition_operators'
2
+
3
+ module MetaWhere
4
+ class Not
5
+ include ConditionOperators
6
+
7
+ attr_reader :expr
8
+
9
+ def initialize(expr)
10
+ @expr = expr
11
+ end
12
+ end
13
+ 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,3 @@
1
+ module MetaWhere
2
+ VERSION = "1.0.4"
3
+ 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