table_sync 1.13.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
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