lib/axiom/relation/gateway.rb
# encoding: utf-8
module Axiom
class Relation
# A relation backed by an adapter
class Gateway < Relation
DECORATED_CLASS = superclass
# remove methods so they can be proxied
undef_method(*DECORATED_CLASS.public_instance_methods(false).map(&:to_s) - %w[materialize])
undef_method(:project, :remove, :extend, :rename, :restrict, :sort_by, :reverse, :drop, :take)
# The adapter the gateway will use to fetch results
#
# @return [Adapter::DataObjects]
#
# @api private
attr_reader :adapter
protected :adapter
# The relation the gateway will use to generate SQL
#
# @return [Relation]
#
# @api private
attr_reader :relation
protected :relation
# Initialize a Gateway
#
# @param [Adapter::DataObjects] adapter
#
# @param [Relation] relation
#
# @return [undefined]
#
# @api private
def initialize(adapter, relation)
@adapter = adapter
@relation = relation
end
# Iterate over each row in the results
#
# @example
# gateway = Gateway.new(adapter, relation)
# gateway.each { |tuple| ... }
#
# @yield [tuple]
#
# @yieldparam [Tuple] tuple
# each tuple in the results
#
# @return [self]
#
# @api public
def each
return to_enum unless block_given?
tuples.each { |tuple| yield tuple }
self
end
# Return a relation that is the join of two relations
#
# @example natural join
# join = relation.join(other)
#
# @example theta-join using a block
# join = relation.join(other) { |r| r.a.gte(r.b) }
#
# @param [Relation] other
# the other relation to join
#
# @yield [relation]
# optional block to restrict the tuples with
#
# @yieldparam [Relation] relation
# the context to evaluate the restriction with
#
# @yieldreturn [Function, #call]
# predicate to restrict the tuples with
#
# @return [Gateway]
# return a gateway if the adapters are equal
# @return [Algebra::Join]
# return a normal join when the adapters are not equal
# @return [Algebra::Restriction]
# return a normal restriction when the adapters are not equal
# for a theta-join
#
# @api public
def join(other)
if block_given?
super
else
binary_operation(__method__, other, Algebra::Join)
end
end
# Return a relation that is the cartesian product of two relations
#
# @example
# product = gateway.product(other)
#
# @param [Relation] other
# the other relation to find the product with
#
# @return [Gateway]
# return a gateway if the adapters are equal
# @return [Algebra::Product]
# return a normal product when the adapters are not equal
#
# @api public
def product(other)
binary_operation(__method__, other, Algebra::Product)
end
# Return the union between relations
#
# @example
# union = gateway.union(other)
#
# @param [Relation] other
# the other relation to find the union with
#
# @return [Gateway]
# return a gateway if the adapters are equal
# @return [Algebra::Union]
# return a normal union when the adapters are not equal
#
# @api public
def union(other)
binary_operation(__method__, other, Algebra::Union)
end
# Return the intersection between relations
#
# @example
# intersect = gateway.intersect(other)
#
# @param [Relation] other
# the other relation to find the intersect with
#
# @return [Gateway]
# return a gateway if the adapters are equal
# @return [Algebra::Intersection]
# return a normal intersection when the adapters are not equal
#
# @api public
def intersect(other)
binary_operation(__method__, other, Algebra::Intersection)
end
# Return the diferrence between relations
#
# @example
# difference = gateway.difference(other)
#
# @param [Relation] other
# the other relation to find the difference with
#
# @return [Gateway]
# return a gateway if the adapters are equal
# @return [Algebra::Difference]
# return a normal dfference when the adapters are not equal
#
# @api public
def difference(other)
binary_operation(__method__, other, Algebra::Difference)
end
# Return a summarized relation
#
# @example with no arguments
# summarization = gateway.summarize do |context|
# context.add(:count, context[:id].count)
# end
#
# @example with a relation
# summarization = gateway.summarize(relation) do |context|
# context.add(:count, context[:id].count)
# end
#
# @example with a header
# summarization = gateway.summarize([:name]) do |context|
# context.add(:count, context[:id].count)
# end
#
# @example with another gateway
# summarization = gateway.summarize(other_gateway) do |context|
# context.add(:count, context[:id].count)
# end
#
# @param [Gateway, Relation, Header, #to_ary] summarize_with
#
# @yield [function]
# Evaluate a summarization function
#
# @yieldparam [Evaluator::Context] context
# the context to evaluate the function within
#
# @return [Gateway]
# return a gateway if the adapters are equal, or there is no adapter
# @return [Algebra::Summarization]
# return a normal summarization when the adapters are not equal
#
# @api public
def summarize(summarize_with = TABLE_DEE, &block)
if summarize_merge?(summarize_with)
summarize_merge(summarize_with, &block)
else
summarize_split(summarize_with, &block)
end
end
# Test if the method is supported on this object
#
# @param [Symbol] method
#
# @return [Boolean]
#
# @api private
def respond_to?(method, *)
super || forwardable?(method)
end
private
# Proxy the message to the relation
#
# @param [Symbol] method
#
# @param [Array] args
#
# @return [self]
# return self for all command methods
# @return [Object]
# return response from all query methods
#
# @api private
def method_missing(method, *args, &block)
forwardable?(method) ? forward(method, *args, &block) : super
end
# Test if the method can be forwarded to the relation
#
# @param [Symbol] method
#
# @return [Boolean]
#
# @api private
def forwardable?(method)
relation.respond_to?(method)
end
# Forward the message to the relation
#
# @param [Array] args
#
# @return [self]
# return self for all command methods
# @return [Object]
# return response from all query methods
#
# @api private
def forward(*args, &block)
relation = self.relation
response = relation.public_send(*args, &block)
if response.equal?(relation)
self
elsif response.kind_of?(DECORATED_CLASS)
self.class.new(adapter, response)
else
response
end
end
# Return a list of tuples to iterate over
#
# @return [#each]
#
# @api private
def tuples
relation = self.relation
if materialized?
relation
else
DECORATED_CLASS.new(header, adapter.read(relation))
end
end
# Return a binary relation
#
# @param [Relation] other
#
# @return [Gateway]
# return a gateway if the adapters are equal
# @return [Relation]
# return a binary relation when the adapters are not equal
#
# @api private
def binary_operation(method, other, factory)
if same_adapter?(other)
forward(method, other.relation)
else
factory.new(self, other)
end
end
# Test if the other object is a Gateway
#
# @param [Gateway, Relation] other
#
# @return [Boolean]
#
# @api private
def gateway?(other)
other.kind_of?(Gateway)
end
# Test if the other object uses the same adapter
#
# @param [Gateway, Relation] other
#
# @return [Boolean]
#
# @api private
def same_adapter?(other)
gateway?(other) && adapter.eql?(other.adapter)
end
# Test if the summarize_with object can be merged into the summarization
#
# @param [Gateway, Relation, Header] summarize_with
#
# @return [Boolean]
#
# @api private
def summarize_merge?(summarize_with)
!summarize_with.respond_to?(:header) ||
summarize_with.equal?(TABLE_DEE) ||
same_adapter?(summarize_with)
end
# Merge the summarize_with into the summarization
#
# @param [Gateway, Relation, Header] summarize_with
#
# @return [Gateway]
#
# @api private
def summarize_merge(summarize_with, &block)
summarize_with = summarize_with.relation if gateway?(summarize_with)
forward(:summarize, summarize_with, &block)
end
# Split the summarize_with into a separate relation, wrapped in a summarization
#
# @param [Gateway, Relation, Header] summarize_with
#
# @return [Algebra::Summarization]
#
# @api private
def summarize_split(summarize_with, &block)
# evaluate the gateway, then summarize with the provided relation
context = Evaluator::Context.new(header - summarize_with.header, &block)
Algebra::Summarization.new(self, summarize_with, context.functions)
end
end # class Gateway
end # class Relation
end # module Axiom