brick 1.0.31 → 1.0.32

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f529097f82af103af54ae472e226244ce8932b64cf974b836d1d23be6cedfbce
4
- data.tar.gz: 1561bea9cdcdd366d2ef9798cff36f3a74f58768f1f14be5c2bfbf4f40feac8d
3
+ metadata.gz: 8483d9a50013e440cce28f648d09d5322985702f952520cb83c69ad0e5742ce4
4
+ data.tar.gz: 40072d0504f4d2b1cc9a59e62fef305b939dd977da0e794445adf003dbfb8b7e
5
5
  SHA512:
6
- metadata.gz: a5d812af4ba7eadae06e28bb9686ea60b79f9df721a51cbddbb0ce87d6abfe78e0675b45446768bd2460f00c1f02bba5276d21d9ec96345dcffa8079d87f2ec0
7
- data.tar.gz: b7ff59c6229dac732eb06a01c59d4a2913675b02a0a8a54106c2dfdcf965ea58cff5dc6d8dfb1c54fb8d9569fa31f2d8217ed281593bdbc167e89d42178225dd
6
+ metadata.gz: 9740778159acb464652322ef36eff763a55b38c6579fe0a7550a5797db44928ad6832a8bccc0d924a4d4c5a14dc02658e61bf4220cecff08fc348d540b67e41c
7
+ data.tar.gz: 1b09db07a284512821b604f1d82f64c4be0c11db7f8a8973f36ab3f78b21804a7fec1eb641bcf1ac5c2ec118cb477c8086c124017c66550753ec4f9beddbd413
@@ -104,7 +104,8 @@ module ActiveRecord
104
104
  if ch == ']' # Time to process a bracketed thing?
105
105
  parts = bracket_name.split('.')
106
106
  first_parts = parts[0..-2].map do |part|
107
- klass = klass.reflect_on_association(part_sym = part.to_sym).klass
107
+ klass = (orig_class = klass).reflect_on_association(part_sym = part.to_sym)&.klass
108
+ puts "Couldn't reference #{orig_class.name}##{part} that's part of the DSL \"#{dsl}\"." if klass.nil?
108
109
  part_sym
109
110
  end
110
111
  parts = prefix + first_parts + [parts[-1]]
@@ -348,6 +349,7 @@ module ActiveRecord
348
349
  next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
349
350
 
350
351
  join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
352
+ distinct!
351
353
  end
352
354
  wheres[k] = v.split(',')
353
355
  end
@@ -420,11 +422,13 @@ module ActiveRecord
420
422
  on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
421
423
  end
422
424
  join_clause = "LEFT OUTER
423
- JOIN (SELECT #{selects.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY #{(1..selects.length).to_a.join(', ')}) AS #{tbl_alias}"
425
+ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}#{count_column
426
+ }) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name
427
+ } GROUP BY #{(1..selects.length).to_a.join(', ')}) AS #{tbl_alias}"
424
428
  joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
425
429
  end
426
430
  where!(wheres) unless wheres.empty?
427
- limit(1000) # Don't want to get too carried away just yet
431
+ limit!(1000) # Don't want to get too carried away just yet
428
432
  wheres unless wheres.empty? # Return the specific parameters that we did use
429
433
  end
430
434
 
@@ -485,7 +489,9 @@ if ActiveSupport::Dependencies.respond_to?(:autoload_module!) # %%% Only works w
485
489
  alias _brick_autoload_module! autoload_module!
486
490
  def autoload_module!(*args)
487
491
  into, const_name, qualified_name, path_suffix = args
488
- if (base_class = ::Brick.config.sti_namespace_prefixes&.fetch("::#{into.name}::", nil)&.constantize)
492
+ base_class_name = ::Brick.config.sti_namespace_prefixes&.fetch("::#{into.name}::", nil)
493
+ base_class_name = "::#{base_class_name}" unless base_class_name.start_with?('::')
494
+ if (base_class = base_class_name&.constantize)
489
495
  ::Brick.sti_models[qualified_name] = { base: base_class }
490
496
  # Build subclass and place it into the specially STI-namespaced module
491
497
  into.const_set(const_name.to_sym, klass = Class.new(base_class))
@@ -506,8 +512,13 @@ end
506
512
  Module.class_exec do
