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)


491
492
493
# File 'lib/active_record_where_assoc/core_logic.rb', line 491

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



403
404
405
406
407
408
409
# File 'lib/active_record_where_assoc/core_logic.rb', line 403

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



273
274
275
276
277
278
279
280
281
282
283
# File 'lib/active_record_where_assoc/core_logic.rb', line 273

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



411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'lib/active_record_where_assoc/core_logic.rb', line 411

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



507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
# File 'lib/active_record_where_assoc/core_logic.rb', line 507

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



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
329
330
331
332
333
334
335
336
337
338
# File 'lib/active_record_where_assoc/core_logic.rb', line 285

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



197
198
199
200
201
202
203
204
205
206
207
# File 'lib/active_record_where_assoc/core_logic.rb', line 197

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)


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

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



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
262
263
264
265
266
267
268
269
270
271
# File 'lib/active_record_where_assoc/core_logic.rb', line 210

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)


486
487
488
# File 'lib/active_record_where_assoc/core_logic.rb', line 486

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



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
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/active_record_where_assoc/core_logic.rb', line 341

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

  if !current_scope.offset_value
    if current_scope.limit_value
      join_keys = ActiveRecordCompat.join_keys(reflection, nil)
      # #join_keys is inverted... the foreign key is on the "source" table, and the key is on the "target" table...
      # Everything is so complicated in ActiveRecord.
      current_scope = current_scope.unscope(:limit) if ActiveRecordCompat.has_unique_index?(current_scope.model, join_keys.key)
    end

    # Order is useless without either limit or offset
    return current_scope.unscope(:order) if !current_scope.limit_value
  end

  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
186
187
188
189
190
191
192
193
194
195
# 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|
      if i.zero?
        if given_conditions || klass_scope || last_assoc_block || current_scope.offset_value || nest_assocs_block == NestWithSumBlock
          # In the deepest layer, the limit & offset complexities only matter when:
          # * There is a condition to apply
          # * There is an offset (which is a form of filtering)
          # * We are counting the total matches
          # Since last_assoc_block is always set except for the deepest association, and is only unset for the deepest layer if
          # there is no condition given, using it as part of the condition does a lot of work here.
          current_scope = process_association_step_limits(current_scope, reflection, record_class, options)
        end

        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
      else
        current_scope = process_association_step_limits(current_scope, reflection, record_class, options)
      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



537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# File 'lib/active_record_where_assoc/core_logic.rb', line 537

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.



499
500
501
502
503
504
505
# File 'lib/active_record_where_assoc/core_logic.rb', line 499

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



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
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
# File 'lib/active_record_where_assoc/core_logic.rb', line 428

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