Module: ActiveRecordWhereAssoc::CoreLogic

Defined in:
lib/active_record_where_assoc/core_logic.rb

Constant Summary collapse

ALIAS_TABLE =

Arel table used for aliasing when handling recursive associations (such as parent/children)

Arel::Table.new("_ar_where_assoc_alias_")
NestWithExistsBlock =

Block used when nesting associations for a where_assoc_exists

lambda do |wrapping_scope, nested_scopes|
  wrapping_scope.where(sql_for_any_exists(nested_scopes))
end
NestWithSumBlock =

Block used when nesting associations for a where_assoc_count

lambda do |wrapping_scope, nested_scopes|
  sql = sql_for_sum_of_counts(nested_scopes)
  wrapping_scope.unscope(:select).select(sql)
end
VALID_OPTIONS_KEYS =

List of available options, used for validation purposes.

ActiveRecordWhereAssoc.default_options.keys.freeze
RIGHT_INFINITE_RANGE_OPERATOR_MAP =

Doing (SQL) BETWEEN v1 AND v2, where v2 is infinite means (SQL) >= v1. However, we place the SQL on the right side, so the operator is flipped to become v1 <= (SQL). Doing (SQL) NOT BETWEEN v1 AND v2 where v2 is infinite means (SQL) < v1. However, we place the SQL on the right side, so the operator is flipped to become v1 > (SQL).

{ "=" => "<=", "<>" => ">" }.freeze
LEFT_INFINITE_RANGE_OPERATOR_MAP =

We flip the operators to use when it’s about the left-side of the range.

Hash[RIGHT_INFINITE_RANGE_OPERATOR_MAP.map { |k, v| [k, v.tr("<>", "><")] }].freeze
RANGE_OPERATOR_MAP =
{ "=" => "BETWEEN", "<>" => "NOT BETWEEN" }.freeze

Class Method Summary collapse

Class Method Details

._relations_on_association_recurse(record_class, association_names, given_conditions, options, last_assoc_block, nest_assocs_block) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/active_record_where_assoc/core_logic.rb', line 110

def self._relations_on_association_recurse(record_class, association_names, given_conditions, options, last_assoc_block, nest_assocs_block)
  if association_names.size > 1
    recursive_scope_block = lambda do |scope|
      nested_scope = _relations_on_association_recurse(scope,
                                                       association_names[1..-1],
                                                       given_conditions,
                                                       options,
                                                       last_assoc_block,
                                                       nest_assocs_block)
      nest_assocs_block.call(scope, nested_scope)
    end

    relations_on_one_association(record_class, association_names.first, nil, options, recursive_scope_block, nest_assocs_block)
  else
    relations_on_one_association(record_class, association_names.first, given_conditions, options, last_assoc_block, nest_assocs_block)
  end
end

.actually_has_and_belongs_to_many?(reflection) ⇒ Boolean

Return true if #user_defined_actual_source_reflection is a has_and_belongs_to_many

Returns:

  • (Boolean)


473
474
475
# File 'lib/active_record_where_assoc/core_logic.rb', line 473

def self.actually_has_and_belongs_to_many?(reflection)
  has_and_belongs_to_many?(user_defined_actual_source_reflection(reflection))
end

.apply_proc_scope(relation, proc_scope) ⇒ Object

Apply a proc used as scope If it can’t receive arguments, call the proc with self set to the relation If it can receive arguments, call the proc the relation passed as argument



385
386
387
388
389
390
391
# File 'lib/active_record_where_assoc/core_logic.rb', line 385

def self.apply_proc_scope(relation, proc_scope)
  if proc_scope.arity == 0
    relation.instance_exec(nil, &proc_scope) || relation
  else
    proc_scope.call(relation) || relation
  end
end

.assoc_exists_sql(record_class, association_names, given_conditions, options, &block) ⇒ Object

Returns the SQL condition to check if the specified association of the record_class exists (has records).

See RelationReturningMethods#where_assoc_exists or SqlReturningMethods#assoc_exists_sql for usage details.



63
64
65
66
# File 'lib/active_record_where_assoc/core_logic.rb', line 63

