table_sync 6.9.3 → 6.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 238a612ebdd5b85291cb3ba0c6675125a19880c83ce9deeea32454a2691a3127
4
- data.tar.gz: 5cb1b9c0c7ce04af3ac19e12935c9f385a820d5c037a7d9f112ac27fbf334951
3
+ metadata.gz: c9053c457facb21583722141c254d7795ce512a50be9b433c1fc6e4678b153c2
4
+ data.tar.gz: a3e030b3ef171c83ae81192712ce9db71789986f4f7d4c2121d335d516a34a1c
5
5
  SHA512:
6
- metadata.gz: 02746efdbdc881da38b0a7db32495ee4e97cb79edaa019d84d56396f366fc985f387e14207465a7ab02e4bde1d28249488c96857cead2fba78160109c7d74949
7
- data.tar.gz: 40a67f153d5ad41df0c54935c756e93f819b2c63a1d688e867bd01797fe3da32f0990cf35f759cb9b27b1c8148b9b791f2ba114111878b0cc36957de025b6aeb
6
+ metadata.gz: ae4407fb443aec3794f52f8b4b81681ea6bae82fe3838b4e59403fde12a4bdc445bb831cffd53afb5759fe99f311fe1dfdbf6b73b2b2fd995bc4fff88cc4befb
7
+ data.tar.gz: 0714f4fecc46b8134cb1f06112d1e72912a28c563118ca901a72f7adb10d42d3a3d4bc65a55d2879e6788255b7c809fc84cd0bc54d7eac39cfa45df61ce28772
data/CHANGELOG.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Changelog
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
+ ## [6.10.0] - 2025-11-21
5
+ ### Fixed
6
+ - Add on_first_sync callback
7
+
4
8
  ## [6.9.3] - 2025-10-09
5
9
  ### Fixed
6
10
  - Add validate_types to model interface
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- table_sync (6.9.3)
4
+ table_sync (6.10.0)
5
5
  memery
6
6
  rabbit_messaging (>= 1.7.0)
7
7
  rails
@@ -90,12 +90,12 @@ GEM
90
90
  bunny (2.24.0)
91
91
  amq-protocol (~> 2.3)
92
92
  sorted_set (~> 1, >= 1.0.2)
93
- cgi (0.4.2)
93
+ cgi (0.5.0)
94
94
  coderay (1.1.3)
95
95
  concurrent-ruby (1.3.5)
96
96
  connection_pool (2.5.3)
97
97
  crass (1.0.6)
98
- date (3.4.1)
98
+ date (3.5.0)
99
99
  diff-lcs (1.6.2)
100
100
  docile (1.4.1)
101
101
  drb (2.2.3)
@@ -106,8 +106,8 @@ GEM
106
106
  activesupport (>= 6.1)
107
107
  i18n (1.14.7)
108
108
  concurrent-ruby (~> 1.0)
109
- io-console (0.8.0)
110
- irb (1.15.2)
109
+ io-console (0.8.1)
110
+ irb (1.15.3)
111
111
  pp (>= 0.6.0)
112
112
  rdoc (>= 4.0.0)
113
113
  reline (>= 0.4.2)
@@ -124,18 +124,19 @@ GEM
124
124
  loofah (2.24.1)
125
125
  crass (~> 1.0.2)
126
126
  nokogiri (>= 1.12.0)
127
- mail (2.8.1)
127
+ mail (2.9.0)
128
+ logger
128
129
  mini_mime (>= 0.1.1)
129
130
  net-imap
130
131
  net-pop
131
132
  net-smtp
132
- marcel (1.0.4)
133
+ marcel (1.1.0)
133
134
  memery (1.7.0)
134
135
  method_source (1.1.0)
135
136
  mini_mime (1.1.5)
136
137
  mini_portile2 (2.8.9)
137
138
  minitest (5.25.5)
138
- net-imap (0.5.8)
139
+ net-imap (0.5.12)
139
140
  date
140
141
  net-protocol
141
142
  net-pop (0.1.2)
@@ -170,7 +171,7 @@ GEM
170
171
  ast (~> 2.4.1)
