culturecode_stagehand 1.1.5 → 1.1.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b9684506c931ca15ad9c467e5d96fb359ede46c3d41624331b72b7efe0b7b850
4
- data.tar.gz: 43f4509740eb53ace3293ad6bdbd718562e165adf282e1ca2114c5daebd5ee15
3
+ metadata.gz: a254ae2d2cba86110f9584be58f6975ca777958bec8c1bc6e71f18195080d43e
4
+ data.tar.gz: 1a4a94337e918823c620bda226fba92789a9e47b9067a6daf6fff5676e33648b
5
5
  SHA512:
6
- metadata.gz: 5cca6f14c6f8c9194d400a5c21bb49f90758e2160a9ebac99b72621f8b64410f91429a5b6f3ba62db4faf92d7d50caf1caee66a4d5631ccf4ac196206677130c
7
- data.tar.gz: 28f4439a34ed046edff565df691fd4d1fada3e227a52a9fc3d60fec9b245db8d0a0c4e50629ab86febc7e5d0171c270b7257ee4121b0b5517296f6fb152b4eb6
6
+ metadata.gz: 1c2a0b58055a9033a86a4f7fce1a3b83f92140e3390f39aa8b745dc8d9b3e0bcc01e2c8724c810c85d67080ff6c2810a61f711735716269143098b04a2c38523
7
+ data.tar.gz: 1355afb448d1f0c5549bb814ef1c9a34f36983ce3c78a81f7bf3edc93f0501bc1349718a5a076548682b604193ac3a991093c2cab24b8f8937f980afa4091b0a
@@ -71,16 +71,16 @@ ActiveRecord::Base.class_eval do
71
71
  # Keep track of the current connection name per-model, per-thread so multithreaded webservers don't overwrite it
72
72
  module StagehandConnectionMap
73
73
  def self.set(klass, connection_name)
74
- currentMap[klass.name] = connection_name
74
+ current_map[klass.name] = connection_name
75
75
  end
76
76
 
77
77
  def self.get(klass)
78
- currentMap[klass.name]
78
+ current_map[klass.name]
79
79
  end
80
80
 
81
- def self.currentMap
81
+ def self.current_map
82
82
  map = Thread.current.thread_variable_get('StagehandConnectionMap')
83
- map = Thread.current.thread_variable_set('StagehandConnectionMap', {}) unless map
83
+ map = Thread.current.thread_variable_set('StagehandConnectionMap', Concurrent::Hash.new) unless map
84
84
  return map
85
85
  end
86
86
  end
@@ -9,7 +9,8 @@ module Stagehand
9
9
  module Configuration
10
10
  extend self
11
11
 
12
- mattr_accessor :checklist_confirmation_filter, :checklist_association_filter, :checklist_relation_filter, :ignored_columns
12
+ mattr_accessor :checklist_confirmation_filter, :checklist_association_filter, :checklist_relation_filter, :staging_model_tables, :ignored_columns
13
+ self.staging_model_tables = Set.new
13
14
  self.ignored_columns = HashWithIndifferentAccess.new
14
15
 
15
16
  def staging_connection_name
@@ -9,14 +9,30 @@ module Stagehand
9
9
  end
10
10
 
11
11
  def self.allow_unsynced_production_writes!(state = true)
12
- Thread.current[:stagehand_allow_unsynced_production_writes] = state
12
+ Thread.current.thread_variable_set(:stagehand_allow_unsynced_production_writes, state)
13
13
  end
14
14
 
15
15
  def self.allow_unsynced_production_writes?
16
- !!Thread.current[:stagehand_allow_unsynced_production_writes]
16
+ !!Thread.current.thread_variable_get(:stagehand_allow_unsynced_production_writes)
17
17
  end
18
18
 
19
19
  module AdapterExtensions
