activerecord_snapshot_view 0.9.2
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/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +53 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/lib/activerecord_snapshot_view.rb +2 -0
- data/lib/activerecord_snapshot_view/snapshot_view.rb +221 -0
- data/spec/activerecord_snapshot_view.rb +7 -0
- data/spec/spec_helper.rb +9 -0
- metadata +88 -0
data/.document
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 mccraig mccraig of the clan mccraig
|
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.rdoc
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
= activerecord_snapshot_view
|
2
|
+
|
3
|
+
activerecord_snapshot_view provides simple management of snapshot materialized views for ActiveRecord
|
4
|
+
|
5
|
+
a snapshot view is created by includeing the <tt>ActiveRecord::SnapshotView</tt> module into an ActiveRecord
|
6
|
+
class. new versions of the snapshot are created using the <tt>new_version</tt> class method
|
7
|
+
|
8
|
+
multiple versions of the view are maintained : there is
|
9
|
+
a single active version, a working version, and one or more history tables [by default 1]
|
10
|
+
|
11
|
+
the model's +table_name+ method is overwritten to reflect the active table, so all ActiveRecord
|
12
|
+
model methods, like finders, work transparently. the caveat is that you must use the +table_name+ method
|
13
|
+
to determine the current snapshot's table name when constructing SQL
|
14
|
+
|
15
|
+
to use, <tt>include ActiveRecord::SnapshotView</tt> into your model :
|
16
|
+
|
17
|
+
class Foo < ActiveRecord::Base
|
18
|
+
include ActiveRecord::SnapshotView
|
19
|
+
end
|
20
|
+
|
21
|
+
there are class methods to get the different table names, and manipulate the version :
|
22
|
+
|
23
|
+
* <tt>Foo.table_name</tt> : current active or working table name
|
24
|
+
* <tt>Foo.working_table_name</tt> : current working table name
|
25
|
+
* <tt>Foo.active_table_name</tt> : current active table name
|
26
|
+
* <tt>Foo.new_version(&block)</tt> : run +block+ to create a new version of the table. during execution of the block <tt>Foo.table_name</tt> will return the working table name. if +block+ completes without raising an exception, or if the exception is an ActiveRecord::SnapshotView::SaveWork, then the working table will become permanently globally active
|
27
|
+
|
28
|
+
the active table name is maintained as a row in an automatically created auxilliary table,
|
29
|
+
[<tt>foos_switch</tt> in the case of <tt>Foo</tt>], and this is updated in a transaction
|
30
|
+
|
31
|
+
the presence of the historical tables [which are recycled] means that even though a transaction
|
32
|
+
may advance_version, other transactions already in progress will continue to see the old
|
33
|
+
active table_name, and selects in progress will continue to completion without rollback [ provided
|
34
|
+
they don't take longer to complete than it takes to recycle all history tables ]
|
35
|
+
|
36
|
+
== Dependencies
|
37
|
+
|
38
|
+
MySQL only at the moment
|
39
|
+
|
40
|
+
== Note on Patches/Pull Requests
|
41
|
+
|
42
|
+
* Fork the project.
|
43
|
+
* Make your feature addition or bug fix.
|
44
|
+
* Add tests for it. This is important so I don't break it in a
|
45
|
+
future version unintentionally.
|
46
|
+
* Commit, do not mess with rakefile, version, or history.
|
47
|
+
(if you want to have your own version, that is fine but
|
48
|
+
bump version in a commit by itself I can ignore when I pull)
|
49
|
+
* Send me a pull request. Bonus points for topic branches.
|
50
|
+
|
51
|
+
== Copyright
|
52
|
+
|
53
|
+
Copyright (c) 2009 mccraig mccraig of the clan mccraig. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "activerecord_snapshot_view"
|
8
|
+
gem.summary = %Q{snapshot materialized view support for ActiveRecord}
|
9
|
+
gem.description = %Q{manage snapshot materialized views for ActiveRecord. multiple
|
10
|
+
versions of each view are maintained, a live version, a working
|
11
|
+
version and 1 or more history versions, all created and managed
|
12
|
+
automatically}
|
13
|
+
gem.email = "mccraigmccraig@googlemail.com"
|
14
|
+
gem.homepage = "http://github.com/mccraigmccraig/activerecord_snapshot_view"
|
15
|
+
gem.authors = ["mccraig mccraig of the clan mccraig"]
|
16
|
+
gem.add_development_dependency "rspec"
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'spec/rake/spectask'
|
25
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
26
|
+
spec.libs << 'lib' << 'spec'
|
27
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
28
|
+
end
|
29
|
+
|
30
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
31
|
+
spec.libs << 'lib' << 'spec'
|
32
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
33
|
+
spec.rcov = true
|
34
|
+
end
|
35
|
+
|
36
|
+
task :spec => :check_dependencies
|
37
|
+
|
38
|
+
task :default => :spec
|
39
|
+
|
40
|
+
require 'rake/rdoctask'
|
41
|
+
Rake::RDocTask.new do |rdoc|
|
42
|
+
if File.exist?('VERSION')
|
43
|
+
version = File.read('VERSION')
|
44
|
+
else
|
45
|
+
version = ""
|
46
|
+
end
|
47
|
+
|
48
|
+
rdoc.rdoc_dir = 'rdoc'
|
49
|
+
rdoc.title = "activerecord_snapshot_view #{version}"
|
50
|
+
rdoc.rdoc_files.include('README*')
|
51
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
52
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.9.2
|
@@ -0,0 +1,221 @@
|
|
1
|
+
# implements snapshot materialized views for ActiveRecord
|
2
|
+
#
|
3
|
+
# currently active version of a view, one or more historical
|
4
|
+
# versions and a working version. modifications are made
|
5
|
+
# by writing new data to the working version of the table
|
6
|
+
# and then switching the active version to what was the working version.
|
7
|
+
#
|
8
|
+
# each version is a separate database table, and there is a
|
9
|
+
# switch table with a single row which names the currently live
|
10
|
+
# version table in the database
|
11
|
+
|
12
|
+
module ActiveRecord
|
13
|
+
module SnapshotView
|
14
|
+
def self.included(mod)
|
15
|
+
mod.instance_eval do
|
16
|
+
class << self
|
17
|
+
include ClassMethods
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# if a block given to the +new_version+ method throws this exception,
|
23
|
+
# then the working table will still be made current
|
24
|
+
class SaveWork < Exception
|
25
|
+
attr_reader :cause
|
26
|
+
def initialize(cause=nil)
|
27
|
+
@cause = cause
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
ALPHABET = "abcdefghijklmnopqrstuvwxyz"
|
33
|
+
|
34
|
+
def ClassMethods.included(mod)
|
35
|
+
mod.instance_eval do
|
36
|
+
alias_method :org_table_name, :table_name
|
37
|
+
alias_method :table_name, :active_working_table_or_active_table_name
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# number of historical tables to keep around for posterity, or more likely
|
42
|
+
# to ensure running transactions aren't taken down by advance_version
|
43
|
+
# recreating a table. default 2
|
44
|
+
def historical_version_count
|
45
|
+
@historical_version_count || 2
|
46
|
+
end
|
47
|
+
|
48
|
+
# set the number of historical tables to keep around to ensure running
|
49
|
+
# transactions aren't interrupted by truncating working tables. 2 is default
|
50
|
+
def set_historical_version_count(count)
|
51
|
+
@historical_version_count = count
|
52
|
+
end
|
53
|
+
alias :historical_version_count= :set_historical_version_count
|
54
|
+
|
55
|
+
# hide the ActiveRecord::Base method, which redefines a table_name method,
|
56
|
+
# and instead capture the given name as the base_table_name
|
57
|
+
def set_table_name(name)
|
58
|
+
@base_table_name = name
|
59
|
+
end
|
60
|
+
alias :table_name= :set_table_name
|
61
|
+
|
62
|
+
def base_table_name
|
63
|
+
if !@base_table_name
|
64
|
+
@base_table_name = org_table_name
|
65
|
+
# the original table_name method re-aliases itself !
|
66
|
+
class << self
|
67
|
+
alias_method :table_name, :active_working_table_or_active_table_name
|
68
|
+
end
|
69
|
+
end
|
70
|
+
@base_table_name
|
71
|
+
end
|
72
|
+
|
73
|
+
# use schema of from table to recreate to table
|
74
|
+
def dup_table_schema(from, to)
|
75
|
+
connection.execute( "drop table if exists #{to}")
|
76
|
+
ct = connection.select_one( "show create table #{from}")["Create Table"]
|
77
|
+
ct_no_constraint_names = ct.gsub(/CONSTRAINT `[^`]*`/, "CONSTRAINT ``")
|
78
|
+
i = 0
|
79
|
+
ct_uniq_constraint_names = ct_no_constraint_names.gsub(/CONSTRAINT ``/) { |s| i+=1 ; "CONSTRAINT `#{to}_#{i}`" }
|
80
|
+
|
81
|
+
new_ct = ct_uniq_constraint_names.gsub( /CREATE TABLE `#{from}`/, "CREATE TABLE `#{to}`")
|
82
|
+
connection.execute(new_ct)
|
83
|
+
end
|
84
|
+
|
85
|
+
def ensure_version_table(name)
|
86
|
+
if !connection.table_exists?(name) &&
|
87
|
+
base_table_name!=name # don't execute ddl unless necessary
|
88
|
+
dup_table_schema(base_table_name, name)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# create a switch table of given name, if it doesn't already exist
|
93
|
+
def create_switch_table(name)
|
94
|
+
connection.execute( "create table if not exists #{name} (`current` varchar(255))" )
|
95
|
+
end
|
96
|
+
|
97
|
+
# create the switch table if it doesn't already exist. return the switch table name
|
98
|
+
def ensure_switch_table
|
99
|
+
stn = switch_table_name
|
100
|
+
if !connection.table_exists?(stn) # don't execute any ddl code if we don't need to
|
101
|
+
create_switch_table(stn)
|
102
|
+
end
|
103
|
+
stn
|
104
|
+
end
|
105
|
+
|
106
|
+
# name of the table with a row holding the active table name
|
107
|
+
def switch_table_name
|
108
|
+
base_table_name + "_switch"
|
109
|
+
end
|
110
|
+
|
111
|
+
# list of suffixed table names
|
112
|
+
def suffixed_table_names
|
113
|
+
suffixes = []
|
114
|
+
(0...historical_version_count).each{ |i| suffixes << ALPHABET[i...i+1] }
|
115
|
+
suffixes.map do |suffix|
|
116
|
+
base_table_name + "_" + suffix
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# ordered vector of table version names, starting with base name
|
121
|
+
def table_version_names
|
122
|
+
[base_table_name] + suffixed_table_names
|
123
|
+
end
|
124
|
+
|
125
|
+
def default_active_table_name
|
126
|
+
# no longer use a different table name for test environments...
|
127
|
+
# it makes a mess with named scopes
|
128
|
+
base_table_name
|
129
|
+
end
|
130
|
+
|
131
|
+
# name of the active table read direct from db
|
132
|
+
def active_table_name
|
133
|
+
st = switch_table_name
|
134
|
+
begin
|
135
|
+
connection.select_value( "select current from #{st}" )
|
136
|
+
rescue
|
137
|
+
end || default_active_table_name
|
138
|
+
end
|
139
|
+
|
140
|
+
# name of the working table
|
141
|
+
def working_table_name
|
142
|
+
atn = active_table_name
|
143
|
+
tvn = table_version_names
|
144
|
+
tvn[ (tvn.index(atn) + 1) % tvn.size ]
|
145
|
+
end
|
146
|
+
|
147
|
+
def ensure_all_tables
|
148
|
+
suffixed_table_names.each do |table_name|
|
149
|
+
ensure_version_table(table_name)
|
150
|
+
end
|
151
|
+
ensure_switch_table
|
152
|
+
end
|
153
|
+
|
154
|
+
# make working table active, then recreate new working table from base table schema
|
155
|
+
def advance_version
|
156
|
+
st = ensure_switch_table
|
157
|
+
|
158
|
+
# want a transaction at least here [surround is ok too] so
|
159
|
+
# there is never an empty switch table
|
160
|
+
ActiveRecord::Base.transaction do
|
161
|
+
wtn = working_table_name
|
162
|
+
connection.execute( "delete from #{st}")
|
163
|
+
connection.execute( "insert into #{st} values (\'#{wtn}\')")
|
164
|
+
end
|
165
|
+
|
166
|
+
# ensure the presence of the new active and working tables.
|
167
|
+
# happens after the switch table update, since this may commit a surrounding
|
168
|
+
# transaction in dbs with retarded non-transactional ddl like, oh i dunno, MyFuckingSQL
|
169
|
+
ensure_version_table(active_table_name)
|
170
|
+
|
171
|
+
# recreate the new working table from the base schema.
|
172
|
+
new_wtn = working_table_name
|
173
|
+
if new_wtn != base_table_name
|
174
|
+
dup_table_schema(base_table_name, new_wtn)
|
175
|
+
else
|
176
|
+
connection.execute( "truncate table #{new_wtn}" )
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def thread_local_key_name
|
181
|
+
"ActiveRecord::SnapshotView::" + self.to_s
|
182
|
+
end
|
183
|
+
|
184
|
+
def active_working_table_name
|
185
|
+
Thread.current[thread_local_key_name]
|
186
|
+
end
|
187
|
+
|
188
|
+
def active_working_table_name=(name)
|
189
|
+
Thread.current[thread_local_key_name] = name
|
190
|
+
end
|
191
|
+
|
192
|
+
# name of the active table, or the working table if inside a new_version block
|
193
|
+
def active_working_table_or_active_table_name
|
194
|
+
active_working_table_name || active_table_name
|
195
|
+
end
|
196
|
+
|
197
|
+
# make the working table temporarily active [ for this thread only ],
|
198
|
+
# execute the block, and if completed without exception then
|
199
|
+
# make the working table permanently active
|
200
|
+
def new_version(&block)
|
201
|
+
begin
|
202
|
+
self.active_working_table_name = working_table_name
|
203
|
+
ensure_version_table(working_table_name)
|
204
|
+
connection.execute("truncate table #{working_table_name}")
|
205
|
+
r = block.call
|
206
|
+
advance_version
|
207
|
+
r
|
208
|
+
rescue SaveWork => e
|
209
|
+
advance_version
|
210
|
+
if e.cause
|
211
|
+
raise e.cause
|
212
|
+
else
|
213
|
+
raise e
|
214
|
+
end
|
215
|
+
ensure
|
216
|
+
self.active_working_table_name = nil
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord_snapshot_view
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 9
|
8
|
+
- 2
|
9
|
+
version: 0.9.2
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- mccraig mccraig of the clan mccraig
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-05-11 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :development
|
31
|
+
version_requirements: *id001
|
32
|
+
description: |-
|
33
|
+
manage snapshot materialized views for ActiveRecord. multiple
|
34
|
+
versions of each view are maintained, a live version, a working
|
35
|
+
version and 1 or more history versions, all created and managed
|
36
|
+
automatically
|
37
|
+
email: mccraigmccraig@googlemail.com
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- LICENSE
|
44
|
+
- README.rdoc
|
45
|
+
files:
|
46
|
+
- .document
|
47
|
+
- .gitignore
|
48
|
+
- LICENSE
|
49
|
+
- README.rdoc
|
50
|
+
- Rakefile
|
51
|
+
- VERSION
|
52
|
+
- lib/activerecord_snapshot_view.rb
|
53
|
+
- lib/activerecord_snapshot_view/snapshot_view.rb
|
54
|
+
- spec/activerecord_snapshot_view.rb
|
55
|
+
- spec/spec_helper.rb
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://github.com/mccraigmccraig/activerecord_snapshot_view
|
58
|
+
licenses: []
|
59
|
+
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options:
|
62
|
+
- --charset=UTF-8
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
version: "0"
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
requirements: []
|
80
|
+
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 1.3.6
|
83
|
+
signing_key:
|
84
|
+
specification_version: 3
|
85
|
+
summary: snapshot materialized view support for ActiveRecord
|
86
|
+
test_files:
|
87
|
+
- spec/activerecord_snapshot_view.rb
|
88
|
+
- spec/spec_helper.rb
|