def self.assoc_exists_sql(record_class, association_names, given_conditions, options, &block)
  nested_relations = relations_on_association(record_class, association_names, given_conditions, options, block, NestWithExistsBlock)
  sql_for_any_exists(nested_relations)
end

.assoc_not_exists_sql(record_class, association_names, given_conditions, options, &block) ⇒ Object

Returns the SQL condition to check if the specified association of the record_class doesn’t exist (has no records).

See RelationReturningMethods#where_assoc_not_exists or SqlReturningMethods#assoc_not_exists_sql for usage details.



71
72
73
74
# File 'lib/active_record_where_assoc/core_logic.rb', line 71

def self.assoc_not_exists_sql(record_class, association_names, given_conditions, options, &block)
  nested_relations = relations_on_association(record_class, association_names, given_conditions, options, block, NestWithExistsBlock)
  "NOT #{sql_for_any_exists(nested_relations)}"
end

.assoc_scope_to_keep_lim_off_from(reflection) ⇒ Object



263
264
265
266
267
268
269
270
271
272
273
# File 'lib/active_record_where_assoc/core_logic.rb', line 263

def self.assoc_scope_to_keep_lim_off_from(reflection)
  # For :through associations, it's pretty hard/tricky to apply limit/offset/order of the
  # whole has_* :through. For now, we only apply those of the direct associations from one model
  # to another that the :through uses and we ignore the limit/offset/order from the scope of has_* :through.
  #
  # The exception is for has_and_belongs_to_many, which behind the scene, use a has_many :through.
  # For those, since we know there is no limits on the internal has_many and the belongs_to,
  # we can do a special case and handle their limit. This way, we can treat them the same way we treat
  # the other macros, we only apply the limit/offset/order of the deepest user-define association.
  user_defined_actual_source_reflection(reflection).scope
end

.build_alias_scope_for_recursive_association(reflection, poly_belongs_to_klass) ⇒ Object



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/active_record_where_assoc/core_logic.rb', line 393

def self.build_alias_scope_for_recursive_association(reflection, poly_belongs_to_klass)
  klass = poly_belongs_to_klass || reflection.klass
  table = klass.arel_table
  primary_key = klass.primary_key
  foreign_klass = reflection.send(:actual_source_reflection).active_record

  alias_scope = foreign_klass.base_class.unscoped
  alias_scope = alias_scope.from("#{table.name} #{ALIAS_TABLE.name}")

  primary_key_constraints =
    Array(primary_key).map do |a_primary_key|
      table[a_primary_key].eq(ALIAS_TABLE[a_primary_key])
    end

  alias_scope.where(primary_key_constraints.inject(&:and))
end

.check_reflection_validity!(reflection) ⇒ Object



489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'lib/active_record_where_assoc/core_logic.rb', line 489

def self.check_reflection_validity!(reflection)
  if ActiveRecordCompat.through_reflection?(reflection)
    # Copied from ActiveRecord
    if reflection.through_reflection.polymorphic?
      # Since deep_cover/builtin_takeover lacks some granularity,
      # it can sometimes happen that it won't display 100% coverage while a regular would
      # be 100%. This happens when multiple banches are on in a single line.
      # For this reason, I split this condition in 2
      if ActiveRecord.const_defined?(:HasOneAssociationPolymorphicThroughError)
        if reflection.has_one?
          raise ActiveRecord::HasOneAssociationPolymorphicThroughError.new(reflection.active_record.name, reflection)
        end
      end
      raise ActiveRecord::HasManyThroughAssociationPolymorphicThroughError.new(reflection.active_record.name, reflection)
    end
    check_reflection_validity!(reflection.through_reflection)
    check_reflection_validity!(reflection.source_reflection)
  end
end

.classes_with_scope_for_reflection(record_class, reflection, options) ⇒ Object



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/active_record_where_assoc/core_logic.rb', line 275