20
+ def quote_table_name(table_name)
21
+ if prefix_table_name_with_database?(table_name)
22
+ super("#{Stagehand::Database.staging_database_name}.#{table_name}")
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ def prefix_table_name_with_database?(table_name)
29
+ return false if Configuration.single_connection?
30
+ return false unless Database.connected_to_production?
31
+ return false if Connection.allow_unsynced_production_writes?
32
+ return false unless Configuration.staging_model_tables.include?(table_name)
33
+ true
34
+ end
35
+
20
36
  def exec_insert(*)
21
37
  handle_readonly_writes!
22
38
  super
@@ -5,6 +5,31 @@ module Stagehand
5
5
  module Database
6
6
  extend self
7
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
+
8
33
  def each(&block)
9
34
  with_production_connection(&block) unless Configuration.single_connection?
10
35
  with_staging_connection(&block)
@@ -62,30 +87,17 @@ module Stagehand
62
87
  return output
63
88
  end
64
89
 
65
- def transaction
66
- success = false
67
- output = nil
68
- ActiveRecord::Base.transaction do
69
- Production::Record.transaction do
70
- output = yield
71
- success = true
72
- end
73
- raise ActiveRecord::Rollback unless success
74
- end
75
- return output
76
- end
77
-
78
90
  private
79
91
 
80
92
  def swap_connection(connection_name)
93
+ pushed = ConnectionStack.push(connection_name.to_sym)
81
94
  cache = ActiveRecord::Base.connection_pool.query_cache_enabled
82
- ConnectionStack.push(connection_name.to_sym)
83
95
  ActiveRecord::Base.connection_specification_name = current_connection_name
84
96
  ActiveRecord::Base.connection_pool.enable_query_cache! if cache
85
97
 
86
98
  yield connection_name
87
99
  ensure
88
- ConnectionStack.pop
100
+ ConnectionStack.pop if pushed
89
101
  ActiveRecord::Base.connection_specification_name = current_connection_name
90
102
  ActiveRecord::Base.connection_pool.enable_query_cache! if cache
91
103
  end
@@ -128,6 +140,14 @@ module Stagehand
128
140
  super(Configuration.staging_connection_name)
129
141
  end
130
142
 
143
+ def self.connection
144
+ if Stagehand::Database.connected_to_staging?
145
+ ActiveRecord::Base.connection # Reuse existing connection so we stay within the current transaction
146
+ else
147
+ super
148
+ end
149
+ end
150
+
131
151
  init_connection
132
152
  end
133
153
 
@@ -143,8 +163,6 @@ module Stagehand
143
163
 
144
164
  # Threadsafe tracking of the connection stack
145
165
  module ConnectionStack
146
- @@connection_name_stack = Hash.new { |h,k| h[k] = [ Rails.env.to_sym ] }
147
-
148
166
  def self.push(connection_name)
149
167
  current_stack.push connection_name
150
168
  end
@@ -158,8 +176,18 @@ module Stagehand
158
176
  end
159
177
 
160
178
  def self.current_stack
161
- @@connection_name_stack[Thread.current.object_id]
179
+ if stack = Thread.current.thread_variable_get('sparkle_connection_name_stack')
180
+ stack
181
+ else
182
+ stack = Concurrent::Array.new
183
+ stack << Rails.env.to_sym
184
+ Thread.current.thread_variable_set('sparkle_connection_name_stack', stack)
185
+ stack
186
+ end
162
187
  end
163
188
  end
189
+
190
+ class InvalidConnectionError < StandardError; end
191
+ class NoRetryError < StandardError; end
164
192
  end
165
193
  end
@@ -24,16 +24,19 @@ module Stagehand
24
24
  end
25
25
 
26
26
  def write(staging_record, attributes, table_name = nil)
27
- Connection.with_production_writes do
28
- is_new = matching(staging_record, table_name).update_all(attributes).zero?
27
+ table_name, id = Stagehand::Key.generate(staging_record, :table_name => table_name)
29
28
 
