Module: Familia::Features::Relationships::Indexing::MultiIndexGenerators

Defined in:
lib/familia/features/relationships/indexing/multi_index_generators.rb

Overview

Generators for multi-value index (1:many) methods

Multi-value indexes use UnsortedSet DataType for grouping objects by field value. Each field value gets its own set of object identifiers.

Example: multi_index :department, :dept_index, within: Company

Generates on Company (destination):

  • company.sample_from_department(dept, count=1)
  • company.find_all_by_department(dept)
  • company.dept_index_for(dept_value)
  • company.rebuild_dept_index

Generates on Employee (self):

  • employee.add_to_company_dept_index(company)
  • employee.remove_from_company_dept_index(company)
  • employee.update_in_company_dept_index(company, old_dept)

Class Method Summary collapse

Class Method Details

.generate_factory_method(target_class, index_name) ⇒ Object

Generates the factory method ON THE PARENT CLASS (Company when within: Company):

  • company.index_name_for(field_value) - DataType factory (always needed)

This method is required by mutation methods even when query: false

Parameters:

  • target_class (Class)

    The parent class (e.g., Company)

  • index_name (Symbol)

    Name of the index (e.g., :dept_index)



72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/familia/features/relationships/indexing/multi_index_generators.rb', line 72

def generate_factory_method(target_class, index_name)
  actual_target_class = Familia.resolve_class(target_class)

  actual_target_class.class_eval do
    # Helper method to get index set for a specific field value
    # This acts as a factory for field-value-specific DataTypes
    define_method("#{index_name}_for") do |field_value|
      # Return properly managed DataType instance with parameterized key
      index_key = "#{index_name}:#{field_value}"
      Familia::UnsortedSet.new(index_key, parent: self)
    end
  end
end

.generate_mutation_methods_self(indexed_class, field, target_class, index_name) ⇒ Object

Generates mutation methods ON THE INDEXED CLASS (Employee):

  • employee.add_to_company_dept_index(company)
  • employee.remove_from_company_dept_index(company)
  • employee.update_in_company_dept_index(company, old_dept)

Parameters:

  • indexed_class (Class)

    The class being indexed (e.g., Employee)

  • field (Symbol)

    The field to index (e.g., :department)

  • target_class (Class)

    The parent class (e.g., Company)

  • index_name (Symbol)

    Name of the index (e.g., :dept_index)



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/familia/features/relationships/indexing/multi_index_generators.rb', line 138

def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
  target_class_config = target_class.config_name
  indexed_class.class_eval do
    method_name = "add_to_#{target_class_config}_#{index_name}"
    Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")

    define_method(method_name) do |target_instance|
      return unless target_instance

      field_value = send(field)
      return unless field_value

      # Use helper method on target instance instead of manual instantiation
      index_set = target_instance.send("#{index_name}_for", field_value)

      # Use UnsortedSet DataType method (no scoring)
      index_set.add(identifier)
    end

    method_name = "remove_from_#{target_class_config}_#{index_name}"
    Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")

    define_method(method_name) do |target_instance|
      return unless target_instance

      field_value = send(field)
      return unless field_value

      # Use helper method on target instance instead of manual instantiation
      index_set = target_instance.send("#{index_name}_for", field_value)

      # Remove using UnsortedSet DataType method
      index_set.remove(identifier)
    end

    method_name = "update_in_#{target_class_config}_#{index_name}"
    Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")

    define_method(method_name) do |target_instance, old_field_value = nil|
      return unless target_instance

      new_field_value = send(field)

      # Use Familia's transaction method for atomicity with DataType abstraction
      target_instance.transaction do |_tx|
        # Remove from old index if provided - use helper method
        if old_field_value
          old_index_set = target_instance.send("#{index_name}_for", old_field_value)
          old_index_set.remove(identifier)
        end

        # Add to new index if present - use helper method
        if new_field_value
          new_index_set = target_instance.send("#{index_name}_for", new_field_value)
          new_index_set.add(identifier)
        end
      end
    end
  end
end

.generate_query_methods_destination(indexed_class, field, target_class, index_name) ⇒ Object

Generates query methods ON THE PARENT CLASS (Company when within: Company):

  • company.sample_from_department(dept, count=1) - random sampling
  • company.find_all_by_department(dept) - all objects
  • company.rebuild_dept_index - rebuild index

Parameters:

  • indexed_class (Class)

    The class being indexed (e.g., Employee)

  • field (Symbol)

    The field to index (e.g., :department)

  • target_class (Class)

    The parent class (e.g., Company)

  • index_name (Symbol)

    Name of the index (e.g., :dept_index)



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/familia/features/relationships/indexing/multi_index_generators.rb', line 95

def generate_query_methods_destination(indexed_class, field, target_class, index_name)
  # Resolve target class using Familia pattern
  actual_target_class = Familia.resolve_class(target_class)

  # Generate instance sampling method (e.g., company.sample_from_department)
  actual_target_class.class_eval do

    define_method("sample_from_#{field}") do |field_value, count = 1|
      index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet

      # Get random members efficiently (O(1) via SRANDMEMBER with count)
      # Returns array even for count=1 for consistent API
      index_set.sample(count).map do |id|
        indexed_class.new(index_set.deserialize_value(id))
      end
    end

    # Generate bulk query method (e.g., company.find_all_by_department)
    define_method("find_all_by_#{field}") do |field_value|
      index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet

      # Get all members from set
      index_set.members.map { |id| indexed_class.new(id) }
    end

    # Generate method to rebuild the index for this parent instance
    define_method("rebuild_#{index_name}") do
      # This would need to be implemented based on how you track which
      # objects belong to this parent instance
      # For now, just a placeholder
    end
  end
end

.setup(indexed_class:, field:, index_name:, within:, query:) ⇒ Object

Main setup method that orchestrates multi-value index creation

Parameters:

  • indexed_class (Class)

    The class being indexed (e.g., Employee)

  • field (Symbol)

    The field to index

  • index_name (Symbol)

    Name of the index

  • within (Class, Symbol)

    Parent class for instance-scoped index (required)

  • query (Boolean)

    Whether to generate query methods



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
# File 'lib/familia/features/relationships/indexing/multi_index_generators.rb', line 37

def setup(indexed_class:, field:, index_name:, within:, query:)
  # Multi-index always requires a parent context
  target_class = within
  resolved_class = Familia.resolve_class(target_class)

  # Store metadata for this indexing relationship
  indexed_class.indexing_relationships << IndexingRelationship.new(
    field:             field,
    target_class:      target_class,
    index_name:        index_name,
    query:            query,
    cardinality:       :multi,
  )

  # Always generate the factory method - required by mutation methods
  if target_class.is_a?(Class)
    generate_factory_method(resolved_class, index_name)
  end

  # Generate query methods on the parent class (optional)
  if query && target_class.is_a?(Class)
    generate_query_methods_destination(indexed_class, field, resolved_class, index_name)
  end

  # Generate mutation methods on the indexed class
  generate_mutation_methods_self(indexed_class, field, resolved_class, index_name)
end