summaryrefslogtreecommitdiff
path: root/lib/gitlab/sql/recursive_cte.rb
blob: e45ac5d4765796540daaffdc8c2ffae1b37f2ebb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# frozen_string_literal: true

module Gitlab
  module SQL
    # Class for easily building recursive CTE statements.
    #
    # Example:
    #
    #     cte = RecursiveCTE.new(:my_cte_name)
    #     ns = Arel::Table.new(:namespaces)
    #
    #     cte << Namespace.
    #       where(ns[:parent_id].eq(some_namespace_id))
    #
    #     cte << Namespace.
    #       from([ns, cte.table]).
    #       where(ns[:parent_id].eq(cte.table[:id]))
    #
    #     Namespace.with.
    #       recursive(cte.to_arel).
    #       from(cte.alias_to(ns))
    class RecursiveCTE
      attr_reader :table

      # name - The name of the CTE as a String or Symbol.
      def initialize(name)
        @table = Arel::Table.new(name)
        @queries = []
      end

      # Adds a query to the body of the CTE.
      #
      # relation - The relation object to add to the body of the CTE.
      def <<(relation)
        @queries << relation
      end

      # Returns the Arel relation for this CTE.
      def to_arel
        sql = Arel::Nodes::SqlLiteral.new(Union.new(@queries).to_sql)

        Arel::Nodes::As.new(table, Arel::Nodes::Grouping.new(sql))
      end

      # Returns an "AS" statement that aliases the CTE name as the given table
      # name. This allows one to trick ActiveRecord into thinking it's selecting
      # from an actual table, when in reality it's selecting from a CTE.
      #
      # alias_table - The Arel table to use as the alias.
      def alias_to(alias_table)
        Arel::Nodes::As.new(table, Arel::Table.new(alias_table.name.tr('.', '_')))
      end

      # Applies the CTE to the given relation, returning a new one that will
      # query from it.
      def apply_to(relation)
        relation.except(:where)
          .with
          .recursive(to_arel)
          .from(alias_to(relation.model.arel_table))
      end
    end
  end
end