brick 1.0.27 → 1.0.30

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.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Some future enhancement ideas:
4
+
3
5
  # Have markers on HM relationships to indicate "load this one every time" or "lazy load it" or "don't bother"
4
6
  # Others on BT to indicate "this is a lookup"
5
7
 
@@ -18,7 +20,7 @@
18
20
  # Sensitive stuff -- make a lock icon thing so people don't accidentally edit stuff
19
21
 
20
22
  # Static text that can go on pages - headings and footers and whatever
21
- # Eventually some indication about if it should be a paginated table / unpaginated / a list of just some fields / etc
23
+ # Eventually some indication about if it should be a paginated table / unpaginated / a list of just some fields / columns shown in a different sequence / etc
22
24
 
23
25
  # Grid where each cell is one field and then when you mouse over then it shows a popup other table of detail inside
24
26
 
@@ -32,6 +34,9 @@
32
34
 
33
35
  # Currently quadrupling up routes
34
36
 
37
+ # Modal pop-up things for editing large text / date ranges / hierarchies of data
38
+
39
+ # For recognised self-references, have the show page display all related objects up to the parent (or the start of a circular reference)
35
40
 
36
41
  # ==========================================================
37
42
  # Dynamically create model or controller classes when needed
@@ -62,6 +67,18 @@ module ActiveRecord
62
67
  false
63
68
  end
64
69
 
70
+ def self._brick_primary_key(relation = nil)
71
+ return instance_variable_get(:@_brick_primary_key) if instance_variable_defined?(:@_brick_primary_key)
72
+
73
+ pk = primary_key.is_a?(String) ? [primary_key] : primary_key || []
74
+ # Just return [] if we're missing any part of the primary key. (PK is usually just "id")
75
+ if relation && pk.present?
76
+ @_brick_primary_key ||= pk.any? { |pk_part| !relation[:cols].key?(pk_part) } ? [] : pk
77
+ else # No definitive key yet, so return what we can without setting the instance variable
78
+ pk
79
+ end
80
+ end
81
+
65
82
  # Used to show a little prettier name for an object
66
83
  def self.brick_get_dsl
67
84
  # If there's no DSL yet specified, just try to find the first usable column on this model
@@ -179,7 +196,7 @@ module ActiveRecord
179
196
  def self.bt_link(assoc_name)
180
197
  model_underscore = name.underscore
181
198
  assoc_name = CGI.escapeHTML(assoc_name.to_s)
182
- model_path = Rails.application.routes.url_helpers.send("#{model_underscore.pluralize}_path".to_sym)
199
+ model_path = Rails.application.routes.url_helpers.send("#{model_underscore.tr('/', '_').pluralize}_path".to_sym)
183
200
  link = Class.new.extend(ActionView::Helpers::UrlHelper).link_to(name, model_path)
184
201
  model_underscore == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
185
202
  end
@@ -289,8 +306,12 @@ module ActiveRecord
289
306
 
290
307
  # %%% Skip the metadata columns
291
308
  if selects&.empty? # Default to all columns
309
+ tbl_no_schema = table.name.split('.').last
292
310
  columns.each do |col|
293
- selects << "#{table.name}.#{col.name}"
311
+ if (col_name = col.name) == 'class'
312
+ col_alias = ' AS _class'
313
+ end
314
+ selects << "\"#{tbl_no_schema}\".\"#{col_name}\"#{col_alias}"
294
315
  end
295
316
  end
296
317
 
@@ -332,7 +353,7 @@ module ActiveRecord
332
353
  end
333
354
 
334
355
  if join_array.present?
335
- left_outer_joins!(join_array) # joins!(join_array)
356
+ left_outer_joins!(join_array)
336
357
  # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
337
358
  (rel_dupe = dup)._arel_alias_names
338
359
  core_selects = selects.dup
@@ -343,24 +364,20 @@ module ActiveRecord
343
364
  v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
344
365
  next if chains[k1].nil?
345
366
 
346
- tbl_name = field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])
347
- # if (col_name = v1[1].last&.last) # col_name is weak when there are multiple, using sel_col.last instead
367
+ tbl_name = (field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])).split('.').last
348
368
  field_tbl_name = nil
349
- v1.map { |x|
350
- [translations[x[0..-2].map(&:to_s).join('.')], x.last]
351
- }.each_with_index do |sel_col, idx|
352
- field_tbl_name ||= field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])
369
+ v1.map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
370
+ field_tbl_name = (field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])).split('.').last
353
371
 
354
- selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
372
+ selects << "#{"\"#{field_tbl_name}\".\"#{sel_col.last}\""} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
355
373
  v1[idx] << col_alias
356
374
  end
357
- # end
358
375
 
359
376
  unless id_for_tables.key?(v.first)
360
377
  # Accommodate composite primary key by allowing id_col to come in as an array
361
378
  ((id_col = k1.primary_key).is_a?(Array) ? id_col : [id_col]).each do |id_part|
362
379
  id_for_tables[v.first] << if id_part
363
- selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
380
+ selects << "#{"\"#{tbl_name}\".\"#{id_part}\""} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
364
381
  id_alias
365
382
  end
366
383
  end
@@ -407,6 +424,7 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associ
407
424
  joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
408
425
  end
409
426
  where!(wheres) unless wheres.empty?
427
+ limit(1000) # Don't want to get too carried away just yet
410
428
  wheres unless wheres.empty? # Return the specific parameters that we did use
411
429
  end
412
430
 
@@ -471,7 +489,7 @@ if ActiveSupport::Dependencies.respond_to?(:autoload_module!) # %%% Only works w
471
489
  ::Brick.sti_models[qualified_name] = { base: base_class }
472
490
  # Build subclass and place it into the specially STI-namespaced module
