Skip to content

Commit

Permalink
initial code base
Browse files Browse the repository at this point in the history
  • Loading branch information
bibendi committed Jan 29, 2015
1 parent a238032 commit 86918bd
Show file tree
Hide file tree
Showing 9 changed files with 595 additions and 4 deletions.
6 changes: 4 additions & 2 deletions lib/redis_counters/dumpers.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
require "redis_counters/dumpers/version"
require 'redis_counters/dumpers/version'

module RedisCounters
module Dumpers
# Your code goes here...
autoload :Engine, 'redis_counters/dumpers/engine'
autoload :Destination, 'redis_counters/dumpers/destination'
autoload :List, 'redis_counters/dumpers/list'
end
end
128 changes: 128 additions & 0 deletions lib/redis_counters/dumpers/destination.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# coding: utf-8
require 'forwardable'
require 'active_support/core_ext/hash/indifferent_access'
require_relative 'dsl/destination'

module RedisCounters
module Dumpers
# Класс представляет конечную точку сохранения данных счетчика.
#
# Описывает в какую модель (таблицу), какие поля имеющиеся в распоряжении дампера,
# должны быть сохранены и каким образом.
#
# По сути, мерджит указанные поля из temp - таблицы, дампера
# в указанную таблицу.
#
# Может использоваться как напрямую так и с помощью DSL (см. модуль RedisCounters::Dumpers::Dsl::Destination).
class Destination
extend Forwardable
include ::RedisCounters::Dumpers::Dsl::Destination

# Ссылка на родительский движек - дампер.
attr_accessor :engine

# Модель, в таблицу, которой будет производится мердж данных, AR::Model.
attr_accessor :model

# Список полей, из доступных дамперу, которые необходимо сохранить, Array.
attr_accessor :fields

# Список полей, по комбинации которых, будет происходить определение существования записи,
# при мердже данных, Array.
attr_accessor :key_fields

# Список полей, которые будет инкрементированы при обновлении существующей записи, Array.
attr_accessor :increment_fields

# Карта полей - карта псевдонимов полей, Hash.
# Названия полей в целевой таблице, могут отличаться от названий полей дампера.
# Для сопоставления полей целевой таблицы и дампера, необходимо заполнить карту соответствия.
# Карта, заполняется только для тех полей, названия которых отличаются.
# Во всех свойствах, содержащий указания полей: fields, key_fields, increment_fields, conditions
# используются имена конечных полей целевой таблицы.
#
# Example:
# fields_map = {:pages => :value, :date => :start_month_date}
#
# Означает, что целевое поле :pages, указывает на поле :value, дампера,
# а целевое поле :date, указывает на поле :start_month_date, дампера.
attr_accessor :fields_map

# Список дополнительных условий, которые применяются при обновлении целевой таблицы, Array of String.
# Каждое условие представляет собой строку - часть SQL выражения, которое может включать именованные
# параметры из числа доступных в хеше оббщих параметров дампера: engine.common_params.
# Условия соеденяются через AND.
attr_accessor :conditions

def initialize(engine)
@engine = engine
@fields_map = HashWithIndifferentAccess.new
@conditions = []
end

def merge
target_fields = fields.join(', ')

sql = <<-SQL
WITH
source AS
(
SELECT #{selected_fields_expression}
FROM #{source_table}
),
updated AS
(
UPDATE #{target_table} target
SET
#{updating_expression}
FROM source
WHERE #{matching_expression}
#{extra_conditions}
RETURNING target.*
)
INSERT INTO #{target_table} (#{target_fields})
SELECT #{target_fields}
FROM source
WHERE NOT EXISTS (
SELECT 1
FROM updated target
WHERE #{matching_expression}
#{extra_conditions}
)
SQL

sql = model.send(:sanitize_sql, [sql, engine.common_params])
connection.execute sql
end

def_delegator :model, :connection
def_delegator :model, :quoted_table_name, :target_table
def_delegator :engine, :temp_table_name, :source_table

protected

def selected_fields_expression
full_fields_map.map { |target_field, source_field| "#{source_field} as #{target_field}" }.join(', ')
end

