table_sync 2.3.0 → 4.0.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -0
  3. data/CHANGELOG.md +40 -0
  4. data/Gemfile.lock +82 -77
  5. data/README.md +4 -2
  6. data/docs/message_protocol.md +24 -0
  7. data/docs/notifications.md +45 -0
  8. data/docs/publishing.md +147 -0
  9. data/docs/receiving.md +341 -0
  10. data/lib/table_sync.rb +16 -31
  11. data/lib/table_sync/errors.rb +39 -23
  12. data/lib/table_sync/publishing.rb +11 -0
  13. data/lib/table_sync/{base_publisher.rb → publishing/base_publisher.rb} +1 -1
  14. data/lib/table_sync/{batch_publisher.rb → publishing/batch_publisher.rb} +4 -4
  15. data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/active_record.rb +3 -7
  16. data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/sequel.rb +2 -6
  17. data/lib/table_sync/{publisher.rb → publishing/publisher.rb} +4 -4
  18. data/lib/table_sync/receiving.rb +14 -0
  19. data/lib/table_sync/receiving/config.rb +218 -0
  20. data/lib/table_sync/receiving/config_decorator.rb +27 -0
  21. data/lib/table_sync/receiving/dsl.rb +28 -0
  22. data/lib/table_sync/receiving/handler.rb +131 -0
  23. data/lib/table_sync/{model → receiving/model}/active_record.rb +36 -22
  24. data/lib/table_sync/{model → receiving/model}/sequel.rb +13 -8
  25. data/lib/table_sync/utils.rb +9 -0
  26. data/lib/table_sync/utils/interface_checker.rb +97 -0
  27. data/lib/table_sync/utils/proc_array.rb +17 -0
  28. data/lib/table_sync/utils/proc_keywords_resolver.rb +46 -0
  29. data/lib/table_sync/version.rb +1 -1
  30. data/table_sync.gemspec +2 -1
  31. metadata +42 -30
  32. data/docs/development.md +0 -43
  33. data/docs/synopsis.md +0 -336
  34. data/lib/table_sync/config.rb +0 -105
  35. data/lib/table_sync/config/callback_registry.rb +0 -53
  36. data/lib/table_sync/config_decorator.rb +0 -38
  37. data/lib/table_sync/dsl.rb +0 -25
  38. data/lib/table_sync/event_actions.rb +0 -96
  39. data/lib/table_sync/event_actions/data_wrapper.rb +0 -7
  40. data/lib/table_sync/event_actions/data_wrapper/base.rb +0 -23
  41. data/lib/table_sync/event_actions/data_wrapper/destroy.rb +0 -19
  42. data/lib/table_sync/event_actions/data_wrapper/update.rb +0 -21
  43. data/lib/table_sync/plugins.rb +0 -72
  44. data/lib/table_sync/plugins/abstract.rb +0 -55
  45. data/lib/table_sync/plugins/access_mixin.rb +0 -49
  46. data/lib/table_sync/plugins/registry.rb +0 -153
  47. data/lib/table_sync/receiving_handler.rb +0 -76
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module TableSync::Model
3
+ module TableSync::Receiving::Model
4
4
  class ActiveRecord
5
5
  class AfterCommitWrap
6
6
  def initialize(&block)
@@ -52,27 +52,29 @@ module TableSync::Model
52
52
  SQL
53
53
  end
54
54
 
55
- def upsert(data:, target_keys:, version_key:, first_sync_time_key:, default_values:)
56
- data = Array.wrap(data)
55
+ def upsert(data:, target_keys:, version_key:, default_values:)
56
+ result = data.map do |datum|
57
+ conditions = datum.select { |k| target_keys.include?(k) }
57
58
 
58
- result = transaction do
59
- data.map do |datum|
60
- conditions = datum.select { |k| target_keys.include?(k) }
59
+ row = raw_model.lock("FOR NO KEY UPDATE").where(conditions)
61
60
 
62
- row = raw_model.lock("FOR NO KEY UPDATE").find_by(conditions)
63
- if row
64
- next if datum[version_key] <= row[version_key]
61
+ if row.to_a.size > 1
62
+ raise TableSync::UpsertError.new(data: datum, target_keys: target_keys, result: row)
63
+ end
65
64
 
66
- row.update!(datum)
67
- else
68
- create_data = default_values.merge(datum)
69
- create_data[first_sync_time_key] = Time.current if first_sync_time_key
70
- row = raw_model.create!(create_data)
71
- end
65
+ row = row.first
72
66
 
