class Torque::PostgreSQL::AuxiliaryStatement

Constants

TABLE_COLUMN_AS_STRING

Attributes

config[R]
bound_attributes[R]
join_sources[R]

Public Class Methods

arel_query?(obj) click to toggle source

Identify if the query set may be used as arel

# File lib/torque/postgresql/auxiliary_statement.rb, line 52
def arel_query?(obj)
  !obj.nil? && obj.is_a?(::Arel::SelectManager)
end
build(statement, base, options = nil, bound_attributes = [], join_sources = []) click to toggle source

Fast access to statement build

# File lib/torque/postgresql/auxiliary_statement.rb, line 36
def build(statement, base, options = nil, bound_attributes = [], join_sources = [])
  klass = instantiate(statement, base, options)
  result = klass.build(base)

  bound_attributes.concat(klass.bound_attributes)
  join_sources.concat(klass.join_sources)
  result
end
configurator(config) click to toggle source

Set a configuration block or static hash

# File lib/torque/postgresql/auxiliary_statement.rb, line 74
def configurator(config)
  if config.is_a?(Hash)
    # Map the aliases
    config[:attributes] = config.delete(:select) if config.key?(:select)

    # Create the struct that mocks a configuration result
    config = OpenStruct.new(config)
    table_name = config[:query]&.klass&.name&.underscore
    instance_variable_set(:@table_name, table_name)
  end

  @config = config
end
configure(base, instance) click to toggle source

Run a configuration block or get the static configuration

# File lib/torque/postgresql/auxiliary_statement.rb, line 89
def configure(base, instance)
  return @config unless @config.respond_to?(:call)

  settings = Settings.new(base, instance)
  settings.instance_exec(settings, &@config)
  settings
end
create(table_or_settings, &block) click to toggle source

A way to create auxiliary statements outside of models configurations, being able to use on extensions

# File lib/torque/postgresql/auxiliary_statement.rb, line 58
def create(table_or_settings, &block)
  klass = Class.new(AuxiliaryStatement)

  if block_given?
    klass.instance_variable_set(:@table_name, table_or_settings)
    klass.configurator(block)
  elsif relation_query?(table_or_settings)
    klass.configurator(query: table_or_settings)
  else
    klass.configurator(table_or_settings)
  end

  klass
end
instantiate(statement, base, options = nil) click to toggle source

Create a new instance of an auxiliary statement

# File lib/torque/postgresql/auxiliary_statement.rb, line 21
        def instantiate(statement, base, options = nil)
          klass = while base < ActiveRecord::Base
            list = base.auxiliary_statements_list
            break list[statement] if list.present? && list.key?(statement)

            base = base.superclass
          end

          return klass.new(options) unless klass.nil?
          raise ArgumentError, <<-MSG.squish
            There's no '#{statement}' auxiliary statement defined for #{base.class.name}.
          MSG
        end
lookup(name, base) click to toggle source

Find or create the class that will handle statement

# File lib/torque/postgresql/auxiliary_statement.rb, line 14
def lookup(name, base)
  const = name.to_s.camelize << '_' << self.name.demodulize
  return base.const_get(const, false) if base.const_defined?(const, false)
  base.const_set(const, Class.new(AuxiliaryStatement))
end
new(*args) click to toggle source

Start a new auxiliary statement giving extra options

# File lib/torque/postgresql/auxiliary_statement.rb, line 114
def initialize(*args)
  options = args.extract_options!
  args_key = Torque::PostgreSQL.config.auxiliary_statement.send_arguments_key

  @join = options.fetch(:join, {})
  @args = options.fetch(args_key, {})
  @where = options.fetch(:where, {})
  @select = options.fetch(:select, {})
  @join_type = options.fetch(:join_type, nil)

  @bound_attributes = []
  @join_sources = []
end
relation_query?(obj) click to toggle source

Identify if the query set may be used as a relation

# File lib/torque/postgresql/auxiliary_statement.rb, line 46
def relation_query?(obj)
  !obj.nil? && obj.respond_to?(:ancestors) && \
    obj.ancestors.include?(ActiveRecord::Base)
end
table() click to toggle source

Get the arel version of the statement table

# File lib/torque/postgresql/auxiliary_statement.rb, line 98
def table
  @table ||= ::Arel::Table.new(table_name)
end
table_name() click to toggle source

Get the name of the table of the configurated statement

# File lib/torque/postgresql/auxiliary_statement.rb, line 103
def table_name
  @table_name ||= self.name.demodulize.split('_').first.underscore
end

Public Instance Methods

build(base) click to toggle source

Build the statement on the given arel and return the WITH statement

# File lib/torque/postgresql/auxiliary_statement.rb, line 129
def build(base)
  @bound_attributes.clear
  @join_sources.clear

  # Prepare all the data for the statement
  prepare(base)

  # Add the join condition to the list
  @join_sources << build_join(base)

  # Return the statement with its dependencies
  [@dependencies, ::Arel::Nodes::As.new(table, build_query(base))]
end

Private Instance Methods

arel_join() click to toggle source

Get the class of the join on arel

# File lib/torque/postgresql/auxiliary_statement.rb, line 251
        def arel_join
          case @join_type
          when :inner then ::Arel::Nodes::InnerJoin
          when :left  then ::Arel::Nodes::OuterJoin
          when :right then ::Arel::Nodes::RightOuterJoin
          when :full  then ::Arel::Nodes::FullOuterJoin
          else
            raise ArgumentError, <<-MSG.squish
              The '#{@join_type}' is not implemented as a join type.
            MSG
          end
        end
