methodmissing-scrooge 2.2.0 → 2.2.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/README.textile +1 -1
- data/VERSION.yml +2 -2
- data/lib/callsite.rb +76 -0
- data/lib/optimizations/columns/attributes_proxy.rb +138 -0
- data/lib/optimizations/columns/macro.rb +186 -0
- data/lib/scrooge.rb +22 -182
- data/test/scrooge_test.rb +5 -10
- metadata +7 -3
- data/lib/attributes_proxy.rb +0 -121
data/README.textile
CHANGED
@@ -214,7 +214,7 @@ h2. Todo
|
|
214
214
|
|
215
215
|
* Deeper coverage for Scrooge::AttributesProxy ( pending conversion to a subclass of Hash instead )
|
216
216
|
|
217
|
-
*
|
217
|
+
* Test cases for Scrooge::Callsite
|
218
218
|
|
219
219
|
* Track both columns AND association invocations off Scrooge::Callsite
|
220
220
|
|
data/VERSION.yml
CHANGED
data/lib/callsite.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
module Scrooge
|
2
|
+
class Callsite
|
3
|
+
|
4
|
+
# Represents a Callsite and is a container for any columns and
|
5
|
+
# associations ( coming soon ) referenced at the callsite.
|
6
|
+
#
|
7
|
+
|
8
|
+
Mutex = Mutex.new
|
9
|
+
|
10
|
+
attr_accessor :klass,
|
11
|
+
:signature,
|
12
|
+
:columns,
|
13
|
+
:associations
|
14
|
+
|
15
|
+
def initialize( klass, signature )
|
16
|
+
@klass = klass
|
17
|
+
@signature = signature
|
18
|
+
@columns = setup_columns
|
19
|
+
@associations = setup_associations
|
20
|
+
end
|
21
|
+
|
22
|
+
# Flag a column as seen
|
23
|
+
#
|
24
|
+
def column!( column )
|
25
|
+
Mutex.synchronize do
|
26
|
+
@columns << column
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Flag an association as seen
|
31
|
+
#
|
32
|
+
def association!( association )
|
33
|
+
Mutex.synchronize do
|
34
|
+
@associations << association
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Is the table a container for STI models ?
|
41
|
+
#
|
42
|
+
def inheritable?
|
43
|
+
@klass.column_names.include?( inheritance_column )
|
44
|
+
end
|
45
|
+
|
46
|
+
# Ensure that at least the primary key and optionally the inheritance
|
47
|
+
# column ( for STI ) is set.
|
48
|
+
#
|
49
|
+
def setup_columns
|
50
|
+
if inheritable?
|
51
|
+
Set.new([primary_key, inheritance_column])
|
52
|
+
else
|
53
|
+
Set.new([primary_key])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Stubbed for future use
|
58
|
+
#
|
59
|
+
def setup_associations
|
60
|
+
Set.new
|
61
|
+
end
|
62
|
+
|
63
|
+
# Memoize a string representation of the inheritance column
|
64
|
+
#
|
65
|
+
def inheritance_column
|
66
|
+
@inheritance_column ||= @klass.inheritance_column.to_s
|
67
|
+
end
|
68
|
+
|
69
|
+
# Memoize a string representation of the primary
|
70
|
+
#
|
71
|
+
def primary_key
|
72
|
+
@primary_key ||= @klass.primary_key.to_s
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
# TODO: Circumvent this hack
|
4
|
+
alias_method :update_without_scrooge, :update
|
5
|
+
def update(other)
|
6
|
+
update_without_scrooge(other.to_hash)
|
7
|
+
end
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
module Scrooge
|
12
|
+
module Optimizations
|
13
|
+
module Columns
|
14
|
+
class AttributesProxy < Hash
|
15
|
+
|
16
|
+
# Hash like Proxy container for attributes
|
17
|
+
#
|
18
|
+
|
19
|
+
attr_accessor :callsite_signature, :scrooge_columns, :fully_fetched, :klass
|
20
|
+
|
21
|
+
def self.setup(record, scrooge_columns, klass, callsite_signature)
|
22
|
+
hash = new.replace(record)
|
23
|
+
hash.scrooge_columns = scrooge_columns.dup
|
24
|
+
hash.fully_fetched = false
|
25
|
+
hash.klass = klass
|
26
|
+
hash.callsite_signature = callsite_signature
|
27
|
+
hash
|
28
|
+
end
|
29
|
+
|
30
|
+
# Delegate Hash keys to all defined columns
|
31
|
+
#
|
32
|
+
def keys
|
33
|
+
@klass.column_names
|
34
|
+
end
|
35
|
+
|
36
|
+
# Let #has_key? consider defined columns
|
37
|
+
#
|
38
|
+
def has_key?(attr_name)
|
39
|
+
keys.include?(attr_name.to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
alias_method :include?, :has_key?
|
43
|
+
alias_method :key?, :has_key?
|
44
|
+
alias_method :member?, :has_key?
|
45
|
+
|
46
|
+
# Lazily augment and load missing attributes
|
47
|
+
#
|
48
|
+
def [](attr_name)
|
49
|
+
attr_s = attr_name.to_s
|
50
|
+
if interesting_for_scrooge?( attr_s )
|
51
|
+
augment_callsite!( attr_s )
|
52
|
+
fetch_remaining
|
53
|
+
@scrooge_columns << attr_s
|
54
|
+
end
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
def fetch(*args, &block)
|
59
|
+
self[args[0]]
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
def []=(attr_name, value)
|
64
|
+
@scrooge_columns << attr_name.to_s
|
65
|
+
super
|
66
|
+
end
|
67
|
+
|
68
|
+
alias_method :store, :[]=
|
69
|
+
|
70
|
+
def dup
|
71
|
+
super.dup_self
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_hash
|
75
|
+
fetch_remaining
|
76
|
+
super
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_a
|
80
|
+
fetch_remaining
|
81
|
+
super
|
82
|
+
end
|
83
|
+
|
84
|
+
def delete(attr_name)
|
85
|
+
self[attr_name]
|
86
|
+
super
|
87
|
+
end
|
88
|
+
|
89
|
+
def update(hash)
|
90
|
+
@fully_fetched = true
|
91
|
+
super
|
92
|
+
end
|
93
|
+
|
94
|
+
alias_method :merge!, :update
|
95
|
+
|
96
|
+
def fetch_remaining
|
97
|
+
unless @fully_fetched
|
98
|
+
columns_to_fetch = @klass.column_names - @scrooge_columns.to_a
|
99
|
+
unless columns_to_fetch.empty?
|
100
|
+
fetch_remaining!( columns_to_fetch )
|
101
|
+
end
|
102
|
+
@fully_fetched = true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
|
108
|
+
def fetch_remaining!( columns_to_fetch )
|
109
|
+
begin
|
110
|
+
new_object = fetch_record_with_remaining_columns( columns_to_fetch )
|
111
|
+
rescue ActiveRecord::RecordNotFound
|
112
|
+
raise ActiveRecord::MissingAttributeError, "scrooge cannot fetch missing attribute(s) #{columns_to_fetch.to_a.join(', ')} because record went away"
|
113
|
+
end
|
114
|
+
replace(new_object.instance_variable_get(:@attributes).merge(self))
|
115
|
+
end
|
116
|
+
|
117
|
+
def fetch_record_with_remaining_columns( columns_to_fetch )
|
118
|
+
@klass.send(:with_exclusive_scope) do
|
119
|
+
@klass.find(self[@klass.primary_key], :select=>@klass.scrooge_select_sql(columns_to_fetch))
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def interesting_for_scrooge?( attr_s )
|
124
|
+
has_key?(attr_s) && !@scrooge_columns.include?(attr_s)
|
125
|
+
end
|
126
|
+
|
127
|
+
def augment_callsite!( attr_s )
|
128
|
+
@klass.scrooge_seen_column!(callsite_signature, attr_s)
|
129
|
+
end
|
130
|
+
|
131
|
+
def dup_self
|
132
|
+
@scrooge_columns = @scrooge_columns.dup
|
133
|
+
self
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
module Scrooge
|
2
|
+
module Optimizations
|
3
|
+
module Columns
|
4
|
+
module Macro
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# Inject into ActiveRecord
|
9
|
+
#
|
10
|
+
def install!
|
11
|
+
if scrooge_installable?
|
12
|
+
ActiveRecord::Base.send( :extend, Scrooge::Optimizations::Columns::SingletonMethods )
|
13
|
+
ActiveRecord::Base.send( :include, Scrooge::Optimizations::Columns::InstanceMethods )
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def scrooge_installable?
|
20
|
+
!ActiveRecord::Base.included_modules.include?( Scrooge::Optimizations::Columns::InstanceMethods )
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
module SingletonMethods
|
28
|
+
|
29
|
+
ScroogeBlankString = "".freeze
|
30
|
+
ScroogeComma = ",".freeze
|
31
|
+
ScroogeRegexSanitize = /(?:LIMIT|WHERE|FROM|GROUP\s*BY|HAVING|ORDER\s*BY|PROCEDURE|FOR\s*UPDATE|INTO\s*OUTFILE).*/i
|
32
|
+
ScroogeRegexJoin = /(?:left|inner|outer|cross)*\s*(?:straight_join|join)/i
|
33
|
+
|
34
|
+
@@scrooge_select_regexes = {}
|
35
|
+
|
36
|
+
# Augment a given callsite signature with a column / attribute.
|
37
|
+
#
|
38
|
+
def scrooge_seen_column!( callsite_signature, attr_name )
|
39
|
+
scrooge_callsite( callsite_signature ).column!( attr_name )
|
40
|
+
end
|
41
|
+
|
42
|
+
# Generates a SELECT snippet for this Model from a given Set of columns
|
43
|
+
#
|
44
|
+
def scrooge_select_sql( set )
|
45
|
+
set.map{|a| attribute_with_table( a ) }.join( ScroogeComma )
|
46
|
+
end
|
47
|
+
|
48
|
+
# Marshal.load
|
49
|
+
#
|
50
|
+
def _load(str)
|
51
|
+
Marshal.load(str)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# Only scope n-1 rows by default.
|
57
|
+
# Stephen: Temp. relaxed the LIMIT constraint - please advise.
|
58
|
+
def scope_with_scrooge?( sql )
|
59
|
+
sql =~ scrooge_select_regex &&
|
60
|
+
column_names.include?(self.primary_key.to_s) &&
|
61
|
+
sql !~ ScroogeRegexJoin
|
62
|
+
end
|
63
|
+
|
64
|
+
# Find through callsites.
|
65
|
+
#
|
66
|
+
def find_by_sql_with_scrooge( sql )
|
67
|
+
callsite_signature = (caller[ActiveRecord::Base::ScroogeCallsiteSample] << callsite_sql( sql )).hash
|
68
|
+
callsite_set = scrooge_callsite(callsite_signature).columns
|
69
|
+
sql = sql.gsub(scrooge_select_regex, "SELECT #{scrooge_select_sql(callsite_set)} FROM")
|
70
|
+
result = connection.select_all(sanitize_sql(sql), "#{name} Load Scrooged").collect! do |record|
|
71
|
+
instantiate( Scrooge::Optimizations::Columns::AttributesProxy.setup(record, callsite_set, self, callsite_signature) )
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Generate a regex that respects the table name as well to catch
|
76
|
+
# verbose SQL from JOINS etc.
|
77
|
+
#
|
78
|
+
def scrooge_select_regex
|
79
|
+
@@scrooge_select_regexes[self.table_name] ||= Regexp.compile( "SELECT (`?(?:#{table_name})?`?.?\\*) FROM" )
|
80
|
+
end
|
81
|
+
|
82
|
+
# Trim any conditions
|
83
|
+
#
|
84
|
+
def callsite_sql( sql )
|
85
|
+
sql.gsub(ScroogeRegexSanitize, ScroogeBlankString)
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
module InstanceMethods
|
91
|
+
|
92
|
+
def self.included( base )
|
93
|
+
base.alias_method_chain :delete, :scrooge
|
94
|
+
base.alias_method_chain :destroy, :scrooge
|
95
|
+
base.alias_method_chain :respond_to?, :scrooge
|
96
|
+
end
|
97
|
+
|
98
|
+
# Is this instance being handled by scrooge?
|
99
|
+
#
|
100
|
+
def scrooged?
|
101
|
+
@attributes.is_a?(Scrooge::Optimizations::Columns::AttributesProxy)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Delete should fully load all the attributes before the @attributes hash is frozen
|
105
|
+
#
|
106
|
+
def delete_with_scrooge
|
107
|
+
scrooge_fetch_remaining
|
108
|
+
delete_without_scrooge
|
109
|
+
end
|
110
|
+
|
111
|
+
# Destroy should fully load all the attributes before the @attributes hash is frozen
|
112
|
+
#
|
113
|
+
def destroy_with_scrooge
|
114
|
+
scrooge_fetch_remaining
|
115
|
+
destroy_without_scrooge
|
116
|
+
end
|
117
|
+
|
118
|
+
# Augment callsite info for new model class when using STI
|
119
|
+
#
|
120
|
+
def becomes(klass)
|
121
|
+
returning klass.new do |became|
|
122
|
+
became.instance_variable_set("@attributes", @attributes)
|
123
|
+
became.instance_variable_set("@attributes_cache", @attributes_cache)
|
124
|
+
became.instance_variable_set("@new_record", new_record?)
|
125
|
+
if scrooged?
|
126
|
+
self.class.scrooge_callsite(@attributes.callsite_signature).columns.each do |attrib|
|
127
|
+
became.class.scrooge_seen_column!(@attributes.callsite_signature, attrib)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Marshal
|
134
|
+
# force a full load if needed, and remove any possibility for missing attr flagging
|
135
|
+
#
|
136
|
+
def _dump(depth)
|
137
|
+
scrooge_fetch_remaining
|
138
|
+
scrooge_dump_flag_this
|
139
|
+
str = Marshal.dump(self)
|
140
|
+
scrooge_dump_unflag_this
|
141
|
+
str
|
142
|
+
end
|
143
|
+
|
144
|
+
# Enables us to use Marshal.dump inside our _dump method without an infinite loop
|
145
|
+
#
|
146
|
+
def respond_to_with_scrooge?(symbol, include_private=false)
|
147
|
+
if symbol == :_dump && scrooge_dump_flagged?
|
148
|
+
false
|
149
|
+
else
|
150
|
+
respond_to_without_scrooge?(symbol, include_private)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
# Flag Marshal dump in progress
|
157
|
+
#
|
158
|
+
def scrooge_dump_flag_this
|
159
|
+
Thread.current[:scrooge_dumping_objects] ||= []
|
160
|
+
Thread.current[:scrooge_dumping_objects] << object_id
|
161
|
+
end
|
162
|
+
|
163
|
+
# Flag Marhsal dump not in progress
|
164
|
+
#
|
165
|
+
def scrooge_dump_unflag_this
|
166
|
+
Thread.current[:scrooge_dumping_objects].delete(object_id)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Flag scrooge as dumping ( excuse my French )
|
170
|
+
#
|
171
|
+
def scrooge_dump_flagged?
|
172
|
+
Thread.current[:scrooge_dumping_objects] &&
|
173
|
+
Thread.current[:scrooge_dumping_objects].include?(object_id)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Fetch any missing attributes
|
177
|
+
#
|
178
|
+
def scrooge_fetch_remaining
|
179
|
+
@attributes.fetch_remaining if scrooged?
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
data/lib/scrooge.rb
CHANGED
@@ -1,19 +1,14 @@
|
|
1
1
|
$:.unshift(File.dirname(__FILE__))
|
2
2
|
|
3
3
|
require 'set'
|
4
|
-
require '
|
4
|
+
require 'callsite'
|
5
|
+
require 'optimizations/columns/attributes_proxy'
|
6
|
+
require 'optimizations/columns/macro'
|
5
7
|
|
6
8
|
module ActiveRecord
|
7
9
|
class Base
|
8
10
|
|
9
|
-
@@scrooge_mutex = Mutex.new
|
10
11
|
@@scrooge_callsites = {}
|
11
|
-
@@scrooge_select_regexes = {}
|
12
|
-
|
13
|
-
ScroogeBlankString = "".freeze
|
14
|
-
ScroogeComma = ",".freeze
|
15
|
-
ScroogeRegexWhere = /WHERE.*/i
|
16
|
-
ScroogeRegexJoin = /(?:left|inner|outer|cross)*\s*(?:straight_join|join)/i
|
17
12
|
ScroogeCallsiteSample = 0..10
|
18
13
|
|
19
14
|
class << self
|
@@ -29,198 +24,43 @@ module ActiveRecord
|
|
29
24
|
find_by_sql_without_scrooge(sql)
|
30
25
|
end
|
31
26
|
end
|
32
|
-
|
33
|
-
# Populate the storage for a given callsite signature
|
34
|
-
#
|
35
|
-
def scrooge_callsite_set!(callsite_signature, set)
|
36
|
-
@@scrooge_callsites[self.table_name][callsite_signature] = set
|
37
|
-
end
|
38
|
-
|
39
|
-
# Reference storage for a given callsite signature
|
40
|
-
#
|
41
|
-
def scrooge_callsite_set(callsite_signature)
|
42
|
-
scrooge_callsites
|
43
|
-
@@scrooge_callsites[self.table_name][callsite_signature]
|
44
|
-
end
|
45
27
|
|
46
28
|
# Expose known callsites for this model
|
47
29
|
#
|
48
30
|
def scrooge_callsites
|
49
31
|
@@scrooge_callsites[self.table_name] ||= {}
|
50
32
|
end
|
51
|
-
|
33
|
+
|
34
|
+
# Fetch or setup a callsite instance for a given signature
|
35
|
+
#
|
36
|
+
def scrooge_callsite( callsite_signature )
|
37
|
+
@@scrooge_callsites[self.table_name] ||= {}
|
38
|
+
@@scrooge_callsites[self.table_name][callsite_signature] ||= callsite( callsite_signature )
|
39
|
+
end
|
40
|
+
|
52
41
|
# Flush all known callsites.Mostly a test helper.
|
53
42
|
#
|
54
43
|
def scrooge_flush_callsites!
|
55
44
|
@@scrooge_callsites[self.table_name] = {}
|
56
45
|
end
|
57
|
-
|
58
|
-
# Augment a given callsite signature with a column / attribute.
|
59
|
-
#
|
60
|
-
def augment_scrooge_callsite!( callsite_signature, attr_name )
|
61
|
-
set = set_for_callsite( callsite_signature ) # make set if needed - eg unserialized models after restart
|
62
|
-
@@scrooge_mutex.synchronize do
|
63
|
-
set << attr_name
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# Generates a SELECT snippet for this Model from a given Set of columns
|
68
|
-
#
|
69
|
-
def scrooge_sql( set )
|
70
|
-
set.map{|a| attribute_with_table( a ) }.join( ScroogeComma )
|
71
|
-
end
|
72
46
|
|
73
47
|
private
|
74
48
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
column_names.include?(self.primary_key.to_s) &&
|
80
|
-
sql !~ ScroogeRegexJoin
|
81
|
-
end
|
82
|
-
|
83
|
-
# Find through callsites.
|
84
|
-
#
|
85
|
-
def find_by_sql_with_scrooge( sql )
|
86
|
-
callsite_signature = (caller[ScroogeCallsiteSample] << sql.gsub(ScroogeRegexWhere, ScroogeBlankString)).hash
|
87
|
-
callsite_set = set_for_callsite(callsite_signature)
|
88
|
-
sql = sql.gsub(scrooge_select_regex, "SELECT #{scrooge_sql(callsite_set)} FROM")
|
89
|
-
result = connection.select_all(sanitize_sql(sql), "#{name} Load Scrooged").collect! do |record|
|
90
|
-
instantiate(Scrooge::AttributesProxy.new(record, callsite_set, self, callsite_signature))
|
49
|
+
# Initialize a callsite
|
50
|
+
#
|
51
|
+
def callsite( signature )
|
52
|
+
Scrooge::Callsite.new( self, signature )
|
91
53
|
end
|
92
|
-
end
|
93
54
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
def set_for_callsite( callsite_signature )
|
99
|
-
@@scrooge_mutex.synchronize do
|
100
|
-
callsite_set = scrooge_callsite_set(callsite_signature)
|
101
|
-
unless callsite_set
|
102
|
-
callsite_set = scrooge_default_callsite_set
|
103
|
-
scrooge_callsite_set!(callsite_signature, callsite_set)
|
104
|
-
end
|
105
|
-
callsite_set
|
55
|
+
# Link the column to its table
|
56
|
+
#
|
57
|
+
def attribute_with_table( attr_name )
|
58
|
+
"#{quoted_table_name}.#{attr_name.to_s}"
|
106
59
|
end
|
107
|
-
end
|
108
|
-
|
109
|
-
# Ensure that the inheritance column is defined for the callsite if
|
110
|
-
# this is an STI klass tree.
|
111
|
-
#
|
112
|
-
def scrooge_default_callsite_set
|
113
|
-
if column_names.include?( self.inheritance_column.to_s )
|
114
|
-
Set.new([self.primary_key.to_s, self.inheritance_column.to_s])
|
115
|
-
else
|
116
|
-
Set.new([self.primary_key.to_s])
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
# Generate a regex that respects the table name as well to catch
|
121
|
-
# verbose SQL from JOINS etc.
|
122
|
-
#
|
123
|
-
def scrooge_select_regex
|
124
|
-
@@scrooge_select_regexes[self.table_name] ||= Regexp.compile( "SELECT (`?(?:#{table_name})?`?.?\\*) FROM" )
|
125
|
-
end
|
126
|
-
|
127
|
-
# Link the column to its table
|
128
|
-
#
|
129
|
-
def attribute_with_table( attr_name )
|
130
|
-
"#{quoted_table_name}.#{attr_name.to_s}"
|
131
|
-
end
|
132
60
|
|
133
61
|
end # class << self
|
134
62
|
|
135
|
-
# Is this instance being handled by scrooge?
|
136
|
-
#
|
137
|
-
def scrooged?
|
138
|
-
@attributes.is_a?(Scrooge::AttributesProxy)
|
139
|
-
end
|
140
|
-
|
141
|
-
# Delete should fully load all the attributes before the @attributes hash is frozen
|
142
|
-
#
|
143
|
-
alias_method :delete_without_scrooge, :delete
|
144
|
-
def delete
|
145
|
-
scrooge_fetch_remaining
|
146
|
-
delete_without_scrooge
|
147
|
-
end
|
148
|
-
|
149
|
-
# Destroy should fully load all the attributes before the @attributes hash is frozen
|
150
|
-
#
|
151
|
-
alias_method :destroy_without_scrooge, :destroy
|
152
|
-
def destroy
|
153
|
-
scrooge_fetch_remaining
|
154
|
-
destroy_without_scrooge
|
155
|
-
end
|
156
|
-
|
157
|
-
# Augment callsite info for new model class when using STI
|
158
|
-
#
|
159
|
-
def becomes(klass)
|
160
|
-
returning klass.new do |became|
|
161
|
-
became.instance_variable_set("@attributes", @attributes)
|
162
|
-
became.instance_variable_set("@attributes_cache", @attributes_cache)
|
163
|
-
became.instance_variable_set("@new_record", new_record?)
|
164
|
-
if scrooged?
|
165
|
-
self.class.scrooge_callsite_set(@attributes.callsite_signature).each do |attrib|
|
166
|
-
became.class.augment_scrooge_callsite!(@attributes.callsite_signature, attrib)
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
# Marshal
|
173
|
-
# force a full load if needed, and remove any possibility for missing attr flagging
|
174
|
-
#
|
175
|
-
def _dump(depth)
|
176
|
-
scrooge_fetch_remaining
|
177
|
-
scrooge_dump_flag_this
|
178
|
-
str = Marshal.dump(self)
|
179
|
-
scrooge_dump_unflag_this
|
180
|
-
str
|
181
|
-
end
|
182
|
-
|
183
|
-
# Marshal.load
|
184
|
-
#
|
185
|
-
def self._load(str)
|
186
|
-
Marshal.load(str)
|
187
|
-
end
|
188
|
-
|
189
|
-
# Enables us to use Marshal.dump inside our _dump method without an infinite loop
|
190
|
-
#
|
191
|
-
alias_method :respond_to_without_scrooge, :respond_to?
|
192
|
-
def respond_to?(symbol, include_private=false)
|
193
|
-
if symbol == :_dump && scrooge_dump_flagged?
|
194
|
-
false
|
195
|
-
else
|
196
|
-
respond_to_without_scrooge(symbol, include_private)
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
private
|
201
|
-
|
202
|
-
# Flag Marshal dump in progress
|
203
|
-
#
|
204
|
-
def scrooge_dump_flag_this
|
205
|
-
Thread.current[:scrooge_dumping_objects] ||= []
|
206
|
-
Thread.current[:scrooge_dumping_objects] << object_id
|
207
|
-
end
|
208
|
-
|
209
|
-
# Flag Marhsal dump not in progress
|
210
|
-
#
|
211
|
-
def scrooge_dump_unflag_this
|
212
|
-
Thread.current[:scrooge_dumping_objects].delete(object_id)
|
213
|
-
end
|
214
|
-
|
215
|
-
# Flag scrooge as dumping ( excuse my French )
|
216
|
-
#
|
217
|
-
def scrooge_dump_flagged?
|
218
|
-
Thread.current[:scrooge_dumping_objects] && Thread.current[:scrooge_dumping_objects].include?(object_id)
|
219
|
-
end
|
220
|
-
|
221
|
-
def scrooge_fetch_remaining
|
222
|
-
@attributes.fetch_remaining if scrooged?
|
223
|
-
end
|
224
|
-
|
225
63
|
end
|
226
|
-
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Scrooge::Optimizations::Columns::Macro.install!
|
data/test/scrooge_test.rb
CHANGED
@@ -37,27 +37,22 @@ class ScroogeTest < ActiveSupport::TestCase
|
|
37
37
|
|
38
38
|
test "should be able to retrieve a callsite form a given signature" do
|
39
39
|
assert MysqlUser.find(:first).scrooged?
|
40
|
-
assert_instance_of
|
41
|
-
end
|
42
|
-
|
43
|
-
test "should be able to populate the callsite for a given signature" do
|
44
|
-
MysqlUser.scrooge_callsite_set!(123456, Set[1,2,3])
|
45
|
-
assert_equal MysqlUser.scrooge_callsite_set(123456), Set[1,2,3]
|
40
|
+
assert_instance_of Scrooge::Callsite, MysqlUser.scrooge_callsite( first_callsite )
|
46
41
|
end
|
47
42
|
|
48
43
|
test "should be able to augment an existing callsite with attributes" do
|
49
44
|
MysqlUser.find(:first)
|
50
|
-
MysqlUser.
|
51
|
-
assert MysqlUser.
|
45
|
+
MysqlUser.scrooge_seen_column!( first_callsite, 'Password' )
|
46
|
+
assert MysqlUser.scrooge_callsite( first_callsite ).columns.include?( 'Password' )
|
52
47
|
end
|
53
48
|
|
54
49
|
test "should be able to generate a SQL select snippet from a given set" do
|
55
|
-
assert_equal MysqlUser.
|
50
|
+
assert_equal MysqlUser.scrooge_select_sql( Set['Password','User','Host'] ), "`user`.User,`user`.Password,`user`.Host"
|
56
51
|
end
|
57
52
|
|
58
53
|
test "should be able to augment an existing callsite when attributes is referenced that we haven't seen yet" do
|
59
54
|
user = MysqlUser.find(:first)
|
60
|
-
MysqlUser.expects(:
|
55
|
+
MysqlUser.expects(:scrooge_seen_column!).times(2)
|
61
56
|
user.Password
|
62
57
|
user.Host
|
63
58
|
end
|
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: 2.2.
|
4
|
+
version: 2.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- "Lourens Naud\xC3\xA9"
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2009-03-
|
13
|
+
date: 2009-03-15 00:00:00 -07:00
|
14
14
|
default_executable:
|
15
15
|
dependencies: []
|
16
16
|
|
@@ -27,7 +27,11 @@ files:
|
|
27
27
|
- README
|
28
28
|
- README.textile
|
29
29
|
- VERSION.yml
|
30
|
-
- lib/
|
30
|
+
- lib/callsite.rb
|
31
|
+
- lib/optimizations
|
32
|
+
- lib/optimizations/columns
|
33
|
+
- lib/optimizations/columns/attributes_proxy.rb
|
34
|
+
- lib/optimizations/columns/macro.rb
|
31
35
|
- lib/scrooge.rb
|
32
36
|
- test/helper.rb
|
33
37
|
- test/models
|
data/lib/attributes_proxy.rb
DELETED
@@ -1,121 +0,0 @@
|
|
1
|
-
module Scrooge
|
2
|
-
class AttributesProxy
|
3
|
-
attr_reader :callsite_signature
|
4
|
-
|
5
|
-
def initialize(record, scrooge_columns, klass, callsite_signature)
|
6
|
-
@attributes = record
|
7
|
-
@scrooge_columns = scrooge_columns.dup
|
8
|
-
@fully_fetched = false
|
9
|
-
@klass = klass
|
10
|
-
@callsite_signature = callsite_signature
|
11
|
-
end
|
12
|
-
|
13
|
-
# Delegate Hash keys to all defined columns
|
14
|
-
#
|
15
|
-
def keys
|
16
|
-
@klass.column_names
|
17
|
-
end
|
18
|
-
|
19
|
-
# Let #has_key? consider defined columns
|
20
|
-
#
|
21
|
-
def has_key?(attr_name)
|
22
|
-
keys.include?(attr_name.to_s)
|
23
|
-
end
|
24
|
-
|
25
|
-
alias_method :include?, :has_key?
|
26
|
-
alias_method :key?, :has_key?
|
27
|
-
|
28
|
-
# Lazily augment and load missing attributes
|
29
|
-
#
|
30
|
-
def [](attr_name)
|
31
|
-
attr_s = attr_name.to_s
|
32
|
-
if interesting_for_scrooge?( attr_s )
|
33
|
-
augment_callsite!( attr_s )
|
34
|
-
fetch_remaining
|
35
|
-
@scrooge_columns << attr_s
|
36
|
-
end
|
37
|
-
@attributes[attr_s]
|
38
|
-
end
|
39
|
-
|
40
|
-
def fetch(*args, &block)
|
41
|
-
self[args[0]]
|
42
|
-
@attributes.fetch(*args, &block)
|
43
|
-
end
|
44
|
-
|
45
|
-
def []=(attr_name, value)
|
46
|
-
attr_s = attr_name.to_s
|
47
|
-
@attributes[attr_s] = value
|
48
|
-
@scrooge_columns << attr_s
|
49
|
-
end
|
50
|
-
|
51
|
-
def dup
|
52
|
-
super.dup_self
|
53
|
-
end
|
54
|
-
|
55
|
-
def to_a
|
56
|
-
fetch_remaining
|
57
|
-
@attributes.to_a
|
58
|
-
end
|
59
|
-
|
60
|
-
def delete(attr_name)
|
61
|
-
self[attr_name]
|
62
|
-
@attributes.delete(attr_name)
|
63
|
-
end
|
64
|
-
|
65
|
-
def update(hash)
|
66
|
-
hash.to_hash.each do |k, v|
|
67
|
-
self[k] = v
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def to_hash
|
72
|
-
fetch_remaining
|
73
|
-
@attributes
|
74
|
-
end
|
75
|
-
|
76
|
-
def freeze
|
77
|
-
@attributes.freeze
|
78
|
-
end
|
79
|
-
|
80
|
-
def frozen?
|
81
|
-
@attributes.frozen?
|
82
|
-
end
|
83
|
-
|
84
|
-
def fetch_remaining
|
85
|
-
unless @fully_fetched
|
86
|
-
columns_to_fetch = @klass.column_names - @scrooge_columns.to_a
|
87
|
-
unless columns_to_fetch.empty?
|
88
|
-
begin
|
89
|
-
new_object = fetch_record_with_remaining_columns( columns_to_fetch )
|
90
|
-
rescue ActiveRecord::RecordNotFound
|
91
|
-
raise ActiveRecord::MissingAttributeError, "scrooge cannot fetch missing attribute(s) because record went away"
|
92
|
-
end
|
93
|
-
@attributes = new_object.instance_variable_get(:@attributes).merge(@attributes)
|
94
|
-
end
|
95
|
-
@fully_fetched = true
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
protected
|
100
|
-
|
101
|
-
def fetch_record_with_remaining_columns( columns_to_fetch )
|
102
|
-
@klass.send(:with_exclusive_scope) do
|
103
|
-
@klass.find(@attributes[@klass.primary_key], :select=>@klass.scrooge_sql(columns_to_fetch))
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def interesting_for_scrooge?( attr_s )
|
108
|
-
has_key?(attr_s) && !@scrooge_columns.include?(attr_s)
|
109
|
-
end
|
110
|
-
|
111
|
-
def augment_callsite!( attr_s )
|
112
|
-
@klass.augment_scrooge_callsite!(callsite_signature, attr_s)
|
113
|
-
end
|
114
|
-
|
115
|
-
def dup_self
|
116
|
-
@attributes = @attributes.dup
|
117
|
-
@scrooge_columns = @scrooge_columns.dup
|
118
|
-
self
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|