activerecord_worm_table 0.1.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/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +51 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/lib/activerecord_worm_table.rb +151 -0
- data/spec/activerecord_worm_table_spec.rb +7 -0
- data/spec/spec_helper.rb +9 -0
- metadata +80 -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,51 @@
|
|
1
|
+
= activerecord_worm_table
|
2
|
+
|
3
|
+
activerecord_worm_table manages multiple backing tables for an ActiveRecord model. there is
|
4
|
+
a single active table, a different working table, and one or more history tables [default 1]
|
5
|
+
|
6
|
+
inserts are done to the working table. the model's class method <tt>advance_version</tt> then makes
|
7
|
+
the the active table history, the working table active, and creates a new working table with
|
8
|
+
identical schema to the base table
|
9
|
+
|
10
|
+
the model's +table_name+ method is overwritten to reflect the active table, so all ActiveRecord
|
11
|
+
model methods, like finders, work transparently
|
12
|
+
|
13
|
+
to use, just <tt>include ActiveRecord::WormTable</tt> into your model :
|
14
|
+
|
15
|
+
class Foo \< ActiveRecord::Base
|
16
|
+
include ActiveRecord::WormTable
|
17
|
+
end
|
18
|
+
|
19
|
+
there are class methods to get the different table names, and manipulate the version :
|
20
|
+
|
21
|
+
* <tt>Foo.table_name</tt> : current active table name
|
22
|
+
* <tt>Foo.working_table_name</tt> : current working table name
|
23
|
+
* <tt>Foo.advance_version</tt> : make active table historical, working table active and create a new working table
|
24
|
+
|
25
|
+
the active table name is maintained as a row in an automatically created auxilliary table,
|
26
|
+
[<tt>foos_switch</tt> in the case of <tt>Foo</tt>], and this is updated in a transaction
|
27
|
+
|
28
|
+
the presence of the historical tables [which are recycled] means that even though a transaction
|
29
|
+
may advance_version, other transactions already in progress will continue to see the old
|
30
|
+
active table_name, and selects in progress will continue to completion without rollback [ provided
|
31
|
+
they don't take longer to complete than it takes to recycle all history tables ]
|
32
|
+
|
33
|
+
== Dependencies
|
34
|
+
|
35
|
+
only works with MySQL at the moment : ddl statements are too database specific, and rails' schema
|
36
|
+
operations too limited, to easily make it generic. wouldn't be too hard to extend for another db tho
|
37
|
+
|
38
|
+
== Note on Patches/Pull Requests
|
39
|
+
|
40
|
+
* Fork the project.
|
41
|
+
* Make your feature addition or bug fix.
|
42
|
+
* Add tests for it. This is important so I don't break it in a
|
43
|
+
future version unintentionally.
|
44
|
+
* Commit, do not mess with rakefile, version, or history.
|
45
|
+
(if you want to have your own version, that is fine but
|
46
|
+
bump version in a commit by itself I can ignore when I pull)
|
47
|
+
* Send me a pull request. Bonus points for topic branches.
|
48
|
+
|
49
|
+
== Copyright
|
50
|
+
|
51
|
+
Copyright (c) 2009 mccraig mccraig of the clan mccraig. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "activerecord_worm_table"
|
8
|
+
gem.summary = %Q{WORM tables for ActiveRecord models}
|
9
|
+
gem.description = %Q{manage WriteOnceReadMany tables backing ActiveRecord models.
|
10
|
+
there will be a switch table and multiple backing tables for each
|
11
|
+
ActiveRecord model, all created and managed automatically. a new
|
12
|
+
version of a table is created by writing to the working table, and
|
13
|
+
then Model.advance_version which makes the working table active,
|
14
|
+
and creates a new working table from the base model schmea}
|
15
|
+
gem.email = "mccraigmccraig@googlemail.com"
|
16
|
+
gem.homepage = "http://github.com/mccraigmccraig/activerecord_worm_table"
|
17
|
+
gem.authors = ["mccraig mccraig of the clan mccraig"]
|
18
|
+
gem.add_development_dependency "rspec"
|
19
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
20
|
+
end
|
21
|
+
Jeweler::GemcutterTasks.new
|
22
|
+
rescue LoadError
|
23
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'spec/rake/spectask'
|
27
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
28
|
+
spec.libs << 'lib' << 'spec'
|
29
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
30
|
+
end
|
31
|
+
|
32
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
33
|
+
spec.libs << 'lib' << 'spec'
|
34
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
35
|
+
spec.rcov = true
|
36
|
+
end
|
37
|
+
|
38
|
+
task :spec => :check_dependencies
|
39
|
+
|
40
|
+
task :default => :spec
|
41
|
+
|
42
|
+
require 'rake/rdoctask'
|
43
|
+
Rake::RDocTask.new do |rdoc|
|
44
|
+
if File.exist?('VERSION')
|
45
|
+
version = File.read('VERSION')
|
46
|
+
else
|
47
|
+
version = ""
|
48
|
+
end
|
49
|
+
|
50
|
+
rdoc.rdoc_dir = 'rdoc'
|
51
|
+
rdoc.title = "activerecord_worm_table #{version}"
|
52
|
+
rdoc.rdoc_files.include('README*')
|
53
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
54
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# implements a write-once-read-many table, wherein there is a
|
2
|
+
# currently active version of a table, one or more historical
|
3
|
+
# versions and a working version. modifications are made
|
4
|
+
# by writing new data to the working version of the table
|
5
|
+
# and then switching the active version to what was the working version.
|
6
|
+
#
|
7
|
+
# each version is a separate database table, and there is a
|
8
|
+
# switch table with a single row which names the currently live
|
9
|
+
# version table in the database
|
10
|
+
|
11
|
+
module ActiveRecord
|
12
|
+
module WormTable
|
13
|
+
def self.included(mod)
|
14
|
+
mod.instance_eval do
|
15
|
+
class << self
|
16
|
+
include ClassMethods
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
ALPHABET = "abcdefghijklmnopqrstuvwxyz"
|
23
|
+
|
24
|
+
def ClassMethods.included(mod)
|
25
|
+
mod.instance_eval do
|
26
|
+
alias_method :org_table_name, :table_name
|
27
|
+
alias_method :table_name, :active_table_name
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# hide the ActiveRecord::Base method, which redefines a table_name method,
|
32
|
+
# and instead capture the given name as the base_table_name
|
33
|
+
def set_table_name(name)
|
34
|
+
@base_table_name = name
|
35
|
+
end
|
36
|
+
alias :table_name= :set_table_name
|
37
|
+
|
38
|
+
def base_table_name
|
39
|
+
if !@base_table_name
|
40
|
+
@base_table_name = org_table_name
|
41
|
+
class << self
|
42
|
+
alias_method :table_name, :active_table_name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
@base_table_name
|
46
|
+
end
|
47
|
+
|
48
|
+
# number of historical tables to keep around for posterity, or more likely
|
49
|
+
# to ensure running transactions aren't taken down by advance_version
|
50
|
+
# recreating a table
|
51
|
+
def historical_version_count
|
52
|
+
@historical_version_count || 2
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_historical_version_count(count)
|
56
|
+
@historical_version_count = count
|
57
|
+
end
|
58
|
+
alias :historical_version_count= :set_historical_version_count
|
59
|
+
|
60
|
+
# use schema of from table to recreate to table
|
61
|
+
def dup_table_schema(from, to)
|
62
|
+
connection.execute( "drop table if exists #{to}")
|
63
|
+
ct = connection.select_one( "show create table #{from}")["Create Table"]
|
64
|
+
new_ct = ct.gsub( /CREATE TABLE `#{from}`/, "CREATE TABLE `#{to}`")
|
65
|
+
connection.execute(new_ct)
|
66
|
+
end
|
67
|
+
|
68
|
+
def ensure_active_table(name)
|
69
|
+
if !connection.table_exists?(name) # don't execute ddl unless necessary
|
70
|
+
dup_table_schema(base_table_name, name)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# create a switch table of given name, if it doesn't already exist
|
75
|
+
def create_switch_table(name)
|
76
|
+
connection.execute( "create table if not exists #{name} (`current` varchar(255))" )
|
77
|
+
end
|
78
|
+
|
79
|
+
# create the switch table if it doesn't already exist. return the switch table name
|
80
|
+
def ensure_switch_table
|
81
|
+
stn = switch_table_name
|
82
|
+
if !connection.table_exists?(stn) # don't execute any ddl code if we don't need to
|
83
|
+
create_switch_table(stn)
|
84
|
+
end
|
85
|
+
stn
|
86
|
+
end
|
87
|
+
|
88
|
+
# name of the table with a row holding the active table name
|
89
|
+
def switch_table_name
|
90
|
+
base_table_name + "_switch"
|
91
|
+
end
|
92
|
+
|
93
|
+
# list of suffixed table names
|
94
|
+
def suffixed_table_names
|
95
|
+
suffixes = []
|
96
|
+
(0...historical_version_count).each{ |i| suffixes << ALPHABET[i...i+1] }
|
97
|
+
suffixes.map do |suffix|
|
98
|
+
base_table_name + "_" + suffix
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# ordered vector of table version names, starting with base name
|
103
|
+
def table_version_names
|
104
|
+
[base_table_name] + suffixed_table_names
|
105
|
+
end
|
106
|
+
|
107
|
+
# name of the active table
|
108
|
+
def active_table_name
|
109
|
+
st = switch_table_name
|
110
|
+
begin
|
111
|
+
connection.select_value( "select current from #{st}" )
|
112
|
+
rescue
|
113
|
+
end || base_table_name
|
114
|
+
end
|
115
|
+
|
116
|
+
# name of the working table
|
117
|
+
def working_table_name
|
118
|
+
atn = active_table_name
|
119
|
+
tvn = table_version_names
|
120
|
+
tvn[ (tvn.index(atn) + 1) % tvn.size ]
|
121
|
+
end
|
122
|
+
|
123
|
+
# make working table active, then recreate new working table from base table schema
|
124
|
+
def advance_version
|
125
|
+
st = ensure_switch_table
|
126
|
+
|
127
|
+
# want a transaction at least here [surround is ok too] so
|
128
|
+
# there is never an empty switch table
|
129
|
+
ActiveRecord::Base.transaction do
|
130
|
+
wtn = working_table_name
|
131
|
+
connection.execute( "delete from #{st}")
|
132
|
+
connection.execute( "insert into #{st} values (\'#{wtn}\')")
|
133
|
+
end
|
134
|
+
|
135
|
+
# ensure the presence of the new active and working tables.
|
136
|
+
# happens after the switch table update, since this may commit a surrounding
|
137
|
+
# transaction in dbs with retarded non-transactional ddl like, oh i dunno, MyFuckingSQL
|
138
|
+
ensure_active_table(active_table_name)
|
139
|
+
|
140
|
+
# recreate the new working table from the base schema.
|
141
|
+
new_wtn = working_table_name
|
142
|
+
if new_wtn != base_table_name
|
143
|
+
dup_table_schema(base_table_name, new_wtn)
|
144
|
+
else
|
145
|
+
connection.execute( "truncate table #{new_wtn}" )
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord_worm_table
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- mccraig mccraig of the clan mccraig
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-04 00:00:00 +00:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description: |-
|
26
|
+
manage WriteOnceReadMany tables backing ActiveRecord models.
|
27
|
+
there will be a switch table and multiple backing tables for each
|
28
|
+
ActiveRecord model, all created and managed automatically. a new
|
29
|
+
version of a table is created by writing to the working table, and
|
30
|
+
then Model.advance_version which makes the working table active,
|
31
|
+
and creates a new working table from the base model schmea
|
32
|
+
email: mccraigmccraig@googlemail.com
|
33
|
+
executables: []
|
34
|
+
|
35
|
+
extensions: []
|
36
|
+
|
37
|
+
extra_rdoc_files:
|
38
|
+
- LICENSE
|
39
|
+
- README.rdoc
|
40
|
+
files:
|
41
|
+
- .document
|
42
|
+
- .gitignore
|
43
|
+
- LICENSE
|
44
|
+
- README.rdoc
|
45
|
+
- Rakefile
|
46
|
+
- VERSION
|
47
|
+
- lib/activerecord_worm_table.rb
|
48
|
+
- spec/activerecord_worm_table_spec.rb
|
49
|
+
- spec/spec_helper.rb
|
50
|
+
has_rdoc: true
|
51
|
+
homepage: http://github.com/mccraigmccraig/activerecord_worm_table
|
52
|
+
licenses: []
|
53
|
+
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options:
|
56
|
+
- --charset=UTF-8
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: "0"
|
70
|
+
version:
|
71
|
+
requirements: []
|
72
|
+
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 1.3.5
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: WORM tables for ActiveRecord models
|
78
|
+
test_files:
|
79
|
+
- spec/activerecord_worm_table_spec.rb
|
80
|
+
- spec/spec_helper.rb
|