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.
@@ -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