507
513
  alias _brick_const_missing const_missing
508
514
  def const_missing(*args)
509
- if (self.const_defined?(args.first) && (possible = self.const_get(args.first)) != self) ||
510
- (self != Object && Object.const_defined?(args.first) && (possible = Object.const_get(args.first)) != self)
515
+ if (self.const_defined?(args.first) && (possible = self.const_get(args.first)) && possible != self) ||
516
+ (self != Object && Object.const_defined?(args.first) &&
517
+ (
518
+ (possible = Object.const_get(args.first)) &&
519
+ (possible != self || (possible == self && possible.is_a?(Class)))
520
+ )
521
+ )
511
522
  return possible
512
523
  end
513
524
  class_name = args.first.to_s
@@ -533,7 +544,7 @@ Module.class_exec do
533
544
  result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
534
545
  # Otherwise now it's up to us to fill in the gaps
535
546
  # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
536
- # VAbc instead of VABC)
547
+ # Vabc instead of VABC)
537
548
  full_class_name = +''
538
549
  full_class_name << "::#{self.name}" unless self == Object
539
550
  full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
@@ -543,12 +554,13 @@ Module.class_exec do
543
554
  end
544
555
  elsif (::Brick.enable_models? || ::Brick.enable_controllers?) && # Schema match?
545
556
  self == Object && # %%% This works for Person::Person -- but also limits us to not being able to allow more than one level of namespacing
546
- schema_name = [(singular_table_name = class_name.underscore),
547
- (table_name = singular_table_name.pluralize),
548
- class_name,
549
- (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas.include?(s) }
557
+ (schema_name = [(singular_table_name = class_name.underscore),
558
+ (table_name = singular_table_name.pluralize),
559
+ class_name,
560
+ (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas.include?(s) }&.camelize ||
561
+ (::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name))
550
562
  # Build out a module for the schema if it's namespaced
551
- schema_name = schema_name.camelize
563
+ # schema_name = schema_name.camelize
552
564
  self.const_set(schema_name.to_sym, (built_module = Module.new))
553
565
 
554
566
  [built_module, "module #{schema_name}; end\n"]
@@ -557,27 +569,34 @@ Module.class_exec do
557
569
  # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
558
570
  # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
559
571
 
560
- unless self == Object # Are we in some namespace?
572
+ if self != Object || # Are we in some namespace? ...
573
+ (base_model = ::Brick.config.sti_namespace_prefixes&.fetch("::#{name}::", nil)&.constantize) # ... or part of an auto-STI namespace?
561
574
  schema_name = [(singular_schema_name = name.underscore),
562
575
  (schema_name = singular_schema_name.pluralize),
563
576
  name,
564
577
  name.pluralize].find { |s| Brick.db_schemas.include?(s) }
565
578
  end
566
-
567
579
  plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
568
580
  # If it's namespaced then we turn the first part into what would be a schema name
569
- singular_table_name = ActiveSupport::Inflector.underscore(model_name).gsub('::', '.')
570
-
571
- # Adjust for STI if we know of a base model for the requested model name
572
- table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
573
- base_model.table_name
574
- else
575
- ActiveSupport::Inflector.pluralize(singular_table_name)
576
- end
577
-
578
- # Maybe, just maybe there's a database table that will satisfy this need
579
- if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(schema_name ? "#{schema_name}.#{m}" : m) })
580
- Object.send(:build_model, schema_name, model_name, singular_table_name, table_name, relations, matching)
581
+ singular_table_name = ActiveSupport::Inflector.underscore(model_name).gsub('/', '.')
582
+
583
+ if base_model
584
+ schema_name = name.underscore # For the auto-STI namespace models
585
+ table_name = base_model.table_name
586
+ Object.send(:build_model, self, model_name, singular_table_name, table_name, relations, table_name)
587
+ else
588
+ # Adjust for STI if we know of a base model for the requested model name
589
+ # %%% Does not yet work with namespaced model names. Perhaps prefix with plural_class_name when doing the lookups here.
590
+ table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
591
+ base_model.table_name
592
+ else
593
+ ActiveSupport::Inflector.pluralize(singular_table_name)
594
+ end
595
+
596
+ # Maybe, just maybe there's a database table that will satisfy this need
597
+ if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(schema_name ? "#{schema_name}.#{m}" : m) })
598
+ Object.send(:build_model, schema_name, model_name, singular_table_name, table_name, relations, matching)
599
+ end
581
600
  end
