brick 1.0.91 → 1.0.93

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: 81eaa518a99452286bdaf1e5ed33b83962ede9f9c6509f0b472fd0fe19a8eceb
4
- data.tar.gz: 06f5653ce40eb6ee1568b380cf3063073ac7735add8aab6c5873a343e8018a70
3
+ metadata.gz: 8488acac0623c04f11e916c67303fd06d471154231c92613318cc7458fcfe23f
4
+ data.tar.gz: af566c853bb25e7497a6afbba539ad0d14ec23ad000c053e6dbdf757deefbaa0
5
5
  SHA512:
6
- metadata.gz: 7e0a678885c927a7a80c8758d265c2ff351f67ea44e8e1b2c128e123df441f4f72a62f775b9785615eeacbdbff99427e9d1590890bdffd06165ae7b2339fe1af
7
- data.tar.gz: 7bd3e424225e435bab4517fb14aeaa3ca128041a746aaf413ecdb17a771444c3413f058ff28cc5e303d074fdd7cc7d21ff66f842667c14226d1849e96d4b3a98
6
+ metadata.gz: d0d8428b6f80bf3a79abb16265fb8625404f92caf30034da94186a5c5982a78543f29e0a49cc0dc515eb8b6d6da24ccdd53e8d089c90dce46ce214ce5bee4339
7
+ data.tar.gz: 562bd7522f0f45843a89464645e1cc153e455e1e5b4e3dec5f1b38a8bfaefd8bd359a2538aa274b90b50353285d2175f315b7e304e54e60516a5ce2ed3b2fc7e
@@ -11,6 +11,7 @@ unless ActiveRecord.respond_to?(:version)
11
11
  end
12
12
 
13
13
  # ActiveSupport, ActionPack, and ActionView before 4.0 didn't have #version
14
+ require 'active_support' # Needed for Rails 4.x
14
15
  unless ActiveSupport.respond_to?(:version)
15
16
  module ActiveSupport
16
17
  def self.version
@@ -42,21 +42,6 @@
42
42
  # Dynamically create model or controller classes when needed
43
43
  # ==========================================================
44
44
 
45
- # By default all models indicate that they are not views
46
- module Arel
47
- class Table
48
- def _arel_table_type
49
- # AR < 4.2 doesn't have type_caster at all, so rely on an instance variable getting set
50
- # AR 4.2 - 5.1 have buggy type_caster entries for the root node
51
- instance_variable_get(:@_arel_table_type) ||
52
- # 5.2-7.0 does type_caster just fine, no bugs there, but the property with the type differs:
53
- # 5.2 has "types" as public, 6.0 "types" as private, and >= 6.1 "klass" as private.
54
- ((tc = send(:type_caster)) && tc.instance_variable_get(:@types)) ||
55
- tc.send(:klass)
56
- end
57
- end
58
- end
59
-
60
45
  module ActiveRecord
61
46
  class Base
62
47
  def self.is_brick?
@@ -270,7 +255,7 @@ module ActiveRecord
270
255
 
271
256
  def self._brick_index(mode = nil)
272
257
  tbl_parts = ((mode == :singular) ? table_name.singularize : table_name).split('.')
273
- tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == Apartment.default_schema
258
+ tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
274
259
  tbl_parts.unshift(::Brick.config.path_prefix) if ::Brick.config.path_prefix
275
260
  index = tbl_parts.map(&:underscore).join('_')
276
261
  # Rails applies an _index suffix to that route when the resource name is singular
@@ -407,83 +392,10 @@ module ActiveRecord
407
392
  end
408
393
 
409
394
  class Relation
410
- attr_reader :_brick_chains, :_arel_applied_aliases
411
-
412
- # CLASS STUFF
413
- def _recurse_arel(piece, prefix = '')
414
- names = []
415
- # Our JOINs mashup of nested arrays and hashes
416
- # binding.pry if defined?(@arel)
417
- case piece
418
- when Array
419
- names += piece.inject([]) { |s, v| s + _recurse_arel(v, prefix) }
420
- when Hash
421
- names += piece.inject([]) do |s, v|
422
- new_prefix = "#{prefix}#{v.first}_"
423
- s << [v.last.shift, new_prefix]
424
- s + _recurse_arel(v.last, new_prefix)
425
- end
426
-
427
- # ActiveRecord AREL objects
428
- when Arel::Nodes::Join # INNER or OUTER JOIN
429
- # rubocop:disable Style/IdenticalConditionalBranches
430
- if piece.right.is_a?(Arel::Table) # Came in from AR < 3.2?
431
- # Arel 2.x and older is a little curious because these JOINs work "back to front".
432
- # The left side here is either another earlier JOIN, or at the end of the whole tree, it is
433
- # the first table.
434
- names += _recurse_arel(piece.left)
435
- # The right side here at the top is the very last table, and anywhere else down the tree it is
436
- # the later "JOIN" table of this pair. (The table that comes after all the rest of the JOINs
437
- # from the left side.)
438
- names << [piece.right._arel_table_type, (piece.right.table_alias || piece.right.name)]
439
- else # "Normal" setup, fed from a JoinSource which has an array of JOINs
440
- # The left side is the "JOIN" table
441
- names += _recurse_arel(table = piece.left)
442
- # The expression on the right side is the "ON" clause
443
- # on = piece.right.expr
444
- # # Find the table which is not ourselves, and thus must be the "path" that led us here
445
- # parent = piece.left == on.left.relation ? on.right.relation : on.left.relation
446
- # binding.pry if piece.left.is_a?(Arel::Nodes::TableAlias)
447
- if table.is_a?(Arel::Nodes::TableAlias)
448
- @_arel_applied_aliases << (alias_name = table.right)
449
- table = table.left
450
- end
451
- (_brick_chains[table._arel_table_type] ||= []) << (alias_name || table.table_alias || table.name)
452
- end
453
- # rubocop:enable Style/IdenticalConditionalBranches
454
- when Arel::Table # Table
455
- names << [piece._arel_table_type, (piece.table_alias || piece.name)]
456
- when Arel::Nodes::TableAlias # Alias
457
- # Can get the real table name from: self._recurse_arel(piece.left)
458
- names << [piece.left._arel_table_type, piece.right.to_s] # This is simply a string; the alias name itself
459
- when Arel::Nodes::JoinSource # Leaving this until the end because AR < 3.2 doesn't know at all about JoinSource!
460
- # Spin up an empty set of Brick alias name chains at the start
461
- @_brick_chains = {}
462
- # The left side is the "FROM" table
463
- names << (this_name = [piece.left._arel_table_type, (piece.left.table_alias || piece.left.name)])
464
- # # Do not currently need the root "FROM" table in our list of chains
465
- # (_brick_chains[this_name.first] ||= []) << this_name.last
466
- # The right side is an array of all JOINs
467
- piece.right.each { |join| names << _recurse_arel(join) }
468
- end
469
- names
470
- end
471
-
472
- # INSTANCE STUFF
473
- def _arel_alias_names
474
- @_arel_applied_aliases = []
475
- # %%% If with Rails 3.1 and older you get "NoMethodError: undefined method `eq' for nil:NilClass"
476
- # when trying to call relation.arel, then somewhere along the line while navigating a has_many
477
- # relationship it can't find the proper foreign key.
478
- core = arel.ast.cores.first
479
- # Accommodate AR < 3.2
480
- if core.froms.is_a?(Arel::Table)
481
- # All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource
482
- _recurse_arel(core.source)
483
- else
484
- # With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
485
- _recurse_arel(core.froms)
486
- end
395
+ # Links from ActiveRecord association pathing names over to real table correlation names
396
+ # that get chosen when the AREL AST tree is walked.
397
+ def brick_links
398
+ @brick_links ||= {}
487
399
  end