def self.classes_with_scope_for_reflection(record_class, reflection, options)
  actual_source_reflection = user_defined_actual_source_reflection(reflection)

  if poly_belongs_to?(actual_source_reflection)
    on_poly_belongs_to = option_value(options, :poly_belongs_to)

    if reflection.options[:source_type]
      [reflection.options[:source_type].safe_constantize].compact
    else
      case on_poly_belongs_to
      when :pluck
        model_for_ids = actual_source_reflection.active_record

        if model_for_ids.abstract_class
          # When the reflection is defined on an abstract model, we fallback to the model
          # on which this was called
          model_for_ids = record_class
        end

        class_names = model_for_ids.distinct.pluck(actual_source_reflection.foreign_type)
        class_names.compact.map!(&:safe_constantize).compact
      when Array, Hash
        array = on_poly_belongs_to.to_a
        bad_class = array.detect { |c, _p| !c.is_a?(Class) || !(c < ActiveRecord::Base) }
        if bad_class.is_a?(ActiveRecord::Base)
          raise ArgumentError, "Must receive the Class of the model, not an instance. This is wrong: #{bad_class.inspect}"
        elsif bad_class
          raise ArgumentError, "Expected #{bad_class.inspect} to be a subclass of ActiveRecord::Base"
        end
        array
      when :raise
        msg = String.new
        if actual_source_reflection == reflection
          msg << "Association #{reflection.name.inspect} is a polymorphic belongs_to. "
        else
          msg << "Association #{reflection.name.inspect} is a :through relation that uses a polymorphic belongs_to"
          msg << "#{actual_source_reflection.name.inspect} as source without without a source_type. "
        end
        msg << "This is not supported by ActiveRecord when doing joins, but it is by WhereAssoc. However, "
        msg << "you must pass the :poly_belongs_to option to specify what to do in this case.\n"
        msg << "See https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/RelationReturningMethods.html#module-ActiveRecordWhereAssoc::RelationReturningMethods-label-3Apoly_belongs_to+option"
        raise ActiveRecordWhereAssoc::PolymorphicBelongsToWithoutClasses, msg
      else
        if on_poly_belongs_to.is_a?(Class) && on_poly_belongs_to < ActiveRecord::Base
          [on_poly_belongs_to]
        else
          raise ArgumentError, "Received a bad value for :poly_belongs_to: #{on_poly_belongs_to.inspect}"
        end
      end
    end
  else
    [reflection.klass]
  end
end

.compare_assoc_count_sql(record_class, left_operand, operator, association_names, given_conditions, options, &block) ⇒ Object

Returns the SQL condition to check if the specified association of the record_class has the desired number of records.

See RelationReturningMethods#where_assoc_count or SqlReturningMethods#compare_assoc_count_sql for usage details.



95
96
97
98
99
# File 'lib/active_record_where_assoc/core_logic.rb', line 95

def self.compare_assoc_count_sql(record_class, left_operand, operator, association_names, given_conditions, options, &block)
  right_sql = only_assoc_count_sql(record_class, association_names, given_conditions, options, &block)

  sql_for_count_operator(left_operand, operator, right_sql)
end

.fetch_reflection(relation_klass, association_name) ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
# File 'lib/active_record_where_assoc/core_logic.rb', line 187

def self.fetch_reflection(relation_klass, association_name)
  association_name = ActiveRecordCompat.normalize_association_name(association_name)
  reflection = relation_klass._reflections[association_name]

  if reflection.nil?
    # Need to use build because this exception expects a record...
    raise ActiveRecord::AssociationNotFoundError.new(relation_klass.new, association_name)
  end

  reflection
end

.has_and_belongs_to_many?(reflection) ⇒ Boolean

Because we work using Model._reflections, we don’t actually get the :has_and_belongs_to_many. Instead, we get a has_many :through, which is was ActiveRecord created behind the scene. This code detects that a :through is actually a has_and_belongs_to_many.

Returns:

  • (Boolean)


463
464
465
466
# File 'lib/active_record_where_assoc/core_logic.rb', line 463

def self.has_and_belongs_to_many?(reflection) # rubocop:disable Naming/PredicateName
  parent = ActiveRecordCompat.parent_reflection(reflection)
  parent && parent.macro == :has_and_belongs_to_many
end

.initial_scopes_from_reflection(record_class, reflection_chain, assoc_scopes, options) ⇒ Object