30
- # Ensure we always return a record, even when updating instead of creating
31
- Record.new.tap do |record|
32
- record.assign_attributes(attributes)
33
- record.id = Stagehand::Key.generate(staging_record, :table_name => table_name).last unless record.id
34
- record.save if is_new
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))
35
36
  end
36
37
  end
38
+
39
+ return production_record
37
40
  end
38
41
 
39
42
  def delete(staging_record, table_name = nil)
@@ -67,25 +70,58 @@ module Stagehand
67
70
 
68
71
  private
69
72
 
70
- def prepare_to_modify(table_name)
71
- raise "Can't prepare to modify production records without knowning the table_name" unless table_name.present?
72
- Record.table_name = table_name
73
- end
74
-
75
73
  def production_record_attributes(staging_record, table_name = nil)
76
74
  Record.connection.select_one(matching(staging_record, table_name))
77
75
  end
78
76
 
79
77
  def staging_record_attributes(staging_record, table_name = nil)
80
78
  table_name, id = Stagehand::Key.generate(staging_record, :table_name => table_name)
81
- hash = Stagehand::Staging::CommitEntry.connection.select_one("SELECT * FROM #{table_name} WHERE id = #{id}")
82
- hash&.except(*ignored_columns(table_name))
79
+ hash = select(table_name, id)
80
+ hash.except(*ignored_columns(table_name)) if hash
83
81
  end
84
82
 
85
83
  def ignored_columns(table_name)
86
84
  Array.wrap(Configuration.ignored_columns[table_name]).map(&:to_s)
87
85
  end
88
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
+
89
125
  # CLASSES
90
126
 
91
127
  class Record < Stagehand::Database::ProductionProbe
@@ -28,6 +28,8 @@ module Stagehand
28
28
  end
29
29
 
30
30
  def add_stagehand!(options = {})
31
+ return if Database.connected_to_production? && !Stagehand::Configuration.single_connection?
32
+
31
33
  ActiveRecord::Schema.define do
32
34
  Stagehand::Schema.send :each_table, options do |table_name|
33
35
  Stagehand::Schema.send :create_operation_trigger, table_name, 'insert', 'NEW'
@@ -40,6 +42,7 @@ module Stagehand
40
42
  def remove_stagehand!(options = {})
41
43
  ActiveRecord::Schema.define do
42
44
  Stagehand::Schema.send :each_table, options do |table_name|
45
+ next unless Stagehand::Schema.send :has_stagehand_triggers?, table_name
43
46
  Stagehand::Schema.send :drop_trigger, table_name, 'insert'
44
47
  Stagehand::Schema.send :drop_trigger, table_name, 'update'
45
48
  Stagehand::Schema.send :drop_trigger, table_name, 'delete'
@@ -53,9 +56,9 @@ module Stagehand
53
56
  if UNTRACKED_TABLES.include?(table_name.to_s)
54
57
  return false
55
58
  elsif table_name
56
- trigger_exists?(table_name, 'insert')
59
+ has_stagehand_triggers?(table_name)
57
60
  else
58
- ActiveRecord::Base.Connection.table_exists?(Stagehand::Staging::CommitEntry.table_name)
61
+ ActiveRecord::Base.connection.table_exists?(Stagehand::Staging::CommitEntry.table_name)
59
62
  end
60
63
  end
61
64
 
@@ -106,8 +109,21 @@ module Stagehand
106
109
  ActiveRecord::Base.connection.select_one("SHOW TRIGGERS where `trigger` = '#{trigger_name(table_name, trigger_event)}'").present?
107
110
  end
108
111
 
112
+ def has_stagehand_triggers?(table_name)
113
+ get_triggers(table_name).present?
114
+ end
115
+
109
116
  def trigger_name(table_name, trigger_event)
110
117
  "stagehand_#{trigger_event}_trigger_#{table_name}".downcase
111
118
  end
