cell 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +1 -1
- data/cell.gemspec +5 -5
- data/lib/cell/clone_schema.rb +195 -192
- data/lib/cell/console.rb +11 -4
- data/lib/cell/context.rb +7 -7
- data/lib/cell/ext/active_job.rb +33 -0
- data/lib/cell/ext/active_record.rb +81 -0
- data/lib/cell/ext/migration.rb +198 -0
- data/lib/cell/meta.rb +3 -2
- data/lib/cell/railtie.rb +6 -7
- data/lib/cell/sanity_check.rb +9 -10
- data/lib/cell/schema.rb +8 -13
- data/lib/cell/tenant.rb +13 -12
- data/lib/cell/version.rb +1 -1
- metadata +16 -22
- data/lib/cell/active_job.rb +0 -32
- data/lib/cell/migration.rb +0 -201
- data/lib/cell/model_extensions.rb +0 -85
data/lib/cell/migration.rb
DELETED
@@ -1,201 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# This is the messiest part of Cell, I think by necessity.
|
3
|
-
#
|
4
|
-
# A migration has two execution modes, global and targeted, with correspond to "db:migrate" and
|
5
|
-
# "cell:db:migrate"
|
6
|
-
#
|
7
|
-
# "global" is ran with a "db:migrate", which means no Tenant is activated. DDL commands here are
|
8
|
-
# directed to either 'public' or 'cell_prototype'
|
9
|
-
#
|
10
|
-
# "targeted" is ran with a tenant activated (cell:db:migrate), and all DDL commands are directed to
|
11
|
-
# the current tenant's schema.
|
12
|
-
#
|
13
|
-
# In 'rails db:migrate' (global mode), we actually execute each migration twice: once with a
|
14
|
-
# pass_context set to :global, where ONLY commands in a 'global {}' block are executed, and then
|
15
|
-
# with pass_context set to :prototype, where ONLY commands outside of a 'global {}' block are
|
16
|
-
# executed.
|
17
|
-
#
|
18
|
-
# ContextTracker keeps up with this.
|
19
|
-
|
20
|
-
require 'cell/meta'
|
21
|
-
require 'cell/schema'
|
22
|
-
require 'cell/clone_schema'
|
23
|
-
|
24
|
-
|
25
|
-
module Cell
|
26
|
-
module Migration
|
27
|
-
|
28
|
-
# OK, ContextTracker keeps up with:
|
29
|
-
# * The context in which this migration is being ran, e.g. the :global first pass, the
|
30
|
-
# :prototype second pass, OR the :targeted per tenant pass.
|
31
|
-
# * If it's in a global block or not, which determines if actions are actually executed
|
32
|
-
# * If force_execution is set, which is for the benefit of playing back CommandRecorder
|
33
|
-
# commands.
|
34
|
-
module ContextTracker
|
35
|
-
mattr_accessor :pass_context
|
36
|
-
mattr_accessor :global_block
|
37
|
-
mattr_accessor :force_execution
|
38
|
-
|
39
|
-
def execute_ddl?
|
40
|
-
force_execution ||
|
41
|
-
(pass_context == :global && global_block) ||
|
42
|
-
(pass_context == :prototype && !global_block) ||
|
43
|
-
(pass_context == :target && !global_block)
|
44
|
-
end
|
45
|
-
|
46
|
-
# When the CommandRecorder's commands are executed, they're rewritten to go through
|
47
|
-
# force_call, because the "effective set of commands" have already been determined by the
|
48
|
-
# CommandRecorder run.
|
49
|
-
def force_call(*args, &block)
|
50
|
-
saved, self.force_execution = self.force_execution, true
|
51
|
-
send(*args, &block)
|
52
|
-
ensure
|
53
|
-
self.force_execution = saved
|
54
|
-
end
|
55
|
-
|
56
|
-
def with_context(context, search_path, exclusive: false)
|
57
|
-
Cell::Meta::with_schema(search_path, exclusive: exclusive) do
|
58
|
-
begin
|
59
|
-
saved, self.pass_context = self.pass_context, context
|
60
|
-
yield
|
61
|
-
ensure
|
62
|
-
self.pass_context = saved
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def global
|
68
|
-
saved, self.global_block = self.global_block, true
|
69
|
-
yield
|
70
|
-
ensure
|
71
|
-
self.global_block = saved
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
|
76
|
-
# This module intercepts create_table and drop_table, and updates the list of global tables
|
77
|
-
# in ::ActiveRecord::InternalMetadata['cell.global'].
|
78
|
-
module MetadataIntercept
|
79
|
-
def create_table(name, *args, &block)
|
80
|
-
super.tap do
|
81
|
-
::Cell::Meta.add_global_table(name) if pass_context == :global
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def drop_table(name, *args, &block)
|
86
|
-
super.tap do
|
87
|
-
::Cell::Meta.remove_global_table(name) if pass_context == :global
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
# We intercept every method given, and only execute it if appropriate according to execute_ddl?
|
93
|
-
def self.intercept(methods)
|
94
|
-
methods.each do |method|
|
95
|
-
define_method(method) do |*args, &block|
|
96
|
-
super(*args, &block) if execute_ddl?
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
# This sucks. In the future, we may want to pull these from
|
102
|
-
# SchemaStatements
|
103
|
-
intercept %i(add_belongs_to add_column add_foreign_key add_index
|
104
|
-
add_index_sort_order add_reference add_timestamps
|
105
|
-
change_column change_column_default change_column_null
|
106
|
-
change_table change_table_comment create_join_table
|
107
|
-
create_table drop_join_table drop_table
|
108
|
-
initialize_schema_migrations_table remove_belongs_to
|
109
|
-
remove_column remove_columns remove_foreign_key remove_index
|
110
|
-
remove_reference remove_timestamps rename_column
|
111
|
-
rename_column_indexes rename_index rename_table
|
112
|
-
rename_table_indexes table_alias_for table_comment) +
|
113
|
-
%i(enable_extension disable_extension truncate) +
|
114
|
-
%i(execute)
|
115
|
-
|
116
|
-
def global_schema
|
117
|
-
Cell::Meta.global_schema
|
118
|
-
end
|
119
|
-
|
120
|
-
def prototype_schema
|
121
|
-
Cell::Meta.prototype_schema
|
122
|
-
end
|
123
|
-
|
124
|
-
def tenant_schema
|
125
|
-
target.schema_name
|
126
|
-
end
|
127
|
-
|
128
|
-
def target
|
129
|
-
Cell::Model.current
|
130
|
-
end
|
131
|
-
|
132
|
-
def targeted?
|
133
|
-
!! target
|
134
|
-
end
|
135
|
-
|
136
|
-
# This is our super-special initialization function.
|
137
|
-
def initialize_cell!
|
138
|
-
CloneSchema.install_function!
|
139
|
-
execute "CREATE SCHEMA #{connection.quote_schema_name(prototype_schema)}"
|
140
|
-
end
|
141
|
-
|
142
|
-
def exec_migration(con, direction)
|
143
|
-
if ! targeted?
|
144
|
-
with_context(:global, global_schema) do
|
145
|
-
super
|
146
|
-
end
|
147
|
-
|
148
|
-
with_context(:prototype, prototype_schema) do
|
149
|
-
super
|
150
|
-
end
|
151
|
-
else
|
152
|
-
with_context(:target, tenant_schema, exclusive: true) do
|
153
|
-
super
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
ActiveSupport.on_load(:active_record) do
|
159
|
-
ActiveRecord::Migration.prepend(::Cell::Migration::MetadataIntercept)
|
160
|
-
ActiveRecord::Migration.prepend(::Cell::Migration::ContextTracker)
|
161
|
-
ActiveRecord::Migration.prepend(::Cell::Migration)
|
162
|
-
end
|
163
|
-
|
164
|
-
|
165
|
-
# Patches to CommandRecorder, which let us roll back.
|
166
|
-
module CommandRecorderFilter
|
167
|
-
# This maybe should've been attr_reader in CommandRecorder
|
168
|
-
def commands=(*)
|
169
|
-
# If this is actually used, we're fucked.
|
170
|
-
fail "The problem with monkey patching is..."
|
171
|
-
end
|
172
|
-
|
173
|
-
def commands
|
174
|
-
@commands.select do |command|
|
175
|
-
command[0]
|
176
|
-
end.map do |command|
|
177
|
-
[:force_call, [command[1], *command[2]], command[3]]
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
# saves the state of a recorded command with the context it was in.
|
182
|
-
def add_command(command)
|
183
|
-
@commands << [execute_ddl?, *command]
|
184
|
-
end
|
185
|
-
|
186
|
-
# We override #record to proxy through add_command
|
187
|
-
def record(*command, &block)
|
188
|
-
if @reverting
|
189
|
-
add_command inverse_of(*command, &block)
|
190
|
-
else
|
191
|
-
add_command (command << block)
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
ActiveSupport.on_load(:active_record) do
|
196
|
-
::ActiveRecord::Migration::CommandRecorder.prepend(::Cell::Migration::ContextTracker)
|
197
|
-
::ActiveRecord::Migration::CommandRecorder.prepend(::Cell::Migration::CommandRecorderFilter)
|
198
|
-
end
|
199
|
-
end
|
200
|
-
end
|
201
|
-
end
|
@@ -1,85 +0,0 @@
|
|
1
|
-
# These are added to ActiveRecord::Base, regardless if they're the tenant model or not.
|
2
|
-
|
3
|
-
# There are two big tricks here:
|
4
|
-
# 1. Assigning a "tenant" attribute to every model that is loaded from the db, or created while a
|
5
|
-
# tenant is active.
|
6
|
-
# 2. Making sure the object is validated and saved with the proper tenant activated, even if the
|
7
|
-
# tenant has changed since the object was created
|
8
|
-
#
|
9
|
-
# So you can load an object from "tenant_a", switch to "tenant_b", and if you save or validate the
|
10
|
-
# object, it'll do so while switched to "tenant_a", and then restore.
|
11
|
-
|
12
|
-
require 'cell/meta'
|
13
|
-
|
14
|
-
module Cell
|
15
|
-
module ModelExtensions
|
16
|
-
|
17
|
-
# Say we have a model 'u', that is from an old Tenant, loading an association like 'u.books'
|
18
|
-
# should swap into u.tenant while fetching.
|
19
|
-
module AssociationExtensions
|
20
|
-
def load_target
|
21
|
-
owner._activate_tenant do
|
22
|
-
super
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
|
28
|
-
module ClassMethods
|
29
|
-
def global_model?
|
30
|
-
::Cell::Meta.global_model?(self)
|
31
|
-
end
|
32
|
-
|
33
|
-
# When a model's schema is lazy-loaded, we want to make sure our view of the DB allows us to
|
34
|
-
# get introspection data.
|
35
|
-
def load_schema!
|
36
|
-
::Cell::Meta.with_structural_schema do
|
37
|
-
return super
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
attr_reader :tenant
|
43
|
-
|
44
|
-
def _assign_tenant
|
45
|
-
unless self.class.global_model?
|
46
|
-
@tenant = Cell::Model.current
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def _activate_tenant(&block)
|
51
|
-
if tenant && tenant != ::Cell::Model.current
|
52
|
-
tenant.use(&block)
|
53
|
-
else
|
54
|
-
yield
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
# undocumented, but makes sure validations are ran in the context of the object's tenant, e.g.,
|
59
|
-
# uniqueness.
|
60
|
-
def run_validations!
|
61
|
-
_activate_tenant do
|
62
|
-
super
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def self.prepended(cls)
|
67
|
-
class << cls
|
68
|
-
prepend ClassMethods
|
69
|
-
end
|
70
|
-
|
71
|
-
cls.after_initialize :_assign_tenant
|
72
|
-
cls.around_save :_activate_tenant
|
73
|
-
end
|
74
|
-
|
75
|
-
def self.install!
|
76
|
-
ActiveSupport.on_load(:active_record) do
|
77
|
-
::ActiveRecord::Base.prepend(::Cell::ModelExtensions)
|
78
|
-
::ActiveRecord::Associations::Association.prepend(::Cell::ModelExtensions::AssociationExtensions)
|
79
|
-
::ActiveRecord::Associations::CollectionAssociation.prepend(::Cell::ModelExtensions::AssociationExtensions)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
Cell::ModelExtensions.install!
|