Can return multiple pairs for polymorphic belongs_to, one per table to look into



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/active_record_where_assoc/core_logic.rb', line 200

def self.initial_scopes_from_reflection(record_class, reflection_chain, assoc_scopes, options)
  reflection = reflection_chain.first
  actual_source_reflection = user_defined_actual_source_reflection(reflection)

  on_poly_belongs_to = option_value(options, :poly_belongs_to) if poly_belongs_to?(actual_source_reflection)

  classes_with_scope = classes_with_scope_for_reflection(record_class, reflection, options)

  assoc_scope_allowed_lim_off = assoc_scope_to_keep_lim_off_from(reflection)

  classes_with_scope.map do |klass, klass_scope|
    current_scope = klass.default_scoped

    if actually_has_and_belongs_to_many?(actual_source_reflection)
      # has_and_belongs_to_many, behind the scene has a secret model and uses a has_many through.
      # This is the first of those two secret has_many through.
      #
      # In order to handle limit, offset, order correctly on has_and_belongs_to_many,
      # we must do both this reflection and the next one at the same time.
      # Think of it this way, if you have limit 3:
      #   Apply only on 1st step: You check that any of 2nd step for the first 3 of 1st step match
      #   Apply only on 2nd step: You check that any of the first 3 of second step match for any 1st step
      #   Apply over both (as we do): You check that only the first 3 of doing both step match,

      # To create the join, simply using next_reflection.klass.default_scoped.joins(reflection.name)
      # would be great, except we cannot add a given_conditions afterward because we are on the wrong "base class",
      # and we can't do #merge because of the LEW crap.
      # So we must do the joins ourself!
      _wrapper, sub_join_contraints = wrapper_and_join_constraints(record_class, reflection)
      next_reflection = reflection_chain[1]

      current_scope = current_scope.joins(<<-SQL)
          INNER JOIN #{next_reflection.klass.quoted_table_name} ON #{sub_join_contraints.to_sql}
      SQL

      alias_scope, join_constraints = wrapper_and_join_constraints(record_class, next_reflection, habtm_other_reflection: reflection)
    elsif on_poly_belongs_to
      alias_scope, join_constraints = wrapper_and_join_constraints(record_class, reflection, poly_belongs_to_klass: klass)
    else
      alias_scope, join_constraints = wrapper_and_join_constraints(record_class, reflection)
    end

    assoc_scopes.each do |callable|
      relation = klass.unscoped.instance_exec(nil, &callable)

      if callable != assoc_scope_allowed_lim_off
        # I just want to remove the current values without screwing things in the merge below
        # so we cannot use #unscope
        relation.limit_value = nil
        relation.offset_value = nil
        relation.order_values = []
      end

      # Need to use merge to replicate the Last Equality Wins behavior of associations
      # https://github.com/rails/rails/issues/7365
      # See also the test/tests/wa_last_equality_wins_test.rb for an explanation
      current_scope = current_scope.merge(relation)
    end

    [alias_scope, current_scope.where(join_constraints), klass_scope]
  end
end

.only_assoc_count_sql(record_class, association_names, given_conditions, options, &block) ⇒ Object

This does not return an SQL condition. Instead, it returns only the SQL to count the number of records for the specified association.

See SqlReturningMethods#only_assoc_count_sql for usage details.



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/active_record_where_assoc/core_logic.rb', line 80

def self.only_assoc_count_sql(record_class, association_names, given_conditions, options, &block)
  deepest_scope_mod = lambda do |deepest_scope|
    deepest_scope = apply_proc_scope(deepest_scope, block) if block

    deepest_scope.unscope(:select).select("COUNT(*)")
  end

  nested_relations = relations_on_association(record_class, association_names, given_conditions, options, deepest_scope_mod, NestWithSumBlock)
  nested_relations = nested_relations.reject { |rel| ActiveRecordCompat.null_relation?(rel) }
  nested_relations.map { |nr| "COALESCE((#{nr.to_sql}), 0)" }.join(" + ").presence || "0"
end

.option_value(options, key) ⇒ Object

Gets the value from the options or fallback to default



56
57
58
# File 'lib/active_record_where_assoc/core_logic.rb', line 56

