constant_table_saver 2.0.0

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/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ constant_table_saver-*.gem
3
+ Gemfile.lock
4
+ test/constant_table_saver.db
5
+ test/log/test.log
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Declare your gem's dependencies in transaction_isolation_level.gemspec.
4
+ # Bundler will treat runtime dependencies like base dependencies, and
5
+ # development dependencies will be added by default to the :development group.
6
+ gemspec
7
+
8
+ # Declare any dependencies that are still in development here instead of in
9
+ # your gemspec. These might include edge Rails or gems from your path or
10
+ # Git. Remember to move these dependencies to your gemspec before releasing
11
+ # your gem to rubygems.org.
12
+
13
+ # To use debugger
14
+ # gem 'ruby-debug19', :require => 'ruby-debug'
15
+ # gem 'ruby-debug'
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Will Bryant, Sekuda Ltd
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,65 @@
1
+ ConstantTableSaver
2
+ ==================
3
+
4
+ Loads all records from the table on first use, and thereafter returns the
5
+ cached (and frozen) records for all find calls.
6
+
7
+ Optionally, creates class-level methods you can use to grab the records,
8
+ named after the name field you specify.
9
+
10
+
11
+ Compatibility
12
+ =============
13
+
14
+ Currently tested against Rails 2.3.14, 3.0.17, 3.1.8, and 3.2.8.
15
+
16
+
17
+ Example
18
+ =======
19
+
20
+ Txn.all.each {|txn| .. do something with txn.txn_type ..}
21
+ - would load each txn_type individually
22
+
23
+ Txn.all(:include => :txn_type).each {|txn| .. do something with txn.txn_type ..}
24
+ - would load the txn_types in one go after the txns query, but would still need
25
+ a query every time you load txns
26
+
27
+
28
+ But if you use:
29
+
30
+ class TxnType
31
+ constant_table
32
+ end
33
+
34
+ Txn.all.each {|txn| .. do something with txn.txn_type ..}
35
+ - no longer requires individual txn_type loads, just every time you start the
36
+ server (or every request, in development mode)
37
+
38
+ TxnType.all
39
+ - also cached, but:
40
+
41
+ TxnType.all(:conditions => "name LIKE '%foo%'")
42
+ TxnType.find(2, :lock => true)
43
+ - all still result in traditional finds, since you gave options
44
+
45
+
46
+ You can also use:
47
+
48
+ class TxnType
49
+ constant_table :name => :label
50
+ end
51
+
52
+ Which if you have:
53
+
54
+ TxnType.create!(:label => "Customer Purchase")
55
+ TxnType.create!(:label => "Refund")
56
+
57
+ Means you will also have methods returning those records:
58
+
59
+ TxnType.customer_purchase
60
+ TxnType.refund
61
+
62
+ Optionally, you can specify a :name_prefix and/or :name_suffix.
63
+
64
+
65
+ Copyright (c) 2010-2012 Will Bryant, Sekuda Ltd, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => :test
9
+
10
+ desc 'Test the constant_table_saver plugin.'
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << 'lib'
13
+ t.libs << 'test'
14
+ t.pattern = 'test/*_test.rb'
15
+ t.verbose = true
16
+ end
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/constant_table_saver/version', __FILE__)
3
+
4
+ spec = Gem::Specification.new do |gem|
5
+ gem.name = 'constant_table_saver'
6
+ gem.version = ConstantTableSaver::VERSION
7
+ gem.summary = "Caches the records from fixed tables, and provides convenience methods to get them."
8
+ gem.description = <<-EOF
9
+ Loads all records from the table on first use, and thereafter returns the
10
+ cached (and frozen) records for all find calls.
11
+
12
+ Optionally, creates class-level methods you can use to grab the records,
13
+ named after the name field you specify.
14
+
15
+
16
+ Compatibility
17
+ =============
18
+
19
+ Currently tested against Rails 2.3.14, 3.0.17, 3.1.8, and 3.2.8.
20
+ EOF
21
+ gem.has_rdoc = false
22
+ gem.author = "Will Bryant"
23
+ gem.email = "will.bryant@gmail.com"
24
+ gem.homepage = "http://github.com/willbryant/constant_table_saver"
25
+
26
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
27
+ gem.files = `git ls-files`.split("\n")
28
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
29
+ gem.require_path = "lib"
30
+
31
+ gem.add_dependency "activerecord"
32
+ gem.add_development_dependency "rake"
33
+ gem.add_development_dependency "sqlite3"
34
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'constant_table_saver'
@@ -0,0 +1,3 @@
1
+ module ConstantTableSaver
2
+ VERSION = '2.0.0'
3
+ end
@@ -0,0 +1,209 @@
1
+ require 'active_record/fixtures' # so we can hook it & reset our cache afterwards
2
+
3
+ module ConstantTableSaver
4
+ module BaseMethods
5
+ def constant_table(options = {})
6
+ options.assert_valid_keys(:name, :name_prefix, :name_suffix)
7
+ class_attribute :constant_table_options, :instance_writer => false
8
+ self.constant_table_options = options
9
+
10
+ if ActiveRecord::VERSION::MAJOR > 2
11
+ require 'active_support/core_ext/object/to_param'
12
+ extend ActiveRecord3ClassMethods
13
+ else
14
+ extend ActiveRecord2ClassMethods
15
+ end
16
+ extend ClassMethods
17
+ extend NameClassMethods if constant_table_options[:name]
18
+
19
+ klass = defined?(Fixtures) ? Fixtures : ActiveRecord::Fixtures
20
+ class <<klass
21
+ # normally, create_fixtures method gets called exactly once - but unfortunately, it
22
+ # loads the class and does a #respond_to?, which causes us to load and cache before
23
+ # the new records are added, so we need to reset our cache afterwards.
24
+ def create_fixtures_with_constant_tables(*args)
25
+ create_fixtures_without_constant_tables(*args).tap { ConstantTableSaver.reset_all_caches }
26
+ end
27
+ def reset_cache_with_constant_tables(*args)
28
+ reset_cache_without_constant_tables(*args).tap { ConstantTableSaver.reset_all_caches }
29
+ end
30
+ alias_method_chain :create_fixtures, :constant_tables
31
+ alias_method_chain :reset_cache, :constant_tables
32
+ end unless klass.respond_to?(:create_fixtures_with_constant_tables)
33
+ end
34
+ end
35
+
36
+ def self.reset_all_caches
37
+ klasses = ActiveRecord::Base.respond_to?(:descendants) ? ActiveRecord::Base.descendants : ActiveRecord::Base.send(:subclasses)
38
+ klasses.each {|klass| klass.reset_constant_record_cache! if klass.respond_to?(:reset_constant_record_cache!)}
39
+ end
40
+
41
+ module ClassMethods
42
+ # Resets the cached records. Remember that this only affects this process, so while this
43
+ # is useful for running tests, it's unlikely that you can use this in production - you
44
+ # would need to call it on every Rails instance on every Rails server. Don't use this
45
+ # plugin on if the table isn't really constant!
46
+ def reset_constant_record_cache!
47
+ @constant_record_methods.each {|method_id| (class << self; self; end;).send(:remove_method, method_id)} if @constant_record_methods
48
+ @cached_records = @cached_records_by_id = @constant_record_methods = @cached_blank_scope = nil
49
+ end
50
+ end
51
+
52
+ module ActiveRecord3ClassMethods
53
+ def scoped(options = nil)
54
+ return super if options
55
+ return super if respond_to?(:current_scoped_methods) && current_scoped_methods
56
+ return super if respond_to?(:current_scope) && current_scope
57
+ @cached_blank_scope ||= super.tap do |s|
58
+ class << s
59
+ def to_a
60
+ return @records if loaded?
61
+ super.each(&:freeze)
62
+ end
63
+
64
+ def find(*args)
65
+ # annoyingly, the call to find to load a belongs_to passes :conditions => nil, which causes
66
+ # the base find method to apply_finder_options and construct an entire new scope, which is
67
+ # unnecessary and also means that it bypasses our find_one implementation (we don't interfere
68
+ # with scopes or finds that actually do apply conditions etc.), so we check as a special case
69
+ return find_with_ids(args.first) if args.length == 2 && args.last == {:conditions => nil}
70
+ super
71
+ end
72
+
73
+ def find_first
74
+ # the normal scope implementation would cache this anyway, but we force a load of all records,
75
+ # since otherwise if the app used .first before using .all there'd be unnecessary queries
76
+ to_a.first
77
+ end
78
+
79
+ def find_last
80
+ # as for find_first
81
+ to_a.last
82
+ end
83
+
84
+ def find_one(id)
85
+ # see below re to_param
86
+ cached_records_by_id[id.to_param] || raise(::ActiveRecord::RecordNotFound, "Couldn't find #{name} with ID=#{id}")
87
+ end
88
+
89
+ def find_some(ids)
90
+ # see below re to_param
91
+ ids.collect {|id| cached_records_by_id[id.to_param]}.tap do |results| # obviously since find_one caches efficiently, this isn't inefficient as it would be for real finds
92
+ results.compact!
93
+ raise(::ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs #{ids.join ','} (found #{results.size} results, but was looking for #{ids.size}") unless results.size == ids.size
94
+ end
95
+ end
96
+
97
+ # in Rails 3.1 the associations code was rewritten to generalise its sql generation to support
98
+ # more complex relationships (eg. nested :through associations). however unfortunately, during
99
+ # this work the implementation of belongs_to associations was changed so that it no longer calls
100
+ # one of the basic find_ methods above; instead a vanilla target scope is constructed, a where()
101
+ # scope to add the constraint that the primary key = the FK value is constructed, the two are
102
+ # merged, and then #first is called on that scope. frustratingly, all this complexity means that
103
+ # our find_ hooks above are no longer called when dereferencing a belongs_to association; they
104
+ # work fine and are used elsewhere, but we have to explicitly handle belongs_to target scope
105
+ # merging to avoid those querying, which is a huge PITA. because we want to ensure that we don't
106
+ # end up accidentally caching other scope requests, we explicitly build a list of the possible
107
+ # ARel constrained scopes - indexing them by their expression in SQL so that we don't need to
108
+ # code in the list of all the possible ARel terms. we then go one step further and make this
109
+ # cached scope pre-loaded using the record we already have - there's sadly no external way to do
110
+ # this so we have to shove in the instance variables.
111
+ #
112
+ # it will be clear that this was a very problematic ActiveRecord refactoring.
113
+ if ActiveRecord::VERSION::MINOR > 0
114
+ def belongs_to_record_scopes
115
+ @belongs_to_record_scopes ||= to_a.each_with_object({}) do |record, results|
116
+ scope_that_belongs_to_will_want = where(table[primary_key].eq(record.id))
117
+ scope_that_belongs_to_will_want.instance_variable_set("@loaded", true)
118
+ scope_that_belongs_to_will_want.instance_variable_set("@records", [record])
119
+ results[scope_that_belongs_to_will_want.to_sql] = scope_that_belongs_to_will_want
120
+ end.freeze
121
+ end
122
+
123
+ def merge(other)
124
+ if belongs_to_record_scope = belongs_to_record_scopes[other.to_sql]
125
+ return belongs_to_record_scope
126
+ end
127
+
128
+ super other
129
+ end
130
+ end
131
+
132
+ private
133
+ def cached_records_by_id
134
+ # we'd like to use the same as ActiveRecord's finder_methods.rb, which uses:
135
+ # id = id.id if ActiveRecord::Base === id
136
+ # but referencing ActiveRecord::Base here segfaults my ruby 1.8.7
137
+ # (2009-06-12 patchlevel 174) [universal-darwin10.0]! instead we use to_param.
138
+ @cached_records_by_id ||= all.index_by {|record| record.id.to_param}
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ module ActiveRecord2ClassMethods
146
+ def find(*args)
147
+ options = args.last if args.last.is_a?(Hash)
148
+ return super unless options.blank? || options.all? {|k, v| v.nil?}
149
+ scope_options = scope(:find)
150
+ return super unless scope_options.blank? || scope_options.all? {|k, v| v.nil?}
151
+
152
+ args.pop unless options.nil?
153
+
154
+ @cached_records ||= super(:all, :order => primary_key).each(&:freeze)
155
+ @cached_records_by_id ||= @cached_records.index_by {|record| record.id.to_param}
156
+
157
+ case args.first
158
+ when :first then @cached_records.first
159
+ when :last then @cached_records.last
160
+ when :all then @cached_records.dup # shallow copy of the array
161
+ else
162
+ expects_array = args.first.kind_of?(Array)
163
+ return args.first if expects_array && args.first.empty?
164
+ ids = expects_array ? args.first : args
165
+ ids = ids.flatten.compact.uniq
166
+
167
+ case ids.size
168
+ when 0
169
+ raise ::ActiveRecord::RecordNotFound, "Couldn't find #{name} without an ID"
170
+ when 1
171
+ result = @cached_records_by_id[ids.first.to_param] || raise(::ActiveRecord::RecordNotFound, "Couldn't find #{name} with ID=#{ids.first}")
172
+ expects_array ? [result] : result
173
+ else
174
+ ids.collect {|id| @cached_records_by_id[id.to_param]}.tap do |results|
175
+ results.compact!
176
+ raise(::ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs #{ids.join ','} (found #{results.size} results, but was looking for #{ids.size}") unless results.size == ids.size
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ module NameClassMethods
184
+ def define_named_record_methods
185
+ @constant_record_methods = [] # dummy so respond_to? & method_missing don't call us again if reading an attribute causes another method_missing
186
+ @constant_record_methods = all.collect do |record|
187
+ method_name = "#{constant_table_options[:name_prefix]}#{record[constant_table_options[:name]].downcase.gsub(/\W+/, '_')}#{constant_table_options[:name_suffix]}"
188
+ next if method_name.blank?
189
+ (class << self; self; end;).instance_eval { define_method(method_name) { record } }
190
+ method_name.to_sym
191
+ end.compact.uniq
192
+ end
193
+
194
+ def respond_to?(method_id, include_private = false)
195
+ super || (@constant_record_methods.nil? && define_named_record_methods && super)
196
+ end
197
+
198
+ def method_missing(method_id, *arguments, &block)
199
+ if @constant_record_methods.nil?
200
+ define_named_record_methods
201
+ send(method_id, *arguments, &block) # retry
202
+ else
203
+ super
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ ActiveRecord::Base.send(:extend, ConstantTableSaver::BaseMethods)
@@ -0,0 +1,70 @@
1
+ if ActiveRecord::VERSION::MAJOR < 3 || (ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR < 1)
2
+ # proudly stolen from ActiveRecord's test suite, with addition of BEGIN and COMMIT
3
+ ActiveRecord::Base.connection.class.class_eval do
4
+ IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /SHOW FIELDS/, /^BEGIN$/, /^COMMIT$/]
5
+
6
+ def execute_with_query_record(sql, name = nil, &block)
7
+ $queries_executed ||= []
8
+ $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r }
9
+ execute_without_query_record(sql, name, &block)
10
+ end
11
+
12
+ alias_method_chain :execute, :query_record
13
+ end
14
+ elsif ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR < 2
15
+ # this is from 3.1's test suite. ugly.
16
+ class ActiveRecord::SQLCounter
17
+ cattr_accessor :ignored_sql
18
+ self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
19
+
20
+ # FIXME: this needs to be refactored so specific database can add their own
21
+ # ignored SQL. This ignored SQL is for Oracle.
22
+ ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
23
+
24
+ def initialize
25
+ $queries_executed = []
26
+ end
27
+
28
+ def call(name, start, finish, message_id, values)
29
+ sql = values[:sql]
30
+
31
+ # FIXME: this seems bad. we should probably have a better way to indicate
32
+ # the query was cached
33
+ unless ['CACHE', 'SCHEMA'].include?(values[:name]) # we have altered this from the original, to exclude SCHEMA as well
34
+ # debugger if sql =~ /^PRAGMA table_info/ && Kernel.caller.any? {|i| i.include?('test_it_creates_named_class_methods_if_a_')}
35
+ $queries_executed << sql unless self.class.ignored_sql.any? { |r| sql =~ r }
36
+ end
37
+ end
38
+ end
39
+ ActiveSupport::Notifications.subscribe('sql.active_record', ActiveRecord::SQLCounter.new)
40
+ else
41
+ # this is from 3.2's test suite. ugly.
42
+ class ActiveRecord::SQLCounter
43
+ cattr_accessor :ignored_sql
44
+ self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
45
+
46
+ # FIXME: this needs to be refactored so specific database can add their own
47
+ # ignored SQL. This ignored SQL is for Oracle.
48
+ ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
49
+
50
+ cattr_accessor :log
51
+ self.log = []
52
+
53
+ attr_reader :ignore
54
+
55
+ def initialize(ignore = self.class.ignored_sql)
56
+ @ignore = ignore
57
+ end
58
+
59
+ def call(name, start, finish, message_id, values)
60
+ sql = values[:sql]
61
+
62
+ # FIXME: this seems bad. we should probably have a better way to indicate
63
+ # the query was cached
64
+ return if 'CACHE' == values[:name] || ignore.any? { |x| x =~ sql }
65
+ self.class.log << sql
66
+ end
67
+ end
68
+
69
+ ActiveSupport::Notifications.subscribe('sql.active_record', ActiveRecord::SQLCounter.new)
70
+ end
@@ -0,0 +1,230 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2
+ require File.expand_path(File.join(File.dirname(__FILE__), 'activerecord_count_queries'))
3
+
4
+ class StandardPie < ActiveRecord::Base
5
+ set_table_name "pies"
6
+ end
7
+
8
+ class ConstantPie < ActiveRecord::Base
9
+ set_table_name "pies"
10
+ constant_table
11
+
12
+ if defined?(named_scope)
13
+ named_scope :filled_with_unicorn, :conditions => {:filling => 'unicorn'}
14
+ else
15
+ scope :filled_with_unicorn, :conditions => {:filling => 'unicorn'}
16
+ end
17
+
18
+ def self.with_unicorn_filling_scope
19
+ with_scope(:find => {:conditions => {:filling => 'unicorn'}}) { yield }
20
+ end
21
+ end
22
+
23
+ class ConstantNamedPie < ActiveRecord::Base
24
+ set_table_name "pies"
25
+ constant_table :name => :filling
26
+ end
27
+
28
+ class ConstantLongNamedPie < ActiveRecord::Base
29
+ set_table_name "pies"
30
+ constant_table :name => :filling, :name_prefix => "a_", :name_suffix => "_pie"
31
+ end
32
+
33
+ class IngredientForStandardPie < ActiveRecord::Base
34
+ set_table_name "ingredients"
35
+ belongs_to :pie, :class_name => "StandardPie"
36
+ end
37
+
38
+ class IngredientForConstantPie < ActiveRecord::Base
39
+ set_table_name "ingredients"
40
+ belongs_to :pie, :class_name => "ConstantPie"
41
+ end
42
+
43
+ class ConstantTableSaverTest < ActiveRecord::TestCase
44
+ fixtures :all
45
+
46
+ setup do
47
+ ConstantPie.reset_constant_record_cache!
48
+ end
49
+
50
+ test "it caches find(:all) results" do
51
+ @pies = StandardPie.find(:all)
52
+ assert_queries(1) do
53
+ assert_equal @pies.collect(&:attributes), ConstantPie.find(:all).collect(&:attributes)
54
+ end
55
+ assert_no_queries do
56
+ assert_equal @pies.collect(&:attributes), ConstantPie.find(:all).collect(&:attributes)
57
+ end
58
+ end
59
+
60
+ test "it caches all() results" do
61
+ @pies = StandardPie.all
62
+ assert_queries(1) do
63
+ assert_equal @pies.collect(&:attributes), ConstantPie.all.collect(&:attributes)
64
+ end
65
+ assert_no_queries do
66
+ assert_equal @pies.collect(&:attributes), ConstantPie.all.collect(&:attributes)
67
+ end
68
+ end
69
+
70
+ test "it caches find(id) results" do
71
+ @pie = StandardPie.find(1)
72
+ assert_queries(1) do
73
+ assert_equal @pie.attributes, ConstantPie.find(1).attributes
74
+ end
75
+ assert_no_queries do
76
+ assert_equal @pie.attributes, ConstantPie.find(1).attributes
77
+ end
78
+ end
79
+
80
+ test "it caches find(ids) results" do
81
+ @pie1 = StandardPie.find(1)
82
+ @pie2 = StandardPie.find(2)
83
+ assert_queries(1) do
84
+ assert_equal [@pie1.attributes, @pie2.attributes], ConstantPie.find([1, 2]).collect(&:attributes)
85
+ end
86
+ assert_no_queries do
87
+ assert_equal [@pie1.attributes, @pie2.attributes], ConstantPie.find([1, 2]).collect(&:attributes)
88
+ end
89
+ end
90
+
91
+ test "it caches find(:first) results" do
92
+ @pie = StandardPie.find(:first)
93
+ assert_queries(1) do
94
+ assert_equal @pie.attributes, ConstantPie.find(:first).attributes
95
+ end
96
+ assert_no_queries do
97
+ assert_equal @pie.attributes, ConstantPie.find(:first).attributes
98
+ end
99
+ end
100
+
101
+ test "it caches first() results" do
102
+ @pie = StandardPie.first
103
+ assert_queries(1) do
104
+ assert_equal @pie.attributes, ConstantPie.first.attributes
105
+ end
106
+ assert_no_queries do
107
+ assert_equal @pie.attributes, ConstantPie.first.attributes
108
+ end
109
+ end
110
+
111
+ test "it caches find(:last) results" do
112
+ @pie = StandardPie.find(:last)
113
+ assert_queries(1) do
114
+ assert_equal @pie.attributes, ConstantPie.find(:last).attributes
115
+ end
116
+ assert_no_queries do
117
+ assert_equal @pie.attributes, ConstantPie.find(:last).attributes
118
+ end
119
+ end
120
+
121
+ test "it caches last() results" do
122
+ @pie = StandardPie.last
123
+ assert_queries(1) do
124
+ assert_equal @pie.attributes, ConstantPie.last.attributes
125
+ end
126
+ assert_no_queries do
127
+ assert_equal @pie.attributes, ConstantPie.last.attributes
128
+ end
129
+ end
130
+
131
+ test "it caches belongs_to association find queries" do
132
+ @standard_pie_ingredients = IngredientForStandardPie.all
133
+ @standard_pies = @standard_pie_ingredients.collect(&:pie)
134
+ @constant_pie_ingredients = IngredientForConstantPie.all
135
+ assert_queries(1) do # doesn't need to make 3 queries for 3 pie assocations!
136
+ assert_equal @standard_pies.collect(&:attributes), @constant_pie_ingredients.collect(&:pie).collect(&:attributes)
137
+ end
138
+ assert_no_queries do # and once cached, needs no more
139
+ assert_equal @standard_pies.collect(&:attributes), @constant_pie_ingredients.collect(&:pie).collect(&:attributes)
140
+ end
141
+ end
142
+
143
+ test "it isn't affected by scopes active at the time of first load" do
144
+ assert_equal 0, ConstantPie.filled_with_unicorn.all.size
145
+ assert_equal 0, ConstantPie.with_unicorn_filling_scope { ConstantPie.all.length }
146
+ assert_equal StandardPie.all.size, ConstantPie.all.size
147
+ end
148
+
149
+ test "it isn't affected by relational algebra active at the time of first load" do
150
+ assert_equal 0, ConstantPie.filled_with_unicorn.all.size
151
+ assert_equal 0, ConstantPie.where(:filling => 'unicorn').all.length
152
+ assert_equal 2, ConstantPie.where("filling LIKE 'Tasty%'").all.length
153
+ assert_equal StandardPie.all.size, ConstantPie.all.size
154
+ end if ActiveRecord::VERSION::MAJOR > 2
155
+
156
+ test "prevents the returned records from modification" do
157
+ @pie = ConstantPie.find(:first)
158
+ assert @pie.frozen?
159
+ assert !StandardPie.find(:first).frozen?
160
+ end
161
+
162
+ test "isn't affected by modifying the returned result arrays" do
163
+ @pies = ConstantPie.all
164
+ @pies.reject! {|pie| pie.filling =~ /Steak/}
165
+ assert_equal StandardPie.all.collect(&:attributes), ConstantPie.all.collect(&:attributes)
166
+ end
167
+
168
+ test "it doesn't cache find queries with options" do
169
+ @pies = StandardPie.find(:all, :lock => true)
170
+ @pie = StandardPie.find(1, :lock => true)
171
+ assert_queries(2) do
172
+ assert_equal @pies.collect(&:attributes), ConstantPie.find(:all, :lock => true).collect(&:attributes)
173
+ assert_equal @pie.attributes, ConstantPie.find(1, :lock => true).attributes
174
+ end
175
+ assert_queries(2) do
176
+ assert_equal @pies.collect(&:attributes), ConstantPie.find(:all, :lock => true).collect(&:attributes)
177
+ assert_equal @pie.attributes, ConstantPie.find(1, :lock => true).attributes
178
+ end
179
+ end
180
+
181
+ test "it passes the options preventing caching to the underlying query methods" do
182
+ assert_equal nil, ConstantPie.find(:first, :conditions => {:filling => 'unicorn'})
183
+ assert_equal [], ConstantPie.find(:all, :conditions => {:filling => 'unicorn'})
184
+ end
185
+
186
+ test "it creates named class methods if a :name option is given" do
187
+ @steak_pie = StandardPie.find_by_filling("Tasty beef steak")
188
+ @mushroom_pie = StandardPie.find_by_filling("Tasty mushrooms with tarragon")
189
+ @mince_pie = StandardPie.find_by_filling("Mince")
190
+ assert_queries(1) do
191
+ assert_equal @steak_pie.attributes, ConstantNamedPie.tasty_beef_steak.attributes
192
+ assert_equal @mushroom_pie.attributes, ConstantNamedPie.tasty_mushrooms_with_tarragon.attributes
193
+ assert_equal @mince_pie.attributes, ConstantNamedPie.mince.attributes
194
+ end
195
+ assert_raises(NoMethodError) do
196
+ ConstantNamedPie.unicorn_and_thyme
197
+ end
198
+ assert_raises(NoMethodError) do
199
+ ConstantPie.tasty_beef_steak
200
+ end
201
+ assert_raises(NoMethodError) do
202
+ ActiveRecord::Base.tasty_beef_steak
203
+ end
204
+ end
205
+
206
+ test "it supports :name_prefix and :name_suffix options" do
207
+ @steak_pie = StandardPie.find_by_filling("Tasty beef steak")
208
+ assert_equal @steak_pie.attributes, ConstantLongNamedPie.a_tasty_beef_steak_pie.attributes
209
+ end
210
+
211
+ test "it raises the usual exception if asked for a record with id nil" do
212
+ assert_raises ActiveRecord::RecordNotFound do
213
+ ConstantPie.find(nil)
214
+ end
215
+ end
216
+
217
+ test "it raises the usual exception if asked for a nonexistant records" do
218
+ max_id = ConstantPie.all.collect(&:id).max
219
+ assert_raises ActiveRecord::RecordNotFound do
220
+ ConstantPie.find(max_id + 1)
221
+ end
222
+ end
223
+
224
+ test "it raises the usual exception if asked for a mixture of present records and nonexistant records" do
225
+ max_id = ConstantPie.all.collect(&:id).max
226
+ assert_raises ActiveRecord::RecordNotFound do
227
+ ConstantPie.find([max_id, max_id + 1])
228
+ end
229
+ end
230
+ end
data/test/database.yml ADDED
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: test/constant_table_saver.db
@@ -0,0 +1,12 @@
1
+ steak:
2
+ id: 1
3
+ pie_id: 1
4
+ name: Steak
5
+ mushrooms:
6
+ id: 2
7
+ pie_id: 2
8
+ name: Mushrooms
9
+ tarragon:
10
+ id: 3
11
+ pie_id: 2
12
+ name: Tarragon
@@ -0,0 +1,9 @@
1
+ steak:
2
+ id: 1
3
+ filling: Tasty beef steak
4
+ mushroom_and_tarragon:
5
+ id: 2
6
+ filling: Tasty mushrooms with tarragon
7
+ mince:
8
+ id: 3
9
+ filling: Mince
data/test/schema.rb ADDED
@@ -0,0 +1,10 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+ create_table :pies, :force => true do |t|
3
+ t.string :filling, :null => false
4
+ end
5
+
6
+ create_table :ingredients, :force => true do |t|
7
+ t.integer :pie_id, :null => false
8
+ t.string :name, :null => false
9
+ end
10
+ end
@@ -0,0 +1,31 @@
1
+ if File.exist?("../../../config/boot.rb")
2
+ require "../../../config/boot.rb"
3
+ else
4
+ require 'rubygems'
5
+ end
6
+
7
+ gem 'activesupport', ENV['RAILS_VERSION']
8
+ gem 'activerecord', ENV['RAILS_VERSION']
9
+
10
+ require 'test/unit'
11
+ require 'active_support'
12
+ require 'active_support/test_case'
13
+ require 'active_record'
14
+ require 'active_record/fixtures'
15
+
16
+ begin
17
+ require 'ruby-debug'
18
+ Debugger.start
19
+ rescue LoadError
20
+ # ruby-debug not installed, no debugging for you
21
+ end
22
+
23
+ RAILS_ENV = ENV['RAILS_ENV'] ||= 'test'
24
+
25
+ ActiveRecord::Base.configurations = YAML::load(IO.read(File.join(File.dirname(__FILE__), "database.yml")))
26
+ ActiveRecord::Base.establish_connection ActiveRecord::Base.configurations[ENV['RAILS_ENV']]
27
+ load(File.join(File.dirname(__FILE__), "/schema.rb"))
28
+ ActiveSupport::TestCase.send(:include, ActiveRecord::TestFixtures) if ActiveRecord.const_defined?('TestFixtures')
29
+ ActiveSupport::TestCase.fixture_path = File.join(File.dirname(__FILE__), "fixtures")
30
+
31
+ require File.expand_path(File.join(File.dirname(__FILE__), '../init')) # load the plugin
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: constant_table_saver
3
+ version: !ruby/object:Gem::Version
4
+ hash: 15
5
+ prerelease:
6
+ segments:
7
+ - 2
8
+ - 0
9
+ - 0
10
+ version: 2.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Will Bryant
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-08-19 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activerecord
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rake
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: sqlite3
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :development
61
+ version_requirements: *id003
62
+ description: |
63
+ Loads all records from the table on first use, and thereafter returns the
64
+ cached (and frozen) records for all find calls.
65
+
66
+ Optionally, creates class-level methods you can use to grab the records,
67
+ named after the name field you specify.
68
+
69
+
70
+ Compatibility
71
+ =============
72
+
73
+ Currently tested against Rails 2.3.14, 3.0.17, 3.1.8, and 3.2.8.
74
+
75
+ email: will.bryant@gmail.com
76
+ executables: []
77
+
78
+ extensions: []
79
+
80
+ extra_rdoc_files: []
81
+
82
+ files:
83
+ - .gitignore
84
+ - Gemfile
85
+ - MIT-LICENSE
86
+ - README
87
+ - Rakefile
88
+ - constant_table_saver.gemspec
89
+ - init.rb
90
+ - lib/constant_table_saver.rb
91
+ - lib/constant_table_saver/version.rb
92
+ - test/activerecord_count_queries.rb
93
+ - test/constant_table_saver_test.rb
94
+ - test/database.yml
95
+ - test/fixtures/ingredients.yml
96
+ - test/fixtures/pies.yml
97
+ - test/schema.rb
98
+ - test/test_helper.rb
99
+ homepage: http://github.com/willbryant/constant_table_saver
100
+ licenses: []
101
+
102
+ post_install_message:
103
+ rdoc_options: []
104
+
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ hash: 3
113
+ segments:
114
+ - 0
115
+ version: "0"
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ hash: 3
122
+ segments:
123
+ - 0
124
+ version: "0"
125
+ requirements: []
126
+
127
+ rubyforge_project:
128
+ rubygems_version: 1.8.15
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: Caches the records from fixed tables, and provides convenience methods to get them.
132
+ test_files:
133
+ - test/activerecord_count_queries.rb
134
+ - test/constant_table_saver_test.rb
135
+ - test/database.yml
136
+ - test/fixtures/ingredients.yml
137
+ - test/fixtures/pies.yml
138
+ - test/schema.rb
139
+ - test/test_helper.rb