171
172
  racc
172
173
  pg (1.5.9)
173
- pp (0.6.2)
174
+ pp (0.6.3)
174
175
  prettyprint
175
176
  prettyprint (0.2.0)
176
177
  prism (1.4.0)
@@ -224,11 +225,12 @@ GEM
224
225
  rainbow (3.1.1)
225
226
  rake (13.2.1)
226
227
  rbtree (0.4.6)
227
- rdoc (6.14.0)
228
+ rdoc (6.15.1)
228
229
  erb
229
230
  psych (>= 4.0.0)
231
+ tsort
230
232
  regexp_parser (2.10.0)
231
- reline (0.6.1)
233
+ reline (0.6.3)
232
234
  io-console (~> 0.5)
233
235
  rspec (3.13.1)
234
236
  rspec-core (~> 3.13.0)
@@ -308,10 +310,11 @@ GEM
308
310
  sorted_set (1.0.3)
309
311
  rbtree
310
312
  set (~> 1.0)
311
- stringio (3.1.7)
312
- thor (1.3.2)
313
+ stringio (3.1.8)
314
+ thor (1.4.0)
313
315
  timecop (0.9.10)
314
316
  timeout (0.4.3)
317
+ tsort (0.2.0)
315
318
  tzinfo (2.0.6)
316
319
  concurrent-ruby (~> 1.0)
317
320
  unicode-display_width (3.1.4)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "hooks/once"
4
+
3
5
  module TableSync::Receiving
4
6
  class Config
5
7
  attr_reader :model, :events
@@ -23,13 +25,10 @@ module TableSync::Receiving
23
25
  class << self
24
26
  attr_reader :default_values_for_options
25
27
 
26
- # In a configs this options are requested as they are
28
+ # In a configs these options are requested as they are
27
29
  # config.option - get value
28
30
  # config.option(args) - set static value
29
31
  # config.option { ... } - set proc as value
30
- #
31
- # In `Receiving::Handler` or `Receiving::EventActions` this options are requested
32
- # through `Receiving::ConfigDecorator#method_missing` which always executes `config.option`
33
32
 
34
33
  def add_option(name, value_setter_wrapper:, value_as_proc_setter_wrapper:, default:)
35
34
  ivar = :"@#{name}"
@@ -55,11 +54,30 @@ module TableSync::Receiving
55
54
  instance_variable_set(ivar, result_value)
56
55
  end
57
56
  end
57
+
58
+ def add_hook_option(name, hook_class:)
59
+ ivar = :"@#{name}"
60
+
61
+ @default_values_for_options ||= {}
62
+ @default_values_for_options[ivar] = proc { [] }
63
+
64
+ define_method(name) do |conditions, &handler|
65
+ hooks = instance_variable_get(ivar)
66
+ hooks ||= []
67
+
68
+ hooks << hook_class.new(conditions:, handler:)
69
+ instance_variable_set(ivar, hooks)
70
+ end
71
+ end
58
72
  end
59
73
 
60
74
  def allow_event?(name)
61
75
  events.include?(name)
62
76
  end
77
+
78
+ def option(name)
79
+ instance_variable_get(:"@#{name}")
80
+ end
63
81
  end
64
82
  end
65
83
 
@@ -201,6 +219,11 @@ TableSync::Receiving::Config.add_option :wrap_receiving,
201
219
  value_as_proc_setter_wrapper: any_value,
202
220
  default: proc { proc { |&block| block.call } }
203
221
 
