Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file.

## [6.10.0] - 2025-11-21
### Fixed
- Add on_first_sync callback

## [6.9.3] - 2025-10-09
### Fixed
- Add validate_types to model interface
Expand Down
29 changes: 16 additions & 13 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
table_sync (6.9.3)
table_sync (6.10.0)
memery
rabbit_messaging (>= 1.7.0)
rails
Expand Down Expand Up @@ -90,12 +90,12 @@ GEM
bunny (2.24.0)
amq-protocol (~> 2.3)
sorted_set (~> 1, >= 1.0.2)
cgi (0.4.2)
cgi (0.5.0)
coderay (1.1.3)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
crass (1.0.6)
date (3.4.1)
date (3.5.0)
diff-lcs (1.6.2)
docile (1.4.1)
drb (2.2.3)
Expand All @@ -106,8 +106,8 @@ GEM
activesupport (>= 6.1)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
io-console (0.8.0)
irb (1.15.2)
io-console (0.8.1)
irb (1.15.3)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
Expand All @@ -124,18 +124,19 @@ GEM
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
mail (2.9.0)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.4)
marcel (1.1.0)
memery (1.7.0)
method_source (1.1.0)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.25.5)
net-imap (0.5.8)
net-imap (0.5.12)
date
net-protocol
net-pop (0.1.2)
Expand Down Expand Up @@ -170,7 +171,7 @@ GEM
ast (~> 2.4.1)
racc
pg (1.5.9)
pp (0.6.2)
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.4.0)
Expand Down Expand Up @@ -224,11 +225,12 @@ GEM
rainbow (3.1.1)
rake (13.2.1)
rbtree (0.4.6)
rdoc (6.14.0)
rdoc (6.15.1)
erb
psych (>= 4.0.0)
tsort
regexp_parser (2.10.0)
reline (0.6.1)
reline (0.6.3)
io-console (~> 0.5)
rspec (3.13.1)
rspec-core (~> 3.13.0)
Expand Down Expand Up @@ -308,10 +310,11 @@ GEM
sorted_set (1.0.3)
rbtree
set (~> 1.0)
stringio (3.1.7)
thor (1.3.2)
stringio (3.1.8)
thor (1.4.0)
timecop (0.9.10)
timeout (0.4.3)
tsort (0.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.4)
Expand Down
1 change: 1 addition & 0 deletions lib/table_sync/receiving.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Receiving
require_relative "receiving/config_decorator"
require_relative "receiving/dsl"
require_relative "receiving/handler"
require_relative "receiving/hooks/once"
require_relative "receiving/model/active_record"
require_relative "receiving/model/sequel"
end
Expand Down
31 changes: 27 additions & 4 deletions lib/table_sync/receiving/config.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require_relative "hooks/once"

module TableSync::Receiving
class Config
attr_reader :model, :events
Expand All @@ -23,13 +25,10 @@ def invalid_events
class << self
attr_reader :default_values_for_options

# In a configs this options are requested as they are
# In a configs these options are requested as they are
# config.option - get value
# config.option(args) - set static value
# config.option { ... } - set proc as value
#
# In `Receiving::Handler` or `Receiving::EventActions` this options are requested
# through `Receiving::ConfigDecorator#method_missing` which always executes `config.option`