def self.option_value(options, key)
  options.fetch(key) { ActiveRecordWhereAssoc.default_options[key] }
end

.poly_belongs_to?(reflection) ⇒ Boolean

Returns:

  • (Boolean)


468
469
470
# File 'lib/active_record_where_assoc/core_logic.rb', line 468

def self.poly_belongs_to?(reflection)
  reflection.macro == :belongs_to && reflection.options[:polymorphic]
end

.process_association_step_limits(current_scope, reflection, relation_klass, options) ⇒ Object

Creates a sub_query that the current_scope gets nested into if there is limit/offset to apply



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/active_record_where_assoc/core_logic.rb', line 331

def self.process_association_step_limits(current_scope, reflection, relation_klass, options)
  if user_defined_actual_source_reflection(reflection).macro == :belongs_to || option_value(options, :ignore_limit)
    return current_scope.unscope(:limit, :offset, :order)
  end

  # No need to do transformations if this is already a NullRelation
  return current_scope if ActiveRecordCompat.null_relation?(current_scope)

  current_scope = current_scope.limit(1) if reflection.macro == :has_one

  # Order is useless without either limit or offset
  current_scope = current_scope.unscope(:order) if !current_scope.limit_value && !current_scope.offset_value

  return current_scope unless current_scope.limit_value || current_scope.offset_value
  if %w(mysql mysql2).include?(relation_klass.connection.adapter_name.downcase)
    msg = String.new
    msg << "Associations and default_scopes with a limit or offset are not supported for MySQL (this includes has_many). "
    msg << "Use ignore_limit: true to ignore both limit and offset, and treat has_one like has_many. "
    msg << "See https://github.com/MaxLap/activerecord_where_assoc#mysql-doesnt-support-sub-limit for details."
    raise MySQLDoesntSupportSubLimitError, msg
  end

  # We only check the records that would be returned by the associations if called on the model. If:
  # * the association has a limit in its lambda
  # * the default scope of the model has a limit
  # * the association is a has_one
  # Then not every records that match a naive join would be returned. So we first restrict the query to
  # only the records that would be in the range of limit and offset.
  #
  # Note that if the #where_assoc_* block adds a limit or an offset, it has no effect. This is intended.
  # An argument could be made for it to maybe make sense for #where_assoc_count, not sure why that would
  # be useful.

  if reflection.klass.table_name.include?(".") || option_value(options, :never_alias_limit)
    # We use unscoped to avoid duplicating the conditions in the query, which is noise. (unless it
    # could helps the query planner of the DB, if someone can show it to be worth it, then this can be changed.)
    if reflection.klass.primary_key.is_a?(Array)
      raise NeverAliasLimitDoesntWorkWithCompositePrimaryKeysError, "Sorry, it just doesn't work..."
    end

    reflection.klass.unscoped.where(reflection.klass.primary_key.to_sym => current_scope)
  else
    # This works as long as the table_name doesn't have a schema/database, since we need to use an alias
    # with the table name to make scopes and everything else work as expected.

    # We use unscoped to avoid duplicating the conditions in the query, which is noise. (unless if it
    # could helps the query planner of the DB, if someone can show it to be worth it, then this can be changed.)
    reflection.klass.unscoped.from("(#{current_scope.to_sql}) #{reflection.klass.table_name}")
  end
end

.relations_on_association(record_class, association_names, given_conditions, options, last_assoc_block, nest_assocs_block) ⇒ Object

Returns relations on the associated model meant to be embedded in a query Will only return more than one association when there are polymorphic belongs_to association_names: can be an array of association names or a single one



104
105
106
107
108
# File 'lib/active_record_where_assoc/core_logic.rb', line 104

def self.relations_on_association(record_class, association_names, given_conditions, options, last_assoc_block, nest_assocs_block)
  validate_options(options)
  association_names = Array.wrap(association_names)
  _relations_on_association_recurse(record_class, association_names, given_conditions, options, last_assoc_block, nest_assocs_block)
end

.relations_on_one_association(record_class, association_name, given_conditions, options, last_assoc_block, nest_assocs_block) ⇒ Object

