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_page_numObject

Returns the value of attribute _brick_page_num.



518
519
520
# File 'lib/brick/extensions.rb', line 518

def _brick_page_num
  @_brick_page_num
end

Instance Method Details

#brick_(method, *args, brick_orig_relation: nil, **kwargs, &block) ⇒ Object

Accommodate when a relation gets queried for a model, and in that model it has an #after_initialize block which references attributes that were not originally included as part of the select_values.



1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
# File 'lib/brick/extensions.rb', line 1038

def brick_(method, *args, brick_orig_relation: nil, **kwargs, &block)
  begin
    send(method, *args, **kwargs, &block) # method will be something like :uniq or :each
  rescue ActiveModel::MissingAttributeError => e
    if e.message.start_with?('missing attribute: ') &&
       klass.column_names.include?(col_name = e.message[19..-1])
      (dup_rel = dup).select_values << col_name
      ret = dup_rel.brick_(method, *args, brick_orig_relation: (brick_orig_relation ||= self), **kwargs, &block)
      always_loads = (::Brick.config.always_load_fields ||= {})

      # Find the most parent STI superclass for this model, and apply an always_load_fields entry for this missing column
      has_field = false
      this_model = klass
      loop do
        has_field = true if always_loads.key?(this_model.name) && always_loads[this_model.name]&.include?(col_name)
        break if has_field || (next_model = this_model.superclass).abstract_class? || next_model == ActiveRecord::Base
        this_model = next_model
      end
      unless has_field
        (brick_orig_relation || self).instance_variable_set(:@brick_new_alf, ((always_loads[this_model.name] ||= []) << col_name))
      end

      if self.object_id == brick_orig_relation.object_id
        puts "*** WARNING: Missing field#{'s' if @brick_new_alf.length > 1}!