222
+ TableSync::Receiving::Config.add_hook_option(
223
+ :on_first_sync,
224
+ hook_class: TableSync::Receiving::Hooks::Once,
225
+ )
226
+
204
227
  %i[
205
228
  before_update
206
229
  after_commit_on_update
@@ -2,9 +2,6 @@
2
2
 
3
3
  module TableSync::Receiving
4
4
  class ConfigDecorator
5
- extend Forwardable
6
-
7
- def_delegators :@config, :allow_event?
8
5
  # rubocop:disable Metrics/ParameterLists
9
6
  def initialize(config:, event:, model:, version:, project_id:, raw_data:)
10
7
  @config = config
@@ -19,9 +16,17 @@ module TableSync::Receiving
19
16
  end
20
17
  # rubocop:enable Metrics/ParameterLists
21
18
 
22
- def method_missing(name, **additional_params, &)
23
- value = @config.send(name)
19
+ def option(name, **additional_params, &)
20
+ value = @config.option(name)
24
21
  value.is_a?(Proc) ? value.call(@default_params.merge(additional_params), &) : value
25
22
  end
23
+
24
+ def model
25
+ @config.model
26
+ end
27
+
28
+ def allow_event?(name)
29
+ @config.allow_event?(name)
30
+ end
26
31
  end
27
32
  end
@@ -22,22 +22,20 @@ class TableSync::Receiving::Handler < Rabbit::EventHandler
22
22
 
23
23
  next if data.empty?
24
24
 
25
- version_key = config.version_key(data:)
26
- data.each { |row| row[version_key] = version }
27
-
28
- target_keys = config.target_keys(data:)
25
+ target_keys = config.option(:target_keys, data:)
29
26
 
30
27
  validate_data(data, target_keys:)
31
28
 
32
29
  data.sort_by! { |row| row.values_at(*target_keys).map { |value| sort_key(value) } }
33
30
 
31
+ version_key = config.option(:version_key, data:)
34
32
  params = { data:, target_keys:, version_key: }
35
33
 
36
34
  if event == :update
37
- params[:default_values] = config.default_values(data:)
35
+ params[:default_values] = config.option(:default_values, data:)
38
36
  end
39
37
 
40
- config.wrap_receiving(event:, **params) do
38
+ config.option(:wrap_receiving, **params) do
41
39
  perform(config, params)
42
40
  end
43
41
  end
@@ -83,25 +81,28 @@ class TableSync::Receiving::Handler < Rabbit::EventHandler
83
81
  end
84
82
 
85
83
  def processed_data(config)
84
+ version_key = config.option(:version_key, data:)
86
85
  data.filter_map do |row|
87
- next if config.skip(row:)
86
+ next if config.option(:skip, row:)
88
87
 
89
88
  row = row.dup
90
89
 
91
- config.mapping_overrides(row:).each do |before, after|
90
+ config.option(:mapping_overrides, row:).each do |before, after|
92
91
  row[after] = row.delete(before)
93
92
  end
94
93
 
95
- config.except(row:).each { |x| row.delete(x) }
94
+ config.option(:except, row:).each { |x| row.delete(x) }
96
95
 
97
- row.merge!(config.additional_data(row:))
96
+ row.merge!(config.option(:additional_data, row:))
98
97
 
99
- only = config.only(row:)
98
+ only = config.option(:only, row:)
100
99
  row, rest = row.partition { |key, _| key.in?(only) }.map(&:to_h)
101
100
 
102
- rest_key = config.rest_key(row:, rest:)
101
+ rest_key = config.option(:rest_key, row:, rest:)
103
102
  (row[rest_key] ||= {}).merge!(rest) if rest_key
104
103
 
104
+ row[version_key] = version
105
+
105
106
  row
106
107
  end
107
108
  end
@@ -138,16 +139,16 @@ class TableSync::Receiving::Handler < Rabbit::EventHandler
138
139
  raise TableSync::DataError.new(data, errors.keys, errors.to_json)
139
140
  end
140
141
 
141
- def perform(config, params)
142
+ def perform(config, params) # rubocop:disable Metrics/MethodLength
142
143
  model = config.model
143
144
 
144
145
  model.transaction do
145
146
  results = if event == :update
146
- config.before_update(**params)
147
+ config.option(:before_update, **params)
147
148
  validate_data_types(model, params[:data])
148
149
  model.upsert(**params)
149
150
  else
150
- config.before_destroy(**params)
151
+ config.option(:before_destroy, **params)
151
152
  model.destroy(**params)
152
153
  end
153
154
 
@@ -157,9 +158,15 @@ class TableSync::Receiving::Handler < Rabbit::EventHandler
157
158
  end
158
159
 
159
160
  if event == :update
160
- model.after_commit { config.after_commit_on_update(**params, results:) }
161
+ model.after_commit do
162
+ config.option(:after_commit_on_update, **params, results:)
163
+
164
+ Array(config.option(:on_first_sync)).each do |hook|
165
+ hook.perform(config:, targets: results) if hook.enabled?
166
+ end
167
+ end
161
168
  else
162
- model.after_commit { config.after_commit_on_destroy(**params, results:) }
169
+ model.after_commit { config.option(:after_commit_on_destroy, **params, results:) }
163
170
  end
164
171
  end
165
172
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TableSync::Receiving::Hooks
4
+ class Once
5
+ LOCK_KEY = "hook-once-lock-key"
6
+
7
+ attr_reader :conditions, :handler, :lookup_code
8
+
9
+ def initialize(conditions:, handler:)
10
+ @conditions = conditions
11
+ @handler = handler
12
+ init_lookup_code
13
+ end
14
+
15
+ def enabled?
16
+ conditions[:columns].any?
17
+ end
18
+
19
+ def perform(config:, targets:)
20
+ target_keys = config.option(:target_keys)
21
+ model = config.model
22
+
23
+ targets.each do |target|
24
+ next unless conditions?(target)
25
+
26
+ keys = target.slice(*target_keys)
27
+ model.try_advisory_lock(prepare_lock_key(keys)) do
28
+ model.find_and_save(keys:) do |entry|
29
+ next unless allow?(entry)
30
+
31
+ entry.hooks ||= []
32
+ entry.hooks << lookup_code
33
+ model.after_commit { handler.call(entry:) }
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def allow?(entry)
42
+ Array(entry.hooks).exclude?(lookup_code)
43
+ end
44
+
45
+ def init_lookup_code
46
+ @lookup_code = conditions[:columns].map do |column|
47
+ "#{column}-#{conditions[column]}"
48
+ end.join(":")
49
+ end
50
+
51
+ def conditions?(row)
52
+ conditions[:columns].all? do |column|
53
+ row[column] == (conditions[column] || row[column])
54
+ end
55
+ end
56
+
57
+ def prepare_lock_key(row_keys)
58
+ lock_keys = [LOCK_KEY] + row_keys.values
59
+ Zlib.crc32(lock_keys.join(":")) % (2**31)
60
+ end
61
+ end
62
+ end
@@ -2,6 +2,13 @@
2
2
 
3
3
  module TableSync::Receiving::Model
4
4
  class ActiveRecord
5
+ ISOLATION_LEVELS = {
6
+ uncommitted: :read_uncommitted,
7
+ committed: :read_committed,
8
+ repeatable: :repeatable_read,
9
+ serializable: :serializable,
10
+ }.freeze
11
+
5
12
  class AfterCommitWrap
6
13
  def initialize(&block)
7
14
  @callback = block
@@ -33,6 +40,10 @@ module TableSync::Receiving::Model
33
40
  @schema = model_naming.schema.to_sym
34
41
  end
35
42
 
43
+ def isolation_level(lookup_code)
44
+ ISOLATION_LEVELS.fetch(lookup_code)
45
+ end
46
+
36
47
  def columns
37
48
  raw_model.column_names.map(&:to_sym)
38
49
  end
@@ -110,14 +121,30 @@ module TableSync::Receiving::Model
110
121
  types_validator.validate(data)
111
122
  end
112
123
 
113
- def transaction(&)
114
- ::ActiveRecord::Base.transaction(&)
124
+ def transaction(**params, &)
125
+ ::ActiveRecord::Base.transaction(**params, &)
115
126
  end
116
127
 
117
128
  def after_commit(&)
118
129
  db.add_transaction_record(AfterCommitWrap.new(&))
119
130
  end
120
131
 
132
+ def try_advisory_lock(lock_key)
133
+ transaction do
134
+ if db.query_value("SELECT pg_try_advisory_xact_lock(#{lock_key.to_i})")
135
+ yield
136
+ end
137
+ end
138
+ end
139
+
140
+ def find_and_save(keys:)
141
+ entry = raw_model.find_by(keys)
142
+ return unless entry
143
+
144
+ yield entry
145
+ entry.save!
146
+ end
147
+
121
148
  private
122
149
 
123
150
  attr_reader :raw_model, :types_validator
@@ -4,6 +4,13 @@ module TableSync::Receiving::Model
4
4
  class Sequel
5
5
  attr_reader :table, :schema
6
6
 
7
+ ISOLATION_LEVELS = {
8
+ uncommitted: :uncommitted,
9
+ committed: :committed,
10
+ repeatable: :repeatable,
11
+ serializable: :serializable,
12
+ }.freeze
13
+
7
14
  def initialize(table_name)
8
15
  @raw_model = Class.new(::Sequel::Model(table_name)).tap(&:unrestrict_primary_key)
9
16
  @types_validator = TableSync::Utils::Schema::Builder::Sequel.build(@raw_model)
@@ -17,6 +24,10 @@ module TableSync::Receiving::Model
17
24
  @schema = model_naming.schema.to_sym
18
25
  end
19
26
 
27
+ def isolation_level(lookup_code)
28
+ ISOLATION_LEVELS.fetch(lookup_code)
29
+ end
30
+
20
31
  def columns
21
32
  dataset.columns
22
33
  end
@@ -57,14 +68,30 @@ module TableSync::Receiving::Model
57
68
  types_validator.validate(data)
58
69
  end
59
70
 
60
- def transaction(&)
61
- db.transaction(&)
71
+ def transaction(**params, &)
72
+ db.transaction(**params, &)
62
73
  end
63
74
 
64
75
  def after_commit(&)
65
76
  db.after_commit(&)
66
77
  end
67
78
 
79
+ def try_advisory_lock(lock_key)
80
+ transaction do
81
+ if db.get(::Sequel.function(:pg_try_advisory_xact_lock, lock_key.to_i))
82
+ yield
83
+ end
84
+ end
85
+ end
86
+
87
+ def find_and_save(keys:)
88
+ entry = dataset.first(keys)
89
+ return unless entry
90
+
91
+ yield entry
92
+ entry.save_changes
93
+ end
94
+
68
95
  private
69
96
 
70
97
  attr_reader :raw_model, :types_validator
@@ -6,6 +6,7 @@ module TableSync
6
6
  require_relative "receiving/config_decorator"
7
7
  require_relative "receiving/dsl"
8
8
  require_relative "receiving/handler"
9
+ require_relative "receiving/hooks/once"
9
10
  require_relative "receiving/model/active_record"
10
11
  require_relative "receiving/model/sequel"
11
12
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TableSync
4
- VERSION = "6.9.3"
4
+ VERSION = "6.10.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: table_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.9.3
4
+ version: 6.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Umbrellio
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-10-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: memery
@@ -118,6 +117,7 @@ files:
118
117
  - lib/table_sync/receiving/config_decorator.rb
119
118
  - lib/table_sync/receiving/dsl.rb
120
119
  - lib/table_sync/receiving/handler.rb
120
+ - lib/table_sync/receiving/hooks/once.rb
121
121
  - lib/table_sync/receiving/model/active_record.rb
122
122
  - lib/table_sync/receiving/model/sequel.rb
123
123
  - lib/table_sync/setup/active_record.rb
@@ -141,7 +141,6 @@ homepage: https://github.com/umbrellio/table_sync
141
141
  licenses:
142
142
  - MIT
143
143
  metadata: {}
144
- post_install_message:
145
144
  rdoc_options: []
146
145
  require_paths:
147
146
  - lib
@@ -156,8 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
155
  - !ruby/object:Gem::Version
157
156
  version: '0'
158
157
  requirements: []
159
- rubygems_version: 3.3.27
160
- signing_key:
158
+ rubygems_version: 3.6.9
161
159
  specification_version: 4
162
160
  summary: DB Table synchronization between microservices based on Model's event system
163
161
  and RabbitMQ messaging