class ActiveRecord::HierarchicalQuery::Query

Constants

CHILD_SCOPE_METHODS

@api private

ORDERING_COLUMN_NAME

@api private

Attributes

child_scope_value[R]

@api private

connect_by_value[R]

@api private

distinct_value[R]

@api private

klass[R]

@api private

limit_value[R]

@api private

nocycle_value[R]

@api private

offset_value[R]

@api private

order_values[R]

@api private

start_with_value[R]

@api private

Public Class Methods

new(klass) click to toggle source
# File lib/active_record/hierarchical_query/query.rb, line 29
def initialize(klass)
  @klass = klass

  # start with :all
  @start_with_value = klass.__send__(HierarchicalQuery::DELEGATOR_SCOPE)
  @connect_by_value = nil
  @child_scope_value = klass.__send__(HierarchicalQuery::DELEGATOR_SCOPE)
  @limit_value = nil
  @offset_value = nil
  @nocycle_value = false
  @order_values = []
  @distinct_value = false
end

Public Instance Methods

connect_by(conditions = nil, &block) click to toggle source

Specify relationship between parent rows and child rows of the hierarchy. It can be specified with Hash where keys are parent columns names and values are child columns names, or with block (see example below).

@example Specify relationship with Hash (traverse descendants)

MyModel.join_recursive do |hierarchy|
  # join child rows with condition `parent.id = child.parent_id`
  hierarchy.connect_by(id: :parent_id)
end

@example Specify relationship with block (traverse descendants)

MyModel.join_recursive do |hierarchy|
  hierarchy.connect_by { |parent, child| parent[:id].eq(child[:parent_id]) }
end

@param [Hash, nil] conditions (optional) relationship between parent rows and

child rows map, where keys are parent columns names and values are child columns names.

@yield [parent, child] Yields both parent and child tables. @yieldparam [Arel::Table] parent parent rows table instance. @yieldparam [Arel::Table] child child rows table instance. @yieldreturn [Arel::Nodes::Node] relationship condition expressed as Arel node. @return [ActiveRecord::HierarchicalQuery::Query] self

# File lib/active_record/hierarchical_query/query.rb, line 133
def connect_by(conditions = nil, &block)
  # convert hash to block which returns Arel node
  if conditions
    block = conditions_to_proc(conditions)
  end

  raise ArgumentError, 'CONNECT BY: Conditions hash or block expected, none given' unless block

  @connect_by_value = block

  self
end
distinct() click to toggle source

Turn on select distinct option in the CTE.

@return [ActiveRecord::HierarchicalQuery::Query] self

# File lib/active_record/hierarchical_query/query.rb, line 277
def distinct
  @distinct_value = true
  self
end
join_conditions() click to toggle source

@return [Arel::Nodes::Node] @api private

# File lib/active_record/hierarchical_query/query.rb, line 284
def join_conditions
  connect_by_value.call(recursive_table, table)
end
join_to(relation, join_options = {}) click to toggle source

Builds recursive query and joins it to given relation.

