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

#_arel_applied_aliasesObject (readonly)

Returns the value of attribute _arel_applied_aliases.



379
380
381
# File 'lib/brick/extensions.rb', line 379

def _arel_applied_aliases
  @_arel_applied_aliases
end

#_brick_chainsObject (readonly)

Returns the value of attribute _brick_chains.



379
380
381
# File 'lib/brick/extensions.rb', line 379

def _brick_chains
  @_brick_chains
end

Instance Method Details

#_arel_alias_namesObject

INSTANCE STUFF



442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/brick/extensions.rb', line 442

def _arel_alias_names
  @_arel_applied_aliases = []
  # %%% 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



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
# File 'lib/brick/extensions.rb', line 382

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)
        @_arel_applied_aliases << (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 = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new) ⇒ Object



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
526
527
528
529
530
531
532
533
534
535
536
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
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
# File 'lib/brick/extensions.rb', line 458

def brick_select(params, selects = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
  is_add_bts = is_add_hms = true

  # Build out cust_cols, bt_descrip and hm_counts now so that they are available on the
  # model early in case the user wants to do an ORDER BY based on any of that.
  model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms

  is_postgres = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
  is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2'
  is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer'
  is_distinct = nil
  wheres = {}
  params.each do |k, v|
    next if ['_brick_schema', '_brick_order', 'controller', 'action'].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
    # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
    #     ActiveRecord::StatementInvalid (TinyTds::Error: DBPROCESS is dead or not enabled)
    #     Relevant info here:  https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/402
    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}"
                 elsif is_postgres || is_mssql
                   # 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}"
                 elsif col.type # Could be Sqlite or Oracle
                   if col_alias || !(/^[a-z0-9_]+$/ =~ col_name)
                     "#{tbl_no_schema}.\"#{col_name}\"#{col_alias}"
                   else
                     "#{tbl_no_schema}.#{col_name}"
                   end
                 else # Oracle with a custom data type
                   typ = col.sql_type
                   "'<#{typ.end_with?('_TYP') ? typ[0..-5] : typ}>' AS #{col.name}"
                 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

    # CUSTOM COLUMNS
    # ==============
    klass._br_cust_cols.each do |k, cc|
      if rel_dupe.respond_to?(k) # Name already taken?
        # %%% Use ensure_unique here in this kind of fashion:
        # cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
        # binding.pry
        next
      end

      cc.first.each do |cc_part|
        dest_klass = cc_part[0..-2].inject(klass) { |kl, cc_part_term| kl.reflect_on_association(cc_part_term).klass }
        tbl_name = (field_tbl_names[k][cc_part.last] ||= shift_or_first(chains[dest_klass])).split('.').last
        # Deal with the conflict if there are two parts in the custom column named the same,
        # "category.name" and "product.name" for instance will end up with aliases of "name"
        # and "product__name".
        cc_part_idx = cc_part.length - 1
        while cc_part_idx > 0 &&
              (col_alias = "br_cc_#{k}__#{cc_part[cc_part_idx..-1].map(&:to_s).join('__')}") &&
              used_col_aliases.key?(col_alias)
          cc_part_idx -= 1
        end
        selects << "#{tbl_name}.#{cc_part.last} AS #{col_alias}"
        cc_part << col_alias
        used_col_aliases[col_alias] = nil
      end
    end

    klass._br_bt_descrip.each do |v|
      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
        # If it's Oracle, quote any AREL aliases that had been applied
        tbl_name = "\"#{tbl_name}\"" if ::Brick.is_oracle && rel_dupe._arel_applied_aliases.include?(tbl_name)
        field_tbl_name = nil
        v1.map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
          # unless chains[sel_col.first]
          #   puts 'You might have some bogus DSL in your brick.rb file'
          #   next
          # end
          field_tbl_name = (field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])).split('.').last
          # If it's Oracle, quote any AREL aliases that had been applied
          field_tbl_name = "\"#{field_tbl_name}\"" if ::Brick.is_oracle && rel_dupe._arel_applied_aliases.include?(field_tbl_name)

          # 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 = "br_fk_#{v.first}__#{sel_col.last}")
            col_alias = "br_fk_#{v.first}__#{v1[idx][-2..-1].map(&:to_s).join('__')}"
          end
          selects << if is_mysql
                       "`#{field_tbl_name}`.`#{sel_col.last}` AS `#{col_alias}`"
                     elsif is_postgres
                       "\"#{field_tbl_name}\".\"#{sel_col.last}\"#{'::text' if is_xml} AS \"#{col_alias}\""
                     elsif is_mssql
                       "\"#{field_tbl_name}\".\"#{sel_col.last}\" AS \"#{col_alias}\""
                     else
                       "#{field_tbl_name}.#{sel_col.last} 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 = "br_fk_#{v.first}__#{id_part}")}`"
                                                   elsif is_postgres || is_mssql
                                                     "#{"\"#{tbl_name}\".\"#{id_part}\""} AS \"#{(id_alias = "br_fk_#{v.first}__#{id_part}")}\""
                                                   else
                                                     "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "br_fk_#{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
  nix = []
  klass._br_hm_counts.each do |k, hm|
    count_column = if hm.options[:through]
                     # Build the chain of JOINs going to the final destination HMT table
                     # (Usually just one JOIN, but could be many.)
                     hmt_assoc = hm
                     x = []
                     x.unshift(hmt_assoc) while hmt_assoc.options[:through] && (hmt_assoc = klass.reflect_on_association(hmt_assoc.options[:through]))
                     from_clause = +"#{x.first.table_name} br_t0"
                     fk_col = x.shift.foreign_key
                     link_back = [klass.primary_key] # %%% Inverse path back to the original object -- used to build out a link with a filter
                     idx = 0
                     bail_out = nil
                     x.map do |a|
                       from_clause << "\n LEFT OUTER JOIN #{a.table_name} br_t#{idx += 1} "
                       from_clause << if (src_ref = a.source_reflection).macro == :belongs_to
                                        "ON br_t#{idx}.id = br_t#{idx - 1}.#{a.foreign_key}"
                                      elsif src_ref.options[:as]
                                        "ON br_t#{idx}.#{src_ref.type} = '#{src_ref.active_record.name}'" + # "polymorphable_type"
                                        " AND br_t#{idx}.#{src_ref.foreign_key} = br_t#{idx - 1}.id"
                                      elsif src_ref.options[:source_type]
                                        print "Skipping #{hm.name} --HMT-> #{hm.source_reflection.name} as it uses source_type which is not supported"
                                        nix << k
                                        bail_out = true
                                        break
                                      else # Standard has_many
                                        "ON br_t#{idx}.#{a.foreign_key} = br_t#{idx - 1}.id"
                                      end
                       link_back.unshift(a.source_reflection.name)
                       [a.table_name, a.foreign_key, a.source_reflection.macro]
                     end
                     next if bail_out

                     # count_column is determined from the originating HMT member
                     if (src_ref = hm.source_reflection).nil?
                       puts "*** Warning:  Could not determine destination model for this HMT association in model #{klass.name}:\n  has_many :#{hm.name}, through: :#{hm.options[:through]}"
                       puts
                       nix << k
                       next
                     elsif src_ref.macro == :belongs_to # Traditional HMT using an associative table
                       "br_t#{idx}.#{hm.foreign_key}"
                     else # A HMT that goes HM -> HM, something like Categories -> Products -> LineItems
                       "br_t#{idx}.#{src_ref.active_record.primary_key}"
                     end
                   else
                     fk_col = (inv = hm.inverse_of)&.foreign_key || hm.foreign_key
                     poly_type = inv.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 = if is_mysql
                  "`b_r_#{hm.name}`"
                elsif is_postgres
                  "\"b_r_#{hm.name}\""
                else
                  "b_r_#{hm.name}"
                end
    pri_tbl = hm.active_record
    pri_tbl_name = is_mysql ? "`#{pri_tbl.table_name}`" : "\"#{pri_tbl.table_name.gsub('.', '"."')}\""
    pri_tbl_name = if is_mysql
                     "`#{pri_tbl.table_name}`"
                   elsif is_postgres || is_mssql
                     "\"#{pri_tbl.table_name.gsub('.', '"."')}\""
                   else
                     pri_tbl.table_name
                   end
    on_clause = []
    hm_selects = 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]}" }
                   fk_col.dup
                 else
                   on_clause << "#{tbl_alias}.#{fk_col} = #{pri_tbl_name}.#{pri_tbl.primary_key}"
                   [fk_col]
                 end
    if poly_type
      hm_selects << poly_type
      on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
    end
    unless from_clause
      hm_table_name = if is_mysql
                        "`#{hm.table_name}`"
                      elsif is_postgres || is_mssql
                        "\"#{(hm.table_name).gsub('.', '"."')}\""
                      else
                        hm.table_name
                      end
    end
    group_bys = ::Brick.is_oracle || is_mssql ? hm_selects : (1..hm_selects.length).to_a
    join_clause = "LEFT OUTER