488
400
 
489
401
  def brick_select(params, selects = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
@@ -501,13 +413,16 @@ module ActiveRecord
501
413
  params.each do |k, v|
502
414
  next if ['_brick_schema', '_brick_order', 'controller', 'action'].include?(k)
503
415
 
504
- case (ks = k.split('.')).length
416
+ if (where_col = (ks = k.split('.')).last)[-1] == '!'
417
+ where_col = where_col[0..-2]
418
+ end
419
+ case ks.length
505
420
  when 1
506
- next unless klass.column_names.any?(k) || klass._brick_get_fks.include?(k)
421
+ next unless klass.column_names.any?(where_col) || klass._brick_get_fks.include?(where_col)
507
422
  when 2
508
423
  assoc_name = ks.first.to_sym
509
424
  # Make sure it's a good association name and that the model has that column name
510
- next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
425
+ next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(where_col)
511
426
 
512
427
  join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
513
428
  is_distinct = true
@@ -545,10 +460,20 @@ module ActiveRecord
545
460
 
546
461
  if join_array.present?
547
462
  left_outer_joins!(join_array)
548
- # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
549
- (rel_dupe = dup)._arel_alias_names
463
+ # Touching AREL AST walks the JoinDependency tree, and in that process uses our
464
+ # "brick_links" patch to find how every AR chain of association names relates to exact
465
+ # table correlation names chosen by AREL. We use a duplicate relation object for this
466
+ # because an important side-effect of referencing the AST is that the @arel instance
467
+ # variable gets set, and this is a signal to ActiveRecord that a relation has now
468
+ # become immutable. (We aren't quite ready for our "real deal" relation object to be
469
+ # set in stone ... still need to add .select(), and possibly .where() and .order()
470
+ # things ... also if there are any HM counts then an OUTER JOIN for each of them out
471
+ # to a derived table to do that counting. All of these things need to know proper
472
+ # table correlation names, which will now become available in brick_links on the
473
+ # rel_dupe object.)
474
+ (rel_dupe = dup).arel.ast
475
+
550
476
  core_selects = selects.dup
551
- chains = rel_dupe._brick_chains
552
477
  id_for_tables = Hash.new { |h, k| h[k] = [] }
553
478
  field_tbl_names = Hash.new { |h, k| h[k] = {} }
554
479
  used_col_aliases = {} # Used to make sure there is not a name clash
@@ -569,7 +494,7 @@ module ActiveRecord
569
494
  key_alias = nil
570
495
  cc.first.each do |cc_part|
571
496
  dest_klass = cc_part[0..-2].inject(klass) { |kl, cc_part_term| kl.reflect_on_association(cc_part_term).klass }
572
- tbl_name = (field_tbl_names[k][cc_part.last] ||= shift_or_first(chains[dest_klass])).split('.').last
497
+ tbl_name = rel_dupe.brick_links[cc_part[0..-2].map(&:to_s).join('.')]
573
498
  # Deal with the conflict if there are two parts in the custom column named the same,
574
499
  # "category.name" and "product.name" for instance will end up with aliases of "name"
575
500
  # and "product__name".
@@ -606,23 +531,18 @@ module ActiveRecord
606
531
 
607
532
  klass._br_bt_descrip.each do |v|
608
533
  v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
609
- next if chains[k1].nil?
534
+ next unless (tbl_name = rel_dupe.brick_links[v.first.to_s]&.split('.')&.last)
610
535
 
611
- tbl_name = (field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])).split('.').last
612
536
  # If it's Oracle, quote any AREL aliases that had been applied
613
- tbl_name = "\"#{tbl_name}\"" if ::Brick.is_oracle && rel_dupe._arel_applied_aliases.include?(tbl_name)
537
+ tbl_name = "\"#{tbl_name}\"" if ::Brick.is_oracle && rel_dupe.brick_links.values.include?(tbl_name)
614
538
  field_tbl_name = nil
615
- v1.map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
616
- # unless chains[sel_col.first]
617
- # puts 'You might have some bogus DSL in your brick.rb file'
618
- # next
619
- # end
620
- field_tbl_name = (field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])).split('.').last
539
+ v1.map { |x| [x[0..-2].map(&:to_s).join('.'), x.last] }.each_with_index do |sel_col, idx|
540
+ field_tbl_name = rel_dupe.brick_links[sel_col.first].split('.').last
621
541
  # If it's Oracle, quote any AREL aliases that had been applied
622
- field_tbl_name = "\"#{field_tbl_name}\"" if ::Brick.is_oracle && rel_dupe._arel_applied_aliases.include?(field_tbl_name)
542
+ field_tbl_name = "\"#{field_tbl_name}\"" if ::Brick.is_oracle && rel_dupe.brick_links.values.include?(field_tbl_name)
623
543
 
624
544
  # Postgres can not use DISTINCT with any columns that are XML, so for any of those just convert to text
625
- is_xml = is_distinct && Brick.relations[sel_col.first.table_name]&.[](:cols)&.[](sel_col.last)&.first&.start_with?('xml')
545
+ is_xml = is_distinct && Brick.relations[field_tbl_name]&.[](:cols)&.[](sel_col.last)&.first&.start_with?('xml')
626
546
  # If it's not unique then also include the belongs_to association name before the column name
627
547
  if used_col_aliases.key?(col_alias = "br_fk_#{v.first}__#{sel_col.last}")
628
548
  col_alias = "br_fk_#{v.first}__#{v1[idx][-2..-1].map(&:to_s).join('__')}"
@@ -659,14 +579,9 @@ module ActiveRecord
659
579
  end
660
580
  end
661
581
  join_array.each do |assoc_name|
662
- # %%% Need to support {user: :profile}
663
582
  next unless assoc_name.is_a?(Symbol)
664
583
 
665
- table_alias = if (chain = chains[klass = reflect_on_association(assoc_name)&.klass])
666
- shift_or_first(chain)
667
- else
668
- klass.table_name # ActiveRecord < 4.2 can't (yet) use the cool chains thing
669
- end
584
+ table_alias = rel_dupe.brick_links[assoc_name.to_s]
670
585
  _assoc_names[assoc_name] = [table_alias, klass]
671
586
  end
672
587
  end
@@ -677,26 +592,47 @@ module ActiveRecord
677
592
  # Build the chain of JOINs going to the final destination HMT table
678
593
  # (Usually just one JOIN, but could be many.)
679
594
  hmt_assoc = hm
680
- x = []
681
- x.unshift(hmt_assoc) while hmt_assoc.options[:through] && (hmt_assoc = klass.reflect_on_association(hmt_assoc.options[:through]))
682
- from_clause = +"#{x.first.table_name} br_t0"
683
- fk_col = x.shift.foreign_key
684
- link_back = [klass.primary_key] # %%% Inverse path back to the original object -- used to build out a link with a filter
595
+ through_sources = []
596
+ # %%% Inverse path back to the original object -- not yet used, but soon
597
+ # will be leveraged in order to build links with multi-table-hop filters.
598
+ link_back = []
599
+ # Track polymorphic type field if necessary
600
+ if hm.source_reflection.options[:as]
601
+ poly_ft = [hm.source_reflection.inverse_of.foreign_type, hmt_assoc.source_reflection.class_name]
602
+ end
603
+ # link_back << hm.source_reflection.inverse_of.name
604
+ while hmt_assoc.options[:through] && (hmt_assoc = klass.reflect_on_association(hmt_assoc.options[:through]))
605
+ through_sources.unshift(hmt_assoc)
606
+ end
607
+ # Turn the last member of link_back into a foreign key
608
+ link_back << hmt_assoc.source_reflection.foreign_key
609
+ # If it's a HMT based on a HM -> HM, must JOIN the last table into the mix at the end
610
+ through_sources.push(hm.source_reflection) unless hm.source_reflection.belongs_to?
611
+ from_clause = +"#{through_sources.first.table_name} br_t0"
612
+ fk_col = through_sources.shift.foreign_key
613
+
685
614
  idx = 0