73
- row_to_hash(row)
74
- end.compact
75
- end
67
+ if row
68
+ next if datum[version_key] <= row[version_key]
69
+
70
+ row.update!(datum)
71
+ else
72
+ create_data = default_values.merge(datum)
73
+ row = raw_model.create!(create_data)
74
+ end
75
+
76
+ row_to_hash(row)
77
+ end.compact
76
78
 
77
79
  TableSync::Instrument.notify(table: model_naming.table, schema: model_naming.schema,
78
80
  event: :update, count: result.count, direction: :receive)
@@ -80,10 +82,22 @@ module TableSync::Model
80
82
  result
81
83
  end
82
84
 
83
- def destroy(data)
84
- result = transaction do
85
- row = raw_model.lock("FOR UPDATE").find_by(data)&.destroy!
86
- [row_to_hash(row)]
85
+ def destroy(data:, target_keys:, version_key:)
86
+ sanitized_data = data.map { |attr| attr.select { |key, _value| target_keys.include?(key) } }
87
+
88
+ query = nil
89
+ sanitized_data.each_with_index do |row, index|
90
+ if index == 0
91
+ query = raw_model.lock("FOR UPDATE").where(row)
92
+ else
93
+ query = query.or(raw_model.lock("FOR UPDATE").where(row))
94
+ end
95
+ end
96
+
97
+ result = query.destroy_all.map(&method(:row_to_hash))
98
+
99
+ if result.size > data.size
100
+ raise TableSync::DestroyError.new(data: data, target_keys: target_keys, result: result)
87
101
  end
88
102
 