473
491
  into.const_set(const_name.to_sym, klass = Class.new(base_class))
474
- # %%% used to also have: autoload_once_paths.include?(base_path) ||
492
+ # %%% used to also have: autoload_once_paths.include?(base_path) ||
475
493
  autoloaded_constants << qualified_name unless autoloaded_constants.include?(qualified_name)
476
494
  klass
477
495
  elsif (base_class = ::Brick.config.sti_namespace_prefixes&.fetch("::#{const_name}", nil)&.constantize)
@@ -485,75 +503,209 @@ if ActiveSupport::Dependencies.respond_to?(:autoload_module!) # %%% Only works w
485
503
  end
486
504
  end
487
505
 
488
- class Object
489
- class << self
490
- alias _brick_const_missing const_missing
491
- def const_missing(*args)
492
- return self.const_get(args.first) if self.const_defined?(args.first)
493
- return Object.const_get(args.first) if Object.const_defined?(args.first) unless self == Object
494
-
495
- class_name = args.first.to_s
496
- # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
497
- # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
498
- # that is, checking #qualified_name_for with: from_mod, const_name
499
- # If we want to support namespacing in the future, might have to utilise something like this:
500
- # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
501
- # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
502
- # If the file really exists, go and snag it:
503
- if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
504
- filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
505
- end
506
- if is_found
507
- return self._brick_const_missing(*args)
508
- elsif ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
509
- my_const = parent.const_missing(class_name) # ends up having: MyModule::MyClass
510
- return my_const
511
- end
506
+ Module.class_exec do
507
+ alias _brick_const_missing const_missing
508
+ def const_missing(*args)
509
+ if (self.const_defined?(args.first) && (possible = self.const_get(args.first)) != self) ||
510
+ (self != Object && Object.const_defined?(args.first) && (possible = Object.const_get(args.first)) != self)
511
+ return possible
512
+ end
513
+ class_name = args.first.to_s
514
+ # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
515
+ # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
516
+ # that is, checking #qualified_name_for with: from_mod, const_name
517
+ # If we want to support namespacing in the future, might have to utilise something like this:
518
+ # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
519
+ # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
520
+ # If the file really exists, go and snag it:
521
+ if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
522
+ filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
523
+ end
524
+ if is_found
525
+ return self._brick_const_missing(*args)
526
+ # elsif ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
527
+ # my_const = parent.const_missing(class_name) # ends up having: MyModule::MyClass
528
+ # return my_const
529
+ end
512
530
 
513
- relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
514
- result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
515
- # Otherwise now it's up to us to fill in the gaps
516
- if (model = plural_class_name.singularize.constantize)
517
- # if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
518
- build_controller(class_name, plural_class_name, model, relations)
519
- end
520
- elsif ::Brick.enable_models?
521
- # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
522
- # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
523
- plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
524
- singular_table_name = ActiveSupport::Inflector.underscore(model_name)
525
-
526
- # Adjust for STI if we know of a base model for the requested model name
527
- table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, ::Brick.existing_stis[model_name]&.constantize))
528
- base_model.table_name
529
- else
530
- ActiveSupport::Inflector.pluralize(singular_table_name)
531
- end
532
-
533
- # Maybe, just maybe there's a database table that will satisfy this need
534
- if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(m) })
535
- build_model(model_name, singular_table_name, table_name, relations, matching)
536
- end
531
+ relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
532
+ # puts "ON OBJECT: #{args.inspect}" if self.module_parent == Object
533
+ result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
534
+ # Otherwise now it's up to us to fill in the gaps
535
+ # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
536
+ # VAbc instead of VABC)
537
+ full_class_name = +''
538
+ full_class_name << "::#{self.name}" unless self == Object
539
+ full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
540
+ if (model = self.const_get(full_class_name))
541
+ # if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
542
+ Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
543
+ end
544
+ elsif (::Brick.enable_models? || ::Brick.enable_controllers?) && # Schema match?
545
+ self == Object && # %%% This works for Person::Person -- but also limits us to not being able to allow more than one level of namespacing
546
+ schema_name = [(singular_table_name = class_name.underscore),
547
+ (table_name = singular_table_name.pluralize),
548
+ class_name,
549
+ (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas.include?(s) }
550
+ # Build out a module for the schema if it's namespaced
551
+ schema_name = schema_name.camelize
552
+ self.const_set(schema_name.to_sym, (built_module = Module.new))
553
+
554
+ [built_module, "module #{schema_name}; end\n"]
555
+ # # %%% Perhaps an option to use the first module just as schema, and additional modules as namespace with a table name prefix applied
556
+ elsif ::Brick.enable_models?
557
+ # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
558
+ # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
559
+
560
+ unless self == Object # Are we in some namespace?
561
+ schema_name = [(singular_schema_name = name.underscore),
562
+ (schema_name = singular_schema_name.pluralize),
563
+ name,
564
+ name.pluralize].find { |s| Brick.db_schemas.include?(s) }
537
565
  end
538
- if result
539
- built_class, code = result
540
- puts "\n#{code}"
541
- built_class
542
- elsif ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}")
566
+
567
+ plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
568
+ # If it's namespaced then we turn the first part into what would be a schema name
569
+ singular_table_name = ActiveSupport::Inflector.underscore(model_name).gsub('::', '.')
570
+
571
+ # Adjust for STI if we know of a base model for the requested model name
572
+ table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
573
+ base_model.table_name
574
+ else
575
+ ActiveSupport::Inflector.pluralize(singular_table_name)
576
+ end
577
+
578
+ # Maybe, just maybe there's a database table that will satisfy this need
579
+ if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(schema_name ? "#{schema_name}.#{m}" : m) })
580
+ Object.send(:build_model, schema_name, model_name, singular_table_name, table_name, relations, matching)
581
+ end
582
+ end
583
+ if result
584
+ built_class, code = result
585
+ puts "\n#{code}"
586
+ built_class
587
+ elsif ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}") && !schema_name
543
588
  # module_prefixes = type_name.split('::')
