combinaut_stagehand 1.2.1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +20 -0
- data/lib/combinaut_stagehand.rb +2 -0
- data/lib/stagehand/active_record_extensions.rb +110 -0
- data/lib/stagehand/auditor/checklist_visualizer.rb +90 -0
- data/lib/stagehand/auditor.rb +90 -0
- data/lib/stagehand/cache.rb +12 -0
- data/lib/stagehand/configuration.rb +45 -0
- data/lib/stagehand/connection_adapter_extensions.rb +75 -0
- data/lib/stagehand/controller_extensions.rb +35 -0
- data/lib/stagehand/database.rb +197 -0
- data/lib/stagehand/engine.rb +26 -0
- data/lib/stagehand/key.rb +23 -0
- data/lib/stagehand/production/controller.rb +12 -0
- data/lib/stagehand/production.rb +132 -0
- data/lib/stagehand/schema/statements.rb +53 -0
- data/lib/stagehand/schema.rb +145 -0
- data/lib/stagehand/schema_extensions.rb +10 -0
- data/lib/stagehand/staging/checklist.rb +202 -0
- data/lib/stagehand/staging/commit.rb +168 -0
- data/lib/stagehand/staging/commit_entry.rb +180 -0
- data/lib/stagehand/staging/controller.rb +27 -0
- data/lib/stagehand/staging/model.rb +29 -0
- data/lib/stagehand/staging/synchronizer.rb +198 -0
- data/lib/stagehand/staging.rb +11 -0
- data/lib/stagehand/version.rb +3 -0
- data/lib/stagehand.rb +5 -0
- data/lib/tasks/stagehand_tasks.rake +43 -0
- metadata +166 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
require 'stagehand/active_record_extensions'
|
|
3
|
+
|
|
4
|
+
module Stagehand
|
|
5
|
+
module Database
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
def transaction
|
|
9
|
+
raise InvalidConnectionError, "Calling Stagehand::Database.transaction is not valid unless connected to staging" unless connected_to_staging?
|
|
10
|
+
|
|
11
|
+
success = false
|
|
12
|
+
attempts = 0
|
|
13
|
+
output = nil
|
|
14
|
+
ActiveRecord::Base.transaction do
|
|
15
|
+
Production::Record.transaction do
|
|
16
|
+
attempts += 1
|
|
17
|
+
|
|
18
|
+
raise NoRetryError, "Retrying is not allowed in Stagehand::Database.transaction" if attempts > 1
|
|
19
|
+
|
|
20
|
+
output = yield
|
|
21
|
+
|
|
22
|
+
success = true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
raise ActiveRecord::Rollback unless success
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return output
|
|
29
|
+
ensure
|
|
30
|
+
Rails.logger.warn "Stagehand::Database transaction was rolled back" unless success
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def each(&block)
|
|
34
|
+
with_production_connection(&block) unless Configuration.single_connection?
|
|
35
|
+
with_staging_connection(&block)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def connected_to_production?
|
|
39
|
+
current_connection_name == Configuration.production_connection_name
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def connected_to_staging?
|
|
43
|
+
current_connection_name == Configuration.staging_connection_name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def production_connection
|
|
47
|
+
ProductionProbe.connection
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def staging_connection
|
|
51
|
+
StagingProbe.connection
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def production_database_name
|
|
55
|
+
database_name(Configuration.production_connection_name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def staging_database_name
|
|
59
|
+
database_name(Configuration.staging_connection_name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def staging_database_versions
|
|
63
|
+
Stagehand::Database.staging_connection.select_values(versions_scope)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def production_database_versions
|
|
67
|
+
Stagehand::Database.production_connection.select_values(versions_scope)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def with_staging_connection(&block)
|
|
71
|
+
with_connection(Configuration.staging_connection_name, &block)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def with_production_connection(&block)
|
|
75
|
+
with_connection(Configuration.production_connection_name, &block)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def with_connection(connection_name, &block)
|
|
79
|
+
if current_connection_name != connection_name.to_sym
|
|
80
|
+
Rails.logger.debug "Connecting to #{connection_name}"
|
|
81
|
+
output = swap_connection(connection_name, &block)
|
|
82
|
+
Rails.logger.debug "Restoring connection to #{current_connection_name}"
|
|
83
|
+
else
|
|
84
|
+
Rails.logger.debug "Already connected to #{connection_name}"
|
|
85
|
+
output = yield connection_name
|
|
86
|
+
end
|
|
87
|
+
return output
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def swap_connection(connection_name)
|
|
93
|
+
pushed = ConnectionStack.push(connection_name.to_sym)
|
|
94
|
+
cache = ActiveRecord::Base.connection_pool.query_cache_enabled
|
|
95
|
+
ActiveRecord::Base.connection_specification_name = current_connection_name
|
|
96
|
+
ActiveRecord::Base.connection_pool.enable_query_cache! if cache
|
|
97
|
+
|
|
98
|
+
yield connection_name
|
|
99
|
+
ensure
|
|
100
|
+
ConnectionStack.pop if pushed
|
|
101
|
+
ActiveRecord::Base.connection_specification_name = current_connection_name
|
|
102
|
+
ActiveRecord::Base.connection_pool.enable_query_cache! if cache
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def current_connection_name
|
|
106
|
+
ConnectionStack.last
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def database_name(connection_name)
|
|
110
|
+
database_configuration.dig(connection_name.to_s, 'database')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def database_configuration
|
|
114
|
+
@database_configuration ||= Rails.configuration.database_configuration
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def versions_scope
|
|
118
|
+
ActiveRecord::SchemaMigration.order(:version)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# CLASSES
|
|
122
|
+
|
|
123
|
+
class Probe < ActiveRecord::Base
|
|
124
|
+
self.abstract_class = true
|
|
125
|
+
self.stagehand_threadsafe_connections = false # We don't want to track connection per-thread for Probes
|
|
126
|
+
|
|
127
|
+
# We fake the class name so we can create a connection pool with the desired connection name instead of the name of the class
|
|
128
|
+
def self.init_connection(connection_name)
|
|
129
|
+
@probe_name = connection_name
|
|
130
|
+
establish_connection(connection_name)
|
|
131
|
+
ensure
|
|
132
|
+
@probe_name = nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.name
|
|
136
|
+
@probe_name || super
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class StagingProbe < Probe
|
|
141
|
+
self.abstract_class = true
|
|
142
|
+
|
|
143
|
+
def self.init_connection
|
|
144
|
+
super(Configuration.staging_connection_name)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def self.connection
|
|
148
|
+
if Stagehand::Database.connected_to_staging?
|
|
149
|
+
ActiveRecord::Base.connection # Reuse existing connection so we stay within the current transaction
|
|
150
|
+
else
|
|
151
|
+
super
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
init_connection
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
class ProductionProbe < Probe
|
|
159
|
+
self.abstract_class = true
|
|
160
|
+
|
|
161
|
+
def self.init_connection
|
|
162
|
+
super(Configuration.production_connection_name)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
init_connection unless Configuration.single_connection?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Threadsafe tracking of the connection stack
|
|
169
|
+
module ConnectionStack
|
|
170
|
+
def self.push(connection_name)
|
|
171
|
+
current_stack.push connection_name
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def self.pop
|
|
175
|
+
current_stack.pop
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def self.last
|
|
179
|
+
current_stack.last
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.current_stack
|
|
183
|
+
if stack = Thread.current.thread_variable_get('sparkle_connection_name_stack')
|
|
184
|
+
stack
|
|
185
|
+
else
|
|
186
|
+
stack = Concurrent::Array.new
|
|
187
|
+
stack << Rails.env.to_sym
|
|
188
|
+
Thread.current.thread_variable_set('sparkle_connection_name_stack', stack)
|
|
189
|
+
stack
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
class InvalidConnectionError < StandardError; end
|
|
195
|
+
class NoRetryError < StandardError; end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'stagehand/configuration'
|
|
2
|
+
|
|
3
|
+
module Stagehand
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace Stagehand
|
|
6
|
+
|
|
7
|
+
config.generators do |g|
|
|
8
|
+
g.test_framework :rspec
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# These require the rails application to be initialized because configuration variables are used
|
|
12
|
+
initializer 'stagehand.load_modules' do
|
|
13
|
+
require 'stagehand/cache'
|
|
14
|
+
require 'stagehand/key'
|
|
15
|
+
require 'stagehand/database'
|
|
16
|
+
require 'stagehand/connection_adapter_extensions'
|
|
17
|
+
require 'stagehand/controller_extensions'
|
|
18
|
+
require 'stagehand/active_record_extensions'
|
|
19
|
+
require 'stagehand/schema_extensions'
|
|
20
|
+
require 'stagehand/staging'
|
|
21
|
+
require 'stagehand/production'
|
|
22
|
+
require 'stagehand/schema'
|
|
23
|
+
require 'stagehand/auditor'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Stagehand
|
|
2
|
+
module Key
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
def generate(staging_record, options = {})
|
|
6
|
+
case staging_record
|
|
7
|
+
when Staging::CommitEntry
|
|
8
|
+
id = staging_record.record_id || staging_record.id
|
|
9
|
+
table_name = staging_record.table_name || staging_record.class.table_name
|
|
10
|
+
when ActiveRecord::Base
|
|
11
|
+
id = staging_record.id
|
|
12
|
+
table_name = staging_record.class.table_name
|
|
13
|
+
else
|
|
14
|
+
id = staging_record
|
|
15
|
+
table_name = options[:table_name]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
raise 'Invalid input' unless table_name && id
|
|
19
|
+
|
|
20
|
+
return [table_name, id]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require 'stagehand/production/controller'
|
|
2
|
+
|
|
3
|
+
module Stagehand
|
|
4
|
+
module Production
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
# Outputs a symbol representing the status of the staging record as it exists in the production database
|
|
8
|
+
def status(staging_record, table_name = nil)
|
|
9
|
+
if !exists?(staging_record, table_name)
|
|
10
|
+
:new
|
|
11
|
+
elsif modified?(staging_record, table_name)
|
|
12
|
+
:modified
|
|
13
|
+
else
|
|
14
|
+
:not_modified
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def save(staging_record, table_name = nil)
|
|
19
|
+
attributes = staging_record_attributes(staging_record, table_name)
|
|
20
|
+
|
|
21
|
+
return unless attributes.present?
|
|
22
|
+
|
|
23
|
+
write(staging_record, attributes, table_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def write(staging_record, attributes, table_name = nil)
|
|
27
|
+
table_name, id = Stagehand::Key.generate(staging_record, :table_name => table_name)
|
|
28
|
+
|
|
29
|
+
production_record = Connection.with_production_writes do
|
|
30
|
+
prepare_to_modify(table_name)
|
|
31
|
+
|
|
32
|
+
if update(table_name, id, attributes).nonzero?
|
|
33
|
+
Record.find(id)
|
|
34
|
+
else
|
|
35
|
+
Record.find(insert(table_name, attributes))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
return production_record
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def delete(staging_record, table_name = nil)
|
|
43
|
+
Connection.with_production_writes do
|
|
44
|
+
matching(staging_record, table_name).delete_all
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def exists?(staging_record, table_name = nil)
|
|
49
|
+
matching(staging_record, table_name).exists?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns true if the staging record's attributes are different from the production record's attributes
|
|
53
|
+
# Returns true if the staging_record does not exist on production
|
|
54
|
+
# Returns false if the staging record is identical to the production record
|
|
55
|
+
def modified?(staging_record, table_name = nil)
|
|
56
|
+
production_record_attributes(staging_record, table_name) != staging_record_attributes(staging_record, table_name)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def find(*args)
|
|
60
|
+
matching(*args).first
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns a scope that limits results any occurrences of the specified record.
|
|
64
|
+
# Record can be specified by passing a staging record, or an id and table_name.
|
|
65
|
+
def matching(staging_record, table_name = nil)
|
|
66
|
+
table_name, id = Stagehand::Key.generate(staging_record, :table_name => table_name)
|
|
67
|
+
prepare_to_modify(table_name)
|
|
68
|
+
return Record.where(:id => id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def production_record_attributes(staging_record, table_name = nil)
|
|
74
|
+
Record.connection.select_one(matching(staging_record, table_name))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def staging_record_attributes(staging_record, table_name = nil)
|
|
78
|
+
table_name, id = Stagehand::Key.generate(staging_record, :table_name => table_name)
|
|
79
|
+
hash = select(table_name, id)
|
|
80
|
+
hash.except(*ignored_columns(table_name)) if hash
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def ignored_columns(table_name)
|
|
84
|
+
Array.wrap(Configuration.ignored_columns[table_name]).map(&:to_s)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def select(table_name, id)
|
|
88
|
+
table = Arel::Table.new(table_name)
|
|
89
|
+
statement = Arel::SelectManager.new
|
|
90
|
+
statement.from table
|
|
91
|
+
statement.project Arel.star
|
|
92
|
+
statement.where table[:id].eq(id)
|
|
93
|
+
|
|
94
|
+
Stagehand::Database::StagingProbe.connection.select_one(statement)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def update(table_name, id, attributes)
|
|
98
|
+
table = Arel::Table.new(table_name)
|
|
99
|
+
statement = Arel::UpdateManager.new
|
|
100
|
+
statement.table table
|
|
101
|
+
statement.set attributes.map {|attribute, value| [table[attribute], value] }
|
|
102
|
+
statement.where table[:id].eq(id)
|
|
103
|
+
|
|
104
|
+
Record.connection.update(statement)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def insert(table_name, attributes)
|
|
108
|
+
table = Arel::Table.new(table_name)
|
|
109
|
+
statement = Arel::InsertManager.new
|
|
110
|
+
statement.into table
|
|
111
|
+
statement.insert attributes.map {|attribute, value| [table[attribute], value] }
|
|
112
|
+
|
|
113
|
+
Record.connection.insert(statement)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def prepare_to_modify(table_name)
|
|
117
|
+
raise "Can't prepare to modify production records without knowning the table_name" unless table_name.present?
|
|
118
|
+
|
|
119
|
+
return if Record.table_name == table_name
|
|
120
|
+
|
|
121
|
+
Record.table_name = table_name
|
|
122
|
+
Record.reset_column_information
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# CLASSES
|
|
126
|
+
|
|
127
|
+
class Record < Stagehand::Database::ProductionProbe
|
|
128
|
+
self.record_timestamps = false
|
|
129
|
+
self.inheritance_column = nil
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Stagehand
|
|
2
|
+
module Schema
|
|
3
|
+
module Statements
|
|
4
|
+
# Ensure that developers are aware they need to make a determination of whether stagehand should track this table or not
|
|
5
|
+
def create_table(table_name, options = {})
|
|
6
|
+
super
|
|
7
|
+
options = options.symbolize_keys
|
|
8
|
+
|
|
9
|
+
return if Database.connected_to_production? && !Stagehand::Configuration.single_connection?
|
|
10
|
+
return if options[:stagehand] == false
|
|
11
|
+
return if UNTRACKED_TABLES.include?(table_name)
|
|
12
|
+
|
|
13
|
+
Schema.add_stagehand! :only => table_name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def rename_table(old_table_name, new_table_name, *)
|
|
17
|
+
return super unless Schema.has_stagehand?(old_table_name)
|
|
18
|
+
|
|
19
|
+
Schema.remove_stagehand!(:only => old_table_name, :remove_entries => false)
|
|
20
|
+
super
|
|
21
|
+
Schema.add_stagehand!(:only => new_table_name)
|
|
22
|
+
Staging::CommitEntry.where(:table_name => old_table_name).update_all(:table_name => new_table_name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def drop_table(table_name, *)
|
|
26
|
+
return super unless Schema.has_stagehand?(table_name) && table_exists?(Staging::CommitEntry.table_name)
|
|
27
|
+
|
|
28
|
+
Schema.remove_stagehand!(:only => table_name, :remove_entries => true)
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Allow dumping of stagehand create_table directive
|
|
34
|
+
# e.g. create_table "comments", stagehand: true do |t|
|
|
35
|
+
module DumperExtensions
|
|
36
|
+
def table(table_name, stream)
|
|
37
|
+
stagehand = Stagehand::Schema.has_stagehand?(table_name)
|
|
38
|
+
stagehand = ':commit_entries' if table_name == Staging::CommitEntry.table_name
|
|
39
|
+
|
|
40
|
+
table_stream = StringIO.new
|
|
41
|
+
super(table_name, table_stream)
|
|
42
|
+
table_stream.rewind
|
|
43
|
+
table_schema = table_stream.read.gsub(/create_table (.+) do/, 'create_table \1' + ", stagehand: #{stagehand} do")
|
|
44
|
+
stream.puts table_schema
|
|
45
|
+
|
|
46
|
+
return stream
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
ActiveRecord::Base.connection.class.include Stagehand::Schema::Statements
|
|
53
|
+
ActiveRecord::SchemaDumper.prepend Stagehand::Schema::DumperExtensions
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
require "stagehand/schema/statements"
|
|
2
|
+
|
|
3
|
+
module Stagehand
|
|
4
|
+
module Schema
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
UNTRACKED_TABLES = ['ar_internal_metadata', 'schema_migrations', 'data_migrations', Stagehand::Staging::CommitEntry.table_name]
|
|
8
|
+
|
|
9
|
+
def init_stagehand!(**table_options)
|
|
10
|
+
ActiveRecord::Schema.define do
|
|
11
|
+
create_table :stagehand_commit_entries do |t|
|
|
12
|
+
t.integer :record_id
|
|
13
|
+
t.string :table_name
|
|
14
|
+
t.string :operation, :null => false
|
|
15
|
+
t.integer :commit_id
|
|
16
|
+
t.boolean :committed, :null => false, :default => false
|
|
17
|
+
t.datetime :created_at
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :stagehand_commit_entries, :commit_id # Used for looking up all entries within a commit
|
|
21
|
+
add_index :stagehand_commit_entries, [:record_id, :table_name, :committed], :name => 'index_stagehand_commit_entries_for_matching' # Used for 'matching' scope
|
|
22
|
+
add_index :stagehand_commit_entries, [:operation, :committed, :commit_id], :name => 'index_stagehand_commit_entries_for_loading' # Used for looking up start entries
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Stagehand::Staging::CommitEntry.reset_column_information
|
|
26
|
+
|
|
27
|
+
add_stagehand!(table_options)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def add_stagehand!(force: false, **table_options)
|
|
32
|
+
return if Database.connected_to_production? && !Stagehand::Configuration.single_connection?
|
|
33
|
+
|
|
34
|
+
ActiveRecord::Schema.define do
|
|
35
|
+
Stagehand::Schema.send :each_table, table_options do |table_name|
|
|
36
|
+
Stagehand::Schema.send :create_operation_trigger, table_name, 'insert', 'NEW', force: force
|
|
37
|
+
Stagehand::Schema.send :create_operation_trigger, table_name, 'update', 'NEW', force: force
|
|
38
|
+
Stagehand::Schema.send :create_operation_trigger, table_name, 'delete', 'OLD', force: force
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def remove_stagehand!(remove_entries: true, **table_options)
|
|
44
|
+
ActiveRecord::Schema.define do
|
|
45
|
+
Stagehand::Schema.send :each_table, table_options do |table_name|
|
|
46
|
+
next unless Stagehand::Schema.send :has_stagehand_triggers?, table_name
|
|
47
|
+
Stagehand::Schema.send :drop_trigger, table_name, 'insert'
|
|
48
|
+
Stagehand::Schema.send :drop_trigger, table_name, 'update'
|
|
49
|
+
Stagehand::Schema.send :drop_trigger, table_name, 'delete'
|
|
50
|
+
Stagehand::Schema.send :expunge, table_name if remove_entries
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
drop_table :stagehand_commit_entries unless table_options[:only].present?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def has_stagehand?(table_name = nil)
|
|
58
|
+
if UNTRACKED_TABLES.include?(table_name.to_s)
|
|
59
|
+
return false
|
|
60
|
+
elsif table_name
|
|
61
|
+
has_stagehand_triggers?(table_name)
|
|
62
|
+
else
|
|
63
|
+
ActiveRecord::Base.connection.table_exists?(Stagehand::Staging::CommitEntry.table_name)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def each_table(only: nil, except: nil)
|
|
70
|
+
table_names = ActiveRecord::Base.connection.tables
|
|
71
|
+
table_names -= UNTRACKED_TABLES
|
|
72
|
+
table_names -= Array(except).collect(&:to_s)
|
|
73
|
+
table_names &= Array(only).collect(&:to_s) if only.present?
|
|
74
|
+
|
|
75
|
+
table_names.each do |table_name|
|
|
76
|
+
yield table_name
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create_operation_trigger(table_name, trigger_event, record, force: false)
|
|
81
|
+
if force
|
|
82
|
+
drop_trigger(table_name, trigger_event)
|
|
83
|
+
elsif trigger_exists?(table_name, trigger_event)
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
create_trigger(table_name, trigger_event, :after, <<-SQL)
|
|
88
|
+
BEGIN
|
|
89
|
+
INSERT INTO stagehand_commit_entries (record_id, table_name, operation, commit_id, created_at)
|
|
90
|
+
VALUES (#{record}.id, '#{table_name}', '#{trigger_event}', @stagehand_commit_id, CURRENT_TIMESTAMP());
|
|
91
|
+
END;
|
|
92
|
+
SQL
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def create_trigger(table_name, trigger_event, trigger_time, row_action)
|
|
96
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
|
97
|
+
CREATE TRIGGER #{trigger_name(table_name, trigger_event)} #{trigger_time} #{trigger_event}
|
|
98
|
+
ON #{table_name} FOR EACH ROW #{row_action}
|
|
99
|
+
SQL
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def drop_trigger(table_name, trigger_event)
|
|
103
|
+
ActiveRecord::Base.connection.execute("DROP TRIGGER IF EXISTS #{trigger_name(table_name, trigger_event)};")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def trigger_exists?(table_name, trigger_event)
|
|
107
|
+
ActiveRecord::Base.connection.select_one("SHOW TRIGGERS where `trigger` = '#{trigger_name(table_name, trigger_event)}'").present?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def has_stagehand_triggers?(table_name)
|
|
111
|
+
get_triggers(table_name).present?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def trigger_name(table_name, trigger_event)
|
|
115
|
+
"stagehand_#{trigger_event}_trigger_#{table_name}".downcase
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def get_triggers(table_name = nil)
|
|
119
|
+
statement = <<~SQL
|
|
120
|
+
SHOW TRIGGERS WHERE `Trigger` LIKE 'stagehand_%'
|
|
121
|
+
SQL
|
|
122
|
+
statement << " AND `Table` LIKE #{ActiveRecord::Base.connection.quote(table_name)}" if table_name.present?
|
|
123
|
+
|
|
124
|
+
return ActiveRecord::Base.connection.select_all(statement)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def expunge(table_name)
|
|
128
|
+
commit_ids = [] # Keep track of commits that we need to clean up if they're now empty
|
|
129
|
+
|
|
130
|
+
# Remove records from the table as the subject of any commits
|
|
131
|
+
Stagehand::Staging::CommitEntry.start_operations.where(:table_name => table_name).in_batches do |batch|
|
|
132
|
+
commit_ids.concat batch.contained.distinct.pluck(:commit_id)
|
|
133
|
+
batch.update_all(:record_id => nil, :table_name => nil)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Remove commit entries for records from the table
|
|
137
|
+
Stagehand::Staging::CommitEntry.content_operations.where(:table_name => table_name).in_batches do |batch|
|
|
138
|
+
commit_ids.concat batch.contained.distinct.pluck(:commit_id)
|
|
139
|
+
batch.delete_all
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
Stagehand::Staging::Commit.find(commit_ids).select(&:empty?).each(&:destroy)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Stagehand
|
|
2
|
+
module SchemaExtensions
|
|
3
|
+
def define(*)
|
|
4
|
+
# Allow production writes during Schema.define to allow Rails to write to ar_internal_metadata table
|
|
5
|
+
Stagehand::Connection.with_production_writes { super }
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
ActiveRecord::Schema.prepend(Stagehand::SchemaExtensions)
|