Might want to add this in your brick.rb:
  ::Brick.always_load_fields = { #{klass.name.inspect} => #{@brick_new_alf.inspect} }"
        remove_instance_variable(:@brick_new_alf)
      end
      ret
    else
      []
    end
  end
end

Links from ActiveRecord association pathing names over to real table correlation names that get chosen when the AREL AST tree is walked.



522
523
524
# File 'lib/brick/extensions.rb', line 522

def brick_links
  @brick_links ||= { '' => table_name }
end

#brick_listObject

Build out an AR relation that queries for a list of objects, and include all the appropriate JOINs to later apply DSL using #brick_descrip



968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
# File 'lib/brick/extensions.rb', line 968

def brick_list
  pks = klass.primary_key.is_a?(String) ? [klass.primary_key] : klass.primary_key
  selects = pks.each_with_object([]) { |pk, s| s << pk unless s.include?(pk) }
  # Get foreign keys for anything marked to be auto-preloaded, or a self-referencing JOIN
  klass_cols = klass.column_names
  reflect_on_all_associations.each do |a|
    selects << a.foreign_key if a.belongs_to? &&
                                (preload_values.include?(a.name) ||
                                 (!a.options[:polymorphic] && a.klass == klass && klass_cols.include?(a.foreign_key))
                                )
  end

  # ActiveStorage compatibility
  selects << 'service_name' if klass.name == 'ActiveStorage::Blob' && ActiveStorage::Blob.columns_hash.key?('service_name')
  selects << 'blob_id' if klass.name == 'ActiveStorage::Attachment' && ActiveStorage::Attachment.columns_hash.key?('blob_id')
  # Pay gem compatibility
  selects << 'processor' if klass.name == 'Pay::Customer' && Pay::Customer.columns_hash.key?('processor')
  selects << 'customer_id' if klass.name == 'Pay::Subscription' && Pay::Subscription.columns_hash.key?('customer_id')

  pieces, my_dsl = klass.brick_parse_dsl(join_array = ::Brick::JoinArray.new, [], translations = {}, false, nil, true)
  brick_select(
    selects, where_values_hash, nil, translations: translations, join_array: join_array,
    cust_col_override: { '_br' => (descrip_cols = [pieces, my_dsl]) }
  )
  order_values = "#{klass.table_name}.#{klass.primary_key}"
  [self.select(selects), descrip_cols]
end

#brick_select(*args, params: {}, order_by: nil, translations: {}, join_array: ::Brick::JoinArray.new, cust_col_override: nil, brick_col_names: true) ⇒ Object



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
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
# File 'lib/brick/extensions.rb', line 526

def brick_select(*args, params: {}, order_by: nil, translations: {},
                 join_array: ::Brick::JoinArray.new,
                 cust_col_override: nil,
                 brick_col_names: true)
  selects = args[0].is_a?(Array) ? args[0] : args
  if selects.present? && cust_col_override.nil? # See if there's any fancy ones in the select list
    idx = 0
    while idx < selects.length
      v = selects[idx]
      if v.is_a?(String) && v.index('.')
        # No prefixes and not polymorphic
        pieces = self.brick_parse_dsl(join_array, [], translations, false, dsl = "[#{v}]")
        (cust_col_override ||= {})[v.tr('.', '_').to_sym] = [pieces, dsl, true]
        selects.delete_at(idx)
      else
        idx += 1
      end
    end
  elsif selects.is_a?(Hash) && params.empty? && cust_col_override.nil? # Make sense of things if they've passed in only params
    params = selects
    selects = []
  end
  is_add_bts = is_add_hms = !cust_col_override

  # 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_distinct = nil
  wheres = {}
  params.each do |k, v|
    k = k.to_s # Rails < 4.2 comes in as a symbol
    next unless k.start_with?('__')

    k = k[2..-1] # Take off leading "__"
    if (where_col = (ks = k.split('.')).last)[-1] == '!'
      where_col = where_col[0..-2]
    end
    case ks.length
    when 1
      next unless klass.column_names.any?(where_col) || klass._brick_get_fks.include?(where_col)
    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?(where_col)

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

  # %%% Skip the metadata columns
  if selects.empty? # Default to all columns
    id_parts = (id_col = klass.primary_key).is_a?(Array) ? id_col : [id_col]
    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
    is_api = params['_brick_is_api']
    columns.each do |col|
      next if (col.type.nil? || col.type == :binary) && is_api

      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
                   if is_distinct # Postgres can not use DISTINCT with any columns that are XML or JSON
                     cast_as_text = if Brick.relations[klass.table_name]&.[](:cols)&.[](col_name)&.first == 'json'
                                      '::jsonb' # Convert JSON to JSONB
                                    elsif Brick.relations[klass.table_name]&.[](:cols)&.[](col_name)&.first&.start_with?('xml')
                                      '::text' # Convert XML to text
                                    end
                   end
                   "\"#{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
  else # Having some select columns chosen, add any missing always_load_fields for this model ...
    this_model = klass
    loop do
      ::Brick.config.always_load_fields.fetch(this_model.name, nil)&.each do |alf|
        selects << alf unless selects.include?(alf)
      end
      # ... plus any and all STI superclasses it may inherit from
      break if (this_model = this_model.superclass).abstract_class? || this_model == ActiveRecord::Base
    end
  end

  if join_array.present?
    if ActiveRecord.version < Gem::Version.new('4.2')
      self.joins_values += join_array # Same as:  joins!(join_array)
    else
      left_outer_joins!(join_array)
    end
  end

  # If it's a CollectionProxy (which inherits from Relation) then need to dig out the
  # core Relation object which is found in the association scope.
  rel_dupe = (is_a?(ActiveRecord::Associations::CollectionProxy) ? scope : self).dup

  # Touching AREL AST walks the JoinDependency tree, and in that process uses our
  # "brick_links" patch to find how every AR chain of association names relates to exact
  # table correlation names chosen by AREL.  We use a duplicate relation object for this
  # because an important side-effect of referencing the AST is that the @arel instance
  # variable gets set, and this is a signal to ActiveRecord that a relation has now
  # become immutable.  (We aren't quite ready for our "real deal" relation object to be
  # set in stone ... still need to add .select(), and possibly .where() and .order()
  # things ... also if there are any HM counts then an OUTER JOIN for each of them out
  # to a derived table to do that counting.  All of these things need to know proper
  # table correlation names, which will now become available in brick_links on the
  # rel_dupe object.)
  rel_dupe.arel.ast

  core_selects = selects.dup
  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
  # ==============
  (cust_col_override || 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

    key_klass = nil
    key_tbl_name = nil
    dest_pk = nil
    key_alias = nil
    cc.first.each do |cc_part|
      dest_klass = cc_part[0..-2].inject(klass) do |kl, cc_part_term|
        # %%% Clear column info properly so we can do multiple subsequent requests
        # binding.pry unless kl.reflect_on_association(cc_part_term)
        kl.reflect_on_association(cc_part_term)&.klass || klass
      end
      tbl_name = rel_dupe.brick_links[cc_part[0..-2].map(&:to_s).join('.')]
      # 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".
      col_prefix = 'br_cc_' if brick_col_names
      if (cc_part_idx = cc_part.length - 1).zero?
        col_alias = "#{col_prefix}#{k}__#{table_name.tr('.', '_')}_#{cc_part.first}"
      elsif brick_col_names ||
            used_col_aliases.key?(col_alias = k.to_s) # This sets a simpler custom column name if possible
        while cc_part_idx > 0 &&
              (col_alias = "#{col_prefix}#{k}__#{cc_part[cc_part_idx..-1].map(&:to_s).join('__').tr('.', '_')}") &&
              used_col_aliases.key?(col_alias)
          cc_part_idx -= 1
        end
      end
      used_col_aliases[col_alias] = nil
      # Set up custom column links by preparing key_klass and key_alias
      # (If there are multiple different tables referenced in the DSL, we end up creating a link to the last one)
      if cc[2] && (dest_pk = dest_klass.primary_key)
        key_klass = dest_klass
        key_tbl_name = tbl_name
        cc_part_idx = cc_part.length - 1
        while cc_part_idx > 0 &&
              (key_alias = "#{col_prefix}#{k}__#{(cc_part[cc_part_idx..-2] + [dest_pk]).map(&:to_s).join('__')}") &&
              key_alias != col_alias && # We break out if this key alias does exactly match the col_alias
              used_col_aliases.key?(key_alias)
          cc_part_idx -= 1
        end
      end
      selects << "#{tbl_name}.#{cc_part.last} AS #{col_alias}"
      cc_part << col_alias
    end
    # Add a key column unless we've already got it
    if key_alias && !used_col_aliases.key?(key_alias)
      selects << "#{key_tbl_name}.#{dest_pk} AS #{key_alias}"
      used_col_aliases[key_alias] = nil
    end
    cc[2] = key_alias ? [key_klass, key_alias] : nil
  end

  # LEFT OUTER JOINs
  unless cust_col_override
    klass._br_bt_descrip.each do |v|
      v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
        next unless (tbl_name = rel_dupe.brick_links[v.first.to_s]&.split('.')&.last)

        # If it's Oracle, quote any AREL aliases that had been applied
        tbl_name = "\"#{tbl_name}\"" if ::Brick.is_oracle && rel_dupe.brick_links.values.include?(tbl_name)
        field_tbl_name = nil
        v1.map { |x| [x[0..-2].map(&:to_s).join('.'), x.last] }.each_with_index do |sel_col, idx|
          # %%% Strangely in Rails 7.1 on a slower system then very rarely brick_link comes back nil...
          brick_link = rel_dupe.brick_links[sel_col.first]
          field_tbl_name = brick_link&.split('.')&.last ||
            # ... so here's a best-effort guess for what the table name might be.
            rel_dupe.klass.reflect_on_association(sel_col.first)&.klass&.table_name
          # If it's Oracle, quote any AREL aliases that had been applied
          field_tbl_name = "\"#{field_tbl_name}\"" if ::Brick.is_oracle && rel_dupe.brick_links.values.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[k1.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|
      next unless assoc_name.is_a?(Symbol)

      table_alias = rel_dupe.brick_links[assoc_name.to_s]
      _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
                     through_sources = []
                     # %%% Inverse path back to the original object -- not yet used, but soon
                     # will be leveraged in order to build links with multi-table-hop filters.
                     link_back = []
                     # Track polymorphic type field if necessary
                     if hm.source_reflection.options[:as]
                       # Might be able to simplify as:  hm.source_reflection.type
                       poly_ft = [hm.source_reflection.inverse_of.foreign_type, hmt_assoc.source_reflection.class_name]
                     end
                     # link_back << hm.source_reflection.inverse_of.name
                     while hmt_assoc.options[:through] && (hmt_assoc = klass.reflect_on_association(hmt_assoc.options[:through]))
                       through_sources.unshift(hmt_assoc)
                     end
                     # Turn the last member of link_back into a foreign key
                     link_back << hmt_assoc.source_reflection.foreign_key
                     # If it's a HMT based on a HM -> HM, must JOIN the last table into the mix at the end
                     this_hm = hm
                     while !(src_ref = this_hm.source_reflection).belongs_to? && (thr = src_ref.options[:through])
                       through_sources.push(this_hm = src_ref.active_record.reflect_on_association(thr))
                     end
                     through_sources.push(src_ref) unless src_ref.belongs_to?
                     from_clause = +"#{through_sources.first.table_name} br_t0"
                     fk_col = through_sources.shift.foreign_key

                     idx = 0
                     bail_out = nil
                     through_sources.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
                                        link_back << (nm = hmt_assoc.source_reflection.inverse_of&.name)
                                        # puts "BT #{a.table_name}"
                                        "ON br_t#{idx}.#{a.active_record.primary_key} = 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]
                                        if a == hm.source_reflection
                                          print "Skipping #{hm.name} --HMT-> #{hm.source_reflection.name} as it uses source_type in a way which is not yet supported"
                                          nix << k
                                          bail_out = true
                                          break
                                          # "ON br_t#{idx}.#{a.foreign_type} = '#{src_ref.options[:source_type]}' AND "                                            #   "br_t#{idx}.#{a.foreign_key} = br_t#{idx - 1}.#{a.active_record.primary_key}"
                                        else # Works for HMT through a polymorphic HO
                                          link_back << hmt_assoc.source_reflection.inverse_of&.name # Some polymorphic "_able" thing
                                          "ON br_t#{idx - 1}.#{a.foreign_type} = '#{src_ref.options[:source_type]}' AND " \
                                            "br_t#{idx - 1}.#{a.foreign_key} = br_t#{idx}.#{a.active_record.primary_key}"
                                        end
                                      else # Standard has_many or has_one
                                        # puts "HM #{a.table_name}"
                                        nm = hmt_assoc.source_reflection.inverse_of&.name
                                        # binding.pry unless nm
                                        link_back << nm # if nm
                                        "ON br_t#{idx}.#{a.foreign_key} = br_t#{idx - 1}.#{a.active_record.primary_key}"
                                      end
                       link_back.unshift(a.source_reflection.name)
                       [a.table_name, a.foreign_key, a.source_reflection.macro]
                     end
                     next if bail_out

                     # puts "LINK BACK! #{k} : #{hm.table_name} #{link_back.map(&:to_s).join('.')}"
                     # 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
                     # %%% Might only need hm.type and not the first part :)
                     poly_type = inv&.foreign_type || hm.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

    pri_tbl = hm.active_record
    pri_key = hm.options[:primary_key] || pri_tbl.primary_key
    if hm.active_record.abstract_class || hm.active_record.column_names.exclude?(pri_key)
      # %%% When this gets hit then if an attempt is made to display the ERD, it might end up being blank
      nix << k
      next
    end

    tbl_alias = "b_r_#{hm.name}"
    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.table_name}.#{pri_key[idx]}" }
                   fk_col.dup
                 else
                   on_clause << "#{_br_quoted_name("#{tbl_alias}.#{fk_col}")} = #{_br_quoted_name("#{pri_tbl.table_name}.#{pri_key}")}"
                   [fk_col]
                 end
    if poly_type
      hm_selects << poly_type
      on_clause << "#{_br_quoted_name("#{tbl_alias}.#{poly_type}")} = '#{name}'"
    end
    unless from_clause
      tbl_nm = hm.macro == :has_and_belongs_to_many ? hm.join_table : hm.table_name
      hm_table_name = _br_quoted_name(tbl_nm)
    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_quoted_name("#{'br_t0.' if from_clause}#{s}") }.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}#{_br_quoted_name(count_column)
      }) AS c_t_ FROM #{from_clause || hm_table_name} GROUP BY #{group_bys.join(', ')}) #{_br_quoted_name(tbl_alias)}"
    self.joins_values |= ["#{join_clause} ON #{on_clause.join(' AND ')}"] # Same as:  joins!(...)
  end unless cust_col_override
  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
    where_nots = {}
    wheres2 = wheres.each_with_object({}) do |v, s|
      is_not = if v.first[-1] == '!'
                 v[0] = v[0][0..-2] # Take off ending ! from column name
               end
      if (v_parts = v.first.split('.')).length == 1
        (is_not ? where_nots : s)[v.first] = v.last
      else
        tbl_name = rel_dupe.brick_links[v_parts.first].split('.').last
        (is_not ? where_nots : s)["#{tbl_name}.#{v_parts.last}"] = v.last
      end
    end
    if respond_to?(:where!)
      where!(wheres2) if wheres2.present?
      if where_nots.present?
        self.where_clause += WhereClause.new(predicate_builder.build_from_hash(where_nots)).invert
      end
    else # AR < 4.0
      self.where_values << build_where(wheres2)
    end
  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?
    order_by, _ = klass._brick_calculate_ordering(order_by, true) # Don't do the txt part
    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 << _br_quoted_name(v2.last) if v2.length > 1 }
          end
        elsif (cc_cols = klass._br_cust_cols[v])
          cc_cols.first.each { |v1| s << _br_quoted_name(v1.last) if v1.length > 1 }
        else
          s << v
        end
      else # String stuff (which defines a custom ORDER BY) just comes straight through
        # v = v.split('.').map { |x| _br_quoted_name(x) }.join('.')
        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
    self.order_values |= final_order_by # Same as:  order!(*final_order_by)
  end
  # By default just 1000 rows
  row_limit = params['_brick_limit'] || params['_brick_page_size'] || 1000
  offset = if (page = params['_brick_page']&.to_i)
             page = 1 if page < 1
             (page - 1) * row_limit.to_i
           else
             params['_brick_offset']
           end
  if offset.is_a?(Numeric) || offset&.present?
    offset = offset.to_i
    self.offset_value = offset unless offset == 0
    @_brick_page_num = (offset / row_limit.to_i) + 1 if row_limit&.!= 0 && (offset % row_limit.to_i) == 0
  end
  # Setting limit_value= is the same as doing:  limit!(1000)  but this way is compatible with AR <= 4.2
  self.limit_value = row_limit.to_i unless row_limit.is_a?(String) && row_limit.empty?
  wheres unless wheres.empty? # Return the specific parameters that we did use