582
601
  end
583
602
  if result
@@ -593,7 +612,7 @@ Module.class_exec do
593
612
  elsif self != Object
594
613
  module_parent.const_missing(*args)
595
614
  else
596
- puts "MISSING! mod #{self.name} #{args.inspect} #{table_name}"
615
+ puts "MISSING! #{self.name} #{args.inspect} #{table_name}"
597
616
  self._brick_const_missing(*args)
598
617
  end
599
618
  end
@@ -601,99 +620,6 @@ end
601
620
 
602
621
  class Object
603
622
  class << self
604
- # alias _brick_const_missing const_missing
605
- # def const_missing(*args)
606
- # # return self.const_get(args.first) if self.const_defined?(args.first)
607
- # # return Object.const_get(args.first) if Object.const_defined?(args.first) unless self == Object
608
- # if self.const_defined?(args.first) && (possible = self.const_get(args.first)) != self
609
- # return possible
610
- # end
611
- # if self != Object && Object.const_defined?(args.first) && (possible = Object.const_get(args.first)) != self
612
- # return possible
613
- # end
614
-
615
- # class_name = args.first.to_s
616
- # # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
617
- # # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
618
- # # that is, checking #qualified_name_for with: from_mod, const_name
619
- # # If we want to support namespacing in the future, might have to utilise something like this:
620
- # # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
621
- # # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
622
- # # If the file really exists, go and snag it:
623
- # if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
624
- # filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
625
- # end
626
- # if is_found
627
- # return self._brick_const_missing(*args)
628
- # elsif ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
629
- # my_const = parent.const_missing(class_name) # ends up having: MyModule::MyClass
630
- # return my_const
631
- # end
632
- # relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
633
- # result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
634
- # # Otherwise now it's up to us to fill in the gaps
635
- # # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
636
- # # VAbc instead of VABC)
637
- # if (model = Object.const_get(plural_class_name.underscore.singularize.camelize))
638
- # # if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
639
- # build_controller(nil, class_name, plural_class_name, model, relations)
640
- # end
641
- # elsif (::Brick.enable_models? || ::Brick.enable_controllers?) && # Schema match?
642
- # db_schema_name = [(singular_table_name = class_name.underscore),
643
- # (table_name = singular_table_name.pluralize),
644
- # class_name,
645
- # (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas.include?(s) }
646
- # # Build out a module for the schema if it's namespaced
647
- # schema_name = db_schema_name.camelize
648
- # unless Object.const_defined?(schema_name.to_sym)
649
- # Object.const_set(schema_name.to_sym, (built_module = Module.new))
650
- # Brick.db_schemas[db_schema_name] = built_module
651
- # [built_module, "module #{schema_name}; end\n"]
652
- # end
653
- # # # %%% Perhaps an option to use the first module just as schema, and additional modules as namespace with a table name prefix applied
654
- # # schema_name, model_name =
655
- # # code = +''
656
- # # mod_tree = +''
657
- # # model_name.split('::')[0..-2].each do |mod_name|
658
- # # mod_tree << "::#{mod_name}"
659
- # # Module.new(mod_tree)
660
- # # code << "module #{mod_tree}; end\n"
661
- # # end
662
- # elsif ::Brick.enable_models?
663
- # # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
664
- # # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
665
- # plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
666
- # singular_table_name = ActiveSupport::Inflector.underscore(model_name)
667
-
668
- # # Adjust for STI if we know of a base model for the requested model name
669
- # table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
670
- # base_model.table_name
671
- # else
672
- # ActiveSupport::Inflector.pluralize(singular_table_name)
673
- # end
674
-
675
- # # Maybe, just maybe there's a database table that will satisfy this need
676
- # if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(m) })
677
- # build_model(nil, model_name, singular_table_name, table_name, relations, matching)
678
- # end
679
- # end
680
- # if result
681
- # built_class, code = result
682
- # puts "\n#{code}"
683
- # built_class
684
- # elsif ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}") && !schema_name
685
- # # module_prefixes = type_name.split('::')
686
- # # path = self.name.split('::')[0..-2] + []
687
- # # module_prefixes.unshift('') unless module_prefixes.first.blank?
688
- # # candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
689
- # self._brick_const_missing(*args)
690
- # elsif self != Object
691
- # module_parent.const_missing(*args)
692
- # else
693
- # puts "MISSING! obj #{self.name}/#{schema_name} #{args.inspect} #{table_name}"
694
- # self._brick_const_missing(*args)
695
- # end
696
- # end
697
623
 