544
589
  # path = self.name.split('::')[0..-2] + []
545
590
  # module_prefixes.unshift('') unless module_prefixes.first.blank?
546
591
  # candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
547
- self._brick_const_missing(*args)
548
- else
549
- puts "MISSING! #{self.name} #{args.inspect} #{table_name}"
550
- self._brick_const_missing(*args)
551
- end
592
+ self._brick_const_missing(*args)
593
+ elsif self != Object
594
+ module_parent.const_missing(*args)
595
+ else
596
+ puts "MISSING! mod #{self.name} #{args.inspect} #{table_name}"
597
+ self._brick_const_missing(*args)
552
598
  end
599
+ end
600
+ end
601
+
602
+ class Object
603
+ class << self
604
+ # alias _brick_const_missing const_missing
605
+ # def const_missing(*args)
606
+ # # return self.const_get(args.first) if self.const_defined?(args.first)
607
+ # # return Object.const_get(args.first) if Object.const_defined?(args.first) unless self == Object
608
+ # if self.const_defined?(args.first) && (possible = self.const_get(args.first)) != self
609
+ # return possible
610
+ # end
611
+ # if self != Object && Object.const_defined?(args.first) && (possible = Object.const_get(args.first)) != self
612
+ # return possible
613
+ # end
614
+
615
+ # class_name = args.first.to_s
616
+ # # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
617
+ # # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
618
+ # # that is, checking #qualified_name_for with: from_mod, const_name
619
+ # # If we want to support namespacing in the future, might have to utilise something like this:
620
+ # # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
621
+ # # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
622
+ # # If the file really exists, go and snag it:
623
+ # if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
624
+ # filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
625
+ # end
626
+ # if is_found
627
+ # return self._brick_const_missing(*args)
628
+ # elsif ActiveSupport::Dependencies.search_for_file(filepath) # Last-ditch effort to pick this thing up before we fill in the gaps on our own
629
+ # my_const = parent.const_missing(class_name) # ends up having: MyModule::MyClass
630
+ # return my_const
631
+ # end
632
+ # relations = ::Brick.instance_variable_get(:@relations)[ActiveRecord::Base.connection_pool.object_id] || {}
633
+ # result = if ::Brick.enable_controllers? && class_name.end_with?('Controller') && (plural_class_name = class_name[0..-11]).length.positive?
634
+ # # Otherwise now it's up to us to fill in the gaps
635
+ # # (Go over to underscores for a moment so that if we have something come in like VABCsController then the model name ends up as
636
+ # # VAbc instead of VABC)
637
+ # if (model = Object.const_get(plural_class_name.underscore.singularize.camelize))
638
+ # # if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
639
+ # build_controller(nil, class_name, plural_class_name, model, relations)
640
+ # end
641
+ # elsif (::Brick.enable_models? || ::Brick.enable_controllers?) && # Schema match?
642
+ # db_schema_name = [(singular_table_name = class_name.underscore),
643
+ # (table_name = singular_table_name.pluralize),
644
+ # class_name,
645
+ # (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas.include?(s) }
646
+ # # Build out a module for the schema if it's namespaced
647
+ # schema_name = db_schema_name.camelize
648
+ # unless Object.const_defined?(schema_name.to_sym)
649
+ # Object.const_set(schema_name.to_sym, (built_module = Module.new))
650
+ # Brick.db_schemas[db_schema_name] = built_module
651
+ # [built_module, "module #{schema_name}; end\n"]
652
+ # end
653
+ # # # %%% Perhaps an option to use the first module just as schema, and additional modules as namespace with a table name prefix applied
654
+ # # schema_name, model_name =
655
+ # # code = +''
656
+ # # mod_tree = +''
657
+ # # model_name.split('::')[0..-2].each do |mod_name|
658
+ # # mod_tree << "::#{mod_name}"
659
+ # # Module.new(mod_tree)
660
+ # # code << "module #{mod_tree}; end\n"
661
+ # # end
662
+ # elsif ::Brick.enable_models?
663
+ # # See if a file is there in the same way that ActiveSupport::Dependencies#load_missing_constant
664
+ # # checks for it in ~/.rvm/gems/ruby-2.7.5/gems/activesupport-5.2.6.2/lib/active_support/dependencies.rb
665
+ # plural_class_name = ActiveSupport::Inflector.pluralize(model_name = class_name)
666
+ # singular_table_name = ActiveSupport::Inflector.underscore(model_name)
667
+
668
+ # # Adjust for STI if we know of a base model for the requested model name
669
+ # table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
670
+ # base_model.table_name
671
+ # else
672
+ # ActiveSupport::Inflector.pluralize(singular_table_name)
673
+ # end
674
+
675
+ # # Maybe, just maybe there's a database table that will satisfy this need
676
+ # if (matching = [table_name, singular_table_name, plural_class_name, model_name].find { |m| relations.key?(m) })
677
+ # build_model(nil, model_name, singular_table_name, table_name, relations, matching)
678
+ # end
679
+ # end
680
+ # if result
681
+ # built_class, code = result
682
+ # puts "\n#{code}"
683
+ # built_class
684
+ # elsif ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}") && !schema_name
685
+ # # module_prefixes = type_name.split('::')
686
+ # # path = self.name.split('::')[0..-2] + []
687
+ # # module_prefixes.unshift('') unless module_prefixes.first.blank?
688
+ # # candidate_file = Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb')
689
+ # self._brick_const_missing(*args)
690
+ # elsif self != Object
691
+ # module_parent.const_missing(*args)
692
+ # else
693
+ # puts "MISSING! obj #{self.name}/#{schema_name} #{args.inspect} #{table_name}"
694
+ # self._brick_const_missing(*args)
695
+ # end
696
+ # end
553
697
 