89
103
  TableSync::Instrument.notify(
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module TableSync::Model
3
+ module TableSync::Receiving::Model
4
4
  class Sequel
5
5
  def initialize(table_name)
6
6
  @raw_model = Class.new(::Sequel::Model(table_name)).tap(&:unrestrict_primary_key)
@@ -14,8 +14,7 @@ module TableSync::Model
14
14
  [raw_model.primary_key].flatten
15
15
  end
16
16
 
17
- def upsert(data:, target_keys:, version_key:, first_sync_time_key:, default_values:)
18
- data = Array.wrap(data)
17
+ def upsert(data:, target_keys:, version_key:, default_values:)
19
18
  qualified_version = ::Sequel.qualify(table_name, version_key)
20
19
  version_condition = ::Sequel.function(:coalesce, qualified_version, 0) <
21
20
  ::Sequel.qualify(:excluded, version_key)
@@ -24,9 +23,6 @@ module TableSync::Model
24
23
  data.map! { |d| default_values.merge(d) }
25
24
 
26
25
  insert_data = type_cast(data)
27
- if first_sync_time_key
28
- insert_data.each { |datum| datum[first_sync_time_key] = Time.current }
29
- end
30
26
 
31
27
  result = dataset.returning
32
28
  .insert_conflict(
@@ -38,14 +34,23 @@ module TableSync::Model
38
34
 
39
35
  TableSync::Instrument.notify table: model_naming.table, schema: model_naming.schema,
40
36
  count: result.count, event: :update, direction: :receive
37
+
41
38
  result
42
39
  end
43
40
 
44
- def destroy(data)
45
- result = dataset.returning.where(data).delete
41
+ def destroy(data:, target_keys:, version_key:)
42
+ sanitized_data = data.map { |attr| attr.select { |key, _value| target_keys.include?(key) } }
43
+ sanitized_data = type_cast(sanitized_data)
44
+ result = dataset.returning.where(::Sequel.|(*sanitized_data)).delete
45
+
46
+ if result.size > data.size
47
+ raise TableSync::DestroyError.new(data: data, target_keys: target_keys, result: result)
48
+ end
49
+
46
50
  TableSync::Instrument.notify table: model_naming.table, schema: model_naming.schema,
47
51
  count: result.count,
48
52
  event: :destroy, direction: :receive
53
+
49
54
  result
50
55
  end
51
56
 
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TableSync
4
+ module Utils
5
+ require_relative "utils/proc_array"
6
+ require_relative "utils/proc_keywords_resolver"
7
+ require_relative "utils/interface_checker"
8
+ end
9
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby does not support interfaces, and there is no way to implement them.
4
+ # Interfaces check a methods of a class after the initialization of the class is complete.
5
+ # But in Ruby, the initialization of a class cannot be completed.
6
+ # In execution time we can open any class and add some methods (monkey patching).
7
+ # Ruby has `define_method`, singleton methods, etc.
8
+ #
9
+ # Duck typing is a necessary measure, the only one available in the Ruby architecture.
10
+ #
11
+ # Interfaces can be implemented in particular cases with tests for example.
12
+ # But this is not suitable for gems that are used by third-party code.
13
+ #
14
+ # So, we still want to check interfaces and have a nice error messages,
15
+ # even if it will be duck typing.
16
+ #
17
+ # Next code do this.
18
+
19
+ class TableSync::Utils::InterfaceChecker
20
+ INTERFACES = SelfData.load
21
+
22
+ attr_reader :object
23
+
24
+ def initialize(object)
25
+ @object = object
26
+ end
27
+
28
+ def implements(interface_name)
29
+ INTERFACES[interface_name].each do |method_name, options|
30
+ unless object.respond_to?(method_name)
31
+ raise_error(method_name, options)
32
+ end
33
+
34
+ unless include?(object.method(method_name).parameters, options[:parameters])
35
+ raise_error(method_name, options)
36
+ end
37
+ end
38
+ self
39
+ end
40
+
41
+ private
42
+
43
+ def include?(checked, expected)
44
+ (filter(expected) - filter(checked)).empty?
45
+ end
46
+
47
+ def raise_error(method_name, options)
48
+ raise TableSync::InterfaceError.new(
49
+ object,
50
+ method_name,
51
+ options[:parameters],
52
+ options[:description],
53
+ )
54
+ end
55
+
56
+ def filter(parameters)
57
+ # for req and block parameters types we can ignore names
58
+ parameters.map { |param| %i[req block].include?(param.first) ? [param.first] : param }
59
+ end
60
+ end
61
+
62
+ __END__
63
+ :receiving_model:
64
+ :upsert:
65
+ :parameters:
66
+ - - :keyreq
67
+ - :data
68
+ - - :keyreq
69
+ - :target_keys
70
+ - - :keyreq
71
+ - :version_key
72
+ - - :keyreq
73
+ - :default_values
74
+ :description: "returns an array with updated rows"
75
+ :columns:
76
+ :parameters: []
77
+ :description: "returns all table columns"
78
+ :destroy:
79
+ :parameters:
80
+ - - :keyreq
81
+ - :data
82
+ - - :keyreq
83
+ - :target_keys
84
+ :description: "returns an array with destroyed rows"
85
+ :transaction:
86
+ :parameters:
87
+ - - :block
88
+ - :block
89
+ :description: "implements the database transaction"
90
+ :after_commit:
91
+ :parameters:
92
+ - - :block
93
+ - :block
94
+ :description: "executes the block after committing the transaction"
95
+ :primary_keys:
96
+ :parameters: []
97
+ :description: "returns an array with the primary_keys"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TableSync::Utils::ProcArray < Proc
4
+ def initialize(&block)
5
+ @array = []
6
+ super(&block)
7
+ end
8
+
9
+ def push(&block)
10
+ @array.push(block)
11
+ self
12
+ end
13
+
14
+ def call(*args, &block)
15
+ super(@array, args, &block)
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Problem:
4
+ #
5
+ # > fn = proc { |first| puts first }
6
+ # > fn.call(:first, :second, :third)
7
+ # first
8
+ #
9
+ # :second and :third was ignored. It's ok.
10
+ #
11
+ # > fn = proc { puts "test" }
12
+ # > fn.call(first: :first, second: :second, third: :third)
13
+ # test
14
+ #
15
+ # And it's ok.
16
+ #
17
+ # > fn = proc { |&block| block.call }
18
+ # > fn.call(first: :first, second: :second, third: :third) { puts "test" }
19
+ # test
20
+ #
21
+ # And this is ok too.
22
+ #
23
+ # > fn = proc { |first:| puts first }
24
+ # > fn.call(first: :first, second: :second, third: :third)
25
+ # ArgumentError (unknown keywords: :second, :third)
26
+ #
27
+ # ¯\_(ツ)_/¯
28
+ #
29
+ # ❤ Ruby ❤
30
+ #
31
+ # Next code solve this problem for procs without word arguments,
32
+ # only keywords and block.
33
+
34
+ module TableSync::Utils
35
+ module_function
36
+
37
+ def proc_keywords_resolver(&proc_for_wrap)
38
+ available_keywords = proc_for_wrap.parameters
39
+ .select { |type, _name| type == :keyreq }
40
+ .map { |_type, name| name }
41
+
42
+ proc do |keywords = {}, &block|
43
+ proc_for_wrap.call(**keywords.slice(*available_keywords), &block)
44
+ end
45
+ end
46
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TableSync
4
- VERSION = "2.3.0"
4
+ VERSION = "4.0.0"
5
5
  end
@@ -29,10 +29,11 @@ Gem::Specification.new do |spec|
29
29
  spec.add_runtime_dependency "memery"
30
30
  spec.add_runtime_dependency "rabbit_messaging", "~> 0.3"
31
31
  spec.add_runtime_dependency "rails"
32
+ spec.add_runtime_dependency "self_data"
32
33
 
33
34
  spec.add_development_dependency "coveralls", "~> 0.8"
34
35
  spec.add_development_dependency "rspec", "~> 3.8"
35
- spec.add_development_dependency "rubocop-config-umbrellio", "~> 0.81"
36
+ spec.add_development_dependency "rubocop-config-umbrellio"
36
37
  spec.add_development_dependency "simplecov", "~> 0.16"
37
38
 
38
39
  spec.add_development_dependency "activejob", ">= 6.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: table_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Umbrellio
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-22 00:00:00.000000000 Z
11
+ date: 2020-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: memery
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: self_data
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: coveralls
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -84,16 +98,16 @@ dependencies:
84
98
  name: rubocop-config-umbrellio
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
- - - "~>"
101
+ - - ">="
88
102
  - !ruby/object:Gem::Version
89
- version: '0.81'
103
+ version: '0'
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
- - - "~>"
108
+ - - ">="
95
109
  - !ruby/object:Gem::Version
96
- version: '0.81'
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: simplecov
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -254,35 +268,33 @@ files:
254
268
  - Rakefile
255
269
  - bin/console
256
270
  - bin/setup
257
- - docs/development.md
258
- - docs/synopsis.md
271
+ - docs/message_protocol.md
272
+ - docs/notifications.md
273
+ - docs/publishing.md
274
+ - docs/receiving.md
259
275
  - lib/table_sync.rb
260
- - lib/table_sync/base_publisher.rb
261
- - lib/table_sync/batch_publisher.rb
262
- - lib/table_sync/config.rb
263
- - lib/table_sync/config/callback_registry.rb
264
- - lib/table_sync/config_decorator.rb
265
- - lib/table_sync/dsl.rb
266
276
  - lib/table_sync/errors.rb
267
- - lib/table_sync/event_actions.rb
268
- - lib/table_sync/event_actions/data_wrapper.rb
269
- - lib/table_sync/event_actions/data_wrapper/base.rb
270
- - lib/table_sync/event_actions/data_wrapper/destroy.rb
271
- - lib/table_sync/event_actions/data_wrapper/update.rb
272
277
  - lib/table_sync/instrument.rb
273
278
  - lib/table_sync/instrument_adapter/active_support.rb
274
- - lib/table_sync/model/active_record.rb
275
- - lib/table_sync/model/sequel.rb
276
279
  - lib/table_sync/naming_resolver/active_record.rb
277
280
  - lib/table_sync/naming_resolver/sequel.rb
278
- - lib/table_sync/orm_adapter/active_record.rb
279
- - lib/table_sync/orm_adapter/sequel.rb
280
- - lib/table_sync/plugins.rb
281
- - lib/table_sync/plugins/abstract.rb
282
- - lib/table_sync/plugins/access_mixin.rb
283
- - lib/table_sync/plugins/registry.rb
284
- - lib/table_sync/publisher.rb
285
- - lib/table_sync/receiving_handler.rb
281
+ - lib/table_sync/publishing.rb
282
+ - lib/table_sync/publishing/base_publisher.rb
283
+ - lib/table_sync/publishing/batch_publisher.rb
284
+ - lib/table_sync/publishing/orm_adapter/active_record.rb
285
+ - lib/table_sync/publishing/orm_adapter/sequel.rb
286
+ - lib/table_sync/publishing/publisher.rb
287
+ - lib/table_sync/receiving.rb
288
+ - lib/table_sync/receiving/config.rb
289
+ - lib/table_sync/receiving/config_decorator.rb
290
+ - lib/table_sync/receiving/dsl.rb
291
+ - lib/table_sync/receiving/handler.rb
292
+ - lib/table_sync/receiving/model/active_record.rb
293
+ - lib/table_sync/receiving/model/sequel.rb
294
+ - lib/table_sync/utils.rb
295
+ - lib/table_sync/utils/interface_checker.rb
296
+ - lib/table_sync/utils/proc_array.rb
297
+ - lib/table_sync/utils/proc_keywords_resolver.rb
286
298
  - lib/table_sync/version.rb
287
299
  - log/.keep
288
300
  - table_sync.gemspec
@@ -305,7 +317,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
305
317
  - !ruby/object:Gem::Version
306
318
  version: '0'
307
319
  requirements: []
308
- rubygems_version: 3.1.2
320
+ rubygems_version: 3.2.0.rc.1
309
321
  signing_key:
310
322
  specification_version: 4
311
323
  summary: DB Table synchronization between microservices based on Model's event system