119
+
120
+ def get_triggers(table_name = nil)
121
+ statement = <<~SQL
122
+ SHOW TRIGGERS WHERE `Trigger` LIKE 'stagehand_%'
123
+ SQL
124
+ statement << " AND `Table` LIKE #{ActiveRecord::Base.connection.quote(table_name)}" if table_name.present?
125
+
126
+ return ActiveRecord::Base.connection.select_all(statement)
127
+ end
112
128
  end
113
129
  end
@@ -33,7 +33,7 @@ module Stagehand
33
33
  end
34
34
 
35
35
  # Also include uncontained commit entries that matched
36
- related_entries.concat(CommitEntry.uncontained.matching(entries + related_entries))
36
+ related_entries.concat(CommitEntry.uncontained.not_in_progress.matching(entries + related_entries))
37
37
  related_entries.uniq!
38
38
 
39
39
  return related_entries
@@ -61,8 +61,9 @@ module Stagehand
61
61
  end
62
62
 
63
63
  def self.infer_base_class(table_name)
64
- classes = ActiveRecord::Base.descendants.select {|klass| klass.table_name == table_name }
65
- classes.delete(Stagehand::Production::Record)
64
+ classes = ActiveRecord::Base.descendants
65
+ classes.select! {|klass| klass.table_name == table_name }
66
+ classes.reject! {|klass| klass < Stagehand::Database::Probe }
66
67
  return classes.first || table_name.classify.constantize.base_class # Try loading the class if it isn't loaded yet
67
68
  rescue NameError
68
69
  raise(IndeterminateRecordClass, "Can't determine class from table name: #{table_name}")
@@ -3,12 +3,22 @@ module Stagehand
3
3
  module Model
4
4
  extend ActiveSupport::Concern
5
5
 
6
+ included do
7
+ Stagehand::Configuration.staging_model_tables << table_name
8
+ end
9
+
6
10
  class_methods do
11
+ def quoted_table_name
12
+ if connection.prefix_table_name_with_database?(table_name)
13
+ @prefixed_quoted_table_name ||= connection.quote_table_name(table_name)
14
+ else
15
+ super
16
+ end
17
+ end
18
+
7
19
  def connection
8
20
  if Configuration.ghost_mode?
9
21
  super
10
- elsif Stagehand::Database.connected_to_staging?
11
- ActiveRecord::Base.connection
12
22
  else
13
23
  Stagehand::Database::StagingProbe.connection
14
24
  end
@@ -133,7 +133,7 @@ module Stagehand
133
133
  raise SchemaMismatch unless schemas_match?
134
134
 
135
135
  run_sync_callbacks(entry, callbacks) do
136
- Rails.logger.info "Synchronizing #{entry.table_name} #{entry.record_id}" if entry.content_operation?
136
+ Rails.logger.info "Synchronizing #{entry.table_name} #{entry.record_id} (#{entry.operation})" if entry.content_operation?
137
137
  if Configuration.single_connection?
138
138
  next # Avoid deadlocking if the databases are the same
139
139
  elsif entry.delete_operation?
@@ -1,3 +1,3 @@
1
1
  module Stagehand
2
- VERSION = "1.1.5"
2
+ VERSION = "1.1.10"
3
3
  end
@@ -34,6 +34,8 @@ namespace :stagehand do
34
34
  end
35
35
 
36
36
  rake_both_databases('db:migrate')
37
+ rake_both_databases('db:migrate:up')
38
+ rake_both_databases('db:migrate:down')
37
39
  rake_both_databases('db:rollback')
38
40
  rake_both_databases('db:test:load_structure')
39
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: culturecode_stagehand
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.5
4
+ version: 1.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicholas Jakobsen
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-06-29 00:00:00.000000000 Z
12
+ date: 2021-11-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -166,8 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
166
166
  - !ruby/object:Gem::Version
167
167
  version: '0'
168
168
  requirements: []
169
- rubyforge_project:
170
- rubygems_version: 2.7.9
169
+ rubygems_version: 3.0.3
171
170
  signing_key:
172
171
  specification_version: 4
173
172
  summary: Simplify the management of a sandbox database that can sync content to a