JOIN (SELECT #{hm_selects.map { |s| "#{'br_t0.' if from_clause}#{s}" }.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}#{count_column
      }) AS c_t_ FROM #{from_clause || hm_table_name} GROUP BY #{group_bys.join(', ')}) #{tbl_alias}"
    joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
  end
  while (n = nix.pop)
    klass._br_hm_counts.delete(n)
  end

  unless wheres.empty?
    # Rewrite the wheres to reference table and correlation names built out by AREL
    wheres2 = wheres.each_with_object({}) do |v, s|
      if (v_parts = v.first.split('.')).length == 1
        s[v.first] = v.last
      else
        k1 = klass.reflect_on_association(v_parts.first)&.klass
        tbl_name = (field_tbl_names[v_parts.first][k1] ||= shift_or_first(chains[k1])).split('.').last
        s["#{tbl_name}.#{v_parts.last}"] = v.last
      end
    end
    where!(wheres2)
  end
  # Must parse the order_by and see if there are any symbols which refer to BT associations
  # or custom columns as they must be expanded to find the corresponding b_r_model__column
  # or br_cc_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
        elsif (cc_cols = klass._br_cust_cols[v])
          cc_cols.first.each { |v1| s << "\"#{v1.last}\"" if v1.length > 1 }
        else
          s << v
        end
      else # String stuff (which defines a custom ORDER BY) just comes straight through
        s << v
        # Avoid "PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list" in Postgres
        selects << v if is_distinct
      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