constant_table_saver 2.0.0

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