brick 1.0.92 → 1.0.94

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: 6bb6854dd2295476ae00996f23e801587869463aa7114d9240c0e97b1cce25b1
4
- data.tar.gz: f174948106c6a06be049f8285fb3358813d09b9970ce484554ad3c86bc3cb8fb
3
+ metadata.gz: 4b4de74edca4a5dd2c7d57d23dda2aa4cbc966402db4f2d0490dfeb741224dd5
4
+ data.tar.gz: fe905ed82bf8ae8f249e5ce3fe1646cb6366caf3343086ea8ba597e51525f521
5
5
  SHA512:
6
- metadata.gz: 9384ef5db0a03fcc8fda6a3ceafc763a468f88033420d785814d657303027593c7fef64770087975d73d4041f577864c1d934629cb50083fb93d2591f719f136
7
- data.tar.gz: e2b3e21982b9a6d132a3d9d05041759e06a3ac3ffbfdf2ea3675ee0140150f2d33b6e60cbeb0033a7f7469bba227f198a6cc6a0ef34c229ba7a1132cd1175b0d
6
+ metadata.gz: 45d2dda76345ab043c4e62d92b5e12a466ffec892e1ce63b540bd6427fcecfa777739732754d554f78b5803599fca1390fa2d38cb8f0acd9b0ffd8e5c6dd34aa
7
+ data.tar.gz: 65b73535a5874fe5b3c46fec455fe62857259c7d56b68241a729f2c928a47e0fdcc71988a3b7ead506de717fae71932007855efaf18dd8866b920da2c6d5c779
@@ -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?
@@ -260,17 +245,20 @@ module ActiveRecord
260
245
  end
261
246
 
262
247
  def self.bt_link(assoc_name)
263
- assoc_name = CGI.escapeHTML(assoc_name.to_s)
248
+ assoc_html_name = unless (assoc_name = assoc_name.to_s).camelize == name
249
+ CGI.escapeHTML(assoc_name)
250
+ end
264
251
  model_path = ::Rails.application.routes.url_helpers.send("#{_brick_index}_path".to_sym)
252
+ model_path << "?#{self.inheritance_column}=#{self.name}" if self != base_class
265
253
  av_class = Class.new.extend(ActionView::Helpers::UrlHelper)
266
254
  av_class.extend(ActionView::Helpers::TagHelper) if ActionView.version < ::Gem::Version.new('7')
267
- link = av_class.link_to(name, model_path)
268
- table_name == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
255
+ link = av_class.link_to(assoc_html_name ? name : assoc_name, model_path)
256
+ assoc_html_name ? "#{assoc_name}-#{link}".html_safe : link
269
257
  end
270
258
 
271
259
  def self._brick_index(mode = nil)
272
260
  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
261
+ tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
274
262
  tbl_parts.unshift(::Brick.config.path_prefix) if ::Brick.config.path_prefix
275
263
  index = tbl_parts.map(&:underscore).join('_')
276
264
  # Rails applies an _index suffix to that route when the resource name is singular
@@ -407,86 +395,12 @@ module ActiveRecord
407
395
  end
408
396
 
409
397
  class Relation
410
- attr_reader :_arel_applied_aliases
411
-
412
- # Links from ActiveRecord association pathing names over to real
413
- # table correlation names built from AREL aliasing
398
+ # Links from ActiveRecord association pathing names over to real table correlation names
399
+ # that get chosen when the AREL AST tree is walked.
414
400
  def brick_links
415
401
  @brick_links ||= {}
416
402
  end
417
403
 