Returns relations on the associated model meant to be embedded in a query Will return more than one association only for polymorphic belongs_to



130
131
132
133
134
135
136
137
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
# File 'lib/active_record_where_assoc/core_logic.rb', line 130

def self.relations_on_one_association(record_class, association_name, given_conditions, options, last_assoc_block, nest_assocs_block)
  final_reflection = fetch_reflection(record_class, association_name)

  check_reflection_validity!(final_reflection)

  nested_scopes = nil
  current_scopes = nil

  # Chain deals with through stuff
  # We will start with the reflection that points on the final model, and slowly move back to the reflection
  # that points on the model closest to self
  # Each step, we get all of the scoping lambdas that were defined on associations that apply for
  # the reflection's target
  # Basically, we start from the deepest part of the query and wrap it up
  reflection_chain, constraints_chain = ActiveRecordCompat.chained_reflection_and_chained_constraints(final_reflection)
  skip_next = false

  reflection_chain.each_with_index do |reflection, i|
    if skip_next
      skip_next = false
      next
    end

    # the 2nd part of has_and_belongs_to_many is handled at the same time as the first.
    skip_next = true if actually_has_and_belongs_to_many?(reflection)

    init_scopes = initial_scopes_from_reflection(record_class, reflection_chain[i..-1], constraints_chain[i], options)
    current_scopes = init_scopes.map do |alias_scope, current_scope, klass_scope|
      current_scope = process_association_step_limits(current_scope, reflection, record_class, options)

      if i.zero?
        current_scope = current_scope.where(given_conditions) if given_conditions
        if klass_scope
          if klass_scope.respond_to?(:call)
            current_scope = apply_proc_scope(current_scope, klass_scope)
          else
            current_scope = current_scope.where(klass_scope)
          end
        end
        current_scope = apply_proc_scope(current_scope, last_assoc_block) if last_assoc_block
      end

      # Those make no sense since at this point, we are only limiting the value that would match using conditions
      # Those could have been added by the received block, so just remove them
      current_scope = current_scope.unscope(:limit, :order, :offset)

      current_scope = nest_assocs_block.call(current_scope, nested_scopes) if nested_scopes
      current_scope = nest_assocs_block.call(alias_scope, current_scope) if alias_scope
      current_scope
    end

    nested_scopes = current_scopes
  end

  current_scopes
end

.sql_for_any_exists(relations) ⇒ Object

Returns the SQL for checking if any of the received relation exists. Uses a OR if there are multiple relations.

> “EXISTS (SELECT… relation1) OR EXISTS (SELECT… relation2)”



14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/active_record_where_assoc/core_logic.rb', line 14

def self.sql_for_any_exists(relations)
  relations = [relations] unless relations.is_a?(Array)
  relations = relations.reject { |rel| ActiveRecordCompat.null_relation?(rel) }
  sqls = relations.map { |rel| "EXISTS (#{rel.select('1').to_sql})" }
  if sqls.size > 1
    "(#{sqls.join(" OR ")})" # Parens needed when embedding the sql in a `where`, because the OR could make things wrong
  elsif sqls.size == 1
    sqls.first
  else
    "0=1"
  end
end

.sql_for_count_operator(left_operand, operator, right_sql) ⇒ Object



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
# File 'lib/active_record_where_assoc/core_logic.rb', line 519

def self.sql_for_count_operator(left_operand, operator, right_sql)
  operator = case operator.to_s
             when "=="
               "="
             when "!="
               "<>"
             else
               operator.to_s
             end

  return "(#{left_operand}) #{operator} #{right_sql}" unless left_operand.is_a?(Range)

  unless %w(= <>).include?(operator)
    raise ArgumentError, "Operator should be one of '==', '=', '<>' or '!=' when using a Range not: #{operator.inspect}"
  end

  v1 = left_operand.begin
  v2 = left_operand.end || Float::INFINITY

  # We are doing a count and summing them, the lowest possible is 0, so just use that instead of changing the SQL used.
  v1 = 0 if v1 == -Float::INFINITY

  return sql_for_count_operator(v1, RIGHT_INFINITE_RANGE_OPERATOR_MAP.fetch(operator), right_sql) if v2 == Float::INFINITY

  # Its int or a rounded float. Since we are comparing to integer values (count), exclude_end? just means -1
  v2 -= 1 if left_operand.exclude_end? && v2 % 1 == 0

  "#{right_sql} #{RANGE_OPERATOR_MAP.fetch(operator)} #{v1} AND #{v2}"