686
615
  bail_out = nil
687
- x.map do |a|
616
+ through_sources.map do |a|
688
617
  from_clause << "\n LEFT OUTER JOIN #{a.table_name} br_t#{idx += 1} "
689
618
  from_clause << if (src_ref = a.source_reflection).macro == :belongs_to
619
+ (nm = hmt_assoc.source_reflection.inverse_of&.name)
620
+ # binding.pry unless nm
621
+ link_back << nm
690
622
  "ON br_t#{idx}.id = br_t#{idx - 1}.#{a.foreign_key}"
691
623
  elsif src_ref.options[:as]
692
624
  "ON br_t#{idx}.#{src_ref.type} = '#{src_ref.active_record.name}'" + # "polymorphable_type"
693
625
  " AND br_t#{idx}.#{src_ref.foreign_key} = br_t#{idx - 1}.id"
694
626
  elsif src_ref.options[:source_type]
695
- print "Skipping #{hm.name} --HMT-> #{hm.source_reflection.name} as it uses source_type which is not supported"
627
+ print "Skipping #{hm.name} --HMT-> #{hm.source_reflection.name} as it uses source_type which is not yet supported"
696
628
  nix << k
697
629
  bail_out = true
698
630
  break
699
631
  else # Standard has_many
632
+ # binding.pry unless (
633
+ nm = hmt_assoc.source_reflection.inverse_of&.name
634
+ # )
635
+ link_back << nm # if nm
700
636
  "ON br_t#{idx}.#{a.foreign_key} = br_t#{idx - 1}.id"
701
637
  end
702
638
  link_back.unshift(a.source_reflection.name)
@@ -704,6 +640,7 @@ module ActiveRecord
704
640
  end
705
641
  next if bail_out
706
642
 
643
+ # puts "LINK BACK! #{k} : #{hm.table_name} #{link_back.map(&:to_s).join('.')}"
707
644
  # count_column is determined from the originating HMT member
708
645
  if (src_ref = hm.source_reflection).nil?
709
646
  puts "*** Warning: Could not determine destination model for this HMT association in model #{klass.name}:\n has_many :#{hm.name}, through: :#{hm.options[:through]}"
@@ -711,8 +648,10 @@ module ActiveRecord
711
648
  nix << k
712
649
  next
713
650
  elsif src_ref.macro == :belongs_to # Traditional HMT using an associative table
651
+ # binding.pry if link_back.length > 2
714
652
  "br_t#{idx}.#{hm.foreign_key}"
715
653
  else # A HMT that goes HM -> HM, something like Categories -> Products -> LineItems
654
+ # binding.pry if link_back.length > 2
716
655
  "br_t#{idx}.#{src_ref.active_record.primary_key}"
717
656
  end
718
657
  else
@@ -772,17 +711,23 @@ JOIN (SELECT #{hm_selects.map { |s| "#{'br_t0.' if from_clause}#{s}" }.join(', '
772
711
 
773
712
  unless wheres.empty?
774
713
  # Rewrite the wheres to reference table and correlation names built out by AREL
714
+ where_nots = {}
775
715
  wheres2 = wheres.each_with_object({}) do |v, s|
716
+ is_not = if v.first[-1] == '!'
717
+ v[0] = v[0][0..-2] # Take off ending ! from column name
718
+ end
776
719
  if (v_parts = v.first.split('.')).length == 1
777
- s[v.first] = v.last
720
+ (is_not ? where_nots : s)[v.first] = v.last
778
721
  else
779
- k1 = klass.reflect_on_association(v_parts.first)&.klass
780
- tbl_name = (field_tbl_names[v_parts.first][k1] ||= shift_or_first(chains[k1])).split('.').last
781
- s["#{tbl_name}.#{v_parts.last}"] = v.last
722
+ tbl_name = rel_dupe.brick_links[v_parts.first].split('.').last
723
+ (is_not ? where_nots : s)["#{tbl_name}.#{v_parts.last}"] = v.last
782
724
  end
783
725
  end
784
726
  if respond_to?(:where!)
785
- where!(wheres2)
727
+ where!(wheres2) if wheres2.present?
728
+ if where_nots.present?
729
+ self.where_clause += WhereClause.new(predicate_builder.build_from_hash(where_nots)).invert
730
+ end
786
731
  else # AR < 4.0
787
732
  self.where_values << build_where(wheres2)
788
733
  end
@@ -1078,7 +1023,8 @@ Module.class_exec do
1078
1023
  (table_name = singular_table_name.pluralize),
1079
1024
  ::Brick.is_oracle ? class_name.upcase : class_name,
1080
1025
  (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas&.include?(s) }&.camelize ||
1081
- (::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name))
1026
+ (::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name) ||
1027
+ (::Brick.config.table_name_prefixes&.values.include?(class_name) && class_name))
1082
1028
  return self.const_get(schema_name) if self.const_defined?(schema_name)
1083
1029
 
1084
1030
  # Build out a module for the schema if it's namespaced
@@ -1129,6 +1075,7 @@ class Object
1129
1075
  private
1130
1076
 
1131
1077
  def build_model(relations, base_module, base_name, class_name, inheritable_name = nil)
1078
+ tnp = ::Brick.config.table_name_prefixes&.find { |p| p.last == base_module.name }&.first
1132
1079
  if (base_model = ::Brick.config.sti_namespace_prefixes&.fetch("::#{base_module.name}::", nil)&.constantize) || # Are we part of an auto-STI namespace? ...
1133
1080
  base_module != Object # ... or otherwise already in some namespace?
1134
1081
  schema_name = [(singular_schema_name = base_name.underscore),
@@ -1150,11 +1097,11 @@ class Object
1150
1097
  table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
1151
1098
  base_model.table_name
1152
1099
  else
1153
- ActiveSupport::Inflector.pluralize(singular_table_name)
1100
+ "#{tnp}#{ActiveSupport::Inflector.pluralize(singular_table_name)}"
1154
1101
  end
1155
1102
  if ::Brick.apartment_multitenant &&
1156
1103
  Apartment.excluded_models.include?(table_name.singularize.camelize)
1157
- schema_name = Apartment.default_schema
1104
+ schema_name = ::Brick.apartment_default_tenant
1158
1105
  end
1159
1106
  # Maybe, just maybe there's a database table that will satisfy this need
1160
1107
  if (matching = [table_name, singular_table_name, plural_class_name, model_name, table_name.titleize].find { |m| relations.key?(schema_name ? "#{schema_name}.#{m}" : m) })
@@ -1165,7 +1112,7 @@ class Object
1165
1112
 
1166
1113
  def build_model_worker(schema_name, inheritable_name, model_name, singular_table_name, table_name, relations, matching)
1167
1114
  if ::Brick.apartment_multitenant &&
1168
- schema_name == Apartment.default_schema
1115
+ schema_name == ::Brick.apartment_default_tenant
1169
1116
  relation = relations["#{schema_name}.#{matching}"]
1170
1117
  end
1171
1118
  full_name = if relation || schema_name.blank?
@@ -1367,7 +1314,7 @@ class Object
1367
1314
  # If it's multitenant with something like: public.____ ...
1368
1315
  if (it_parts = inverse_table.split('.')).length > 1 &&
1369
1316
  ::Brick.apartment_multitenant &&
1370
- it_parts.first == Apartment.default_schema
1317
+ it_parts.first == ::Brick.apartment_default_tenant
1371
1318
  it_parts.shift # ... then ditch the generic schema name
1372
1319
  end
1373
1320
  inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[inverse_table], inverse, it_parts.join('_').singularize)
