lib/axiom/algebra/projection.rb
# encoding: utf-8
module Axiom
module Algebra
# Specify only specific attributes to keep in the relation
class Projection < Relation
include Relation::Operation::Unary
include Equalizer.new(:operand, :header)
# Initialize a Projection
#
# @param [Relation] operand
# the relation to project
# @param [#to_ary] attributes
# the attributes to keep in the header
#
# @return [undefined]
#
# @api private
def initialize(operand, attributes)
super(operand)
@header = @header.project(attributes)
end
# Iterate over each tuple in the set
#
# @example
# projection = Projection.new(operand, attributes)
# projection.each { |tuple| ... }
#
# @yield [tuple]
#
# @yieldparam [Tuple] tuple
# each tuple in the set
#
# @return [self]
#
# @api public
def each
return to_enum unless block_given?
seen = {}
operand.each do |tuple|
tuple = tuple.project(header)
yield seen[tuple] = tuple unless seen.key?(tuple)
end
self
end
# Insert a relation into the Projection
#
# @example
# new_relation = projection.insert(other)
#
# @param [Relation] other
#
# @return [Projection]
#
# @raise [RequiredAttributesError]
# raised when inserting into a relation with required attributes removed
#
# @raise [InvalidHeaderError]
# raised if the headers are not equivalent
#
# @api public
def insert(other)
other = coerce(other)
assert_removed_attributes_optional
assert_equivalent_headers(other)
operand.insert(extend_other(other)).project(header)
end
# Delete a relation from the Projection
#
# @example
# new_relation = projection.delete(other)
#
# @param [Relation] other
#
# @return [Projection]
#
# @raise [InvalidHeaderError]
# raised if the headers are not equivalent
#
# @api public
def delete(other)
other = coerce(other)
assert_equivalent_headers(other)
operand.delete(extend_other(other)).project(header)
end
private
# Assert that removed attributes are optional
#
# @return [undefined]
#
# @raise [RequiredAttributesError]
# raised when inserting into a relation with required attributes removed
#
# @api private
def assert_removed_attributes_optional
names = required_attribute_names
return if names.empty?
fail RequiredAttributesError, "required attributes #{names} have been removed"
end
# Assert that other relation header is equivalent
#
# @param [Relation] other
#
# @return [undefined]
#
# @raise [InvalidHeaderError]
# raised if the headers are not equivalent
#
# @api private
def assert_equivalent_headers(other)
return if header == other.header
fail InvalidHeaderError, 'the headers must be equivalent'
end
# Names of the required attributes that were removed
#
# @return [Array<Symbol>]
#
# @api private
def required_attribute_names
removed_attributes.each_with_object([]) do |attribute, names|
names << attribute.name if attribute.required?
end
end
# Attributes that were removed by the projection
#
# @return [Array<Attribute>]
#
# @api private
def removed_attributes
operand.header - header
end
# Extend the other relation with removed attributes
#
# @param [Relation] other
#
# @return [Extension]
#
# @api private
def extend_other(other)
other.extend(Hash[removed_attributes.zip])
end
module Methods
# Return a relation with only the attributes specified
#
# @example
# projection = relation.project([:a, :b, :c])
#
# @param [#to_ary] attributes
# the attributes to keep in the header
#
# @return [Projection]
#
# @api public
def project(attributes)
Projection.new(self, attributes)
end
# Return a relation with attributes not specified
#
# @example
# projection = relation.remove([:a, b, c])
#
# @param [#to_ary] attributes
# the attributes to remove from the header
#
# @return [Projection]
#
# @api public
def remove(attributes)
project(header - attributes)
end
end # module Methods
Relation.class_eval { include Methods }
memoize :removed_attributes
end # class Projection
end # module Algebra
end # module Axiom