record_filter 0.9.12
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/CHANGELOG +232 -0
- data/README.rdoc +354 -0
- data/Rakefile +92 -0
- data/TODO +3 -0
- data/VERSION.yml +4 -0
- data/config/roodi.yml +14 -0
- data/lib/record_filter/active_record.rb +108 -0
- data/lib/record_filter/column_parser.rb +14 -0
- data/lib/record_filter/conjunctions.rb +169 -0
- data/lib/record_filter/dsl/class_join.rb +16 -0
- data/lib/record_filter/dsl/conjunction.rb +57 -0
- data/lib/record_filter/dsl/conjunction_dsl.rb +317 -0
- data/lib/record_filter/dsl/dsl.rb +143 -0
- data/lib/record_filter/dsl/dsl_factory.rb +19 -0
- data/lib/record_filter/dsl/group_by.rb +11 -0
- data/lib/record_filter/dsl/join.rb +12 -0
- data/lib/record_filter/dsl/join_condition.rb +21 -0
- data/lib/record_filter/dsl/join_dsl.rb +49 -0
- data/lib/record_filter/dsl/limit.rb +12 -0
- data/lib/record_filter/dsl/named_filter.rb +12 -0
- data/lib/record_filter/dsl/order.rb +12 -0
- data/lib/record_filter/dsl/restriction.rb +314 -0
- data/lib/record_filter/dsl.rb +21 -0
- data/lib/record_filter/filter.rb +105 -0
- data/lib/record_filter/group_by.rb +21 -0
- data/lib/record_filter/join.rb +66 -0
- data/lib/record_filter/order.rb +27 -0
- data/lib/record_filter/query.rb +60 -0
- data/lib/record_filter/restriction_factory.rb +21 -0
- data/lib/record_filter/restrictions.rb +97 -0
- data/lib/record_filter/table.rb +172 -0
- data/lib/record_filter.rb +35 -0
- data/record_filter.gemspec +108 -0
- data/script/console +8 -0
- data/spec/active_record_spec.rb +211 -0
- data/spec/exception_spec.rb +208 -0
- data/spec/explicit_join_spec.rb +132 -0
- data/spec/implicit_join_spec.rb +403 -0
- data/spec/limits_and_ordering_spec.rb +230 -0
- data/spec/models.rb +109 -0
- data/spec/named_filter_spec.rb +264 -0
- data/spec/proxying_spec.rb +63 -0
- data/spec/restrictions_spec.rb +251 -0
- data/spec/select_spec.rb +79 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/test.db +0 -0
- data/tasks/db.rake +106 -0
- data/tasks/rcov.rake +9 -0
- data/tasks/spec.rake +10 -0
- data/test/performance_test.rb +39 -0
- data/test/test.db +0 -0
- 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
|