build_join(base) click to toggle source

Build the join statement that will be sent to the main arel

# File lib/torque/postgresql/auxiliary_statement.rb, line 198
        def build_join(base)
          conditions = table.create_and([])
          builder = base.predicate_builder
          foreign_table = base.arel_table

          # Check if it's necessary to load the join from an association
          if @association.present?
            association = base.reflections[@association]

            # Require source of a through reflection
            if association.through_reflection?
              base.joins(association.source_reflection_name)

              # Changes the base of the connection to the reflection table
              builder = association.klass.predicate_builder
              foreign_table = ::Arel::Table.new(association.plural_name)
            end

            @query.merge(association.join_scope(@query.arel_table, foreign_table, base))

            # Add the join constraints
            constraint = association.build_join_constraint(table, foreign_table)
            constraint = constraint.children if constraint.is_a?(::Arel::Nodes::And)
            conditions.children.concat(Array.wrap(constraint))
          end

          # Build all conditions for the join on statement
          @join.inject(conditions.children) do |arr, (left, right)|
            left = project(left, foreign_table)
            item = right.is_a?(Symbol) ? project(right).eq(left) : builder.build(left, right)
            arr.push(item)
          end

          # Raise an error when there's no join conditions
          raise ArgumentError, <<-MSG.squish if conditions.children.empty?
            You must provide the join columns when using '#{@query.class.name}'
            as a query object on #{self.class.name}.
          MSG

          # Expose join columns
          if relation_query?(@query)
            query_table = @query.arel_table
            conditions.children.each do |item|
              @query.select_values += [query_table[item.left.name]] \
                if item.left.relation.eql?(table)
            end
          end

          # Build the join based on the join type
          arel_join.new(table, table.create_on(conditions))
        end
build_query(base) click to toggle source

Build the string or arel query

# File lib/torque/postgresql/auxiliary_statement.rb, line 177
        def build_query(base)
          # Expose columns and get the list of the ones for select
          columns = expose_columns(base, @query.try(:arel_table))

          # Prepare the query depending on its type
          if @query.is_a?(String)
            args = @args.map{ |k, v| [k, base.connection.quote(v)] }.to_h
            ::Arel.sql("(#{@query})" % args)
          elsif relation_query?(@query)
            @query = @query.where(@where) if @where.present?
            @bound_attributes.concat(@query.send(:bound_attributes))
            @query.select(*columns).arel
          else
            raise ArgumentError, <<-MSG.squish
              Only String and ActiveRecord::Base objects are accepted as query objects,
              #{@query.class.name} given for #{self.class.name}.
            MSG
          end
        end
ensure_dependencies(list, base) click to toggle source

Ensure that all the dependencies are loaded in the base relation

# File lib/torque/postgresql/auxiliary_statement.rb, line 274
        def ensure_dependencies(list, base)
          with_options = list.extract_options!.to_a
          (list + with_options).map do |dependent, options|
            dependent_klass = base.model.auxiliary_statements_list[dependent]

            raise ArgumentError, <<-MSG.squish if dependent_klass.nil?
              The '#{dependent}' auxiliary statement dependency can't found on
              #{self.class.name}.
            MSG

            next if base.auxiliary_statements_values.any? do |cte|
              cte.is_a?(dependent_klass)
            end

            AuxiliaryStatement.build(dependent, base, options, bound_attributes, join_sources)
          end
        end
expose_columns(base, query_table = nil) click to toggle source

Mount the list of selected attributes

# File lib/torque/postgresql/auxiliary_statement.rb, line 265
def expose_columns(base, query_table = nil)
  # Add select columns to the query and get exposed columns
  @select.map do |left, right|
    base.select_extra_values += [table[right.to_s]]
    project(left, query_table).as(right.to_s) if query_table
  end
end
prepare(base) click to toggle source

Setup the statement using the class configuration

# File lib/torque/postgresql/auxiliary_statement.rb, line 145
def prepare(base)
  settings = configure(base, self)
  requires = Array.wrap(settings.requires).flatten.compact
  @dependencies = ensure_dependencies(requires, base).flatten.compact

  @join_type ||= settings.join_type || :inner
  @query = settings.query

  # Call a proc to get the real query
  if @query.methods.include?(:call)
    call_args = @query.try(:arity) === 0 ? [] : [OpenStruct.new(@args)]
    @query = @query.call(*call_args)
    @args = []
  end

  # Manually set the query table when it's not an relation query
  @query_table = settings.query_table unless relation_query?(@query)
  @select = settings.attributes.merge(@select) if settings.attributes.present?

  # Merge join settings
  if settings.join.present?
    @join = settings.join.merge(@join)
  elsif settings.through.present?
    @association = settings.through.to_s
  elsif relation_query?(@query)
    @association = base.reflections.find do |name, reflection|
      break name if @query.klass.eql? reflection.klass
    end
  end
end
project(column, arel_table = nil) click to toggle source

Project a column on a given table, or use the column table

# File lib/torque/postgresql/auxiliary_statement.rb, line 293
def project(column, arel_table = nil)
  if column.respond_to?(:as)
    return column
  elsif (as_string = TABLE_COLUMN_AS_STRING.match(column.to_s))
    column = as_string[2]
    arel_table = ::Arel::Table.new(as_string[1]) unless as_string[1].nil?
  end

  arel_table ||= table
  arel_table[column.to_s]
end