record_filter 0.9.12

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