brendan-entrails 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+