brick 1.0.91 → 1.0.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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