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.
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