def full_fields_map
fields_map.reverse_merge(Hash[fields.zip(fields)])
end

def updating_expression
increment_fields.map { |field| "#{field} = COALESCE(target.#{field}, 0) + source.#{field}" }.join(', ')
end

def matching_expression
source_key_fields = key_fields.map { |field| "source.#{field}" }.join(', ')
target_key_fields = key_fields.map { |field| "target.#{field}" }.join(', ')
"(#{source_key_fields}) = (#{target_key_fields})"
end

def extra_conditions
result = conditions.map { |condition| "(#{condition})" }.join(' AND ')
result.present? ? "AND #{result}" : result
end
end
end
end
46 changes: 46 additions & 0 deletions lib/redis_counters/dumpers/dsl/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# coding: utf-8
module RedisCounters
module Dumpers
module Dsl
# Базовый класс для создания DSL к другим классам.
# Класс обертка, который имеет все свойства, включая callbacks,
# который просто настраивает целевой класс через его стандартные свойства.
# Профит в простоте реализации DSL, в его изоляции от основного класса,
# в разделении логики основного класса и DSL к нему.
class Base
attr_accessor :target

class << self
def setter(*method_names)
method_names.each do |name|
send :define_method, name do |data|
target.send "#{name}=".to_sym, data
end
end
end

def varags_setter(*method_names)
method_names.each do |name|
send :define_method, name do |*data|
target.send "#{name}=".to_sym, data.flatten
end
end
end

def callback_setter(*method_names)
method_names.each do |name|
send :define_method, name do |method = nil, &block|
target.send "#{name}=".to_sym, method, &block
end
end
end
end

def initialize(target, &block)
@target = target
instance_eval(&block)
end
end
end
end
end
42 changes: 42 additions & 0 deletions lib/redis_counters/dumpers/dsl/destination.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# coding: utf-8
require 'active_support/concern'
require_relative 'base'

module RedisCounters
module Dumpers
module Dsl
# Модуль реализующий DSL для класса Destination
module Destination
extend ActiveSupport::Concern

class Configuration < ::RedisCounters::Dumpers::Dsl::Base
alias_method :destination, :target

setter :model

varags_setter :fields
varags_setter :key_fields
varags_setter :increment_fields

alias_method :take, :fields

def map(field, target_field)
destination.fields_map.merge!(field.to_sym => target_field[:to])
end

def condition(value)
destination.conditions << value
end
end

module ClassMethods
def build(engine, &block)
destination = new(engine)
Configuration.new(destination, &block)
destination
end
end
end
end
end
end
39 changes: 39 additions & 0 deletions lib/redis_counters/dumpers/dsl/engine.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# coding: utf-8
require 'active_support/concern'
require_relative 'base'

module RedisCounters
module Dumpers
module Dsl
# Модуль реализующий DSL для класса Engine::Base
module Engine
extend ActiveSupport::Concern

class Configuration < ::RedisCounters::Dumpers::Dsl::Base
alias_method :engine, :target

setter :name
setter :fields
setter :temp_table_name

callback_setter :on_before_merge
callback_setter :on_prepare_row
callback_setter :on_after_merge
callback_setter :on_after_delete

def destination(&block)
engine.destinations << ::RedisCounters::Dumpers::Destination.build(engine, &block)
end
end

module ClassMethods
def build(&block)
engine = new
Configuration.new(engine, &block)
engine
end
end
end
end
end
end
27 changes: 27 additions & 0 deletions lib/redis_counters/dumpers/dsl/list.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# coding: utf-8
require 'active_support/concern'
require 'redis_counters/dumpers/engine'

module RedisCounters
module Dumpers
module Dsl
module List
extend ActiveSupport::Concern

module ClassMethods
def build(&block)
instance = new
instance.instance_eval(&block)
instance
end
end

def dumper(id, &block)
engine = ::RedisCounters::Dumpers::Engine.build(&block)
engine.name = id
@dumpers[id] = engine
end
end
end
end
end
Loading

0 comments on commit 86918bd

Please sign in to comment.