698
624
  private
699
625
 
@@ -701,8 +627,12 @@ class Object
701
627
  full_name = if schema_name.blank?
702
628
  model_name
703
629
  else # Prefix the schema to the table name + prefix the schema namespace to the class name
704
- schema_module = (Brick.db_schemas[schema_name] ||= self.const_get(schema_name.singularize.camelize))
705
- matching = "#{schema_name}.#{matching}"
630
+ schema_module = if schema_name.instance_of?(Module) # from an auto-STI namespace?
631
+ schema_name
632
+ else
633
+ matching = "#{schema_name}.#{matching}"
634
+ (Brick.db_schemas[schema_name] ||= self.const_get(schema_name.singularize.camelize))
635
+ end
706
636
  "#{schema_module&.name}::#{model_name}"
707
637
  end
708
638
 
@@ -711,7 +641,7 @@ class Object
711
641
 
712
642
  # Are they trying to use a pluralised class name such as "Employees" instead of "Employee"?
713
643
  if table_name == singular_table_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
714
- unless ::Brick.config.sti_namespace_prefixes&.key?("::#{singular_table_name.titleize}::")
644
+ unless ::Brick.config.sti_namespace_prefixes&.key?("::#{singular_table_name.camelize}::")
715
645
  puts "Warning: Class name for a model that references table \"#{matching}\" should be \"#{ActiveSupport::Inflector.singularize(model_name)}\"."
716
646
  end
717
647
  return
@@ -792,7 +722,7 @@ class Object
792
722
  end # class definition
793
723
  # Having this separate -- will this now work out better?
794
724
  built_model.class_exec do
795
- hmts.each do |hmt_fk, fks|
725
+ hmts&.each do |hmt_fk, fks|
796
726
  hmt_fk = hmt_fk.tr('.', '_')
797
727
  fks.each do |fk|
798
728
  through = fk.first[:assoc_name]
@@ -807,10 +737,14 @@ class Object
807
737
  else
808
738
  hmt_fk
809
739
  end
810
- source = fk.last unless hmt_name.singularize == fk.last
811
- code << " has_many :#{hmt_name}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
812
- options = { through: assoc_name }
813
- options[:source] = source.to_sym if source
740
+ options = { through: through.to_sym }
741
+ if relation[:fks].any? { |k, v| v[:assoc_name] == hmt_name }
742
+ hmt_name = "#{hmt_name.singularize}_#{fk.first[:assoc_name]}"
743
+ options[:class_name] = fk.first[:inverse_table].singularize.camelize
744
+ options[:foreign_key] = fk.first[:fk].to_sym
745
+ end
746
+ options[:source] = fk.last.to_sym unless hmt_name.singularize == fk.last
747
+ code << " has_many :#{hmt_name}#{options.map { |opt| ", #{opt.first}: #{opt.last.inspect}" }.join}\n"
814
748
  self.send(:has_many, hmt_name.to_sym, **options)
815
749
  end
816
750
  end
@@ -832,6 +766,7 @@ class Object
832
766
  else
833
767
  assoc[:assoc_name]
834
768
  end
769
+ options[:optional] = true if assoc.key?(:optional)
835
770
  if assoc.key?(:polymorphic)
836
771
  options[:polymorphic] = true
837
772
  else
@@ -875,7 +810,11 @@ class Object
875
810
  end
876
811
  # Figure out if we need to specially call out the class_name and/or foreign key
877
812
  # (and if either of those then definitely also a specific inverse_of)
878
- options[:class_name] = "::#{assoc[:primary_class]&.name || singular_table_name.split('.').map(&:camelize).join('::')}" if need_class_name
813
+ if (singular_table_parts = singular_table_name.split('.')).length > 1 &&
814
+ ::Brick.config.schema_behavior[:multitenant] && singular_table_parts.first == 'public'
815
+ singular_table_parts.shift
816
+ end
817
+ options[:class_name] = "::#{assoc[:primary_class]&.name || singular_table_parts.map(&:camelize).join('::')}" if need_class_name
879
818
  # Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