@api private @param [ActiveRecord::Relation] relation @param [Hash] join_options @option join_options [#to_s] :as joined table alias @api private

# File lib/active_record/hierarchical_query/query.rb, line 306
def join_to(relation, join_options = {})
  raise 'Recursive query requires CONNECT BY clause, please use #connect_by method' unless
      connect_by_value

  table_alias = join_options.fetch(:as, "#{normalized_table_name}__recursive")

  JoinBuilder.new(self, relation, table_alias, join_options).build
end
limit(value) click to toggle source

Specifies a limit for the number of records to retrieve.

@param [Fixnum] value @return [ActiveRecord::HierarchicalQuery::Query] self

# File lib/active_record/hierarchical_query/query.rb, line 193
def limit(value)
  @limit_value = value

  self
end
nocycle(value = true) click to toggle source

Turn on/off cycles detection. This option can prevent endless loops if your tree could contain cycles.

@param [true, false] value @return [ActiveRecord::HierarchicalQuery::Query] self

# File lib/active_record/hierarchical_query/query.rb, line 237
def nocycle(value = true)
  @nocycle_value = value
  self
end
offset(value) click to toggle source

Specifies the number of rows to skip before returning row

@param [Fixnum] value @return [ActiveRecord::HierarchicalQuery::Query] self

# File lib/active_record/hierarchical_query/query.rb, line 203
def offset(value)
  @offset_value = value

  self
end
order(*columns)
Alias for: order_siblings
order_siblings(*columns) click to toggle source

Specifies hierarchical order of the recursive query results.

@example

MyModel.join_recursive do |hierarchy|
  hierarchy.connect_by(id: :parent_id)
           .order_siblings(:name)
end

@example

MyModel.join_recursive do |hierarchy|
  hierarchy.connect_by(id: :parent_id)
           .order_siblings('name DESC, created_at ASC')
end

@param [<Symbol, String, Arel::Nodes::Node, Arel::Attributes::Attribute>] columns @return [ActiveRecord::HierarchicalQuery::Query] self

# File lib/active_record/hierarchical_query/query.rb, line 225
def order_siblings(*columns)
  @order_values += columns

  self
end
Also aliased as: order
ordering_column_name() click to toggle source

@api private

# File lib/active_record/hierarchical_query/query.rb, line 295
def ordering_column_name
  ORDERING_COLUMN_NAME
end
orderings() click to toggle source

@return [ActiveRecord::HierarchicalQuery::Orderings] @api private

# File lib/active_record/hierarchical_query/query.rb, line 290
def orderings
  @orderings ||= Orderings.new(order_values, table)
end
previous()
Alias for: prior
prior() click to toggle source

Returns object representing parent rows table, so it could be used in complex WHEREs.

@example

MyModel.join_recursive do |hierarchy|
  hierarchy.connect_by(id: :parent_id)
           .start_with(parent_id: nil) { select(:depth) }
           .select(hierarchy.table[:depth])
           .where(hierarchy.prior[:depth].lteq 1)
end

@return [Arel::Table]

# File lib/active_record/hierarchical_query/query.rb, line 254
def prior
  @recursive_table ||= Arel::Table.new("#{normalized_table_name}__recursive")
end
Also aliased as: previous, recursive_table
recursive_table()
Alias for: prior
select(*columns) click to toggle source

Specify which columns should be selected in addition to primary key, CONNECT BY columns and ORDER SIBLINGS columns.

@param [Array<Symbol, String, Arel::Attributes::Attribute, Arel::Nodes::Node>] columns @option columns [true, false] :start_with include given columns to START WITH clause (true by default) @return [ActiveRecord::HierarchicalQuery::Query] self

# File lib/active_record/hierarchical_query/query.rb, line 152
def select(*columns)
  options = columns.extract_options!

  columns = columns.flatten.map do |column|
    column.is_a?(Symbol) ? table[column] : column
  end

  # TODO: detect if column already present in START WITH clause and skip it
  if options.fetch(:start_with, true)
    start_with { |scope| scope.select(columns) }
  end

  @child_scope_value = @child_scope_value.select(columns)

  self
end
start_with(scope = nil, *arguments, &block) click to toggle source

Specify root scope of the hierarchy.

@example When scope given

MyModel.join_recursive do |hierarchy|
  hierarchy.start_with(MyModel.where(parent_id: nil))
           .connect_by(id: :parent_id)
end

@example When Hash given

MyModel.join_recursive do |hierarchy|
  hierarchy.start_with(parent_id: nil)
           .connect_by(id: :parent_id)
end

@example When String given

MyModel.join_recursive do |hierarchy|
  hierararchy.start_with('parent_id = ?', 1)
             .connect_by(id: :parent_id)
end

@example When block given

MyModel.join_recursive do |hierarchy|
  hierarchy.start_with { |root| root.where(parent_id: nil) }
           .connect_by(id: :parent_id)
end

@example When block with arity=0 given

MyModel.join_recursive do |hierarchy|
  hierarchy.start_with { where(parent_id: nil) }
           .connect_by(id: :parent_id)
end

@example Specify columns for root relation (PostgreSQL-specific)

MyModel.join_recursive do |hierarchy|
  hierarchy.start_with { select('ARRAY[id] AS _path') }
           .connect_by(id: :parent_id)
           .select('_path || id', start_with: false) # `start_with: false` tells not to include this expression into START WITH clause
end

@param [ActiveRecord::Relation, Hash, String, nil] scope root scope (optional). @return [ActiveRecord::HierarchicalQuery::Query] self

# File lib/active_record/hierarchical_query/query.rb, line 84
def start_with(scope = nil, *arguments, &block)
  raise ArgumentError, 'START WITH: scope or block expected, none given' unless scope || block

  case scope
    when Hash, String
      @start_with_value = klass.where(scope, *arguments)

    when ActiveRecord::Relation
      @start_with_value = scope

    else
      # do nothing if something weird given
  end

  if block
    object = @start_with_value || @klass

    @start_with_value = if block.arity == 0
      object.instance_eval(&block)
    else
      block.call(object)
    end
  end

  self
end
table() click to toggle source

Returns object representing child rows table, so it could be used in complex WHEREs.

@example

MyModel.join_recursive do |hierarchy|
  hierarchy.connect_by(id: :parent_id)
           .start_with(parent_id: nil) { select(:depth) }
           .select(hierarchy.table[:depth])
           .where(hierarchy.prior[:depth].lteq 1)
end
# File lib/active_record/hierarchical_query/query.rb, line 270
def table
  @klass.arel_table
end

Private Instance Methods

conditions_to_proc(conditions) click to toggle source

converts conditions given as a hash to proc

# File lib/active_record/hierarchical_query/query.rb, line 317
def conditions_to_proc(conditions)
  proc do |parent, child|
    conditions.map do |parent_expression, child_expression|
      parent_expression = parent[parent_expression] if parent_expression.is_a?(Symbol)
      child_expression = child[child_expression] if child_expression.is_a?(Symbol)

      Arel::Nodes::Equality.new(parent_expression, child_expression)
    end.reduce(:and)
  end
end
normalized_table_name() click to toggle source
# File lib/active_record/hierarchical_query/query.rb, line 328
def normalized_table_name
  table.name.gsub('.', '_')
end