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.
- checksums.yaml +4 -4
- data/.rubocop.yml +21 -0
- data/CHANGELOG.md +40 -0
- data/Gemfile.lock +82 -77
- data/README.md +4 -2
- data/docs/message_protocol.md +24 -0
- data/docs/notifications.md +45 -0
- data/docs/publishing.md +147 -0
- data/docs/receiving.md +341 -0
- data/lib/table_sync.rb +16 -31
- data/lib/table_sync/errors.rb +39 -23
- data/lib/table_sync/publishing.rb +11 -0
- data/lib/table_sync/{base_publisher.rb → publishing/base_publisher.rb} +1 -1
- data/lib/table_sync/{batch_publisher.rb → publishing/batch_publisher.rb} +4 -4
- data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/active_record.rb +3 -7
- data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/sequel.rb +2 -6
- data/lib/table_sync/{publisher.rb → publishing/publisher.rb} +4 -4
- data/lib/table_sync/receiving.rb +14 -0
- data/lib/table_sync/receiving/config.rb +218 -0
- data/lib/table_sync/receiving/config_decorator.rb +27 -0
- data/lib/table_sync/receiving/dsl.rb +28 -0
- data/lib/table_sync/receiving/handler.rb +131 -0
- data/lib/table_sync/{model → receiving/model}/active_record.rb +36 -22
- data/lib/table_sync/{model → receiving/model}/sequel.rb +13 -8
- data/lib/table_sync/utils.rb +9 -0
- data/lib/table_sync/utils/interface_checker.rb +97 -0
- data/lib/table_sync/utils/proc_array.rb +17 -0
- data/lib/table_sync/utils/proc_keywords_resolver.rb +46 -0
- data/lib/table_sync/version.rb +1 -1
- data/table_sync.gemspec +2 -1
- metadata +42 -30
- data/docs/development.md +0 -43
- data/docs/synopsis.md +0 -336
- data/lib/table_sync/config.rb +0 -105
- data/lib/table_sync/config/callback_registry.rb +0 -53
- data/lib/table_sync/config_decorator.rb +0 -38
- data/lib/table_sync/dsl.rb +0 -25
- data/lib/table_sync/event_actions.rb +0 -96
- data/lib/table_sync/event_actions/data_wrapper.rb +0 -7
- data/lib/table_sync/event_actions/data_wrapper/base.rb +0 -23
- data/lib/table_sync/event_actions/data_wrapper/destroy.rb +0 -19
- data/lib/table_sync/event_actions/data_wrapper/update.rb +0 -21
- data/lib/table_sync/plugins.rb +0 -72
- data/lib/table_sync/plugins/abstract.rb +0 -55
- data/lib/table_sync/plugins/access_mixin.rb +0 -49
- data/lib/table_sync/plugins/registry.rb +0 -153
- 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:,
|
56
|
-
|
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
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
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:,
|
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
|
-
|
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,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
|
data/lib/table_sync/version.rb
CHANGED
data/table_sync.gemspec
CHANGED
@@ -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"
|
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:
|
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-
|
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
|
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
|
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/
|
258
|
-
- docs/
|
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/
|
279
|
-
- lib/table_sync/
|
280
|
-
- lib/table_sync/
|
281
|
-
- lib/table_sync/
|
282
|
-
- lib/table_sync/
|
283
|
-
- lib/table_sync/
|
284
|
-
- lib/table_sync/
|
285
|
-
- lib/table_sync/
|
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
|
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
|