record_filter 0.9.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.gitignore +9 -0
  2. data/CHANGELOG +232 -0
  3. data/README.rdoc +354 -0
  4. data/Rakefile +92 -0
  5. data/TODO +3 -0
  6. data/VERSION.yml +4 -0
  7. data/config/roodi.yml +14 -0
  8. data/lib/record_filter/active_record.rb +108 -0
  9. data/lib/record_filter/column_parser.rb +14 -0
  10. data/lib/record_filter/conjunctions.rb +169 -0
  11. data/lib/record_filter/dsl/class_join.rb +16 -0
  12. data/lib/record_filter/dsl/conjunction.rb +57 -0
  13. data/lib/record_filter/dsl/conjunction_dsl.rb +317 -0
  14. data/lib/record_filter/dsl/dsl.rb +143 -0
  15. data/lib/record_filter/dsl/dsl_factory.rb +19 -0
  16. data/lib/record_filter/dsl/group_by.rb +11 -0
  17. data/lib/record_filter/dsl/join.rb +12 -0
  18. data/lib/record_filter/dsl/join_condition.rb +21 -0
  19. data/lib/record_filter/dsl/join_dsl.rb +49 -0
  20. data/lib/record_filter/dsl/limit.rb +12 -0
  21. data/lib/record_filter/dsl/named_filter.rb +12 -0
  22. data/lib/record_filter/dsl/order.rb +12 -0
  23. data/lib/record_filter/dsl/restriction.rb +314 -0
  24. data/lib/record_filter/dsl.rb +21 -0
  25. data/lib/record_filter/filter.rb +105 -0
  26. data/lib/record_filter/group_by.rb +21 -0
  27. data/lib/record_filter/join.rb +66 -0
  28. data/lib/record_filter/order.rb +27 -0
  29. data/lib/record_filter/query.rb +60 -0
  30. data/lib/record_filter/restriction_factory.rb +21 -0
  31. data/lib/record_filter/restrictions.rb +97 -0
  32. data/lib/record_filter/table.rb +172 -0
  33. data/lib/record_filter.rb +35 -0
  34. data/record_filter.gemspec +108 -0
  35. data/script/console +8 -0
  36. data/spec/active_record_spec.rb +211 -0
  37. data/spec/exception_spec.rb +208 -0
  38. data/spec/explicit_join_spec.rb +132 -0
  39. data/spec/implicit_join_spec.rb +403 -0
  40. data/spec/limits_and_ordering_spec.rb +230 -0
  41. data/spec/models.rb +109 -0
  42. data/spec/named_filter_spec.rb +264 -0
  43. data/spec/proxying_spec.rb +63 -0
  44. data/spec/restrictions_spec.rb +251 -0
  45. data/spec/select_spec.rb +79 -0
  46. data/spec/spec_helper.rb +39 -0
  47. data/spec/test.db +0 -0
  48. data/tasks/db.rake +106 -0
  49. data/tasks/rcov.rake +9 -0
  50. data/tasks/spec.rake +10 -0
  51. data/test/performance_test.rb +39 -0
  52. data/test/test.db +0 -0
  53. metadata +137 -0
