Class: ActiveRecord::Relation

Inherits:
Object
  • Object
show all
Defined in:
lib/brick/extensions.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#_brick_chainsObject (readonly)

Returns the value of attribute _brick_chains.



297
298
299
# File 'lib/brick/extensions.rb', line 297

def _brick_chains
  @_brick_chains
end

Instance Method Details

#_arel_alias_namesObject

INSTANCE STUFF



360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/brick/extensions.rb', line 360

def _arel_alias_names
  # %%% If with Rails 3.1 and older you get "NoMethodError: undefined method `eq' for nil:NilClass"
  # when trying to call relation.arel, then somewhere along the line while navigating a has_many
  # relationship it can't find the proper foreign key.
  core = arel.ast.cores.first
  # Accommodate AR < 3.2
  if core.froms.is_a?(Arel::Table)
    # All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource
    _recurse_arel(core.source)
  else
    # With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
    _recurse_arel(core.froms)
  end
end

#_recurse_arel(piece, prefix = '') ⇒ Object

CLASS STUFF



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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/brick/extensions.rb', line 300

def _recurse_arel(piece, prefix = '')
  names = []
  # Our JOINs mashup of nested arrays and hashes
  # binding.pry if defined?(@arel)
  case piece
  when Array
    names += piece.inject([]) { |s, v| s + _recurse_arel(v, prefix) }
  when Hash
    names += piece.inject([]) do |s, v|
      new_prefix = "#{prefix}#{v.first}_"
      s << [v.last.shift, new_prefix]
      s + _recurse_arel(v.last, new_prefix)
    end

  # ActiveRecord AREL objects
  when Arel::Nodes::Join # INNER or OUTER JOIN
    # rubocop:disable Style/IdenticalConditionalBranches
    if piece.right.is_a?(Arel::Table) # Came in from AR < 3.2?
      # Arel 2.x and older is a little curious because these JOINs work "back to front".
      # The left side here is either another earlier JOIN, or at the end of the whole tree, it is
      # the first table.
      names += _recurse_arel(piece.left)
      # The right side here at the top is the very last table, and anywhere else down the tree it is
      # the later "JOIN" table of this pair.  (The table that comes after all the rest of the JOINs
      # from the left side.)
      names << [piece.right._arel_table_type, (piece.right.table_alias || piece.right.name)]
    else # "Normal" setup, fed from a JoinSource which has an array of JOINs
      # The left side is the "JOIN" table
      names += _recurse_arel(table = piece.left)
      # The expression on the right side is the "ON" clause
      # on = piece.right.expr
      # # Find the table which is not ourselves, and thus must be the "path" that led us here
      # parent = piece.left == on.left.relation ? on.right.relation : on.left.relation
      # binding.pry if piece.left.is_a?(Arel::Nodes::TableAlias)
      if table.is_a?(Arel::Nodes::TableAlias)
        alias_name = table.right
        table = table.left
      end
      (_brick_chains[table._arel_table_type] ||= []) << (alias_name || table.table_alias || table.name)
    end
    # rubocop:enable Style/IdenticalConditionalBranches
  when Arel::Table # Table
    names << [piece._arel_table_type, (piece.table_alias || piece.name)]
  when Arel::Nodes::TableAlias # Alias
    # Can get the real table name from:  self._recurse_arel(piece.left)
    names << [piece.left._arel_table_type, piece.right.to_s] # This is simply a string; the alias name itself
  when Arel::Nodes::JoinSource # Leaving this until the end because AR < 3.2 doesn't know at all about JoinSource!
    # Spin up an empty set of Brick alias name chains at the start
    @_brick_chains = {}
    # The left side is the "FROM" table
    names << (this_name = [piece.left._arel_table_type, (piece.left.table_alias || piece.left.name)])
    # # Do not currently need the root "FROM" table in our list of chains
    # (_brick_chains[this_name.first] ||= []) << this_name.last
    # The right side is an array of all JOINs
    piece.right.each { |join| names << _recurse_arel(join) }
  end
  names
end

#brick_select(params, selects = nil, order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new) ⇒ Object



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
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
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
# File 'lib/brick/extensions.rb', line 375

