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.



583
584
585
# File 'lib/brick/extensions.rb', line 583

def _brick_page_num
  @_brick_page_num
end

Instance Method Details

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



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
966
967
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
995
996
997
998
999
1000
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
1035
1036
1037
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
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
# File 'lib/brick/extensions.rb', line 631

def _brick_querying(*args, grouping: nil, withhold_ids: nil, params: {}, order_by: nil, translations: {},
                    join_array: ::Brick::JoinArray.new,
                    cust_col_override: nil,
                    brick_col_names: nil)
  selects = args[0].is_a?(Array) ? args[0] : args
  unless cust_col_override
    if selects.present? # 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? # Make sense of things if they've passed in only params
      params = selects
      selects = []
    end
  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
  elsif !withhold_ids # 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 ALF fields from any and all STI superclasses it may inherit from
      break if (this_model = this_model.superclass).abstract_class? || this_model == ActiveRecord::Base
    end
  end

  # Establish necessary JOINs for any custom GROUP BY columns
  grouping&.each do |group_item|
    # JOIN in all the same ways as the pathing describes
    if group_item.is_a?(String) && (ref_parts = group_item.split('.')).length > 1
      join_array.add_parts(ref_parts)
    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

  # 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_cols = cust_col_override
  cust_cols ||= klass._br_cust_cols unless withhold_ids
  cust_cols&.each do |k, cc|
    brick_links # Intentionally create a relation duplicate
    if @_brick_rel_dup.respond_to?(k) # Name already taken?
      # %%% Use ensure_unique here in this kind of fashion:
      # cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", nil, 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 = 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 << "#{_br_quoted_name(tbl_name)}.#{_br_quoted_name(cc_part.last)} AS #{_br_quoted_name(col_alias)}"
      cc_part << col_alias
    end
    unless withhold_ids
      # Add a key column unless we've already got it
      if key_alias && !used_col_aliases.key?(key_alias)
        selects << "#{_br_quoted_name(key_tbl_name)}.#{_br_quoted_name(dest_pk)} AS #{_br_quoted_name(key_alias)}"
        used_col_aliases[key_alias] = nil
      end
      cc[2] = key_alias ? [key_klass, key_alias] : nil
    end
  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 = 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 && 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 = brick_links[sel_col.first]
          field_tbl_name = brick_link&.split('.')&.last ||
            # ... so if it is nil then here's a best-effort guess as to what the table name might be.
            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 && 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
        # if k1.name == 'ActiveStorage::Attachment'
        #   binding.pry
        #   (@_brick_includes ||= {})[v1.first.first.to_s] = [v1.first[1..-1], 'blob']
        #   # x = 5
        # # elsif k1.name == 'ActiveStorage::Blob'
        # #   binding.pry
        # #   (@_brick_includes ||= {})[v1.first.first.to_s] = v1[0..1]
        # #   # x = 5
        # end
      end
    end
    join_array.each do |assoc_name|
      next unless assoc_name.is_a?(Symbol)

      table_alias = 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 = []
  previous = []
  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 = +"#{_br_quoted_name(through_sources.first.table_name)} br_t0"
                     # ActiveStorage will not get the correct count unless we do some extra filtering later
                     if Object.const_defined?('ActiveStorage') && through_sources.first.klass <= ::ActiveStorage::Attachment
                       # binding.pry
                       tbl_nm = 'br_t0'
                       # Need to somehow have this kind of an include in order to avoid an N+1 problem:
                       # .include(images_attachments: [blob: { variant_records: :blob }])
                     end
                     fk_col = through_sources.shift.foreign_key

                     idx = 0
                     bail_out = nil
                     the_chain = 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 || case pri_key
                                          when String
                                            hm.active_record.column_names.exclude?(pri_key)
                                          when Array
                                            (pri_key - hm.active_record.column_names).length > 0
                                          end
      # %%% 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 = unique63("b_r_#{hm.name}", previous)
    on_clause = []
    hm_selects = if !pri_key.is_a?(Array) # Probable standard key?
                   if fk_col.is_a?(Array) # Foreign is composite but not Primary?  OK, or choose the first part of the foreign key if nothing else
                     fk_col = fk_col.find { |col_name| col_name == pri_key } || # Try to associate with the same-named part of the foreign key ...
                              fk_col.first # ... and if no good match, just choose the first part
                   end
                   on_clause << "#{_br_quoted_name("#{tbl_alias}.#{fk_col}")} = #{_br_quoted_name("#{pri_tbl.table_name}.#{pri_key}")}"
                   [fk_col]
                 else # Composite key
                   fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{_br_quoted_name("#{tbl_alias}.#{fk_col_part}")} = #{_br_quoted_name("#{pri_tbl.table_name}.#{pri_key[idx]}")}" }
                   fk_col.dup
                 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
    # ActiveStorage has_many_attached needs a bit more filtering
    if (k_str = hm.klass._active_storage_name(k))
      where_ct_clause = "WHERE #{_br_quoted_name("#{tbl_nm}.name")} = '#{k_str}' "
    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} #{where_ct_clause}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

  # Rewrite the group values to reference table and correlation names built out by AREL
  if grouping
    group2 = (gvgu = (group_values + grouping).uniq).each_with_object([]) do |v, s|
      if v.is_a?(Symbol) || (v_parts = v.split('.')).length == 1
        s << v
      elsif (tbl_name = brick_links[v_parts[0..-2].join('.')]&.split('.')&.last)
        s << "#{tbl_name}.#{v_parts.last}"
      else
        s << v
      end
    end
    group!(*group2)
  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 = brick_links[v_parts[0..-2].join('.')].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_(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.



1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
# File 'lib/brick/extensions.rb', line 1185

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

#brick_group(*args, **kwargs) ⇒ Object



624
625
626
627
628
629
# File 'lib/brick/extensions.rb', line 624

def brick_group(*args, **kwargs)
  grouping = args[0].is_a?(Array) ? args[0] : args
  _brick_querying(select_values.frozen? ? select_values.dup : select_values,
                  grouping: grouping, **kwargs)
  self
end

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



587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
# File 'lib/brick/extensions.rb', line 587

def brick_links(do_dup = true)
  # 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.  Unless a relation has already had its AST
  # tree built out, we will use a duplicate relation object for this, because an important
  # side-effect of referencing the AST is that the @arel instance variable gets set.  This
  # is a signal to ActiveRecord that a relation has now become immutable.  (When Brick is
  # still in the middle of calculating its query, we aren't quite ready for the 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 from brick_links on
  # the rel_dupe object.)
  @_brick_links ||= begin
                      # 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.
                      brick_rel = is_a?(ActiveRecord::Associations::CollectionProxy) ? scope : self
                      brick_rel = (@_brick_rel_dup ||= brick_rel.dup) if do_dup
                      # Start out with a hash that has only the root table name
                      brick_rel.instance_variable_set(:@_brick_links, bl = { '' => table_name })
                      brick_rel.arel.ast if do_dup # Walk the AST tree in order to capture all the other correlation names
                      bl
                    end
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



1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
# File 'lib/brick/extensions.rb', line 1115

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
  if klass.name == 'ActiveStorage::Blob' && ::ActiveStorage::Blob.columns_hash.key?('service_name')
    selects << 'service_name'
  end
  if klass.name == 'ActiveStorage::Attachment' && ::ActiveStorage::Attachment.columns_hash.key?('blob_id')
    selects << 'blob_id'
  end
  # 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_querying(
    selects, where_values_hash, nil, translations: translations, join_array: join_array,
    cust_col_override: { '_br' => (descrip_cols = [pieces, my_dsl]) },
    brick_col_names: true
  )
  order_values = "#{_br_quoted_name(klass.table_name)}.#{_br_quoted_name(klass.primary_key)}"
  [self.select(selects), descrip_cols]
end

#brick_pluck(*args, withhold_ids: true, **kwargs) ⇒ Object



618
619
620
621
622
# File 'lib/brick/extensions.rb', line 618

def brick_pluck(*args, withhold_ids: true, **kwargs)
  selects = args[0].is_a?(Array) ? args[0] : args
  _brick_querying(selects, withhold_ids: withhold_ids, **kwargs)
  pluck(selects)
end

#brick_select(*args, **kwargs) ⇒ Object



612
613
614
615
616
# File 'lib/brick/extensions.rb', line 612

def brick_select(*args, **kwargs)
  selects = args[0].is_a?(Array) ? args[0] : args
  _brick_querying(selects, **kwargs)
  select(selects)
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)



1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
# File 'lib/brick/extensions.rb', line 1153

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|
      # JOIN in all the same ways as the pathing describes
      if k.is_a?(String) && (ref_parts = k.split('.')).length > 1
        (ja ||= ::Brick::JoinArray.new).add_parts(ref_parts)
      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
      conditions = opts.each_with_object({}) do |v, s|
        if (ref_parts = v.first.split('.')).length > 1 &&
           (tbl = 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