activerecord-model-spaces 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,184 @@
1
+ require 'active_record/model_spaces/table_names'
2
+ require 'active_record/model_spaces/table_manager'
3
+ require 'active_record/model_spaces/util'
4
+
5
+ module ActiveRecord
6
+ module ModelSpaces
7
+
8
+ # holds the current and working tables for a ModelSpace
9
+ class Context
10
+ include Util
11
+
12
+ attr_reader :model_space
13
+ attr_reader :model_space_key
14
+ attr_reader :persistor
15
+ attr_reader :current_model_versions
16
+ attr_reader :working_model_versions
17
+
18
+ def initialize(model_space, model_space_key, persistor)
19
+ @model_space = model_space
20
+ @model_space_key = model_space_key.to_sym
21
+ @persistor = persistor
22
+ read_versions
23
+ create_tables
24
+ end
25
+
26
+ def read_versions
27
+ @current_model_versions = persistor.read_model_space_model_versions(model_space.name, model_space_key)
28
+ @working_model_versions = {}
29
+ end
30
+
31
+ def create_tables
32
+ model_space.registered_model_keys.map do |model_name|
33
+ m = Util.model_from_name(model_name)
34
+ tm = TableManager.new(m)
35
+ tm.create_table(base_table_name(m), current_table_name(m))
36
+ end
37
+ end
38
+
39
+ def self.drop_tables(model_space, model_space_key)
40
+ model_space.registered_model_keys.map do |model_name|
41
+ m = Util.model_from_name(model_name)
42
+ tm = TableManager.new(m)
43
+
44
+ all_table_names = (0..model_space.history_versions(m)).map do |v|
45
+ TableNames.table_name(model_space.name, model_space_key, model_space.base_table_name(m), model_space.history_versions(m), v)
46
+ end
47
+
48
+ all_table_names.each do |table_name|
49
+ tm.drop_table(table_name)
50
+ end
51
+ end
52
+ end
53
+
54
+ # implements the Model.table_name method
55
+ def table_name(model)
56
+ version = get_working_model_version(model) || get_current_model_version(model)
57
+ table_name_from_model_version(model, version)
58
+ end
59
+
60
+ # base table name
61
+ def base_table_name(model)
62
+ model_space.base_table_name(model)
63
+ end
64
+
65
+ # table_name for version 0
66
+ def hoovered_table_name(model)
67
+ table_name_from_model_version(model, 0)
68
+ end
69
+
70
+ # current table_name, seen by everyone outside of this context
71
+ def current_table_name(model)
72
+ table_name_from_model_version(model, get_current_model_version(model))
73
+ end
74
+
75
+ # table_name which would be seen by this context in or after a new_version/updated_version. always returns a name
76
+ def next_table_name(model)
77
+ current_version = get_current_model_version(model)
78
+ next_version = TableNames.next_version(model_space.history_versions(model), current_version)
79
+ table_name_from_model_version(model, next_version)
80
+ end
81
+
82
+ # table_name of working table, seen by this context in or after new_version/updated_version. null if no new_version/updated_version has been issued/completed
83
+ def working_table_name(model)
84
+ table_name_from_model_version(model, get_working_model_version(model)) if get_working_model_version(model)
85
+ end
86
+
87
+ def new_version(model, copy_old_version=false, &block)
88
+ raise "new_version: a block must be supplied" if !block
89
+
90
+ if get_working_model_version(model)
91
+ block.call # nothing to do
92
+ else
93
+ current_version = get_current_model_version(model)
94
+ next_version = TableNames.next_version(model_space.history_versions(model), current_version)
95
+
96
+ tm = TableManager.new(model)
97
+ ok = false
98
+ begin
99
+ btn = base_table_name(model)
100
+ ctn = current_table_name(model)
101
+ ntn = next_table_name(model)
102
+ if next_version != current_version
103
+ tm.recreate_table(btn, ntn)
104
+ tm.copy_table(ctn, ntn) if copy_old_version
105
+ else # no history
106
+ tm.truncate_table(ntn) if !copy_old_version
107
+ end
108
+ set_working_model_version(model, next_version)
109
+ r = block.call
110
+ ok = true
111
+ r
112
+ ensure
113
+ delete_working_model_version(model) if !ok
114
+ end
115
+ end
116
+ end
117
+
118
+ def updated_version(model, &block)
119
+ new_version(model, true, &block)
120
+ end
121
+
122
+ # copy all data to the base model-space tables and drop all history tables
123
+ def hoover
124
+ raise "can't hoover with active working versions: #{working_model_versions.keys.inspect}" if !working_model_versions.empty?
125
+
126
+ model_names = model_space.registered_model_keys
127
+
128
+ new_versions = Hash[ model_names.map do |model_name|
129
+ m = Util.model_from_name(model_name)
130
+ base_name = base_table_name(m)
131
+ current_name = current_table_name(m)
132
+ hoovered_name = hoovered_table_name(m)
133
+
134
+ tm = TableManager.new(m)
135
+
136
+ # copy to hoovered table
137
+ if current_name != hoovered_name
138
+ tm.recreate_table(base_name, hoovered_name)
139
+ tm.copy_table(current_name, hoovered_name)
140
+ end
141
+
142
+ # drop history tables
143
+ (1..model_space.history_versions(m)).map do |v|
144
+ htn = table_name_from_model_version(m, v)
145
+ tm.drop_table(htn)
146
+ end
147
+
148
+ [model_name, 0]
149
+ end ]
150
+ persistor.update_model_space_model_versions(new_versions)
151
+
152
+ read_versions
153
+ end
154
+
155
+ def commit
156
+ persistor.update_model_space_model_versions(model_space.name, model_space_key, current_model_versions.merge(working_model_versions))
157
+ end
158
+
159
+ private
160
+
161
+ def table_name_from_model_version(model, version)
162
+ TableNames.table_name(model_space.name, model_space_key, model_space.base_table_name(model), model_space.history_versions(model), version)
163
+ end
164
+
165
+ def get_current_model_version(model)
166
+ raise "#{model}: not registered with ModelSpace: #{model_space.name}" if !model_space.is_registered?(model)
167
+ self.current_model_versions[model_space.registered_model_name(model)] || 0
168
+ end
169
+
170
+ def get_working_model_version(model)
171
+ raise "#{model}: not registered with ModelSpace: #{model_space.name}" if !model_space.is_registered?(model)
172
+ self.working_model_versions[model_space.registered_model_name(model)]
173
+ end
174
+
175
+ def set_working_model_version(model, version)
176
+ self.working_model_versions[model_space.registered_model_name(model)] = version
177
+ end
178
+
179
+ def delete_working_model_version(model)
180
+ self.working_model_versions.delete(model_space.registered_model_name(model))
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,121 @@
1
+ require 'set'
2
+ require 'active_support'
3
+ require 'active_record/model_spaces/context'
4
+ require 'active_record/model_spaces/persistor'
5
+ require 'active_record/model_spaces/table_names'
6
+ require 'active_record/model_spaces/util'
7
+
8
+ module ActiveRecord
9
+ module ModelSpaces
10
+
11
+ class << self
12
+ attr_accessor :connection
13
+ attr_accessor :table_name
14
+ end
15
+
16
+ # a ModelSpace has a set of models registered with it,
17
+ # from which a Context can be created
18
+ class ModelSpace
19
+ include Util
20
+
21
+ attr_reader :name
22
+ attr_reader :model_registrations
23
+
24
+ def initialize(name)
25
+ @name = name.to_sym
26
+ @model_registrations = {}
27
+ end
28
+
29
+ def register_model(model, opts=nil)
30
+ raise "#{model} is not an ActiveRecord model Class" if !is_active_record_model?(model)
31
+ opts ||= {}
32
+ ModelSpaces.check_model_registration_keys(opts.keys)
33
+ set_model_registration(model, opts.merge(:model=>model))
34
+ self
35
+ end
36
+
37
+ def deregister_model(model)
38
+ delete_model_registration(model)
39
+ end
40
+
41
+ def history_versions(model)
42
+ get_model_registration(model)[:history_versions] || 0
43
+ end
44
+
45
+ def set_base_table_name(model, table_name)
46
+ get_model_registration(model)[:base_table_name] = table_name
47
+ end
48
+
49
+ def registered_model(model)
50
+ get_model_registration(model)[:model]
51
+ end
52
+
53
+ def registered_model_name(model)
54
+ name_from_model(get_model_registration(model)[:model])
55
+ end
56
+
57
+ def base_table_name(model)
58
+ r = get_model_registration(model)
59
+ r[:base_table_name] || TableNames.base_table_name(r[:model])
60
+ end
61
+
62
+ def registered_model_keys
63
+ self.model_registrations.keys
64
+ end
65
+
66
+ def is_registered?(model)
67
+ !!unchecked_get_model_registration(model)
68
+ end
69
+
70
+ def create_context(model_space_key)
71
+ Context.new(self, model_space_key, ModelSpaces.create_persistor)
72
+ end
73
+
74
+ def kill_context(model_space_key)
75
+ Context.drop_tables(self, model_space_key)
76
+ true
77
+ end
78
+
79
+ private
80
+
81
+ def unchecked_get_model_registration(model)
82
+ mc = all_model_superclasses(model).find do |klass|
83
+ model_registrations[name_from_model(klass)]
84
+ end
85
+ self.model_registrations[name_from_model(mc)] if mc
86
+ end
87
+
88
+ def get_model_registration(model)
89
+ r = unchecked_get_model_registration(model)
90
+ raise "model: #{model.to_s} is not registered in ModelSpace: #{name}" if !r
91
+ r
92
+ end
93
+
94
+ def set_model_registration(model, registration)
95
+ self.model_registrations[name_from_model(model)] = registration
96
+ end
97
+
98
+ def delete_model_registration(model)
99
+ self.model_registrations.delete(name_from_model(model))
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ module_function
106
+
107
+ MODEL_REGISTRATION_KEYS = [:history_versions, :base_table_name].to_set
108
+
109
+ def check_model_registration_keys(keys)
110
+ unknown_keys = (keys.map(&:to_sym).to_set - MODEL_REGISTRATION_KEYS).to_a
111
+ raise "unknown keys: #{unknown_keys.inspect}" if !unknown_keys.empty?
112
+ end
113
+
114
+ # get a persistor given a connection... returns an instance of
115
+ # ActiveRecord::ModelSpaces::AdapterNamePersistor
116
+ def create_persistor
117
+ Persistor.new(ModelSpaces.connection || ActiveRecord::Base.connection, ModelSpaces.table_name)
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,72 @@
1
+ require 'active_record/model_spaces/util'
2
+
3
+ module ActiveRecord
4
+ module ModelSpaces
5
+
6
+ # manages ModelSpace persistence...
7
+ class Persistor
8
+ include Util
9
+
10
+ attr_reader :connection
11
+ attr_reader :table_name
12
+
13
+ def initialize(connection, table_name)
14
+ @connection = connection
15
+ @table_name = table_name || "model_spaces_tables"
16
+ create_model_spaces_table(@connection, @table_name)
17
+ end
18
+
19
+ # list all persisted prefixes for a given model space
20
+ def list_keys(model_space_name)
21
+ connection.select_rows("select model_space_key from #{table_name} where model_space_name='#{model_space_name}'").map{|r| r.first}
22
+ end
23
+
24
+ # returns a map of {ModelName => version} entries for a given model-space and model_space_key
25
+ def read_model_space_model_versions(model_space_name, model_space_key)
26
+ connection.select_all("select model_name, version from #{table_name} where model_space_name='#{model_space_name}' and model_space_key='#{model_space_key}'").reduce({}){|h,r| h[r["model_name"]] = r["version"].to_i ; h}
27
+ end
28
+
29
+ # update
30
+ def update_model_space_model_versions(model_space_name, model_space_key, new_model_versions)
31
+ ActiveRecord::Base.transaction do
32
+ old_model_versions = read_model_space_model_versions(model_space_name, model_space_key)
33
+
34
+ new_model_versions.map do |model_or_name, new_version|
35
+ model_name = name_from_model(model_or_name)
36
+ old_version = old_model_versions[model_name]
37
+
38
+ if old_version && new_version && old_version != new_version && new_version != 0
39
+
40
+ connection.execute("update #{table_name} set version=#{new_version} where model_space_name='#{model_space_name}' and model_space_key='#{model_space_key}' and model_name='#{model_name}'")
41
+
42
+ elsif !old_version && new_version && new_version != 0
43
+
44
+ connection.execute("insert into #{table_name} (model_space_name, model_space_key, model_name, version) values ('#{model_space_name}', '#{model_space_key}', '#{model_name}', #{new_version})")
45
+
46
+ elsif old_version && ( !new_version || new_version == 0 )
47
+
48
+ connection.execute("delete from #{table_name} where model_space_name='#{model_space_name}' and model_space_key='#{model_space_key}' and model_name='#{model_name}'")
49
+ end
50
+ end
51
+ true
52
+ end
53
+ end
54
+
55
+ # create the model_spaces table if it doesn't exist
56
+ def create_model_spaces_table(connection, tn)
57
+ if !connection.table_exists?(tn)
58
+ connection.instance_eval do
59
+ create_table(tn) do |t|
60
+ t.string :model_space_name, :null=>false
61
+ t.string :model_space_key, :null=>false
62
+ t.string :model_name, :null=>false
63
+ t.integer :version, :null=>false, :default=>0
64
+ end
65
+ add_index tn, [:model_space_name, :prefix, :model_name], :unique=>true
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,195 @@
1
+ require 'active_record'
2
+ require 'active_record/model_spaces/model_space'
3
+
4
+ module ActiveRecord
5
+ module ModelSpaces
6
+
7
+ class Registry
8
+ include Util
9
+
10
+ attr_reader :model_spaces
11
+ attr_reader :model_spaces_by_models
12
+ attr_reader :enforce_context
13
+
14
+ def initialize
15
+ reset!
16
+ end
17
+
18
+ # drop all model_space and model registrations. will cause any with_model_space_context
19
+ # to most likely bork horribly
20
+ def reset!
21
+ @model_spaces = {}
22
+ @model_spaces_by_models = {}
23
+ end
24
+
25
+ def register_model(model, model_space_name, opts={})
26
+ old_ms = unchecked_get_model_space_for_model(model)
27
+ old_ms.deregister_model(model) if old_ms
28
+
29
+ new_ms = register_model_space(model_space_name).register_model(model, opts)
30
+ register_model_space_for_model(model, new_ms)
31
+ end
32
+
33
+ def set_base_table_name(model, table_name)
34
+ ms = unchecked_get_model_space_for_model(model)
35
+ raise "model #{model} is not (yet) registered to a ModelSpace. do in_model_space before set_table_name or use the :base_table_name option of in_model_space" if !ms
36
+ ms.set_base_table_name(model, table_name)
37
+ end
38
+
39
+ def base_table_name(model)
40
+ get_model_space_for_model(model).base_table_name(model)
41
+ end
42
+
43
+ def set_enforce_context(v)
44
+ @enforce_context = !!v
45
+ end
46
+
47
+ def table_name(model)
48
+ ctx = enforce_context ? get_context_for_model(model) : unchecked_get_context_for_model(model)
49
+ if ctx
50
+ ctx.table_name(model)
51
+ else
52
+ get_model_space_for_model(model).base_table_name(model)
53
+ end
54
+ end
55
+
56
+ def current_table_name(model)
57
+ get_context_for_model(model).current_table_name(model)
58
+ end
59
+
60
+ def working_table_name(model)
61
+ get_context_for_model(model).working_table_name(model)
62
+ end
63
+
64
+ # create a new version of the model
65
+ def new_version(model, &block)
66
+ get_context_for_model(model).new_version(model, &block)
67
+ end
68
+
69
+ # create an updated version of the model
70
+ def updated_version(model, &block)
71
+ get_context_for_model(model).updated_version(model, &block)
72
+ end
73
+
74
+ def hoover(model)
75
+ get_context_for_model(model).hoover
76
+ end
77
+
78
+ # execute a block with a ModelSpace context.
79
+ # only a single context can be active for a given ModelSpace on any Thread at
80
+ # any time, though different ModelSpaces may have active contexts concurrently
81
+ def with_context(model_space_name, model_space_key, &block)
82
+
83
+ ms = get_model_space(model_space_name)
84
+ raise "no such model space: #{model_space_name}" if !ms
85
+
86
+ current_ctx = merged_context[ms.name]
87
+
88
+ if current_ctx && current_ctx.model_space_key==model_space_key.to_sym
89
+
90
+ block.call # same context is already active
91
+
92
+ elsif current_ctx
93
+
94
+ raise "ModelSpace: #{model_space_name}: context with key #{current_ctx.model_space_key} already active"
95
+
96
+ else
97
+
98
+ old_merged_context = self.send(:merged_context)
99
+ ctx = ms.create_context(model_space_key)
100
+ context_stack << ctx
101
+ begin
102
+ self.merged_context = merge_context_stack
103
+
104
+ r = block.call
105
+ ctx.commit
106
+ r
107
+ ensure
108
+ context_stack.pop
109
+ self.merged_context = old_merged_context
110
+ end
111
+
112
+ end
113
+ end
114
+
115
+ def kill_context(model_space_name, model_space_key)
116
+ get_model_space(model_space_name).kill_context(model_space_key)
117
+ end
118
+
119
+ # return the key of the active context for the given model_space, or nil
120
+ # if there is no active context
121
+ def active_key(model_space_name)
122
+ ms = get_model_space(model_space_name)
123
+ raise "no such model space: #{model_space_name}" if !ms
124
+
125
+ ctx = merged_context[ms.name]
126
+
127
+ ctx.model_space_key if ctx
128
+ end
129
+
130
+ private
131
+
132
+ def register_model_space(model_space_name)
133
+ model_spaces[model_space_name.to_sym] ||= ModelSpace.new(model_space_name.to_sym)
134
+ end
135
+
136
+ def get_model_space(model_space_name)
137
+ model_spaces[model_space_name.to_sym]
138
+ end
139
+
140
+ def register_model_space_for_model(model, model_space)
141
+ model_spaces_by_models[name_from_model(model)] = model_space
142
+ end
143
+
144
+ def unchecked_get_model_space_for_model(model)
145
+ mc = all_model_superclasses(model).find do |klass|
146
+ model_spaces_by_models[name_from_model(klass)]
147
+ end
148
+ model_spaces_by_models[name_from_model(mc)] if mc
149
+ end
150
+
151
+ def get_model_space_for_model(model)
152
+ ms = unchecked_get_model_space_for_model(model)
153
+ raise "model: #{model} is not registered to any ModelSpace" if !ms
154
+ ms
155
+ end
156
+
157
+ CONTEXT_STACK_KEY = "ActiveRecord::ModelSpaces.context_stack"
158
+
159
+ def context_stack
160
+ Thread.current[CONTEXT_STACK_KEY] ||= []
161
+ end
162
+
163
+ MERGED_CONTEXT_KEY = "ActiveRecord::ModelSpaces.merged_context"
164
+
165
+ def merged_context
166
+ Thread.current[MERGED_CONTEXT_KEY] || {}
167
+ end
168
+
169
+ def merged_context=(mc)
170
+ Thread.current[MERGED_CONTEXT_KEY] = mc
171
+ end
172
+
173
+ # merge all entries in the context stack into a map
174
+ def merge_context_stack
175
+ context_stack.reduce({}) do |m, ctx|
176
+ raise "ModelSpace: #{ctx.model_space.name}: already has an active context" if m[ctx.model_space.name]
177
+ m[ctx.model_space.name] = ctx
178
+ m
179
+ end
180
+ end
181
+
182
+ def unchecked_get_context_for_model(model)
183
+ ms = unchecked_get_model_space_for_model(model)
184
+ merged_context[ms.name] if ms
185
+ end
186
+
187
+ def get_context_for_model(model)
188
+ ms = get_model_space_for_model(model)
189
+ ctx = merged_context[ms.name]
190
+ raise "ModelSpace: '#{ms.name}' has no current context" if !ctx
191
+ ctx
192
+ end
193
+ end
194
+ end
195
+ end