methodmissing-scrooge 2.2.3 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +6 -4
- data/Rakefile +3 -1
- data/VERSION.yml +3 -3
- data/lib/callsite.rb +34 -22
- data/lib/optimizations/associations/macro.rb +39 -76
- data/lib/optimizations/columns/attributes_proxy.rb +2 -4
- data/lib/optimizations/columns/macro.rb +28 -17
- data/lib/optimizations/result_sets/updateable_result_set.rb +21 -1
- data/lib/scrooge.rb +4 -4
- data/test/callsite_test.rb +41 -0
- data/test/helper.rb +5 -3
- data/test/models/mysql_column_privilege.rb +7 -0
- data/test/models/mysql_host.rb +5 -0
- data/test/models/mysql_table_privilege.rb +8 -0
- data/test/models/mysql_user.rb +5 -0
- data/test/optimizations/associations/macro_test.rb +27 -0
- data/test/scrooge_helper.rb +12 -0
- data/test/scrooge_test.rb +12 -4
- data/test/setup.rb +1 -1
- metadata +11 -3
data/README.textile
CHANGED
@@ -72,7 +72,7 @@ h4. As a Gem
|
|
72
72
|
|
73
73
|
h2. Stability
|
74
74
|
|
75
|
-
The whole ActiveRecord test suite passes with scrooge, except for
|
75
|
+
The whole Rails 2.3.2 ActiveRecord test suite passes with scrooge, except for 13 failures related to callsite augmentation (note the SQL reload snippets below). Thoughts on handling or circumventing this much appreciated.
|
76
76
|
|
77
77
|
<pre>
|
78
78
|
<code>
|
@@ -203,7 +203,7 @@ on subsequent requests :
|
|
203
203
|
|
204
204
|
h4. How it tracks
|
205
205
|
|
206
|
-
The ActiveRecord attributes Hash is replaced with a proxy that automatically augments the callsite with any attributes referenced through the Hash lookup keys.We're also able to learn which associations is invoked from a given callsite, for preloading on subsequent requests.
|
206
|
+
The ActiveRecord attributes Hash is replaced with a proxy that automatically augments the callsite with any attributes referenced through the Hash lookup keys. We're also able to learn which associations is invoked from a given callsite, for preloading on subsequent requests.
|
207
207
|
|
208
208
|
h4. Storage
|
209
209
|
|
@@ -221,12 +221,14 @@ The tracking and scoping phases is superseded by this implementation - none of t
|
|
221
221
|
|
222
222
|
h2. Todo
|
223
223
|
|
224
|
-
* Deeper coverage for Scrooge::AttributesProxy
|
224
|
+
* Deeper coverage for Scrooge::AttributesProxy, possible handling of replace
|
225
225
|
|
226
|
-
*
|
226
|
+
* More test cases for Scrooge::Callsite
|
227
227
|
|
228
228
|
* Have invoking Model#attributes not associate all columns with the callsite
|
229
229
|
|
230
230
|
* Avoid possible missing attribute exceptions for destroyed objects
|
231
231
|
|
232
|
+
* Track rows of result set to allow more targeted loading of associations for a callsite
|
233
|
+
|
232
234
|
(c) 2009 Lourens Naudé (methodmissing) and Stephen Sykes (sdsykes)
|
data/Rakefile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# coding: UTF-8
|
2
|
+
|
1
3
|
require 'rake'
|
2
4
|
require 'rake/testtask'
|
3
5
|
require 'test/helper'
|
@@ -24,7 +26,7 @@ begin
|
|
24
26
|
Jeweler::Tasks.new do |s|
|
25
27
|
s.name = "scrooge"
|
26
28
|
s.summary = "Scrooge - Fetch exactly what you need"
|
27
|
-
s.email = "lourens@methodmissing.com or
|
29
|
+
s.email = "lourens@methodmissing.com or sdsykes@gmail.com"
|
28
30
|
s.homepage = "http://github.com/methodmissing/scrooge"
|
29
31
|
s.description = "An ActiveRecord attribute tracker to ensure production
|
30
32
|
Ruby applications only fetch the database content needed to minimize wire traffic
|
data/VERSION.yml
CHANGED
@@ -1,4 +1,4 @@
|
|
1
1
|
---
|
2
|
-
:
|
3
|
-
:
|
4
|
-
:
|
2
|
+
:major: 3
|
3
|
+
:minor: 0
|
4
|
+
:patch: 0
|
data/lib/callsite.rb
CHANGED
@@ -2,10 +2,10 @@ module Scrooge
|
|
2
2
|
class Callsite
|
3
3
|
|
4
4
|
# Represents a Callsite and is a container for any columns and
|
5
|
-
# associations
|
5
|
+
# associations referenced at the callsite.
|
6
6
|
#
|
7
7
|
|
8
|
-
|
8
|
+
Mtx = Mutex.new
|
9
9
|
|
10
10
|
attr_accessor :klass,
|
11
11
|
:signature,
|
@@ -15,40 +15,52 @@ module Scrooge
|
|
15
15
|
def initialize( klass, signature )
|
16
16
|
@klass = klass
|
17
17
|
@signature = signature
|
18
|
-
@columns = setup_columns
|
19
|
-
@associations = setup_associations
|
20
18
|
end
|
21
19
|
|
22
20
|
# Flag a column as seen
|
23
21
|
#
|
24
22
|
def column!( column )
|
25
|
-
|
26
|
-
|
23
|
+
Mtx.synchronize do
|
24
|
+
columns << column
|
27
25
|
end
|
28
26
|
end
|
29
27
|
|
30
|
-
# Diff known associations with given includes
|
31
|
-
#
|
32
|
-
def preload( includes )
|
33
|
-
# Ignore nested includes for the time being
|
34
|
-
#
|
35
|
-
if includes.is_a?(Hash)
|
36
|
-
includes
|
37
|
-
else
|
38
|
-
@associations.merge( Array(includes) ).to_a
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
28
|
# Flag an association as seen
|
43
29
|
#
|
44
30
|
def association!( association )
|
45
|
-
|
46
|
-
|
31
|
+
Mtx.synchronize do
|
32
|
+
associations << association if preloadable_association?( association )
|
47
33
|
end
|
48
34
|
end
|
49
35
|
|
36
|
+
def inspect
|
37
|
+
"<##{@klass.name} :select => '#{@klass.scrooge_select_sql( columns )}', :include => [#{associations_for_inspect}]>"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Lazy init default columns
|
41
|
+
#
|
42
|
+
def default_columns
|
43
|
+
@default_columns ||= setup_columns
|
44
|
+
end
|
45
|
+
|
46
|
+
# Lazy init columns
|
47
|
+
#
|
48
|
+
def columns
|
49
|
+
@columns ||= default_columns.dup
|
50
|
+
end
|
51
|
+
|
52
|
+
# Lazy init associations
|
53
|
+
#
|
54
|
+
def associations
|
55
|
+
@associations ||= setup_associations
|
56
|
+
end
|
57
|
+
|
50
58
|
private
|
51
59
|
|
60
|
+
def associations_for_inspect
|
61
|
+
associations.map{|a| ":#{a.to_s}" }.join(', ')
|
62
|
+
end
|
63
|
+
|
52
64
|
# Only register associations that isn't polymorphic or a collection
|
53
65
|
#
|
54
66
|
def preloadable_association?( association )
|
@@ -68,11 +80,11 @@ module Scrooge
|
|
68
80
|
if inheritable?
|
69
81
|
Set.new([primary_key, inheritance_column])
|
70
82
|
else
|
71
|
-
Set.new([primary_key])
|
83
|
+
primary_key.blank? ? Set.new : Set.new([primary_key])
|
72
84
|
end
|
73
85
|
end
|
74
86
|
|
75
|
-
#
|
87
|
+
# Start with no registered associations
|
76
88
|
#
|
77
89
|
def setup_associations
|
78
90
|
Set.new
|
@@ -8,16 +8,16 @@ module Scrooge
|
|
8
8
|
# Inject into ActiveRecord
|
9
9
|
#
|
10
10
|
def install!
|
11
|
-
|
11
|
+
unless scrooge_installed?
|
12
12
|
ActiveRecord::Base.send( :extend, SingletonMethods )
|
13
|
-
ActiveRecord::
|
13
|
+
ActiveRecord::Associations::AssociationProxy.send( :include, InstanceMethods )
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
|
17
|
+
protected
|
18
18
|
|
19
|
-
def
|
20
|
-
|
19
|
+
def scrooge_installed?
|
20
|
+
ActiveRecord::Associations::AssociationProxy.included_modules.include?( InstanceMethods )
|
21
21
|
end
|
22
22
|
|
23
23
|
end
|
@@ -27,99 +27,62 @@ module Scrooge
|
|
27
27
|
module SingletonMethods
|
28
28
|
|
29
29
|
@@preloadable_associations = {}
|
30
|
-
FindAssociatedRegex = /find_associated_records/
|
31
30
|
|
32
31
|
def self.extended( base )
|
33
32
|
eigen = class << base; self; end
|
34
|
-
|
35
|
-
# Let :scrooge_callsite be a valid find option
|
36
|
-
#
|
37
|
-
remove_const(:VALID_FIND_OPTIONS)
|
38
|
-
const_set( :VALID_FIND_OPTIONS, [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :having, :from, :lock, :scrooge_callsite ] )
|
39
|
-
end
|
40
|
-
eigen.alias_method_chain :find, :scrooge
|
41
|
-
eigen.alias_method_chain :find_every, :scrooge
|
33
|
+
# not used at present
|
42
34
|
end
|
43
|
-
|
44
|
-
# Let .find setup callsite information and preloading.
|
45
|
-
#
|
46
|
-
def find_with_scrooge(*args)
|
47
|
-
options = args.extract_options!
|
48
|
-
validate_find_options(options)
|
49
|
-
set_readonly_option!(options)
|
50
35
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
case args.first
|
57
|
-
when :first then find_initial(options)
|
58
|
-
when :last then find_last(options)
|
59
|
-
when :all then find_every(options)
|
60
|
-
else find_from_ids(args, options)
|
36
|
+
def preload_scrooge_associations(result_set, callsite_sig)
|
37
|
+
scrooge_preloading_exclude do
|
38
|
+
callsite_associations = scrooge_callsite(callsite_sig).associations.to_a
|
39
|
+
preload_associations(result_set, callsite_associations) unless callsite_associations.empty?
|
61
40
|
end
|
62
41
|
end
|
63
|
-
|
64
|
-
# Override find_ever to pass along the callsite signature
|
65
|
-
#
|
66
|
-
def find_every_with_scrooge(options)
|
67
|
-
include_associations = merge_includes(scope(:find, :include), options[:include])
|
68
42
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
preload_associations(records, include_associations)
|
75
|
-
end
|
43
|
+
def scrooge_preloading_exclude
|
44
|
+
unless Thread.current[:scrooge_preloading]
|
45
|
+
Thread.current[:scrooge_preloading] = true
|
46
|
+
yield
|
47
|
+
Thread.current[:scrooge_preloading] = false
|
76
48
|
end
|
77
|
-
|
78
|
-
records.each { |record| record.readonly! } if options[:readonly]
|
79
|
-
|
80
|
-
records
|
81
|
-
end
|
49
|
+
end
|
82
50
|
|
83
51
|
# Let's not preload polymorphic associations or collections
|
84
52
|
#
|
85
53
|
def preloadable_associations
|
86
|
-
@@preloadable_associations[self.name] ||=
|
87
|
-
|
88
|
-
|
54
|
+
@@preloadable_associations[self.name] ||=
|
55
|
+
reflect_on_all_associations.reject{|a| a.options[:polymorphic] || a.macro == :has_many}.map(&:name)
|
56
|
+
end
|
57
|
+
|
89
58
|
end
|
90
59
|
|
91
60
|
module InstanceMethods
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
def association_instance_get(name)
|
96
|
-
association = instance_variable_get("@#{name}")
|
97
|
-
if association.respond_to?(:loaded?)
|
98
|
-
scrooge_seen_association!( name )
|
99
|
-
association
|
100
|
-
end
|
61
|
+
|
62
|
+
def self.included( base )
|
63
|
+
base.alias_method_chain :load_target, :scrooge
|
101
64
|
end
|
102
65
|
|
103
|
-
#
|
66
|
+
# note AssociationCollection has its own version of load_target, but we don't
|
67
|
+
# do collections at the moment anyway
|
104
68
|
#
|
105
|
-
def
|
106
|
-
scrooge_seen_association!(
|
107
|
-
|
108
|
-
end
|
109
|
-
|
110
|
-
# Register an association with Scrooge
|
111
|
-
#
|
112
|
-
def scrooge_seen_association!( association )
|
113
|
-
if scrooged? && !scrooge_seen_association?( association )
|
114
|
-
@attributes.scrooge_associations << association
|
115
|
-
self.class.scrooge_callsite( @attributes.callsite_signature ).association!( association )
|
116
|
-
end
|
69
|
+
def load_target_with_scrooge
|
70
|
+
scrooge_seen_association!(@reflection.name)
|
71
|
+
load_target_without_scrooge
|
117
72
|
end
|
118
|
-
|
73
|
+
|
119
74
|
private
|
120
75
|
|
121
|
-
|
122
|
-
|
76
|
+
# Register an association with Scrooge
|
77
|
+
#
|
78
|
+
def scrooge_seen_association!( association )
|
79
|
+
if @owner.scrooged? && !@loaded
|
80
|
+
@owner.class.scrooge_callsite(callsite_signature).association!(association)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def callsite_signature
|
85
|
+
@owner.instance_variable_get(:@attributes).callsite_signature
|
123
86
|
end
|
124
87
|
|
125
88
|
end
|
@@ -31,12 +31,11 @@ module Scrooge
|
|
31
31
|
# Hash container for attributes with scrooge monitoring of attribute access
|
32
32
|
#
|
33
33
|
|
34
|
-
attr_accessor :callsite_signature, :scrooge_columns, :
|
34
|
+
attr_accessor :callsite_signature, :scrooge_columns, :fully_fetched, :klass, :updateable_result_set
|
35
35
|
|
36
|
-
def self.setup(record, scrooge_columns,
|
36
|
+
def self.setup(record, scrooge_columns, klass, callsite_signature, updateable_result_set)
|
37
37
|
hash = new.replace(record)
|
38
38
|
hash.scrooge_columns = scrooge_columns.dup
|
39
|
-
hash.scrooge_associations = scrooge_associations.dup
|
40
39
|
hash.fully_fetched = false
|
41
40
|
hash.klass = klass
|
42
41
|
hash.callsite_signature = callsite_signature
|
@@ -141,7 +140,6 @@ module Scrooge
|
|
141
140
|
|
142
141
|
def dup_self
|
143
142
|
@scrooge_columns = @scrooge_columns.dup
|
144
|
-
@scrooge_associations = @scrooge_associations.dup
|
145
143
|
self
|
146
144
|
end
|
147
145
|
end
|
@@ -56,36 +56,41 @@ module Scrooge
|
|
56
56
|
#
|
57
57
|
def scrooge_reload( p_keys, missing_columns )
|
58
58
|
sql_keys = p_keys.collect{|pk| "'#{pk}'"}.join(ScroogeComma)
|
59
|
-
connection.send( :select, "SELECT #{scrooge_select_sql(missing_columns)} FROM #{quoted_table_name} WHERE #{quoted_table_name}.#{primary_key} IN (#{sql_keys})" )
|
59
|
+
connection.send( :select, "SELECT #{scrooge_select_sql(missing_columns)} FROM #{quoted_table_name} WHERE #{quoted_table_name}.#{primary_key} IN (#{sql_keys})", "#{name} Scrooge Reload" )
|
60
60
|
end
|
61
61
|
|
62
|
+
# Only scope n-1 rows by default.
|
63
|
+
#
|
64
|
+
def scope_with_scrooge?( sql )
|
65
|
+
sql =~ scrooge_select_regex &&
|
66
|
+
column_names.include?(self.primary_key.to_s) &&
|
67
|
+
sql !~ ScroogeRegexJoin
|
68
|
+
end
|
69
|
+
|
62
70
|
private
|
63
|
-
|
64
|
-
# Only scope n-1 rows by default.
|
65
|
-
# Stephen: Temp. relaxed the LIMIT constraint - please advise.
|
66
|
-
def scope_with_scrooge?( sql )
|
67
|
-
sql =~ scrooge_select_regex &&
|
68
|
-
column_names.include?(self.primary_key.to_s) &&
|
69
|
-
sql !~ ScroogeRegexJoin
|
70
|
-
end
|
71
71
|
|
72
72
|
# Find through callsites.
|
73
73
|
#
|
74
|
-
def find_by_sql_with_scrooge(
|
75
|
-
callsite_sig
|
74
|
+
def find_by_sql_with_scrooge(sql)
|
75
|
+
callsite_sig = callsite_signature( caller, callsite_sql( sql ) )
|
76
76
|
callsite_columns = scrooge_callsite(callsite_sig).columns
|
77
|
-
callsite_associations = scrooge_callsite(callsite_sig).associations
|
78
77
|
sql = sql.gsub(scrooge_select_regex, "SELECT #{scrooge_select_sql(callsite_columns)} FROM")
|
79
78
|
results = connection.select_all(sanitize_sql(sql), "#{name} Load Scrooged")
|
79
|
+
|
80
80
|
result_set = ResultSets::ResultArray.new
|
81
81
|
updateable = ResultSets::UpdateableResultSet.new(result_set, self)
|
82
82
|
results.inject(result_set) do |memo, record|
|
83
|
-
memo << instantiate(ScroogedAttributes.setup(record, callsite_columns,
|
83
|
+
memo << instantiate(ScroogedAttributes.setup(record, callsite_columns, self, callsite_sig, updateable))
|
84
84
|
end
|
85
|
+
|
86
|
+
if Associations::Macro.scrooge_installed?
|
87
|
+
preload_scrooge_associations(result_set, callsite_sig)
|
88
|
+
end
|
89
|
+
|
90
|
+
result_set
|
85
91
|
end
|
86
92
|
|
87
|
-
def find_by_sql_without_scrooge( sql
|
88
|
-
scrooge_unlink_callsite!( callsite ) if callsite
|
93
|
+
def find_by_sql_without_scrooge( sql)
|
89
94
|
connection.select_all(sanitize_sql(sql), "#{name} Load").collect! do |record|
|
90
95
|
instantiate( UnscroogedAttributes.setup(record) )
|
91
96
|
end
|
@@ -140,8 +145,8 @@ module Scrooge
|
|
140
145
|
#
|
141
146
|
def becomes_with_scrooge(klass)
|
142
147
|
if scrooged?
|
143
|
-
self.class.scrooge_callsite(
|
144
|
-
klass.scrooge_seen_column!(
|
148
|
+
self.class.scrooge_callsite(callsite_signature).columns.each do |attrib|
|
149
|
+
klass.scrooge_seen_column!(callsite_signature, attrib)
|
145
150
|
end
|
146
151
|
end
|
147
152
|
becomes_without_scrooge(klass)
|
@@ -168,6 +173,12 @@ module Scrooge
|
|
168
173
|
respond_to_without_scrooge?(symbol, include_private)
|
169
174
|
end
|
170
175
|
end
|
176
|
+
|
177
|
+
# Expose this record's callsite signature
|
178
|
+
#
|
179
|
+
def callsite_signature
|
180
|
+
@attributes.callsite_signature
|
181
|
+
end
|
171
182
|
|
172
183
|
private
|
173
184
|
|
@@ -16,6 +16,9 @@ module Scrooge
|
|
16
16
|
@klass = klass # expected class of items in the array
|
17
17
|
end
|
18
18
|
|
19
|
+
# Called by a ScroogedAttributes hash when it is asked for a column
|
20
|
+
# that was not fetched from the DB
|
21
|
+
#
|
19
22
|
def reload_columns!(columns_to_fetch)
|
20
23
|
reloaded_columns = hash_by_primary_key(reload_columns_for_ids(columns_to_fetch, result_set_ids))
|
21
24
|
if !reloaded_columns.has_key?(@updaters_attributes[primary_key_name])
|
@@ -40,6 +43,10 @@ module Scrooge
|
|
40
43
|
end.uniq
|
41
44
|
end
|
42
45
|
|
46
|
+
# The result set is weak referenced by its object_id
|
47
|
+
# So we check that a unique id matches what we have remembered to make sure
|
48
|
+
# that we got the right object (object ids are recycled by ruby)
|
49
|
+
#
|
43
50
|
def result_set
|
44
51
|
return nil unless @result_set_object_id
|
45
52
|
result_set = ObjectSpace._id2ref(@result_set_object_id)
|
@@ -48,10 +55,17 @@ module Scrooge
|
|
48
55
|
nil
|
49
56
|
end
|
50
57
|
|
58
|
+
# When called from after_find and after_initialize, the object
|
59
|
+
# being accessed (and causing the reload) is not in the result set yet.
|
60
|
+
# We make sure that we add its attributes to result_set_attributes so it
|
61
|
+
# gets reloaded too
|
62
|
+
#
|
51
63
|
def default_attributes
|
52
64
|
[@updaters_attributes]
|
53
65
|
end
|
54
66
|
|
67
|
+
# Ids of the items to be reloaded
|
68
|
+
#
|
55
69
|
def result_set_ids
|
56
70
|
result_set_attributes.inject([]) do |memo, attributes|
|
57
71
|
unless attributes.fully_fetched
|
@@ -61,16 +75,22 @@ module Scrooge
|
|
61
75
|
end
|
62
76
|
end
|
63
77
|
|
78
|
+
# Update the result set with attributes provided, as returned by
|
79
|
+
# the sql server
|
80
|
+
#
|
64
81
|
def update_with(remaining_attributes)
|
65
82
|
current_attributes = hash_by_primary_key(result_set_attributes)
|
66
83
|
remaining_attributes.each do |r_id, r_att|
|
67
84
|
old_attributes = current_attributes[r_id]
|
68
85
|
if old_attributes
|
69
|
-
old_attributes.update(r_att.merge(old_attributes))
|
86
|
+
old_attributes.update(r_att.merge(old_attributes)) # must call update, do not use reverse_update
|
70
87
|
end
|
71
88
|
end
|
72
89
|
end
|
73
90
|
|
91
|
+
# Make an efficient data structure to access items when the
|
92
|
+
# primary key is known
|
93
|
+
#
|
74
94
|
def hash_by_primary_key(rows)
|
75
95
|
rows.inject({}) {|memo, row| memo[row[primary_key_name]] = row; memo}
|
76
96
|
end
|
data/lib/scrooge.rb
CHANGED
@@ -19,11 +19,11 @@ module ActiveRecord
|
|
19
19
|
# Determine if a given SQL string is a candidate for callsite <=> columns
|
20
20
|
# optimization.
|
21
21
|
#
|
22
|
-
def find_by_sql(sql
|
22
|
+
def find_by_sql(sql)
|
23
23
|
if scope_with_scrooge?(sql)
|
24
|
-
find_by_sql_with_scrooge(sql
|
24
|
+
find_by_sql_with_scrooge(sql)
|
25
25
|
else
|
26
|
-
find_by_sql_without_scrooge(sql
|
26
|
+
find_by_sql_without_scrooge(sql)
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
@@ -79,4 +79,4 @@ module ActiveRecord
|
|
79
79
|
end
|
80
80
|
|
81
81
|
Scrooge::Optimizations::Columns::Macro.install!
|
82
|
-
Scrooge::Optimizations::Associations::Macro.install!
|
82
|
+
Scrooge::Optimizations::Associations::Macro.install!
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/helper"
|
2
|
+
|
3
|
+
Scrooge::Test.prepare!
|
4
|
+
|
5
|
+
class CallsiteTest < ActiveSupport::TestCase
|
6
|
+
|
7
|
+
def setup
|
8
|
+
@callsite = Scrooge::Callsite.new( MysqlTablePrivilege, 123456 )
|
9
|
+
end
|
10
|
+
|
11
|
+
test "should initialize with a default set of columns" do
|
12
|
+
assert @callsite.columns.empty?
|
13
|
+
assert_equal Scrooge::Callsite.new( MysqlUser, 123456 ).columns, Set["User"]
|
14
|
+
Scrooge::Callsite.any_instance.stubs(:inheritable?).returns(true)
|
15
|
+
Scrooge::Callsite.any_instance.stubs(:inheritance_column).returns("inheritance")
|
16
|
+
assert_equal Scrooge::Callsite.new( MysqlUser, 123456 ).columns, Set["User","inheritance"]
|
17
|
+
end
|
18
|
+
|
19
|
+
test "should be inspectable" do
|
20
|
+
@callsite.association! :mysql_user
|
21
|
+
@callsite.column! :db
|
22
|
+
assert_equal @callsite.inspect, "<#MysqlTablePrivilege :select => '`tables_priv`.db', :include => [:mysql_user]>"
|
23
|
+
end
|
24
|
+
|
25
|
+
test "should flag a column as seen" do
|
26
|
+
assert_difference '@callsite.columns.size' do
|
27
|
+
@callsite.column! :Db
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
test "should flag only preloadable associations as seen" do
|
32
|
+
assert_no_difference '@callsite.associations.size' do
|
33
|
+
@callsite.association! :undefined
|
34
|
+
end
|
35
|
+
assert_difference '@callsite.associations.size', 2 do
|
36
|
+
@callsite.association! :column_privilege
|
37
|
+
@callsite.association! :mysql_user
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
data/test/helper.rb
CHANGED
@@ -2,6 +2,7 @@ require File.join( File.dirname(__FILE__), 'setup' )
|
|
2
2
|
require 'rubygems'
|
3
3
|
require 'mocha'
|
4
4
|
require 'active_support/test_case'
|
5
|
+
ENV["BACKTRACE"] = "1"
|
5
6
|
|
6
7
|
module Scrooge
|
7
8
|
class Test
|
@@ -13,7 +14,8 @@ module Scrooge
|
|
13
14
|
def prepare!
|
14
15
|
require 'test/unit'
|
15
16
|
connect!
|
16
|
-
|
17
|
+
require File.join( File.dirname(__FILE__), 'scrooge_helper' ) unless defined?(ActiveRecord::Base.connection.class::IGNORED_SQL)
|
18
|
+
require_models()
|
17
19
|
end
|
18
20
|
|
19
21
|
def setup!
|
@@ -31,7 +33,7 @@ module Scrooge
|
|
31
33
|
end
|
32
34
|
|
33
35
|
def test_files
|
34
|
-
glob( "#{File.dirname(__FILE__)}
|
36
|
+
glob( "#{File.dirname(__FILE__)}/**/*_test.rb" )
|
35
37
|
end
|
36
38
|
|
37
39
|
private
|
@@ -85,4 +87,4 @@ module Scrooge
|
|
85
87
|
end
|
86
88
|
end
|
87
89
|
|
88
|
-
Scrooge::Test.setup!
|
90
|
+
Scrooge::Test.setup!
|
@@ -0,0 +1,8 @@
|
|
1
|
+
class MysqlTablePrivilege < ActiveRecord::Base
|
2
|
+
set_table_name 'tables_priv'
|
3
|
+
set_primary_key nil
|
4
|
+
|
5
|
+
belongs_to :mysql_user, :class_name => 'MysqlUser', :foreign_key => 'User'
|
6
|
+
belongs_to :column_privilege, :class_name => 'MysqlColumnPrivilege', :foreign_key => 'Column_priv'
|
7
|
+
|
8
|
+
end
|
data/test/models/mysql_user.rb
CHANGED
@@ -2,7 +2,12 @@ class MysqlUser < ActiveRecord::Base
|
|
2
2
|
set_table_name 'user'
|
3
3
|
set_primary_key 'User'
|
4
4
|
|
5
|
+
has_many :table_privileges, :class_name => 'MysqlTablePrivilege', :foreign_key => 'User'
|
6
|
+
has_many :column_privileges, :class_name => 'MysqlColumnPrivilege', :foreign_key => 'User'
|
7
|
+
belongs_to :host, :class_name => 'MysqlHost', :foreign_key => 'Host'
|
8
|
+
|
5
9
|
def after_initialize
|
6
10
|
max_connections if @attributes.has_key?("max_user_connections")
|
7
11
|
end
|
12
|
+
|
8
13
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../../helper"
|
2
|
+
|
3
|
+
Scrooge::Test.prepare!
|
4
|
+
|
5
|
+
class OptimizationsAssociationsMacroTest < ActiveSupport::TestCase
|
6
|
+
|
7
|
+
test "should be able to flag any associations instantiated from a record" do
|
8
|
+
@user = MysqlUser.find(:first)
|
9
|
+
@user.host
|
10
|
+
assert_equal MysqlUser.scrooge_callsite( @user.callsite_signature ).associations, Set[:host]
|
11
|
+
end
|
12
|
+
|
13
|
+
test "should only flag preloadable associations" do
|
14
|
+
Scrooge::Callsite.any_instance.expects(:association!).once
|
15
|
+
@user = MysqlUser.find(:first)
|
16
|
+
@user.host
|
17
|
+
assert_equal MysqlUser.scrooge_callsite( @user.callsite_signature ).associations, Set.new
|
18
|
+
end
|
19
|
+
|
20
|
+
test "should be able to identify all preloadable associations for a given Model" do
|
21
|
+
assert_equal MysqlUser.preloadable_associations, [:host]
|
22
|
+
assert_equal MysqlHost.preloadable_associations, []
|
23
|
+
assert_equal MysqlColumnPrivilege.preloadable_associations, [:mysql_user]
|
24
|
+
assert_equal MysqlTablePrivilege.preloadable_associations, [:mysql_user, :column_privilege]
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# Borrowed from active record's helper
|
2
|
+
ActiveRecord::Base.connection.class.class_eval do
|
3
|
+
IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /SHOW FIELDS/]
|
4
|
+
|
5
|
+
def execute_with_query_record(sql, name = nil, &block)
|
6
|
+
$queries_executed ||= []
|
7
|
+
$queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r }
|
8
|
+
execute_without_query_record(sql, name, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
alias_method_chain :execute, :query_record
|
12
|
+
end
|
data/test/scrooge_test.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
require "#{File.dirname(__FILE__)}/helper"
|
2
|
-
|
2
|
+
|
3
3
|
Scrooge::Test.prepare!
|
4
|
-
|
5
|
-
class ScroogeTest <
|
6
|
-
|
4
|
+
|
5
|
+
class ScroogeTest < ActiveRecord::TestCase
|
6
|
+
|
7
7
|
teardown do
|
8
8
|
MysqlUser.scrooge_flush_callsites!
|
9
9
|
end
|
@@ -39,6 +39,10 @@ class ScroogeTest < ActiveSupport::TestCase
|
|
39
39
|
assert MysqlUser.find(:first).scrooged?
|
40
40
|
assert_instance_of Scrooge::Callsite, MysqlUser.scrooge_callsite( first_callsite )
|
41
41
|
end
|
42
|
+
|
43
|
+
test "should not flag records via Model.find with a custom :select requirement as scrooged" do
|
44
|
+
assert !MysqlUser.find(:first, :select => 'user.Password' ).scrooged?
|
45
|
+
end
|
42
46
|
|
43
47
|
test "should be able to augment an existing callsite with attributes" do
|
44
48
|
MysqlUser.find(:first)
|
@@ -68,6 +72,10 @@ class ScroogeTest < ActiveSupport::TestCase
|
|
68
72
|
[:max_connections, :max_user_connections].each {|f| MysqlUser.find(:first).read_attribute(f)}
|
69
73
|
end
|
70
74
|
|
75
|
+
test "should make 3 queries to fetch same item twice due to reload and remembering" do
|
76
|
+
assert_queries(3) {2.times {MysqlUser.find(:first).max_connections}}
|
77
|
+
end
|
78
|
+
|
71
79
|
def first_callsite
|
72
80
|
MysqlUser.scrooge_callsites.to_a.flatten.first
|
73
81
|
end
|
data/test/setup.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: methodmissing-scrooge
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- "Lourens Naud\xC3\xA9"
|
@@ -10,12 +10,12 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2009-03-
|
13
|
+
date: 2009-03-22 00:00:00 -07:00
|
14
14
|
default_executable:
|
15
15
|
dependencies: []
|
16
16
|
|
17
17
|
description: An ActiveRecord attribute tracker to ensure production Ruby applications only fetch the database content needed to minimize wire traffic and reduce conversion overheads to native Ruby types.
|
18
|
-
email: lourens@methodmissing.com or
|
18
|
+
email: lourens@methodmissing.com or sdsykes@gmail.com
|
19
19
|
executables: []
|
20
20
|
|
21
21
|
extensions: []
|
@@ -38,9 +38,17 @@ files:
|
|
38
38
|
- lib/optimizations/result_sets/result_array.rb
|
39
39
|
- lib/optimizations/result_sets/updateable_result_set.rb
|
40
40
|
- lib/scrooge.rb
|
41
|
+
- test/callsite_test.rb
|
41
42
|
- test/helper.rb
|
42
43
|
- test/models
|
44
|
+
- test/models/mysql_column_privilege.rb
|
45
|
+
- test/models/mysql_host.rb
|
46
|
+
- test/models/mysql_table_privilege.rb
|
43
47
|
- test/models/mysql_user.rb
|
48
|
+
- test/optimizations
|
49
|
+
- test/optimizations/associations
|
50
|
+
- test/optimizations/associations/macro_test.rb
|
51
|
+
- test/scrooge_helper.rb
|
44
52
|
- test/scrooge_test.rb
|
45
53
|
- test/setup.rb
|
46
54
|
- rails/init.rb
|