Skip to content

Commit 86918bd

Browse files
committed
initial code base
1 parent a238032 commit 86918bd

9 files changed

+595
-4
lines changed

lib/redis_counters/dumpers.rb

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
require "redis_counters/dumpers/version"
1+
require 'redis_counters/dumpers/version'
22

33
module RedisCounters
44
module Dumpers
5-
# Your code goes here...
5+
autoload :Engine, 'redis_counters/dumpers/engine'
6+
autoload :Destination, 'redis_counters/dumpers/destination'
7+
autoload :List, 'redis_counters/dumpers/list'
68
end
79
end
+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# coding: utf-8
2+
require 'forwardable'
3+
require 'active_support/core_ext/hash/indifferent_access'
4+
require_relative 'dsl/destination'
5+
6+
module RedisCounters
7+
module Dumpers
8+
# Класс представляет конечную точку сохранения данных счетчика.
9+
#
10+
# Описывает в какую модель (таблицу), какие поля имеющиеся в распоряжении дампера,
11+
# должны быть сохранены и каким образом.
12+
#
13+
# По сути, мерджит указанные поля из temp - таблицы, дампера
14+
# в указанную таблицу.
15+
#
16+
# Может использоваться как напрямую так и с помощью DSL (см. модуль RedisCounters::Dumpers::Dsl::Destination).
17+
class Destination
18+
extend Forwardable
19+
include ::RedisCounters::Dumpers::Dsl::Destination
20+
21+
# Ссылка на родительский движек - дампер.
22+
attr_accessor :engine
23+
24+
# Модель, в таблицу, которой будет производится мердж данных, AR::Model.
25+
attr_accessor :model
26+
27+
# Список полей, из доступных дамперу, которые необходимо сохранить, Array.
28+
attr_accessor :fields
29+
30+
# Список полей, по комбинации которых, будет происходить определение существования записи,
31+
# при мердже данных, Array.
32+
attr_accessor :key_fields
33+
34+
# Список полей, которые будет инкрементированы при обновлении существующей записи, Array.
35+
attr_accessor :increment_fields
36+
37+
# Карта полей - карта псевдонимов полей, Hash.
38+
# Названия полей в целевой таблице, могут отличаться от названий полей дампера.
39+
# Для сопоставления полей целевой таблицы и дампера, необходимо заполнить карту соответствия.
40+
# Карта, заполняется только для тех полей, названия которых отличаются.
41+
# Во всех свойствах, содержащий указания полей: fields, key_fields, increment_fields, conditions
42+
# используются имена конечных полей целевой таблицы.
43+
#
44+
# Example:
45+
# fields_map = {:pages => :value, :date => :start_month_date}
46+
#
47+
# Означает, что целевое поле :pages, указывает на поле :value, дампера,
48+
# а целевое поле :date, указывает на поле :start_month_date, дампера.
49+
attr_accessor :fields_map
50+
51+
# Список дополнительных условий, которые применяются при обновлении целевой таблицы, Array of String.
52+
# Каждое условие представляет собой строку - часть SQL выражения, которое может включать именованные
53+
# параметры из числа доступных в хеше оббщих параметров дампера: engine.common_params.
54+
# Условия соеденяются через AND.
55+
attr_accessor :conditions
56+
57+
def initialize(engine)
58+
@engine = engine
59+
@fields_map = HashWithIndifferentAccess.new
60+
@conditions = []
61+
end
62+
63+
def merge
64+
target_fields = fields.join(', ')
65+
66+
sql = <<-SQL
67+
WITH
68+
source AS
69+
(
70+
SELECT #{selected_fields_expression}
71+
FROM #{source_table}
72+
),
73+
updated AS
74+
(
75+
UPDATE #{target_table} target
76+
SET
77+
#{updating_expression}
78+
FROM source
79+
WHERE #{matching_expression}
80+
#{extra_conditions}
81+
RETURNING target.*
82+
)
83+
INSERT INTO #{target_table} (#{target_fields})
84+
SELECT #{target_fields}
85+
FROM source
86+
WHERE NOT EXISTS (
87+
SELECT 1
88+
FROM updated target
89+
WHERE #{matching_expression}
90+
#{extra_conditions}
91+
)
92+
SQL
93+
94+
sql = model.send(:sanitize_sql, [sql, engine.common_params])
95+
connection.execute sql
96+
end
97+
98+
def_delegator :model, :connection
99+
def_delegator :model, :quoted_table_name, :target_table
100+
def_delegator :engine, :temp_table_name, :source_table
101+
102+
protected
103+
104+
def selected_fields_expression
105+
full_fields_map.map { |target_field, source_field| "#{source_field} as #{target_field}" }.join(', ')
106+
end
107+
108+
def full_fields_map
109+
fields_map.reverse_merge(Hash[fields.zip(fields)])
110+
end
111+
112+
def updating_expression
113+
increment_fields.map { |field| "#{field} = COALESCE(target.#{field}, 0) + source.#{field}" }.join(', ')
114+
end
115+
116+
def matching_expression
117+
source_key_fields = key_fields.map { |field| "source.#{field}" }.join(', ')
118+
target_key_fields = key_fields.map { |field| "target.#{field}" }.join(', ')
119+
"(#{source_key_fields}) = (#{target_key_fields})"
120+
end
121+
122+
def extra_conditions
123+
result = conditions.map { |condition| "(#{condition})" }.join(' AND ')
124+
result.present? ? "AND #{result}" : result
125+
end
126+
end
127+
end
128+
end
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# coding: utf-8
2+
module RedisCounters
3+
module Dumpers
4+
module Dsl
5+
# Базовый класс для создания DSL к другим классам.
6+
# Класс обертка, который имеет все свойства, включая callbacks,
7+
# который просто настраивает целевой класс через его стандартные свойства.
8+
# Профит в простоте реализации DSL, в его изоляции от основного класса,
9+
# в разделении логики основного класса и DSL к нему.
10+
class Base
11+
attr_accessor :target
12+
13+
class << self
14+
def setter(*method_names)
15+
method_names.each do |name|
16+
send :define_method, name do |data|
17+
target.send "#{name}=".to_sym, data
18+
end
19+
end
20+
end
21+
22+
def varags_setter(*method_names)
23+
method_names.each do |name|
24+
send :define_method, name do |*data|
25+
target.send "#{name}=".to_sym, data.flatten
26+
end
27+
end
28+
end
29+
30+
def callback_setter(*method_names)
31+
method_names.each do |name|
32+
send :define_method, name do |method = nil, &block|
33+
target.send "#{name}=".to_sym, method, &block
34+
end
35+
end
36+
end
37+
end
38+
39+
def initialize(target, &block)
40+
@target = target
41+
instance_eval(&block)
42+
end
43+
end
44+
end
45+
end
46+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# coding: utf-8
2+
require 'active_support/concern'
3+
require_relative 'base'
4+
5+
module RedisCounters
6+
module Dumpers
7+
module Dsl
8+
# Модуль реализующий DSL для класса Destination
9+
module Destination
10+
extend ActiveSupport::Concern
11+
12+
class Configuration < ::RedisCounters::Dumpers::Dsl::Base
13+
alias_method :destination, :target
14+
15+
setter :model
16+
17+
varags_setter :fields
18+
varags_setter :key_fields
19+
varags_setter :increment_fields
20+
21+
alias_method :take, :fields
22+
23+
def map(field, target_field)
24+
destination.fields_map.merge!(field.to_sym => target_field[:to])
25+
end
26+
27+
def condition(value)
28+
destination.conditions << value
29+
end
30+
end
31+
32+
module ClassMethods
33+
def build(engine, &block)
34+
destination = new(engine)
35+
Configuration.new(destination, &block)
36+
destination
37+
end
38+
end
39+
end
40+
end
41+
end
42+
end
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# coding: utf-8
2+
require 'active_support/concern'
3+
require_relative 'base'
4+
5+
module RedisCounters
6+
module Dumpers
7+
module Dsl
8+
# Модуль реализующий DSL для класса Engine::Base
9+
module Engine
10+
extend ActiveSupport::Concern
11+
12+
class Configuration < ::RedisCounters::Dumpers::Dsl::Base
13+
alias_method :engine, :target
14+
15+
setter :name
16+
setter :fields
17+
setter :temp_table_name
18+
19+
callback_setter :on_before_merge
20+
callback_setter :on_prepare_row
21+
callback_setter :on_after_merge
22+
callback_setter :on_after_delete
23+
24+
def destination(&block)
25+
engine.destinations << ::RedisCounters::Dumpers::Destination.build(engine, &block)
26+
end
27+
end
28+
29+
module ClassMethods
30+
def build(&block)
31+
engine = new
32+
Configuration.new(engine, &block)
33+
engine
34+
end
35+
end
36+
end
37+
end
38+
end
39+
end
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# coding: utf-8
2+
require 'active_support/concern'
3+
require 'redis_counters/dumpers/engine'
4+
5+
module RedisCounters
6+
module Dumpers
7+
module Dsl
8+
module List
9+
extend ActiveSupport::Concern
10+
11+
module ClassMethods
12+
def build(&block)
13+
instance = new
14+
instance.instance_eval(&block)
15+
instance
16+
end
17+
end
18+
19+
def dumper(id, &block)
20+
engine = ::RedisCounters::Dumpers::Engine.build(&block)
21+
engine.name = id
22+
@dumpers[id] = engine
23+
end
24+
end
25+
end
26+
end
27+
end

0 commit comments

Comments
 (0)