554
698
  private
555
699
 
556
- def build_model(model_name, singular_table_name, table_name, relations, matching)
700
+ def build_model(schema_name, model_name, singular_table_name, table_name, relations, matching)
701
+ full_name = if schema_name.blank?
702
+ model_name
703
+ else # Prefix the schema to the table name + prefix the schema namespace to the class name
704
+ schema_module = (Brick.db_schemas[schema_name] ||= self.const_get(schema_name.singularize.camelize))
705
+ matching = "#{schema_name}.#{matching}"
706
+ "#{schema_module&.name}::#{model_name}"
707
+ end
708
+
557
709
  return if ((is_view = (relation = relations[matching]).key?(:isView)) && ::Brick.config.skip_database_views) ||
558
710
  ::Brick.config.exclude_tables.include?(matching)
559
711
 
@@ -565,14 +717,15 @@ class Object
565
717
  return
566
718
  end
567
719
 
568
- if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, ::Brick.existing_stis[model_name]&.constantize))
720
+ if (base_model = ::Brick.sti_models[full_name]&.fetch(:base, nil) || ::Brick.existing_stis[full_name]&.constantize)
569
721
  is_sti = true
570
722
  else
571
723
  base_model = ::Brick.config.models_inherit_from || ActiveRecord::Base
572
724
  end
573
- code = +"class #{model_name} < #{base_model.name}\n"
725
+ hmts = nil
726
+ code = +"class #{full_name} < #{base_model.name}\n"
574
727
  built_model = Class.new(base_model) do |new_model_class|
575
- Object.const_set(model_name.to_sym, new_model_class)
728
+ (schema_module || Object).const_set(model_name.to_sym, new_model_class)
576
729
  # Accommodate singular or camel-cased table names such as "order_detail" or "OrderDetails"
577
730
  code << " self.table_name = '#{self.table_name = matching}'\n" unless table_name == matching
578
731
 
@@ -585,16 +738,14 @@ class Object
585
738
  code << " def self.is_view?; true; end\n"
586
739
  end
587
740
 
588
- # Missing a primary key column? (Usually "id")
589
- ar_pks = primary_key.is_a?(String) ? [primary_key] : primary_key || []
590
741
  db_pks = relation[:cols]&.map(&:first)
591
- has_pk = ar_pks.length.positive? && (db_pks & ar_pks).sort == ar_pks.sort
742
+ has_pk = _brick_primary_key(relation).length.positive? && (db_pks & _brick_primary_key).sort == _brick_primary_key.sort
592
743
  our_pks = relation[:pkey].values.first
593
744
  # No primary key, but is there anything UNIQUE?
594
745
  # (Sort so that if there are multiple UNIQUE constraints we'll pick one that uses the least number of columns.)
595
746
  our_pks = relation[:ukeys].values.sort { |a, b| a.length <=> b.length }.first unless our_pks&.present?
596
747
  if has_pk
597
- code << " # Primary key: #{ar_pks.join(', ')}\n" unless ar_pks == ['id']
748
+ code << " # Primary key: #{_brick_primary_key.join(', ')}\n" unless _brick_primary_key == ['id']
598
749
  elsif our_pks&.present?
599
750
  if our_pks.length > 1 && respond_to?(:'primary_keys=') # Using the composite_primary_keys gem?
600
751
  new_model_class.primary_keys = our_pks
@@ -603,9 +754,14 @@ class Object
603
754
  new_model_class.primary_key = (pk_sym = our_pks.first.to_sym)
604
755
  code << " self.primary_key = #{pk_sym.inspect}\n"
605
756
  end
757
+ _brick_primary_key(relation) # Set the newly-found PK in the instance variable
606
758
  else
607
759
  code << " # Could not identify any column(s) to use as a primary key\n" unless is_view
608
760
  end
761
+ if (sti_col = relation.fetch(:sti_col, nil))
762
+ new_model_class.send(:'inheritance_column=', sti_col)
763
+ code << " self.inheritance_column = #{sti_col.inspect}\n"
764
+ end
609
765
 
610
766
  unless is_sti
611
767
  fks = relation[:fks] || {}
@@ -614,13 +770,30 @@ class Object
614
770
  # The key in each hash entry (fk.first) is the constraint name
615
771
  inverse_assoc_name = (assoc = fk.last)[:inverse]&.fetch(:assoc_name, nil)
616
772
  if (invs = assoc[:inverse_table]).is_a?(Array)
617
- invs.each { |inv| build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inv, code) }
618
- else
619
- build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, invs, code)
773
+ if assoc[:is_bt]
774
+ invs = invs.first # Just do the first one of what would be multiple identical polymorphic belongs_to
775
+ else
776
+ invs.each { |inv| build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inv, code) }
777
+ end
620
778
  end
779
+ build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, invs, code) unless invs.is_a?(Array)
621
780
  hmts
622
781
  end
782
+ # # Not NULLables
783
+ # # %%% For the minute we've had to pull this out because it's been troublesome implementing the NotNull validator
784
+ # relation[:cols].each do |col, datatype|
785
+ # if (datatype[3] && _brick_primary_key.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
786
+ # ::Brick.config.not_nullables.include?("#{matching}.#{col}")
787
+ # code << " validates :#{col}, not_null: true\n"
788
+ # self.send(:validates, col.to_sym, { not_null: true })
789
+ # end
790
+ # end
791
+ end
792
+ end # class definition
793
+ # Having this separate -- will this now work out better?
794
+ built_model.class_exec do
623
795
  hmts.each do |hmt_fk, fks|