@@ -1464,7 +1411,7 @@ class Object
1464
1411
  instance_variable_set(:@resources, ::Brick.get_status_of_resources)
1465
1412
  end
1466
1413
  self.define_method :orphans do
1467
- instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params)))
1414
+ instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params).first))
1468
1415
  end
1469
1416
  return [new_controller_class, code + "end # BrickGem controller\n"]
1470
1417
  when 'BrickOpenapi'
@@ -1482,7 +1429,7 @@ class Object
1482
1429
  api_params = referrer_params&.to_h
1483
1430
  end
1484
1431
  end
1485
- ::Brick.set_db_schema(params || api_params)
1432
+ _schema, @_is_show_schema_list = ::Brick.set_db_schema(params || api_params)
1486
1433
 
1487
1434
  if is_openapi
1488
1435
  json = { 'openapi': '3.0.1', 'info': { 'title': Rswag::Ui.config.config_object[:urls].last&.fetch(:name, 'API documentation'), 'version': ::Brick.config.api_version },
@@ -1584,7 +1531,6 @@ class Object
1584
1531
  end
1585
1532
 
1586
1533
  unless is_openapi
1587
- ::Brick.set_db_schema
1588
1534
  _, order_by_txt = model._brick_calculate_ordering(default_ordering(table_name, pk)) if pk
1589
1535
  code << " def index\n"
1590
1536
  code << " @#{table_name.pluralize} = #{model.name}#{pk&.present? ? ".order(#{order_by_txt.join(', ')})" : '.all'}\n"
@@ -1597,7 +1543,7 @@ class Object
1597
1543
  code << " #{find_by_name = "find_#{singular_table_name}"}\n"
1598
1544
  code << " end\n"
1599
1545
  self.define_method :show do
1600
- ::Brick.set_db_schema(params)
1546
+ _schema, @_is_show_schema_list = ::Brick.set_db_schema(params)
1601
1547
  instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1602
1548
  end
1603
1549
  end
@@ -1608,7 +1554,7 @@ class Object
1608
1554
  code << " @#{singular_table_name} = #{model.name}.new\n"
1609
1555
  code << " end\n"
1610
1556
  self.define_method :new do
1611
- ::Brick.set_db_schema(params)
1557
+ _schema, @_is_show_schema_list = ::Brick.set_db_schema(params)
1612
1558
  instance_variable_set("@#{singular_table_name}".to_sym, model.new)
1613
1559
  end
1614
1560
 
@@ -1647,7 +1593,7 @@ class Object
1647
1593
  code << " #{find_by_name}\n"
1648
1594
  code << " end\n"
1649
1595
  self.define_method :edit do
1650
- ::Brick.set_db_schema(params)
1596
+ _schema, @_is_show_schema_list = ::Brick.set_db_schema(params)
1651
1597
  instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1652
1598
  end
1653
1599
 
@@ -1876,13 +1822,22 @@ end.class_exec do
1876
1822
  s[row.first] = { dt: row.last } unless ['information_schema', 'pg_catalog', 'pg_toast', 'heroku_ext',
1877
1823
  'INFORMATION_SCHEMA', 'sys'].include?(row.first)
1878
1824
  end
1879
- if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
1880
- (sta = multitenancy[:schema_to_analyse]) != 'public') &&
1881
- ::Brick.db_schemas.key?(sta)
1882
- # Take note of the current schema so we can go back to it at the end of all this
1883
- orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1884
- ::Brick.default_schema = schema = sta
1885
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1825
+ if (possible_schemas = (multitenancy = ::Brick.config.schema_behavior&.[](:multitenant)) &&
1826
+ multitenancy&.[](:schema_to_analyse))
1827
+ possible_schemas = [possible_schemas] unless possible_schemas.is_a?(Array)
1828
+ if (possible_schema = possible_schemas.find { |ps| ::Brick.db_schemas.key?(ps) })
1829
+ ::Brick.default_schema = ::Brick.apartment_default_tenant
1830
+ schema = possible_schema
1831
+ orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1832
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1833
+ elsif Rails.env == 'test' # When testing, just find the most recently-created schema
1834
+ ::Brick.default_schema = schema = ::Brick.db_schemas.to_a.sort { |a, b| b.last[:dt] <=> a.last[:dt] }.first.first
1835
+ 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}."
1836
+ orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1837
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1838
+ else
1839
+ 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. ***"
1840
+ end
1886
1841
  end
1887
1842
  when 'Mysql2'
1888
1843
  ::Brick.default_schema = schema = ActiveRecord::Base.connection.current_database
@@ -1905,24 +1860,6 @@ end.class_exec do
1905
1860
 
1906
1861
  ::Brick.db_schemas ||= {}
1907
1862
 
1908
- if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1909
- if (possible_schemas = ::Brick.config.schema_behavior&.[](:multitenant)&.[](:schema_to_analyse))
1910
- possible_schemas = [possible_schemas] unless possible_schemas.is_a?(Array)
1911
- if (possible_schema = possible_schemas.find { |ps| ::Brick.db_schemas.key?(ps) })
1912
- ::Brick.default_schema = schema = possible_schema
1913
- orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1914
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1915
- elsif Rails.env == 'test' # When testing, just find the most recently-created schema
1916
- ::Brick.default_schema = schema = ::Brick.db_schemas.to_a.sort { |a, b| b.last[:dt] <=> a.last[:dt] }.first.first
1917
- 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}."
1918
- orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1919
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1920
- else
1921
- 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. ***"
1922
- end
1923
- end
1924
- end
1925
-
1926
1863
  # %%% Retrieve internal ActiveRecord table names like this:
1927
1864
  # ActiveRecord::Base.internal_metadata_table_name, ActiveRecord::Base.schema_migrations_table_name
1928
1865
  # For if it's not SQLite -- so this is the Postgres and MySQL version
@@ -1935,7 +1872,7 @@ end.class_exec do
1935
1872
  # If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
1936
1873
  # is the default schema, usually 'public'.
1937
1874
  schema_name = if ::Brick.config.schema_behavior[:multitenant]
1938
- Apartment.default_schema if apartment_excluded&.include?(r['relation_name'].singularize.camelize)
1875
+ ::Brick.apartment_default_tenant if apartment_excluded&.include?(r['relation_name'].singularize.camelize)
1939
1876
  elsif ![schema, 'public'].include?(r['schema'])
1940
1877
  r['schema']
1941
1878
  end
@@ -2086,16 +2023,16 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2086
2023
  fk = fk.values unless fk.is_a?(Array)
2087
2024
  # Multitenancy makes things a little more general overall, except for non-tenanted tables
2088
2025
  if apartment_excluded&.include?(::Brick.namify(fk[1]).singularize.camelize)
2089
- fk[0] = Apartment.default_schema
2090
- elsif (is_postgres && (fk[0] == 'public' || (is_multitenant && fk[0] == schema))) ||
2026
+ fk[0] = ::Brick.apartment_default_tenant
2027
+ elsif (is_postgres && (fk[0] == 'public' || (multitenancy && fk[0] == schema))) ||
2091
2028
  (::Brick.is_oracle && fk[0] == schema) ||
2092
2029
  (is_mssql && fk[0] == 'dbo') ||
2093
2030
  (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[0]))
2094
2031
  fk[0] = nil
2095
2032
  end
