Class: ActiveRecord::HierarchicalQuery::Query

Inherits:
Object
  • Object
show all
Defined in:
lib/active_record/hierarchical_query/query.rb

Constant Summary collapse

ORDERING_COLUMN_NAME =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

'__order_column'.freeze
CHILD_SCOPE_METHODS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

:where, :joins, :group, :having

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(klass) ⇒ Query

Returns a new instance of Query.



29
30
31
32
33
34
35
36
37
38
39
40
41
# 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

Instance Attribute Details

#child_scope_valueObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



16
17
18
# File 'lib/active_record/hierarchical_query/query.rb', line 16

def child_scope_value
  @child_scope_value
end

#connect_by_valueObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



16
17
18
# File 'lib/active_record/hierarchical_query/query.rb', line 16

def connect_by_value
  @connect_by_value
end

#distinct_valueObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



16
17
18
# File 'lib/active_record/hierarchical_query/query.rb', line 16

def distinct_value
  @distinct_value
end

#klassObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



16
17
18
# File 'lib/active_record/hierarchical_query/query.rb', line 16

def klass
  @klass
end

#limit_valueObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



16
17
18
# File 'lib/active_record/hierarchical_query/query.rb', line 16

def limit_value
  @limit_value
end

#nocycle_valueObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



16
17
18
# File 'lib/active_record/hierarchical_query/query.rb', line 16

def nocycle_value
  @nocycle_value
end

#offset_valueObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



16
17
18
# File 'lib/active_record/hierarchical_query/query.rb', line 16

def offset_value
  @offset_value
end

#order_valuesObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



16
17
18
# File 'lib/active_record/hierarchical_query/query.rb', line 16

def order_values
  @order_values
end

#start_with_valueObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



16
17
18
# File 'lib/active_record/hierarchical_query/query.rb', line 16

def start_with_value
  @start_with_value
end

Instance Method Details

#connect_by(conditions = nil) {|parent, child| ... } ⇒ ActiveRecord::HierarchicalQuery::Query

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).

Examples:

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

Specify relationship with block (traverse descendants)

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

Parameters:

  • conditions (Hash, nil) (defaults to: nil)

    (optional) relationship between parent rows and child rows map, where keys are parent columns names and values are child columns names.

Yields:

  • (parent, child)

    Yields both parent and child tables.

Yield Parameters:

  • parent (Arel::Table)

    parent rows table instance.

  • child (Arel::Table)

    child rows table instance.

Yield Returns:

  • (Arel::Nodes::Node)

    relationship condition expressed as Arel node.

Returns:

Raises:

  • (ArgumentError)


133
134
135
136
137
138
139
140
141
142
143
144
# 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

#distinctActiveRecord::HierarchicalQuery::Query

Turn on select distinct option in the CTE.



277
278
279
280
# File 'lib/active_record/hierarchical_query/query.rb', line 277

def distinct
  @distinct_value = true
  self
end

#group(*values) ⇒ Object

Generate methods that apply filters to child scope, such as where or group.

Examples:

Filter child nodes by certain condition

MyModel.join_recursive do |hierarchy|
  hierarchy.where('depth < 5')
end


181
182
183
184
185
186
187
# File 'lib/active_record/hierarchical_query/query.rb', line 181

CHILD_SCOPE_METHODS.each do |method|
  define_method(method) do |*args|
    @child_scope_value = @child_scope_value.public_send(method, *args)

    self
  end
end

#having(*conditions) ⇒ Object

Generate methods that apply filters to child scope, such as where or group.

Examples:

Filter child nodes by certain condition

MyModel.join_recursive do |hierarchy|
  hierarchy.where('depth < 5')
end


181
182
183
184
185
186
187
# File 'lib/active_record/hierarchical_query/query.rb', line 181

CHILD_SCOPE_METHODS.each do |method|
  define_method(method) do |*args|
    @child_scope_value = @child_scope_value.public_send(method, *args)

    self
  end
end

#join_conditionsArel::Nodes::Node

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Arel::Nodes::Node)


284
285
286
# 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 = {}) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Builds recursive query and joins it to given relation.

Parameters:

  • relation (ActiveRecord::Relation)
  • join_options (Hash) (defaults to: {})

