dkubb/axiom-sql-generator

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

Summary

Maintainability
A
0 mins
Test Coverage
# encoding: utf-8

module Axiom
  module SQL
    module Generator

      # Abstract base class for SQL generation from a relation
      class Relation < Visitor
        extend Identifier
        include Attribute

        EMPTY_STRING = ''.freeze
        SEPARATOR    = ', '.freeze
        STAR         = '*'.freeze
        EMPTY_HASH   = {}.freeze

        # Return the alias name
        #
        # @return [#to_s]
        #
        # @api private
        attr_reader :name

        # Factory method to instantiate the generator for the relation
        #
        # @param [Axiom::Relation] relation
        #
        # @return [Generator::Relation]
        #
        # @api private
        def self.visit(relation)
          klass =
            case relation
            when Axiom::Relation::Operation::Insertion then self::Insertion
            when Axiom::Relation::Operation::Set       then self::Set
            when Axiom::Relation::Operation::Binary    then self::Binary
            when Axiom::Relation::Operation::Unary     then self::Unary
            when Axiom::Relation::Base                 then self::Base
            when Axiom::Relation::Materialized         then self::Materialized
            else
              fail InvalidRelationError, "#{relation.class} is not a visitable relation"
            end
          klass.new.visit(relation)
        end

        # Initialize a Generator
        #
        # @return [undefined]
        #
        # @api private
        def initialize
          @sql        = EMPTY_STRING
          @extensions = {}
        end

        # Visit an object and generate SQL from each node
        #
        # @example
        #   generator.visit(visitable)
        #
        # @param [Visitable] visitable
        #   A visitable object
        #
        # @return [self]
        #
        # @raise [Visitor::UnknownObject]
        #   raised when the visitable object has no handler
        #
        # @api public
        def visit(visitable)
          @sql = dispatch(visitable).to_s.freeze
          freeze
        end

        # Returns the current SQL string
        #
        # @example
        #   sql = generator.to_sql
        #
        # @return [String]
        #
        # @api public
        def to_sql
          @sql
        end

        # Return the SQL for the unary relation
        #
        # @example
        #   sql = unary_relation.to_s
        #
        # @return [#to_s]
        #
        # @api public
        def to_s
          return EMPTY_STRING unless visited?
          generate_sql(query_columns)
        end

        # Return the SQL suitable for an subquery
        #
        # @return [#to_s]
        #
        # @api private
        def to_subquery
          return EMPTY_STRING unless visited?
          Generator.parenthesize!(generate_sql(subquery_columns))
        end

        # Test if a visitable object has been visited
        #
        # @example
        #   visitor.visited?  # true or false
        #
        # @return [Boolean]
        #
        # @api public
        def visited?
          instance_variable_defined?(:@name)
        end

      private

        # 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
          implicit_columns
        end

        # Return the implicit columns for the select list
        #
        # @return [#to_s]
        #
        # @api private
        def implicit_columns
          sql = [STAR, column_list_for(@extensions)]
          sql.reject! { |fragment| fragment.empty? }
          sql.join(SEPARATOR)
        end

        # Return the explicit columns for the select list
        #
        # @return [#to_s]
        #
        # @api private
        def explicit_columns
          @distinct.to_s + column_list_for(@extensions.merge(@columns || EMPTY_HASH))
        end

        # Return the list of columns
        #
        # @param [#map] columns
        #
        # @return [#to_s]
        #
        # @api private
        def column_list_for(columns)
          sql = columns.values_at(*@header)
          sql.compact!
          sql.join(SEPARATOR)
        end

        # Return a list of columns in a header
        #
        # @param [Axiom::Relation] relation
        #
        # @param [#[]] aliases
        #   optional aliases for the columns
        #
        # @return [Hash]
        #
        # @api private
        def columns_for(relation, aliases = EMPTY_HASH)
          columns = {}
          relation.header.each do |attribute|
            columns[aliases.fetch(attribute, attribute)] = column_for(attribute, aliases)
          end
          columns
        end

        # Return the column for an attribute
        #
        # @param [Attribute] attribute
        #
        # @param [#[]] aliases
        #   aliases for the columns
        #
        # @return [#to_s]
        #
        # @api private
        def column_for(attribute, aliases)
          if aliases.key?(attribute)
            alias_for(attribute, aliases[attribute])
          else
            dispatch(attribute)
          end
        end

        # Return the column alias for an attribute
        #
        # @param [#to_s] attribute
        #
        # @param [Attribute, nil] alias_attribute
        #   attribute to use for the alias
        #
        # @return [#to_s]
        #
        # @api private
        def alias_for(attribute, alias_attribute)
          "#{dispatch(attribute)} AS #{dispatch(alias_attribute)}"
        end

        # Add extensions for extension and summarize queries
        #
        # @param [#each] extensions
        #
        # @return [undefined]
        #
        # @api private
        def add_extensions(extensions)
          extensions.each do |attribute, function|
            @extensions[attribute] = alias_for(function, attribute)
          end
        end

      end # class Relation
    end # module Generator
  end # module SQL
end # module Axiom