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.
- data/History.txt +6 -0
- data/Manifest.txt +21 -0
- data/README.txt +54 -0
- data/Rakefile +126 -0
- data/bin/entrails +0 -0
- data/entrails.gemspec +22 -0
- data/init.rb +2 -0
- data/lib/entrails.rb +17 -0
- data/lib/entrails/active_record.rb +5 -0
- data/lib/entrails/active_record/better_conditions.rb +48 -0
- data/lib/entrails/active_record/find_by_association.rb +257 -0
- data/spec/entrails/active_record/better_conditions_spec.rb +48 -0
- data/spec/entrails/active_record/find_by_association_spec.rb +37 -0
- data/spec/spec_helper.rb +14 -0
- metadata +77 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -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
|
data/README.txt
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/bin/entrails
ADDED
File without changes
|
data/entrails.gemspec
ADDED
@@ -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
data/lib/entrails.rb
ADDED
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|