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