Options Hash (join_options):

  • :as (#to_s)

    joined table alias



306
307
308
309
310
311
312
313
# 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

#joins(*tables) ⇒ Object

Generate methods that apply filters to child scope, such as where or group.

Examples:

Filter child nodes by certain condition

MyModel.join_recursive do |hierarchy|
  hierarchy.where('depth < 5')
end


181
182
183
184
185
186
187
# File 'lib/active_record/hierarchical_query/query.rb', line 181

CHILD_SCOPE_METHODS.each do |method|
  define_method(method) do |*args|
    @child_scope_value = @child_scope_value.public_send(method, *args)

    self
  end
end

#limit(value) ⇒ ActiveRecord::HierarchicalQuery::Query

Specifies a limit for the number of records to retrieve.

Parameters:

  • value (Fixnum)

Returns:



193
194
195
196
197
# File 'lib/active_record/hierarchical_query/query.rb', line 193

def limit(value)
  @limit_value = value

  self
end

#nocycle(value = true) ⇒ ActiveRecord::HierarchicalQuery::Query

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

Parameters:

  • value (true, false) (defaults to: true)

Returns:



237
238
239
240
# File 'lib/active_record/hierarchical_query/query.rb', line 237

def nocycle(value = true)
  @nocycle_value = value
  self
end

#offset(value) ⇒ ActiveRecord::HierarchicalQuery::Query

Specifies the number of rows to skip before returning row

Parameters:

  • value (Fixnum)

Returns:



203
204
205
206
207
# File 'lib/active_record/hierarchical_query/query.rb', line 203

def offset(value)
  @offset_value = value

  self
end

#order_siblings(*columns) ⇒ ActiveRecord::HierarchicalQuery::Query Also known as: order

Specifies hierarchical order of the recursive query results.

Examples:

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

Parameters:

  • columns (<Symbol, String, Arel::Nodes::Node, Arel::Attributes::Attribute>)

Returns:



225
226
227
228
229
# File 'lib/active_record/hierarchical_query/query.rb', line 225

def order_siblings(*columns)
  @order_values += columns

  self
end

#ordering_column_nameObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



295
296
297
# File 'lib/active_record/hierarchical_query/query.rb', line 295

def ordering_column_name
  ORDERING_COLUMN_NAME
end

#orderingsActiveRecord::HierarchicalQuery::Orderings

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



290
291
292
# File 'lib/active_record/hierarchical_query/query.rb', line 290

def orderings
  @orderings ||= Orderings.new(order_values, table)
end

#priorArel::Table Also known as: previous, recursive_table

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

Examples:

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

Returns:

  • (Arel::Table)


254
255
256
# File 'lib/active_record/hierarchical_query/query.rb', line 254

def prior
  @recursive_table ||= Arel::Table.new("#{normalized_table_name}__recursive")
end

#select(*columns) ⇒ ActiveRecord::HierarchicalQuery::Query

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

Parameters:

  • columns (Array<Symbol, String, Arel::Attributes::Attribute, Arel::Nodes::Node>)

Options Hash (*columns):

  • :start_with (true, false)

    include given columns to START WITH clause (true by default)

Returns:



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# 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) ⇒ ActiveRecord::HierarchicalQuery::Query

Specify root scope of the hierarchy.

Examples:

When scope given

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

When Hash given

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

When String given

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

When block given

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

When block with arity=0 given

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

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

Parameters:

  • scope (ActiveRecord::Relation, Hash, String, nil) (defaults to: nil)

    root scope (optional).

Returns:

Raises:

  • (ArgumentError)


84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# 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

#tableObject

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

Examples:

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


270
271
272
# File 'lib/active_record/hierarchical_query/query.rb', line 270

def table
  @klass.arel_table
end

#where(*conditions) ⇒ Object

Generate methods that apply filters to child scope, such as where or group.

Examples:

Filter child nodes by certain condition

MyModel.join_recursive do |hierarchy|
  hierarchy.where('depth < 5')
end


181
182
183
184
185
186
187
# File 'lib/active_record/hierarchical_query/query.rb', line 181

CHILD_SCOPE_METHODS.each do |method|
  define_method(method) do |*args|
    @child_scope_value = @child_scope_value.public_send(method, *args)

    self
  end
end