Module: Brick

Defined in:
lib/brick/util.rb,
lib/brick.rb,
lib/brick/config.rb,
lib/brick/join_array.rb,
lib/brick/route_mapper.rb,
lib/brick/reflect_tables.rb,
lib/brick/version_number.rb,
lib/brick/serializers/json.rb,
lib/brick/serializers/yaml.rb,
lib/brick/frameworks/cucumber.rb,
lib/brick/frameworks/rails/engine.rb,
lib/generators/brick/seeds_generator.rb,
lib/generators/brick/models_generator.rb,
lib/generators/brick/install_generator.rb,
lib/generators/brick/migration_builder.rb,
lib/generators/brick/salesforce_schema.rb,
lib/generators/brick/migrations_generator.rb,
lib/generators/brick/controllers_generator.rb,
lib/generators/brick/salesforce_migrations_generator.rb,
lib/brick/extensions.rb,
lib/brick/extensions.rb

Overview

:nodoc:

Defined Under Namespace

Modules: Cucumber, Extensions, MigrationBuilder, Rails, RouteMapper, RouteSet, Serializers, Util, VERSION Classes: Config, ControllersGenerator, InstallGenerator, JoinArray, JoinHash, MigrationsGenerator, ModelsGenerator, SalesforceMigrationsGenerator, SalesforceSchema, SeedsGenerator

Constant Summary collapse

ALL_API_ACTIONS =
[:index, :show, :create, :update, :destroy]
CURRENCY_SYMBOLS =
'$£¢₵€₠ƒ¥₿₩₪₹₫₴₱₲₳₸₺₼₽៛₡₢₣₤₥₦₧₨₭₮₯₰₶₷₻₾'
ADD_CONST_MISSING =
lambda do
  return if @_brick_const_missing_done

  @_brick_const_missing_done = true
  alias _brick_const_missing const_missing
  def const_missing(*args)
    requested = args.first.to_s
    is_controller = requested.end_with?('Controller')
    # self.name is nil when a model name is requested in an .erb file
    if self.name && ::Brick.config.path_prefix
      split_self_name.shift if (split_self_name = self.name.split('::')).first.blank?
      # Asking for the prefix module?
      camelize_prefix = ::Brick.config.path_prefix.camelize
      if self == Object && requested == camelize_prefix
        Object.const_set(args.first, (built_module = Module.new))
        puts "module #{camelize_prefix}; end\n"
        return built_module
      elsif module_parent == Object && self.name == camelize_prefix ||
            module_parent.name == camelize_prefix && module_parent.module_parent == Object
        split_self_name.shift # Remove the identified path prefix from the split name
        is_brick_prefix = true
        if is_controller
          brick_root = split_self_name.empty? ? self : camelize_prefix.constantize
        end
      end
    end
    base_module = if self < ActiveRecord::Migration || !self.name
                    brick_root || Object
                  elsif split_self_name&.length&.> 1 # Classic mode
                    begin
                      base = self
                      unless (base_goal = requested.split('::')[0..-2].join('::')).empty?
                        base = base.parent while base.name != base_goal && base != Object
                      end
                      return base._brick_const_missing(*args)

                    rescue NameError # %%% Avoid the error "____ cannot be autoloaded from an anonymous class or module"
                      return self.const_get(args.first) if self.const_defined?(args.first)

                      # unless self == (prnt = (respond_to?(:parent) ? parent : module_parent))
                      unless self == Object
                        begin
                          return Object._brick_const_missing(*args)

                        rescue NameError
                          return Object.const_get(args.first) if Object.const_defined?(args.first)

                        end
                      end
                    end
                    Object
                  else
                    sti_base = (::Brick.config.sti_namespace_prefixes&.fetch("::#{name}::#{requested}", nil) ||
                                ::Brick.config.sti_namespace_prefixes&.fetch("::#{name}::", nil))&.constantize
                    self
                  end
    # puts "#{self.name} - #{args.first}"
    # Unless it's a Brick prefix looking for a TNP that should create a module ...
    relations = ::Brick.relations
    unless (is_tnp_module = (is_brick_prefix && !is_controller && ::Brick.config.table_name_prefixes.values.include?(requested)))
      # ... first look around for an existing module or class.
      desired_classname = (self == Object || !name) ? requested : "#{name}::#{requested}"
      if (self.const_defined?(args.first) && (possible = self.const_get(args.first)) &&
          # Reset `possible` if it's a controller request that's not a perfect match
          # Was:  (possible = nil)  but changed to #local_variable_set in order to suppress the "= should be ==" warning
          (possible&.name == desired_classname || (is_controller && binding.local_variable_set(:possible, nil)))) ||

         # Try to require the respective Ruby file
         # ((filename = ActiveSupport::Dependencies.search_for_file(desired_classname.underscore)) &&
         #  (require_dependency(filename) || true) &&
         (!anonymous? &&
          (filename = ActiveSupport::Dependencies.search_for_file(desired_classname.underscore) ||
                      (self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = requested).underscore))
          ) && (require_dependency(filename) || true) &&
          (filename != Module.instance_variable_get(:@_brick_last_filename) || # Avoid trying the same exact file twice in a row
           Module.instance_variable_set(:@_brick_last_filename, nil)) &&
           Module.instance_variable_set(:@_brick_last_filename, filename) &&

          (possible = self.const_get(args.first)) && possible.name == desired_classname
         ) ||

         # If any class has turned up so far (and we're not in the middle of eager loading)
         # then return what we've found.
         (possible&.module_parent == base_module && !::Brick.is_eager_loading) # Used to also have:   && possible != self
        if ((!brick_root && (filename || possible.instance_of?(Class))) ||
            (possible.instance_of?(Module) && possible&.module_parent == self) ||
            (possible.instance_of?(Class) && possible == self)) && # Are we simply searching for ourselves?
           # Skip when what we found as `possible` is not related to the base class of an STI model
           (!sti_base || possible.is_a?(sti_base))
          # if possible.is_a?(ActiveRecord::Base) && !possible.abstract_class? && (pk = possible.primary_key) &&
          #    !(relation = relations.fetch(possible.table_name, nil))&.fetch(:pks, nil)
          #   binding.pry
          #   x = 5
          # end
          return possible
        end
      end
      Module.instance_variable_set(:@_brick_last_filename, nil) # Clear out the check for trying the same exact file twice in a row
    end
    class_name = ::Brick.namify(requested)
    is_avo_present = Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace)
    #        CONTROLLER
    result = if ::Brick.enable_controllers? &&
                is_controller && (plural_class_name = class_name[0..-11]).length.positive?
               # Otherwise now it's up to us to fill in the gaps
               controller_class_name = +''
               full_class_name = +''
               unless self == Object || (is_avo_present && self.name == 'Avo')
                 controller_class_name << ((split_self_name&.first && split_self_name.join('::')) || self.name)
                 full_class_name << "::#{controller_class_name}"
                 controller_class_name << '::'
               end
               # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
               # Vabc instead of VABC)
               singular_class_name = ::Brick.namify(plural_class_name, :underscore).singularize.camelize
               full_class_name << "::#{singular_class_name}"
               skip_controller = nil
               begin
                 if plural_class_name == 'BrickOpenapi' ||
                    (
                      (::Brick.config.add_status || ::Brick.config.add_orphans) &&
                      plural_class_name == 'BrickGem'
                    # Was:  ) || (model = self.const_get(full_class_name))
                    ) || (model = Object.const_get(full_class_name))
                   # puts "#{self.name} - #{full_class_name}"

                   # In the very rare case that we've picked up a MODULE which has the same name as what would be the
                   # resource's MODEL name, just build out an appropriate auto-model on-the-fly. (RailsDevs code has this in PayCustomer.)
                   # %%% We don't yet display the code for this new model
                   if model && !model.is_a?(Class)
                     model, _code = Object.send(:build_model, relations, model.module_parent, model.module_parent.name, singular_class_name)
                   end
                 end
               rescue NameError # If the const_get for the model has failed...
                 skip_controller = true
                 # ... then just fall through and allow it to fail when trying to load the ____Controller class normally.
               end
               unless skip_controller
                 Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
               end

             # MODULE
             elsif (::Brick.enable_models? || ::Brick.enable_controllers?) && # Schema match?
                   # %%% This works for Person::Person -- but also limits us to not being able to allow more than one level of namespacing
                   (base_module == Object || (camelize_prefix && base_module == Object.const_get(camelize_prefix))) &&
                   (schema_name = [(singular_table_name = class_name.underscore),
                                   (table_name = singular_table_name.pluralize),
                                   ::Brick.is_oracle ? class_name.upcase : class_name,
                                   (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas&.include?(s) }&.camelize ||
                                  (::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name) ||
                                  (::Brick.config.table_name_prefixes&.values&.include?(class_name) && class_name))
               return self.const_get(schema_name) if self.const_defined?(schema_name) &&
                                                     (!is_tnp_module || self.const_get(schema_name).is_a?(Class))

               # Build out a module for the schema if it's namespaced
               # schema_name = schema_name.camelize
               base_module.const_set(schema_name.to_sym, (built_module = Module.new))
               [built_module, "module #{schema_name}; end\n"]
               # %%% Perhaps an option to use the first module just as schema, and additional modules as namespace with a table name prefix applied

             # MODULE (overrides from "treat_as_module")
             elsif (::Brick.enable_models? || ::Brick.enable_controllers?) &&
                   (possible_module = (base_module == Object ? '' : "#{base_module.name}::") + class_name) &&
                   ::Brick.config.treat_as_module.include?(possible_module)
               base_module.const_set(class_name.to_sym, (built_module = Module.new))
               [built_module, "module #{possible_module}; end\n"]

             # AVO 2.x Resource
             elsif base_module == Object && is_avo_present && requested.end_with?('Resource') &&
                   # Expect that anything called MotorResource or SpinaResource could be from those administrative gems
                   requested.length > 8 && ['MotorResource', 'SpinaResource'].exclude?(requested) &&
                   (model = Object.const_get(requested[0..-9])) && model < ActiveRecord::Base
               built_resource = Class.new(Avo::BaseResource) do
                 self.model_class = model
                 self.title = :brick_descrip
                 self.includes = []
                 ::Brick::ADD_AVO_FIELDS.call(self, model)
               end
               base_module.const_set(requested.to_sym, built_resource)
               [built_resource, nil]

             # AVO 3.x Resource
             elsif is_avo_present && self.name == 'Avo::Resources' &&
                   (model = begin
                              (model = Object.const_get(requested)) && model < ActiveRecord::Base
                              model
                            rescue
                            end)
               [::Brick.avo_3x_resource(model, requested), nil]

             # MODEL
             elsif ::Brick.enable_models?
               # Avo sometimes tries to find a model class inside of the Avo namespace
               if is_avo_present && self.name == 'Avo'
                 name = (base_module = Object).name
               end
               name ||= base_module.name
               # Custom inheritable Brick base model?
               class_name = (inheritable_name = class_name)[5..-1] if class_name.start_with?('Brick')
               Object.send(:build_model, relations, base_module, name, class_name, inheritable_name)
             end
    if result
      built_class, code = result
      puts "\n#{code}\n" if code
      built_class
    elsif !schema_name && ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}")