def brick_select(params, selects = nil, order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
  is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2'
  is_distinct = nil
  wheres = {}
  params.each do |k, v|
    next if ['_brick_schema', '_brick_order'].include?(k)

    case (ks = k.split('.')).length
    when 1
      next unless klass.column_names.any?(k) || klass._brick_get_fks.include?(k)
    when 2
      assoc_name = ks.first.to_sym
      # Make sure it's a good association name and that the model has that column name
      next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)

      join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
      is_distinct = true
      distinct!
    end
    wheres[k] = v.split(',')
  end

  # %%% Skip the metadata columns
  if selects&.empty? # Default to all columns
    tbl_no_schema = table.name.split('.').last
    columns.each do |col|
      col_alias = " AS _#{col.name}" if (col_name = col.name) == 'class'
      selects << if is_mysql
                   "`#{tbl_no_schema}`.`#{col_name}`#{col_alias}"
                 else
                   # Postgres can not use DISTINCT with any columns that are XML, so for any of those just convert to text
                   cast_as_text = '::text' if is_distinct && Brick.relations[klass.table_name]&.[](:cols)&.[](col.name)&.first&.start_with?('xml')
                   "\"#{tbl_no_schema}\".\"#{col_name}\"#{cast_as_text}#{col_alias}"
                 end
    end
  end

  if join_array.present?
    left_outer_joins!(join_array)
    # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
    (rel_dupe = dup)._arel_alias_names
    core_selects = selects.dup
    chains = rel_dupe._brick_chains
    id_for_tables = Hash.new { |h, k| h[k] = [] }
    field_tbl_names = Hash.new { |h, k| h[k] = {} }
    used_col_aliases = {} # Used to make sure there is not a name clash
    bt_columns = klass._br_bt_descrip.each_with_object([]) do |v, s|
      v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
        next if chains[k1].nil?

        tbl_name = (field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])).split('.').last
        field_tbl_name = nil
        v1.map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
          field_tbl_name = (field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])).split('.').last

          # Postgres can not use DISTINCT with any columns that are XML, so for any of those just convert to text
          is_xml = is_distinct && Brick.relations[sel_col.first.table_name]&.[](:cols)&.[](sel_col.last)&.first&.start_with?('xml')
          # If it's not unique then also include the belongs_to association name before the column name
          if used_col_aliases.key?(col_alias = "_brfk_#{v.first}__#{sel_col.last}")
            col_alias = "_brfk_#{v.first}__#{v1[idx][-2..-1].map(&:to_s).join('__')}"
          end
          selects << if is_mysql
                       "`#{field_tbl_name}`.`#{sel_col.last}` AS `#{col_alias}`"
                     else
                       "\"#{field_tbl_name}\".\"#{sel_col.last}\"#{'::text' if is_xml} AS \"#{col_alias}\""
                     end
          used_col_aliases[col_alias] = nil
          v1[idx] << col_alias
        end

        unless id_for_tables.key?(v.first)
          # Accommodate composite primary key by allowing id_col to come in as an array
          ((id_col = k1.primary_key).is_a?(Array) ? id_col : [id_col]).each do |id_part|
            id_for_tables[v.first] << if id_part
                                        selects << if is_mysql
                                                     "#{"`#{tbl_name}`.`#{id_part}`"} AS `#{(id_alias = "_brfk_#{v.first}__#{id_part}")}`"
                                                   else
                                                     "#{"\"#{tbl_name}\".\"#{id_part}\""} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
                                                   end
                                        id_alias
                                      end
          end
          v1 << id_for_tables[v.first].compact
        end
      end
    end
    join_array.each do |assoc_name|
      # %%% Need to support {user: :profile}
      next unless assoc_name.is_a?(Symbol)

      table_alias = shift_or_first(chains[klass = reflect_on_association(assoc_name)&.klass])
      _assoc_names[assoc_name] = [table_alias, klass]
    end
  end
  # Add derived table JOIN for the has_many counts
  klass._br_hm_counts.each do |k, hm|
    associative = nil
    count_column = if hm.options[:through]
                     hm.foreign_key if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key)
                   else
                     fk_col = hm.foreign_key
                     poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
                     pk = hm.klass.primary_key
                     (pk.is_a?(Array) ? pk.first : pk) || '*'
                   end
    next unless count_column # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof

    tbl_alias = is_mysql ? "`_br_#{hm.name}`" : "\"_br_#{hm.name}\""
    pri_tbl = hm.active_record
    pri_tbl_name = is_mysql ? "`#{pri_tbl.table_name}`" : "\"#{pri_tbl.table_name.gsub('.', '"."')}\""
    on_clause = []
    if fk_col.is_a?(Array) # Composite key?
      fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{tbl_alias}.#{fk_col_part} = #{pri_tbl_name}.#{pri_tbl.primary_key[idx]}" }
      selects = fk_col.dup
    else
      selects = [fk_col]
      on_clause << "#{tbl_alias}.#{fk_col} = #{pri_tbl_name}.#{pri_tbl.primary_key}"
    end
    if poly_type
      selects << poly_type
      on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
    end
    hm_table_name = is_mysql ? "`#{associative&.table_name || hm.klass.table_name}`" : "\"#{(associative&.table_name || hm.klass.table_name).gsub('.', '"."')}\""
    join_clause = "LEFT OUTER
JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}#{count_column
      }) AS _ct_ FROM #{hm_table_name} GROUP BY #{(1..selects.length).to_a.join(', ')}) AS #{tbl_alias}"
    joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
  end
  where!(wheres) unless wheres.empty?
  # Must parse the order_by and see if there are any symbols which refer to BT associations
  # as they must be expanded to find the corresponding _br_model__column naming for each.
  if order_by.present?
    final_order_by = *order_by.each_with_object([]) do |v, s|
      if v.is_a?(Symbol)
        # Add the ordered series of columns derived from the BT based on its DSL
        if (bt_cols = klass._br_bt_descrip[v])
          bt_cols.values.each do |v1|
            v1.each { |v2| s << v2.last if v2.length > 1 }
          end
        else
          s << v
        end
      else # String stuff just comes straight through
        s << v
      end
    end
    order!(*final_order_by)
  end
  limit!(1000) # Don't want to get too carried away just yet
  wheres unless wheres.empty? # Return the specific parameters that we did use
end