brendan-entrails 1.0.1

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.
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2008-06-20
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
@@ -0,0 +1,21 @@
1
+ bin
2
+ bin/entrails
3
+ entrails.gemspec
4
+ History.txt
5
+ init.rb
6
+ lib
7
+ lib/entrails
8
+ lib/entrails/active_record
9
+ lib/entrails/active_record/better_conditions.rb
10
+ lib/entrails/active_record/find_by_association.rb
11
+ lib/entrails/active_record.rb
12
+ lib/entrails.rb
13
+ Manifest.txt
14
+ Rakefile
15
+ README.txt
16
+ spec
17
+ spec/entrails
18
+ spec/entrails/active_record
19
+ spec/entrails/active_record/better_conditions_spec.rb
20
+ spec/entrails/active_record/find_by_association_spec.rb
21
+ spec/spec_helper.rb
@@ -0,0 +1,54 @@
1
+ = entrails =
2
+
3
+ * http://github.com/brendan/entrails
4
+
5
+ == DESCRIPTION:
6
+
7
+ This is a collection of extensions to Rails internals that I've found to be absolutely indispensible
8
+ since I implimented them.
9
+
10
+ The real action is happening in the following two files at the moment:
11
+ http://github.com/brendan/entrails/tree/master/lib/entrails/active_record/better_conditions.rb
12
+ http://github.com/brendan/entrails/tree/master/lib/entrails/active_record/find_by_association.rb= entrails
13
+
14
+ == FEATURES/PROBLEMS:
15
+
16
+ * FIX (list of features or problems)
17
+
18
+ == SYNOPSIS:
19
+
20
+ FIX (code sample of usage)
21
+
22
+ == REQUIREMENTS:
23
+
24
+ * FIX (list of requirements)
25
+
26
+ == INSTALL:
27
+
28
+ * sudo gem install brendan-entrails -s http://gems.github.com
29
+ * require 'entrails' in your rails app
30
+
31
+ == LICENSE:
32
+
33
+ (The MIT License)
34
+
35
+ Copyright (c) 2008 Brendan Baldwin
36
+
37
+ Permission is hereby granted, free of charge, to any person obtaining
38
+ a copy of this software and associated documentation files (the
39
+ 'Software'), to deal in the Software without restriction, including
40
+ without limitation the rights to use, copy, modify, merge, publish,
41
+ distribute, sublicense, and/or sell copies of the Software, and to
42
+ permit persons to whom the Software is furnished to do so, subject to
43
+ the following conditions:
44
+
45
+ The above copyright notice and this permission notice shall be
46
+ included in all copies or substantial portions of the Software.
47
+
48
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
49
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
50
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
51
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
52
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
53
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
54
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
3
+ require 'rubygems'
4
+ require './lib/entrails.rb'
5
+ require 'rake'
6
+ require 'spec/rake/spectask'
7
+
8
+ desc "Generates the gemspec, Manifest.txt and builds the gem."
9
+ task :gem => ["gem:gemspec","gem:manifest"]
10
+
11
+ namespace :gem do
12
+ desc "Generates the entrails.gemspec file"
13
+ task :gemspec do
14
+ gemspec_code = <<-gemspec
15
+ Gem::Specification.new do |s|
16
+ s.name = %q{entrails}
17
+ s.version = #{Entrails::VERSION.inspect}
18
+
19
+ s.specification_version = 2 if s.respond_to? :specification_version=
20
+
21
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
22
+ s.authors = #{Entrails::AUTHORS.inspect}
23
+ s.date = #{Time.now.strftime('%Y-%m-%d').inspect}
24
+ s.default_executable = %q{entrails}
25
+ s.description = #{Entrails::DESCRIPTION.inspect}
26
+ s.email = #{Entrails::EMAIL.inspect}
27
+ s.executables = ["entrails"]
28
+ s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"]
29
+ s.files = #{Entrails::MANIFEST.inspect}
30
+ s.has_rdoc = true
31
+ s.homepage = #{Entrails::HOMEPAGE.inspect}
32
+ s.rdoc_options = ["--main", "README.txt"]
33
+ s.require_paths = ["lib"]
34
+ s.rubygems_version = %q{1.1.1}
35
+ s.summary = #{Entrails::DESCRIPTION.inspect}
36
+ end
37
+ gemspec
38
+ gemspec = File.open(File.join(File.dirname(__FILE__),'entrails.gemspec'),'w')
39
+ gemspec.write(gemspec_code)
40
+ gemspec.close
41
+ end
42
+ desc "Generates the Manifest.txt file"
43
+ task :manifest do
44
+ manifest = File.open(File.join(File.dirname(__FILE__),'Manifest.txt'),'w')
45
+ manifest.write(Entrails::MANIFEST.join("\n")<<"\n")
46
+ manifest.close
47
+ end
48
+ end
49
+
50
+ desc "Run all specs"
51
+ Spec::Rake::SpecTask.new do |t|
52
+ t.spec_files = FileList['spec/**/*_spec.rb']
53
+ end
54
+
55
+ namespace :spec do
56
+ desc "Run all specs and stake out all files/folders for changes, running rake spec upon change"
57
+ task :auto => 'spec' do
58
+
59
+ # This task assumes you are running on a Mac, with growl installed. Perhaps
60
+ # there will be a better way to abstract this, but it works for us now.
61
+ def growl(title, msg, img, pri=0, sticky="")
62
+ system "growlnotify -n autotest --image ~/.autotest_images/#{img} -p #{pri} -m #{msg.inspect} #{title} #{sticky}"
63
+ end
64
+ def self.growl_fail(output)
65
+ growl "FAIL", "#{output}", "fail.png", 2
66
+ end
67
+ def self.growl_pass(output)
68
+ growl "Pass", "#{output}", "pass.png"
69
+ end
70
+
71
+ command = 'rake spec'
72
+ files = {}
73
+
74
+ def files.reglob
75
+ Dir['**/*.rb'].each {|file| self[file] ||= File.mtime(file)}
76
+ end
77
+
78
+ files.reglob
79
+
80
+ puts "Watching #{files.keys.join(', ')}\n\nFiles: #{files.keys.length}"
81
+
82
+ trap('INT') do
83
+ puts "\nQuitting..."
84
+ exit
85
+ end
86
+
87
+ loop do
88
+
89
+ sleep 3
90
+
91
+ files.reglob
92
+
93
+ changed_file, last_changed = files.find do |file, last_changed|
94
+ begin
95
+ File.mtime(file) > last_changed
96
+ rescue
97
+ files.delete(file)
98
+ end
99
+ end
100
+
101
+ if changed_file
102
+ files[changed_file] = File.mtime(changed_file)
103
+ puts "=> #{changed_file} changed, running #{command}"
104
+ results = `#{command}`
105
+ puts results
106
+
107
+ if results.include? 'tests'
108
+ output = results.slice(/(\d+)\s+tests?,\s*(\d+)\s+assertions?,\s*(\d+)\s+failures?(,\s*(\d+)\s+errors)?/)
109
+ if output
110
+ $~[3].to_i + $~[5].to_i > 0 ? growl_fail(output) : growl_pass(output)
111
+ end
112
+ else
113
+ output = results.slice(/(\d+)\s+examples?,\s*(\d+)\s+failures?(,\s*(\d+)\s+not implemented)?/)
114
+ if output
115
+ $~[2].to_i > 0 ? growl_fail(output) : growl_pass(output)
116
+ end
117
+ end
118
+ # TODO Generic growl notification for other actions
119
+
120
+ puts "=> done"
121
+ end
122
+
123
+ end
124
+
125
+ end
126
+ end
File without changes
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{entrails}
3
+ s.version = "1.0.1"
4
+
5
+ s.specification_version = 2 if s.respond_to? :specification_version=
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Brendan Baldwin"]
9
+ s.date = "2008-06-20"
10
+ s.default_executable = %q{entrails}
11
+ s.description = "This is a collection of extensions to Rails internals that I've found to be absolutely indispensible since I implimented them. The real action is happening in the following two files at the moment: http://github.com/brendan/entrails/tree/master/lib/entrails/active_record/better_conditions.rb http://github.com/brendan/entrails/tree/master/lib/entrails/active_record/find_by_association.rb"
12
+ s.email = ["brendan@usergenic.com"]
13
+ s.executables = ["entrails"]
14
+ s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"]
15
+ s.files = ["bin", "bin/entrails", "entrails.gemspec", "History.txt", "init.rb", "lib", "lib/entrails", "lib/entrails/active_record", "lib/entrails/active_record/better_conditions.rb", "lib/entrails/active_record/find_by_association.rb", "lib/entrails/active_record.rb", "lib/entrails.rb", "Manifest.txt", "Rakefile", "README.txt", "spec", "spec/entrails", "spec/entrails/active_record", "spec/entrails/active_record/better_conditions_spec.rb", "spec/entrails/active_record/find_by_association_spec.rb", "spec/spec_helper.rb"]
16
+ s.has_rdoc = true
17
+ s.homepage = "http://github.com/brendan/entrails"
18
+ s.rdoc_options = ["--main", "README.txt"]
19
+ s.require_paths = ["lib"]
20
+ s.rubygems_version = %q{1.1.1}
21
+ s.summary = "This is a collection of extensions to Rails internals that I've found to be absolutely indispensible since I implimented them. The real action is happening in the following two files at the moment: http://github.com/brendan/entrails/tree/master/lib/entrails/active_record/better_conditions.rb http://github.com/brendan/entrails/tree/master/lib/entrails/active_record/find_by_association.rb"
22
+ end
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'entrails'
2
+ Entrails.apply
@@ -0,0 +1,17 @@
1
+ module Entrails
2
+ VERSION='1.0.1'
3
+ AUTHORS=["Brendan Baldwin"]
4
+ EMAIL=["brendan@usergenic.com"]
5
+ DESCRIPTION=%q{This is a collection of extensions to Rails internals that I've found to be absolutely indispensible since I implimented them. The real action is happening in the following two files at the moment: http://github.com/brendan/entrails/tree/master/lib/entrails/active_record/better_conditions.rb http://github.com/brendan/entrails/tree/master/lib/entrails/active_record/find_by_association.rb}
6
+ FOLDER=File.expand_path(File.join(File.dirname(__FILE__),'..'))
7
+ MANIFEST=Dir.glob(File.join(FOLDER,'**','*')).map{|path|path[FOLDER.size+1..-1]}
8
+ HOMEPAGE="http://github.com/brendan/entrails"
9
+ class << self
10
+ def apply
11
+ ::ActiveRecord::Base.extend ::Entrails::ActiveRecord::BetterConditions
12
+ ::ActiveRecord::Base.extend ::Entrails::ActiveRecord::FindByAssociation
13
+ end
14
+ end
15
+ end
16
+
17
+ require 'entrails/active_record'
@@ -0,0 +1,5 @@
1
+ module Entrails::ActiveRecord
2
+ end
3
+
4
+ require 'entrails/active_record/better_conditions'
5
+ require 'entrails/active_record/find_by_association'
@@ -0,0 +1,48 @@
1
+ module Entrails::ActiveRecord::BetterConditions
2
+
3
+ protected
4
+
5
+ # Allows arrays to represent logical symbolic expressions by use of the
6
+ # symbols :and, :or, and :not and the embedding of nested array and hash
7
+ # conditions.
8
+ def sanitize_sql_array_with_better_conditions(array)
9
+ conditions = []
10
+ joiner = 'OR'
11
+ negate = false
12
+ index = 0
13
+ array.each_with_index do |element, index|
14
+ case element
15
+ when :and, 'and' : joiner = 'AND'
16
+ when :or, 'or' : joiner = 'OR'
17
+ when :not, 'not' : negate = !negate
18
+ else
19
+ # following matches pattern for ActiveRecord's built-in array support
20
+ return sanitize_sql_array_without_better_conditions(array) if index == 0 and element.is_a?(String)
21
+ conditions << sanitize_sql(element)
22
+ end
23
+ end
24
+ return if conditions.empty?
25
+ sql = (conditions.size==1) ? conditions.first : "(#{conditions.join(") #{joiner} (")})"
26
+ sql = "NOT (#{sql})" if negate
27
+ sql
28
+ end
29
+
30
+ # Allows true or false to be used as a convenience to short-circuit conditional
31
+ # branches.
32
+ def sanitize_sql_with_better_conditions(object)
33
+ case object
34
+ when true : '1=1'
35
+ when false : '1=0'
36
+ else sanitize_sql_without_better_conditions(object)
37
+ end
38
+ end
39
+
40
+ def self.extended(host)
41
+ super
42
+ class << host
43
+ alias_method_chain :sanitize_sql, :better_conditions
44
+ alias_method_chain :sanitize_sql_array, :better_conditions
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,257 @@
1
+ module Entrails::ActiveRecord::FindByAssociation
2
+
3
+ protected
4
+
5
+ # Sanitizes a hash of association/value pairs into SQL conditions,
6
+ # passing on attributes that are not associations back down into
7
+ # the original sanitize_sql_hash method.
8
+ def sanitize_sql_hash_with_find_by_association(attrs)
9
+ attrs = attrs.dup
10
+ association_conditions = attrs.map do |attr, value|
11
+ # Lets process Attributes that are names of Associations
12
+ if association = reflect_on_association(attr.to_sym)
13
+ attrs.delete attr
14
+ if association.options[:polymorphic]
15
+ foreign_type_attribute = (association.options[:foreign_type]||"#{association.name}_type").to_s
16
+ polymorphic_type = attrs[foreign_type_attribute.to_sym]
17
+ raise "Polymorphic belongs_to associations must be qualified by the inclusion of a reference to the foreign_type attribute in the conditions hash. Missing #{foreign_type_attribute} key or value in conditions hash passed to sanitize_sql_hash_with_find_by_association for association #{association.name}." unless polymorphic_type
18
+ association_class = polymorphic_type.constantize
19
+ else
20
+ association_class = association.klass
21
+ end
22
+ construct_find_by_association_conditions_sql(association, value, :type => association_class)
23
+ end
24
+ end.compact.join(' AND ')
25
+ non_association_conditions = attrs.empty? ? '' : sanitize_sql_hash_without_find_by_association(attrs)
26
+
27
+ # Non Association Conditions are cheaper to calculate so we optimize the query plan by issuing them first
28
+ [ non_association_conditions, association_conditions ].reject{|conditions| conditions.blank?}.join(' AND ')
29
+ end
30
+
31
+ # Prevent infinite recursion when assigning scopes via with_scope block
32
+ # by sanitizing non-string scope conditions.
33
+ def with_scope_with_find_by_association(options={}, action = :merge, &block)
34
+ if options[:find] and options[:find][:conditions] and !options[:find][:conditions].is_a?(String)
35
+ options = options.dup
36
+ options[:find] = options[:find].dup
37
+ options[:find][:conditions] = sanitize_sql(options[:find][:conditions])
38
+ end
39
+ with_scope_without_find_by_association options, action, &block
40
+ end
41
+
42
+ private
43
+
44
+ def construct_find_by_association_conditions_sql(association, conditions, options={})
45
+
46
+ association_type = options[:type]
47
+ association = reflect_on_association(association) unless association.is_a?(ActiveRecord::Reflection::AssociationReflection)
48
+
49
+ association.options[:polymorphic] and !association_type and
50
+ raise "FindByAssociation requires a :type option for generation of polymorphic belongs_to association subqueries."
51
+
52
+ association_type ||= association.is_a?(String) ? association_type.constantize : association.klass
53
+
54
+ # If a nil is present in the association conditions we have to handle as a special case due to the way
55
+ # sql handles NULL values in sets vs. empty sets.
56
+ nil_condition =
57
+ case conditions
58
+ when Array : !!conditions.compact! # compacts the array and returns true/false if it changed
59
+ when NilClass : true
60
+ else false
61
+ end
62
+
63
+ segments = []
64
+
65
+ # To handle has_many :through, lets process the through_reflection and then send over the conditions...
66
+ conditions.blank? or segments <<
67
+ if association.options[:through]
68
+ through_association = reflect_on_association(association.through_reflection.name)
69
+ source_association ||= through_association.klass.reflect_on_association(association.options[:source])
70
+ source_association ||= through_association.klass.reflect_on_association(association.name)
71
+ source_association ||= through_association.klass.reflect_on_association(association.name.to_s.singularize.to_sym)
72
+
73
+ raise "Unknown source_association for HasManyThroughAssociation #{self}##{association.name}." unless source_association
74
+
75
+ source_subquery_sql = through_association.klass.__send__(:construct_find_by_association_conditions_subquery_sql,
76
+ source_association, conditions, :type => association_type)
77
+
78
+ through_subquery_sql = construct_find_by_association_conditions_subquery_sql(
79
+ through_association, source_subquery_sql, :type => through_association.klass)
80
+ else
81
+ construct_find_by_association_conditions_subquery_sql(association, conditions, :type => association_type)
82
+ end
83
+
84
+ nil_condition and segments << construct_find_by_association_conditions_nil_condition_sql(association)
85
+
86
+ segments.join(' OR ') unless segments.empty?
87
+
88
+ end
89
+
90
+ def construct_find_by_association_conditions_nil_condition_sql(association)
91
+ nil_subquery_sql =
92
+ if association.options[:through]
93
+ through_association = reflect_on_association(association.through_reflection.name)
94
+ through_association.klass.__send__(:construct_finder_sql, options_for_find_by_association_conditions_subquery(through_association, nil))
95
+ else
96
+ association.klass.__send__(:construct_finder_sql, options_for_find_by_association_conditions_subquery(association, nil, :type => association.klass))
97
+ end
98
+
99
+ nil_subquery_sql = "SELECT _id FROM (#{nil_subquery_sql}) _tmp" if use_derived_table_hack_for_subquery_optimization?
100
+
101
+ case association.macro
102
+ when :belongs_to : "#{table_name}.#{connection.quote_column_name(association.primary_key_name)} NOT IN " <<
103
+ "(#{nil_subquery_sql}) OR #{table_name}.#{connection.quote_column_name(association.primary_key_name)} IS NULL"
104
+ when :has_one : "#{table_name}.#{connection.quote_column_name(primary_key)} NOT IN (#{nil_subquery_sql})"
105
+ when :has_many : "#{table_name}.#{connection.quote_column_name(primary_key)} NOT IN (#{nil_subquery_sql})"
106
+ when :has_and_belongs_to_many : "#{table_name}.#{connection.quote_column_name(primary_key)} NOT IN " <<
107
+ "(SELECT #{connection.quote_column_name(association.primary_key_name)} " <<
108
+ "FROM #{association.options[:join_table]} WHERE " <<
109
+ "#{connection.quote_column_name(association.association_foreign_key)} IN " <<
110
+ "(#{nil_subquery_sql}))"
111
+ else raise "FindByAssociation does not recognize the '#{association.macro}' association macro."
112
+ end
113
+
114
+ end
115
+
116
+ def construct_find_by_association_conditions_subquery_sql(association, conditions, options={})
117
+
118
+ association_type, through_type = options[:type], options[:through_type]
119
+ association = reflect_on_association(association) unless association.is_a?(ActiveRecord::Reflection::AssociationReflection)
120
+
121
+ association.options[:polymorphic] and !association_type and
122
+ raise "FindByAssociation requires a :type option for generation of polymorphic belongs_to association subqueries."
123
+
124
+ association_type ||= association_type.is_a?(String) ? association_type.constantize : association.klass
125
+
126
+ conditions =
127
+ case
128
+ when (conditions == []) : nil
129
+ when (conditions.class.is_a?(Entrails::ActiveRecord::FindByAssociation)) : [conditions]
130
+ else conditions
131
+ end
132
+
133
+ if conditions.is_a?(Array) and conditions.all?{|c|c.class.is_a?(Entrails::ActiveRecord::FindByAssociation)}
134
+ ids = conditions.map{|c| c.attributes[association_type.primary_key] }
135
+ ids = ids.first unless ids.size > 1
136
+ conditions = { association_type.primary_key => ids }
137
+ end
138
+
139
+ if conditions and subquery_sql = association_type.__send__(:construct_finder_sql,
140
+ options_for_find_by_association_conditions_subquery(association, conditions, :type => association_type, :through_type => through_type))
141
+
142
+ subquery_sql &&= "SELECT _id FROM (#{subquery_sql}) _tmp" if use_derived_table_hack_for_subquery_optimization?
143
+
144
+ case association.macro
145
+ when :belongs_to : "#{table_name}.#{connection.quote_column_name(association.primary_key_name)} IN (#{subquery_sql})"
146
+ when :has_and_belongs_to_many : "#{table_name}.#{connection.quote_column_name(primary_key)} IN " <<
147
+ "(SELECT #{connection.quote_column_name(association.primary_key_name)} " <<
148
+ "FROM #{association.options[:join_table]} WHERE " <<
149
+ "#{connection.quote_column_name(association.association_foreign_key)} IN " <<
150
+ "(#{subquery_sql}))"
151
+ when :has_one : "#{table_name}.#{connection.quote_column_name(primary_key)} IN (#{subquery_sql})"
152
+ when :has_many : "#{table_name}.#{connection.quote_column_name(primary_key)} IN (#{subquery_sql})"
153
+ else raise "Unrecognized Association Macro '#{association.macro}' not supported by FindByAssociation."
154
+ end
155
+
156
+ end
157
+ end
158
+
159
+ # Update the dynamic finders to allow referencing association names instead of
160
+ # just column names.
161
+ def method_missing_with_find_by_association(method_id, *arguments)
162
+ match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s)
163
+ match = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(method_id.to_s) unless match
164
+ if match
165
+ action_type = ($1 =~ /by/) ? :finder : :instantiator
166
+ attribute_names = extract_attribute_names_from_match(match)
167
+ options_argument = (arguments.size > attribute_names.size) ? arguments.last : {}
168
+ associations = {}
169
+ index = 0
170
+
171
+ non_associations = attribute_names.select do |attribute_name|
172
+ attribute_chain = attribute_name.split('_having_')
173
+ attribute_name = attribute_chain.shift
174
+ if reflect_on_association(attribute_name.to_sym)
175
+ associations[attribute_name.to_sym] ||= attribute_chain.reverse.inject(arguments.delete_at(index)){|v,n|{n=>v}}
176
+ false
177
+ else
178
+ index += 1
179
+ true
180
+ end
181
+ end
182
+
183
+ unless associations.empty?
184
+ find_options = { :conditions => associations }
185
+ set_readonly_option!(find_options)
186
+ with_scope :find => find_options do
187
+ if action_type == :finder
188
+ finder = determine_finder(match)
189
+ return __send__(finder, options_argument) if non_associations.empty?
190
+ return __send__("find#{'_all' if finder == :find_every}_by_#{non_associations.join('_and_')}".to_sym, *arguments)
191
+ else
192
+ instantiator = determine_instantiator(match)
193
+ return find_initial(options_argument) || __send__(instantiator, associations) if non_associations.empty?
194
+ return __send__("find_or_#{instantiator}_by_#{non_associations.join('_and_')}".to_sym, *arguments)
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ method_missing_without_find_by_association method_id, *arguments
201
+
202
+ end
203
+
204
+ def options_for_find_by_association_conditions_subquery(association, conditions, options={})
205
+
206
+ association_type, through_type = options[:type], options[:through_type]
207
+ association = reflect_on_association(association) unless association.is_a? ActiveRecord::Reflection::AssociationReflection
208
+
209
+ association.options[:polymorphic] and !association_type and
210
+ raise "Polymorphic belongs_to associations require the :type argument for options_for_find_by_association_conditions_subquery."
211
+
212
+ association_type ||= association_type.is_a?(String) ? association_type.constantize : association.klass
213
+
214
+ options = {}
215
+
216
+ key_column = case association.macro
217
+ when :belongs_to,
218
+ :has_and_belongs_to_many : association_type.primary_key
219
+ else association.primary_key_name
220
+ end
221
+
222
+ options[:select] = "#{key_column} _id"
223
+ segments = []
224
+ segments << "#{key_column} IS NOT NULL"
225
+ conditions and segments << association_type.__send__(:sanitize_sql, conditions)
226
+ association.options[:conditions] and segments << association_type.__send__(:sanitize_sql, association.options[:conditions])
227
+ association.options[:as] and segments << association_type.__send__(:sanitize_sql, (association_type.reflect_on_association(association.options[:as].to_sym).options[:foreign_type] || :"#{association.options[:as]}_type").to_sym => (through_type||self).name)
228
+ segments.reject! {|c|c.blank?}
229
+ options[:conditions] = segments.size > 1 ? "(#{segments.join(') AND (')})" : segments.first unless segments.empty?
230
+
231
+ # subqueries in MySQL can not use order or limit
232
+ # options[:order] = association.options[:order] if association.options[:order]
233
+ # options[:limit] = association.options[:limit] if association.options[:limit]
234
+
235
+ options
236
+
237
+ end
238
+
239
+ # This is an affordance to turn on/off the use of a wrapper query that generates
240
+ # an aliased derived table for the purpose of query-plan optimization for some
241
+ # database engines. This hack has been shown to significantly benefit query times
242
+ # for mysql and sqlite3. (has not yet been tested with other engines.)
243
+ def use_derived_table_hack_for_subquery_optimization?
244
+ true
245
+ end
246
+
247
+ def self.extended(host)
248
+ super
249
+ class << host
250
+ alias_method :sanitize_sql_hash_for_conditions, :sanitize_sql_hash_with_find_by_association
251
+ alias_method_chain :method_missing, :find_by_association
252
+ alias_method_chain :sanitize_sql_hash, :find_by_association
253
+ alias_method_chain :with_scope, :find_by_association
254
+ end
255
+ end
256
+
257
+ end
@@ -0,0 +1,48 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+
3
+ describe Entrails::ActiveRecord::BetterConditions do
4
+ def base
5
+ @base ||= Class.new do
6
+ stub!(:sanitize_sql).and_return('sql')
7
+ stub!(:sanitize_sql_array).and_return('sql')
8
+ extend Entrails::ActiveRecord::BetterConditions
9
+ end
10
+ end
11
+ describe '#sanitize_sql_array_with_better_conditions' do
12
+ it "returns sql representing the intersection of sub-expressions when array contains the logical :and prefix" do
13
+ base.class_eval{ sanitize_sql_array_with_better_conditions([:and, 'exp1', 'exp2']) }.should == '(sql) AND (sql)'
14
+ end
15
+ it "returns sql representing the union of sub-expressions when array contains the logical :or prefix" do
16
+ base.class_eval{ sanitize_sql_array_with_better_conditions([:or, 'exp1', 'exp2']) }.should == '(sql) OR (sql)'
17
+ end
18
+ it "returns sql representing the negation of sub-expressions when array contains the logical :not prefix" do
19
+ base.class_eval{ sanitize_sql_array_with_better_conditions([:not, 'exp']) }.should == 'NOT (sql)'
20
+ end
21
+ it "returns sql representing the negation of the intersection of sub-expressions when array contains the logical :not and :and prefix" do
22
+ base.class_eval{ sanitize_sql_array_with_better_conditions([:not, :and, 'exp1', 'exp2']) }.should == 'NOT ((sql) AND (sql))'
23
+ end
24
+ it "returns sql representing the negation of the union of sub-expressions when array contains the logical :not and :or prefix" do
25
+ base.class_eval{ sanitize_sql_array_with_better_conditions([:not, :or, 'exp1', 'exp2']) }.should == 'NOT ((sql) OR (sql))'
26
+ end
27
+ it "will delegate to sanitize_sql_array_without_better_conditions if the first element is not one of the logical prefixes, Array, or Hash" do
28
+ base.should_receive(:sanitize_sql_array_without_better_conditions).with(['conditions']).and_return('sql')
29
+ base.class_eval{ sanitize_sql_array_with_better_conditions(['conditions']) }.should == 'sql'
30
+ end
31
+ it "will delegate sub-expressions to sanitize_sql" do
32
+ base.should_receive(:sanitize_sql).with('exp').and_return('sanitized')
33
+ base.class_eval{ sanitize_sql_array_with_better_conditions([:not,'exp']) }.should == 'NOT (sanitized)'
34
+ end
35
+ end
36
+ describe '#sanitize_sql_with_better_conditions' do
37
+ it "returns 1=1 for true" do
38
+ base.class_eval{ sanitize_sql_with_better_conditions(true) }.should == '1=1'
39
+ end
40
+ it "returns 1=0 for false" do
41
+ base.class_eval{ sanitize_sql_with_better_conditions(false) }.should == '1=0'
42
+ end
43
+ it "calls the sanitize_sql_without_better_conditions for values other than true and false" do
44
+ base.should_receive(:sanitize_sql_without_better_conditions).with('other').and_return('expected')
45
+ base.class_eval{ sanitize_sql_with_better_conditions('other') }.should == 'expected'
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,37 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+
3
+ describe Entrails::ActiveRecord::FindByAssociation do
4
+
5
+ def base
6
+ @base ||= Class.new do
7
+ stub!(:with_scope).and_return(nil)
8
+ stub!(:sanitize_sql).and_return('sql')
9
+ stub!(:sanitize_sql_array).and_return('sql')
10
+ stub!(:sanitize_sql_hash).and_return('sql')
11
+ extend Entrails::ActiveRecord::FindByAssociation
12
+ end
13
+ end
14
+
15
+ describe "#construct_find_by_association_conditions_sql" do
16
+ it "delegates to construct_find_by_association_conditions_nil_condition_sql when conditions is nil"
17
+ it "delegates to construct_find_by_association_conditions_nil_condition_sql when conditions is an empty Array"
18
+ it "delegates to construct_find_by_association_conditions_nil_condition_sql when conditions is an Array that contains a nil"
19
+ it "does not delegate to construct_find_by_association_conditions_subquery_sql when conditions is nil"
20
+ it "does not delegate to construct_find_by_association_conditions_subquery_sql when conditions is an empty Array"
21
+ it "does not delegate to construct_find_by_association_conditions_subquery_sql when conditions is an Array that contains only nils"
22
+ end
23
+
24
+ describe "#sanitize_sql_hash_with_find_by_association" do
25
+ it "calls original sanitize_sql_hash with a hash containing all keys that are *not* association names"
26
+ it "calls construct_find_by_association_sql for each key that is an association name"
27
+ it "calls reflect_on_association for each key to determine whether a it is an association name" do
28
+ mock_association = mock('association', :options => {}, :klass => 'class')
29
+ base.should_receive(:construct_find_by_association_conditions_sql).with(mock_association, 'conditions', :type => 'class')
30
+ base.should_receive(:reflect_on_association).with(:friends).exactly(:once).and_return(mock_association)
31
+ base.should_receive(:reflect_on_association).with(:name).exactly(:once).and_return(nil)
32
+ hash = { :name => 'alice', :friends => 'conditions' }
33
+ base.__send__(:sanitize_sql_hash_with_find_by_association, hash)
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,14 @@
1
+ ENV['LOG_NAME'] = 'spec'
2
+
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
4
+
5
+ begin
6
+ require 'spec'
7
+ require 'activesupport'
8
+ rescue LoadError
9
+ require 'rubygems'
10
+ require 'spec'
11
+ require 'activesupport'
12
+ end
13
+
14
+ require 'entrails'
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brendan-entrails
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Brendan Baldwin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-06-20 00:00:00 -07:00
13
+ default_executable: entrails
14
+ dependencies: []
15
+
16
+ description: "This is a collection of extensions to Rails internals that I've found to be absolutely indispensible since I implimented them. The real action is happening in the following two files at the moment: http://github.com/brendan/entrails/tree/master/lib/entrails/active_record/better_conditions.rb http://github.com/brendan/entrails/tree/master/lib/entrails/active_record/find_by_association.rb"
17
+ email:
18
+ - brendan@usergenic.com
19
+ executables:
20
+ - entrails
21
+ extensions: []
22
+
23
+ extra_rdoc_files:
24
+ - History.txt
25
+ - Manifest.txt
26
+ - README.txt
27
+ files:
28
+ - bin
29
+ - bin/entrails
30
+ - entrails.gemspec
31
+ - History.txt
32
+ - init.rb
33
+ - lib
34
+ - lib/entrails
35
+ - lib/entrails/active_record
36
+ - lib/entrails/active_record/better_conditions.rb
37
+ - lib/entrails/active_record/find_by_association.rb
38
+ - lib/entrails/active_record.rb
39
+ - lib/entrails.rb
40
+ - Manifest.txt
41
+ - Rakefile
42
+ - README.txt
43
+ - spec
44
+ - spec/entrails
45
+ - spec/entrails/active_record
46
+ - spec/entrails/active_record/better_conditions_spec.rb
47
+ - spec/entrails/active_record/find_by_association_spec.rb
48
+ - spec/spec_helper.rb
49
+ has_rdoc: true
50
+ homepage: http://github.com/brendan/entrails
51
+ post_install_message:
52
+ rdoc_options:
53
+ - --main
54
+ - README.txt
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ requirements: []
70
+
71
+ rubyforge_project:
72
+ rubygems_version: 1.0.1
73
+ signing_key:
74
+ specification_version: 2
75
+ summary: "This is a collection of extensions to Rails internals that I've found to be absolutely indispensible since I implimented them. The real action is happening in the following two files at the moment: http://github.com/brendan/entrails/tree/master/lib/entrails/active_record/better_conditions.rb http://github.com/brendan/entrails/tree/master/lib/entrails/active_record/find_by_association.rb"
76
+ test_files: []
77
+