end

#brick_where(opts) ⇒ Object

Smart Brick #where that automatically adds the inner JOINs when you have a query like:

Customer.brick_where('orders.order_details.order_date' => '2005-1-1', 'orders.employee.first_name' => 'Nancy')

Way to make it a more intrinsic part of ActiveRecord alias _brick_where! where! def where!(opts, *rest)



1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
# File 'lib/brick/extensions.rb', line 1001

def brick_where(opts)
  if opts.is_a?(Hash)
    # && joins_values.empty? # Make sure we don't step on any toes if they've already specified JOIN things
    ja = nil
    opts.each do |k, v|
      if k.is_a?(String) && (ref_parts = k.split('.')).length > 1
        # JOIN in all the same ways as the pathing describes
        linkage = (ja ||= ::Brick::JoinArray.new)
        ref_parts[0..-3].each do |prefix_part|
          linkage = linkage[prefix_part.to_sym]
        end
        linkage[ref_parts[-2].to_sym] = nil
      end
    end
    if ja&.present?
      if ActiveRecord.version < Gem::Version.new('4.2')
        self.joins_values += ja # Same as:  joins!(ja)
      else
        self.joins!(ja)
      end
      (ast_tree = self.dup).arel.ast # Walk the AST tree so we can rewrite the prefixes accordingly
      conditions = opts.each_with_object({}) do |v, s|
        if (ref_parts = v.first.split('.')).length > 1 &&
           (tbl = ast_tree.brick_links[ref_parts[0..-2].join('.')])
          s["#{tbl}.#{ref_parts.last}"] = v.last
        else
          s[v.first] = v.last
        end
      end
    end
  end
  # If you want it to be more intrinsic with ActiveRecord, do this instead:  super(conditions, *rest)
  self.where!(conditions)
end