culturecode_stagehand 1.1.5 → 1.1.10

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 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