def add_option(name, value_setter_wrapper:, value_as_proc_setter_wrapper:, default:)
ivar = :"@#{name}"
Expand All @@ -55,11 +54,30 @@ def add_option(name, value_setter_wrapper:, value_as_proc_setter_wrapper:, defau
instance_variable_set(ivar, result_value)
end
end

def add_hook_option(name, hook_class:)
ivar = :"@#{name}"

@default_values_for_options ||= {}
@default_values_for_options[ivar] = proc { [] }

define_method(name) do |conditions, &handler|
hooks = instance_variable_get(ivar)
hooks ||= []

hooks << hook_class.new(conditions:, handler:)
instance_variable_set(ivar, hooks)
end
end
end

def allow_event?(name)
events.include?(name)
end

def option(name)
instance_variable_get(:"@#{name}")
end
end
end

Expand Down Expand Up @@ -201,6 +219,11 @@ def allow_event?(name)
value_as_proc_setter_wrapper: any_value,
default: proc { proc { |&block| block.call } }

TableSync::Receiving::Config.add_hook_option(
:on_first_sync,
hook_class: TableSync::Receiving::Hooks::Once,
)

%i[
before_update
after_commit_on_update
Expand Down
15 changes: 10 additions & 5 deletions lib/table_sync/receiving/config_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

module TableSync::Receiving
class ConfigDecorator
extend Forwardable

def_delegators :@config, :allow_event?
# rubocop:disable Metrics/ParameterLists
def initialize(config:, event:, model:, version:, project_id:, raw_data:)
@config = config
Expand All @@ -19,9 +16,17 @@ def initialize(config:, event:, model:, version:, project_id:, raw_data:)
end
# rubocop:enable Metrics/ParameterLists

def method_missing(name, **additional_params, &)
value = @config.send(name)
def option(name, **additional_params, &)
value = @config.option(name)
value.is_a?(Proc) ? value.call(@default_params.merge(additional_params), &) : value
end

def model
@config.model
end

def allow_event?(name)
@config.allow_event?(name)
end
end
end
41 changes: 24 additions & 17 deletions lib/table_sync/receiving/handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,20 @@ def call

next if data.empty?

version_key = config.version_key(data:)
data.each { |row| row[version_key] = version }

target_keys = config.target_keys(data:)
target_keys = config.option(:target_keys, data:)

validate_data(data, target_keys:)

data.sort_by! { |row| row.values_at(*target_keys).map { |value| sort_key(value) } }

version_key = config.option(:version_key, data:)
params = { data:, target_keys:, version_key: }

if event == :update
params[:default_values] = config.default_values(data:)
params[:default_values] = config.option(:default_values, data:)
end

config.wrap_receiving(event:, **params) do
config.option(:wrap_receiving, **params) do
perform(config, params)
end
end
Expand Down Expand Up @@ -83,25 +81,28 @@ def configs
end

def processed_data(config)
version_key = config.option(:version_key, data:)
data.filter_map do |row|
next if config.skip(row:)
next if config.option(:skip, row:)

row = row.dup

config.mapping_overrides(row:).each do |before, after|
config.option(:mapping_overrides, row:).each do |before, after|
row[after] = row.delete(before)
end

config.except(row:).each { |x| row.delete(x) }
config.option(:except, row:).each { |x| row.delete(x) }

row.merge!(config.additional_data(row:))
row.merge!(config.option(:additional_data, row:))

only = config.only(row:)
only = config.option(:only, row:)
row, rest = row.partition { |key, _| key.in?(only) }.map(&:to_h)

rest_key = config.rest_key(row:, rest:)
rest_key = config.option(:rest_key, row:, rest:)
(row[rest_key] ||= {}).merge!(rest) if rest_key

row[version_key] = version

row
end
end
Expand Down Expand Up @@ -138,16 +139,16 @@ def validate_data_types(model, data)
raise TableSync::DataError.new(data, errors.keys, errors.to_json)
end

def perform(config, params)
def perform(config, params) # rubocop:disable Metrics/MethodLength
model = config.model

model.transaction do
results = if event == :update
config.before_update(**params)
config.option(:before_update, **params)
validate_data_types(model, params[:data])
model.upsert(**params)
else
config.before_destroy(**params)
config.option(:before_destroy, **params)
model.destroy(**params)
end

Expand All @@ -157,9 +158,15 @@ def perform(config, params)
end

if event == :update
model.after_commit { config.after_commit_on_update(**params, results:) }
model.after_commit do
config.option(:after_commit_on_update, **params, results:)

Array(config.option(:on_first_sync)).each do |hook|
hook.perform(config:, targets: results) if hook.enabled?
end
end
else
model.after_commit { config.after_commit_on_destroy(**params, results:) }
model.after_commit { config.option(:after_commit_on_destroy, **params, results:) }
end
end
end
Expand Down
62 changes: 62 additions & 0 deletions lib/table_sync/receiving/hooks/once.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

module TableSync::Receiving::Hooks
class Once
LOCK_KEY = "hook-once-lock-key"

attr_reader :conditions, :handler, :lookup_code

def initialize(conditions:, handler:)
@conditions = conditions
@handler = handler
init_lookup_code
end

def enabled?
conditions[:columns].any?
end

def perform(config:, targets:)
target_keys = config.option(:target_keys)
model = config.model

targets.each do |target|
next unless conditions?(target)

keys = target.slice(*target_keys)
model.try_advisory_lock(prepare_lock_key(keys)) do
model.find_and_save(keys:) do |entry|
next unless allow?(entry)

entry.hooks ||= []
entry.hooks << lookup_code
model.after_commit { handler.call(entry:) }
end
end
end
end

private

def allow?(entry)
Array(entry.hooks).exclude?(lookup_code)
end

def init_lookup_code
@lookup_code = conditions[:columns].map do |column|
"#{column}-#{conditions[column]}"
end.join(":")
end

def conditions?(row)
conditions[:columns].all? do |column|
row[column] == (conditions[column] || row[column])
end
end

def prepare_lock_key(row_keys)
lock_keys = [LOCK_KEY] + row_keys.values
Zlib.crc32(lock_keys.join(":")) % (2**31)
end
end
end
Loading