end

.sql_for_sum_of_counts(relations) ⇒ Object

Returns the SQL for getting the sum of of the received relations

> “SUM((SELECT… relation1)) + SUM((SELECT… relation2))”



34
35
36
37
38
39
# File 'lib/active_record_where_assoc/core_logic.rb', line 34

def self.sql_for_sum_of_counts(relations)
  relations = [relations] unless relations.is_a?(Array)
  relations = relations.reject { |rel| ActiveRecordCompat.null_relation?(rel) }
  # Need the double parentheses
  relations.map { |rel| "SUM((#{rel.to_sql}))" }.join(" + ").presence || "0"
end

.user_defined_actual_source_reflection(reflection) ⇒ Object

Returns the deepest user-defined reflection using source_reflection. This is different from #send(:actual_source_reflection) because it stops on has_and_belongs_to_many associations, where as actual_source_reflection would continue down to the belongs_to that is used internally.



481
482
483
484
485
486
487
# File 'lib/active_record_where_assoc/core_logic.rb', line 481

def self.user_defined_actual_source_reflection(reflection)
  loop do
    return reflection if reflection == reflection.source_reflection
    return reflection if has_and_belongs_to_many?(reflection)
    reflection = reflection.source_reflection
  end
end

.validate_options(options) ⇒ Object

Raises:

  • (ArgumentError)


50
51
52
53
# File 'lib/active_record_where_assoc/core_logic.rb', line 50

def self.validate_options(options)
  invalid_keys = options.keys - VALID_OPTIONS_KEYS
  raise ArgumentError, "Invalid option keys received: #{invalid_keys.join(', ')}" unless invalid_keys.empty?
end

.wrapper_and_join_constraints(record_class, reflection, options = {}) ⇒ Object



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# File 'lib/active_record_where_assoc/core_logic.rb', line 410

def self.wrapper_and_join_constraints(record_class, reflection, options = {})
  poly_belongs_to_klass = options[:poly_belongs_to_klass]
  join_keys = ActiveRecordCompat.join_keys(reflection, poly_belongs_to_klass)

  key = join_keys.key
  foreign_key = join_keys.foreign_key

  table = (poly_belongs_to_klass || reflection.klass).arel_table
  foreign_klass = reflection.send(:actual_source_reflection).active_record
  if foreign_klass.abstract_class
    # When the reflection is defined on an abstract model, we fallback to the model
    # on which this was called
    foreign_klass = record_class
  end

  foreign_table = foreign_klass.arel_table

  habtm_other_reflection = options[:habtm_other_reflection]
  habtm_other_table = habtm_other_reflection.klass.arel_table if habtm_other_reflection

  if (habtm_other_table || table).name == foreign_table.name
    alias_scope = build_alias_scope_for_recursive_association(habtm_other_reflection || reflection, poly_belongs_to_klass)
    foreign_table = ALIAS_TABLE
  end

  constraint_keys = Array.wrap(key)
  constraint_foreign_keys = Array.wrap(foreign_key)
  constraint_key_map = constraint_keys.zip(constraint_foreign_keys)

  primary_foreign_key_constraints =
    constraint_key_map.map do |primary_and_foreign_keys|
      a_primary_key, a_foreign_key = primary_and_foreign_keys

      table[a_primary_key].eq(foreign_table[a_foreign_key])
    end

  constraints = primary_foreign_key_constraints.inject(&:and)

  if reflection.type
    # Handling of the polymorphic has_many/has_one's type column
    constraints = constraints.and(table[reflection.type].eq(foreign_klass.base_class.name))
  end

  if poly_belongs_to_klass
    constraints = constraints.and(foreign_table[reflection.foreign_type].eq(poly_belongs_to_klass.base_class.name))
  end

  [alias_scope, constraints]
end