lib/axiom/sql/generator/relation/unary.rb
# encoding: utf-8
module Axiom
module SQL
module Generator
class Relation
# Generates an SQL statement for a unary relation
class Unary < Relation
extend Aliasable
include Direction,
Literal,
Function::Aggregate,
Function::Connective,
Function::Predicate,
Function::Proposition,
Function::String,
Function::Numeric
inheritable_alias(visit_axiom_relation_operation_reverse: :visit_axiom_relation_operation_sorted)
DISTINCT = 'DISTINCT '.freeze
NO_ROWS = ' HAVING FALSE'.freeze
ANY_ROWS = ' HAVING COUNT (*) > 0'
COLLAPSIBLE = {
Algebra::Summarization => Set[].freeze,
Algebra::Projection => Set[Algebra::Projection, Algebra::Restriction].freeze,
Algebra::Extension => Set[Algebra::Projection, Algebra::Restriction, Axiom::Relation::Operation::Sorted, Axiom::Relation::Operation::Reverse, Axiom::Relation::Operation::Offset, Axiom::Relation::Operation::Limit].freeze,
Algebra::Rename => Set[Algebra::Projection, Algebra::Restriction, Axiom::Relation::Operation::Sorted, Axiom::Relation::Operation::Reverse, Axiom::Relation::Operation::Offset, Axiom::Relation::Operation::Limit].freeze,
Algebra::Restriction => Set[Algebra::Projection, Axiom::Relation::Operation::Sorted, Axiom::Relation::Operation::Reverse].freeze,
Axiom::Relation::Operation::Sorted => Set[Algebra::Projection, Algebra::Extension, Algebra::Rename, Algebra::Restriction, Algebra::Summarization, Axiom::Relation::Operation::Sorted, Axiom::Relation::Operation::Reverse].freeze,
Axiom::Relation::Operation::Reverse => Set[Algebra::Projection, Algebra::Extension, Algebra::Rename, Algebra::Restriction, Algebra::Summarization, Axiom::Relation::Operation::Sorted, Axiom::Relation::Operation::Reverse].freeze,
Axiom::Relation::Operation::Offset => Set[Algebra::Projection, Algebra::Extension, Algebra::Rename, Algebra::Restriction, Algebra::Summarization, Axiom::Relation::Operation::Sorted, Axiom::Relation::Operation::Reverse].freeze,
Axiom::Relation::Operation::Limit => Set[Algebra::Projection, Algebra::Extension, Algebra::Rename, Algebra::Restriction, Algebra::Summarization, Axiom::Relation::Operation::Sorted, Axiom::Relation::Operation::Reverse, Axiom::Relation::Operation::Offset].freeze,
}.freeze
# Initialize a Unary relation SQL generator
#
# @return [undefined]
#
# @api private
def initialize
super
@scope = ::Set.new
end
# Visit a Base Relation
#
# @param [Relation::Base] base_relation
#
# @return [self]
#
# @api private
def visit_axiom_relation_base(base_relation)
@name = base_relation.name.to_s.freeze
@from = visit_identifier(@name)
@header = base_relation.header
@columns = columns_for(base_relation)
self
end
# Visit a Projection
#
# @param [Algebra::Projection] projection
#
# @return [self]
#
# @api private
def visit_axiom_algebra_projection(projection)
@from = subquery_for(projection)
@distinct = DISTINCT
@header = projection.header
@columns = columns_for(projection)
scope_query(projection)
self
end
# Visit an Extension
#
# @param [Algebra::Extension] extension
#
# @return [self]
#
# @api private
def visit_axiom_algebra_extension(extension)
@from = subquery_for(extension)
@header = extension.header
@columns ||= columns_for(extension.operand)
add_extensions(extension.extensions)
scope_query(extension)
self
end
# Visit a Rename
#
# @param [Algebra::Rename] rename
#
# @return [self]
#
# @api private
def visit_axiom_algebra_rename(rename)
@from = subquery_for(rename)
@header = rename.header
@columns = columns_for(rename.operand, rename.aliases.to_hash)
scope_query(rename)
self
end
# Visit a Restriction
#
# @param [Algebra::Restriction] restriction
#
# @return [self]
#
# @api private
def visit_axiom_algebra_restriction(restriction)
@from = subquery_for(restriction)
@where = " WHERE #{dispatch(restriction.predicate)}"
@header = restriction.header
@columns ||= columns_for(restriction)
scope_query(restriction)
self
end
# Visit a Summarization
#
# @param [Algebra::Summarization] summarization
#
# @return [self]
#
# @api private
def visit_axiom_algebra_summarization(summarization)
summarize_per = summarization.summarize_per
@from = subquery_for(summarization)
@header = summarization.header
@columns = columns_for(summarize_per)
summarize_per(summarize_per)
group_by_columns
add_extensions(summarization.summarizers)
scope_query(summarization)
self
end
# Visit a Sorted
#
# @param [Relation::Operation::Sorted] sorted
#
# @return [self]
#
# @api private
def visit_axiom_relation_operation_sorted(sorted)
@from = subquery_for(sorted)
@order = " ORDER BY #{order_for(sorted.directions)}"
@header = sorted.header
@columns ||= columns_for(sorted)
scope_query(sorted)
self
end
# Visit a Limit
#
# @param [Relation::Operation::Limit] limit
#
# @return [self]
#
# @api private
def visit_axiom_relation_operation_limit(limit)
@from = subquery_for(limit)
@limit = " LIMIT #{limit.limit}"
@header = limit.header
@columns ||= columns_for(limit)
scope_query(limit)
self
end
# Visit an Offset
#
# @param [Relation::Operation::Offset] offset
#
# @return [self]
#
# @api private
def visit_axiom_relation_operation_offset(offset)
@from = subquery_for(offset)
@offset = " OFFSET #{offset.offset}"
@header = offset.header
@columns ||= columns_for(offset)
scope_query(offset)
self
end
private
# Generate the SQL using the supplied columns
#
# @param [String] columns
#
# @return [#to_s]
#
# @api private
def generate_sql(columns)
["SELECT #{columns} FROM #{@from}", @where, @group, @having, @order, @limit, @offset].join
end
# Return the columns to use in a query
#
# @return [#to_s]
#
# @api private
def query_columns
explicit_columns
end
# Return the columns to use in a subquery
#
# @return [#to_s]
#
# @api private
def subquery_columns
explicit_columns_in_subquery? ? explicit_columns : super
end
# Test if the subquery should use "*" and not specify columns explicitly
#
# @return [Boolean]
#
# @api private
def explicit_columns_in_subquery?
@scope.include?(Algebra::Projection) ||
@scope.include?(Algebra::Rename) ||
@scope.include?(Algebra::Summarization)
end
# Return a list of columns for ordering
#
# @param [DirectionSet] directions
#
# @return [#to_s]
#
# @api private
def order_for(directions)
directions.map { |direction| dispatch(direction) }.join(SEPARATOR)
end
# Summarize the operand over the provided relation
#
# @param [Relation] relation
#
# @return [undefined]
#
# @api private
def summarize_per(relation)
return if relation.eql?(TABLE_DEE)
if relation.eql?(TABLE_DUM) then summarize_per_table_dum
elsif (generator = Binary.visit(relation)).name.eql?(name) then summarize_per_subset
else
summarize_per_relation(generator)
end
end
# Summarize the operand using table dee
#
# @return [undefined]
#
# @api private
def summarize_per_table_dum
@having = NO_ROWS
end
# Summarize the operand using a subset
#
# @return [undefined]
#
# @api private
def summarize_per_subset
@having = ANY_ROWS
end
# Summarize the operand using another relation
#
# @return [undefined]
#
# @api private
def summarize_per_relation(generator)
@from = "#{generator.to_subquery} AS #{visit_identifier(generator.name)} NATURAL LEFT JOIN #{@from}"
end
# Group by the columns
#
# @return [undefined]
#
# @api private
def group_by_columns
@group = " GROUP BY #{column_list_for(@columns)}" if @columns.any?
end
# Return an expression that can be used for the FROM
#
# @param [Relation] relation
#
# @return [#to_s]
#
# @api private
def subquery_for(relation)
operand = relation.operand
subquery = dispatch(operand)
if collapse_subquery_for?(relation)
@from
else
aliased_subquery(subquery)
end
end
# Add the operand to the current scope
#
# @param [Relation] operand
#
# @return [undefined]
#
# @api private
def scope_query(operand)
@scope << operand.class
end
# Test if the relation should be collapsed
#
# @param [Relation] relation
#
# @return [#to_s]
#
# @api private
def collapse_subquery_for?(relation)
@scope.subset?(COLLAPSIBLE.fetch(relation.class))
end
# Returns an aliased subquery
#
# @param [#to_s] subquery
#
# @return [#to_s]
#
# @api private
def aliased_subquery(subquery)
"#{subquery.to_subquery} AS #{visit_identifier(subquery.name)}"
ensure
reset_query_state
end
# Visit a Binary Relation
#
# @param [Relation::Operation::Binary] binary
#
# @return [Relation::Binary]
#
# @api private
def visit_axiom_relation_operation_binary(binary)
generator = self.class.visit(binary)
@name = generator.name
@from = aliased_subquery(generator)
generator
end
# Reset the query state
#
# @return [undefined]
#
# @api private
def reset_query_state
@scope.clear
@extensions.clear
@distinct = @columns = @where = @order = @limit = @offset = @group = @having = nil
end
end # class Unary
end # class Relation
end # module Generator
end # module SQL
end # module Axiom