880
819
  if need_fk # Funky foreign key?
881
820
  options[:foreign_key] = if assoc[:fk].is_a?(Array)
@@ -889,7 +828,6 @@ class Object
889
828
 
890
829
  # Prepare a list of entries for "has_many :through"
891
830
  if macro == :has_many
892
- puts [inverse_table, relations[inverse_table].length].inspect
893
831
  relations[inverse_table][:hmt_fks].each do |k, hmt_fk|
894
832
  next if k == assoc[:fk]
895
833
 
@@ -993,7 +931,9 @@ class Object
993
931
  # return
994
932
  end
995
933
 
996
- instance_variable_set("@#{singular_table_name}".to_sym, (obj = model.find(params[:id].split(','))))
934
+ id = params[:id]&.split(/[\/,_]/)
935
+ id = id.first if id.is_a?(Array) && id.length == 1
936
+ instance_variable_set("@#{singular_table_name}".to_sym, (obj = model.find(id)))
997
937
  obj = obj.first if obj.is_a?(Array)
998
938
  obj.send(:update, send(params_name = params_name.to_sym))
999
939
  end
@@ -1040,11 +980,14 @@ module ActiveRecord::ConnectionHandling
1040
980
  conn
1041
981
  end
1042
982
 
983
+ # This is done separately so that during testing it can be called right after a migration
984
+ # in order to make sure everything is good.
1043
985
  def _brick_reflect_tables
986
+ initializer_loaded = false
1044
987
  if (relations = ::Brick.relations).empty?
1045
- # Hopefully our initializer is named exactly this!
988
+ # If there's schema things configured then we only expect our initializer to be named exactly this
1046
989
  if File.exist?(brick_initializer = Rails.root.join('config/initializers/brick.rb'))
1047
- load brick_initializer
990
+ initializer_loaded = load brick_initializer
1048
991
  end
1049
992
  # Only for Postgres? (Doesn't work in sqlite3)
1050
993
  # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
@@ -1052,7 +995,9 @@ module ActiveRecord::ConnectionHandling
1052
995
  schema_sql = 'SELECT NULL AS table_schema;'
1053
996
  case ActiveRecord::Base.connection.adapter_name
1054
997
  when 'PostgreSQL'
1055
- if (::Brick.default_schema = schema = ::Brick.config.schema_behavior&.[](:multitenant)&.[](:schema_to_analyse))
998
+ if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
999
+ (sta = multitenancy[:schema_to_analyse]) != 'public')
1000
+ ::Brick.default_schema = schema = sta
1056
1001
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1057
1002
  end
1058
1003
  schema_sql = 'SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;'
@@ -1070,6 +1015,28 @@ module ActiveRecord::ConnectionHandling
1070
1015
  puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
1071
1016
  end
1072
1017
 
1018
+ unless (db_schemas = ActiveRecord::Base.execute_sql(schema_sql)).is_a?(Array)
1019
+ db_schemas = db_schemas.to_a
1020
+ end
1021
+ unless db_schemas.empty?
1022
+ ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1023
+ row = row.is_a?(String) ? row : row['table_schema']
1024
+ # Remove any system schemas
1025
+ s[row] = nil unless ['information_schema', 'pg_catalog'].include?(row)
1026
+ end
1027
+ end
1028
+
1029
+ if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1030
+ if (possible_schema = ::Brick.config.schema_behavior&.[](:multitenant)&.[](:schema_to_analyse))
1031
+ if ::Brick.db_schemas.key?(possible_schema)
1032
+ ::Brick.default_schema = schema = possible_schema
1033
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1034
+ else
1035
+ puts "*** In the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\". This schema does not exist. ***"
1036
+ end
1037
+ end
1038
+ end
1039
+
1073
1040
  sql ||= "SELECT t.table_schema AS schema, t.table_name AS relation_name, t.table_type,
1074
1041
  c.column_name, c.data_type,
1075
1042
  COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
@@ -1096,10 +1063,9 @@ module ActiveRecord::ConnectionHandling
1096
1063
  measures = []
