cell 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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!