796
+ hmt_fk = hmt_fk.tr('.', '_')
624
797
  fks.each do |fk|
625
798
  through = fk.first[:assoc_name]
626
799
  hmt_name = if fks.length > 1
@@ -641,18 +814,8 @@ class Object
641
814
  self.send(:has_many, hmt_name.to_sym, **options)
642
815
  end
643
816
  end
644
- # # Not NULLables
645
- # # %%% For the minute we've had to pull this out because it's been troublesome implementing the NotNull validator
646
- # relation[:cols].each do |col, datatype|
647
- # if (datatype[3] && ar_pks.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
648
- # ::Brick.config.not_nullables.include?("#{matching}.#{col}")
649
- # code << " validates :#{col}, not_null: true\n"
650
- # self.send(:validates, col.to_sym, { not_null: true })
651
- # end
652
- # end
653
817
  end
654
- code << "end # model #{model_name}\n\n"
655
- end # class definition
818
+ code << "end # model #{full_name}\n\n"
656
819
  [built_model, code]
657
820
  end
658
821
 
@@ -663,7 +826,7 @@ class Object
663
826
  # Try to take care of screwy names if this is a belongs_to going to an STI subclass
664
827
  assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
665
828
  sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
666
- a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
829
+ a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] == a.foreign_key
667
830
  end
668
831
  sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
669
832
  else
@@ -695,7 +858,7 @@ class Object
695
858
  if assoc.key?(:polymorphic)
696
859
  options[:as] = assoc[:fk].to_sym
697
860
  else
698
- need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
861
+ need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table].split('.').last)}_id" != assoc[:fk]
699
862
  end
700
863
  # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
701
864
  if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
@@ -712,7 +875,7 @@ class Object
712
875
  end
713
876
  # Figure out if we need to specially call out the class_name and/or foreign key
714
877
  # (and if either of those then definitely also a specific inverse_of)
715
- options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
878
+ options[:class_name] = "::#{assoc[:primary_class]&.name || singular_table_name.split('.').map(&:camelize).join('::')}" if need_class_name
716
879
  # Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
717
880
  if need_fk # Funky foreign key?
718
881
  options[:foreign_key] = if assoc[:fk].is_a?(Array)
@@ -722,10 +885,11 @@ class Object
722
885
  assoc[:fk].to_sym
723
886
  end
724
887
  end
725
- options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
888
+ options[:inverse_of] = inverse_assoc_name.tr('.', '_').to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
726
889
 
727
890
  # Prepare a list of entries for "has_many :through"
728
891
  if macro == :has_many
892
+ puts [inverse_table, relations[inverse_table].length].inspect
729
893
  relations[inverse_table][:hmt_fks].each do |k, hmt_fk|
730
894
  next if k == assoc[:fk]
731
895
 
@@ -733,21 +897,23 @@ class Object
733
897
  end
734
898
  end
735
899
  # And finally create a has_one, has_many, or belongs_to for this association
736
- assoc_name = assoc_name.to_sym
900
+ assoc_name = assoc_name.tr('.', '_').to_sym
737
901
  code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
738
902
  self.send(macro, assoc_name, **options)
739
903
  end
740
904
 
741
- def build_controller(class_name, plural_class_name, model, relations)
905
+ def build_controller(namespace, class_name, plural_class_name, model, relations)
742
906
  table_name = ActiveSupport::Inflector.underscore(plural_class_name)
743
907
  singular_table_name = ActiveSupport::Inflector.singularize(table_name)
908
+ pk = model._brick_primary_key(relations.fetch(table_name, nil))
744
909
 
745
- code = +"class #{class_name} < ApplicationController\n"
910
+ namespace_name = "#{namespace.name}::" if namespace
911
+ code = +"class #{namespace_name}#{class_name} < ApplicationController\n"
746
912
  built_controller = Class.new(ActionController::Base) do |new_controller_class|
747
- Object.const_set(class_name.to_sym, new_controller_class)
913
+ (namespace || Object).const_set(class_name.to_sym, new_controller_class)
748
914
 
749
915
  code << " def index\n"
750
- code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect})" : '.all'}\n"
916
+ code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{pk.inspect})" : '.all'}\n"
751
917
  code << " @#{table_name}.brick_select(params)\n"
752
918
  code << " end\n"
753
919
  self.protect_from_forgery unless: -> { self.request.format.js? }
@@ -765,14 +931,16 @@ class Object
765
931
  return
766
932
  end
767
933
 
768
- ar_relation = model.primary_key ? model.order("#{model.table_name}.#{model.primary_key}") : model.all
934
+ order = pk.each_with_object([]) { |pk_part, s| s << "#{model.table_name}.#{pk_part}" }
935
+ ar_relation = order.present? ? model.order("#{order.join(', ')}") : model.all
769
936
  @_brick_params = ar_relation.brick_select(params, (selects = []), (bt_descrip = {}), (hm_counts = {}), (join_array = ::Brick::JoinArray.new))
770
937
  # %%% Add custom HM count columns
771
938
  # %%% What happens when the PK is composite?
772
939
  counts = hm_counts.each_with_object([]) { |v, s| s << "_br_#{v.first}._ct_ AS _br_#{v.first}_ct" }
773
- # *selects,
774
940
  instance_variable_set("@#{table_name}".to_sym, ar_relation.dup._select!(*selects, *counts))
775
- # binding.pry
941
+ if namespace && (idx = lookup_context.prefixes.index(table_name))
942
+ lookup_context.prefixes[idx] = "#{namespace.name.underscore}/#{lookup_context.prefixes[idx]}"
943
+ end
776
944
  @_brick_bt_descrip = bt_descrip
777
945
  @_brick_hm_counts = hm_counts
778
946
  @_brick_join_array = join_array