418
- # CLASS STUFF
419
- def _recurse_arel(piece, prefix = '')
420
- names = []
421
- # Our JOINs mashup of nested arrays and hashes
422
- # binding.pry if defined?(@arel)
423
- case piece
424
- when Array
425
- names += piece.inject([]) { |s, v| s + _recurse_arel(v, prefix) }
426
- when Hash
427
- names += piece.inject([]) do |s, v|
428
- new_prefix = "#{prefix}#{v.first}_"
429
- s << [v.last.shift, new_prefix]
430
- s + _recurse_arel(v.last, new_prefix)
431
- end
432
-
433
- # ActiveRecord AREL objects
434
- when Arel::Nodes::Join # INNER or OUTER JOIN
435
- # rubocop:disable Style/IdenticalConditionalBranches
436
- if piece.right.is_a?(Arel::Table) # Came in from AR < 3.2?
437
- # Arel 2.x and older is a little curious because these JOINs work "back to front".
438
- # The left side here is either another earlier JOIN, or at the end of the whole tree, it is
439
- # the first table.
440
- names += _recurse_arel(piece.left)
441
- # The right side here at the top is the very last table, and anywhere else down the tree it is
442
- # the later "JOIN" table of this pair. (The table that comes after all the rest of the JOINs
443
- # from the left side.)
444
- names << [piece.right._arel_table_type, (piece.right.table_alias || piece.right.name)]
445
- else # "Normal" setup, fed from a JoinSource which has an array of JOINs
446
- # The left side is the "JOIN" table
447
- names += _recurse_arel(table = piece.left)
448
- # The expression on the right side is the "ON" clause
449
- # on = piece.right.expr
450
- # # Find the table which is not ourselves, and thus must be the "path" that led us here
451
- # parent = piece.left == on.left.relation ? on.right.relation : on.left.relation
452
- # binding.pry if piece.left.is_a?(Arel::Nodes::TableAlias)
453
- if table.is_a?(Arel::Nodes::TableAlias)
454
- @_arel_applied_aliases << (alias_name = table.right)
455
- table = table.left
456
- end
457
- end
458
- # rubocop:enable Style/IdenticalConditionalBranches
459
- when Arel::Table # Table
460
- names << [piece._arel_table_type, (piece.table_alias || piece.name)]
461
- when Arel::Nodes::TableAlias # Alias
462
- # Can get the real table name from: self._recurse_arel(piece.left)
463
- names << [piece.left._arel_table_type, piece.right.to_s] # This is simply a string; the alias name itself
464
- when Arel::Nodes::JoinSource # Leaving this until the end because AR < 3.2 doesn't know at all about JoinSource!
465
- # The left side is the "FROM" table
466
- names << (this_name = [piece.left._arel_table_type, (piece.left.table_alias || piece.left.name)])
467
- # The right side is an array of all JOINs
468
- piece.right.each { |join| names << _recurse_arel(join) }
469
- end
470
- names
471
- end
472
-
473
- # INSTANCE STUFF
474
- def _arel_alias_names
475
- @_arel_applied_aliases = []
476
- # %%% If with Rails 3.1 and older you get "NoMethodError: undefined method `eq' for nil:NilClass"
477
- # when trying to call relation.arel, then somewhere along the line while navigating a has_many
478
- # relationship it can't find the proper foreign key.
479
- core = arel.ast.cores.first
480
- # Accommodate AR < 3.2
481
- if core.froms.is_a?(Arel::Table)
482
- # All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource
483
- _recurse_arel(core.source)
484
- else
485
- # With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
486
- _recurse_arel(core.froms)
487
- end
488
- end
489
-
490
404
  def brick_select(params, selects = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
491
405
  is_add_bts = is_add_hms = true
492
406
 
@@ -502,13 +416,16 @@ module ActiveRecord
502
416
  params.each do |k, v|
503
417
  next if ['_brick_schema', '_brick_order', 'controller', 'action'].include?(k)
504
418
 
505
- case (ks = k.split('.')).length
419
+ if (where_col = (ks = k.split('.')).last)[-1] == '!'
420
+ where_col = where_col[0..-2]
421
+ end
422
+ case ks.length
506
423
  when 1
507
- next unless klass.column_names.any?(k) || klass._brick_get_fks.include?(k)
424
+ next unless klass.column_names.any?(where_col) || klass._brick_get_fks.include?(where_col)
508
425
  when 2
509
426
  assoc_name = ks.first.to_sym
510
427
  # Make sure it's a good association name and that the model has that column name
511
- next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
428
+ next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(where_col)
512
429
 
513
430
  join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
514
431
  is_distinct = true
@@ -546,8 +463,19 @@ module ActiveRecord
546
463
 
547
464
  if join_array.present?
548
465
  left_outer_joins!(join_array)
549
- # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
550
- (rel_dupe = dup)._arel_alias_names
466
+ # Touching AREL AST walks the JoinDependency tree, and in that process uses our
467
+ # "brick_links" patch to find how every AR chain of association names relates to exact
468
+ # table correlation names chosen by AREL. We use a duplicate relation object for this
469
+ # because an important side-effect of referencing the AST is that the @arel instance
470
+ # variable gets set, and this is a signal to ActiveRecord that a relation has now
471
+ # become immutable. (We aren't quite ready for our "real deal" relation object to be
472
+ # set in stone ... still need to add .select(), and possibly .where() and .order()
473
+ # things ... also if there are any HM counts then an OUTER JOIN for each of them out
474
+ # to a derived table to do that counting. All of these things need to know proper
475
+ # table correlation names, which will now become available in brick_links on the
476
+ # rel_dupe object.)
477
+ (rel_dupe = dup).arel.ast
478
+
551
479
  core_selects = selects.dup
552
480
  id_for_tables = Hash.new { |h, k| h[k] = [] }
553
481
  field_tbl_names = Hash.new { |h, k| h[k] = {} }
@@ -609,15 +537,15 @@ module ActiveRecord
609
537
  next unless (tbl_name = rel_dupe.brick_links[v.first.to_s]&.split('.')&.last)
610
538
 
611
539
  # If it's Oracle, quote any AREL aliases that had been applied
612
- tbl_name = "\"#{tbl_name}\"" if ::Brick.is_oracle && rel_dupe._arel_applied_aliases.include?(tbl_name)
540
+ tbl_name = "\"#{tbl_name}\"" if ::Brick.is_oracle && rel_dupe.brick_links.values.include?(tbl_name)
613
541
  field_tbl_name = nil
614
542
  v1.map { |x| [x[0..-2].map(&:to_s).join('.'), x.last] }.each_with_index do |sel_col, idx|
615
543
  field_tbl_name = rel_dupe.brick_links[sel_col.first].split('.').last
616
544
  # If it's Oracle, quote any AREL aliases that had been applied
617
- field_tbl_name = "\"#{field_tbl_name}\"" if ::Brick.is_oracle && rel_dupe._arel_applied_aliases.include?(field_tbl_name)
545
+ field_tbl_name = "\"#{field_tbl_name}\"" if ::Brick.is_oracle && rel_dupe.brick_links.values.include?(field_tbl_name)
618
546
 
619
547
  # Postgres can not use DISTINCT with any columns that are XML, so for any of those just convert to text
620
- is_xml = is_distinct && Brick.relations[field_tbl_name]&.[](:cols)&.[](sel_col.last)&.first&.start_with?('xml')
548
+ is_xml = is_distinct && Brick.relations[k1.table_name]&.[](:cols)&.[](sel_col.last)&.first&.start_with?('xml')
621
549
  # If it's not unique then also include the belongs_to association name before the column name
622
550
  if used_col_aliases.key?(col_alias = "br_fk_#{v.first}__#{sel_col.last}")
623
551
  col_alias = "br_fk_#{v.first}__#{v1[idx][-2..-1].map(&:to_s).join('__')}"
@@ -786,16 +714,23 @@ JOIN (SELECT #{hm_selects.map { |s| "#{'br_t0.' if from_clause}#{s}" }.join(', '
786
714
 
787
715
  unless wheres.empty?
788
716
  # Rewrite the wheres to reference table and correlation names built out by AREL
717
+ where_nots = {}
789
718
  wheres2 = wheres.each_with_object({}) do |v, s|
719
+ is_not = if v.first[-1] == '!'
720
+ v[0] = v[0][0..-2] # Take off ending ! from column name
721
+ end
790
722
  if (v_parts = v.first.split('.')).length == 1
791
- s[v.first] = v.last
723
+ (is_not ? where_nots : s)[v.first] = v.last
792
724
  else
793
725
  tbl_name = rel_dupe.brick_links[v_parts.first].split('.').last
794
- s["#{tbl_name}.#{v_parts.last}"] = v.last
726
+ (is_not ? where_nots : s)["#{tbl_name}.#{v_parts.last}"] = v.last
795
727
  end
796
728
  end
797
729
  if respond_to?(:where!)
798
- where!(wheres2)
730
+ where!(wheres2) if wheres2.present?
731
+ if where_nots.present?
732
+ self.where_clause += WhereClause.new(predicate_builder.build_from_hash(where_nots)).invert
733
+ end
799
734
  else # AR < 4.0
800
735
  self.where_values << build_where(wheres2)
801
736
  end
@@ -1091,7 +1026,8 @@ Module.class_exec do
1091
1026
  (table_name = singular_table_name.pluralize),
1092
1027
  ::Brick.is_oracle ? class_name.upcase : class_name,
1093
1028
  (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas&.include?(s) }&.camelize ||
1094
- (::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name))
1029
+ (::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name) ||
1030
+ (::Brick.config.table_name_prefixes&.values.include?(class_name) && class_name))
1095
1031
  return self.const_get(schema_name) if self.const_defined?(schema_name)
