cell 0.1.2 → 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.
- 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!
|