activerecord_snapshot_view 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
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.
@@ -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.
@@ -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,2 @@
1
+ require 'active_record'
2
+ require 'activerecord_snapshot_view/snapshot_view'
@@ -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
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "ActiverecordSnapshotView" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'activerecord_worm_table/worm_table'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
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