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