@@ -797,6 +965,10 @@ class Object
797
965
  code << " # (Define :new, :create)\n"
798
966
 
799
967
  if model.primary_key
968
+ # if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.include?(schema)
969
+ # ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
970
+ # end
971
+
800
972
  is_need_params = true
801
973
  # code << " # (Define :edit, and :destroy)\n"
802
974
  code << " def update\n"
@@ -806,7 +978,6 @@ class Object
806
978
  code << " end\n"
807
979
  self.define_method :update do
808
980
  ::Brick.set_db_schema(params)
809
-
810
981
  if request.format == :csv # Importing CSV?
811
982
  require 'csv'
812
983
  # See if internally it's likely a TSV file (tab-separated)
@@ -830,7 +1001,7 @@ class Object
830
1001
 
831
1002
  if is_need_params
832
1003
  code << "private\n"
833
- code << " def params\n"
1004
+ code << " def #{params_name}\n"
834
1005
  code << " params.require(:#{singular_table_name}).permit(#{model.columns_hash.keys.map { |c| c.to_sym.inspect }.join(', ')})\n"
835
1006
  code << " end\n"
836
1007
  self.define_method(params_name) do
@@ -840,7 +1011,7 @@ class Object
840
1011
  # Get column names for params from relations[model.table_name][:cols].keys
841
1012
  end
842
1013
  end
843
- code << "end # #{class_name}\n\n"
1014
+ code << "end # #{namespace_name}#{class_name}\n\n"
844
1015
  end # class definition
845
1016
  [built_controller, code]
846
1017
  end
@@ -850,7 +1021,8 @@ class Object
850
1021
  plural = ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])
851
1022
  [hm_assoc[:alternate_name] == name.underscore ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural, true]
852
1023
  else
853
- [ActiveSupport::Inflector.pluralize(hm_assoc[:inverse_table]), nil]
1024
+ assoc_name = hm_assoc[:inverse_table].pluralize
1025
+ [assoc_name, assoc_name.include?('.')]
854
1026
  end
855
1027
  end
856
1028
  end
@@ -870,16 +1042,19 @@ module ActiveRecord::ConnectionHandling
870
1042
 
871
1043
  def _brick_reflect_tables
872
1044
  if (relations = ::Brick.relations).empty?
873
- # Only for Postgres? (Doesn't work in sqlite3)
874
- # puts ActiveRecord::Base.connection.execute("SELECT current_setting('SEARCH_PATH')").to_a.inspect
1045
+ load Rails.root.join('config/initializers/brick.rb') # Hopefully our initializer is named exactly this!
1046
+ # Only for Postgres? (Doesn't work in sqlite3)
1047
+ # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
875
1048
 
876
- schema_sql = 'SELECT NULL AS table_schema;'
877
- case ActiveRecord::Base.connection.adapter_name
1049
+ schema_sql = 'SELECT NULL AS table_schema;'
1050
+ case ActiveRecord::Base.connection.adapter_name
878
1051
  when 'PostgreSQL'
879
- schema = 'public'
1052
+ if (::Brick.default_schema = schema = ::Brick.config.schema_behavior&.[](:multitenant)&.[](:schema_to_analyse))
1053
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1054
+ end
880
1055
  schema_sql = 'SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;'
881
1056
  when 'Mysql2'
882
- schema = ActiveRecord::Base.connection.current_database
1057
+ ::Brick.default_schema = schema = ActiveRecord::Base.connection.current_database
883
1058
  when 'SQLite'
884
1059
  sql = "SELECT m.name AS relation_name, UPPER(m.type) AS table_type,
885
1060
  p.name AS column_name, p.type AS data_type,
@@ -892,8 +1067,7 @@ module ActiveRecord::ConnectionHandling
892
1067
  puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
893
1068
  end
894
1069
 
895
- sql ||= ActiveRecord::Base.send(:sanitize_sql_array, [
896
- "SELECT t.table_name AS relation_name, t.table_type,
1070
+ sql ||= "SELECT t.table_schema AS schema, t.table_name AS relation_name, t.table_type,
897
1071
  c.column_name, c.data_type,
898
1072
  COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
899
1073
  tc.constraint_type AS const, kcu.constraint_name AS \"key\",
@@ -911,18 +1085,23 @@ module ActiveRecord::ConnectionHandling
911
1085
  ON kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
912
1086
  AND kcu.TABLE_NAME = tc.TABLE_NAME
913
1087
  AND kcu.CONSTRAINT_NAME = tc.constraint_name
914
- WHERE t.table_schema = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')
1088
+ WHERE t.table_schema NOT IN ('information_schema', 'pg_catalog')#{"
1089
+ AND t.table_schema = COALESCE(current_setting('SEARCH_PATH'), 'public')" if schema }
915
1090
  -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
916
1091
  AND t.table_name NOT IN ('pg_stat_statements', 'ar_internal_metadata', 'schema_migrations')
917
- ORDER BY 1, t.table_type DESC, c.ordinal_position", schema
918
- ])
919
-
1092
+ ORDER BY 1, t.table_type DESC, c.ordinal_position"
920
1093
  measures = []
921
1094
  case ActiveRecord::Base.connection.adapter_name
922
1095
  when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
923
- ActiveRecord::Base.connection.execute(sql).each do |r|
1096
+ schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1097
+ ActiveRecord::Base.execute_sql(sql).each do |r|
924
1098
  # next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
925
- relation = relations[(relation_name = r['relation_name'])]
1099
+ relation_name = if r['schema'] != schema
1100
+ "#{schema_name = r['schema']}.#{r['relation_name']}"
1101
+ else
1102
+ r['relation_name']
1103
+ end
1104
+ relation = relations[relation_name]
926
1105
  relation[:isView] = true if r['table_type'] == 'VIEW'