2096
2033
  if apartment_excluded&.include?(fk[4].singularize.camelize)
2097
- fk[3] = Apartment.default_schema
2098
- elsif (is_postgres && (fk[3] == 'public' || (is_multitenant && fk[3] == schema))) ||
2034
+ fk[3] = ::Brick.apartment_default_tenant
2035
+ elsif (is_postgres && (fk[3] == 'public' || (multitenancy && fk[3] == schema))) ||
2099
2036
  (::Brick.is_oracle && fk[3] == schema) ||
2100
2037
  (is_mssql && fk[3] == 'dbo') ||
2101
2038
  (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[3]))
@@ -2113,7 +2050,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2113
2050
  relations.each do |k, v|
2114
2051
  rel_name = k.split('.').map { |rel_part| ::Brick.namify(rel_part, :underscore) }
2115
2052
  schema_names = rel_name[0..-2]
2116
- schema_names.shift if ::Brick.apartment_multitenant && schema_names.first == Apartment.default_schema
2053
+ schema_names.shift if ::Brick.apartment_multitenant && schema_names.first == ::Brick.apartment_default_tenant
2117
2054
  v[:schema] = schema_names.join('.') unless schema_names.empty?
2118
2055
  # %%% If more than one schema has the same table name, will need to add a schema name prefix to have uniqueness
2119
2056
  v[:resource] = rel_name.last
@@ -2124,7 +2061,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2124
2061
  end
2125
2062
  ::Brick.load_additional_references if initializer_loaded
2126
2063
 
2127
- if orig_schema && (orig_schema = (orig_schema - ['pg_catalog', 'heroku_ext']).first)
2064
+ if orig_schema && (orig_schema = (orig_schema - ['pg_catalog', 'pg_toast', 'heroku_ext']).first)
2128
2065
  puts "Now switching back to \"#{orig_schema}\" schema."
2129
2066
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", orig_schema)
2130
2067
  end
@@ -2162,7 +2099,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2162
2099
  AND kcu.column_name = c.column_name#{"
2163
2100
  -- AND kcu.position_in_unique_constraint IS NULL" unless is_mssql}
2164
2101
  WHERE t.table_schema #{is_postgres || is_mssql ?
2165
- "NOT IN ('information_schema', 'pg_catalog',
2102
+ "NOT IN ('information_schema', 'pg_catalog', 'pg_toast', 'heroku_ext',
2166
2103
  'INFORMATION_SCHEMA', 'sys')"
2167
2104
  :
2168
2105
  "= '#{ActiveRecord::Base.connection.current_database.tr("'", "''")}'"}#{"
@@ -2261,7 +2198,7 @@ module Brick
2261
2198
  for_tbl = fk[1]
2262
2199
  fk_namified = ::Brick.namify(fk[1])
2263
2200
  apartment = Object.const_defined?('Apartment') && Apartment
2264
- fk[0] = Apartment.default_schema if apartment && apartment.excluded_models.include?(fk_namified.singularize.camelize)
2201
+ fk[0] = ::Brick.apartment_default_tenant if apartment && apartment.excluded_models.include?(fk_namified.singularize.camelize)
2265
2202
  fk[1] = "#{fk[0]}.#{fk[1]}" if fk[0] # && fk[0] != ::Brick.default_schema
2266
2203
  bts = (relation = relations.fetch(fk[1], nil))&.fetch(:fks) { relation[:fks] = {} }
2267
2204
 
@@ -2277,7 +2214,7 @@ module Brick
2277
2214
  # If Apartment gem lists the primary table as being associated with a non-tenanted model
2278
2215
  # then use 'public' schema for the primary table
2279
2216
  if apartment && apartment&.excluded_models.include?(fk[4].singularize.camelize)
2280
- fk[3] = Apartment.default_schema
2217
+ fk[3] = ::Brick.apartment_default_tenant
2281
2218
  true
2282
2219
  end
2283
2220
  else
@@ -2352,7 +2289,7 @@ module Brick
2352
2289
  end
2353
2290
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
2354
2291
  else
2355
- inv_tbl = if ::Brick.config.schema_behavior[:multitenant] && apartment && fk[0] == Apartment.default_schema
2292
+ inv_tbl = if ::Brick.config.schema_behavior[:multitenant] && apartment && fk[0] == ::Brick.apartment_default_tenant
2356
2293
  for_tbl
2357
2294
  else
2358
2295
  fk[1]
@@ -2411,7 +2348,7 @@ module Brick
2411
2348
  end
2412
2349
  ::Brick.relations.keys.map do |v|
2413
2350
  tbl_parts = v.split('.')
2414
- tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == Apartment.default_schema
2351
+ tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
2415
2352
  res = tbl_parts.join('.')
2416
2353
  [v, (model = models[res])&.last&.table_name, migrations&.fetch(res, nil), model&.first]
2417
2354
  end
@@ -2441,14 +2378,14 @@ module Brick
2441
2378
 
2442
2379
  # Locate orphaned records
2443
2380
  def find_orphans(multi_schema)
2444
- is_default_schema = multi_schema&.==(Apartment.default_schema)
2381
+ is_default_schema = multi_schema&.==(::Brick.apartment_default_tenant)
2445
2382
  relations.each_with_object([]) do |v, s|
2446
2383
  frn_tbl = v.first
2447
2384
  next if (relation = v.last).key?(:isView) || config.exclude_tables.include?(frn_tbl) ||
2448
2385
  !(for_pk = (relation[:pkey].values.first&.first))
2449
2386
 
2450
2387
  is_default_frn_schema = !is_default_schema && multi_schema &&
2451
- ((frn_parts = frn_tbl.split('.')).length > 1 && frn_parts.first)&.==(Apartment.default_schema)
2388
+ ((frn_parts = frn_tbl.split('.')).length > 1 && frn_parts.first)&.==(::Brick.apartment_default_tenant)
2452
2389
  relation[:fks].select { |_k, assoc| assoc[:is_bt] }.each do |_k, bt|
2453
2390
  begin
2454
2391
  if bt.key?(:polymorphic)
@@ -2464,7 +2401,7 @@ module Brick
2464
2401
  # Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
2465
2402
  # are both in the "public" schema
2466
2403
  next if is_default_frn_schema &&
2467
- ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(Apartment.default_schema)
2404
+ ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(::Brick.apartment_default_tenant)
2468
2405
 
2469
2406
  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
2470
2407
  FROM #{frn_tbl} AS frn
@@ -2483,7 +2420,7 @@ module Brick
2483
2420
  # are both in the "public" schema
2484
2421
  pri_tbl = bt.key?(:inverse_table) && bt[:inverse_table]
2485
2422
  next if is_default_frn_schema &&
2486
- ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(Apartment.default_schema)
2423
+ ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(::Brick.apartment_default_tenant)
2487
2424
 
2488
2425
  pri_pk = relations[pri_tbl].fetch(:pkey, nil)&.values&.first&.first ||
2489
2426
  _class_pk(pri_tbl, multi_schema)
@@ -192,13 +192,13 @@ module Brick
192
192
  end
193
193
  end
194
194
 
195
- apartment_default_schema = ::Brick.apartment_multitenant && Apartment.default_schema
196
- schema_options = if ::Brick.apartment_multitenant &&
197
- (cur_schema = Apartment::Tenant.current) != apartment_default_schema
198
- "<option selected value=\"#{cur_schema}\">#{cur_schema}</option>"
199
- else
200
- ::Brick.db_schemas.keys.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }
201
- end.html_safe
195
+ apartment_default_schema = ::Brick.apartment_multitenant && ::Brick.apartment_default_tenant
196
+ if ::Brick.apartment_multitenant && ::Brick.db_schemas.length > 1
197
+ schema_options = +'<select id="schema"><% if @_is_show_schema_list %>'
198
+ ::Brick.db_schemas.keys.each { |v| schema_options << "\n <option value=\"#{v}\">#{v}</option>" }
199
+ schema_options << "\n<% else %><option selected value=\"#{Apartment::Tenant.current}\">#{Apartment::Tenant.current}</option>\n"
200
+ schema_options << '<% end %></select>'
201
+ end
202
202
  # %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
