Как я могу выполнить рекурсивный CTE-запрос в области ActiveRecord?

#ruby-on-rails #activerecord #arel

#ruby-на-рельсах #активная запись #arel

Вопрос:

У меня есть простая таблица для представления иерархии организаций:

 CREATE TABLE public.organizations (
    id integer NOT NULL,
    name character varying(255) NOT NULL,
    parent_id integer,
    deleted_at timestamp without time zone
)
 

Цель состоит в том, чтобы, учитывая некоторый запрос для некоторых организаций, расширить эти результаты, включив в них всех потомков. В Postgres это можно сделать с помощью рекурсивного CTE, например:

 WITH RECURSIVE "all_orgs" AS (
  SELECT "organizations".*
  FROM "organizations"
  /* some filter, maybe some joins here */

  UNION

  SELECT "organizations".*
  FROM "organizations"
  INNER JOIN "all_orgs" ON "all_orgs"."id" = "organizations"."parent_id"
)
SELECT "all_orgs".* FROM "all_orgs"
 

Я хотел бы запрограммировать повторно используемый способ включения всех потомков для любого произвольного начального набора организаций. Поэтому, естественно, я попытался реализовать область применения:

 class Organization << ApplicationRecord
  scope :recursive_child_orgs, ->(root_orgs) do
    all_orgs = Arel::Table.new(:all_orgs)

    join_constraint = Arel::Nodes::On.new(all_orgs[:id].eq(arel_table[:parent_id]))
    join_node = Arel::Nodes::InnerJoin.new(all_orgs, join_constraint)
    child_orgs = joins(join_node)
    
    union = root_orgs.arel.union(child_orgs.arel)
    
    recursive_cte = Arel::SelectManager.new(all_orgs).tap do |sm|
      sm.with(:recursive, Arel::Nodes::As.new(all_orgs, union))
      sm.project(all_orgs[Arel::star])
    end
    
    from(recursive_cte.as(table_name))
  end
end
 

Это было бы использовано примерно так Organization.recursive_child_orgs(Organization.where(id: 3)) (на практике внутренняя часть этого выражения была бы чем-то менее тривиальным).

Это мучительно близко, генерируя запрос:

 SELECT "organizations".* FROM (
    WITH RECURSIVE "all_orgs" AS ( 
      SELECT "organizations".* 
      FROM "organizations"
      WHERE "organizations"."deleted_at" IS NULL
      AND "organizations"."id" = /* note: missing value! */
    
      UNION
    
      SELECT "organizations".* 
      FROM "organizations" 
      INNER JOIN "all_orgs" ON "all_orgs"."id" = "organizations"."parent_id"
      WHERE "organizations"."deleted_at" IS NULL
    )
    SELECT "all_orgs".* FROM "all_orgs"
) organizations
WHERE "organizations"."deleted_at" IS NULL;
 

Область по умолчанию для Organization была включена, приятно! И весь этот беспорядок CTE объединен в виде объекта с именем organizations , что означает, что ActiveRecord выглядит как таблица организаций, поэтому добавление сортировки и еще много чего после рекурсивного бита должно сработать.

Но, к сожалению, связанный параметр, который был введен с Organization.where(id: 3) помощью, не выдерживает перевода, и в результате генерируется синтаксически недопустимый оператор. Как это можно исправить?