927
1106
  col_name = r['column_name']
928
1107
  key = case r['const']
@@ -939,7 +1118,7 @@ module ActiveRecord::ConnectionHandling
939
1118
  # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
940
1119
  end
941
1120
  else # MySQL2 acts a little differently, bringing back an array for each row
942
- ActiveRecord::Base.connection.execute(sql).each do |r|
1121
+ ActiveRecord::Base.execute_sql(sql).each do |r|
943
1122
  # next if internal_views.include?(r['relation_name']) # Skip internal views such as v_all_assessments
944
1123
  relation = relations[(relation_name = r[0])] # here relation represents a table or view from the database
945
1124
  relation[:isView] = true if r[1] == 'VIEW' # table_type
@@ -979,11 +1158,11 @@ module ActiveRecord::ConnectionHandling
979
1158
  # end
980
1159
  # end
981
1160
  # end
982
-
1161
+ schema = ::Brick.default_schema # Reset back for this next round of fun
983
1162
  case ActiveRecord::Base.connection.adapter_name
984
1163
  when 'PostgreSQL', 'Mysql2'
985
- sql = ActiveRecord::Base.send(:sanitize_sql_array, [
986
- "SELECT kcu1.TABLE_NAME, kcu1.COLUMN_NAME, kcu2.TABLE_NAME AS primary_table, kcu1.CONSTRAINT_NAME
1164
+ sql = "SELECT kcu1.CONSTRAINT_SCHEMA, kcu1.TABLE_NAME, kcu1.COLUMN_NAME,
1165
+ kcu2.CONSTRAINT_SCHEMA AS primary_schema, kcu2.TABLE_NAME AS primary_table, kcu1.CONSTRAINT_NAME AS CONSTRAINT_SCHEMA_FK
987
1166
  FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc
988
1167
  INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
989
1168
  ON kcu1.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG
@@ -993,10 +1172,9 @@ module ActiveRecord::ConnectionHandling
993
1172
  ON kcu2.CONSTRAINT_CATALOG = rc.UNIQUE_CONSTRAINT_CATALOG
994
1173
  AND kcu2.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
995
1174
  AND kcu2.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
996
- AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION
997
- WHERE kcu1.CONSTRAINT_SCHEMA = ? -- COALESCE(current_setting('SEARCH_PATH'), 'public')", schema
1175
+ AND kcu2.ORDINAL_POSITION = kcu1.ORDINAL_POSITION#{"
1176
+ WHERE kcu1.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if schema }"
998
1177
  # AND kcu2.TABLE_NAME = ?;", Apartment::Tenant.current, table_name
999
- ])
1000
1178
  when 'SQLite'
1001
1179
  sql = "SELECT m.name, fkl.\"from\", fkl.\"table\", m.name || '_' || fkl.\"from\" AS constraint_name
1002
1180
  FROM sqlite_master m
@@ -1005,11 +1183,18 @@ module ActiveRecord::ConnectionHandling
1005
1183
  else
1006
1184
  end
1007
1185
  if sql
1008
- ::Brick.db_schemas = ActiveRecord::Base.connection.execute(schema_sql)
1009
- ::Brick.db_schemas = ::Brick.db_schemas.to_a unless ::Brick.db_schemas.is_a?(Array)
1010
- ::Brick.db_schemas.map! { |row| row['table_schema'] } unless ::Brick.db_schemas.empty? || ::Brick.db_schemas.first.is_a?(String)
1011
- ::Brick.db_schemas -= ['information_schema', 'pg_catalog']
1012
- ActiveRecord::Base.connection.execute(sql).each do |fk|
1186
+ ::Brick.default_schema ||= schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1187
+ unless (db_schemas = ActiveRecord::Base.execute_sql(schema_sql)).is_a?(Array)
1188
+ db_schemas = db_schemas.to_a
1189
+ end
1190
+ unless db_schemas.empty?
1191
+ ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1192
+ row = row.is_a?(String) ? row : row['table_schema']
1193
+ # Remove whatever default schema we're using and other system schemas
1194
+ s[row] = nil unless ['information_schema', 'pg_catalog', schema].include?(row)
1195
+ end
1196
+ end
1197
+ ActiveRecord::Base.execute_sql(sql).each do |fk|
1013
1198
  fk = fk.values unless fk.is_a?(Array)
1014
1199
  ::Brick._add_bt_and_hm(fk, relations)
1015
1200
  end
@@ -1055,34 +1240,46 @@ module Brick
1055
1240
 
1056
1241
  class << self
1057
1242
  def _add_bt_and_hm(fk, relations, is_polymorphic = false)
1058
- bt_assoc_name = fk[1].underscore
1059
- bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
1060
-
1061
- bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
1243
+ if (bt_assoc_name = fk[2].underscore).end_with?('_id')
1244
+ bt_assoc_name = bt_assoc_name[0..-4]
1245
+ elsif bt_assoc_name.end_with?('id') && bt_assoc_name.exclude?('_') # Make the bold assumption that we can just peel off the final ID part
1246
+ bt_assoc_name = bt_assoc_name[0..-3]
1247
+ else
1248
+ bt_assoc_name = "#{bt_assoc_name}_bt"
1249
+ end
1250
+ # %%% Temporary schema patch
1251
+ fk[1] = "#{fk[0]}.#{for_tbl = fk[1]}" if fk[0] && fk[0] != ::Brick.default_schema
1252
+ for_tbl << '_' if for_tbl
1253
+ bts = (relation = relations.fetch(fk[1], nil))&.fetch(:fks) { relation[:fks] = {} }
1062
1254
  # %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
1063
1255
  # Maybe it's already gotten this info because we got as far as to say there was a unique class
