Module: Brick
- Defined in:
- lib/brick.rb,
lib/brick/util.rb,
lib/brick/config.rb,
lib/brick/join_array.rb,
lib/brick/rails/engine.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/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 # binding.pry # 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_search || ::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
-
.auto_models ⇒ Object
Returns the value of attribute auto_models.
-
.db_schemas ⇒ Object
Returns the value of attribute db_schemas.
-
.default_schema ⇒ Object
Returns the value of attribute default_schema.
-
.elasticsearch_models ⇒ Object
Returns the value of attribute elasticsearch_models.
-
.established_drf ⇒ Object
Returns the value of attribute established_drf.
-
.initializer_loaded ⇒ Object
Returns the value of attribute initializer_loaded.
-
.is_eager_loading ⇒ Object
Returns the value of attribute is_eager_loading.
-
.is_oracle ⇒ Object
Returns the value of attribute is_oracle.
-
.routes_done ⇒ Object
Returns the value of attribute routes_done.
-
.table_name_lookup ⇒ Object
Returns the value of attribute table_name_lookup.
-
.test_schema ⇒ Object
Returns the value of attribute test_schema.
Class Method Summary collapse
- ._add_bt_and_hm(fk, relations, polymorphic_class = nil, is_optional = false) ⇒ Object
- ._brick_index(tbl_name, mode = nil, separator = nil, relation = nil, not_path = nil) ⇒ Object
- ._class_pk(dotted_name, multitenant) ⇒ Object
- ._find_csv_separator(stream, likely_separator) ⇒ Object
-
.additional_references=(ars) ⇒ Object
Additional table associations to use (Think of these as virtual foreign keys perhaps).
- .always_load_fields=(field_set) ⇒ Object
- .apartment_default_tenant ⇒ Object
- .apartment_multitenant ⇒ Object
- .api_filter ⇒ Object
- .api_filter=(proc) ⇒ Object
- .api_root=(path) ⇒ Object
- .api_roots ⇒ Object
- .api_roots=(paths) ⇒ Object
- .apply_double_underscore_patch ⇒ Object
- .ar_base ⇒ Object
- .ar_tables ⇒ Object
- .avo_3x_resource(model, requested) ⇒ Object
-
.config {|@config| ... } ⇒ Object
(also: configure)
private
Returns Brick’s global configuration object, a singleton.
- .controllers_inherit_from=(value) ⇒ Object
-
.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.
-
.custom_columns=(cust_cols) ⇒ Object
Custom columns to add to a table, minimally defined with a name and DSL string.
- .default_route_fallback=(resource_name) ⇒ Object
-
.defer_references_for_generation=(drfg) ⇒ Object
References to disregard when auto-building migrations, models, and seeds.
- .display_classes(prefix, rels, max_length) ⇒ Object
- .drfgs ⇒ Object
- .eager_load_classes(do_ar_abstract_bases = false) ⇒ Object
- .enable_api ⇒ Object
- .enable_api=(path) ⇒ Object
-
.enable_controllers=(value) ⇒ Object
Switches Brick auto-controllers on or off, for all threads.
-
.enable_controllers? ⇒ Boolean
Returns ‘true` if Brick controllers are on, `false` otherwise.
-
.enable_models=(value) ⇒ Object
Switches Brick auto-models on or off, for all threads.
-
.enable_models? ⇒ Boolean
Returns ‘true` if Brick models are on, `false` otherwise.
-
.enable_routes=(value) ⇒ Object
Switches Brick auto-routes on or off, for all threads.
-
.enable_routes? ⇒ Boolean
Returns ‘true` if Brick routes are on, `false` otherwise.
-
.enable_views=(value) ⇒ Object
Switches Brick auto-views on or off, for all threads.
-
.enable_views? ⇒ Boolean
Returns ‘true` if Brick views are on, `false` otherwise.
- .ensure_unique(name, delimiter, *sources) ⇒ Object
- .exclude_column(table, col) ⇒ Object
-
.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).
- .exclude_tables=(value) ⇒ Object
- .existing_stis ⇒ Object
- .find_col_renaming(api_ver_path, relation_name) ⇒ Object
-
.find_orphans(multi_schema) ⇒ Object
Locate orphaned records.
-
.gem_version ⇒ Object
Returns Brick’s ‘::Gem::Version`, convenient for comparisons.
- .get_bts_and_hms(model, recalculate = nil) ⇒ Object
- .get_possible_schemas ⇒ Object
-
.get_status_of_resources ⇒ Object
Identify built out routes, migrations, models, (and also soon controllers and views!) for each resource.
-
.has_ones=(hos) ⇒ Object
Associations to treat as a has_one.
-
.hmts=(assocs) ⇒ Object
Associations for which to auto-create a has_many _, through: _.
- .is_apartment_excluded_table(tbl) ⇒ Object
- .is_geography?(val) ⇒ Boolean
- .json_columns=(cols) ⇒ Object
- .license=(key) ⇒ Object
-
.load_additional_references ⇒ Object
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.
-
.load_with_retry(loader, autoloaded = nil) ⇒ Object
Some classes (like Phlex::Testing::Rails) will successfully auto-load after a retry.
- .metadata_columns=(value) ⇒ Object
- .mode=(setting) ⇒ Object
-
.model_descrips=(descrips) ⇒ Object
DSL templates for individual models to provide prettier descriptions of objects.
- .models_inherit_from=(value) ⇒ Object
-
.namify(name, action = nil) ⇒ Object
Convert spaces to underscores if the second character and onwards is mixed case.
-
.nested_attributes=(anaf) ⇒ Object
Associations upon which to add #accepts_nested_attributes_for logic.
-
.non_tenanted_models ⇒ Object
If multitenancy is enabled, a list of non-tenanted “global” models.
- .not_nullables=(value) ⇒ Object
- .omit_empty_tables_in_dropdown=(setting) ⇒ Object
- .order=(value) ⇒ Object
-
.path_prefix=(path) ⇒ Object
Any path prefixing to apply to all auto-generated Brick routes.
-
.polymorphics=(polys) ⇒ Object
Polymorphic associations.
-
.reflect_tables ⇒ Object
This is done separately so that during testing it can be called right after a migration in order to make sure everything is good.
-
.relations ⇒ Object
All tables and views (what Postgres calls “relations” including column and foreign key info).
- .retrieve_schema_and_tables(sql = nil, is_postgres = nil, is_mssql = nil, schema = nil) ⇒ Object
-
.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.
-
.schema_behaviour=(behavior) ⇒ Object
For any Brits out there.
-
.serializer ⇒ Object
Get the Brick serializer used by all threads.
-
.serializer=(value) ⇒ Object
Set the Brick serializer.
- .set_db_schema(params = nil) ⇒ Object
- .sidescroll=(scroll) ⇒ Object
- .skip_database_views=(value) ⇒ Object
-
.skip_index_hms=(value) ⇒ Object
Skip showing counts for these specific has_many associations when building auto-generated #index views.
- .sti_models ⇒ Object
-
.sti_namespace_prefixes=(snp) ⇒ Object
Module prefixes to build out and associate with specific base STI models.
- .sti_type_column=(type_col) ⇒ Object
-
.swagger_endpoint(url, name) ⇒ Object
Add a swagger endpoint in the same kind of way that RSwag UI does.
- .table_name_prefixes=(value) ⇒ Object
-
.treat_as_associative=(tables) ⇒ Object
Tables to treat as associative, even when they have data columns.
-
.treat_as_module=(value) ⇒ Object
Causes Decidim to work with this line: Brick.treat_as_module = [‘Decidim::ContentBlocks’].
- .unexclude_column(table, col) ⇒ Object
- .version ⇒ Object
Class Attribute Details
.auto_models ⇒ Object
Returns the value of attribute auto_models.
110 111 112 |
# File 'lib/brick.rb', line 110 def auto_models @auto_models end |
.db_schemas ⇒ Object
Returns the value of attribute db_schemas.
110 111 112 |
# File 'lib/brick.rb', line 110 def db_schemas @db_schemas end |
.default_schema ⇒ Object
Returns the value of attribute default_schema.
110 111 112 |
# File 'lib/brick.rb', line 110 def default_schema @default_schema end |
.elasticsearch_models ⇒ Object
Returns the value of attribute elasticsearch_models.
110 111 112 |
# File 'lib/brick.rb', line 110 def elasticsearch_models @elasticsearch_models end |
.established_drf ⇒ Object
Returns the value of attribute established_drf.
110 111 112 |
# File 'lib/brick.rb', line 110 def established_drf @established_drf end |
.initializer_loaded ⇒ Object
Returns the value of attribute initializer_loaded.
110 111 112 |
# File 'lib/brick.rb', line 110 def initializer_loaded @initializer_loaded end |
.is_eager_loading ⇒ Object
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_oracle ⇒ Object
Returns the value of attribute is_oracle.
110 111 112 |
# File 'lib/brick.rb', line 110 def is_oracle @is_oracle end |
.routes_done ⇒ Object
Returns the value of attribute routes_done.
110 111 112 |
# File 'lib/brick.rb', line 110 def routes_done @routes_done end |
.table_name_lookup ⇒ Object
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_schema ⇒ Object
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
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 3001 3002 3003 3004 3005 3006 3007 3008 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 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 |
# File 'lib/brick/extensions.rb', line 2941 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 fk[4] puts "WARNING: Foreign key \"#{fk[5]}\" for \"#{"#{fk[0]}." if fk[0]}#{fk[1]}.#{fk[2]}\" does not reference a valid primary key or column(s) configured with a unique constraint." return end 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]) ) && ActiveRecord.version >= ::Gem::Version.new('5.0') 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
3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 |
# File 'lib/brick/extensions.rb', line 3216 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
3261 3262 3263 |
# File 'lib/brick/extensions.rb', line 3261 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
862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 |
# File 'lib/brick.rb', line 862 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)
491 492 493 494 495 496 497 498 |
# File 'lib/brick.rb', line 491 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
635 636 637 |
# File 'lib/brick.rb', line 635 def always_load_fields=(field_set) Brick.config.always_load_fields = field_set end |
.apartment_default_tenant ⇒ Object
180 181 182 |
# File 'lib/brick.rb', line 180 def apartment_default_tenant Apartment.default_tenant || 'public' end |
.apartment_multitenant ⇒ Object
173 174 175 176 177 178 |
# File 'lib/brick.rb', line 173 def apartment_multitenant if @apartment_multitenant.nil? @apartment_multitenant = ::Brick.config.schema_behavior[:multitenant] && Object.const_defined?('Apartment') end @apartment_multitenant end |
.api_filter ⇒ Object
444 445 446 |
# File 'lib/brick.rb', line 444 def api_filter Brick.config.api_filter end |
.api_filter=(proc) ⇒ Object
439 440 441 |
# File 'lib/brick.rb', line 439 def api_filter=(proc) Brick.config.api_filter = proc end |
.api_root=(path) ⇒ Object
424 425 426 |
# File 'lib/brick.rb', line 424 def api_root=(path) Brick.config.api_roots = [path] end |
.api_roots ⇒ Object
434 435 436 |
# File 'lib/brick.rb', line 434 def api_roots Brick.config.api_roots end |
.api_roots=(paths) ⇒ Object
429 430 431 |
# File 'lib/brick.rb', line 429 def api_roots=(paths) Brick.config.api_roots = paths end |
.apply_double_underscore_patch ⇒ Object
824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 |
# File 'lib/brick.rb', line 824 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_base ⇒ Object
3070 3071 3072 |
# File 'lib/brick/extensions.rb', line 3070 def ar_base @ar_base ||= Object.const_defined?(:ApplicationRecord) ? ApplicationRecord : Class.new(ActiveRecord::Base) end |
.ar_tables ⇒ Object
585 586 587 588 589 590 591 592 593 |
# File 'lib/brick/reflect_tables.rb', line 585 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
1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 |
# File 'lib/brick/extensions.rb', line 1595 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.
729 730 731 732 733 |
# File 'lib/brick.rb', line 729 def config @config ||= Brick::Config.instance yield @config if block_given? @config end |
.controllers_inherit_from=(value) ⇒ Object
464 465 466 |
# File 'lib/brick.rb', line 464 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
783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 |
# File 'lib/brick.rb', line 783 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.
517 518 519 520 521 522 |
# File 'lib/brick.rb', line 517 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
623 624 625 |
# File 'lib/brick.rb', line 623 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
502 503 504 505 506 507 |
# File 'lib/brick.rb', line 502 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
774 775 776 777 778 779 780 |
# File 'lib/brick.rb', line 774 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 |
.drfgs ⇒ Object
509 510 511 512 513 |
# File 'lib/brick.rb', line 509 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
740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 |
# File 'lib/brick.rb', line 740 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_api ⇒ Object
419 420 421 |
# File 'lib/brick.rb', line 419 def enable_api Brick.config.enable_api end |
.enable_api=(path) ⇒ Object
414 415 416 |
# File 'lib/brick.rb', line 414 def enable_api=(path) Brick.config.enable_api = path end |
.enable_controllers=(value) ⇒ Object
Switches Brick auto-controllers on or off, for all threads
376 377 378 |
# File 'lib/brick.rb', line 376 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.
383 384 385 |
# File 'lib/brick.rb', line 383 def enable_controllers? !!Brick.config.enable_controllers end |
.enable_models=(value) ⇒ Object
Switches Brick auto-models on or off, for all threads
363 364 365 |
# File 'lib/brick.rb', line 363 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.
370 371 372 |
# File 'lib/brick.rb', line 370 def enable_models? !!Brick.config.enable_models end |
.enable_routes=(value) ⇒ Object
Switches Brick auto-routes on or off, for all threads
402 403 404 |
# File 'lib/brick.rb', line 402 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.
409 410 411 |
# File 'lib/brick.rb', line 409 def enable_routes? !!Brick.config.enable_routes end |
.enable_views=(value) ⇒ Object
Switches Brick auto-views on or off, for all threads
389 390 391 |
# File 'lib/brick.rb', line 389 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.
396 397 398 |
# File 'lib/brick.rb', line 396 def enable_views? !!Brick.config.enable_views end |
.ensure_unique(name, delimiter, *sources) ⇒ Object
3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 |
# File 'lib/brick/extensions.rb', line 3128 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
331 332 333 334 |
# File 'lib/brick.rb', line 331 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)
532 533 534 535 536 537 538 539 |
# File 'lib/brick.rb', line 532 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
454 455 456 |
# File 'lib/brick.rb', line 454 def exclude_tables=(value) Brick.config.exclude_tables = value end |
.existing_stis ⇒ Object
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
3254 3255 3256 3257 3258 3259 |
# File 'lib/brick/extensions.rb', line 3254 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
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 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 |
# File 'lib/brick/extensions.rb', line 3153 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_version ⇒ Object
Returns Brick’s ‘::Gem::Version`, convenient for comparisons. This is recommended over `::Brick::VERSION::STRING`.
710 711 712 |
# File 'lib/brick.rb', line 710 def gem_version ::Gem::Version.new(VERSION::STRING) end |
.get_bts_and_hms(model, recalculate = nil) ⇒ Object
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 |
# File 'lib/brick.rb', line 210 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.[:polymorphic] = true end if (through = a.[: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..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.[: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..key?(:as) && (this_fks - a.klass.column_names).length.positive? = ", #{a..map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(', ')}" if a..present? puts "WARNING: Model #{model.name} has this association: has_many :#{a.name}#{} 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.[: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_schemas ⇒ Object
118 119 120 121 122 123 124 125 |
# File 'lib/brick.rb', line 118 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_resources ⇒ Object
Identify built out routes, migrations, models, (and also soon controllers and views!) for each resource
3077 3078 3079 3080 3081 3082 3083 3084 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 |
# File 'lib/brick/extensions.rb', line 3077 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
549 550 551 552 553 554 555 556 557 558 559 560 |
# File 'lib/brick.rb', line 549 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: _
570 571 572 |
# File 'lib/brick.rb', line 570 def hmts=(assocs) Brick.config.hmts = assocs end |
.is_apartment_excluded_table(tbl) ⇒ Object
3265 3266 3267 3268 3269 3270 3271 |
# File 'lib/brick/extensions.rb', line 3265 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
346 347 348 |
# File 'lib/brick.rb', line 346 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
587 588 589 |
# File 'lib/brick.rb', line 587 def json_columns=(cols) Brick.config.json_columns = cols end |
.license=(key) ⇒ Object
627 628 629 |
# File 'lib/brick.rb', line 627 def license=(key) Brick.config.license = key end |
.load_additional_references ⇒ Object
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
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 |
# File 'lib/brick.rb', line 642 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
761 762 763 764 765 766 767 768 769 770 771 772 |
# File 'lib/brick.rb', line 761 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
480 481 482 |
# File 'lib/brick.rb', line 480 def (value) Brick.config. = value end |
.mode=(setting) ⇒ Object
351 352 353 |
# File 'lib/brick.rb', line 351 def mode=(setting) Brick.config.mode = setting end |
.model_descrips=(descrips) ⇒ Object
DSL templates for individual models to provide prettier descriptions of objects
598 599 600 |
# File 'lib/brick.rb', line 598 def model_descrips=(descrips) Brick.config.model_descrips = descrips end |
.models_inherit_from=(value) ⇒ Object
459 460 461 |
# File 'lib/brick.rb', line 459 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
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/brick.rb', line 190 def namify(name, action = nil) has_uppers = name =~ /[A-Z]+/ has_lowers = name =~ /[a-z]+/ name.downcase! if has_uppers && action == :downcase name = "Z#{name}" if name.start_with?('_') # Entity Framework creates a table named __EFMigrationsHistory 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 # .NET has a background job processor called "Hangfire" which creates several tables, # two of which get named the same as core Ruby classes -- "Set" and "Hash". name = "#{name}x" if ['Hash', 'Set', 'hash', 'set'].include?(name) action == :underscore ? name.underscore : name end end |
.nested_attributes=(anaf) ⇒ Object
Associations upon which to add #accepts_nested_attributes_for logic
564 565 566 |
# File 'lib/brick.rb', line 564 def nested_attributes=(anaf) Brick.config.nested_attributes = anaf end |
.non_tenanted_models ⇒ Object
If multitenancy is enabled, a list of non-tenanted “global” models
185 186 187 |
# File 'lib/brick.rb', line 185 def non_tenanted_models @pending_models ||= {} end |
.not_nullables=(value) ⇒ Object
485 486 487 |
# File 'lib/brick.rb', line 485 def not_nullables=(value) Brick.config.not_nullables = value end |
.omit_empty_tables_in_dropdown=(setting) ⇒ Object
631 632 633 |
# File 'lib/brick.rb', line 631 def omit_empty_tables_in_dropdown=(setting) Brick.config.omit_empty_tables_in_dropdown = setting end |
.order=(value) ⇒ Object
525 526 527 |
# File 'lib/brick.rb', line 525 def order=(value) Brick.config.order = value end |
.path_prefix=(path) ⇒ Object
Any path prefixing to apply to all auto-generated Brick routes
357 358 359 |
# File 'lib/brick.rb', line 357 def path_prefix=(path) Brick.config.path_prefix = path end |
.polymorphics=(polys) ⇒ Object
Polymorphic associations
581 582 583 584 |
# File 'lib/brick.rb', line 581 def polymorphics=(polys) polys = polys.each_with_object({}) { |poly, s| s[poly] = nil } if polys.is_a?(Array) Brick.config.polymorphics = polys || {} end |
.reflect_tables ⇒ Object
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 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 |
# File 'lib/brick/reflect_tables.rb', line 5 def reflect_tables require 'brick/join_array' return unless ::Brick.config.mode == :on # return if ActiveRecord::Base.connection.current_database == 'postgres' # Utilise Elasticsearch indexes, if any if Object.const_defined?('Elasticsearch') if ::Elasticsearch.const_defined?('Client') # Allow Elasticsearch gem > 7.10 to work with Opensearch ::Elasticsearch::Client.class_exec do alias _original_initialize initialize def initialize(arguments = {}, &block) _original_initialize(arguments, &block) @verified = true @transport end # Auto-create when there is a missing index alias _original_method_missing method_missing def method_missing(name, *args, &block) _original_method_missing(name, *args, &block) rescue Elastic::Transport::Transport::Errors::NotFound => e if (missing_index = args.last&.fetch(:defined_params, nil)&.fetch(:index, nil)) self.indices.create({ index: missing_index, body: { settings: {}, mappings: { properties: {} } } }) puts "Auto-creating missing index \"#{missing_index}\"" _original_method_missing(name, *args, &block) else raise e end end end end if ::Elasticsearch.const_defined?('Model') # By setting the environment variable ELASTICSEARCH_URL then you can specify an Elasticsearch/Opensearch host host = (client = ::Elasticsearch::Model.client).transport.hosts.first es_uri = URI.parse("#{host[:protocol]}://#{host[:host]}:#{host[:port]}") es_uri = nil if es_uri.to_s == 'http://localhost:9200' begin cluster_info = client.info.body if (es_ver = cluster_info['version']) ::Brick.elasticsearch_models = :all puts "Found Elasticsearch gem and #{'local ' unless es_uri}#{es_ver['distribution'].titleize} #{es_ver['number']} installation#{" at #{es_uri}" if es_uri}." puts "Enable Elasticsearch support by either setting \"::Brick.elasticsearch_models = :all\" or by picking specific models by name." # # Auto-create when trying to import and there is a missing index # ::Elasticsearch::Model::Importing.class_exec do # end end rescue StandardError => e # Errno::ECONNREFUSED puts "Found Elasticsearch gem, but could not connect to #{'local ' unless es_uri}Elasticsearch/Opensearch server#{" at #{es_uri}" if es_uri}." end # require 'net/http' # begin # es_uri = ENV['ELASTICSEARCH_URL'] # binding.pry # cluster_info = JSON.parse(Net::HTTP.get(URI.parse(es_uri || 'http://localhost:9200'))) # if (es_ver = cluster_info['version']) # ::Brick.elasticsearch_models = :all # puts "Found Elasticsearch gem and #{'local ' unless es_uri}#{es_ver['distribution'].titleize} #{es_ver['number']} installation#{" at #{es_uri}" if es_uri}." # puts "Enable Elasticsearch support by either setting \"::Brick.elasticsearch_models = :all\" or by picking specific models by name." # end # rescue StandardError => e # end end end # client = Elasticsearch::Client.new(host: 'https://my-elasticsearch-host.example') # client.ping # client.search(q: 'test') # 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' && ActiveRecord.version >= Gem::Version.new('5.1') 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 = if colliding_thing.is_a?(Module) && Object.const_defined?(:Rails) && colliding_thing.constants.find { |c| (ctc = colliding_thing.const_get(c)).is_a?(Class) && ctc < ::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: #{} 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.split('.').last 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 |
.relations ⇒ Object
All tables and views (what Postgres calls “relations” including column and foreign key info)
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/brick.rb', line 151 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
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 |
# File 'lib/brick/reflect_tables.rb', line 533 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.
611 612 613 |
# File 'lib/brick.rb', line 611 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
615 616 617 |
# File 'lib/brick.rb', line 615 def schema_behaviour=(behavior) Brick.schema_behavior = behavior end |
.serializer ⇒ Object
Get the Brick serializer used by all threads.
722 723 724 |
# File 'lib/brick.rb', line 722 def serializer Brick.config.serializer end |
.serializer=(value) ⇒ Object
Set the Brick serializer. This setting affects all threads.
716 717 718 |
# File 'lib/brick.rb', line 716 def serializer=(value) Brick.config.serializer = value end |
.set_db_schema(params = nil) ⇒ Object
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/brick.rb', line 127 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
592 593 594 |
# File 'lib/brick.rb', line 592 def sidescroll=(scroll) Brick.config.sidescroll = scroll end |
.skip_database_views=(value) ⇒ Object
449 450 451 |
# File 'lib/brick.rb', line 449 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
543 544 545 |
# File 'lib/brick.rb', line 543 def skip_index_hms=(value) Brick.config.skip_index_hms = value end |
.sti_models ⇒ Object
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
604 605 606 |
# File 'lib/brick.rb', line 604 def sti_namespace_prefixes=(snp) Brick.config.sti_namespace_prefixes = snp end |
.sti_type_column=(type_col) ⇒ Object
619 620 621 |
# File 'lib/brick.rb', line 619 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
341 342 343 344 |
# File 'lib/brick.rb', line 341 def swagger_endpoint(url, name) @swagger_endpoints ||= [] @swagger_endpoints << { url: url, name: name } end |
.table_name_prefixes=(value) ⇒ Object
469 470 471 |
# File 'lib/brick.rb', line 469 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
576 577 578 |
# File 'lib/brick.rb', line 576 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’]
475 476 477 |
# File 'lib/brick.rb', line 475 def treat_as_module=(value) Brick.config.treat_as_module = value end |
.unexclude_column(table, col) ⇒ Object
335 336 337 338 |
# File 'lib/brick.rb', line 335 def unexclude_column(table, col) puts "Unexcluding #{table}.#{col}" true end |