Class: LcpRuby::VirtualColumns::Builder

Inherits:
Object
  • Object
show all
Defined in:
lib/lcp_ruby/virtual_columns/builder.rb

Class Method Summary collapse

Class Method Details

.apply(scope, model_definition, vc_names, current_user: nil) ⇒ Array(ActiveRecord::Relation, Array<String>, Boolean)

Inject virtual column subqueries/expressions into a scope.

Parameters:

  • scope (ActiveRecord::Relation)

    the base scope

  • model_definition (Metadata::ModelDefinition)

    the model definition

  • vc_names (Array<String>)

    names of virtual columns to include

  • current_user (Object, nil) (defaults to: nil)

    the current user (for :current_user placeholder)

Returns:

  • (Array(ActiveRecord::Relation, Array<String>, Boolean))

    modified scope, service-only names, and whether GROUP BY was applied



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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/lcp_ruby/virtual_columns/builder.rb', line 11

def self.apply(scope, model_definition, vc_names, current_user: nil)
  return [ scope, [], false ] if vc_names.empty?

  conn = ActiveRecord::Base.connection
  parent_table = conn.quote_table_name(model_definition.table_name)
  subqueries = []
  service_only = []
  joins_to_apply = []
  needs_group_by = false

  vc_names.each do |vc_name|
    vc_def = model_definition.virtual_column(vc_name)
    next unless vc_def

    if vc_def.join.present?
      joins_to_apply << vc_def.join
    end

    needs_group_by = true if vc_def.group

    sql = build_subquery(vc_def, model_definition, parent_table, conn, current_user: current_user)
    if sql
      subqueries << "#{sql} AS #{conn.quote_column_name(vc_name)}"
    elsif vc_def.service_type?
      service_only << vc_name
    end
  end

  applied_vc_names = vc_names - service_only

  if subqueries.any?
    scope = scope.select("#{parent_table}.*", *subqueries)
  end

  # Apply deduplicated JOINs
  if joins_to_apply.any?
    seen = Set.new
    joins_to_apply.each do |join_sql|
      normalized = join_sql.gsub("%{table}", parent_table).gsub(/\s+/, " ").strip
      dedup_key = normalized.downcase
      next if seen.include?(dedup_key)
      seen << dedup_key

      scope = scope.joins(Arel.sql(normalized))
    end
  end

  # Apply GROUP BY if any virtual column requires it
  if needs_group_by
    scope = scope.group("#{parent_table}.#{conn.quote_column_name('id')}")
  end

  # Wrap scope with extending module for loaded-tracking guard.
  # Pushes VC names onto thread-local stack around exec_queries so
  # after_initialize callbacks can capture which VCs were loaded.
  if applied_vc_names.any? && scope.klass.respond_to?(:_virtual_columns_stack)
    vc_name_set = Set.new(applied_vc_names.map(&:to_s)).freeze
    scope = scope.extending(Module.new do
      define_method(:exec_queries) do
        klass._virtual_columns_stack ||= []
        klass._virtual_columns_stack.push(vc_name_set)
        begin
          super()
        ensure
          klass._virtual_columns_stack.pop
        end
      end
    end)
  end

  [ scope, service_only, needs_group_by ]
end