activerecord-model-spaces 0.2.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.
@@ -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