table_sync 1.13.1 → 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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/.rubocop.yml +26 -1
  4. data/.travis.yml +6 -6
  5. data/CHANGELOG.md +74 -1
  6. data/Gemfile.lock +262 -0
  7. data/LICENSE.md +1 -1
  8. data/README.md +4 -1
  9. data/docs/message_protocol.md +24 -0
  10. data/docs/notifications.md +45 -0
  11. data/docs/publishing.md +147 -0
  12. data/docs/receiving.md +341 -0
  13. data/lib/table_sync.rb +23 -33
  14. data/lib/table_sync/errors.rb +60 -22
  15. data/lib/table_sync/instrument.rb +2 -2
  16. data/lib/table_sync/publishing.rb +11 -0
  17. data/lib/table_sync/{base_publisher.rb → publishing/base_publisher.rb} +1 -1
  18. data/lib/table_sync/{batch_publisher.rb → publishing/batch_publisher.rb} +12 -13
  19. data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/active_record.rb +4 -8
  20. data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/sequel.rb +10 -17
  21. data/lib/table_sync/{publisher.rb → publishing/publisher.rb} +4 -4
  22. data/lib/table_sync/receiving.rb +14 -0
  23. data/lib/table_sync/receiving/config.rb +218 -0
  24. data/lib/table_sync/receiving/config_decorator.rb +27 -0
  25. data/lib/table_sync/receiving/dsl.rb +28 -0
  26. data/lib/table_sync/receiving/handler.rb +131 -0
  27. data/lib/table_sync/{model → receiving/model}/active_record.rb +37 -23
  28. data/lib/table_sync/{model → receiving/model}/sequel.rb +13 -8
  29. data/lib/table_sync/utils.rb +9 -0
  30. data/lib/table_sync/utils/interface_checker.rb +97 -0
  31. data/lib/table_sync/utils/proc_array.rb +17 -0
  32. data/lib/table_sync/utils/proc_keywords_resolver.rb +46 -0
  33. data/lib/table_sync/version.rb +1 -1
  34. data/table_sync.gemspec +5 -4
  35. metadata +48 -30
  36. data/docs/synopsis.md +0 -318
  37. data/lib/table_sync/config.rb +0 -105
  38. data/lib/table_sync/config/callback_registry.rb +0 -53
  39. data/lib/table_sync/config_decorator.rb +0 -38
  40. data/lib/table_sync/dsl.rb +0 -25
  41. data/lib/table_sync/event_actions.rb +0 -96
  42. data/lib/table_sync/event_actions/data_wrapper.rb +0 -7
  43. data/lib/table_sync/event_actions/data_wrapper/base.rb +0 -23
  44. data/lib/table_sync/event_actions/data_wrapper/destroy.rb +0 -19
  45. data/lib/table_sync/event_actions/data_wrapper/update.rb +0 -21
  46. data/lib/table_sync/receiving_handler.rb +0 -76
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TableSync::Receiving
4
+ class ConfigDecorator
5
+ extend Forwardable
6
+
7
+ def_delegators :@config, :allow_event?
8
+ # rubocop:disable Metrics/ParameterLists
9
+ def initialize(config:, event:, model:, version:, project_id:, raw_data:)
10
+ @config = config
11
+
12
+ @default_params = {
13
+ event: event,
14
+ model: model,
15
+ version: version,
16
+ project_id: project_id,
17
+ raw_data: raw_data,
18
+ }
19
+ end
20
+ # rubocop:enable Metrics/ParameterLists
21
+
22
+ def method_missing(name, **additional_params, &block)
23
+ value = @config.send(name)
24
+ value.is_a?(Proc) ? value.call(@default_params.merge(additional_params), &block) : value
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TableSync::Receiving
4
+ module DSL
5
+ def inherited(klass)
6
+ klass.instance_variable_set(:@configs, configs.deep_dup)
7
+ super
8
+ end
9
+
10
+ def configs
11
+ @configs ||= Hash.new { |hash, key| hash[key] = [] }
12
+ end
13
+
14
+ def receive(source, to_table: nil, to_model: nil, events: [:update, :destroy], &block)
15
+ model = to_table ? TableSync.receiving_model.new(to_table) : to_model
16
+
17
+ TableSync::Utils::InterfaceChecker.new(model).implements(:receiving_model)
18
+
19
+ config = ::TableSync::Receiving::Config.new(model: model, events: events)
20
+
21
+ config.instance_exec(&block) if block
22
+
23
+ configs[source.to_s] << config
24
+
25
+ self
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TableSync::Receiving::Handler < Rabbit::EventHandler
4
+ extend TableSync::Receiving::DSL
5
+
6
+ # Rabbit::EventHandler uses Tainbox and performs `handler.new(message).call`
7
+ attribute :event
8
+ attribute :model
9
+ attribute :version
10
+
11
+ def call
12
+ configs.each do |config|
13
+ next unless config.allow_event?(event)
14
+
15
+ data = processed_data(config)
16
+
17
+ next if data.empty?
18
+
19
+ version_key = config.version_key(data: data)
20
+ data.each { |row| row[version_key] = version }
21
+
22
+ target_keys = config.target_keys(data: data)
23
+
24
+ validate_data(data, target_keys: target_keys)
25
+
26
+ params = { data: data, target_keys: target_keys, version_key: version_key }
27
+
28
+ if event == :update
29
+ params[:default_values] = config.default_values(data: data)
30
+ end
31
+
32
+ config.wrap_receiving(**params) do
33
+ perform(config, params)
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ # redefine setter from Rabbit::EventHandler
41
+ def data=(data)
42
+ super(Array.wrap(data[:attributes]))
43
+ end
44
+
45
+ def event=(name)
46
+ name = name.to_sym
47
+ raise TableSync::UndefinedEvent.new(event) unless %i[update destroy].include?(name)
48
+ super(name)
49
+ end
50
+
51
+ def model=(name)
52
+ super(name.to_s)
53
+ end
54
+
55
+ def configs
56
+ @configs ||= self.class.configs[model]&.map do |config|
57
+ ::TableSync::Receiving::ConfigDecorator.new(
58
+ config: config,
59
+ # next parameters will be send to each proc-options from config
60
+ event: event,
61
+ model: model,
62
+ version: version,
63
+ project_id: project_id,
64
+ raw_data: data,
65
+ )
66
+ end
67
+ end
68
+
69
+ def processed_data(config)
70
+ data.map do |row|
71
+ next if config.skip(row: row)
72
+
73
+ row = row.dup
74
+
75
+ config.mapping_overrides(row: row).each do |before, after|
76
+ row[after] = row.delete(before)
77
+ end
78
+
79
+ config.except(row: row).each(&row.method(:delete))
80
+
81
+ row.merge!(config.additional_data(row: row))
82
+
83
+ only = config.only(row: row)
84
+ row, rest = row.partition { |key, _| key.in?(only) }.map(&:to_h)
85
+
86
+ rest_key = config.rest_key(row: row, rest: rest)
87
+ (row[rest_key] ||= {}).merge!(rest) if rest_key
88
+
89
+ row
90
+ end.compact
91
+ end
92
+
93
+ def validate_data(data, target_keys:)
94
+ data.each do |row|
95
+ next if target_keys.all?(&row.keys.method(:include?))
96
+ raise TableSync::DataError.new(
97
+ data, target_keys, "Some target keys not found in received attributes!"
98
+ )
99
+ end
100
+
101
+ if data.uniq { |row| row.slice(*target_keys) }.size != data.size
102
+ raise TableSync::DataError.new(
103
+ data, target_keys, "Duplicate rows found!"
104
+ )
105
+ end
106
+ end
107
+
108
+ def perform(config, params)
109
+ model = config.model
110
+
111
+ model.transaction do
112
+ if event == :update
113
+ config.before_update(**params)
114
+
115
+ results = model.upsert(**params)
116
+
117
+ model.after_commit do
118
+ config.after_commit_on_update(**params.merge(results: results))
119
+ end
120
+ else
121
+ config.before_destroy(**params)
122
+
123
+ results = model.destroy(**params)
124
+
125
+ model.after_commit do
126
+ config.after_commit_on_destroy(**params.merge(results: results))
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -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(
@@ -115,7 +129,7 @@ module TableSync::Model
115
129
  end
116
130
 
117
131
  def row_to_hash(row)
118
- row.attributes.each_with_object({}) { |(k, v), o| o[k.to_sym] = v }
132
+ row.attributes.transform_keys(&:to_sym)
119
133
  end
120
134
  end
121
135
  end
@@ -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