dkubb/axiom-sql-generator

View on GitHub
lib/axiom/sql/generator/relation/unary.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# 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