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.
- checksums.yaml +4 -4
- data/lib/brick/config.rb +24 -4
- data/lib/brick/extensions.rb +357 -160
- data/lib/brick/frameworks/rails/engine.rb +71 -38
- data/lib/brick/join_array.rb +1 -1
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +36 -14
- data/lib/generators/brick/install_generator.rb +15 -9
- metadata +2 -2
data/lib/brick/extensions.rb
CHANGED
@@ -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
|
-
|
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)
|
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
|
-
|
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}
|
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}
|
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
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
return
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
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
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
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
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
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
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
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[
|
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
|
-
|
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 =
|
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: #{
|
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
|
-
|
618
|
-
|
619
|
-
|
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 #{
|
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]
|
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
|
-
|
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}#{
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
874
|
-
|
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
|
-
|
877
|
-
|
1049
|
+
schema_sql = 'SELECT NULL AS table_schema;'
|
1050
|
+
case ActiveRecord::Base.connection.adapter_name
|
878
1051
|
when 'PostgreSQL'
|
879
|
-
schema =
|
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 ||=
|
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
|
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"
|
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.
|
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
|
-
|
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.
|
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 =
|
986
|
-
|
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
|
-
|
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.
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
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[
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
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[
|
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[
|
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) #{
|
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[
|
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[
|
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[
|
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[
|
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[
|
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[
|
1101
|
-
elsif assoc_bt[:fk].exclude?(fk[
|
1102
|
-
assoc_bt[:fk] << fk[
|
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[
|
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[
|
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[
|
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
|
1122
|
-
|
1123
|
-
elsif
|
1124
|
-
|
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[
|
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[
|
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
|