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 +5 -0
- data/Gemfile +15 -0
- data/MIT-LICENSE +20 -0
- data/README +65 -0
- data/Rakefile +16 -0
- data/constant_table_saver.gemspec +34 -0
- data/init.rb +1 -0
- data/lib/constant_table_saver/version.rb +3 -0
- data/lib/constant_table_saver.rb +209 -0
- data/test/activerecord_count_queries.rb +70 -0
- data/test/constant_table_saver_test.rb +230 -0
- data/test/database.yml +3 -0
- data/test/fixtures/ingredients.yml +12 -0
- data/test/fixtures/pies.yml +9 -0
- data/test/schema.rb +10 -0
- data/test/test_helper.rb +31 -0
- metadata +139 -0
data/.gitignore
ADDED
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,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
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
|
data/test/test_helper.rb
ADDED
@@ -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
|