data/config/roodi.yml ADDED
@@ -0,0 +1,14 @@
1
+ AssignmentInConditionalCheck: { }
2
+ CaseMissingElseCheck: { }
3
+ ClassLineCountCheck: { line_count: 300 }
4
+ ClassNameCheck: { pattern: !ruby/regexp /^[A-Z][a-zA-Z0-9]*$/ }
5
+ CyclomaticComplexityBlockCheck: { complexity: 4 }
6
+ CyclomaticComplexityMethodCheck: { complexity: 8 }
7
+ EmptyRescueBodyCheck: { }
8
+ ForLoopCheck: { }
9
+ MethodLineCountCheck: { line_count: 20 }
10
+ MethodNameCheck: { pattern: !ruby/regexp /^[_a-z<>=\[\]|+-\/\*`]+[_a-z0-9_<>=~@\[\]]*[=!\?]?$/ }
11
+ ModuleLineCountCheck: { line_count: 300 }
12
+ ModuleNameCheck: { pattern: !ruby/regexp /^[A-Z][a-zA-Z0-9]*$/ }
13
+ ParameterNumberCheck: { parameter_count: 5 }
14
+
@@ -0,0 +1,108 @@
1
+ module RecordFilter
2
+ # The ActiveRecordExtension module is mixed in to ActiveRecord::Base to form the
3
+ # top-level API for interacting with record_filter. It adds public methods for
4
+ # executing ad-hoc filters as well as for creating and querying named filters.
5
+ # See RecordFilter::ActiveRecordExtension::ClassMethods for more detail on the
6
+ # API.
7
+ module ActiveRecordExtension
8
+ module ClassMethods
9
+
10
+ # Create a filter on the fly to find a set of results that matches the given criteria.
11
+ # This method, which can be called on any ActiveRecord::Base subclass, accepts a block
12
+ # that defines the contents of the filter and returns a Filter object that contains a list
13
+ # of the objects resulting from the query. See the documentation for RecordFilter::DSL
14
+ # for more information on how to specify the filter.
15
+ #
16
+ # ==== Parameters
17
+ # block<Proc>::
18
+ # A block that specifies the contents of the filter.
19
+ #
20
+ # ==== Returns
21
+ # Filter::
22
+ # The Filter object resulting from the query, which can be treated as an array of the results.
23
+ #
24
+ # ==== Example
25
+ # Blog.filter do
26
+ # having(:posts).with(:name, nil)
27
+ # end
28
+ #
29
+ # @public
30
+ def filter(&block)
31
+ Filter.new(self, nil, &block)
32
+ end
33
+
34
+ # Create a new named filter, which defines a function on the callee class that provides easy
35
+ # access to the query defined by the filter. Any number of named filters can be created on a
36
+ # class, and they can also be chained to create complex queries out of simple building blocks.
37
+ # In addition, named filters can accept any number of arguments in order to allow customization
38
+ # of their behavior when used. For more details on how to specify the contents of named filters,
39
+ # see the documentation for RecordFilter::DSL.
40
+ #
41
+ # Post.named_filter(:without_permalink) do
42
+ # with(:permalink, nil)
43
+ # end
44
+ #
45
+ # Post.named_filter(:created_after) do |time|
46
+ # with(:created_at).gt(time)
47
+ # end
48
+ #
49
+ # Post.without_permalink # :conditions => ['permalink IS NULL']
50
+ # Post.created_after(3.hours.ago) # :conditions => ['created_at > ?', 3.hours.ago]
51
+ # Post.without_permalink.created_after(3.hours.ago) # :conditions => ['permalink IS NULL AND created_at > ?', 3.hours.ago]
52
+ #
53
+ # ==== Raises
54
+ # InvalidFilterNameException::
55
+ # There is already a named filter with the given name on this class or one of its superclasses.
56
+ #
57
+ # ==== Returns
58
+ # nil
59
+ #
60
+ # @public
61
+ def named_filter(name, &block)
62
+ name = name.to_sym
63
+ if named_filters.include?(name)
64
+ raise InvalidFilterNameException.new("A named filter with the name #{name} already exists on the class #{self.name}.")
65
+ end
66
+ local_named_filters << name
67
+ DSL::DSLFactory::get_subclass(self).module_eval do
68
+ define_method(name, &block)
69
+ end
70
+
71
+ (class << self; self; end).instance_eval do
72
+ define_method(name) do |*args|
73
+ Filter.new(self, name, *args)
74
+ end
75
+ end
76
+ nil
77
+ end
78
+
79
+ # Retreive a list of named filters that apply to a specific class, including ones
80
+ # that were defined in its superclasses.
81
+ #
82
+ # ==== Returns
83
+ # Array:: A list of the names of the named filters, as symbols.
84
+ #
85
+ # @public
86
+ def named_filters
87
+ result = local_named_filters.dup
88
+ result.concat(superclass.named_filters) if (superclass && superclass.respond_to?(:named_filters))
89
+ result
90
+ end
91
+
92
+ protected
93
+
94
+ def local_named_filters # :nodoc:
95
+ @local_named_filters ||= []
96
+ end
97
+ end
98
+
99
+ module AssociationInstanceMethods
100
+ def filter(&block)
101
+ Filter.new(self, @finder_sql, &block)
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ ActiveRecord::Base.send(:extend, RecordFilter::ActiveRecordExtension::ClassMethods)
108
+ ActiveRecord::Associations::AssociationCollection.send(:include, RecordFilter::ActiveRecordExtension::AssociationInstanceMethods)
@@ -0,0 +1,14 @@
1
+ module RecordFilter
2
+ module ColumnParser # :nodoc: all
3
+
4
+ protected
5
+
6
+ def parse_column_in_table(column, table)
7
+ while column.is_a?(Hash)
8
+ table = table.find_join!(column.keys[0]).right_table
9
+ column = column.values[0]
10
+ end
11
+ [column, table]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,169 @@
1
+ module RecordFilter
2
+ module Conjunctions # :nodoc: all
3
+ class Base
4
+ attr_reader :table_name, :limit, :offset, :distinct
5
+
6
+ def self.create_from(dsl_conjunction, table)
7
+ result = case dsl_conjunction.type
8
+ when :any_of then AnyOf.new(table)
9
+ when :all_of then AllOf.new(table)
10
+ when :none_of then NoneOf.new(table)
11
+ when :not_all_of then NotAllOf.new(table)
12
+ else raise InvalidFilterException.new("An invalid conjunction type of #{dsl_conjunction.type} was used.")
13
+ end
14
+
15
+ dsl_conjunction.steps.each do |step|
16
+ case step
17
+ when DSL::Restriction
18
+ handle_restriction_step(step, result, table, true)
19
+ when DSL::Conjunction
20
+ result.add_conjunction(create_from(step, table))
21
+ when DSL::Join
22
+ join = result.add_join_on_association(step.association, step.join_type, step.aliaz)
23
+ result.add_conjunction(create_from(step.conjunction, join.right_table))
24
+ when DSL::ClassJoin
25
+ join = result.add_join_on_class(
26
+ step.join_class, step.join_type, step.table_alias, step.conditions)
27
+ result.add_conjunction(create_from(step.conjunction, join.right_table))
28
+ when DSL::Limit
29
+ result.add_limit_and_offset(step.limit, step.offset)
30
+ when DSL::Order
31
+ result.add_order(step.column, step.direction)
32
+ when DSL::GroupBy
33
+ result.add_group_by(step.column)
34
+ when DSL::NamedFilter
35
+ result.add_named_filter(step.name, step.args)
36
+ else raise InvalidFilterException.new('And invalid filter step was provided.')
37
+ end
38
+ end
39
+ result.set_distinct(dsl_conjunction.distinct)
40
+ result
41
+ end
42
+
43
+ def initialize(table, restrictions = nil, joins = nil)
44
+ @table = table
45
+ @table_name = table.table_alias
46
+ @restrictions = restrictions || []
47
+ @joins = joins || []
48
+ @distinct = false
49
+ end
50
+
51
+ def add_restriction(column_name, operator, value, options={})
52
+ check_column_exists!(column_name)
53
+ restriction = RestrictionFactory.build(operator, "#{@table_name}.#{column_name}", value, options)
54
+ self << restriction
55
+ end
56
+
57
+ def add_conjunction(conjunction)
58
+ self << conjunction
59
+ conjunction
60
+ end
61
+
62
+ def add_join_on_association(association_name, join_type, aliaz)
63
+ table = @table
64
+ while association_name.is_a?(Hash)
65
+ table = table.join_association(association_name.keys[0], { :join_type => join_type }).right_table
66
+ association_name = association_name.values[0]
67
+ end
68
+ table.join_association(association_name, :join_type => join_type, :alias => aliaz)
69
+ end
70
+
71
+ def add_join_on_class(join_class, join_type, table_alias, conditions)
72
+ @table.join_class(join_class, join_type, table_alias, conditions)
73
+ end
74
+
75
+ def add_order(column_name, direction)
76
+ @table.order_column(column_name, direction)
77
+ end
78
+
79
+ def add_group_by(column_name)
80
+ @table.group_by_column(column_name)
81
+ end
82
+
83
+ def add_limit_and_offset(limit, offset)
84
+ @limit, @offset = limit, offset
85
+ end
86
+
87
+ def set_distinct(value)
88
+ @distinct = value
89
+ end
90
+
91
+ def add_named_filter(name, args)
92
+ unless @table.model_class.named_filters.include?(name.to_sym)
93
+ raise NamedFilterNotFoundException.new("The named filter #{name} was not found in #{@table.model_class}")
94
+ end
95
+ query = Query.new(@table.model_class, name, *args)
96
+ self << self.class.create_from(query.dsl_conjunction, @table)
97
+ end
98
+
99
+ def <<(restriction)
100
+ @restrictions << restriction
101
+ end
102
+
103
+ def to_conditions
104
+ result = begin
105
+ if @restrictions.empty?
106
+ nil
107
+ elsif @restrictions.length == 1
108
+ @restrictions.first.to_conditions
109
+ else
110
+ @restrictions.map do |restriction|
111
+ conditions = restriction.to_conditions
112
+ if conditions
113
+ conditions[0] = "(#{conditions[0]})"
114
+ conditions
115
+ else
116
+ nil
117
+ end
118
+ end.compact.inject do |conditions, new_conditions|
119
+ conditions.first << " #{conjunctor} #{new_conditions.shift}"
120
+ conditions.concat(new_conditions)
121
+ conditions
122
+ end
123
+ end
124
+ end
125
+ result[0] = "NOT (#{result[0]})" if (negated && !result.nil? && !result[0].nil?)
126
+ result
127
+ end
128
+
129
+ protected
130
+
131
+ def check_column_exists!(column_name)
132
+ if (!@table.has_column(column_name))
133
+ raise ColumnNotFoundException.new("The column #{column_name} was not found in #{@table.table_name}.")
134
+ end
135
+ end
136
+
137
+ def self.handle_restriction_step(step, conjunction, table, follow_conjunctions=true)
138
+ if ((cr = step.conjuncted_restriction) && follow_conjunctions)
139
+ restriction_conjunction = create_from(DSL::Conjunction.new(nil, step.conjuncted_restriction_type), table)
140
+ handle_restriction_step(step, restriction_conjunction, table, false)
141
+ handle_restriction_step(cr, restriction_conjunction, table, true)
142
+ conjunction.add_conjunction(restriction_conjunction)
143
+ else
144
+ conjunction.add_restriction(step.column, step.operator, step.value, :negated => step.negated)
145
+ end
146
+ end
147
+ end
148
+
149
+ class AnyOf < Base
150
+ def conjunctor; 'OR'; end
151
+ def negated; false; end
152
+ end
153
+
154
+ class AllOf < Base
155
+ def conjunctor; 'AND'; end
156
+ def negated; false; end
157
+ end
158
+
159
+ class NoneOf < Base
160
+ def conjunctor; 'OR'; end
161
+ def negated; true; end
162
+ end
163
+
164
+ class NotAllOf < Base
165
+ def conjunctor; 'AND'; end
166
+ def negated; true; end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,16 @@
1
+ module RecordFilter
2
+ module DSL
3
+ class ClassJoin # :nodoc: all
4
+ attr_reader :join_class, :join_type, :table_alias, :conjunction
5
+
6
+ def initialize(join_class, join_type, table_alias, conjunction, join_conditions)
7
+ @join_class, @join_type, @table_alias, @conjunction, @join_conditions =
8
+ join_class, join_type, table_alias, conjunction, join_conditions
9
+ end
10
+
11
+ def conditions
12
+ @join_conditions ? @join_conditions.map { |c| c.condition } : nil
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,57 @@
1
+ module RecordFilter
2
+ module DSL
3
+ class Conjunction # :nodoc: all
4
+
5
+ attr_reader :type, :steps, :distinct
6
+
7
+ def initialize(model_class, type=:all_of)
8
+ @model_class, @type, @steps, @distinct = model_class, type, [], false
9
+ end
10
+
11
+ def add_restriction(column, value)
12
+ @steps << (restriction = Restriction.new(column, value))
13
+ restriction
14
+ end
15
+
16
+ def add_conjunction(type, &block)
17
+ dsl = ConjunctionDSL.new(@model_class, Conjunction.new(@model_class, type))
18
+ dsl.instance_eval(&block) if block
19
+ @steps << dsl.conjunction
20
+ end
21
+
22
+ def add_join(association, join_type, aliaz, &block)
23
+ dsl = ConjunctionDSL.new(@model_class, Conjunction.new(@model_class, :all_of))
24
+ dsl.instance_eval(&block) if block
25
+ @steps << Join.new(association, join_type, dsl.conjunction, aliaz)
26
+ dsl
27
+ end
28
+
29
+ def add_class_join(clazz, join_type, table_alias, &block)
30
+ dsl = JoinDSL.new(@model_class, Conjunction.new(@model_class, :all_of))
31
+ dsl.instance_eval(&block) if block
32
+ @steps << ClassJoin.new(clazz, join_type, table_alias, dsl.conjunction, dsl.conditions)
33
+ dsl
34
+ end
35
+
36
+ def add_limit(limit, offset)
37
+ @steps << Limit.new(limit, offset)
38
+ end
39
+
40
+ def add_order(column, direction)
41
+ @steps << Order.new(column, direction)
42
+ end
43
+
44
+ def add_group_by(column)
45
+ @steps << GroupBy.new(column)
46
+ end
47
+
48
+ def set_distinct
49
+ @distinct = true
50
+ end
51
+
52
+ def add_named_filter(method, *args)
53
+ @steps << NamedFilter.new(method, *args)
54
+ end
55
+ end
56
+ end
57
+ end