record_filter 0.9.17 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :major: 0
3
- :minor: 9
4
- :patch: 17
2
+ :major: 1
3
+ :minor: 0
4
+ :patch: 0
@@ -1,7 +1,7 @@
1
1
  module RecordFilter
2
2
  module Conjunctions # :nodoc: all
3
3
  class Base
4
- attr_reader :table_name, :limit, :offset, :distinct
4
+ attr_reader :table_name, :limit, :offset, :distinct, :select_columns
5
5
 
6
6
  def self.create_from(dsl_conjunction, table)
7
7
  result = case dsl_conjunction.type
@@ -39,6 +39,7 @@ module RecordFilter
39
39
  end
40
40
  end
41
41
  result.set_distinct(dsl_conjunction.distinct)
42
+ result.set_select_columns(dsl_conjunction.select_columns)
42
43
  result
43
44
  end
44
45
 
@@ -94,6 +95,10 @@ module RecordFilter
94
95
  @distinct = value
95
96
  end
96
97
 
98
+ def set_select_columns(select_columns)
99
+ @select_columns = select_columns
100
+ end
101
+
97
102
  def add_named_filter(name, args)
98
103
  unless @table.model_class.named_filters.include?(name.to_sym)
99
104
  raise NamedFilterNotFoundException.new("The named filter #{name} was not found in #{@table.model_class}")
@@ -2,7 +2,7 @@ module RecordFilter
2
2
  module DSL
3
3
  class Conjunction # :nodoc: all
4
4
 
5
- attr_reader :type, :steps, :distinct
5
+ attr_reader :type, :steps, :distinct, :select_columns
6
6
 
7
7
  def initialize(model_class, type=:all_of)
8
8
  @model_class, @type, @steps, @distinct = model_class, type, [], false
@@ -53,6 +53,10 @@ module RecordFilter
53
53
  @distinct = true
54
54
  end
55
55
 
56
+ def set_select_columns(columns)
57
+ @select_columns = columns
58
+ end
59
+
56
60
  def add_named_filter(method, *args)
57
61
  @steps << NamedFilter.new(method, *args)
58
62
  end
@@ -129,16 +129,27 @@ module RecordFilter
129
129
  # block, etc.).
130
130
  #
131
131
  # ==== Parameters
132
- # none
132
+ #
133
+ # columns...<Symbol>:
134
+ # Zero or more column names to select. Equivalent to
135
+ # select(:col1, :col2) ; distinct. If empty, select all columns.
133
136
  #
134
137
  # ==== Returns
135
138
  # nil
136
139
  #
137
140
  # @public
138
- def distinct
141
+ def distinct(*columns)
142
+ select(*columns) unless columns.empty?
139
143
  @conjunction.set_distinct
140
144
  nil
141
145
  end
146
+
147
+ #
148
+ # Specify one or more columns to select from the database.
149
+ #
150
+ def select(*columns)
151
+ @conjunction.set_select_columns(columns)
152
+ end
142
153
  end
143
154
  end
144
155
  end
@@ -305,7 +305,7 @@ module RecordFilter
305
305
  def take_value(value) # :nodoc:
306
306
  if value.nil?
307
307
  is_null
308
- elsif value != DEFAULT_VALUE
308
+ elsif value.respond_to?(:to_subquery) || value != DEFAULT_VALUE
309
309
  equal_to(value)
310
310
  end
311
311
  end
@@ -65,6 +65,20 @@ module RecordFilter
65
65
  @query.to_find_params(count_query)
66
66
  end
67
67
 
68
+ def to_subquery(single_result = true)
69
+ proxy_options = proxy_options()
70
+ if @query.select_columns && @query.select_columns.length > 1
71
+ raise(InvalidFilterException, "Only one select column allowed in a subquery")
72
+ end
73
+ @clazz.module_eval do
74
+ options = proxy_options.reverse_merge(
75
+ :select => "#{quoted_table_name}.#{primary_key}"
76
+ )
77
+ options.merge!(:limit => 1) if single_result
78
+ construct_finder_sql(options)
79
+ end
80
+ end
81
+
68
82
  protected