#         module_prefixes = type_name.split('::')
#         path = base_module.name.split('::')[0..-2] + []
#         module_prefixes.unshift('') unless module_prefixes.first.blank?
#         candidate_file = ::Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
      base_module._brick_const_missing(*args)
    # elsif base_module != Object
    #   module_parent.const_missing(*args)
    elsif Object.const_defined?('Rails') && ::Rails.respond_to?(:autoloaders) && # After finding nothing else, if Zeitwerk is enabled ...
          (Rails::Autoloaders.respond_to?(:zeitwerk_enabled?) ? Rails::Autoloaders.zeitwerk_enabled? : true)
      self._brick_const_missing(*args) # ... rely solely on Zeitwerk.
    else # Classic mode
      unless (found = base_module._brick_const_missing(*args))
        puts "MISSING! #{base_module.name} #{args.inspect} #{table_name}"
      end
      found
    end
  end

  # Support Rails < 6.0 which adds #parent instead of #module_parent
  unless respond_to?(:module_parent)
    def module_parent # Weirdly for Grape::API does NOT come in with the proper class, but some anonymous Class thing
      parent
    end
  end
end
ADD_AVO_FIELDS =
lambda do |obj, model|
  require 'generators/avo/resource_generator'
  field_generator = Generators::Avo::ResourceGenerator.new([''])
  field_generator.instance_variable_set(:@model, model)
  flds = field_generator.send(:generate_fields)&.split("\n")
                        &.each_with_object([]) do |f, s|
                          if (f = f.strip).start_with?('field ')
                            f = f[6..-1].split(',')
                            s << [f.first[1..-1].to_sym, [f[1][1..-1].split(': :').map(&:to_sym)].to_h]
                          end
                        end || []
  if (!model.is_view? && mod_pk = model.primary_key)
    obj.field((mod_pk.is_a?(Array) ? mod_pk.first : mod_pk).to_sym, **{ as: :id })
  end
  # Create a call such as:  field :name, as: :text
  flds.each do |f|
    # Add proper types if this is a polymorphic belongs_to
    if f.last == { as: :belongs_to } &&
       (fk = ::Brick.relations[model.table_name][:fks].find { |k, v| v[:assoc_name] == f.first.to_s }) &&
       fk.last.fetch(:polymorphic, nil)
      poly_types = fk.last.fetch(:inverse_table, nil)&.each_with_object([]) do |poly_table, s|
        s << Object.const_get(::Brick.relations[poly_table][:class_name])
      end
      if poly_types.present?
        f.last[:polymorphic_as] = f.first
        f.last[:types] = poly_types
      end
    end
    kwargs = f.last.is_a?(Hash) ? f.pop : {}
    obj.send(:field, *f, **kwargs)
  end
end

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.auto_modelsObject

Returns the value of attribute auto_models.



110
111
112
# File 'lib/brick.rb', line 110

def auto_models
  @auto_models
end

.db_schemasObject

Returns the value of attribute db_schemas.



110
111
112
# File 'lib/brick.rb', line 110

def db_schemas
  @db_schemas
end

.default_schemaObject

Returns the value of attribute default_schema.



110
111
112
# File 'lib/brick.rb', line 110

def default_schema
  @default_schema
end

.established_drfObject

Returns the value of attribute established_drf.



110
111
112
# File 'lib/brick.rb', line 110

def established_drf
  @established_drf
end

.initializer_loadedObject

Returns the value of attribute initializer_loaded.



110
111
112
# File 'lib/brick.rb', line 110

def initializer_loaded
  @initializer_loaded
end

.is_eager_loadingObject

Returns the value of attribute is_eager_loading.



110
111
112
# File 'lib/brick.rb', line 110

def is_eager_loading
  @is_eager_loading
end

.is_oracleObject

Returns the value of attribute is_oracle.



110
111
112
# File 'lib/brick.rb', line 110

def is_oracle
  @is_oracle
end

.routes_doneObject

Returns the value of attribute routes_done.



5
6
7
# File 'lib/brick/route_mapper.rb', line 5

def routes_done
  @routes_done
end

.table_name_lookupObject

Returns the value of attribute table_name_lookup.



110
111
112
# File 'lib/brick.rb', line 110

def table_name_lookup
  @table_name_lookup
end

.test_schemaObject

Returns the value of attribute test_schema.



110
111
112
# File 'lib/brick.rb', line 110

def test_schema
  @test_schema
end

Class Method Details

._add_bt_and_hm(fk, relations, polymorphic_class = nil, is_optional = false) ⇒ Object



2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
# File 'lib/brick/extensions.rb', line 2878

def _add_bt_and_hm(fk, relations, polymorphic_class = nil, is_optional = false)
  bt_assoc_name = ::Brick.namify(fk[2].dup, :downcase)
  unless polymorphic_class
    bt_assoc_name = if bt_assoc_name.underscore.end_with?('_id')
                      bt_assoc_name[-3] == '_' ? bt_assoc_name[0..-4] : bt_assoc_name[0..-3]
                    elsif bt_assoc_name.downcase.end_with?('id') && bt_assoc_name.exclude?('_')
                      bt_assoc_name[0..-3] # Make the bold assumption that we can just peel off any final ID part
                    else
                      "#{bt_assoc_name}_bt"
                    end
  end
  bt_assoc_name = "#{bt_assoc_name}_" if bt_assoc_name == 'attribute'

  # %%% Temporary schema patch
  for_tbl = fk[1]
  fk_namified = ::Brick.namify(fk[1])
  fk[0] = ::Brick.apartment_default_tenant if ::Brick.is_apartment_excluded_table(fk_namified)
  fk[1] = "#{fk[0]}.#{fk[1]}" if fk[0] # && fk[0] != ::Brick.default_schema
  bts = (relation = relations.fetch(fk[1], nil))&.fetch(:fks) { relation[:fks] = {} }

  # %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
  # Maybe it's already gotten this info because we got as far as to say there was a unique class
  primary_table = if (is_class = fk[4].is_a?(Hash) && fk[4].key?(:class))
                    pri_tbl = (primary_class = fk[4][:class].constantize).table_name
                    if (pri_tbl_parts = pri_tbl.split('.')).length > 1
                      fk[3] = pri_tbl_parts.first
                    end
                  else
                    is_schema = if ::Brick.config.schema_behavior[:multitenant]
                                  # If Apartment gem lists the primary table as being associated with a non-tenanted model
                                  # then use 'public' schema for the primary table
                                  if ::Brick.is_apartment_excluded_table(fk[4])
                                    fk[3] = ::Brick.apartment_default_tenant
                                    true
                                  end
                                else
                                  fk[3] && fk[3] != ::Brick.default_schema && fk[3] != 'public'
                                end
                    pri_tbl = fk[4]
                    is_schema ? "#{fk[3]}.#{pri_tbl}" : pri_tbl
                  end
  hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class

  unless (cnstr_name = fk[5])
    # For any appended references (those that come from config), arrive upon a definitely unique constraint name
    pri_tbl = is_class ? fk[4][:class].underscore : pri_tbl
    pri_tbl = "#{bt_assoc_name}_#{pri_tbl}" if pri_tbl&.singularize != bt_assoc_name
    cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", nil, bts, hms)
    missing = []
    missing << fk[1] unless relations.key?(fk[1])
    missing << primary_table unless is_class || relations.key?(primary_table)
    unless missing.empty?
      tables = relations.reject { |_k, v| v.is_a?(Hash) && v.fetch(:isView, nil) }.keys
                        .select { |table_name| table_name.is_a?(String) }.sort
      puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
      return
    end
    unless (cols = relations[fk[1]][:cols]).key?(fk[2]) || (polymorphic_class && cols.key?("#{fk[2]}_id") && cols.key?("#{fk[2]}_type"))
      columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
      puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[2]}. (Columns present in #{fk[1]} are #{columns.join(', ')}.)"
      return
    end
    if (redundant = bts.find { |_k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[1] && v[:fk] == fk[2] && v[:inverse_table] == primary_table })
      if is_class && !redundant.last.key?(:class)
        redundant.last[:primary_class] = primary_class # Round out this BT so it can find the proper :source for a HMT association that references an STI subclass
      else
        puts "Brick: Additional reference #{fk.inspect} is redundant and can be removed.  (Already established by #{redundant.first}.)"
      end
      return
    end
  end
  return unless bts # Rails 5.0 and older can have bts end up being nil

  if (assoc_bt = bts[cnstr_name])
    if polymorphic_class
      # Assuming same fk (don't yet support composite keys for polymorphics)
      assoc_bt[:inverse_table] << fk[4]
      assoc_bt[:polymorphic] << polymorphic_class
    else # Expect we could have a composite key going
      if assoc_bt[:fk].is_a?(String)
        assoc_bt[:fk] = [assoc_bt[:fk], fk[2]] unless fk[2] == assoc_bt[:fk]
      elsif assoc_bt[:fk].exclude?(fk[2])
        assoc_bt[:fk] << fk[2]
      end
      assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[2]}"
    end
  else
    inverse_table = [primary_table] if polymorphic_class
    assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[2], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
    assoc_bt[:optional] = true if is_optional ||
                                  (is_optional.nil? && !relations[fk[1]][:cols][fk[2]][3])
    assoc_bt[:polymorphic] = [polymorphic_class] if polymorphic_class
  end
  if is_class
    # For use in finding the proper :source for a HMT association that references an STI subclass
    assoc_bt[:primary_class] = primary_class
    # For use in finding the proper :inverse_of for a BT association that references an STI subclass
    # assoc_bt[:inverse_of] = primary_class.reflect_on_all_associations.find { |a| a.foreign_key == bt[1] }
  end

  return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[1] == exclusion[0] && fk[2] == exclusion[1] && primary_table == exclusion[2] } || hms.nil?

  if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
    if assoc_hm[:fk].is_a?(String)
      assoc_hm[:fk] = [assoc_hm[:fk], fk[2]] unless fk[2] == assoc_hm[:fk]
    elsif assoc_hm[:fk].exclude?(fk[2])
      assoc_hm[:fk] << fk[2]
    end
    assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
  else
    inv_tbl = if ::Brick.config.schema_behavior[:multitenant] && Object.const_defined?('Apartment') && fk[0] == ::Brick.apartment_default_tenant
                for_tbl
              else
                fk[1]
              end
    assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[2], assoc_name: fk_namified.pluralize, alternate_name: bt_assoc_name,
                                      inverse_table: inv_tbl, inverse: assoc_bt }
    assoc_hm[:polymorphic] = true if polymorphic_class
    hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
    this_hm_count = hm_counts[fk[1]] = hm_counts.fetch(fk[1]) { 0 } + 1
  end
  assoc_bt[:inverse] = assoc_hm
end

._brick_index(tbl_name, mode = nil, separator = nil, relation = nil, not_path = nil) ⇒ Object



3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
# File 'lib/brick/extensions.rb', line 3148

def _brick_index(tbl_name, mode = nil, separator = nil, relation = nil, not_path = nil)
  separator ||= '_'
  relation ||= ::Brick.relations.fetch(tbl_name, nil)
  if mode == :migration
    res_name = tbl_name
  else
    res_name = (tbl_name_parts = tbl_name.split('.'))[0..-2].first
    res_name << '.' if res_name
    (res_name ||= +'') << (relation&.fetch(:resource, nil) || tbl_name_parts.last)
  end

  res_parts = ((mode == :singular) ? res_name.singularize : res_name).split('.')
  res_parts.shift if ::Brick.apartment_multitenant && res_parts.length > 1 && res_parts.first == ::Brick.apartment_default_tenant
  index2 = []
  if ::Brick.config.path_prefix
    res_parts.unshift(::Brick.config.path_prefix)
    index2 << ::Brick.config.path_prefix
  end
  if (aps = relation&.fetch(:auto_prefixed_schema, nil)) # && res_parts.last.start_with?(aps)
    aps = aps[0..-2] if aps[-1] == '_'
    last_part = res_parts.last # [aps.length..-1]
    res_parts[-1] = aps
    res_parts << last_part
    index2 << aps
  end
  index = res_parts.map(&:underscore).join(separator)
  if separator == 'x'
    index = index.tr('_', 'x')
  else
    # Rails applies an _index suffix to that route when the resource name isn't something plural
    index << '_index' if mode != :singular && !not_path &&
                         index == (
                                    index2 + [relation[:class_name][(relation&.fetch(:auto_prefixed_class, nil)&.length&.+ 2) || 0..-1]&.underscore&.tr('/', '_') || '_']
                                  ).join(separator)
  end
  index
