take-five/activerecord-hierarchical_query

View on GitHub
lib/active_record/hierarchical_query/orderings.rb

Summary

Maintainability
A
0 mins
Test Coverage
module ActiveRecord
  module HierarchicalQuery
    class Orderings
      NATURAL_SORT_TYPES = Set[
          :integer, :float, :decimal,
          :datetime, :timestamp, :time, :date,
          :boolean, :itet, :cidr, :ltree
      ]

      include Enumerable

      attr_reader :order_values, :table

      # @param [Array] order_values
      # @param [Arel::Table] table
      def initialize(order_values, table)
        @order_values, @table = order_values, table
        @values = nil # cached orderings
      end

      def each(&block)
        return @values.each(&block) if @values
        return enum_for(__method__) unless block_given?

        @values = []

        order_values.each do |value|
          Array.wrap(as_orderings(value)).each do |ordering|
            @values << ordering
          end
        end

        @values.each(&block)
      end

      # Returns order expression to be inserted into SELECT clauses of both
      # non-recursive and recursive terms.
      #
      # @return [Arel::Nodes::Node] order expression
      def row_number_expression
        if raw_ordering?
          order_attribute
        else
          Arel.sql("ROW_NUMBER() OVER (ORDER BY #{map(&:to_sql).join(', ')})")
        end
      end

      private
      def as_orderings(value)
        case value
          when Arel::Nodes::Ordering
            value

          when Arel::Nodes::Node, Arel::Attributes::Attribute
            value.asc

          when Symbol
            table[value].asc

          when Hash
            value.map { |field, dir| table[field].send(dir) }

          when String
            value.split(',').map do |expr|
              string_as_ordering(expr)
            end

          else
            raise 'Unknown expression in ORDER BY SIBLINGS clause'
        end
      end

      def string_as_ordering(expr)
        expr.strip!

        if expr.gsub!(/\bdesc\z/i, '')
          Arel.sql(expr).desc
        else
          expr.gsub!(/\basc\z/i, '')
          Arel.sql(expr).asc
        end
      end

      def raw_ordering?
        ordered_by_attribute? &&
            (column = order_column) &&
            NATURAL_SORT_TYPES.include?(column.type)
      end

      def ordered_by_attribute?
        one? && first.ascending? && order_attribute.is_a?(Arel::Attributes::Attribute)
      end

      def order_attribute
        first.expr
      end

      def order_column
        table = order_attribute.relation

        engine = table.class.engine
        if engine == ActiveRecord::Base
          columns =
            if engine.connection.respond_to?(:schema_cache)
              engine.connection.schema_cache.columns_hash(table.name)
            else
              engine.connection_pool.columns_hash[table.name]
            end
        else
          columns = engine.columns_hash
        end

        columns[order_attribute.name.to_s]
      end
    end
  end
end