1064
- primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
1256
+ primary_table = if (is_class = fk[4].is_a?(Hash) && fk[4].key?(:class))
1257
+ pri_tbl = (primary_class = fk[4][:class].constantize).table_name
1258
+ else
1259
+ pri_tbl = fk[4]
1260
+ (fk[3] && fk[3] != ::Brick.default_schema) ? "#{fk[3]}.#{pri_tbl}" : pri_tbl
1261
+ end
1065
1262
  hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
1066
1263
 
1067
- unless (cnstr_name = fk[3])
1264
+ unless (cnstr_name = fk[5])
1068
1265
  # For any appended references (those that come from config), arrive upon a definitely unique constraint name
1069
- cnstr_base = cnstr_name = "(brick) #{fk[0]}_#{is_class ? fk[2][:class].underscore : fk[2]}"
1266
+ cnstr_base = cnstr_name = "(brick) #{for_tbl}#{is_class ? fk[4][:class].underscore : pri_tbl}"
1070
1267
  cnstr_added_num = 1
1071
1268
  cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
1072
1269
  missing = []
1073
- missing << fk[0] unless relations.key?(fk[0])
1270
+ missing << fk[1] unless relations.key?(fk[1])
1074
1271
  missing << primary_table unless is_class || relations.key?(primary_table)
1075
1272
  unless missing.empty?
1076
1273
  tables = relations.reject { |_k, v| v.fetch(:isView, nil) }.keys.sort
1077
1274
  puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
1078
1275
  return
1079
1276
  end
1080
- unless (cols = relations[fk[0]][:cols]).key?(fk[1]) || (is_polymorphic && cols.key?("#{fk[1]}_id") && cols.key?("#{fk[1]}_type"))
1277
+ unless (cols = relations[fk[1]][:cols]).key?(fk[2]) || (is_polymorphic && cols.key?("#{fk[2]}_id") && cols.key?("#{fk[2]}_type"))
1081
1278
  columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
1082
- puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
1279
+ puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[2]}. (Columns present in #{fk[1]} are #{columns.join(', ')}.)"
1083
1280
  return
1084
1281
  end
1085
- if (redundant = bts.find { |_k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[0] && v[:fk] == fk[1] && v[:inverse_table] == primary_table })
1282
+ if (redundant = bts.find { |_k, v| v[:inverse]&.fetch(:inverse_table, nil) == fk[1] && v[:fk] == fk[2] && v[:inverse_table] == primary_table })
1086
1283
  if is_class && !redundant.last.key?(:class)
1087
1284
  redundant.last[:primary_class] = primary_class # Round out this BT so it can find the proper :source for a HMT association that references an STI subclass
1088
1285
  else
@@ -1094,18 +1291,18 @@ module Brick
1094
1291
  if (assoc_bt = bts[cnstr_name])
1095
1292
  if is_polymorphic
1096
1293
  # Assuming same fk (don't yet support composite keys for polymorphics)
1097
- assoc_bt[:inverse_table] << fk[2]
1294
+ assoc_bt[:inverse_table] << fk[4]
1098
1295
  else # Expect we could have a composite key going
1099
1296
  if assoc_bt[:fk].is_a?(String)
1100
- assoc_bt[:fk] = [assoc_bt[:fk], fk[1]] unless fk[1] == assoc_bt[:fk]
1101
- elsif assoc_bt[:fk].exclude?(fk[1])
1102
- assoc_bt[:fk] << fk[1]
1297
+ assoc_bt[:fk] = [assoc_bt[:fk], fk[2]] unless fk[2] == assoc_bt[:fk]
1298
+ elsif assoc_bt[:fk].exclude?(fk[2])
1299
+ assoc_bt[:fk] << fk[2]
1103
1300
  end
1104
- assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
1301
+ assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[2]}"
1105
1302
  end
1106
1303
  else
1107
1304
  inverse_table = [primary_table] if is_polymorphic
1108
- assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
1305
+ assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[2], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
1109
1306
  assoc_bt[:polymorphic] = true if is_polymorphic
1110
1307
  end
1111
1308
  if is_class
@@ -1115,21 +1312,21 @@ module Brick
1115
1312
  # assoc_bt[:inverse_of] = primary_class.reflect_on_all_associations.find { |a| a.foreign_key == bt[1] }
1116
1313
  end
1117
1314
 
1118
- return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
1315
+ return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[1] == exclusion[0] && fk[2] == exclusion[1] && primary_table == exclusion[2] } || hms.nil?
1119
1316
 
1120
1317
  if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
1121
- if assoc_bt[:fk].is_a?(String)
1122
- assoc_bt[:fk] = [assoc_bt[:fk], fk[1]] unless fk[1] == assoc_bt[:fk]
1123
- elsif assoc_bt[:fk].exclude?(fk[1])
1124
- assoc_bt[:fk] << fk[1]
1318
+ if assoc_hm[:fk].is_a?(String)
1319
+ assoc_hm[:fk] = [assoc_hm[:fk], fk[2]] unless fk[2] == assoc_hm[:fk]
1320
+ elsif assoc_hm[:fk].exclude?(fk[2])
1321
+ assoc_hm[:fk] << fk[2]
1125
1322
  end
1126
1323
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
1127
1324
  assoc_hm[:inverse] = assoc_bt
1128
1325
  else
1129
- assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
1326
+ assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[2], assoc_name: fk[1].tr('.', '_').pluralize, alternate_name: bt_assoc_name, inverse_table: fk[1], inverse: assoc_bt }
1130
1327
  assoc_hm[:polymorphic] = true if is_polymorphic
1131
1328
  hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
1132
- hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
1329
+ hm_counts[fk[1]] = hm_counts.fetch(fk[1]) { 0 } + 1
1133
1330
  end
1134
1331
  assoc_bt[:inverse] = assoc_hm
1135
1332
  end