1097
1064
  case ActiveRecord::Base.connection.adapter_name
1098
1065
  when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
1099
- schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1066
+ # schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1100
1067
  ActiveRecord::Base.execute_sql(sql).each do |r|
1101
- # next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
1102
- relation_name = if r['schema'] != schema
1068
+ relation_name = if ![schema, 'public'].include?(r['schema']) && !::Brick.config.schema_behavior[:multitenant]
1103
1069
  "#{schema_name = r['schema']}.#{r['relation_name']}"
1104
1070
  else
1105
1071
  r['relation_name']
@@ -1122,7 +1088,6 @@ module ActiveRecord::ConnectionHandling
1122
1088
  end
1123
1089
  else # MySQL2 acts a little differently, bringing back an array for each row
1124
1090
  ActiveRecord::Base.execute_sql(sql).each do |r|
1125
- # next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
1126
1091
  relation = relations[(relation_name = r[0])] # here relation represents a table or view from the database
1127
1092
  relation[:isView] = true if r[1] == 'VIEW' # table_type
1128
1093
  col_name = r[2]
@@ -1161,7 +1126,7 @@ module ActiveRecord::ConnectionHandling
1161
1126
  # end
1162
1127
  # end
1163
1128
  # end
1164
- schema = ::Brick.default_schema # Reset back for this next round of fun
1129
+ # schema = ::Brick.default_schema # Reset back for this next round of fun
1165
1130
  case ActiveRecord::Base.connection.adapter_name
1166
1131
  when 'PostgreSQL', 'Mysql2'
1167
1132
  sql = "SELECT kcu1.CONSTRAINT_SCHEMA, kcu1.TABLE_NAME, kcu1.COLUMN_NAME,
@@ -1176,7 +1141,7 @@ module ActiveRecord::ConnectionHandling
1176
1141
  AND kcu2.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
1177
1142
  AND kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
1178
1143
  AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION#{"
1179
- WHERE kcu1.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if schema }"
1144
+ WHERE kcu1.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if schema }"
1180
1145
  # AND kcu2.TABLE_NAME = ?;", Apartment::Tenant.current, table_name
1181
1146
  when 'SQLite'
1182
1147
  sql = "SELECT m.name, fkl.\"from\", fkl.\"table\", m.name || '_' || fkl.\"from\" AS constraint_name
@@ -1186,19 +1151,12 @@ module ActiveRecord::ConnectionHandling
1186
1151
  else
1187
1152
  end
1188
1153
  if sql
1189
- ::Brick.default_schema ||= schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1190
- unless (db_schemas = ActiveRecord::Base.execute_sql(schema_sql)).is_a?(Array)
1191
- db_schemas = db_schemas.to_a
1192
- end
1193
- unless db_schemas.empty?
1194
- ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1195
- row = row.is_a?(String) ? row : row['table_schema']
1196
- # Remove whatever default schema we're using and other system schemas
1197
- s[row] = nil unless ['information_schema', 'pg_catalog', schema].include?(row)
1198
- end
1199
- end
1154
+ # ::Brick.default_schema ||= schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1200
1155
  ActiveRecord::Base.execute_sql(sql).each do |fk|
1201
1156
  fk = fk.values unless fk.is_a?(Array)
1157
+ # Multitenancy makes things a little more general overall
1158
+ fk[0] = nil if fk[0] == 'public' || (is_multitenant && fk[0] == schema)
1159
+ fk[3] = nil if fk[3] == 'public' || (is_multitenant && fk[3] == schema)
1202
1160
  ::Brick._add_bt_and_hm(fk, relations)
1203
1161
  end
1204
1162
  end
@@ -1211,11 +1169,7 @@ module ActiveRecord::ConnectionHandling
1211
1169
  views.keys.each { |k| puts ActiveSupport::Inflector.singularize(k).camelize }
1212
1170
  end
1213
1171
 
1214
- # Try to load the initializer pretty danged early
1215
- if File.exist?(brick_initialiser = Rails.root.join('config/initializers/brick.rb'))
1216
- load brick_initialiser
1217
- ::Brick.load_additional_references
1218
- end
1172
+ ::Brick.load_additional_references if initializer_loaded
1219
1173
  end