69
83
 
70
84
  def method_missing(method, *args, &block) # :nodoc:
@@ -36,15 +36,25 @@ module RecordFilter
36
36
  end
37
37
  end
38
38
 
39
+ def select_columns
40
+ @conjunction.select_columns
41
+ end
42
+
39
43
  protected
40
44
 
41
45
  def set_select(params, count_query)
42
- if @conjunction.distinct || (@table.all_joins.any? { |j| j.requires_distinct_select? })
46
+ select_column_statement =
43
47
  if count_query
44
- params[:select] = "DISTINCT #{@table.table_name}.#{@table.model_class.primary_key}"
48
+ "#{@table.table_name}.#{@table.model_class.primary_key}"
49
+ elsif select_columns
50
+ select_columns.map { |column| "#{@table.table_name}.#{column}" }.join(', ')
45
51
  else
46
- params[:select] = "DISTINCT #{@table.table_name}.*"
52
+ "#{@table.table_name}.*"
47
53
  end
54
+ if @conjunction.distinct || (@table.all_joins.any? { |j| j.requires_distinct_select? })
55
+ params[:select] = "DISTINCT #{select_column_statement}"
56
+ elsif @conjunction.select_columns
57
+ params[:select] = select_column_statement
48
58
  end
49
59
  end
50
60
 
@@ -5,11 +5,11 @@ module RecordFilter
5
5
 
6
6
  def initialize(column_name, value, table, options={})
7
7
  @column_name, @value, @table, @negated = column_name, value, table, !!options.delete(:negated)
8
- @value = @value.id if @value.kind_of?(ActiveRecord::Base)
8
+ @value = @value.id if !subquery? && @value.kind_of?(ActiveRecord::Base)
9
9
  end
10
10
 
11
11
  def to_conditions
12
- if @value.nil? || @value.is_a?(Hash)
12
+ if subquery? || @value.nil? || @value.is_a?(Hash)
13
13
  [to_sql]
14
14
  else
15
15
  [to_sql, @value]
@@ -25,7 +25,9 @@ module RecordFilter
25
25
  end
26
26
 
27
27
  def value_hash_as_column_or_question_mark
28
- if @value.is_a?(Hash)
28
+ if subquery?
29
+ "(#{@value.to_subquery})"
30
+ elsif @value.is_a?(Hash)
29
31
  column, table = parse_column_in_table(@value, @table)
30
32
  if (table.has_column(column))
31
33
  "#{table.table_alias}.#{column}"
@@ -34,6 +36,10 @@ module RecordFilter
34
36
  '?'
35
37
  end
36
38
  end
39
+
40
+ def subquery?
41
+ @value.respond_to?(:to_subquery)
42
+ end
37
43
  end
38
44
 
39
45
  class EqualTo < Base
@@ -94,12 +100,22 @@ module RecordFilter
94
100
  # produce a condition like "WHERE id NOT IN (NULL)", and that will
95
101
  # return an empty set, which is presumably not what the user meant
96
102
  # to get. Doing some babysitting here.
97
- if @negated && @value.respond_to?(:empty?) && @value.empty?
103
+ if subquery?
104
+ [to_sql]
105
+ elsif @negated && @value.respond_to?(:empty?) && @value.empty?
98
106
  []
99
107
  else
100
108
  [to_sql, @value]
101
109
  end
102
110
  end
111
+
112
+ def value_hash_as_column_or_question_mark
113
+ if subquery?
114
+ @value.to_subquery(false)
115
+ else
116
+ super
117
+ end
118
+ end
103
119
  end
104
120
 
