lib/torque/postgresql/auxiliary_statement/recursive.rb
# frozen_string_literal: true
module Torque
module PostgreSQL
class AuxiliaryStatement
class Recursive < AuxiliaryStatement
# Setup any additional option in the recursive mode
def initialize(*, **options)
super
@connect = options[:connect]&.to_a&.first
@union_all = options[:union_all]
@sub_query = options[:sub_query]
if options.key?(:with_depth)
@depth = options[:with_depth].values_at(:name, :start, :as)
@depth[0] ||= 'depth'
end
if options.key?(:with_path)
@path = options[:with_path].values_at(:name, :source, :as)
@path[0] ||= 'path'
end
end
private
# Build the string or arel query
def build_query(base)
# Expose columns and get the list of the ones for select
columns = expose_columns(base, @query.try(:arel_table))
sub_columns = columns.dup
type = @union_all.present? ? 'all' : ''
# Build any extra columns that are dynamic and from the recursion
extra_columns(base, columns, sub_columns)
# Prepare the query depending on its type
if @query.is_a?(String) && @sub_query.is_a?(String)
args = @args.each_with_object({}) { |h, (k, v)| h[k] = base.connection.quote(v) }
::Arel.sql("(#{@query} UNION #{type.upcase} #{@sub_query})" % args)
elsif relation_query?(@query)
@query = @query.where(@where) if @where.present?
@bound_attributes.concat(@query.send(:bound_attributes))
if relation_query?(@sub_query)
@bound_attributes.concat(@sub_query.send(:bound_attributes))
sub_query = @sub_query.select(*sub_columns).arel
sub_query.from([@sub_query.arel_table, table])
else
sub_query = ::Arel.sql(@sub_query)
end
@query.select(*columns).arel.union(type, sub_query)
else
raise ArgumentError, <<-MSG.squish
Only String and ActiveRecord::Base objects are accepted as query and sub query
objects, #{@query.class.name} given for #{self.class.name}.
MSG
end
end
# Setup the statement using the class configuration
def prepare(base, settings)
super
prepare_sub_query(base, settings)
end
# Make sure that both parts of the union are ready
def prepare_sub_query(base, settings)
@union_all = settings.union_all if @union_all.nil?
@sub_query ||= settings.sub_query
@depth ||= settings.depth
@path ||= settings.path
# Collect the connection
@connect ||= settings.connect || begin
key = base.primary_key
[key.to_sym, :"parent_#{key}"] unless key.nil?
end
raise ArgumentError, <<-MSG.squish if @sub_query.nil? && @query.is_a?(String)
Unable to generate sub query from a string query. Please provide a `sub_query`
property on the "#{table_name}" settings.
MSG
if @sub_query.nil?
raise ArgumentError, <<-MSG.squish if @connect.blank?
Unable to generate sub query without setting up a proper way to connect it
with the main query. Please provide a `connect` property on the "#{table_name}"
settings.
MSG
left, right = @connect.map(&:to_s)
condition = @query.arel_table[right].eq(table[left])
if @query.where_values_hash.key?(right)
@sub_query = @query.unscope(where: right.to_sym).where(condition)
else
@sub_query = @query.where(condition)
@query = @query.where(right => nil)
end
elsif @sub_query.respond_to?(:call)
# Call a proc to get the real sub query
call_args = @sub_query.try(:arity) === 0 ? [] : [OpenStruct.new(@args)]
@sub_query = @sub_query.call(*call_args)
end
end
# Add depth and path if they were defined in settings
def extra_columns(base, columns, sub_columns)
return if @query.is_a?(String) || @sub_query.is_a?(String)
# Add the connect attribute to the query
if defined?(@connect)
columns.unshift(@query.arel_table[@connect[0]])
sub_columns.unshift(@sub_query.arel_table[@connect[0]])
end
# Build a column to represent the depth of the recursion
if @depth.present?
name, start, as = @depth
col = table[name]
base.select_extra_values += [col.as(as)] unless as.nil?
columns << ::Arel.sql(start.to_s).as(name)
sub_columns << (col + ::Arel.sql('1')).as(name)
end
# Build a column to represent the path of the record access
if @path.present?
name, source, as = @path
source = @query.arel_table[source || @connect[0]]
col = table[name]
base.select_extra_values += [col.as(as)] unless as.nil?
parts = [col, source.cast(:varchar)]
columns << ::Arel.array([source]).cast(:varchar, true).as(name)
sub_columns << ::Arel::Nodes::NamedFunction.new('array_append', parts).as(name)
end
end
end
end
end
end