1220
1174
  end
1221
1175
 
@@ -1242,31 +1196,37 @@ module Brick
1242
1196
  # rubocop:enable Style/CommentedKeyword
1243
1197
 
1244
1198
  class << self
1245
- def _add_bt_and_hm(fk, relations, is_polymorphic = false)
1246
- if (bt_assoc_name = fk[2].underscore).end_with?('_id')
1247
- bt_assoc_name = bt_assoc_name[0..-4]
1248
- elsif bt_assoc_name.end_with?('id') && bt_assoc_name.exclude?('_') # Make the bold assumption that we can just peel off the final ID part
1249
- bt_assoc_name = bt_assoc_name[0..-3]
1250
- else
1251
- bt_assoc_name = "#{bt_assoc_name}_bt"
1199
+ def _add_bt_and_hm(fk, relations, is_polymorphic = false, is_optional = false)
1200
+ bt_assoc_name = fk[2]
1201
+ unless is_polymorphic
1202
+ bt_assoc_name = if bt_assoc_name.underscore.end_with?('_id')
1203
+ bt_assoc_name[0..-4]
1204
+ elsif bt_assoc_name.downcase.end_with?('id') && bt_assoc_name.exclude?('_')
1205
+ bt_assoc_name[0..-3] # Make the bold assumption that we can just peel off any final ID part
1206
+ else
1207
+ "#{bt_assoc_name}_bt"
1208
+ end
1252
1209
  end
1253
1210
  # %%% Temporary schema patch
1254
- fk[1] = "#{fk[0]}.#{for_tbl = fk[1]}" if fk[0] && fk[0] != ::Brick.default_schema
1255
- for_tbl << '_' if for_tbl
1211
+ for_tbl = fk[1]
1212
+ fk[1] = "#{fk[0]}.#{fk[1]}" if fk[0] # && fk[0] != ::Brick.default_schema
1256
1213
  bts = (relation = relations.fetch(fk[1], nil))&.fetch(:fks) { relation[:fks] = {} }
1257
1214
  # %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
1258
1215
  # Maybe it's already gotten this info because we got as far as to say there was a unique class
1259
1216
  primary_table = if (is_class = fk[4].is_a?(Hash) && fk[4].key?(:class))
1260
1217
  pri_tbl = (primary_class = fk[4][:class].constantize).table_name
1261
1218
  else
1219
+ is_schema = fk[3] != ::Brick.default_schema && (::Brick.config.schema_behavior[:multitenant] || fk[3] != 'public')
1262
1220
  pri_tbl = fk[4]
1263
- (fk[3] && fk[3] != ::Brick.default_schema) ? "#{fk[3]}.#{pri_tbl}" : pri_tbl
1221
+ fk[3] && is_schema ? "#{fk[3]}.#{pri_tbl}" : pri_tbl
1264
1222
  end
1265
1223
  hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
1266
1224
 
1267
1225
  unless (cnstr_name = fk[5])
1268
1226
  # For any appended references (those that come from config), arrive upon a definitely unique constraint name
1269
- cnstr_base = cnstr_name = "(brick) #{for_tbl}#{is_class ? fk[4][:class].underscore : pri_tbl}"
1227
+ pri_tbl = is_class ? fk[4][:class].underscore : pri_tbl
1228
+ pri_tbl = "#{bt_assoc_name}_#{pri_tbl}" if pri_tbl.singularize != bt_assoc_name
1229
+ cnstr_base = cnstr_name = "(brick) #{for_tbl}_#{pri_tbl}"
1270
1230
  cnstr_added_num = 1
1271
1231
  cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
1272
1232
  missing = []
@@ -1306,6 +1266,7 @@ module Brick
1306
1266
  else
1307
1267
  inverse_table = [primary_table] if is_polymorphic
1308
1268
  assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[2], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
1269
+ assoc_bt[:optional] = true if is_optional
1309
1270
  assoc_bt[:polymorphic] = true if is_polymorphic
1310
1271
  end
1311
1272
  if is_class
@@ -1326,7 +1287,7 @@ module Brick
1326
1287
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
1327
1288
  assoc_hm[:inverse] = assoc_bt
1328
1289
  else