1096
1032
 
1097
1033
  # Build out a module for the schema if it's namespaced
@@ -1142,6 +1078,7 @@ class Object
1142
1078
  private
1143
1079
 
1144
1080
  def build_model(relations, base_module, base_name, class_name, inheritable_name = nil)
1081
+ tnp = ::Brick.config.table_name_prefixes&.find { |p| p.last == base_module.name }&.first
1145
1082
  if (base_model = ::Brick.config.sti_namespace_prefixes&.fetch("::#{base_module.name}::", nil)&.constantize) || # Are we part of an auto-STI namespace? ...
1146
1083
  base_module != Object # ... or otherwise already in some namespace?
1147
1084
  schema_name = [(singular_schema_name = base_name.underscore),
@@ -1163,11 +1100,11 @@ class Object
1163
1100
  table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
1164
1101
  base_model.table_name
1165
1102
  else
1166
- ActiveSupport::Inflector.pluralize(singular_table_name)
1103
+ "#{tnp}#{ActiveSupport::Inflector.pluralize(singular_table_name)}"
1167
1104
  end
1168
1105
  if ::Brick.apartment_multitenant &&
1169
1106
  Apartment.excluded_models.include?(table_name.singularize.camelize)
1170
- schema_name = Apartment.default_schema
1107
+ schema_name = ::Brick.apartment_default_tenant
1171
1108
  end
1172
1109
  # Maybe, just maybe there's a database table that will satisfy this need
1173
1110
  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) })
@@ -1178,7 +1115,7 @@ class Object
1178
1115
 
1179
1116
  def build_model_worker(schema_name, inheritable_name, model_name, singular_table_name, table_name, relations, matching)
1180
1117
  if ::Brick.apartment_multitenant &&
1181
- schema_name == Apartment.default_schema
1118
+ schema_name == ::Brick.apartment_default_tenant
1182
1119
  relation = relations["#{schema_name}.#{matching}"]
1183
1120
  end
1184
1121
  full_name = if relation || schema_name.blank?
@@ -1380,7 +1317,7 @@ class Object
1380
1317
  # If it's multitenant with something like: public.____ ...
1381
1318
  if (it_parts = inverse_table.split('.')).length > 1 &&
1382
1319
  ::Brick.apartment_multitenant &&
1383
- it_parts.first == Apartment.default_schema
1320
+ it_parts.first == ::Brick.apartment_default_tenant
1384
1321
  it_parts.shift # ... then ditch the generic schema name
1385
1322
  end
1386
1323
  inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[inverse_table], inverse, it_parts.join('_').singularize)
@@ -1477,7 +1414,7 @@ class Object
1477
1414
  instance_variable_set(:@resources, ::Brick.get_status_of_resources)
1478
1415
  end
1479
1416
  self.define_method :orphans do
1480
- instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params)))
1417
+ instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params).first))
1481
1418
  end
1482
1419
  return [new_controller_class, code + "end # BrickGem controller\n"]
1483
1420
  when 'BrickOpenapi'
@@ -1495,7 +1432,7 @@ class Object
1495
1432
  api_params = referrer_params&.to_h
1496
1433
  end
1497
1434
  end
1498
- ::Brick.set_db_schema(params || api_params)
1435
+ _schema, @_is_show_schema_list = ::Brick.set_db_schema(params || api_params)
1499
1436
 
1500
1437
  if is_openapi
1501
1438
  json = { 'openapi': '3.0.1', 'info': { 'title': Rswag::Ui.config.config_object[:urls].last&.fetch(:name, 'API documentation'), 'version': ::Brick.config.api_version },
@@ -1597,7 +1534,6 @@ class Object
1597
1534
  end
1598
1535
 
1599
1536
  unless is_openapi
1600
- ::Brick.set_db_schema
1601
1537
  _, order_by_txt = model._brick_calculate_ordering(default_ordering(table_name, pk)) if pk
1602
1538
  code << " def index\n"
1603
1539
  code << " @#{table_name.pluralize} = #{model.name}#{pk&.present? ? ".order(#{order_by_txt.join(', ')})" : '.all'}\n"
@@ -1610,7 +1546,7 @@ class Object
1610
1546
  code << " #{find_by_name = "find_#{singular_table_name}"}\n"
1611
1547
  code << " end\n"
1612
1548
  self.define_method :show do
1613
- ::Brick.set_db_schema(params)
1549
+ _schema, @_is_show_schema_list = ::Brick.set_db_schema(params)
1614
1550
  instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1615
1551
  end
1616
1552
  end
@@ -1621,7 +1557,7 @@ class Object
1621
1557
  code << " @#{singular_table_name} = #{model.name}.new\n"
1622
1558
  code << " end\n"
1623
1559
  self.define_method :new do
1624
- ::Brick.set_db_schema(params)
1560
+ _schema, @_is_show_schema_list = ::Brick.set_db_schema(params)
1625
1561
  instance_variable_set("@#{singular_table_name}".to_sym, model.new)
1626
1562
  end
1627
1563
 
@@ -1660,7 +1596,7 @@ class Object
1660
1596
  code << " #{find_by_name}\n"
1661
1597
  code << " end\n"
1662
1598
  self.define_method :edit do
1663
- ::Brick.set_db_schema(params)
1599
+ _schema, @_is_show_schema_list = ::Brick.set_db_schema(params)
1664
1600
  instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1665
1601
  end
1666
1602
 
@@ -1889,13 +1825,22 @@ end.class_exec do
1889
1825
  s[row.first] = { dt: row.last } unless ['information_schema', 'pg_catalog', 'pg_toast', 'heroku_ext',
1890
1826
  'INFORMATION_SCHEMA', 'sys'].include?(row.first)
1891
1827
  end
1892
- if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
1893
- (sta = multitenancy[:schema_to_analyse]) != 'public') &&
1894
- ::Brick.db_schemas.key?(sta)
1895
- # Take note of the current schema so we can go back to it at the end of all this
1896
- orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1897
- ::Brick.default_schema = schema = sta
1898
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1828
+ if (possible_schemas = (multitenancy = ::Brick.config.schema_behavior&.[](:multitenant)) &&
1829
+ multitenancy&.[](:schema_to_analyse))
1830
+ possible_schemas = [possible_schemas] unless possible_schemas.is_a?(Array)
1831
+ if (possible_schema = possible_schemas.find { |ps| ::Brick.db_schemas.key?(ps) })
1832
+ ::Brick.default_schema = ::Brick.apartment_default_tenant
1833
+ schema = possible_schema
1834
+ orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1835
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1836
+ elsif Rails.env == 'test' # When testing, just find the most recently-created schema
1837
+ ::Brick.default_schema = schema = ::Brick.db_schemas.to_a.sort { |a, b| b.last[:dt] <=> a.last[:dt] }.first.first
1838
+ 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}."
1839
+ orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1840
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1841
+ else
1842
+ 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. ***"
1843
+ end
1899
1844
  end
1900
1845
  when 'Mysql2'
1901
1846
  ::Brick.default_schema = schema = ActiveRecord::Base.connection.current_database
@@ -1918,24 +1863,6 @@ end.class_exec do
1918
1863
 
1919
1864
  ::Brick.db_schemas ||= {}
1920
1865
 
1921
- if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1922
- if (possible_schemas = ::Brick.config.schema_behavior&.[](:multitenant)&.[](:schema_to_analyse))
1923
- possible_schemas = [possible_schemas] unless possible_schemas.is_a?(Array)
1924
- if (possible_schema = possible_schemas.find { |ps| ::Brick.db_schemas.key?(ps) })
1925
- ::Brick.default_schema = schema = possible_schema
1926
- orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1927
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1928
- elsif Rails.env == 'test' # When testing, just find the most recently-created schema
1929
- ::Brick.default_schema = schema = ::Brick.db_schemas.to_a.sort { |a, b| b.last[:dt] <=> a.last[:dt] }.first.first
1930
- 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}."
1931
- orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
1932
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1933
- else
1934
- 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. ***"
1935
- end
1936
- end
1937
- end
1938
-
1939
1866
  # %%% Retrieve internal ActiveRecord table names like this:
1940
1867
  # ActiveRecord::Base.internal_metadata_table_name, ActiveRecord::Base.schema_migrations_table_name
1941
1868
  # For if it's not SQLite -- so this is the Postgres and MySQL version
@@ -1948,7 +1875,7 @@ end.class_exec do
1948
1875
  # If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
1949
1876
  # is the default schema, usually 'public'.
1950
1877
  schema_name = if ::Brick.config.schema_behavior[:multitenant]
1951
- Apartment.default_schema if apartment_excluded&.include?(r['relation_name'].singularize.camelize)
1878
+ ::Brick.apartment_default_tenant if apartment_excluded&.include?(r['relation_name'].singularize.camelize)
1952
1879
  elsif ![schema, 'public'].include?(r['schema'])
1953
1880
  r['schema']
1954
1881
  end
@@ -2099,16 +2026,16 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2099
2026
  fk = fk.values unless fk.is_a?(Array)
2100
2027
  # Multitenancy makes things a little more general overall, except for non-tenanted tables
2101
2028
  if apartment_excluded&.include?(::Brick.namify(fk[1]).singularize.camelize)
2102
- fk[0] = Apartment.default_schema
2103
- elsif (is_postgres && (fk[0] == 'public' || (is_multitenant && fk[0] == schema))) ||
2029
+ fk[0] = ::Brick.apartment_default_tenant
2030
+ elsif (is_postgres && (fk[0] == 'public' || (multitenancy && fk[0] == schema))) ||
2104
2031
  (::Brick.is_oracle && fk[0] == schema) ||
2105
2032
  (is_mssql && fk[0] == 'dbo') ||
2106
2033
  (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[0]))
2107
2034
  fk[0] = nil
2108
2035
  end
2109
2036
  if apartment_excluded&.include?(fk[4].singularize.camelize)
2110
- fk[3] = Apartment.default_schema
2111
- elsif (is_postgres && (fk[3] == 'public' || (is_multitenant && fk[3] == schema))) ||
2037
+ fk[3] = ::Brick.apartment_default_tenant
2038
+ elsif (is_postgres && (fk[3] == 'public' || (multitenancy && fk[3] == schema))) ||
2112
2039
  (::Brick.is_oracle && fk[3] == schema) ||
2113
2040
  (is_mssql && fk[3] == 'dbo') ||
2114
2041
  (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[3]))
@@ -2126,7 +2053,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2126
2053
  relations.each do |k, v|
2127
2054
  rel_name = k.split('.').map { |rel_part| ::Brick.namify(rel_part, :underscore) }
2128
2055
  schema_names = rel_name[0..-2]
2129
- schema_names.shift if ::Brick.apartment_multitenant && schema_names.first == Apartment.default_schema
2056
+ schema_names.shift if ::Brick.apartment_multitenant && schema_names.first == ::Brick.apartment_default_tenant
2130
2057
  v[:schema] = schema_names.join('.') unless schema_names.empty?
2131
2058
  # %%% If more than one schema has the same table name, will need to add a schema name prefix to have uniqueness
2132
2059
  v[:resource] = rel_name.last
@@ -2137,7 +2064,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2137
2064
  end
2138
2065
  ::Brick.load_additional_references if initializer_loaded
2139
2066
 
2140
- if orig_schema && (orig_schema = (orig_schema - ['pg_catalog', 'heroku_ext']).first)
2067
+ if orig_schema && (orig_schema = (orig_schema - ['pg_catalog', 'pg_toast', 'heroku_ext']).first)
2141
2068
  puts "Now switching back to \"#{orig_schema}\" schema."
2142
2069
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", orig_schema)
2143
2070
  end
@@ -2175,7 +2102,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2175
2102
  AND kcu.column_name = c.column_name#{"
2176
2103
  -- AND kcu.position_in_unique_constraint IS NULL" unless is_mssql}
2177
2104
  WHERE t.table_schema #{is_postgres || is_mssql ?
2178
- "NOT IN ('information_schema', 'pg_catalog',
2105
+ "NOT IN ('information_schema', 'pg_catalog', 'pg_toast', 'heroku_ext',
2179
2106
  'INFORMATION_SCHEMA', 'sys')"
2180
2107
  :
2181
2108
  "= '#{ActiveRecord::Base.connection.current_database.tr("'", "''")}'"}#{"
@@ -2274,7 +2201,7 @@ module Brick
2274
2201
  for_tbl = fk[1]
2275
2202
  fk_namified = ::Brick.namify(fk[1])
2276
2203
  apartment = Object.const_defined?('Apartment') && Apartment
2277
- fk[0] = Apartment.default_schema if apartment && apartment.excluded_models.include?(fk_namified.singularize.camelize)
2204
+ fk[0] = ::Brick.apartment_default_tenant if apartment && apartment.excluded_models.include?(fk_namified.singularize.camelize)
2278
2205
  fk[1] = "#{fk[0]}.#{fk[1]}" if fk[0] # && fk[0] != ::Brick.default_schema
2279
2206
  bts = (relation = relations.fetch(fk[1], nil))&.fetch(:fks) { relation[:fks] = {} }
2280
2207
 
@@ -2290,7 +2217,7 @@ module Brick
2290
2217
  # If Apartment gem lists the primary table as being associated with a non-tenanted model
2291
2218
  # then use 'public' schema for the primary table
2292
2219
  if apartment && apartment&.excluded_models.include?(fk[4].singularize.camelize)
2293
- fk[3] = Apartment.default_schema
2220
+ fk[3] = ::Brick.apartment_default_tenant
2294
2221
  true
2295
2222
  end
2296
2223
  else
@@ -2365,7 +2292,7 @@ module Brick
2365
2292
  end
2366
2293
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
2367
2294
  else
2368
- inv_tbl = if ::Brick.config.schema_behavior[:multitenant] && apartment && fk[0] == Apartment.default_schema
2295
+ inv_tbl = if ::Brick.config.schema_behavior[:multitenant] && apartment && fk[0] == ::Brick.apartment_default_tenant
2369
2296
  for_tbl
2370
2297
  else
2371
2298
  fk[1]
@@ -2386,7 +2313,7 @@ module Brick
2386
2313
  rails_root = ::Rails.root.to_s
2387
2314
  migrations = if Dir.exist?(mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{rails_root}/db/migrate")
2388
2315
  Dir["#{mig_path}/**/*.rb"].each_with_object(Hash.new { |h, k| h[k] = [] }) do |v, s|
2389
- File.read(v).split("\n").each do |line|
2316
+ File.read(v).split("\n").each_with_index do |line, line_idx|
2390
2317
  # For all non-commented lines, look for any that have "create_table", "alter_table", or "drop_table"
2391
2318
  if !line.lstrip.start_with?('#') &&
2392
2319
  (idx = (line.index('create_table ') || line.index('create_table('))&.+(13)) ||
@@ -2394,8 +2321,9 @@ module Brick
2394
2321
  (idx = (line.index('drop_table ') || line.index('drop_table('))&.+(11))
2395
2322
  tbl = line[idx..-1].match(/([:'"\w\.]+)/)&.captures&.first
2396
2323
  if tbl
2397
- s[tbl.tr(':\'"', '').pluralize] << v
2398
- break
2324
+ v = v[(rails_root.length)..-1] if v.start_with?(rails_root)
2325
+ v = v[1..-1] if v.start_with?('/')
2326
+ s[tbl.tr(':\'"', '').pluralize] << [v, line_idx + 1]
2399
2327
  end
2400
2328
  end
2401
2329
  end
@@ -2424,7 +2352,7 @@ module Brick
2424
2352
  end
2425
2353
  ::Brick.relations.keys.map do |v|
2426
2354
  tbl_parts = v.split('.')
2427
- tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == Apartment.default_schema
2355
+ tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
2428
2356
  res = tbl_parts.join('.')
2429
2357
  [v, (model = models[res])&.last&.table_name, migrations&.fetch(res, nil), model&.first]
2430
2358
  end
@@ -2454,14 +2382,14 @@ module Brick
2454
2382
 
2455
2383
  # Locate orphaned records
2456
2384
  def find_orphans(multi_schema)
2457
- is_default_schema = multi_schema&.==(Apartment.default_schema)
2385
+ is_default_schema = multi_schema&.==(::Brick.apartment_default_tenant)
2458
2386
  relations.each_with_object([]) do |v, s|
2459
2387
  frn_tbl = v.first
2460
2388
  next if (relation = v.last).key?(:isView) || config.exclude_tables.include?(frn_tbl) ||
2461
2389
  !(for_pk = (relation[:pkey].values.first&.first))
2462
2390
 
2463
2391
  is_default_frn_schema = !is_default_schema && multi_schema &&
2464
- ((frn_parts = frn_tbl.split('.')).length > 1 && frn_parts.first)&.==(Apartment.default_schema)
2392
+ ((frn_parts = frn_tbl.split('.')).length > 1 && frn_parts.first)&.==(::Brick.apartment_default_tenant)
2465
2393
  relation[:fks].select { |_k, assoc| assoc[:is_bt] }.each do |_k, bt|
2466
2394
  begin
2467
2395
  if bt.key?(:polymorphic)
@@ -2477,7 +2405,7 @@ module Brick
2477
2405
  # Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
2478
2406
  # are both in the "public" schema
2479
2407
  next if is_default_frn_schema &&
2480
- ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(Apartment.default_schema)
2408
+ ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(::Brick.apartment_default_tenant)
2481
2409
 
2482
2410
  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
2483
2411
  FROM #{frn_tbl} AS frn
@@ -2496,7 +2424,7 @@ module Brick
2496
2424
  # are both in the "public" schema
2497
2425
  pri_tbl = bt.key?(:inverse_table) && bt[:inverse_table]
2498
2426
  next if is_default_frn_schema &&
2499
- ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(Apartment.default_schema)
2427
+ ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(::Brick.apartment_default_tenant)
2500
2428
 
2501
2429
  pri_pk = relations[pri_tbl].fetch(:pkey, nil)&.values&.first&.first ||
2502
2430
  _class_pk(pri_tbl, multi_schema)
@@ -141,10 +141,10 @@ module Brick
141
141
  next unless @_brick_model.instance_methods.include?(through) &&
142
142
  (associative = @_brick_model._br_associatives.fetch(hm.first, nil))
143
143
 
144
- tbl_nm = if (source = hm_assoc.source_reflection).macro == :belongs_to
145
- hm_assoc.through_reflection&.name # for standard HMT, which is HM -> BT
146
- else
144
+ tbl_nm = if (source = hm_assoc.source_reflection).macro == :has_many
147
145
  source.inverse_of&.name # For HM -> HM style HMT
146
+ else # belongs_to or has_one
147
+ hm_assoc.through_reflection&.name # for standard HMT, which is HM -> BT
148
148
  end
149
149
  # If there is no inverse available for the source belongs_to association, make one based on the class name
150
150
  unless tbl_nm
@@ -192,18 +192,17 @@ 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
205
205
  table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables).each_with_object({}) do |tbl, s|
206
- binding.pry if tbl.is_a?(Symbol)
207
206
  if (tbl_parts = tbl.split('.')).first == apartment_default_schema
208
207
  tbl = tbl_parts.last
209
208
  end
@@ -466,6 +465,22 @@ var #{table_name}HtColumns;
466
465
  // This PageTransitionEvent fires when the page first loads, as well as after any other history
467
466
  // transition such as when using the browser's Back and Forward buttons.
468
467
  window.addEventListener(\"pageshow\", function() {
468
+ if (tblSelect) { // Always present
469
+ var i = #{::Brick.config.path_prefix ? '0' : 'schemaSelect ? 1 : 0'},
470
+ changeoutList = changeout(location.href);
471
+ for (; i < changeoutList.length; ++i) {
472
+ tblSelect.value = changeoutList[i];
473
+ if (tblSelect.value !== \"\") break;
474
+ }
475
+
476
+ tblSelect.addEventListener(\"change\", function () {
477
+ var lhr = changeout(location.href, null, this.value);
478
+ if (brickSchema)
479
+ lhr = changeout(lhr, \"_brick_schema\", schemaSelect.value);
480
+ location.href = lhr;
481
+ });
482
+ }
483
+
469
484
  if (schemaSelect && schemaSelect.options.length > 1) { // First drop-down is only present if multitenant
470
485
  brickSchema = changeout(location.href, \"_brick_schema\");
471
486
  if (brickSchema) {
@@ -478,6 +493,7 @@ window.addEventListener(\"pageshow\", function() {
478
493
  location.href = changeout(location.href, \"_brick_schema\", this.value, tblSelect.value);
479
494
  });
480
495
  }
496
+
481
497
  [... document.getElementsByTagName(\"FORM\")].forEach(function (form) {
482
498
  if (brickSchema)
483
499
  form.action = changeout(form.action, \"_brick_schema\", brickSchema);
@@ -489,22 +505,6 @@ window.addEventListener(\"pageshow\", function() {
489
505
  return true;
490
506
  });
491
507
  });
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
508
  });
509
509
 
510
510
  // Add \"Are you sure?\" behaviour to any data-confirm buttons out there
@@ -834,7 +834,7 @@ erDiagram
834
834
  </head>
835
835
  <body>
836
836
  <p style=\"color: green\"><%= notice %></p>#{"
837
- <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
837
+ #{schema_options}" if schema_options}
838
838
  <select id=\"tbl\">#{table_options}</select>
839
839
  <table id=\"resourceName\"><tr>
840
840
  <td><h1>#{model_name}</h1></td>
@@ -910,7 +910,9 @@ erDiagram
910
910
  end
911
911
  unless @_brick_sequence # If no sequence is defined, start with all inclusions
912
912
  cust_cols = #{model_name}._br_cust_cols
913
- @_brick_sequence = col_keys + cust_cols.keys + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
913
+ # HOT columns, kept as symbols
914
+ hots = #{model_name}._br_bt_descrip.keys.select { |k| bts.key?(k) }
915
+ @_brick_sequence = col_keys + cust_cols.keys + hots + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
914
916
  end
915
917
  @_brick_sequence.reject! { |nm| @_brick_excl.include?(nm) } if @_brick_excl # Reject exclusions
916
918
  @_brick_sequence.each_with_object(+'') do |col_name, s|
@@ -927,8 +929,12 @@ erDiagram
927
929
  elsif col # HM column
928
930
  s << \"<th#\{' x-order=\"' + col_name + '\"' if true}>#\{col[2]} \"
929
931
  s << (col.first ? \"#\{col[3]}\" : \"#\{link_to(col[3], send(\"#\{col[1]._brick_index}_path\"))}\")
930
- elsif (cc = cust_cols.key?(col_name)) # Custom column
932
+ elsif cust_cols.key?(col_name) # Custom column
931
933
  s << \"<th x-order=\\\"#\{col_name}\\\">#\{col_name}\"
934
+ elsif col_name.is_a?(Symbol) && (hot = bts[col_name]) # has_one :through
935
+ s << \"<th x-order=\\\"#\{hot.first.to_s}\\\">HOT \" +
936
+ hot[1].map { |hot_pair| hot_pair.first.bt_link(col_name) }.join(' ')
937
+ hot[1].first
932
938
  else # Bad column name!
933
939
  s << \"<th title=\\\"<< Unknown column >>\\\">#\{col_name}\"
934
940
  end
@@ -946,18 +952,22 @@ erDiagram
946
952
  <td><%= link_to '⇛', #{path_obj_name}_path(slashify(#{obj_pk})), { class: 'big-arrow' } %></td>" if obj_pk}
947
953
  <% @_brick_sequence.each do |col_name|
948
954
  val = #{obj_name}.attributes[col_name] %>
949
- <td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name) || (cust_col = cust_cols[col_name])%>><%
955
+ <td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name) || (cust_col = cust_cols[col_name]) ||
956
+ (col_name.is_a?(Symbol) && bts.key?(col_name)) # HOT
957
+ %>><%
950
958
  if (bt = bts[col_name])
951
959
  if bt[2] # Polymorphic?
952
960
  bt_class = #{obj_name}.send(\"#\{bt.first}_type\")
953
961
  base_class_underscored = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class._brick_index(:singular)
954
962
  poly_id = #{obj_name}.send(\"#\{bt.first}_id\")
955
963
  %><%= link_to(\"#\{bt_class} ##\{poly_id}\", send(\"#\{base_class_underscored}_path\".to_sym, poly_id)) if poly_id %><%
956
- else
957
- # binding.pry if @_brick_bt_descrip[bt.first][bt[1].first.first].nil?
964
+ else # BT or HOT
958
965
  bt_class = bt[1].first.first
959
966
  descrips = @_brick_bt_descrip[bt.first][bt_class]
960
- bt_id_col = if descrips.length == 1
967
+ bt_id_col = if descrips.nil?
968
+ puts \"Caught it in the act for #{obj_name} / #\{col_name}!\"
969
+ # binding.pry
970
+ elsif descrips.length == 1
961
971
  [#{obj_name}.class.reflect_on_association(bt.first)&.foreign_key]
962
972
  else
963
973
  descrips.last
@@ -974,16 +984,16 @@ erDiagram
974
984
  if hms_col.length == 1 %>
975
985
  <%= hms_col.first %>
976
986
  <% 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? %>
987
+ %><%= klass = (col = cols[col_name])[1]
988
+ if col[2] == 'HO'
989
+ descrips = @_brick_bt_descrip[col_name.to_sym][klass]
990
+ if (ho_id = (ho_id_col = descrips.last).map { |id_col| #{obj_name}.send(id_col.to_sym) })&.first
991
+ ho_txt = klass.brick_descrip(#{obj_name}, descrips[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, ho_id_col)
992
+ link_to(ho_txt, send(\"#\{klass.base_class._brick_index(:singular)}_path\".to_sym, ho_id))
993
+ end
994
+ elsif hms_col[1]&.positive?
995
+ link_to \"#\{hms_col[1] || 'View'} #\{hms_col.first}\", send(\"#\{klass._brick_index}_path\".to_sym, hms_col[2])
996
+ end %>
987
997
  <% end
988
998
  elsif (col = cols[col_name])
989
999
  col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
@@ -1019,7 +1029,7 @@ erDiagram
1019
1029
  # Easily could be multiple files involved (STI for instance)
1020
1030
  +"#{css}
1021
1031
  <p style=\"color: green\"><%= notice %></p>#{"
1022
- <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
1032
+ #{schema_options}" if schema_options}
1023
1033
  <select id=\"tbl\">#{table_options}</select>
1024
1034
  <h1>Status</h1>
1025
1035
  <table id=\"status\" class=\"shadow\"><thead><tr>
@@ -1038,7 +1048,7 @@ erDiagram
1038
1048
  %>
1039
1049
  <tr>
1040
1050
  <td><%= begin
1041
- kls = Object.const_get(::Brick.relations[r[0]].fetch(:class_name, nil))
1051
+ kls = Object.const_get(::Brick.relations.fetch(r[0], nil)&.fetch(:class_name, nil))
1042
1052
  rescue
1043
1053
  end
1044
1054
  kls ? link_to(r[0], send(\"#\{kls._brick_index}_path\".to_sym)) : r[0] %></td>
@@ -1048,8 +1058,9 @@ erDiagram
1048
1058
  ' class=\"dimmed\"'
1049
1059
  end&.html_safe %>><%= # Table
1050
1060
  r[1] %></td>
1051
- <td<%= ' class=\"dimmed\"'.html_safe unless r[2] %>><%= # Migration
1052
- r[2]&.join('<br>')&.html_safe %></td>
1061
+ <td<%= lines = r[2]&.map { |line| \"#\{line.first}:#\{line.last}\" }
1062
+ ' class=\"dimmed\"'.html_safe unless r[2] %>><%= # Migration
1063
+ lines&.join('<br>')&.html_safe %></td>
1053
1064
  <td<%= ' class=\"dimmed\"'.html_safe unless r[3] %>><%= # Model
1054
1065
  r[3] %></td>
1055
1066
  <td<%= ' class=\"dimmed\"'.html_safe unless r[4] %>><%= # Route
@@ -1068,7 +1079,7 @@ erDiagram
1068
1079
  if is_orphans
1069
1080
  +"#{css}
1070
1081
  <p style=\"color: green\"><%= notice %></p>#{"
1071
- <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
1082
+ #{schema_options}" if schema_options}
1072
1083
  <select id=\"tbl\">#{table_options}</select>
1073
1084
  <h1>Orphans<%= \" for #\{}\" if false %></h1>
1074
1085
  <% @orphans.each do |o|
@@ -1099,7 +1110,7 @@ erDiagram
1099
1110
  </svg>
1100
1111
 
1101
1112
  <p style=\"color: green\"><%= notice %></p>#{"
1102
- <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
1113
+ #{schema_options}" if schema_options}
1103
1114
  <select id=\"tbl\">#{table_options}</select>
1104
1115
  <h1><%= page_title %></h1><%
1105
1116
  if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(:description, nil)) %><%=
@@ -1411,9 +1422,26 @@ document.querySelectorAll(\"input, select\").forEach(function (inp) {
1411
1422
  # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
1412
1423
  keys = options.has_key?(:locals) ? options[:locals].keys : []
1413
1424
  handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
1414
- ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys)
1425
+ ActionView::Template.new(inline, "auto-generated #{args.first} template", handler, locals: keys).tap do |t|
1426
+ t.instance_variable_set(:@is_brick, true)
1427
+ end
1415
1428
  end
1416
- end
1429
+ end # LookupContext
1430
+
1431
+ # For any auto-generated template, if multitenancy is active via some flavour of an Apartment gem, switch back to the default tenant.
1432
+ # (Underlying reason -- ros-apartment can hold on to a selected tenant between requests when there is no elevator middleware.)
1433
+ ActionView::TemplateRenderer.class_exec do
1434
+ private
1435
+
1436
+ alias _brick_render_template render_template
1437
+ def render_template(view, template, *args)
1438
+ result = _brick_render_template(view, template, *args)
1439
+ if template.instance_variable_get(:@is_brick)
1440
+ Apartment::Tenant.switch!(::Brick.apartment_default_tenant) if ::Brick.apartment_multitenant
1441
+ end
1442
+ result
1443
+ end
1444
+ end # TemplateRenderer
1417
1445
  end
1418
1446
 
1419
1447
  if ::Brick.enable_routes?
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 92
8
+ TINY = 94
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 ||= {}
@@ -205,12 +211,13 @@ module Brick
205
211
  else
206
212
  s.first[a.foreign_key.to_s] = [a.name, a.klass]
207
213
  end
208
- else # This gets has_many as well as has_one and has_many :through
209
- if through
214
+ else # This gets all forms of has_many and has_one
215
+ if through # has_many :through or has_one :through
210
216
  is_invalid_source = nil
211
217
  begin
212
- if a.through_reflection&.belongs_to?
213
- puts "WARNING: HMT relationship :#{a.name} in model #{model.name} tries to go through belongs_to association :#{through}. This is not possible."
218
+ if a.through_reflection.macro != :has_many # This HM goes through either a belongs_to or a has_one, so essentially a HOT?
219
+ # Treat it like a belongs_to - just keyed on the association name instead of a foreign_key
220
+ s.first[a.name] = [a.name, a.klass]
214
221
  next
215
222
  elsif !a.source_reflection # Had considered: a.active_record.reflect_on_association(a.source_reflection_name).nil?
216
223
  is_invalid_source = true
@@ -1086,21 +1093,9 @@ ActiveSupport.on_load(:active_record) do
1086
1093
  end
1087
1094
  end
1088
1095
 
1089
- # First part of arel_table_type stuff:
1090
- # ------------------------------------
1091
- # (more found below)
1092
1096
  # was: ActiveRecord.version >= ::Gem::Version.new('3.2') &&
1093
1097
  if ActiveRecord.version < ::Gem::Version.new('5.0')
1094
- # Used by Util#_arel_table_type
1095
1098
  module ActiveRecord
1096
- class Base
1097
- def self.arel_table
1098
- @arel_table ||= Arel::Table.new(table_name, arel_engine).tap do |x|
1099
- x.instance_variable_set(:@_arel_table_type, self)
1100
- end
1101
- end
1102
- end
1103
-
1104
1099
  # Final pieces for left_outer_joins support, which was derived from this commit:
1105
1100
  # https://github.com/rails/rails/commit/3f46ef1ddab87482b730a3f53987e04308783d8b
1106
1101
  module Associations
@@ -1185,13 +1180,9 @@ if is_postgres && ActiveRecord.version < ::Gem::Version.new('5.0') # Was: && Ob
1185
1180
  PGError = PG::Error
1186
1181
  end
1187
1182
 
1188
- # More arel_table_type stuff:
1189
- # ---------------------------
1190
1183
  if ActiveRecord.version < ::Gem::Version.new('5.2')
1191
1184
  # Specifically for AR 3.1 and 3.2 to avoid: "undefined method `delegate' for ActiveRecord::Reflection::ThroughReflection:Class"
1192
1185
  require 'active_support/core_ext/module/delegation' if ActiveRecord.version < ::Gem::Version.new('4.0')
1193
- # Used by Util#_arel_table_type
1194
- # rubocop:disable Style/CommentedKeyword
1195
1186
  module ActiveRecord
1196
1187
  module Reflection
1197
1188
  # AR < 4.0 doesn't know about join_table and derive_join_table
@@ -1209,61 +1200,23 @@ if ActiveRecord.version < ::Gem::Version.new('5.2')
1209
1200
  end
1210
1201
  end
1211
1202
  end
1212
-
1213
- module Associations
1214
- # Specific to AR 4.2 - 5.1:
1215
- if self.const_defined?('JoinDependency') && JoinDependency.private_instance_methods.include?(:table_aliases_for)
1216
- class JoinDependency
1217
- private
1218
-
1219
- if ActiveRecord.version < ::Gem::Version.new('5.1') # 4.2 or 5.0
1220
- def table_aliases_for(parent, node)
1221
- node.reflection.chain.map do |reflection|
1222
- alias_tracker.aliased_table_for(
1223
- reflection.table_name,
1224
- table_alias_for(reflection, parent, reflection != node.reflection)
1225
- ).tap do |x|
1226
- # %%% Specific only to Rails 4.2 (and maybe 4.1?)
1227
- x = x.left if x.is_a?(Arel::Nodes::TableAlias)
1228
- y = reflection.chain.find { |c| c.table_name == x.name }
1229
- x.instance_variable_set(:@_arel_table_type, y.klass)
1230
- end
1231
- end
1232
- end
1233
- end
1234
- end
1235
- elsif Associations.const_defined?('JoinHelper') && JoinHelper.private_instance_methods.include?(:construct_tables)
1236
- module JoinHelper
1237
- private
1238
-
1239
- # AR > 3.0 and < 4.2 (%%% maybe only < 4.1?) uses construct_tables like this:
1240
- def construct_tables
1241
- tables = []
1242
- chain.each do |reflection|
1243
- tables << alias_tracker.aliased_table_for(
1244
- table_name_for(reflection),
1245
- table_alias_for(reflection, reflection != self.reflection)
1246
- ).tap do |x|
1247
- x = x.left if x.is_a?(Arel::Nodes::TableAlias)
1248
- x.instance_variable_set(:@_arel_table_type, reflection.chain.find { |c| c.table_name == x.name }.klass)
1249
- end
1250
-
1251
- next unless reflection.source_macro == :has_and_belongs_to_many
1252
-
1253
- tables << alias_tracker.aliased_table_for(
1254
- (reflection.source_reflection || reflection).join_table,
1255
- table_alias_for(reflection, true)
1256
- )
1257
- end
1258
- tables
1259
- end
1260
- end
1261
- end
1262
- end
1263
- end # module ActiveRecord
1264
- # rubocop:enable Style/CommentedKeyword
1203
+ end
1265
1204
  end
1266
1205
 
1206
+ # The "brick_links" patch -- this finds how every AR chain of association names
1207
+ # relates back to an exact table correlation name chosen by AREL when the AST tree is
1208
+ # walked. For instance, from a Customer model there could be a join_tree such as
1209
+ # { orders: { line_items: :product} }, which would end up recording three entries, the
1210
+ # last of which for products would have a key of "orders.line_items.product" after
1211
+ # having gone through two HMs and one BT. AREL would have chosen a correlation name of
1212
+ # "products", being able to use the same name as the table name because it's the first
1213
+ # time that table is used in this query. But let's see what happens if each customer
1214
+ # also had a BT to a favourite product, referenced earlier in the join_tree like this:
1215
+ # [:favourite_product, orders: { line_items: :product}] -- then the second reference to
1216
+ # "products" would end up being called "products_line_items" in order to differentiate
1217
+ # it from the first reference, which would have already snagged the simpler name
1218
+ # "products". It's essential that The Brick can find accurate correlation names when
1219
+ # there are multiple JOINs to the same table.
1267
1220
  module ActiveRecord
1268
1221
  module QueryMethods
1269
1222
  private
@@ -1290,7 +1243,7 @@ module ActiveRecord
1290
1243
  end
1291
1244
  end
1292
1245
 
1293
- # require 'activerecord/associations/join_dependency'
1246
+ # require 'active_record/associations/join_dependency'
1294
1247
  module Associations
1295
1248
  # For AR >= 4.2
1296
1249
  if self.const_defined?('JoinDependency')
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.92
4
+ version: 1.0.94
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-15 00:00:00.000000000 Z
11
+ date: 2022-11-22 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