end

._class_pk(dotted_name, multitenant) ⇒ Object



3193
3194
3195
# File 'lib/brick/extensions.rb', line 3193

def _class_pk(dotted_name, multitenant)
  Object.const_get((multitenant ? [dotted_name.split('.').last] : dotted_name.split('.')).map { |nm| "::#{nm.singularize.camelize}" }.join).primary_key
end

._find_csv_separator(stream, likely_separator) ⇒ Object



856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
# File 'lib/brick.rb', line 856

def _find_csv_separator(stream, likely_separator)
  line = nil
  last_char = nil
  in_quotes = false
  ret = +''
  while true
    (line = stream.readline).each_char do |ch|
      likely_separator[ch] += 2 if !in_quotes && last_char == '"' && ch != "\n"
      if ch == '"'
        in_quotes = !in_quotes
        likely_separator[last_char] += 2 if in_quotes && last_char != "\n"
      end
      last_char = ch
    end
    ret << line
    break if !in_quotes || stream.eof?
  end
  likely_separator[line[0]] += 1
  likely_separator[line[-2]] += 1
  ret
end

.additional_references=(ars) ⇒ Object

Additional table associations to use (Think of these as virtual foreign keys perhaps)



485
486
487
488
489
490
491
492
# File 'lib/brick.rb', line 485

def additional_references=(ars)
  if ars
    ars = ars.call if ars.is_a?(Proc)
    ars = ars.to_a unless ars.is_a?(Array)
    ars = [ars] unless ars.empty? || ars.first.is_a?(Array)
    Brick.config.additional_references = ars
  end
end

.always_load_fields=(field_set) ⇒ Object



629
630
631
# File 'lib/brick.rb', line 629

def always_load_fields=(field_set)
  Brick.config.always_load_fields = field_set
end

.apartment_default_tenantObject



178
179
180
# File 'lib/brick.rb', line 178

def apartment_default_tenant
  Apartment.default_tenant || 'public'
end

.apartment_multitenantObject



171
172
173
174
175
176
# File 'lib/brick.rb', line 171

def apartment_multitenant
  if @apartment_multitenant.nil?
    @apartment_multitenant = ::Brick.config.schema_behavior[:multitenant] && Object.const_defined?('Apartment')
  end
  @apartment_multitenant
end

.api_filterObject



438
439
440
# File 'lib/brick.rb', line 438

def api_filter
  Brick.config.api_filter
end

.api_filter=(proc) ⇒ Object



433
434
435
# File 'lib/brick.rb', line 433

def api_filter=(proc)
  Brick.config.api_filter = proc
end

.api_root=(path) ⇒ Object



418
419
420
# File 'lib/brick.rb', line 418

def api_root=(path)
  Brick.config.api_roots = [path]
end

.api_rootsObject



428
429
430
# File 'lib/brick.rb', line 428

def api_roots
  Brick.config.api_roots
end

.api_roots=(paths) ⇒ Object



423
424
425
# File 'lib/brick.rb', line 423

def api_roots=(paths)
  Brick.config.api_roots = paths
end

.apply_double_underscore_patchObject



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

def apply_double_underscore_patch
  unless @double_underscore_applied
    # Same as normal #camelize and #underscore, just that double-underscores turn into a single underscore
    ActiveSupport::Inflector.class_eval do
      def camelize(term, uppercase_first_letter = true)
        strings = term.to_s.split('__').map do |string|
          # String#camelize takes a symbol (:upper or :lower), so here we also support :lower to keep the methods consistent.
          if !uppercase_first_letter || uppercase_first_letter == :lower
            string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase! || match }
          else
            string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize! || match }
          end
          string.gsub!(/(?:_|(\/))([a-z\d]*)/i) do
            word = $2
            substituted = inflections.acronyms[word] || word.capitalize! || word
            $1 ? "::#{substituted}" : substituted
          end
          string
        end
        strings.join('_')
      end

      def underscore(camel_cased_word)
        return camel_cased_word.to_s unless /[A-Z-]|::/.match?(camel_cased_word)
        regex = inflections.respond_to?(:acronyms_underscore_regex) ? inflections.acronyms_underscore_regex : inflections.acronym_regex
        camel_cased_word.to_s.gsub('::', '/').split('_').map do |word|
          word.gsub!(regex) { "#{$1 && '_' }#{$2.downcase}" }
          word.gsub!(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { ($1 || $2) << '_' }
          word.tr!('-', '_')
          word.downcase!
          word
        end.join('__')
      end
    end
    @double_underscore_applied = true
  end
end

.ar_baseObject



3002
3003
3004
# File 'lib/brick/extensions.rb', line 3002

def ar_base
  @ar_base ||= Object.const_defined?(:ApplicationRecord) ? ApplicationRecord : Class.new(ActiveRecord::Base)
end

.ar_tablesObject



519
520
521
522
523
524
525
526
527
# File 'lib/brick/reflect_tables.rb', line 519

def ar_tables
  ar_smtn = if ActiveRecord::Base.respond_to?(:schema_migrations_table_name)
              ActiveRecord::Base.schema_migrations_table_name
            else
              'schema_migrations'
            end
  ar_imtn = ActiveRecord.version >= ::Gem::Version.new('5.0') ? ActiveRecord::Base. : 'ar_internal_metadata'
  [ar_smtn, ar_imtn]
end

.avo_3x_resource(model, requested) ⇒ Object



1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
# File 'lib/brick/extensions.rb', line 1586

def self.avo_3x_resource(model, requested)
  built_resource = Class.new(Avo::BaseResource) do
    self.model_class = model
    self.title = :brick_descrip
    self.includes = []
    define_method :fields do # Have to be inside of a fields method
      ::Brick::ADD_AVO_FIELDS.call(self, model)
    end
  end
  ::Avo::Resources.const_set(requested.to_sym, built_resource)
  built_resource
end

.config {|@config| ... } ⇒ Object Also known as: configure

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns Brick’s global configuration object, a singleton. These settings affect all threads.

Yields:



723
724
725
726
727
# File 'lib/brick.rb', line 723

def config
  @config ||= Brick::Config.instance
  yield @config if block_given?
  @config
end

.controllers_inherit_from=(value) ⇒ Object



458
459
460
# File 'lib/brick.rb', line 458

def controllers_inherit_from=(value)
  Brick.config.controllers_inherit_from = value
end

.ctrl_to_klass(ctrl_path, res_names = {}) ⇒ Object

Attempt to determine an ActiveRecord::Base class and additional STI information when given a controller’s path



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

def ctrl_to_klass(ctrl_path, res_names = {})
  klass = nil
  sti_type = nil

  if res_names.empty?
    ::Brick.relations.each_with_object({}) do |v, s|
      next if v.first.is_a?(Symbol)

      v_parts = v.first.split('.')
      v_parts.shift if v_parts.first == 'public'
      res_names[v_parts.join('.')] = v.first
    end
  end

  c_path_parts = ctrl_path.split('/')
  found = nil
  while c_path_parts.present?
    possible_c_path = c_path_parts.join('.')
    possible_c_path_singular = c_path_parts[0..-2] + [c_path_parts.last.singularize]
    possible_sti = possible_c_path_singular.join('/').camelize
    break if (
               res_name = res_names[found = possible_c_path] ||
                          ((klass = Brick.config.sti_namespace_prefixes.key?("::#{possible_sti}") && possible_sti.constantize) &&
                           (sti_type = possible_sti)) ||
                          # %%% Used to have the more flexible:  (DidYouMean::SpellChecker.new(dictionary: res_names.keys).correct(possible_c_path)).first
                          res_names[found = possible_c_path] || res_names[found = possible_c_path_singular.join('.')] ||
                          ((::Brick.config.table_name_prefixes.key?(tn_prefix = c_path_parts.first) ||
                            ::Brick.config.table_name_prefixes.key?(tn_prefix = "#{c_path_parts.first}_")) &&
                           res_names[found = tn_prefix + c_path_parts.last]
                          )
             ) &&
             (
               klass ||
               ((rel = ::Brick.relations.fetch(res_name, nil)) &&
               (klass ||= rel[:class_name]&.constantize))
             )
    c_path_parts.shift
  end
  [klass, sti_type, found]
end

.custom_columns=(cust_cols) ⇒ Object

Custom columns to add to a table, minimally defined with a name and DSL string.



511
512
513
514
515
516
# File 'lib/brick.rb', line 511

def custom_columns=(cust_cols)
  if cust_cols
    cust_cols = cust_cols.call if cust_cols.is_a?(Proc)
    Brick.config.custom_columns = cust_cols
  end
end

.default_route_fallback=(resource_name) ⇒ Object



617
618
619
# File 'lib/brick.rb', line 617

def default_route_fallback=(resource_name)
  Brick.config.default_route_fallback = resource_name
end

.defer_references_for_generation=(drfg) ⇒ Object

References to disregard when auto-building migrations, models, and seeds



496
497
498
499
500
501
# File 'lib/brick.rb', line 496

def defer_references_for_generation=(drfg)
  if drfg
    drfg = [drfg] unless drfg.empty? || drfg.first.is_a?(Array)
    Brick.config.defer_references_for_generation = drfg
  end
end

.display_classes(prefix, rels, max_length) ⇒ Object



768
769
770
771
772
773
774
# File 'lib/brick.rb', line 768

def display_classes(prefix, rels, max_length)
  rels.sort.each do |rel|
    ::Brick.auto_models << rel.first
    puts "#{rel.first}#{' ' * (max_length - rel.first.length)}  /#{prefix}#{"#{rel[1]}/" if rel[1]}#{rel.last}"
  end
  puts "\n"
end

.drfgsObject



503
504
505
506
507
# File 'lib/brick.rb', line 503

def drfgs
  (::Brick.config.defer_references_for_generation || []).each_with_object({}) do |drfg, s|
    s[drfg.first] = [drfg[1..-1]]
  end
end

.eager_load_classes(do_ar_abstract_bases = false) ⇒ Object



734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
# File 'lib/brick.rb', line 734

def eager_load_classes(do_ar_abstract_bases = false)
  ::Brick.is_eager_loading = true
  if ::ActiveSupport.version < ::Gem::Version.new('6') ||
     ::Rails.configuration.instance_variable_get(:@autoloader) == :classic
    if ::ActiveSupport.version < ::Gem::Version.new('4')
      ::Rails.application.eager_load!
    else
      ::Rails.configuration.eager_load_namespaces.select { |ns| ns < ::Rails::Application }.each(&:eager_load!)
    end
  else
    # Same as:  Zeitwerk::Loader.eager_load_all -- plus retry when something skips a beat
    Zeitwerk::Registry.loaders.each { |loader| load_with_retry(loader) }
  end
  abstract_ar_bases = if do_ar_abstract_bases
                        ActiveRecord::Base.descendants.select { |ar| ar.abstract_class? }.map(&:name)
                      end
  ::Brick.is_eager_loading = false
  abstract_ar_bases
end

.enable_apiObject



413
414
415
# File 'lib/brick.rb', line 413

def enable_api
  Brick.config.enable_api
end

.enable_api=(path) ⇒ Object



408
409
410
# File 'lib/brick.rb', line 408

def enable_api=(path)
  Brick.config.enable_api = path
end

.enable_controllers=(value) ⇒ Object

Switches Brick auto-controllers on or off, for all threads



370
371
372
# File 'lib/brick.rb', line 370

def enable_controllers=(value)
  Brick.config.enable_controllers = value
end

.enable_controllers?Boolean

Returns ‘true` if Brick controllers are on, `false` otherwise. This affects all threads. Enabled by default.

Returns:

  • (Boolean)


377
378
379
# File 'lib/brick.rb', line 377

def enable_controllers?
  !!Brick.config.enable_controllers
end

.enable_models=(value) ⇒ Object

Switches Brick auto-models on or off, for all threads



357
358
359
# File 'lib/brick.rb', line 357

def enable_models=(value)
  Brick.config.enable_models = value
end

.enable_models?Boolean

Returns ‘true` if Brick models are on, `false` otherwise. This affects all threads. Enabled by default.

Returns:

  • (Boolean)


364
365
366
# File 'lib/brick.rb', line 364

def enable_models?
  !!Brick.config.enable_models
end

.enable_routes=(value) ⇒ Object

Switches Brick auto-routes on or off, for all threads



396
397
398
# File 'lib/brick.rb', line 396

def enable_routes=(value)
  Brick.config.enable_routes = value
end

.enable_routes?Boolean

Returns ‘true` if Brick routes are on, `false` otherwise. This affects all threads. Enabled by default.

Returns:

  • (Boolean)


403
404
405
# File 'lib/brick.rb', line 403

def enable_routes?
  !!Brick.config.enable_routes
end

.enable_views=(value) ⇒ Object

Switches Brick auto-views on or off, for all threads



383
384
385
# File 'lib/brick.rb', line 383

def enable_views=(value)
  Brick.config.enable_views = value
end

.enable_views?Boolean

Returns ‘true` if Brick views are on, `false` otherwise. This affects all threads. Enabled by default.

Returns:

  • (Boolean)


390
391
392
# File 'lib/brick.rb', line 390

def enable_views?
  !!Brick.config.enable_views
end

.ensure_unique(name, delimiter, *sources) ⇒ Object



3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
# File 'lib/brick/extensions.rb', line 3060

def ensure_unique(name, delimiter, *sources)
  base = name
  delimiter ||= '_'
  # By default ends up building this regex:  /_(\d+)$/
  if (added_num = name.slice!(Regexp.new("#{delimiter}(\d+)$")))
    added_num = added_num[1..-1].to_i
  else
    added_num = 1
  end
  while (
    name = "#{base}#{delimiter}#{added_num += 1}"
    sources.each_with_object(nil) do |v, s|
      s || case v
           when Hash
             v.key?(name)
           when Array
             v.include?(name)
           end
    end
  )
  end
  name
end

.exclude_column(table, col) ⇒ Object



325
326
327
328
# File 'lib/brick.rb', line 325

def exclude_column(table, col)
  puts "Excluding #{table}.#{col}"
  true
end

.exclude_hms=(skips) ⇒ Object

Skip creating a has_many association for these (Uses the same exact three-part format as would define an additional_reference)



526
527
528
529
530
531
532
533
# File 'lib/brick.rb', line 526

def exclude_hms=(skips)
  if skips
    skips = skips.call if skips.is_a?(Proc)
    skips = skips.to_a unless skips.is_a?(Array)
    skips = [skips] unless skips.empty? || skips.first.is_a?(Array)
    Brick.config.exclude_hms = skips
  end
end

.exclude_tables=(value) ⇒ Object



448
449
450
# File 'lib/brick.rb', line 448

def exclude_tables=(value)
  Brick.config.exclude_tables = value
end

.existing_stisObject



106
107
108
# File 'lib/brick.rb', line 106

def existing_stis
  @existing_stis ||= Brick.config.sti_namespace_prefixes.each_with_object({}) { |snp, s| s[snp.first[2..-1]] = snp.last unless snp.first.end_with?('::') }
end

.find_col_renaming(api_ver_path, relation_name) ⇒ Object



3186
3187
3188
3189
3190
3191
# File 'lib/brick/extensions.rb', line 3186

def find_col_renaming(api_ver_path, relation_name)
  ::Brick.config.api_column_renaming&.fetch(
    api_ver_path,
    ::Brick.config.api_column_renaming&.fetch(relation_name, nil)
  )
end

.find_orphans(multi_schema) ⇒ Object

Locate orphaned records



3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
# File 'lib/brick/extensions.rb', line 3085

def find_orphans(multi_schema)
  is_default_schema = multi_schema&.==(::Brick.apartment_default_tenant)
  relations.each_with_object([]) do |v, s|
    frn_tbl = v.first
    next if frn_tbl.is_a?(Symbol) || # Skip internal metadata entries
            (relation = v.last).key?(:isView) || config.exclude_tables.include?(frn_tbl) ||
            !(for_pk = (relation[:pkey].values.first&.first))

    is_default_frn_schema = !is_default_schema && multi_schema &&
                            ((frn_parts = frn_tbl.split('.')).length > 1 && frn_parts.first)&.==(::Brick.apartment_default_tenant)
    relation[:fks].select { |_k, assoc| assoc[:is_bt] }.each do |_k, bt|
      begin
        if bt.key?(:polymorphic)
          pri_pk = for_pk
          pri_tables = Brick.config.polymorphics["#{frn_tbl}.#{bt[:fk]}"]
                            .each_with_object(Hash.new { |h, k| h[k] = [] }) do |pri_class, s|
            s[Object.const_get(pri_class).table_name] << pri_class
          end
          fk_id_col = "#{bt[:fk]}_id"
          fk_type_col = "#{bt[:fk]}_type"
          selects = []
          pri_tables.each do |pri_tbl, pri_types|
            # Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
            # are both in the "public" schema
            next if is_default_frn_schema &&
                    ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(::Brick.apartment_default_tenant)

            selects << "SELECT '#{pri_tbl}' AS pri_tbl, frn.#{fk_type_col} AS pri_type, frn.#{fk_id_col} AS pri_id, frn.#{for_pk} AS frn_id
            FROM #{frn_tbl} AS frn
              LEFT OUTER JOIN #{pri_tbl} AS pri ON pri.#{pri_pk} = frn.#{fk_id_col}
            WHERE frn.#{fk_type_col} IN (#{
              pri_types.map { |pri_type| "'#{pri_type}'" }.join(', ')
            }) AND frn.#{bt[:fk]}_id IS NOT NULL AND pri.#{pri_pk} IS NULL\n"
          end
          ActiveRecord::Base.execute_sql(selects.join("UNION ALL\n")).each do |o|
            entry = [frn_tbl, o['frn_id'], o['pri_type'], o['pri_id'], fk_id_col]
            entry << o['pri_tbl'] if (pri_class = Object.const_get(o['pri_type'])) != pri_class.base_class
            s << entry
          end
        else
          # Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
          # are both in the "public" schema
          pri_tbl = bt.key?(:inverse_table) && bt[:inverse_table]
          next if is_default_frn_schema &&
                  ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(::Brick.apartment_default_tenant)

          pri_pk = relations[pri_tbl].fetch(:pkey, nil)&.values&.first&.first ||
                   _class_pk(pri_tbl, multi_schema)
          ActiveRecord::Base.execute_sql(
            "SELECT frn.#{bt[:fk]} AS pri_id, frn.#{for_pk} AS frn_id
            FROM #{frn_tbl} AS frn
              LEFT OUTER JOIN #{pri_tbl} AS pri ON pri.#{pri_pk} = frn.#{bt[:fk]}
            WHERE frn.#{bt[:fk]} IS NOT NULL AND pri.#{pri_pk} IS NULL
            ORDER BY 1, 2"
          ).each { |o| s << [frn_tbl, o['frn_id'], pri_tbl, o['pri_id'], bt[:fk]] }
        end
      rescue StandardError => err
        puts "Strange -- #{err.inspect}"
      end
    end
  end
end

.gem_versionObject

Returns Brick’s ‘::Gem::Version`, convenient for comparisons. This is recommended over `::Brick::VERSION::STRING`.



704
705
706
# File 'lib/brick.rb', line 704

def gem_version
  ::Gem::Version.new(VERSION::STRING)
end

.get_bts_and_hms(model, recalculate = nil) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/brick.rb', line 204

def get_bts_and_hms(model, recalculate = nil)
  if !recalculate && (ret = model.instance_variable_get(:@_brick_bts_and_hms))
    return ret
  end

  model_cols = model.columns_hash
  bts, hms = model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
    if a.belongs_to? && !a.polymorphic? && ::Brick.config.polymorphics.fetch(full_assoc_name = "#{model.table_name}.#{a.name}", nil)
      puts "Based on inclusion in ::Brick.polymorphics, marking association #{full_assoc_name} as being polymorphic."
      a.options[:polymorphic] = true
    end
    if (through = a.options[:through]) && !a.through_reflection
      puts "WARNING:  In model #{model.name}, unable to utilise:  has_many #{a.name}, through: #{through.inspect}"
    end
    primary_klass = if a.belongs_to?
                      a.polymorphic? ? nil : a.klass
                    else
                      a.active_record
                    end
    a_pk = a.options.fetch(:primary_key, nil)&.to_s ||
           primary_klass&.primary_key # Was:  a.klass.primary_key
    if !a.polymorphic? && (a.belongs_to? || (through && (thr = a.through_reflection))) &&
       !((kls = thr&.klass || a.klass) && ::Brick.config.exclude_tables.exclude?(kls.table_name) &&
          (!a.belongs_to? ||
            ((fk_type = a.foreign_key.is_a?(Array) ? a.foreign_key.map { |fk_part| model_cols[fk_part.to_s].type } : model_cols[a.foreign_key.to_s]&.type) &&
             (primary_cols = primary_klass.columns_hash) &&
             (pk_type = a_pk.is_a?(Array) ? a_pk.map { |pk_part| primary_cols[pk_part.to_s].type } : primary_cols[a_pk].type) &&
             (same_type = (pk_type == fk_type))
            )
          )
        )
      if same_type == false # We really do want to test specifically for false here, and not nil!
        puts "WARNING:
  Foreign key column #{a.klass.table_name}.#{a.foreign_key} is #{fk_type}, but the primary key it relates to, #{primary_klass.table_name}.#{a_pk}, is #{pk_type}.
  These columns should both be of the same type."
      end
      next
    end

    if a.belongs_to?
      if a.polymorphic?
        rel_poly_bt = relations[model.table_name][:fks].find { |_k, fk| fk[:assoc_name] == a.name.to_s }
        if (primary_tables = rel_poly_bt&.last&.fetch(:inverse_table, [])).is_a?(Array)
          models = rel_poly_bt[1][:polymorphic]&.map { |table| table.singularize.camelize.constantize }
          s.first[a.foreign_key.to_s] = [a.name, models, true]
        else
          # This will come up when using Devise invitable when invited_by_class_name is not
          # specified because in that circumstance it adds a polymorphic :invited_by association,
          # along with appropriate invited_by_type and invited_by_id columns.

          # See if any currently-loaded models have a has_many association over to this polymorphic belongs_to
          hm_models = ActiveRecord::Base.descendants.select do |m|
            m.reflect_on_all_associations.any? { |assoc| !assoc.belongs_to? && assoc.options[:as]&.to_sym == a.name }
          end
          # No need to include models with no table, or subclassed models if their parent is already in the list
          hm_models.reject! { |m| !m.table_exists? || hm_models.any? { |parent| parent != m && m < parent } }
          if hm_models.empty?
            puts "Missing any real indication as to which models \"has_many\" this polymorphic BT in model #{a.active_record.name}:"
            puts "  belongs_to :#{a.name}, polymorphic: true"
          else
            puts "Having analysed all currently-loaded models to infer the various polymorphic has_many associations for #{model.name}, here are the current results:"
            puts "::Brick.polymorphics = { \"#{model.table_name}.#{a.name}\" =>
                     #{hm_models.map(&:name).inspect}
                   }"
            puts 'If you add the above to your brick.rb, it will "cement" these options into place, and avoid this lookup process.'
            s.first[a.foreign_key.to_s] = [a.name, hm_models, true]
          end
        end
      else
        bt_key = a.foreign_key.is_a?(Array) ? a.foreign_key : a.foreign_key.to_s
        s.first[bt_key] = [a.name, a.klass]
      end
    else # This gets all forms of has_many and has_one
      if through # has_many :through or has_one :through
        is_invalid_source = nil
        begin
          if a.through_reflection.macro != :has_many # This HM goes through either a belongs_to or a has_one, so essentially a HOT?
            # Treat it like a belongs_to - just keyed on the association name instead of a foreign_key
            s.first[a.name] = [a.name, a.klass]
            next
          elsif !a.source_reflection # Had considered:  a.active_record.reflect_on_association(a.source_reflection_name).nil?
            is_invalid_source = true
          end
        rescue
          is_invalid_source = true
        end
        if is_invalid_source
          puts "  due to invalid source #{a.source_reflection_name.inspect}."
          next
        end
      else
        this_fks = (this_fk = a.foreign_key).is_a?(Array) ? this_fk.uniq : [this_fk.to_s]
        if !a.options.key?(:as) && (this_fks - a.klass.column_names).length.positive?
          options = ", #{a.options.map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(', ')}" if a.options.present?
          puts "WARNING:  Model #{model.name} has this association:
        has_many :#{a.name}#{options}
      which expects column #{a.foreign_key} to exist in table #{a.klass.table_name}.  This column is missing."
          next

        end
      end
      s.last[a.name] = a
    end
  end
  # Mark has_manys that go to an associative ("join") table so that they are skipped in the UI,
  # as well as any possible polymorphic associations
  skip_hms = {}
  hms.each do |hmt|
    if (through = hmt.last.options[:through])
      # ::Brick.relations[hmt.last.through_reflection.table_name]
      skip_hms[through] = nil if hms[through] && model.is_brick? &&
                                 hmt.last.klass != hmt.last.active_record && # Don't pull HMs for HMTs that point back to the same table
                                 hmt[1].source_reflection.belongs_to? # Don't pull HMs when the HMT is built from HM -> HM
      # End up with a hash of HMT names pointing to join-table associations
      model._br_associatives[hmt.first] = hms[through] # || hms["#{(opt = hmt.last.options)[:through].to_s.singularize}_#{opt[:source].to_s.pluralize}".to_sym]
    end
  end
  skip_hms.each { |k, _v| hms.delete(k) }
  model.instance_variable_set(:@_brick_bts_and_hms, [bts, hms]) # Cache and return this result
end

.get_possible_schemasObject



116
117
118
119
120
121
122
123
# File 'lib/brick.rb', line 116

def get_possible_schemas
  if (possible_schemas = (multitenancy = ::Brick.config.schema_behavior&.[](:multitenant)) &&
                          multitenancy&.[](:schema_to_analyse))
    possible_schemas = [possible_schemas] unless possible_schemas.is_a?(Array)
    possible_schema = possible_schemas.find { |ps| ::Brick.db_schemas.key?(ps) }
  end
  [possible_schema, possible_schemas, multitenancy]
end

.get_status_of_resourcesObject

Identify built out routes, migrations, models, (and also soon controllers and views!) for each resource



3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
# File 'lib/brick/extensions.rb', line 3009

def get_status_of_resources
  rails_root = ::Rails.root.to_s
  migrations = if Dir.exist?(mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{rails_root}/db/migrate")
                 Dir["#{mig_path}/**/*.rb"].each_with_object(Hash.new { |h, k| h[k] = [] }) do |v, s|
                   File.read(v).split("\n").each_with_index do |line, line_idx|
                     # For all non-commented lines, look for any that have "create_table", "alter_table", or "drop_table"
                     if !line.lstrip.start_with?('#') &&
                        (idx = (line.index('create_table ') || line.index('create_table('))&.+(13)) ||
                        (idx = (line.index('alter_table ') || line.index('alter_table('))&.+(12)) ||
                        (idx = (line.index('drop_table ') || line.index('drop_table('))&.+(11))
                       tbl = line[idx..-1].match(/([:'"\w\.]+)/)&.captures&.first
                       if tbl
                         v = v[(rails_root.length)..-1] if v.start_with?(rails_root)
                         v = v[1..-1] if v.start_with?('/')
                         s[tbl.tr(':\'"', '').pluralize] << [v, line_idx + 1]
                       end
                     end
                   end
                 end
               end
  abstract_activerecord_bases = ::Brick.eager_load_classes(true)
  rails_root = ::Rails.root.to_s
  models = ::Brick.relations.each_with_object({}) do |rel, s|
             next if rel.first.is_a?(Symbol)

             begin
               if (model = rel.last[:class_name]&.constantize) &&
                  (inh = ActiveRecord::Base._brick_inheriteds[model]&.join(':'))
                 inh = inh[rails_root.length + 1..-1] if inh.start_with?(rails_root)
                 s[rel.first] = [inh, model]
               end
             rescue
             end
           end
  ::Brick.relations.each_with_object([]) do |v, s|
    next if v.first.is_a?(Symbol) # ||
    #       Brick.config.exclude_tables.include?(v.first)

    tbl_parts = v.first.split('.')
    tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
    res = tbl_parts.join('.')
    table_name = (model = models[res])&.last&.table_name
    table_name ||= begin
                     v.last[:class_name].constantize.table_name
                   rescue
                   end
    model = model.first if model.is_a?(Array)
    s << [v.first, table_name || v.first, migrations&.fetch(res, nil), model]
  end
end

.has_ones=(hos) ⇒ Object

Associations to treat as a has_one



543
544
545
546
547
548
549
550
551
552
553
554
# File 'lib/brick.rb', line 543

def has_ones=(hos)
  if hos
    hos = hos.call if hos.is_a?(Proc)
    hos = hos.to_a unless hos.is_a?(Array)
    hos = [hos] unless hos.empty? || hos.first.is_a?(Array)
    # Translate to being nested hashes
    Brick.config.has_ones = hos&.each_with_object(Hash.new { |h, k| h[k] = {} }) do |v, s|
      s[v.first][v[1]] = v[2] if v[1]
      s
    end
  end
end

.hmts=(assocs) ⇒ Object

Associations for which to auto-create a has_many _, through: _



564
565
566
# File 'lib/brick.rb', line 564

def hmts=(assocs)
  Brick.config.hmts = assocs
end

.is_apartment_excluded_table(tbl) ⇒ Object



3197
3198
3199
3200
3201
3202
3203
# File 'lib/brick/extensions.rb', line 3197

def is_apartment_excluded_table(tbl)
  if Object.const_defined?('Apartment')
    tbl_klass = (tnp = ::Brick.config.table_name_prefixes&.find { |k, _v| tbl.start_with?(k) }) ? +"#{tnp.last}::" : +''
    tbl_klass << tbl[tnp&.first&.length || 0..-1].singularize.camelize
    Apartment.excluded_models&.include?(tbl_klass)
  end
end

.is_geography?(val) ⇒ Boolean

Returns:

  • (Boolean)


340
341
342
# File 'lib/brick.rb', line 340

def is_geography?(val)
  val.length < 31 && (val.length - 6) % 8 == 0 && val[0..5].bytes == [230, 16, 0, 0, 1, 12]
end

.json_columns=(cols) ⇒ Object



581
582
583
# File 'lib/brick.rb', line 581

def json_columns=(cols)
  Brick.config.json_columns = cols
end

.license=(key) ⇒ Object



621
622
623
# File 'lib/brick.rb', line 621

def license=(key)
  Brick.config.license = key
end

.load_additional_referencesObject

Load additional references (virtual foreign keys) This is attempted early if a brick initialiser file is found, and then again as a failsafe at the end of our engine’s initialisation %%% Maybe look for differences the second time ‘round and just add new stuff instead of entirely deferring



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

def load_additional_references
  return if @_additional_references_loaded || ::Brick.config.mode != :on

  relations = ::Brick.relations
  if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
    is_optional = ActiveRecord.version >= ::Gem::Version.new('5.0')
    if ars
      ars.each do |ar|
        fk = ar.length < 5 ? [nil, +ar[0], ar[1], nil, +ar[2]] : [ar[0], +ar[1], ar[2], ar[3], +ar[4], ar[5]]
        ::Brick._add_bt_and_hm(fk, relations, false, is_optional)
      end
    end
    if (polys = ::Brick.config.polymorphics)
      if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.key?(schema)
        ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
      end
      missing_stis = {}
      polys.each do |k, v|
        table_name, poly = k.split('.')
        v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").each_with_object([]) { |result, s| s << result['typ'] if result['typ'] }
        v.each do |type|
          # Allow polymorphic BT to relate to an STI subclass
          base_type = ::Brick.config.sti_namespace_prefixes["::#{type}"] ||
                      ::Brick.config.sti_namespace_prefixes.find { |k, _v| k.end_with?('::') && type.start_with?(k[2..-1]) }&.last&.[](2..-1)
          if relations.key?(primary_table = (base_type || type).underscore.pluralize)
            ::Brick._add_bt_and_hm([nil, table_name, poly, nil, primary_table, "(brick) #{table_name}_#{poly}"], relations,
                                   type, # Polymorphic class
                                   is_optional)
          elsif relations.present?
            missing_stis[primary_table] = type unless ::Brick.existing_stis.key?(type)
          end
        end
      end
      unless missing_stis.empty?
        print "
You might be missing an STI namespace prefix entry for these tables:  #{missing_stis.keys.join(', ')}.
In config/initializers/brick.rb appropriate entries would look something like:
  Brick.sti_namespace_prefixes = {"
        puts missing_stis.map { |_k, missing_sti| "\n    '#{missing_sti}' => 'SomeParentModel'" }.join(',')
        puts "  }
(Just trade out SomeParentModel with some more appropriate one.)"
      end
    end
    @_additional_references_loaded = true
  end

  # Find associative tables that can be set up for has_many :through
  ::Brick.relations.each do |tbl_name, relation|
    next if tbl_name.is_a?(Symbol)

    tbl_cols = relation[:cols].keys
    fks = relation[:fks].each_with_object({}) { |fk, s| s[fk.last[:fk]] = [fk.last[:assoc_name], fk.last[:inverse_table]] if fk.last[:is_bt]; s }
    # Aside from the primary key and the metadata columns created_at, updated_at, and deleted_at, if this table
    # only has foreign keys then it can act as an associative table and thus be used with has_many :through.
    if fks.length > 1 && (
         ::Brick.config.treat_as_associative&.include?(tbl_name) ||
         (tbl_cols - fks.keys - (::Brick.config. || []) - (relation[:pkey].values.first || [])).length.zero?
       )
      fks.each { |fk| relation[:hmt_fks][fk.first] = fk.last }
    end
  end
end

.load_with_retry(loader, autoloaded = nil) ⇒ Object

Some classes (like Phlex::Testing::Rails) will successfully auto-load after a retry



755
756
757
758
759
760
761
762
763
764
765
766
# File 'lib/brick.rb', line 755

def load_with_retry(loader, autoloaded = nil)
  autoloaded ||= loader.send(:autoloaded_dirs).dup
  begin
    loader.eager_load
  rescue Zeitwerk::SetupRequired
    # This is fine -- we eager load what can be eager loaded
  rescue Zeitwerk::NameError
    if autoloaded != (new_auto = loader.send(:autoloaded_dirs))
      load_with_retry(loader, new_auto.dup) # Try one more time and it could come together
    end
  end
end

.metadata_columns=(value) ⇒ Object



474
475
476
# File 'lib/brick.rb', line 474

def (value)
  Brick.config. = value
end

.mode=(setting) ⇒ Object



345
346
347
# File 'lib/brick.rb', line 345

def mode=(setting)
  Brick.config.mode = setting
end

.model_descrips=(descrips) ⇒ Object

DSL templates for individual models to provide prettier descriptions of objects



592
593
594
# File 'lib/brick.rb', line 592

def model_descrips=(descrips)
  Brick.config.model_descrips = descrips
end

.models_inherit_from=(value) ⇒ Object



453
454
455
# File 'lib/brick.rb', line 453

def models_inherit_from=(value)
  Brick.config.models_inherit_from = value
end

.namify(name, action = nil) ⇒ Object

Convert spaces to underscores if the second character and onwards is mixed case



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/brick.rb', line 188

def namify(name, action = nil)
  has_uppers = name =~ /[A-Z]+/
  has_lowers = name =~ /[a-z]+/
  name.downcase! if has_uppers && action == :downcase
  if name.include?(' ')
    # All uppers or all lowers?
    if !has_uppers || !has_lowers
      name.titleize.tr(' ', '_')
    else # Mixed uppers and lowers -- just remove existing spaces
      name.tr(' ', '')
    end
  else
    action == :underscore ? name.underscore : name
  end
end

.nested_attributes=(anaf) ⇒ Object

Associations upon which to add #accepts_nested_attributes_for logic



558
559
560
# File 'lib/brick.rb', line 558

def nested_attributes=(anaf)
  Brick.config.nested_attributes = anaf
end

.non_tenanted_modelsObject

If multitenancy is enabled, a list of non-tenanted “global” models



183
184
185
# File 'lib/brick.rb', line 183

def non_tenanted_models
  @pending_models ||= {}
end

.not_nullables=(value) ⇒ Object



479
480
481
# File 'lib/brick.rb', line 479

def not_nullables=(value)
  Brick.config.not_nullables = value
end

.omit_empty_tables_in_dropdown=(setting) ⇒ Object



625
626
627
# File 'lib/brick.rb', line 625

def omit_empty_tables_in_dropdown=(setting)
  Brick.config.omit_empty_tables_in_dropdown = setting
end

.order=(value) ⇒ Object



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

def order=(value)
  Brick.config.order = value
end

.path_prefix=(path) ⇒ Object

Any path prefixing to apply to all auto-generated Brick routes



351
352
353
# File 'lib/brick.rb', line 351

def path_prefix=(path)
  Brick.config.path_prefix = path
end

.polymorphics=(polys) ⇒ Object

Polymorphic associations



575
576
577
578
# File 'lib/brick.rb', line 575

def polymorphics=(polys)
  polys = polys.each_with_object({}) { |poly, s| s[poly] = nil } if polys.is_a?(Array)
  Brick.config.polymorphics = polys || {}
end

.reflect_tablesObject

This is done separately so that during testing it can be called right after a migration in order to make sure everything is good.



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
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
# File 'lib/brick/reflect_tables.rb', line 5

def reflect_tables
  return unless ::Brick.config.mode == :on

  # return if ActiveRecord::Base.connection.current_database == 'postgres'

  # Overwrite SQLite's #begin_db_transaction so it opens in IMMEDIATE mode instead of
  # the default DEFERRED mode.
  #   https://discuss.rubyonrails.org/t/failed-write-transaction-upgrades-in-sqlite3/81480/2
  if ActiveRecord::Base.connection.adapter_name == 'SQLite'
    arca = ::ActiveRecord::ConnectionAdapters
    db_statements = arca::SQLite3.const_defined?('DatabaseStatements') ? arca::SQLite3::DatabaseStatements : arca::SQLite3::SchemaStatements
    # Rails 7.1 and later
    if arca::AbstractAdapter.private_instance_methods.include?(:with_raw_connection)
      db_statements.define_method(:begin_db_transaction) do
        log("begin immediate transaction", "TRANSACTION") do
          with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
            conn.transaction(:immediate)
          end
        end
      end
    else # Rails < 7.1
      db_statements.define_method(:begin_db_transaction) do
        log('begin immediate transaction', 'TRANSACTION') { @connection.transaction(:immediate) }
      end
    end
  end

  orig_schema = nil
  if (relations = ::Brick.relations).keys == [:db_name]
    # ::Brick.remove_instance_variable(:@_additional_references_loaded) if ::Brick.instance_variable_defined?(:@_additional_references_loaded)

    # Only for Postgres  (Doesn't work in sqlite3 or MySQL)
    # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect

    # ---------------------------
    # 1. Figure out schema things
    is_postgres = nil
    is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer'
    case ActiveRecord::Base.connection.adapter_name
    when 'PostgreSQL', 'SQLServer'
      is_postgres = !is_mssql
      db_schemas = if is_postgres
                     ActiveRecord::Base.execute_sql('SELECT nspname AS table_schema, MAX(oid) AS dt FROM pg_namespace GROUP BY 1 ORDER BY 1;')
                   else
                     ActiveRecord::Base.execute_sql('SELECT DISTINCT table_schema, NULL AS dt FROM INFORMATION_SCHEMA.tables;')
                   end
      ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
        row = case row
              when Array
                row
              else
                [row['table_schema'], row['dt']]
              end
        # Remove any system schemas
        s[row.first] = { dt: row.last } unless ['information_schema', 'pg_catalog', 'pg_toast', 'heroku_ext',
                                                'INFORMATION_SCHEMA', 'sys'].include?(row.first)
      end
      possible_schema, possible_schemas, multitenancy = ::Brick.get_possible_schemas
      if possible_schemas
        if possible_schema
          ::Brick.default_schema = ::Brick.apartment_default_tenant
          schema = possible_schema
          orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
          ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
        # When testing, just find the most recently-created schema
        elsif begin
                Rails.env == 'test' ||
                  ActiveRecord::Base.execute_sql("SELECT value FROM ar_internal_metadata WHERE key='environment';").first&.fetch('value', nil) == 'test'
              rescue
              end
          ::Brick.default_schema = ::Brick.apartment_default_tenant
          ::Brick.test_schema = schema = ::Brick.db_schemas.to_a.sort { |a, b| b.last[:dt] <=> a.last[:dt] }.first.first
          if possible_schema.blank?
            puts "While running tests, using the most recently-created schema, #{schema}."
          else
            puts "While running tests, had noticed in the brick.rb initializer that the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\" which does not exist.  Reading table structure from the most recently-created schema, #{schema}."
          end
          orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
          ::Brick.config.schema_behavior = { multitenant: {} } # schema_to_analyse: [schema]
          ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
        else
          puts "*** In the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to schema(s) called #{possible_schemas.map { |s| "\"#{s}\"" }.join(', ')}.  No mentioned schema exists. ***"
          if ::Brick.db_schemas.key?(::Brick.apartment_default_tenant)
            ::Brick.default_schema = schema = ::Brick.apartment_default_tenant
          end
        end
      end
    when 'Mysql2', 'Trilogy'
      ::Brick.default_schema = schema = ActiveRecord::Base.connection.current_database
    when 'OracleEnhanced'
      # ActiveRecord::Base.connection.current_database will be something like "XEPDB1"
      ::Brick.default_schema = schema = ActiveRecord::Base.connection.raw_connection.username
      ::Brick.db_schemas = {}
      ActiveRecord::Base.execute_sql("SELECT username FROM sys.all_users WHERE ORACLE_MAINTAINED != 'Y'").each { |s| ::Brick.db_schemas[s.first] = {} }
    when 'SQLite'
      sql = "SELECT m.name AS relation_name, UPPER(m.type) AS table_type,
        p.name AS column_name, p.type AS data_type,
        CASE p.pk WHEN 1 THEN 'PRIMARY KEY' END AS const
      FROM sqlite_master AS m
        INNER JOIN pragma_table_info(m.name) AS p
      WHERE m.name NOT IN ('sqlite_sequence', ?, ?)
      ORDER BY m.name, p.cid"
    else
      puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
    end

    ::Brick.db_schemas ||= {}

    # ---------------------
    # 2. Tables and columns
    # %%% Retrieve internal ActiveRecord table names like this:
    # ActiveRecord::Base.internal_metadata_table_name, ActiveRecord::Base.schema_migrations_table_name
    # For if it's not SQLite -- so this is the Postgres and MySQL version
    measures = []
    ::Brick.is_oracle = true if ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
    case ActiveRecord::Base.connection.adapter_name
    when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
      # schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
      retrieve_schema_and_tables(sql, is_postgres, is_mssql, schema).each do |r|
        # If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
        # is the default schema, usually 'public'.
        schema_name = if ::Brick.config.schema_behavior[:multitenant]
                        ::Brick.apartment_default_tenant if ::Brick.is_apartment_excluded_table(r['relation_name'])
                      elsif ![schema, 'public'].include?(r['schema'])
                        r['schema']
                      end
        relation_name = schema_name ? "#{schema_name}.#{r['relation_name']}" : r['relation_name']
        # Both uppers and lowers as well as underscores?
        ::Brick.apply_double_underscore_patch if relation_name =~ /[A-Z]/ && relation_name =~ /[a-z]/ && relation_name.index('_')
        relation = relations[relation_name]
        relation[:isView] = true if r['table_type'] == 'VIEW'
        relation[:description] = r['table_description'] if r['table_description']
        col_name = r['column_name']
        key = case r['const']
              when 'PRIMARY KEY'
                relation[:pkey][r['key'] || relation_name] ||= []
              when 'UNIQUE'
                relation[:ukeys][r['key'] || "#{relation_name}.#{col_name}"] ||= []
                # key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
                # key[r['key']]
              else
                if r['data_type'] == 'uuid'
                  # && r['column_name'] == ::Brick.ar_base.primary_key
                  # binding.pry
                  relation[:pkey][r['key'] || relation_name] ||= []
                end
              end
        # binding.pry if key && r['data_type'] == 'uuid'
        key << col_name if key
        cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
        cols[col_name] = [r['data_type'], r['max_length'], measures&.include?(col_name), r['is_nullable'] == 'NO']
        # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
        relation[:col_descrips][col_name] = r['column_description'] if r['column_description']
      end
    else # MySQL2, OracleEnhanced, and MSSQL act a little differently, bringing back an array for each row
      schema_and_tables = case ActiveRecord::Base.connection.adapter_name
                          when 'OracleEnhanced'
                            sql =
"SELECT c.owner AS schema, c.table_name AS relation_name,
  CASE WHEN v.owner IS NULL THEN 'BASE_TABLE' ELSE 'VIEW' END AS table_type,
  c.column_name, c.data_type,
  COALESCE(c.data_length, c.data_precision) AS max_length,
  CASE ac.constraint_type WHEN 'P' THEN 'PRIMARY KEY' END AS const,
  ac.constraint_name AS \"key\",
  CASE c.nullable WHEN 'Y' THEN 'YES' ELSE 'NO' END AS is_nullable
FROM all_tab_cols c
  LEFT OUTER JOIN all_cons_columns acc ON acc.owner = c.owner AND acc.table_name = c.table_name AND acc.column_name = c.column_name
  LEFT OUTER JOIN all_constraints ac ON ac.owner = acc.owner AND ac.table_name = acc.table_name AND ac.constraint_name = acc.constraint_name AND constraint_type = 'P'
  LEFT OUTER JOIN all_views v ON c.owner = v.owner AND c.table_name = v.view_name
WHERE c.owner IN (#{::Brick.db_schemas.keys.map { |s| "'#{s}'" }.join(', ')})
  AND c.table_name NOT IN (?, ?)
ORDER BY 1, 2, c.internal_column_id, acc.position"
                            ActiveRecord::Base.execute_sql(sql, *ar_tables)
                          else
                            retrieve_schema_and_tables(sql)
                          end

      schema_and_tables.each do |r|
        next if r[1].index('$') # Oracle can have goofy table names with $

        if (relation_name = r[1]) =~ /^[A-Z0-9_]+$/
          relation_name.downcase!
        # Both uppers and lowers as well as underscores?
        elsif relation_name =~ /[A-Z]/ && relation_name =~ /[a-z]/ && relation_name.index('_')
          ::Brick.apply_double_underscore_patch
        end
        # Expect the default schema for SQL Server to be 'dbo'.
        if (::Brick.is_oracle && r[0] != schema) || (is_mssql && r[0] != 'dbo')
          relation_name = "#{r[0]}.#{relation_name}"
        end

        relation = relations[relation_name] # here relation represents a table or view from the database
        relation[:isView] = true if r[2] == 'VIEW' # table_type
        col_name = ::Brick.is_oracle ? connection.send(:oracle_downcase, r[3]) : r[3]
        key = case r[6] # constraint type
              when 'PRIMARY KEY'
                # key
                relation[:pkey][r[7] || relation_name] ||= []
              when 'UNIQUE'
                relation[:ukeys][r[7] || "#{relation_name}.#{col_name}"] ||= []
                # key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
                # key[r['key']]
              end
        key << col_name if key
        cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
        # 'data_type', 'max_length', measure, 'is_nullable'
        cols[col_name] = [r[4], r[5], measures&.include?(col_name), r[8] == 'NO']
        # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
      end
    end

    # PostGIS adds three views which would confuse Rails if models were to be built for them.
    if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
      if relations.key?('geography_columns') && relations.key?('geometry_columns') && relations.key?('spatial_ref_sys')
        (::Brick.config.exclude_tables ||= []) << 'geography_columns'
        ::Brick.config.exclude_tables << 'geometry_columns'
        ::Brick.config.exclude_tables << 'spatial_ref_sys'
      end
    end

    # # Add unique OIDs
    # if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
    #   ActiveRecord::Base.execute_sql(
    #     "SELECT c.oid, n.nspname, c.relname
    #     FROM pg_catalog.pg_namespace AS n
    #       INNER JOIN pg_catalog.pg_class AS c ON n.oid = c.relnamespace
    #     WHERE c.relkind IN ('r', 'v')"
    #   ).each do |r|
    #     next if ['pg_catalog', 'information_schema', ''].include?(r['nspname']) ||
    #       ['ar_internal_metadata', 'schema_migrations'].include?(r['relname'])
    #     relation = relations.fetch(r['relname'], nil)
    #     if relation
    #       (relation[:oid] ||= {})[r['nspname']] = r['oid']
    #     else
    #       puts "Where is #{r['nspname']} #{r['relname']} ?"
    #     end
    #   end
    # end
    # schema = ::Brick.default_schema # Reset back for this next round of fun

    # ---------------------------------------------
    # 3. Foreign key info
    # (done in two parts which get JOINed together in Ruby code)
    kcus = nil
    entry_type = nil
    case ActiveRecord::Base.connection.adapter_name
    when 'PostgreSQL', 'Mysql2', 'Trilogy', 'SQLServer'
      # Part 1 -- all KCUs
      sql = "SELECT CONSTRAINT_CATALOG, CONSTRAINT_SCHEMA, CONSTRAINT_NAME, ORDINAL_POSITION,
                    TABLE_NAME, COLUMN_NAME
            FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE#{"
            WHERE CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }#{"
            WHERE CONSTRAINT_SCHEMA = '#{ActiveRecord::Base.connection.current_database&.tr("'", "''")}'" if ActiveRecord::Base.is_mysql
            }"
      kcus = ActiveRecord::Base.execute_sql(sql).each_with_object({}) do |v, s|
               if (entry_type ||= v.is_a?(Array) ? :array : :hash) == :hash
                 key = "#{v['constraint_name']}.#{v['constraint_schema']}.#{v['constraint_catalog']}.#{v['ordinal_position']}"
                 key << ".#{v['table_name']}.#{v['column_name']}" unless is_postgres || is_mssql
                 s[key] = [v['constraint_schema'], v['table_name']]
               else # Array
                 key = "#{v[2]}.#{v[1]}.#{v[0]}.#{v[3]}"
                 key << ".#{v[4]}.#{v[5]}" unless is_postgres || is_mssql
                 s[key] = [v[1], v[4]]
               end
             end

      # Part 2 -- fk_references
      sql = "SELECT kcu.CONSTRAINT_SCHEMA, kcu.TABLE_NAME, kcu.COLUMN_NAME,
          #{# These will get filled in with real values (effectively doing the JOIN in Ruby)
            is_postgres || is_mssql ? 'NULL as primary_schema, NULL as primary_table' :
                                      'kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME'},
          kcu.CONSTRAINT_NAME AS CONSTRAINT_SCHEMA_FK,
          rc.UNIQUE_CONSTRAINT_NAME, rc.UNIQUE_CONSTRAINT_SCHEMA, rc.UNIQUE_CONSTRAINT_CATALOG, kcu.ORDINAL_POSITION
        FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc
          INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu
            ON kcu.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG
            AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
            AND kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME#{"
        WHERE kcu.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema}#{"
        WHERE kcu.CONSTRAINT_SCHEMA = '#{ActiveRecord::Base.connection.current_database&.tr("'", "''")}'" if ActiveRecord::Base.is_mysql}"
      fk_references = ActiveRecord::Base.execute_sql(sql)
    when 'SQLite'
      sql = "SELECT NULL AS constraint_schema, m.name, fkl.\"from\", NULL AS primary_schema, fkl.\"table\", m.name || '_' || fkl.\"from\" AS constraint_name
      FROM sqlite_master m
        INNER JOIN pragma_foreign_key_list(m.name) fkl ON m.type = 'table'
      ORDER BY m.name, fkl.seq"
      fk_references = ActiveRecord::Base.execute_sql(sql)
    when 'OracleEnhanced'
      schemas = ::Brick.db_schemas.keys.map { |s| "'#{s}'" }.join(', ')
      sql =
      "SELECT -- fk
             ac.owner AS constraint_schema, acc_fk.table_name, acc_fk.column_name,
             -- referenced pk
             ac.r_owner AS primary_schema, acc_pk.table_name AS primary_table, acc_fk.constraint_name AS constraint_schema_fk
             -- , acc_pk.column_name
      FROM all_cons_columns acc_fk
        INNER JOIN all_constraints ac ON acc_fk.owner = ac.owner
          AND acc_fk.constraint_name = ac.constraint_name
        INNER JOIN all_cons_columns acc_pk ON ac.r_owner = acc_pk.owner
          AND ac.r_constraint_name = acc_pk.constraint_name
      WHERE ac.constraint_type = 'R'
        AND ac.owner IN (#{schemas})
        AND ac.r_owner IN (#{schemas})"
      fk_references = ActiveRecord::Base.execute_sql(sql)
    end
    ::Brick.is_oracle = true if ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
    # ::Brick.default_schema ||= schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
    ::Brick.default_schema ||= 'public' if is_postgres
    fk_references&.each do |fk|
      fk = fk.values unless fk.is_a?(Array)
      # Virtually JOIN KCUs to fk_references in order to fill in the primary schema and primary table
      kcu_key = "#{fk[6]}.#{fk[7]}.#{fk[8]}.#{fk[9]}"
      kcu_key << ".#{fk[3]}.#{fk[4]}" unless is_postgres || is_mssql
      if (kcu = kcus&.fetch(kcu_key, nil))
        fk[3] = kcu[0]
        fk[4] = kcu[1]
      end
      # Multitenancy makes things a little more general overall, except for non-tenanted tables
      if ::Brick.is_apartment_excluded_table(::Brick.namify(fk[1]))
        fk[0] = ::Brick.apartment_default_tenant
      elsif (is_postgres && (fk[0] == 'public' || (multitenancy && fk[0] == schema))) ||
            (::Brick.is_oracle && fk[0] == schema) ||
            (is_mssql && fk[0] == 'dbo') ||
            (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[0]))
        fk[0] = nil
      end
      if ::Brick.is_apartment_excluded_table(fk[4])
        fk[3] = ::Brick.apartment_default_tenant
      elsif (is_postgres && (fk[3] == 'public' || (multitenancy && fk[3] == schema))) ||
            (::Brick.is_oracle && fk[3] == schema) ||
            (is_mssql && fk[3] == 'dbo') ||
            (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[3]))
        fk[3] = nil
      end
      if ::Brick.is_oracle
        fk[1].downcase! if fk[1] =~ /^[A-Z0-9_]+$/
        fk[4].downcase! if fk[4] =~ /^[A-Z0-9_]+$/
        fk[2] = connection.send(:oracle_downcase, fk[2])
      end
      ::Brick._add_bt_and_hm(fk, relations, nil, nil)
    end
    kcus = nil # Allow this large item to be garbage collected
  end

  table_name_lookup = (::Brick.table_name_lookup ||= {})
  relations.each do |k, v|
    next if k.is_a?(Symbol)

    rel_name = k.split('.').map { |rel_part| ::Brick.namify(rel_part, :underscore) }
    schema_names = rel_name[0..-2]
    schema_names.shift if ::Brick.apartment_multitenant && schema_names.first == ::Brick.apartment_default_tenant
    v[:schema] = schema_names.join('.') unless schema_names.empty?
    # %%% If more than one schema has the same table name, will need to add a schema name prefix to have uniqueness
    if (singular = rel_name.last.singularize).blank?
      singular = rel_name.last
    end
    name_parts = if (tnp = ::Brick.config.table_name_prefixes
                                  &.find do |k1, v1|
                                    k.start_with?(k1) &&
                                    ((k.length >= k1.length && v1) ||
                                     (k.length == k1.length && (v1.nil? || v1.start_with?('::'))))
                                  end
                    )&.present?
                   if tnp.last&.start_with?('::') # TNP that points to an exact class?
                     # Had considered:  [tnp.last[2..-1]]
                     [singular]
                   elsif tnp.last
                     v[:auto_prefixed_schema], v[:auto_prefixed_class] = tnp
                     # v[:resource] = rel_name.last[tnp.first.length..-1]
                     [tnp.last, singular[tnp.first.length..-1]]
                   else # Override applying an auto-prefix for any TNP that points to nil
                     [singular]
                   end
                 else
                   # v[:resource] = rel_name.last
                   [singular]
                 end
    proposed_name_parts = (schema_names + name_parts).map { |p| ::Brick.namify(p, :underscore).camelize }
    # Find out if the proposed name leads to a module or class that already exists and is not an AR class
    colliding_thing = nil
    loop do
      klass = Object
      proposed_name_parts.each do |part|
        if klass.const_defined?(part) && klass.name != part
          begin
            klass = klass.const_get(part)
          rescue NoMethodError => e
            klass = nil
            break
          end
        else
          klass = nil
          break
        end
      end
      break if !klass || (klass < ActiveRecord::Base) # Break if all good -- no conflicts

      # Find a unique name since there's already something that's non-AR with that same name
      last_idx = proposed_name_parts.length - 1
      proposed_name_parts[last_idx] = ::Brick.ensure_unique(proposed_name_parts[last_idx], 'X')
      colliding_thing ||= klass
    end
    v[:class_name] = proposed_name_parts.join('::')
    # Was:  v[:resource] = v[:class_name].underscore.tr('/', '.')
    v[:resource] = proposed_name_parts.last.underscore
    if colliding_thing
      message_start = if colliding_thing.is_a?(Module) && Object.const_defined?(:Rails) &&
                         colliding_thing.constants.find { |c| colliding_thing.const_get(c) < ::Rails::Application }
                        "The module for the Rails application itself, \"#{colliding_thing.name}\","
                      else
                        "Non-AR #{colliding_thing.class.name.downcase} \"#{colliding_thing.name}\""
                      end
      puts "WARNING:  #{message_start} already exists.\n  Will set up to auto-create model #{v[:class_name]} for table #{k}."
    end
    # Track anything that's out-of-the-ordinary
    table_name_lookup[v[:class_name]] = k unless v[:class_name].underscore.pluralize == k
  end
  ::Brick.load_additional_references if ::Brick.initializer_loaded

  if is_postgres
    params = []
    ActiveRecord::Base.execute_sql("-- inherited and partitioned tables counts
    SELECT n.nspname, parent.relname,
      ((SUM(child.reltuples::float) / greatest(SUM(child.relpages), 1))) *
      (SUM(pg_relation_size(child.oid))::float / (current_setting('block_size')::float))::integer AS rowcount
    FROM pg_inherits
      INNER JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
      INNER JOIN pg_class child ON pg_inherits.inhrelid = child.oid
      INNER JOIN pg_catalog.pg_namespace n ON n.oid = parent.relnamespace#{
  if schema
    params << schema
    "
    WHERE n.nspname = COALESCE(?, 'public')"
  end}
    GROUP BY n.nspname, parent.relname, child.reltuples, child.relpages, child.oid

    UNION ALL

    -- table count
    SELECT n.nspname, pg_class.relname,
      (pg_class.reltuples::float / greatest(pg_class.relpages, 1)) *
        (pg_relation_size(pg_class.oid)::float / (current_setting('block_size')::float))::integer AS rowcount
    FROM pg_class
      INNER JOIN pg_catalog.pg_namespace n ON n.oid = pg_class.relnamespace#{
  if schema
    params << schema
    "
    WHERE n.nspname = COALESCE(?, 'public')"
  end}
    GROUP BY n.nspname, pg_class.relname, pg_class.reltuples, pg_class.relpages, pg_class.oid", params).each do |tblcount|
      # %%% What is the default schema here?
      prefix = "#{tblcount['nspname']}." unless tblcount['nspname'] == (schema || 'public')
      relations.fetch("#{prefix}#{tblcount['relname']}", nil)&.[]=(:rowcount, tblcount['rowcount'].to_i.round)
    end
  end

  if orig_schema && (orig_schema = (orig_schema - ['pg_catalog', 'pg_toast', 'heroku_ext']).first)
    puts "Now switching back to \"#{orig_schema}\" schema."
    ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", orig_schema)
  end
end

.relationsObject

All tables and views (what Postgres calls “relations” including column and foreign key info)



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/brick.rb', line 149

def relations
  return {} if (ch = ::ActiveRecord::Base.connection_handler).respond_to?(:connection_pool_list) &&
               ch.connection_pool_list.blank?

  # Key our list of relations for this connection off of the connection pool's object_id
  cp_objid = ActiveRecord::Base.connection_pool.object_id
  @relations ||= {}

  # Have we already seen this database? (Perhaps we're connecting to a replica?)
  @relations[cp_objid] ||= if (db_name = ActiveRecord::Base.connection&.instance_variable_get(:@config)&.fetch(:database, nil)) &&
                              !@relations.key?(cp_objid) &&
                              (existing = @relations.find { |_k, v| v.fetch(:db_name, nil) == db_name })
                             existing.last
                           else
                             Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } }.tap do |h|
                               h[:db_name] = db_name if db_name
                             end
                           end
rescue ActiveRecord::NoDatabaseError
  {}
end

.retrieve_schema_and_tables(sql = nil, is_postgres = nil, is_mssql = nil, schema = nil) ⇒ Object



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

def retrieve_schema_and_tables(sql = nil, is_postgres = nil, is_mssql = nil, schema = nil)
  is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' if is_mssql.nil?
  params = ar_tables
  sql ||= "SELECT t.table_schema AS \"schema\", t.table_name AS relation_name, t.table_type,#{"
    pg_catalog.obj_description(
      ('\"' || t.table_schema || '\".\"' || t.table_name || '\"')::regclass::oid, 'pg_class'
    ) AS table_description,
    pg_catalog.col_description(
      ('\"' || t.table_schema || '\".\"' || t.table_name || '\"')::regclass::oid, c.ordinal_position
    ) AS column_description," if is_postgres}
    c.column_name, #{is_postgres ? "CASE c.data_type WHEN 'USER-DEFINED' THEN pg_t.typname ELSE c.data_type END AS data_type" : 'c.data_type'},
    COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
    kcu.constraint_type AS const, kcu.constraint_name AS \"key\",
    c.is_nullable
  FROM INFORMATION_SCHEMA.tables AS t
    LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
      AND t.table_name = c.table_name
      LEFT OUTER JOIN
      (SELECT kcu1.constraint_schema, kcu1.table_name, kcu1.column_name, kcu1.ordinal_position,
      tc.constraint_type, kcu1.constraint_name
      FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
      INNER JOIN INFORMATION_SCHEMA.table_constraints AS tc
        ON kcu1.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
        AND kcu1.TABLE_NAME = tc.TABLE_NAME
        AND kcu1.CONSTRAINT_NAME = tc.constraint_name
        AND tc.constraint_type != 'FOREIGN KEY' -- For MSSQL
      ) AS kcu ON
      -- kcu.CONSTRAINT_CATALOG = t.table_catalog AND
      kcu.CONSTRAINT_SCHEMA = c.table_schema
      AND kcu.TABLE_NAME = c.table_name
      AND kcu.column_name = c.column_name#{"
  --    AND kcu.position_in_unique_constraint IS NULL" unless is_mssql}#{"
    INNER JOIN pg_catalog.pg_namespace pg_n ON pg_n.nspname = t.table_schema
    INNER JOIN pg_catalog.pg_class pg_c ON pg_n.oid = pg_c.relnamespace AND pg_c.relname = c.table_name
    INNER JOIN pg_catalog.pg_attribute pg_a ON pg_c.oid = pg_a.attrelid AND pg_a.attname = c.column_name
    INNER JOIN pg_catalog.pg_type pg_t ON pg_t.oid = pg_a.atttypid" if is_postgres}
  WHERE t.table_schema #{is_postgres || is_mssql ?
      "NOT IN ('information_schema', 'pg_catalog', 'pg_toast', 'heroku_ext',
               'INFORMATION_SCHEMA', 'sys')"
      :
      "= '#{ActiveRecord::Base.connection.current_database&.tr("'", "''")}'"}#{
  if is_postgres && schema
    params = params.unshift(schema) # Used to use this SQL:  current_setting('SEARCH_PATH')
    "
    AND t.table_schema = COALESCE(?, 'public')"
  end}
--          AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
    AND t.table_name NOT IN ('pg_stat_statements', ?, ?)
  ORDER BY 1, t.table_type DESC, 2, c.ordinal_position"
  ActiveRecord::Base.execute_sql(sql, *params)
end

.schema_behavior=(behavior) ⇒ Object

Database schema to use when analysing existing data, such as deriving a list of polymorphic classes for polymorphics in which it wasn’t originally specified.



605
606
607
# File 'lib/brick.rb', line 605

def schema_behavior=(behavior)
  Brick.config.schema_behavior = (behavior.is_a?(Symbol) ? { behavior => nil } : behavior)
end

.schema_behaviour=(behavior) ⇒ Object

For any Brits out there



609
610
611
# File 'lib/brick.rb', line 609

def schema_behaviour=(behavior)
  Brick.schema_behavior = behavior
end

.serializerObject

Get the Brick serializer used by all threads.



716
717
718
# File 'lib/brick.rb', line 716

def serializer
  Brick.config.serializer
end

.serializer=(value) ⇒ Object

Set the Brick serializer. This setting affects all threads.



710
711
712
# File 'lib/brick.rb', line 710

def serializer=(value)
  Brick.config.serializer = value
end

.set_db_schema(params = nil) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/brick.rb', line 125

def set_db_schema(params = nil)
  # If Apartment::Tenant.current is not still the default (usually 'public') then an elevator has brought us into
  # a different tenant.  If so then don't allow schema navigation.
  if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' && apartment_multitenant
    current_schema = (ActiveRecord::Base.execute_sql('SELECT current_schemas(true)')
                                        .first['current_schemas'][1..-2]
                                        .split(',') - ['pg_catalog', 'pg_toast', 'heroku_ext']).first
    is_show_schema_list = current_schema == ::Brick.default_schema
    schema = (is_show_schema_list && params && params['_brick_schema']) || ::Brick.default_schema
    chosen = if is_show_schema_list && ::Brick.db_schemas&.key?(schema)
               Apartment::Tenant.switch!(schema)
               schema
             elsif ::Brick.test_schema
               is_show_schema_list = true
               Apartment::Tenant.switch!(::Brick.test_schema)
               ::Brick.test_schema
             else
               current_schema # Just return the current schema
             end
  end
  [chosen == ::Brick.default_schema ? nil : chosen, is_show_schema_list]
end

.sidescroll=(scroll) ⇒ Object



586
587
588
# File 'lib/brick.rb', line 586

def sidescroll=(scroll)
  Brick.config.sidescroll = scroll
end

.skip_database_views=(value) ⇒ Object



443
444
445
# File 'lib/brick.rb', line 443

def skip_database_views=(value)
  Brick.config.skip_database_views = value
end

.skip_index_hms=(value) ⇒ Object

Skip showing counts for these specific has_many associations when building auto-generated #index views



537
538
539
# File 'lib/brick.rb', line 537

def skip_index_hms=(value)
  Brick.config.skip_index_hms = value
end

.sti_modelsObject



102
103
104
# File 'lib/brick.rb', line 102

def sti_models
  @sti_models ||= {}
end

.sti_namespace_prefixes=(snp) ⇒ Object

Module prefixes to build out and associate with specific base STI models



598
599
600
# File 'lib/brick.rb', line 598

def sti_namespace_prefixes=(snp)
  Brick.config.sti_namespace_prefixes = snp
end

.sti_type_column=(type_col) ⇒ Object



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

def sti_type_column=(type_col)
  Brick.config.sti_type_column = (type_col.is_a?(String) ? { type_col => nil } : type_col)
end

.swagger_endpoint(url, name) ⇒ Object

Add a swagger endpoint in the same kind of way that RSwag UI does



335
336
337
338
# File 'lib/brick.rb', line 335

def swagger_endpoint(url, name)
  @swagger_endpoints ||= []
  @swagger_endpoints << { url: url, name: name }
end

.table_name_prefixes=(value) ⇒ Object



463
464
465
# File 'lib/brick.rb', line 463

def table_name_prefixes=(value)
  Brick.config.table_name_prefixes = value
end

.treat_as_associative=(tables) ⇒ Object

Tables to treat as associative, even when they have data columns



570
571
572
# File 'lib/brick.rb', line 570

def treat_as_associative=(tables)
  Brick.config.treat_as_associative = tables
end

.treat_as_module=(value) ⇒ Object

Causes Decidim to work with this line: Brick.treat_as_module = [‘Decidim::ContentBlocks’]



469
470
471
# File 'lib/brick.rb', line 469

def treat_as_module=(value)
  Brick.config.treat_as_module = value
end

.unexclude_column(table, col) ⇒ Object



329
330
331
332
# File 'lib/brick.rb', line 329

def unexclude_column(table, col)
  puts "Unexcluding #{table}.#{col}"
  true
end

.versionObject



730
731
732
# File 'lib/brick.rb', line 730

def version
  VERSION::STRING
end