1329
- assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[2], assoc_name: fk[1].tr('.', '_').pluralize, alternate_name: bt_assoc_name, inverse_table: fk[1], inverse: assoc_bt }
1290
+ assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[2], assoc_name: for_tbl.pluralize, alternate_name: bt_assoc_name, inverse_table: fk[1], inverse: assoc_bt }
1330
1291
  assoc_hm[:polymorphic] = true if is_polymorphic
1331
1292
  hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
1332
1293
  hm_counts[fk[1]] = hm_counts.fetch(fk[1]) { 0 } + 1
@@ -222,7 +222,7 @@ end %>"
222
222
  if ['index', 'show', 'update'].include?(args.first)
223
223
  poly_cols = []
224
224
  css << "<% bts = { #{
225
- bts.each_with_object([]) do |v, s|
225
+ bt_items = bts.each_with_object([]) do |v, s|
226
226
  foreign_models = if v.last[2] # Polymorphic?
227
227
  poly_cols << @_brick_model.reflect_on_association(v[1].first).foreign_type
228
228
  v.last[1].each_with_object([]) { |x, s| s << "[#{x.name}, #{x.primary_key.inspect}]" }.join(', ')
@@ -230,7 +230,10 @@ end %>"
230
230
  "[#{v.last[1].name}, #{v.last[1].primary_key.inspect}]"
231
231
  end
232
232
  s << "#{v.first.inspect} => [#{v.last.first.inspect}, [#{foreign_models}], #{v.last[2].inspect}]"
233
- end.join(', ')
233
+ end
234
+ # # %%% Need to fix poly going to an STI class
235
+ # binding.pry unless poly_cols.empty?
236
+ bt_items.join(', ')
234
237
  } }
235
238
  poly_cols = #{poly_cols.inspect} %>"
236
239
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 31
8
+ TINY = 32
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
data/lib/brick.rb CHANGED
@@ -93,16 +93,19 @@ module Brick
93
93
  attr_accessor :default_schema, :db_schemas
94
94
 
95
95
  def set_db_schema(params)
96
- schema = params['_brick_schema']
96
+ schema = params['_brick_schema'] || 'public'
97
97
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema) if schema && ::Brick.db_schemas&.include?(schema)
98
98
  end
99
99
 
100
100
  # All tables and views (what Postgres calls "relations" including column and foreign key info)
101
101
  def relations
102
- connections = Brick.instance_variable_get(:@relations) ||
103
- Brick.instance_variable_set(:@relations, (connections = {}))
104
102
  # Key our list of relations for this connection off of the connection pool's object_id
105
- (connections[ActiveRecord::Base.connection_pool.object_id] ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } })
103
+ (@relations ||= {})[ActiveRecord::Base.connection_pool.object_id] ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } }
104
+ end
105
+
106
+ # If multitenancy is enabled, a list of non-tenanted "global" models
107
+ def non_tenanted_models
108
+ @pending_models ||= {}
106
109
  end
107
110
 
108
111
  def get_bts_and_hms(model)
@@ -314,7 +317,7 @@ module Brick
314
317
  if ars
315
318
  ars.each do |ar|
316
319
  fk = ar.length < 5 ? [nil, +ar[0], ar[1], nil, +ar[2]] : [ar[0], +ar[1], ar[2], ar[3], +ar[4], ar[5]]
317
- ::Brick._add_bt_and_hm(fk, relations)
320
+ ::Brick._add_bt_and_hm(fk, relations, false, true)
318
321
  end
319
322
  end
320
323
  if (polys = ::Brick.config.polymorphics)
@@ -327,7 +330,7 @@ module Brick
327
330
  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'] }
328
331
  v.each do |type|
329
332
  if relations.key?(primary_table = type.underscore.pluralize)
330
- ::Brick._add_bt_and_hm([nil, table_name, poly, nil, primary_table, "(brick) #{table_name}_#{poly}"], relations, true)
333
+ ::Brick._add_bt_and_hm([nil, table_name, poly, nil, primary_table, "(brick) #{table_name}_#{poly}"], relations, true, true)
331
334
  else
332
335
  missing_stis[primary_table] = type unless ::Brick.existing_stis.key?(type)
333
336
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brick
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.31
4
+ version: 1.0.32
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-29 00:00:00.000000000 Z
11
+ date: 2022-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord