brick 1.0.92 → 1.0.94

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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