203
203
  # environment or whatever, then get either the controllers or routes list instead
204
204
  prefix = "#{::Brick.config.path_prefix}/" if ::Brick.config.path_prefix
@@ -466,6 +466,22 @@ var #{table_name}HtColumns;
466
466
  // This PageTransitionEvent fires when the page first loads, as well as after any other history
467
467
  // transition such as when using the browser's Back and Forward buttons.
468
468
  window.addEventListener(\"pageshow\", function() {
469
+ if (tblSelect) { // Always present
470
+ var i = #{::Brick.config.path_prefix ? '0' : 'schemaSelect ? 1 : 0'},
471
+ changeoutList = changeout(location.href);
472
+ for (; i < changeoutList.length; ++i) {
473
+ tblSelect.value = changeoutList[i];
474
+ if (tblSelect.value !== \"\") break;
475
+ }
476
+
477
+ tblSelect.addEventListener(\"change\", function () {
478
+ var lhr = changeout(location.href, null, this.value);
479
+ if (brickSchema)
480
+ lhr = changeout(lhr, \"_brick_schema\", schemaSelect.value);
481
+ location.href = lhr;
482
+ });
483
+ }
484
+
469
485
  if (schemaSelect && schemaSelect.options.length > 1) { // First drop-down is only present if multitenant
470
486
  brickSchema = changeout(location.href, \"_brick_schema\");
471
487
  if (brickSchema) {
@@ -478,6 +494,7 @@ window.addEventListener(\"pageshow\", function() {
478
494
  location.href = changeout(location.href, \"_brick_schema\", this.value, tblSelect.value);
479
495
  });
480
496
  }
497
+
481
498
  [... document.getElementsByTagName(\"FORM\")].forEach(function (form) {
482
499
  if (brickSchema)
483
500
  form.action = changeout(form.action, \"_brick_schema\", brickSchema);
@@ -489,22 +506,6 @@ window.addEventListener(\"pageshow\", function() {
489
506
  return true;
490
507
  });
491
508
  });
492
-
493
- if (tblSelect) { // Always present
494
- var i = #{::Brick.config.path_prefix ? '0' : 'schemaSelect ? 1 : 0'},
495
- changeoutList = changeout(location.href);
496
- for (; i < changeoutList.length; ++i) {
497
- tblSelect.value = changeoutList[i];
498
- if (tblSelect.value !== \"\") break;
499
- }
500
-
501
- tblSelect.addEventListener(\"change\", function () {
502
- var lhr = changeout(location.href, null, this.value);
503
- if (brickSchema)
504
- lhr = changeout(lhr, \"_brick_schema\", schemaSelect.value);
505
- location.href = lhr;
506
- });
507
- }
508
509
  });
509
510
 
510
511
  // Add \"Are you sure?\" behaviour to any data-confirm buttons out there
@@ -834,7 +835,7 @@ erDiagram
834
835
  </head>
835
836
  <body>
836
837
  <p style=\"color: green\"><%= notice %></p>#{"
837
- <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
838
+ #{schema_options}" if schema_options}
838
839
  <select id=\"tbl\">#{table_options}</select>
839
840
  <table id=\"resourceName\"><tr>
840
841
  <td><h1>#{model_name}</h1></td>
@@ -954,10 +955,12 @@ erDiagram
954
955
  poly_id = #{obj_name}.send(\"#\{bt.first}_id\")
955
956
  %><%= link_to(\"#\{bt_class} ##\{poly_id}\", send(\"#\{base_class_underscored}_path\".to_sym, poly_id)) if poly_id %><%
956
957
  else
957
- # binding.pry if @_brick_bt_descrip[bt.first][bt[1].first.first].nil?
958
958
  bt_class = bt[1].first.first
959
959
  descrips = @_brick_bt_descrip[bt.first][bt_class]
960
- bt_id_col = if descrips.length == 1
960
+ bt_id_col = if descrips.nil?
961
+ puts \"Caught it in the act for #{obj_name} / #\{col_name}!\"
962
+ # binding.pry
963
+ elsif descrips.length == 1
961
964
  [#{obj_name}.class.reflect_on_association(bt.first)&.foreign_key]
962
965
  else
963
966
  descrips.last
@@ -974,16 +977,16 @@ erDiagram
974
977
  if hms_col.length == 1 %>
975
978
  <%= hms_col.first %>
976
979
  <% else
977
- klass = (col = cols[col_name])[1]
978
- txt = if col[2] == 'HO'
979
- descrips = @_brick_bt_descrip[col_name.to_sym][klass]
980
- ho_txt = klass.brick_descrip(#{obj_name}, descrips[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, (ho_id_col = descrips.last))
981
- ho_id = ho_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) }
982
- ho_id&.first ? link_to(ho_txt, send(\"#\{klass.base_class._brick_index(:singular)}_path\".to_sym, ho_id)) : ho_txt
983
- else
984
- \"#\{hms_col[1] || 'View'} #\{hms_col.first}\"
985
- end %>
986
- <%= link_to txt, send(\"#\{klass._brick_index}_path\".to_sym, hms_col[2]) unless hms_col[1]&.zero? %>
980
+ %><%= klass = (col = cols[col_name])[1]
981
+ if col[2] == 'HO'
982
+ descrips = @_brick_bt_descrip[col_name.to_sym][klass]
983
+ if (ho_id = (ho_id_col = descrips.last).map { |id_col| #{obj_name}.send(id_col.to_sym) })&.first
984
+ ho_txt = klass.brick_descrip(#{obj_name}, descrips[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, ho_id_col)
985
+ link_to(ho_txt, send(\"#\{klass.base_class._brick_index(:singular)}_path\".to_sym, ho_id))
986
+ end
987
+ elsif hms_col[1]&.positive?
988
+ link_to \"#\{hms_col[1] || 'View'} #\{hms_col.first}\", send(\"#\{klass._brick_index}_path\".to_sym, hms_col[2])
989
+ end %>
987
990
  <% end
988
991
  elsif (col = cols[col_name])
989
992
  col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
@@ -1019,7 +1022,7 @@ erDiagram
1019
1022
  # Easily could be multiple files involved (STI for instance)
1020
1023
  +"#{css}
1021
1024
  <p style=\"color: green\"><%= notice %></p>#{"
1022
- <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
1025
+ #{schema_options}" if schema_options}
1023
1026
  <select id=\"tbl\">#{table_options}</select>
1024
1027
  <h1>Status</h1>
1025
1028
  <table id=\"status\" class=\"shadow\"><thead><tr>
@@ -1068,7 +1071,7 @@ erDiagram
1068
1071
  if is_orphans
1069
1072
  +"#{css}
1070
1073
  <p style=\"color: green\"><%= notice %></p>#{"
1071
- <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
1074
+ #{schema_options}" if schema_options}
1072
1075
  <select id=\"tbl\">#{table_options}</select>
1073
1076
  <h1>Orphans<%= \" for #\{}\" if false %></h1>
1074
1077
  <% @orphans.each do |o|
@@ -1099,7 +1102,7 @@ erDiagram
1099
1102
  </svg>
1100
1103
 
1101
1104
  <p style=\"color: green\"><%= notice %></p>#{"
1102
- <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
1105
+ #{schema_options}" if schema_options}
1103
1106
  <select id=\"tbl\">#{table_options}</select>
1104
1107
  <h1><%= page_title %></h1><%
1105
1108
  if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(:description, nil)) %><%=
@@ -1411,9 +1414,26 @@ document.querySelectorAll(\"input, select\").forEach(function (inp) {
1411
1414
  # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
1412
1415
  keys = options.has_key?(:locals) ? options[:locals].keys : []
1413
1416
  handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
1414
- ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
1417
+ ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys).tap do |t|
1418
+ t.instance_variable_set(:@is_brick, true)
1419
+ end
1415
1420
  end
1416
- end
1421
+ end # LookupContext
1422
+
1423
+ # For any auto-generated template, if multitenancy is active via some flavour of an Apartment gem, switch back to the default tenant.
1424
+ # (Underlying reason -- ros-apartment can hold on to a selected tenant between requests when there is no elevator middleware.)
1425
+ ActionView::TemplateRenderer.class_exec do
1426
+ private
1427
+
1428
+ alias _brick_render_template render_template
1429
+ def render_template(view, template, *args)
1430
+ result = _brick_render_template(view, template, *args)
1431
+ if template.instance_variable_get(:@is_brick)
1432
+ Apartment::Tenant.switch!(::Brick.apartment_default_tenant) if ::Brick.apartment_multitenant
1433
+ end
1434
+ result
1435
+ end
1436
+ end # TemplateRenderer
1417
1437
  end
1418
1438
 
1419
1439
  if ::Brick.enable_routes?
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 91
8
+ TINY = 93
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
@@ -136,17 +136,19 @@ module Brick
136
136
  attr_accessor :default_schema, :db_schemas, :routes_done, :is_oracle, :is_eager_loading, :auto_models
137
137
 
138
138
  def set_db_schema(params = nil)
139
- schema = (params ? params['_brick_schema'] : ::Brick.default_schema)
140
- chosen = if schema && ::Brick.db_schemas&.key?(schema)
141
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
142
- schema
143
- elsif ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
144
- # Just return the current schema
145
- orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
146
- # ::Brick.apartment_multitenant && tbl_parts.first == Apartment.default_schema
147
- (orig_schema - ['pg_catalog']).first
148
- end
149
- chosen == ::Brick.default_schema ? nil : chosen
139
+ # If Apartment::Tenant.current is not still the default (usually 'public') then an elevator has brought us into
140
+ # a different tenant. If so then don't allow schema navigation.
141
+ chosen = if (is_show_schema_list = (apartment_multitenant && Apartment::Tenant.current == ::Brick.default_schema)) &&
142
+ (schema = (params ? params['_brick_schema'] : ::Brick.default_schema)) &&
143
+ ::Brick.db_schemas&.key?(schema)
144
+ Apartment::Tenant.switch!(schema)
145
+ schema
146
+ elsif ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
147
+ # Just return the current schema
148
+ current_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
149
+ (current_schema - ['pg_catalog', 'pg_toast', 'heroku_ext']).first
150
+ end
151
+ [chosen == ::Brick.default_schema ? nil : chosen, is_show_schema_list]
150
152
  end
151
153
 
152
154
  # All tables and views (what Postgres calls "relations" including column and foreign key info)
@@ -162,6 +164,10 @@ module Brick
162
164
  @apartment_multitenant
163
165
  end
164
166
 
167
+ def apartment_default_tenant
168
+ Apartment.default_tenant || 'public'
169
+ end
170
+
165
171
  # If multitenancy is enabled, a list of non-tenanted "global" models
166
172
  def non_tenanted_models
167
173
  @pending_models ||= {}
@@ -1052,21 +1058,43 @@ ActiveSupport.on_load(:active_record) do
1052
1058
  end
1053
1059
  end
1054
1060
 
1055
- # First part of arel_table_type stuff:
1056
- # ------------------------------------
1057
- # (more found below)
1058
- # was: ActiveRecord.version >= ::Gem::Version.new('3.2') &&
1059
- if ActiveRecord.version < ::Gem::Version.new('5.0')
1060
- # Used by Util#_arel_table_type
1061
- module ActiveRecord
1062
- class Base
1063
- def self.arel_table
1064
- @arel_table ||= Arel::Table.new(table_name, arel_engine).tap do |x|
1065
- x.instance_variable_set(:@_arel_table_type, self)
1066
- end
1061
+ class ActiveRecord::Associations::JoinDependency
1062
+ if JoinBase.instance_method(:initialize).arity == 2 # Older ActiveRecord 4.x?
1063
+ def initialize(base, associations, joins)
1064
+ @alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(base.connection, joins)
1065
+ @alias_tracker.aliased_table_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1
1066
+ tree = self.class.make_tree associations
1067
+
1068
+ # Provide a way to find the original relation that this tree is being used for
1069
+ # (so that we can maintain a list of links for all tables used in JOINs)
1070
+ if (relation = associations.instance_variable_get(:@relation))
1071
+ tree.instance_variable_set(:@relation, relation)
1072
+ end
1073
+
1074
+ @join_root = JoinBase.new base, build(tree, base)
1075
+ @join_root.children.each { |child| construct_tables! @join_root, child }
1076
+ end
1077
+
1078
+ else # For ActiveRecord 5.0 - 7.1
1079
+
1080
+ def initialize(base, table, associations, join_type = nil)
1081
+ tree = self.class.make_tree associations
1082
+
1083
+ # Provide a way to find the original relation that this tree is being used for
1084
+ # (so that we can maintain a list of links for all tables used in JOINs)
1085
+ if (relation = associations.instance_variable_get(:@relation))
1086
+ tree.instance_variable_set(:@relation, relation)
1067
1087
  end
1088
+
1089
+ @join_root = JoinBase.new(base, table, build(tree, base))
1090
+ @join_type = join_type if join_type
1068
1091
  end
1092
+ end
1093
+ end
1069
1094
 
1095
+ # was: ActiveRecord.version >= ::Gem::Version.new('3.2') &&
1096
+ if ActiveRecord.version < ::Gem::Version.new('5.0')
1097
+ module ActiveRecord
1070
1098
  # Final pieces for left_outer_joins support, which was derived from this commit:
1071
1099
  # https://github.com/rails/rails/commit/3f46ef1ddab87482b730a3f53987e04308783d8b
1072
1100
  module Associations
@@ -1151,13 +1179,9 @@ if is_postgres && ActiveRecord.version < ::Gem::Version.new('5.0') # Was: && Ob
1151
1179
  PGError = PG::Error
1152
1180
  end
1153
1181
 
1154
- # More arel_table_type stuff:
1155
- # ---------------------------
1156
1182
  if ActiveRecord.version < ::Gem::Version.new('5.2')
1157
1183
  # Specifically for AR 3.1 and 3.2 to avoid: "undefined method `delegate' for ActiveRecord::Reflection::ThroughReflection:Class"
1158
1184
  require 'active_support/core_ext/module/delegation' if ActiveRecord.version < ::Gem::Version.new('4.0')
1159
- # Used by Util#_arel_table_type
1160
- # rubocop:disable Style/CommentedKeyword
1161
1185
  module ActiveRecord
1162
1186
  module Reflection
1163
1187
  # AR < 4.0 doesn't know about join_table and derive_join_table
@@ -1175,59 +1199,108 @@ if ActiveRecord.version < ::Gem::Version.new('5.2')
1175
1199
  end
1176
1200
  end
1177
1201
  end
1202
+ end
1203
+ end
1178
1204
 
1179
- module Associations
1180
- # Specific to AR 4.2 - 5.1:
1181
- if Associations.const_defined?('JoinDependency') && JoinDependency.private_instance_methods.include?(:table_aliases_for)
1182
- class JoinDependency
1183
- private
1205
+ # The "brick_links" patch -- this finds how every AR chain of association names
1206
+ # relates back to an exact table correlation name chosen by AREL when the AST tree is
1207
+ # walked. For instance, from a Customer model there could be a join_tree such as
1208
+ # { orders: { line_items: :product} }, which would end up recording three entries, the
1209
+ # last of which for products would have a key of "orders.line_items.product" after
1210
+ # having gone through two HMs and one BT. AREL would have chosen a correlation name of
1211
+ # "products", being able to use the same name as the table name because it's the first
1212
+ # time that table is used in this query. But let's see what happens if each customer
1213
+ # also had a BT to a favourite product, referenced earlier in the join_tree like this:
1214
+ # [:favourite_product, orders: { line_items: :product}] -- then the second reference to
1215
+ # "products" would end up being called "products_line_items" in order to differentiate
1216
+ # it from the first reference, which would have already snagged the simpler name
1217
+ # "products". It's essential that The Brick can find accurate correlation names when
1218
+ # there are multiple JOINs to the same table.
1219
+ module ActiveRecord
1220
+ module QueryMethods
1221
+ private
1222
+
1223
+ if private_instance_methods.include?(:build_join_query)
1224
+ alias _brick_build_join_query build_join_query
1225
+ def build_join_query(manager, buckets, *args)
1226
+ # %%% Better way to bring relation into the mix
1227
+ if (aj = buckets.fetch(:association_join, nil))
1228
+ aj.instance_variable_set(:@relation, self)
1229
+ end
1184
1230
 
1185
- if ActiveRecord.version < ::Gem::Version.new('5.1') # 4.2 or 5.0
1186
- def table_aliases_for(parent, node)
1187
- node.reflection.chain.map do |reflection|
1188
- alias_tracker.aliased_table_for(
1189
- reflection.table_name,
1190
- table_alias_for(reflection, parent, reflection != node.reflection)
1191
- ).tap do |x|
1192
- # %%% Specific only to Rails 4.2 (and maybe 4.1?)
1193
- x = x.left if x.is_a?(Arel::Nodes::TableAlias)
1194
- y = reflection.chain.find { |c| c.table_name == x.name }
1195
- x.instance_variable_set(:@_arel_table_type, y.klass)
1196
- end
1197
- end
1231
+ _brick_build_join_query(manager, buckets, *args)
1232
+ end
1233
+
1234
+ else
1235
+
1236
+ alias _brick_select_association_list select_association_list
1237
+ def select_association_list(associations, stashed_joins = nil)
1238
+ result = _brick_select_association_list(associations, stashed_joins)
1239
+ result.instance_variable_set(:@relation, self)
1240
+ result
1241
+ end
1242
+ end
1243
+ end
1244
+
1245
+ # require 'active_record/associations/join_dependency'
1246
+ module Associations
1247
+ # For AR >= 4.2
1248
+ if self.const_defined?('JoinDependency')
1249
+ class JoinDependency
1250
+ private
1251
+
1252
+ # %%% Pretty much have to flat-out replace this guy (I think anyway)
1253
+ # Good with Rails 5.24 and 7 on this
1254
+ def build(associations, base_klass, root = nil, path = '')
1255
+ root ||= associations
1256
+ associations.map do |name, right|
1257
+ reflection = find_reflection base_klass, name
1258
+ reflection.check_validity!
1259
+ reflection.check_eager_loadable!
1260
+
1261
+ if reflection.polymorphic?
1262
+ raise EagerLoadPolymorphicError.new(reflection)
1198
1263
  end
1264
+
1265
+ # %%% The path
1266
+ link_path = path.blank? ? name.to_s : path + ".#{name}"
1267
+ ja = JoinAssociation.new(reflection, build(right, reflection.klass, root, link_path))
1268
+ ja.instance_variable_set(:@link_path, link_path) # Make note on the JoinAssociation of its AR path
1269
+ ja.instance_variable_set(:@assocs, root)
1270
+ ja
1199
1271
  end
1200
1272
  end
1201
- elsif Associations.const_defined?('JoinHelper') && JoinHelper.private_instance_methods.include?(:construct_tables)
1202
- module JoinHelper
1203
- private
1204
1273
 
1205
- # AR > 3.0 and < 4.2 (%%% maybe only < 4.1?) uses construct_tables like this:
1206
- def construct_tables
1207
- tables = []
1208
- chain.each do |reflection|
1209
- tables << alias_tracker.aliased_table_for(
1210
- table_name_for(reflection),
1211
- table_alias_for(reflection, reflection != self.reflection)
1212
- ).tap do |x|
1213
- x = x.left if x.is_a?(Arel::Nodes::TableAlias)
1214
- x.instance_variable_set(:@_arel_table_type, reflection.chain.find { |c| c.table_name == x.name }.klass)
1215
- end
1216
-
1217
- next unless reflection.source_macro == :has_and_belongs_to_many
1274
+ if JoinDependency.private_instance_methods.include?(:table_aliases_for)
1275
+ # No matter if it's older or newer Rails, now extend so that we can associate AR links to table_alias names
1276
+ alias _brick_table_aliases_for table_aliases_for
1277
+ def table_aliases_for(parent, node)
1278
+ result = _brick_table_aliases_for(parent, node)
1218
1279
 
1219
- tables << alias_tracker.aliased_table_for(
1220
- (reflection.source_reflection || reflection).join_table,
1221
- table_alias_for(reflection, true)
1222
- )
1280
+ # Capture the table alias name that was chosen
1281
+ link_path = node.instance_variable_get(:@link_path)
1282
+ if (relation = node.instance_variable_get(:@assocs)&.instance_variable_get(:@relation))
1283
+ relation.brick_links[link_path] = result.first.table_alias || result.first.table_name
1223
1284
  end
1224
- tables
1285
+
1286
+ result
1287
+ end
1288
+ else # Same idea but for Rails 7
1289
+ alias _brick_make_constraints make_constraints
1290
+ def make_constraints(parent, child, join_type)
1291
+ result = _brick_make_constraints(parent, child, join_type)
1292
+
1293
+ # Capture the table alias name that was chosen
1294
+ link_path = child.instance_variable_get(:@link_path)
1295
+ relation = child.instance_variable_get(:@assocs)&.instance_variable_get(:@relation)
1296
+ # binding.pry if relation
1297
+ relation.brick_links[link_path] = result.first.left.table_alias || result.first.left.table_name
1298
+ result
1225
1299
  end
1226
1300
  end
1227
1301
  end
1228
1302
  end
1229
- end # module ActiveRecord
1230
- # rubocop:enable Style/CommentedKeyword
1303
+ end
1231
1304
  end
1232
1305
 
1233
1306
  require 'brick/extensions'
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.91
4
+ version: 1.0.93
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-11-09 00:00:00.000000000 Z
11
+ date: 2022-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -164,20 +164,6 @@ dependencies:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
166
  version: 1.42.0
167
- - !ruby/object:Gem::Dependency
168
- name: mysql2
169
- requirement: !ruby/object:Gem::Requirement
170
- requirements:
171
- - - "~>"
172
- - !ruby/object:Gem::Version
173
- version: '0.5'
174
- type: :development
175
- prerelease: false
176
- version_requirements: !ruby/object:Gem::Requirement
177
- requirements:
178
- - - "~>"
179
- - !ruby/object:Gem::Version
180
- version: '0.5'
181
167
  - !ruby/object:Gem::Dependency
182
168
  name: pg
183
169
  requirement: !ruby/object:Gem::Requirement