105
121
  class Between < Base
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{record_filter}
8
- s.version = "0.9.17"
8
+ s.version = "1.0.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Aubrey Holland", "Mat Brown"]
12
- s.date = %q{2010-03-04}
12
+ s.date = %q{2010-03-18}
13
13
  s.description = %q{RecordFilter is a Pure-ruby criteria API for building complex queries in ActiveRecord. It supports queries that are built on the fly as well as named filters that can be added to objects and chained to create complex queries. It also gets rid of the nasty hard-coded SQL that shows up in most ActiveRecord code with a clean API that makes queries simple and intuitive to build.}
14
14
  s.email = %q{aubreyholland@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -55,6 +55,7 @@ Gem::Specification.new do |s|
55
55
  "spec/active_record_spec.rb",
56
56
  "spec/exception_spec.rb",
57
57
  "spec/explicit_join_spec.rb",
58
+ "spec/explicit_subquery_spec.rb",
58
59
  "spec/implicit_join_spec.rb",
59
60
  "spec/limits_and_ordering_spec.rb",
60
61
  "spec/models.rb",
@@ -80,6 +81,7 @@ Gem::Specification.new do |s|
80
81
  "spec/active_record_spec.rb",
81
82
  "spec/exception_spec.rb",
82
83
  "spec/explicit_join_spec.rb",
84
+ "spec/explicit_subquery_spec.rb",
83
85
  "spec/implicit_join_spec.rb",
84
86
  "spec/limits_and_ordering_spec.rb",
85
87
  "spec/models.rb",
@@ -0,0 +1,55 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'explicit subqueries' do
4
+ before :each do
5
+ TestModel.extended_models.each { |model| model.last_find = {} }
6
+ end
7
+
8
+ after :each do
9
+ Blog.last_find.should be_empty
10
+ end
11
+
12
+ describe 'IN subquery' do
13
+ it 'should generate an explicit subquery on ID field' do
14
+ Post.filter do
15
+ with(:blog_id).in(Blog.filter { with(:name, 'Test Name') })
16
+ end.inspect
17
+ Post.last_find[:conditions].should ==
18
+ [%q("posts".blog_id IN (SELECT "blogs".id FROM "blogs" WHERE ("blogs".name = 'Test Name') ))]
19
+ end
20
+
21
+ it 'should generate an explicit subquery on selected field' do
22
+ Post.filter do
23
+ with(:blog_id).in(Blog.filter { select(:name) ; with(:name, 'Test Name') })
24
+ end.inspect
25
+ Post.last_find[:conditions].should ==
26
+ [%q("posts".blog_id IN (SELECT "blogs".name FROM "blogs" WHERE ("blogs".name = 'Test Name') ))]
27
+ end
28
+
29
+ it 'should perform a negated subquery' do
30
+ Post.filter do
31
+ with(:blog_id).not.in(Blog.filter { with(:name, 'Test Name') })
32
+ end.inspect
33
+ Post.last_find[:conditions].should ==
34
+ [%q("posts".blog_id NOT IN (SELECT "blogs".id FROM "blogs" WHERE ("blogs".name = 'Test Name') ))]
35
+ end
36
+
37
+ it 'should fail fast if subselect performed with multiple select fields' do
38
+ lambda do
39
+ Post.filter do
40
+ with(:blog_id).in(Blog.filter { select(:id, :name) ; with(:name, 'Test Name') })
41
+ end.inspect
42
+ end.should raise_error(RecordFilter::InvalidFilterException)
43
+ end
44
+ end
45
+
46
+ describe 'equality subquery' do
47
+ it 'should perform subquery and implicitly add limit' do
48
+ Post.filter do
49
+ with(:blog_id, Blog.filter { with(:name, 'Test Name') })
50
+ end.inspect
51
+ Post.last_find[:conditions].should ==
52
+ [%q("posts".blog_id = (SELECT "blogs".id FROM "blogs" WHERE ("blogs".name = 'Test Name') LIMIT 1))]
53
+ end
54
+ end
55
+ end
data/spec/select_spec.rb CHANGED
@@ -6,22 +6,40 @@ describe 'with custom selects for cases where DISTINCT is required' do
6
6
  end
7
7
 
8
8
  describe 'on a standard filter' do
9
- it 'should put nothing in the select' do
9
+ it 'should put nothing in the select by default' do
10
10
  Post.filter do
11
11
  having(:comments).with(:offensive, true)
12
12
  end.inspect
13
13
  Post.last_find[:select].should be_nil
14
14
  end
15
+
16
+ it 'should use the columns specified in the select' do
17
+ Post.filter do
18
+ select(:id, :title)
19
+ having(:comments).with(:offensive, true)
20
+ end.inspect
21
+ Post.last_find[:select].should == '"posts".id, "posts".title'
22
+ end
23
+
24
+ #XXX check bogus column names
15
25
  end
16
26
 
17
- describe 'with join types that require distinct' do
18
- it 'should put the distinct clause in the select' do
19
- [:left, :right].each do |join_type|
27
+ [:left, :right].each do |join_type|
28
+ describe "with #{join_type} join" do
29
+ it 'should put the distinct clause in the select' do
20
30
  Post.filter do
21
31
  having(:comments, :join_type => join_type).with(:offensive, true)
22
32
  end.inspect rescue nil # required because sqlite doesn't support right joins
23
33
  Post.last_find[:select].should == %q(DISTINCT "posts".*)
24
34
  end
35
+
36
+ it 'should put the distinct clause with the columns specified in the select' do
37
+ Post.filter do
38
+ select(:id, :title)
39
+ having(:comments, :join_type => join_type).with(:offensive, true)
40
+ end.inspect rescue nil # required because sqlite doesn't support right joins
41
+ Post.last_find[:select].should == %q(DISTINCT "posts".id, "posts".title)
42
+ end
25
43
  end
26
44
  end
27
45
 
@@ -63,6 +81,25 @@ describe 'with custom selects for cases where DISTINCT is required' do
63
81
  end.inspect
64
82
  Blog.last_find[:select].should == %q(DISTINCT "blogs".*)
65
83
  end
84
+
85
+ it 'should create a distinct query on selected columns' do
86
+ Blog.filter do
87
+ with(:created_at).gt(1.day.ago)
88
+ having(:posts).with(:permalink, nil)
89
+ distinct
90
+ select(:id, :name)
91
+ end.inspect
92
+ Blog.last_find[:select].should == %q(DISTINCT "blogs".id, "blogs".name)
93
+ end
94
+
95
+ it 'should create a distinct query on selected columns passed into distinct()' do
96
+ Blog.filter do
97
+ with(:created_at).gt(1.day.ago)
98
+ having(:posts).with(:permalink, nil)
99
+ distinct(:id, :name)
100
+ end.inspect
101
+ Blog.last_find[:select].should == %q(DISTINCT "blogs".id, "blogs".name)
102
+ end
66
103
  end
67
104
 
68
105
  describe 'using the distinct method when chaining named scopes with joins that do not need it' do
data/spec/test.db CHANGED
Binary file
metadata CHANGED
@@ -3,10 +3,10 @@ name: record_filter
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
+ - 1
6
7
  - 0
7
- - 9
8
- - 17
9
- version: 0.9.17
8
+ - 0
9
+ version: 1.0.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Aubrey Holland
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-03-04 00:00:00 -05:00
18
+ date: 2010-03-18 00:00:00 -04:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -90,6 +90,7 @@ files:
90
90
  - spec/active_record_spec.rb
91
91
  - spec/exception_spec.rb
92
92
  - spec/explicit_join_spec.rb
93
+ - spec/explicit_subquery_spec.rb
93
94
  - spec/implicit_join_spec.rb
94
95
  - spec/limits_and_ordering_spec.rb
95
96
  - spec/models.rb
@@ -138,6 +139,7 @@ test_files:
138
139
  - spec/active_record_spec.rb
139
140
  - spec/exception_spec.rb
140
141
  - spec/explicit_join_spec.rb
142
+ - spec/explicit_subquery_spec.rb
141
143
  - spec/implicit_join_spec.rb
142
144
  - spec/limits_and_ordering_spec.rb
143
145
  - spec/models.rb