brick 1.0.199 → 1.0.201

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.
@@ -334,10 +334,10 @@ module ActiveRecord
334
334
  end
335
335
 
336
336
  # Providing a relation object allows auto-modules built from table name prefixes to work
337
- def self._brick_index(mode = nil, separator = nil, relation = nil)
337
+ def self._brick_index(mode = nil, separator = nil, relation = nil, not_path = nil)
338
338
  return if abstract_class?
339
339
 
340
- ::Brick._brick_index(table_name, mode, separator, relation)
340
+ ::Brick._brick_index(table_name, mode, separator, relation, not_path)
341
341
  end
342
342
 
343
343
  def self.brick_import_template
@@ -436,7 +436,11 @@ module ActiveRecord
436
436
  order_by = ordering&.each_with_object([]) do |ord_part, s| # %%% If a term is also used as an eqi-condition in the WHERE clause, it can be omitted from ORDER BY
437
437
  case ord_part
438
438
  when String
439
- ord_expr = _br_quoted_name(ord_part.gsub('^^^', table_name))
439
+ ord_expr = if ord_part.index('(') # Any kind of SQL function at play here?
440
+ ord_part.gsub('^^^', _br_quoted_name(table_name))
441
+ else
442
+ _br_quoted_name(ord_part.gsub('^^^', table_name))
443
+ end
440
444
  s << Arel.sql(ord_expr)
441
445
  order_by_txt&.<<(ord_expr.index('.') ? "Arel.sql(#{ord_expr.inspect})" : ord_part.inspect)
442
446
  else # Expecting only Symbol
@@ -887,12 +891,16 @@ module ActiveRecord
887
891
 
888
892
  tbl_alias = unique63("b_r_#{hm.name}", previous)
889
893
  on_clause = []
890
- hm_selects = if fk_col.is_a?(Array) # Composite key?
891
- fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{tbl_alias}.#{fk_col_part} = #{pri_tbl.table_name}.#{pri_key[idx]}" }
892
- fk_col.dup
893
- else
894
+ hm_selects = if !pri_key.is_a?(Array) # Probable standard key?
895
+ if fk_col.is_a?(Array) # Foreign is composite but not Primary? OK, or choose the first part of the foreign key if nothing else
896
+ fk_col = fk_col.find { |col_name| col_name == pri_key } || # Try to associate with the same-named part of the foreign key ...
897
+ fk_col.first # ... and if no good match, just choose the first part
898
+ end
894
899
  on_clause << "#{_br_quoted_name("#{tbl_alias}.#{fk_col}")} = #{_br_quoted_name("#{pri_tbl.table_name}.#{pri_key}")}"
895
900
  [fk_col]
901
+ else # Composite key
902
+ fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{tbl_alias}.#{fk_col_part} = #{pri_tbl.table_name}.#{pri_key[idx]}" }
903
+ fk_col.dup
896
904
  end
897
905
  if poly_type
898
906
  hm_selects << poly_type
@@ -1166,6 +1174,34 @@ Might want to add this in your brick.rb:
1166
1174
  end
1167
1175
  end
1168
1176
 
1177
+ if ::Brick.enable_routes? && Object.const_defined?('ActionDispatch')
1178
+ require 'brick/route_mapper'
1179
+ ActionDispatch::Routing::RouteSet.class_exec do
1180
+ # In order to defer auto-creation of any routes that already exist, calculate Brick routes only after having loaded all others
1181
+ prepend ::Brick::RouteSet
1182
+ end
1183
+ ActionDispatch::Routing::Mapper.class_exec do
1184
+ include ::Brick::RouteMapper
1185
+ end
1186
+
1187
+ # Do the root route before the Rails Welcome one would otherwise take precedence
1188
+ if (route = ::Brick.config.default_route_fallback).present?
1189
+ action = "#{route}#{'#index' unless route.index('#')}"
1190
+ if ::Brick.config.path_prefix
1191
+ ::Rails.application.routes.append do
1192
+ send(:namespace, ::Brick.config.path_prefix) do
1193
+ send(:root, action)
1194
+ end
1195
+ end
1196
+ elsif ::Rails.application.routes.named_routes.send(:routes)[:root].nil?
1197
+ ::Rails.application.routes.append do
1198
+ send(:root, action)
1199
+ end
1200
+ end
1201
+ ::Brick.established_drf = "/#{::Brick.config.path_prefix}#{action[action.index('#')..-1]}"
1202
+ end
1203
+ end
1204
+
1169
1205
  if Object.const_defined?('ActionView')
1170
1206
  require 'brick/frameworks/rails/form_tags'
1171
1207
  require 'brick/frameworks/rails/form_builder'
@@ -1298,19 +1334,23 @@ end
1298
1334
  unless (is_tnp_module = (is_brick_prefix && !is_controller && ::Brick.config.table_name_prefixes.values.include?(requested)))
1299
1335
  # ... first look around for an existing module or class.
1300
1336
  desired_classname = (self == Object || !name) ? requested : "#{name}::#{requested}"
1301
- if ((is_defined = self.const_defined?(args.first)) && (possible = self.const_get(args.first)) &&
1337
+ if (self.const_defined?(args.first) && (possible = self.const_get(args.first)) &&
1302
1338
  # Reset `possible` if it's a controller request that's not a perfect match
1303
1339
  # Was: (possible = nil) but changed to #local_variable_set in order to suppress the "= should be ==" warning
1304
1340
  (possible&.name == desired_classname || (is_controller && binding.local_variable_set(:possible, nil)))) ||
1341
+
1305
1342
  # Try to require the respective Ruby file
1343
+ # ((filename = ActiveSupport::Dependencies.search_for_file(desired_classname.underscore)) &&
1344
+ # (require_dependency(filename) || true) &&
1306
1345
  ((filename = ActiveSupport::Dependencies.search_for_file(desired_classname.underscore) ||
1307
1346
  (self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = requested).underscore))
1308
1347
  ) && (require_dependency(filename) || true) &&
1309
- ((possible = self.const_get(args.first)) && possible.name == desired_classname)
1348
+ (!anonymous? && (possible = self.const_get(args.first)) && possible.name == desired_classname)
1310
1349
  ) ||
1350
+
1311
1351
  # If any class has turned up so far (and we're not in the middle of eager loading)
1312
1352
  # then return what we've found.
1313
- (is_defined && !::Brick.is_eager_loading) # Used to also have: && possible != self
1353
+ (possible&.module_parent == base_module && !::Brick.is_eager_loading) # Used to also have: && possible != self
1314
1354
  if ((!brick_root && (filename || possible.instance_of?(Class))) ||
1315
1355
  (possible.instance_of?(Module) && possible&.module_parent == self) ||
1316
1356
  (possible.instance_of?(Class) && possible == self)) && # Are we simply searching for ourselves?
@@ -1326,13 +1366,14 @@ end
1326
1366
  end
1327
1367
  end
1328
1368
  class_name = ::Brick.namify(requested)
1369
+ is_avo_present = Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace)
1329
1370
  # CONTROLLER
1330
1371
  result = if ::Brick.enable_controllers? &&
1331
1372
  is_controller && (plural_class_name = class_name[0..-11]).length.positive?
1332
1373
  # Otherwise now it's up to us to fill in the gaps
1333
1374
  controller_class_name = +''
1334
1375
  full_class_name = +''
1335
- unless self == Object
1376
+ unless self == Object || (is_avo_present && self.name == 'Avo')
1336
1377
  controller_class_name << ((split_self_name&.first && split_self_name.join('::')) || self.name)
1337
1378
  full_class_name << "::#{controller_class_name}"
1338
1379
  controller_class_name << '::'
@@ -1342,13 +1383,14 @@ end
1342
1383
  singular_class_name = ::Brick.namify(plural_class_name, :underscore).singularize.camelize
1343
1384
  full_class_name << "::#{singular_class_name}"
1344
1385
  skip_controller = nil
1345
- if plural_class_name == 'BrickOpenapi' ||
1346
- (
1347
- (::Brick.config.add_status || ::Brick.config.add_orphans) &&
1348
- plural_class_name == 'BrickGem'
1349
- ) ||
1350
- begin
1351
- model = self.const_get(full_class_name)
1386
+ begin
1387
+ if plural_class_name == 'BrickOpenapi' ||
1388
+ (
1389
+ (::Brick.config.add_status || ::Brick.config.add_orphans) &&
1390
+ plural_class_name == 'BrickGem'
1391
+ # Was: ) || (model = self.const_get(full_class_name))
1392
+ ) || (model = Object.const_get(full_class_name))
1393
+ # puts "#{self.name} - #{full_class_name}"
1352
1394
 
1353
1395
  # In the very rare case that we've picked up a MODULE which has the same name as what would be the
1354
1396
  # resource's MODEL name, just build out an appropriate auto-model on-the-fly. (RailsDevs code has this in PayCustomer.)
@@ -1356,10 +1398,10 @@ end
1356
1398
  if model && !model.is_a?(Class)
1357
1399
  model, _code = Object.send(:build_model, relations, model.module_parent, model.module_parent.name, singular_class_name)
1358
1400
  end
1359
- rescue NameError # If the const_get for the model has failed...
1360
- skip_controller = true
1361
- # ... then just fall through and allow it to fail when trying to load the ____Controller class normally.
1362
1401
  end
1402
+ rescue NameError # If the const_get for the model has failed...
1403
+ skip_controller = true
1404
+ # ... then just fall through and allow it to fail when trying to load the ____Controller class normally.
1363
1405
  end
1364
1406
  unless skip_controller
1365
1407
  Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
@@ -1391,58 +1433,43 @@ end
1391
1433
  base_module.const_set(class_name.to_sym, (built_module = Module.new))
1392
1434
  [built_module, "module #{possible_module}; end\n"]
1393
1435
 
1394
- # AVO Resource
1395
- elsif base_module == Object && Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace) && requested.end_with?('Resource') &&
1436
+ # AVO 2.x Resource
1437
+ elsif base_module == Object && is_avo_present && requested.end_with?('Resource') &&
1396
1438
  # Expect that anything called MotorResource or SpinaResource could be from those administrative gems
1397
- requested.length > 8 && ['MotorResource', 'SpinaResource'].exclude?(requested)
1398
- if (model = Object.const_get(requested[0..-9])) && model < ActiveRecord::Base
1399
- require 'generators/avo/resource_generator'
1400
- field_generator = Generators::Avo::ResourceGenerator.new([''])
1401
- field_generator.instance_variable_set(:@model, model)
1402
- fields = field_generator.send(:generate_fields)&.split("\n")
1403
- &.each_with_object([]) do |f, s|
1404
- if (f = f.strip).start_with?('field ')
1405
- f = f[6..-1].split(',')
1406
- s << [f.first[1..-1].to_sym, [f[1][1..-1].split(': :').map(&:to_sym)].to_h]
1407
- end
1408
- end || []
1409
- built_resource = Class.new(Avo::BaseResource) do |new_resource_class|
1410
- self.model_class = model
1411
- self.title = :brick_descrip
1412
- self.includes = []
1413
- if (!model.is_view? && mod_pk = model.primary_key)
1414
- field((mod_pk.is_a?(Array) ? mod_pk.first : mod_pk).to_sym, { as: :id })
1415
- end
1416
- # Create a call such as: field :name, as: :text
1417
- fields.each do |f|
1418
- # Add proper types if this is a polymorphic belongs_to
1419
- if f.last == { as: :belongs_to } &&
1420
- (fk = ::Brick.relations[model.table_name][:fks].find { |k, v| v[:assoc_name] == f.first.to_s }) &&
1421
- fk.last.fetch(:polymorphic, nil)
1422
- poly_types = fk.last.fetch(:inverse_table, nil)&.each_with_object([]) do |poly_table, s|
1423
- s << Object.const_get(::Brick.relations[poly_table][:class_name])
1424
- end
1425
- if poly_types.present?
1426
- f.last[:polymorphic_as] = f.first
1427
- f.last[:types] = poly_types
1428
- end
1429
- end
1430
- self.send(:field, *f)
1431
- end
1432
- end
1433
- Object.const_set(requested.to_sym, built_resource)
1434
- [built_resource, nil]
1439
+ requested.length > 8 && ['MotorResource', 'SpinaResource'].exclude?(requested) &&
1440
+ (model = Object.const_get(requested[0..-9])) && model < ActiveRecord::Base
1441
+ built_resource = Class.new(Avo::BaseResource) do
1442
+ self.model_class = model
1443
+ self.title = :brick_descrip
1444
+ self.includes = []
1445
+ ::Brick::ADD_AVO_FIELDS.call(self, model)
1435
1446
  end
1447
+ base_module.const_set(requested.to_sym, built_resource)
1448
+ [built_resource, nil]
1449
+
1450
+ # AVO 3.x Resource
1451
+ elsif is_avo_present && self.name == 'Avo::Resources' &&
1452
+ (model = begin
1453
+ (model = Object.const_get(requested)) && model < ActiveRecord::Base
1454
+ model
1455
+ rescue
1456
+ end)
1457
+ [::Brick.avo_3x_resource(model, requested), nil]
1436
1458
 
1437
1459
  # MODEL
1438
1460
  elsif ::Brick.enable_models?
1461
+ # Avo sometimes tries to find a model class inside of the Avo namespace
1462
+ if is_avo_present && self.name == 'Avo'
1463
+ name = (base_module = Object).name
1464
+ end
1465
+ name ||= base_module.name
1439
1466
  # Custom inheritable Brick base model?
1440
1467
  class_name = (inheritable_name = class_name)[5..-1] if class_name.start_with?('Brick')
1441
1468
  Object.send(:build_model, relations, base_module, name, class_name, inheritable_name)
1442
1469
  end
1443
1470
  if result
1444
1471
  built_class, code = result
1445
- puts "\n#{code}\n"
1472
+ puts "\n#{code}\n" if code
1446
1473
  built_class
1447
1474
  elsif !schema_name && ::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}")
1448
1475
  # module_prefixes = type_name.split('::')
@@ -1452,7 +1479,7 @@ end
1452
1479
  base_module._brick_const_missing(*args)
1453
1480
  # elsif base_module != Object
1454
1481
  # module_parent.const_missing(*args)
1455
- elsif Rails.respond_to?(:autoloaders) && # After finding nothing else, if Zeitwerk is enabled ...
1482
+ elsif Object.const_defined?('Rails') && ::Rails.respond_to?(:autoloaders) && # After finding nothing else, if Zeitwerk is enabled ...
1456
1483
  (Rails::Autoloaders.respond_to?(:zeitwerk_enabled?) ? Rails::Autoloaders.zeitwerk_enabled? : true)
1457
1484
  self._brick_const_missing(*args) # ... rely solely on Zeitwerk.
1458
1485
  else # Classic mode
@@ -1471,13 +1498,63 @@ end
1471
1498
  end
1472
1499
  end
1473
1500
 
1501
+ module Brick
1502
+ def self.avo_3x_resource(model, requested)
1503
+ built_resource = Class.new(Avo::BaseResource) do
1504
+ self.model_class = model
1505
+ self.title = :brick_descrip
1506
+ self.includes = []
1507
+ define_method :fields do # Have to be inside of a fields method
1508
+ ::Brick::ADD_AVO_FIELDS.call(self, model)
1509
+ end
1510
+ end
1511
+ ::Avo::BaseResource.const_set(requested.to_sym, built_resource)
1512
+ built_resource
1513
+ end
1514
+ end
1515
+
1516
+ ::Brick::ADD_AVO_FIELDS = lambda do |obj, model|
1517
+ require 'generators/avo/resource_generator'
1518
+ field_generator = Generators::Avo::ResourceGenerator.new([''])
1519
+ field_generator.instance_variable_set(:@model, model)
1520
+ flds = field_generator.send(:generate_fields)&.split("\n")
1521
+ &.each_with_object([]) do |f, s|
1522
+ if (f = f.strip).start_with?('field ')
1523
+ f = f[6..-1].split(',')
1524
+ s << [f.first[1..-1].to_sym, [f[1][1..-1].split(': :').map(&:to_sym)].to_h]
1525
+ end
1526
+ end || []
1527
+ if (!model.is_view? && mod_pk = model.primary_key)
1528
+ obj.field((mod_pk.is_a?(Array) ? mod_pk.first : mod_pk).to_sym, **{ as: :id })
1529
+ end
1530
+ # Create a call such as: field :name, as: :text
1531
+ flds.each do |f|
1532
+ # Add proper types if this is a polymorphic belongs_to
1533
+ if f.last == { as: :belongs_to } &&
1534
+ (fk = ::Brick.relations[model.table_name][:fks].find { |k, v| v[:assoc_name] == f.first.to_s }) &&
1535
+ fk.last.fetch(:polymorphic, nil)
1536
+ poly_types = fk.last.fetch(:inverse_table, nil)&.each_with_object([]) do |poly_table, s|
1537
+ s << Object.const_get(::Brick.relations[poly_table][:class_name])
1538
+ end
1539
+ if poly_types.present?
1540
+ f.last[:polymorphic_as] = f.first
1541
+ f.last[:types] = poly_types
1542
+ end
1543
+ end
1544
+ kwargs = f.last.is_a?(Hash) ? f.pop : {}
1545
+ obj.send(:field, *f, **kwargs)
1546
+ end
1547
+ end
1548
+
1474
1549
  class Object
1475
1550
  class << self
1476
1551
 
1477
1552
  private
1478
1553
 
1479
1554
  def build_model(relations, base_module, base_name, class_name, inheritable_name = nil)
1480
- tnp = ::Brick.config.table_name_prefixes&.find { |p| p.last == base_module.name }&.first
1555
+ tnp = ::Brick.config.table_name_prefixes&.find { |p| p.last == base_module.name }
1556
+ # return [base_module, ''] if !base_module.is_a?(Class) && base_name == tnp&.last
1557
+
1481
1558
  if (base_model = (::Brick.config.sti_namespace_prefixes&.fetch("::#{base_module.name}::#{class_name}", nil) || # Are we part of an auto-STI namespace? ...
1482
1559
  ::Brick.config.sti_namespace_prefixes&.fetch("::#{base_module.name}::", nil))&.constantize) ||
1483
1560
  base_module != Object # ... or otherwise already in some namespace?
@@ -1499,7 +1576,7 @@ class Object
1499
1576
  table_name = if (base_model = ::Brick.sti_models[model_name]&.fetch(:base, nil) || ::Brick.existing_stis[model_name]&.constantize)
1500
1577
  base_model.table_name
1501
1578
  else
1502
- "#{tnp}#{ActiveSupport::Inflector.pluralize(singular_table_name)}"
1579
+ "#{tnp&.first}#{ActiveSupport::Inflector.pluralize(singular_table_name)}"
1503
1580
  end
1504
1581
  if ::Brick.apartment_multitenant &&
1505
1582
  Apartment.excluded_models.include?(table_name.singularize.camelize)
@@ -1553,7 +1630,7 @@ class Object
1553
1630
  else
1554
1631
  # Class for auto-generated models to inherit from
1555
1632
  base_model = (::Brick.config.models_inherit_from ||= (begin
1556
- ::ApplicationRecord
1633
+ Object.const_defined?('ApplicationRecord') ? ::ApplicationRecord : ::ActiveRecord::Base
1557
1634
  rescue StandardError => ex
1558
1635
  ::ActiveRecord::Base
1559
1636
  end))
@@ -1728,7 +1805,8 @@ class Object
1728
1805
  end
1729
1806
 
1730
1807
  def build_bt_or_hm(full_name, relations, relation, hmts, assoc, inverse_assoc_name, inverse_table, code)
1731
- singular_table_name = inverse_table&.singularize
1808
+ return unless (singular_table_name = inverse_table&.singularize)
1809
+
1732
1810
  options = {}
1733
1811
  macro = if assoc[:is_bt]
1734
1812
  # Try to take care of screwy names if this is a belongs_to going to an STI subclass
@@ -2558,8 +2636,9 @@ class Object
2558
2636
  assoc_parts[0].downcase! if assoc_parts[0] =~ /^[A-Z0-9_]+$/
2559
2637
  assoc_name = assoc_parts.join('.')
2560
2638
  else
2561
- class_name_parts = ::Brick.namify(hm_assoc[:inverse_table], :underscore).split('.')
2562
- needs_class = assoc_name.singularize.camelize != class_name_parts.last.singularize.camelize
2639
+ last_class_name_part = ::Brick.relations[hm_assoc[:inverse_table]].fetch(:class_name, nil)&.split('::')&.last ||
2640
+ ::Brick.namify(hm_assoc[:inverse_table], :underscore).split('.').last.singularize.camelize
2641
+ needs_class = assoc_name.singularize.camelize != last_class_name_part
2563
2642
  end
2564
2643
  [assoc_name, needs_class]
2565
2644
  end
@@ -2592,586 +2671,49 @@ end
2592
2671
  # ==========================================================
2593
2672
  # Get info on all relations during first database connection
2594
2673
  # ==========================================================
2674
+ if Object.const_defined?('OTR') && OTR.const_defined?('ActiveRecord')
2675
+ OTR::ActiveRecord.class_exec do
2676
+ class << self
2677
+ alias _brick_establish_connection! establish_connection!
2678
+ def establish_connection!(*args)
2679
+ conn = _brick_establish_connection!(*args)
2680
+ return conn unless ::Brick.config.mode == :on
2595
2681
 
2596
- if ActiveRecord.const_defined?('ConnectionHandling')
2597
- ActiveRecord::ConnectionHandling
2598
- else
2599
- ActiveRecord::ConnectionAdapters::ConnectionHandler
2600
- end.class_exec do
2601
- alias _brick_establish_connection establish_connection
2602
- def establish_connection(*args)
2603
- conn = _brick_establish_connection(*args)
2604
- return conn unless ::Brick.config.mode == :on
2605
-
2606
- begin
2607
- # Overwrite SQLite's #begin_db_transaction so it opens in IMMEDIATE mode instead of
2608
- # the default DEFERRED mode.
2609
- # https://discuss.rubyonrails.org/t/failed-write-transaction-upgrades-in-sqlite3/81480/2
2610
- if ActiveRecord::Base.connection.adapter_name == 'SQLite'
2611
- arca = ::ActiveRecord::ConnectionAdapters
2612
- db_statements = arca::SQLite3.const_defined?('DatabaseStatements') ? arca::SQLite3::DatabaseStatements : arca::SQLite3::SchemaStatements
2613
- # Rails 7.1 and later
2614
- if arca::AbstractAdapter.private_instance_methods.include?(:with_raw_connection)
2615
- db_statements.define_method(:begin_db_transaction) do
2616
- log("begin immediate transaction", "TRANSACTION") do
2617
- with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
2618
- conn.transaction(:immediate)
2619
- end
2620
- end
2621
- end
2622
- else # Rails < 7.1
2623
- db_statements.define_method(:begin_db_transaction) do
2624
- log('begin immediate transaction', 'TRANSACTION') { @connection.transaction(:immediate) }
2625
- end
2682
+ begin
2683
+ # ::Brick.is_db_present = true
2684
+ ::Brick.reflect_tables
2685
+ rescue ActiveRecord::NoDatabaseError
2686
+ # ::Brick.is_db_present = false
2626
2687
  end
2688
+ Module.class_exec &::Brick::ADD_CONST_MISSING
2689
+ conn
2627
2690
  end
2628
- # ::Brick.is_db_present = true
2629
- _brick_reflect_tables
2630
- rescue ActiveRecord::NoDatabaseError
2631
- # ::Brick.is_db_present = false
2632
2691
  end
2633
- conn
2634
2692
  end
2693
+ else
2694
+ if ActiveRecord.const_defined?('ConnectionHandling')
2695
+ ActiveRecord::ConnectionHandling
2696
+ else
2697
+ ActiveRecord::ConnectionAdapters::ConnectionHandler
2698
+ end.class_exec do
2699
+ alias _brick_establish_connection establish_connection
2700
+ def establish_connection(*args)
2701
+ conn = _brick_establish_connection(*args)
2702
+ return conn unless ::Brick.config.mode == :on
2635
2703
 
2636
- # This is done separately so that during testing it can be called right after a migration
2637
- # in order to make sure everything is good.
2638
- def _brick_reflect_tables
2639
- return unless ::Brick.config.mode == :on
2640
-
2641
- # return if ActiveRecord::Base.connection.current_database == 'postgres'
2642
-
2643
- orig_schema = nil
2644
- if (relations = ::Brick.relations).keys == [:db_name]
2645
- ::Brick.remove_instance_variable(:@_additional_references_loaded) if ::Brick.instance_variable_defined?(:@_additional_references_loaded)
2646
-
2647
- # --------------------------------------------
2648
- # 1. Load three initializers early
2649
- # (inflectsions.rb, brick.rb, apartment.rb)
2650
- # Very first thing, load inflections since we'll be using .pluralize and .singularize on table and model names
2651
- if File.exist?(inflections = ::Rails.root&.join('config/initializers/inflections.rb') || '')
2652
- load inflections
2653
- end
2654
- # Now the Brick initializer since there may be important schema things configured
2655
- if !::Brick.initializer_loaded && File.exist?(brick_initializer = ::Rails.root&.join('config/initializers/brick.rb') || '')
2656
- ::Brick.initializer_loaded = load brick_initializer
2657
-
2658
- # After loading the initializer, add compatibility for ActiveStorage and ActionText if those haven't already been
2659
- # defined. (Further JSON configuration for ActiveStorage metadata happens later in the after_initialize hook.)
2660
- ['ActiveStorage', 'ActionText'].each do |ar_extension|
2661
- if Object.const_defined?(ar_extension) &&
2662
- (extension = Object.const_get(ar_extension)).respond_to?(:table_name_prefix) &&
2663
- !::Brick.config.table_name_prefixes.key?(as_tnp = extension.table_name_prefix)
2664
- ::Brick.config.table_name_prefixes[as_tnp] = ar_extension
2665
- end
2666
- end
2667
-
2668
- # Support the followability gem: https://github.com/nejdetkadir/followability
2669
- if Object.const_defined?('Followability') && !::Brick.config.table_name_prefixes.key?('followability_')
2670
- ::Brick.config.table_name_prefixes['followability_'] = 'Followability'
2671
- end
2672
- end
2673
- # Load the initializer for the Apartment gem a little early so that if .excluded_models and
2674
- # .default_schema are specified then we can work with non-tenanted models more appropriately
2675
- if (apartment = Object.const_defined?('Apartment')) &&
2676
- File.exist?(apartment_initializer = ::Rails.root.join('config/initializers/apartment.rb'))
2677
- require 'apartment/adapters/abstract_adapter'
2678
- Apartment::Adapters::AbstractAdapter.class_exec do
2679
- if instance_methods.include?(:process_excluded_models)
2680
- def process_excluded_models
2681
- # All other models will share a connection (at Apartment.connection_class) and we can modify at will
2682
- Apartment.excluded_models.each do |excluded_model|
2683
- begin
2684
- process_excluded_model(excluded_model)
2685
- rescue NameError => e
2686
- (@bad_models ||= []) << excluded_model
2687
- end
2688
- end
2689
- end
2690
- end
2691
- end
2692
- unless @_apartment_loaded
2693
- load apartment_initializer
2694
- @_apartment_loaded = true
2695
- end
2696
- end
2697
- # Only for Postgres (Doesn't work in sqlite3 or MySQL)
2698
- # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
2699
-
2700
- # ---------------------------
2701
- # 2. Figure out schema things
2702
- is_postgres = nil
2703
- is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer'
2704
- case ActiveRecord::Base.connection.adapter_name
2705
- when 'PostgreSQL', 'SQLServer'
2706
- is_postgres = !is_mssql
2707
- db_schemas = if is_postgres
2708
- ActiveRecord::Base.execute_sql('SELECT nspname AS table_schema, MAX(oid) AS dt FROM pg_namespace GROUP BY 1 ORDER BY 1;')
2709
- else
2710
- ActiveRecord::Base.execute_sql('SELECT DISTINCT table_schema, NULL AS dt FROM INFORMATION_SCHEMA.tables;')
2711
- end
2712
- ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
2713
- row = case row
2714
- when Array
2715
- row
2716
- else
2717
- [row['table_schema'], row['dt']]
2718
- end
2719
- # Remove any system schemas
2720
- s[row.first] = { dt: row.last } unless ['information_schema', 'pg_catalog', 'pg_toast', 'heroku_ext',
2721
- 'INFORMATION_SCHEMA', 'sys'].include?(row.first)
2722
- end
2723
- possible_schema, possible_schemas, multitenancy = ::Brick.get_possible_schemas
2724
- if possible_schemas
2725
- if possible_schema
2726
- ::Brick.default_schema = ::Brick.apartment_default_tenant
2727
- schema = possible_schema
2728
- orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
2729
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
2730
- # When testing, just find the most recently-created schema
2731
- elsif begin
2732
- Rails.env == 'test' ||
2733
- ActiveRecord::Base.execute_sql("SELECT value FROM ar_internal_metadata WHERE key='environment';").first&.fetch('value', nil) == 'test'
2734
- rescue
2735
- end
2736
- ::Brick.default_schema = ::Brick.apartment_default_tenant
2737
- ::Brick.test_schema = schema = ::Brick.db_schemas.to_a.sort { |a, b| b.last[:dt] <=> a.last[:dt] }.first.first
2738
- if possible_schema.blank?
2739
- puts "While running tests, using the most recently-created schema, #{schema}."
2740
- else
2741
- puts "While running tests, had noticed in the brick.rb initializer that the line \"::Brick.schema_behavior = ...\" refers to a schema called \"#{possible_schema}\" which does not exist. Reading table structure from the most recently-created schema, #{schema}."
2742
- end
2743
- orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
2744
- ::Brick.config.schema_behavior = { multitenant: {} } # schema_to_analyse: [schema]
2745
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
2746
- else
2747
- puts "*** In the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to schema(s) called #{possible_schemas.map { |s| "\"#{s}\"" }.join(', ')}. No mentioned schema exists. ***"
2748
- if ::Brick.db_schemas.key?(::Brick.apartment_default_tenant)
2749
- ::Brick.default_schema = schema = ::Brick.apartment_default_tenant
2750
- end
2751
- end
2752
- end
2753
- when 'Mysql2', 'Trilogy'
2754
- ::Brick.default_schema = schema = ActiveRecord::Base.connection.current_database
2755
- when 'OracleEnhanced'
2756
- # ActiveRecord::Base.connection.current_database will be something like "XEPDB1"
2757
- ::Brick.default_schema = schema = ActiveRecord::Base.connection.raw_connection.username
2758
- ::Brick.db_schemas = {}
2759
- ActiveRecord::Base.execute_sql("SELECT username FROM sys.all_users WHERE ORACLE_MAINTAINED != 'Y'").each { |s| ::Brick.db_schemas[s.first] = {} }
2760
- when 'SQLite'
2761
- sql = "SELECT m.name AS relation_name, UPPER(m.type) AS table_type,
2762
- p.name AS column_name, p.type AS data_type,
2763
- CASE p.pk WHEN 1 THEN 'PRIMARY KEY' END AS const
2764
- FROM sqlite_master AS m
2765
- INNER JOIN pragma_table_info(m.name) AS p
2766
- WHERE m.name NOT IN ('sqlite_sequence', ?, ?)
2767
- ORDER BY m.name, p.cid"
2768
- else
2769
- puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
2770
- end
2771
-
2772
- ::Brick.db_schemas ||= {}
2773
-
2774
- # ---------------------
2775
- # 3. Tables and columns
2776
- # %%% Retrieve internal ActiveRecord table names like this:
2777
- # ActiveRecord::Base.internal_metadata_table_name, ActiveRecord::Base.schema_migrations_table_name
2778
- # For if it's not SQLite -- so this is the Postgres and MySQL version
2779
- measures = []
2780
- ::Brick.is_oracle = true if ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
2781
- case ActiveRecord::Base.connection.adapter_name
2782
- when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
2783
- # schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
2784
- retrieve_schema_and_tables(sql, is_postgres, is_mssql, schema).each do |r|
2785
- # If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
2786
- # is the default schema, usually 'public'.
2787
- schema_name = if ::Brick.config.schema_behavior[:multitenant]
2788
- ::Brick.apartment_default_tenant if ::Brick.is_apartment_excluded_table(r['relation_name'])
2789
- elsif ![schema, 'public'].include?(r['schema'])
2790
- r['schema']
2791
- end
2792
- relation_name = schema_name ? "#{schema_name}.#{r['relation_name']}" : r['relation_name']
2793
- # Both uppers and lowers as well as underscores?
2794
- ::Brick.apply_double_underscore_patch if relation_name =~ /[A-Z]/ && relation_name =~ /[a-z]/ && relation_name.index('_')
2795
- relation = relations[relation_name]
2796
- relation[:isView] = true if r['table_type'] == 'VIEW'
2797
- relation[:description] = r['table_description'] if r['table_description']
2798
- col_name = r['column_name']
2799
- key = case r['const']
2800
- when 'PRIMARY KEY'
2801
- relation[:pkey][r['key'] || relation_name] ||= []
2802
- when 'UNIQUE'
2803
- relation[:ukeys][r['key'] || "#{relation_name}.#{col_name}"] ||= []
2804
- # key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
2805
- # key[r['key']]
2806
- else
2807
- if r['data_type'] == 'uuid'
2808
- # && r['column_name'] == ::Brick.ar_base.primary_key
2809
- # binding.pry
2810
- relation[:pkey][r['key'] || relation_name] ||= []
2811
- end
2812
- end
2813
- # binding.pry if key && r['data_type'] == 'uuid'
2814
- key << col_name if key
2815
- cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
2816
- cols[col_name] = [r['data_type'], r['max_length'], measures&.include?(col_name), r['is_nullable'] == 'NO']
2817
- # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
2818
- relation[:col_descrips][col_name] = r['column_description'] if r['column_description']
2819
- end
2820
- else # MySQL2, OracleEnhanced, and MSSQL act a little differently, bringing back an array for each row
2821
- schema_and_tables = case ActiveRecord::Base.connection.adapter_name
2822
- when 'OracleEnhanced'
2823
- sql =
2824
- "SELECT c.owner AS schema, c.table_name AS relation_name,
2825
- CASE WHEN v.owner IS NULL THEN 'BASE_TABLE' ELSE 'VIEW' END AS table_type,
2826
- c.column_name, c.data_type,
2827
- COALESCE(c.data_length, c.data_precision) AS max_length,
2828
- CASE ac.constraint_type WHEN 'P' THEN 'PRIMARY KEY' END AS const,
2829
- ac.constraint_name AS \"key\",
2830
- CASE c.nullable WHEN 'Y' THEN 'YES' ELSE 'NO' END AS is_nullable
2831
- FROM all_tab_cols c
2832
- LEFT OUTER JOIN all_cons_columns acc ON acc.owner = c.owner AND acc.table_name = c.table_name AND acc.column_name = c.column_name
2833
- LEFT OUTER JOIN all_constraints ac ON ac.owner = acc.owner AND ac.table_name = acc.table_name AND ac.constraint_name = acc.constraint_name AND constraint_type = 'P'
2834
- LEFT OUTER JOIN all_views v ON c.owner = v.owner AND c.table_name = v.view_name
2835
- WHERE c.owner IN (#{::Brick.db_schemas.keys.map { |s| "'#{s}'" }.join(', ')})
2836
- AND c.table_name NOT IN (?, ?)
2837
- ORDER BY 1, 2, c.internal_column_id, acc.position"
2838
- ActiveRecord::Base.execute_sql(sql, *ar_tables)
2839
- else
2840
- retrieve_schema_and_tables(sql)
2841
- end
2842
-
2843
- schema_and_tables.each do |r|
2844
- next if r[1].index('$') # Oracle can have goofy table names with $
2845
-
2846
- if (relation_name = r[1]) =~ /^[A-Z0-9_]+$/
2847
- relation_name.downcase!
2848
- # Both uppers and lowers as well as underscores?
2849
- elsif relation_name =~ /[A-Z]/ && relation_name =~ /[a-z]/ && relation_name.index('_')
2850
- ::Brick.apply_double_underscore_patch
2851
- end
2852
- # Expect the default schema for SQL Server to be 'dbo'.
2853
- if (::Brick.is_oracle && r[0] != schema) || (is_mssql && r[0] != 'dbo')
2854
- relation_name = "#{r[0]}.#{relation_name}"
2855
- end
2856
-
2857
- relation = relations[relation_name] # here relation represents a table or view from the database
2858
- relation[:isView] = true if r[2] == 'VIEW' # table_type
2859
- col_name = ::Brick.is_oracle ? connection.send(:oracle_downcase, r[3]) : r[3]
2860
- key = case r[6] # constraint type
2861
- when 'PRIMARY KEY'
2862
- # key
2863
- relation[:pkey][r[7] || relation_name] ||= []
2864
- when 'UNIQUE'
2865
- relation[:ukeys][r[7] || "#{relation_name}.#{col_name}"] ||= []
2866
- # key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
2867
- # key[r['key']]
2868
- end
2869
- key << col_name if key
2870
- cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
2871
- # 'data_type', 'max_length', measure, 'is_nullable'
2872
- cols[col_name] = [r[4], r[5], measures&.include?(col_name), r[8] == 'NO']
2873
- # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
2874
- end
2875
- end
2876
-
2877
- # PostGIS adds three views which would confuse Rails if models were to be built for them.
2878
- if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
2879
- if relations.key?('geography_columns') && relations.key?('geometry_columns') && relations.key?('spatial_ref_sys')
2880
- (::Brick.config.exclude_tables ||= []) << 'geography_columns'
2881
- ::Brick.config.exclude_tables << 'geometry_columns'
2882
- ::Brick.config.exclude_tables << 'spatial_ref_sys'
2883
- end
2884
- end
2885
-
2886
- # # Add unique OIDs
2887
- # if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
2888
- # ActiveRecord::Base.execute_sql(
2889
- # "SELECT c.oid, n.nspname, c.relname
2890
- # FROM pg_catalog.pg_namespace AS n
2891
- # INNER JOIN pg_catalog.pg_class AS c ON n.oid = c.relnamespace
2892
- # WHERE c.relkind IN ('r', 'v')"
2893
- # ).each do |r|
2894
- # next if ['pg_catalog', 'information_schema', ''].include?(r['nspname']) ||
2895
- # ['ar_internal_metadata', 'schema_migrations'].include?(r['relname'])
2896
- # relation = relations.fetch(r['relname'], nil)
2897
- # if relation
2898
- # (relation[:oid] ||= {})[r['nspname']] = r['oid']
2899
- # else
2900
- # puts "Where is #{r['nspname']} #{r['relname']} ?"
2901
- # end
2902
- # end
2903
- # end
2904
- # schema = ::Brick.default_schema # Reset back for this next round of fun
2905
-
2906
- # ---------------------------------------------
2907
- # 4. Foreign key info
2908
- # (done in two parts which get JOINed together in Ruby code)
2909
- kcus = nil
2910
- entry_type = nil
2911
- case ActiveRecord::Base.connection.adapter_name
2912
- when 'PostgreSQL', 'Mysql2', 'Trilogy', 'SQLServer'
2913
- # Part 1 -- all KCUs
2914
- sql = "SELECT CONSTRAINT_CATALOG, CONSTRAINT_SCHEMA, CONSTRAINT_NAME, ORDINAL_POSITION,
2915
- TABLE_NAME, COLUMN_NAME
2916
- FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE#{"
2917
- WHERE CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }#{"
2918
- WHERE CONSTRAINT_SCHEMA = '#{ActiveRecord::Base.connection.current_database&.tr("'", "''")}'" if is_mysql
2919
- }"
2920
- kcus = ActiveRecord::Base.execute_sql(sql).each_with_object({}) do |v, s|
2921
- if (entry_type ||= v.is_a?(Array) ? :array : :hash) == :hash
2922
- key = "#{v['constraint_name']}.#{v['constraint_schema']}.#{v['constraint_catalog']}.#{v['ordinal_position']}"
2923
- key << ".#{v['table_name']}.#{v['column_name']}" unless is_postgres || is_mssql
2924
- s[key] = [v['constraint_schema'], v['table_name']]
2925
- else # Array
2926
- key = "#{v[2]}.#{v[1]}.#{v[0]}.#{v[3]}"
2927
- key << ".#{v[4]}.#{v[5]}" unless is_postgres || is_mssql
2928
- s[key] = [v[1], v[4]]
2929
- end
2930
- end
2931
-
2932
- # Part 2 -- fk_references
2933
- sql = "SELECT kcu.CONSTRAINT_SCHEMA, kcu.TABLE_NAME, kcu.COLUMN_NAME,
2934
- #{# These will get filled in with real values (effectively doing the JOIN in Ruby)
2935
- is_postgres || is_mssql ? 'NULL as primary_schema, NULL as primary_table' :
2936
- 'kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME'},
2937
- kcu.CONSTRAINT_NAME AS CONSTRAINT_SCHEMA_FK,
2938
- rc.UNIQUE_CONSTRAINT_NAME, rc.UNIQUE_CONSTRAINT_SCHEMA, rc.UNIQUE_CONSTRAINT_CATALOG, kcu.ORDINAL_POSITION
2939
- FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc
2940
- INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu
2941
- ON kcu.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG
2942
- AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
2943
- AND kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME#{"
2944
- WHERE kcu.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema}#{"
2945
- WHERE kcu.CONSTRAINT_SCHEMA = '#{ActiveRecord::Base.connection.current_database&.tr("'", "''")}'" if is_mysql}"
2946
- fk_references = ActiveRecord::Base.execute_sql(sql)
2947
- when 'SQLite'
2948
- sql = "SELECT NULL AS constraint_schema, m.name, fkl.\"from\", NULL AS primary_schema, fkl.\"table\", m.name || '_' || fkl.\"from\" AS constraint_name
2949
- FROM sqlite_master m
2950
- INNER JOIN pragma_foreign_key_list(m.name) fkl ON m.type = 'table'
2951
- ORDER BY m.name, fkl.seq"
2952
- fk_references = ActiveRecord::Base.execute_sql(sql)
2953
- when 'OracleEnhanced'
2954
- schemas = ::Brick.db_schemas.keys.map { |s| "'#{s}'" }.join(', ')
2955
- sql =
2956
- "SELECT -- fk
2957
- ac.owner AS constraint_schema, acc_fk.table_name, acc_fk.column_name,
2958
- -- referenced pk
2959
- ac.r_owner AS primary_schema, acc_pk.table_name AS primary_table, acc_fk.constraint_name AS constraint_schema_fk
2960
- -- , acc_pk.column_name
2961
- FROM all_cons_columns acc_fk
2962
- INNER JOIN all_constraints ac ON acc_fk.owner = ac.owner
2963
- AND acc_fk.constraint_name = ac.constraint_name
2964
- INNER JOIN all_cons_columns acc_pk ON ac.r_owner = acc_pk.owner
2965
- AND ac.r_constraint_name = acc_pk.constraint_name
2966
- WHERE ac.constraint_type = 'R'
2967
- AND ac.owner IN (#{schemas})
2968
- AND ac.r_owner IN (#{schemas})"
2969
- fk_references = ActiveRecord::Base.execute_sql(sql)
2970
- end
2971
- ::Brick.is_oracle = true if ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
2972
- # ::Brick.default_schema ||= schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
2973
- ::Brick.default_schema ||= 'public' if is_postgres
2974
- fk_references&.each do |fk|
2975
- fk = fk.values unless fk.is_a?(Array)
2976
- # Virtually JOIN KCUs to fk_references in order to fill in the primary schema and primary table
2977
- kcu_key = "#{fk[6]}.#{fk[7]}.#{fk[8]}.#{fk[9]}"
2978
- kcu_key << ".#{fk[3]}.#{fk[4]}" unless is_postgres || is_mssql
2979
- if (kcu = kcus&.fetch(kcu_key, nil))
2980
- fk[3] = kcu[0]
2981
- fk[4] = kcu[1]
2982
- end
2983
- # Multitenancy makes things a little more general overall, except for non-tenanted tables
2984
- if ::Brick.is_apartment_excluded_table(::Brick.namify(fk[1]))
2985
- fk[0] = ::Brick.apartment_default_tenant
2986
- elsif (is_postgres && (fk[0] == 'public' || (multitenancy && fk[0] == schema))) ||
2987
- (::Brick.is_oracle && fk[0] == schema) ||
2988
- (is_mssql && fk[0] == 'dbo') ||
2989
- (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[0]))
2990
- fk[0] = nil
2991
- end
2992
- if ::Brick.is_apartment_excluded_table(fk[4])
2993
- fk[3] = ::Brick.apartment_default_tenant
2994
- elsif (is_postgres && (fk[3] == 'public' || (multitenancy && fk[3] == schema))) ||
2995
- (::Brick.is_oracle && fk[3] == schema) ||
2996
- (is_mssql && fk[3] == 'dbo') ||
2997
- (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[3]))
2998
- fk[3] = nil
2999
- end
3000
- if ::Brick.is_oracle
3001
- fk[1].downcase! if fk[1] =~ /^[A-Z0-9_]+$/
3002
- fk[4].downcase! if fk[4] =~ /^[A-Z0-9_]+$/
3003
- fk[2] = connection.send(:oracle_downcase, fk[2])
3004
- end
3005
- ::Brick._add_bt_and_hm(fk, relations)
3006
- end
3007
- kcus = nil # Allow this large item to be garbage collected
3008
- end
3009
-
3010
- table_name_lookup = (::Brick.table_name_lookup ||= {})
3011
- relations.each do |k, v|
3012
- next if k.is_a?(Symbol)
3013
-
3014
- rel_name = k.split('.').map { |rel_part| ::Brick.namify(rel_part, :underscore) }
3015
- schema_names = rel_name[0..-2]
3016
- schema_names.shift if ::Brick.apartment_multitenant && schema_names.first == ::Brick.apartment_default_tenant
3017
- v[:schema] = schema_names.join('.') unless schema_names.empty?
3018
- # %%% If more than one schema has the same table name, will need to add a schema name prefix to have uniqueness
3019
- if (singular = rel_name.last.singularize).blank?
3020
- singular = rel_name.last
3021
- end
3022
- name_parts = if (tnp = ::Brick.config.table_name_prefixes
3023
- &.find { |k1, _v1| singular.start_with?(k1) && singular.length > k1.length }
3024
- ).present?
3025
- v[:auto_prefixed_schema] = tnp.first
3026
- # v[:resource] = rel_name.last[tnp.first.length..-1]
3027
- [tnp.last, singular[tnp.first.length..-1]]
3028
- else
3029
- # v[:resource] = rel_name.last
3030
- [singular]
3031
- end
3032
- proposed_name_parts = (schema_names + name_parts).map { |p| ::Brick.namify(p, :underscore).camelize }
3033
- # Find out if the proposed name leads to a module or class that already exists and is not an AR class
3034
- colliding_thing = nil
3035
- loop do
3036
- klass = Object
3037
- proposed_name_parts.each do |part|
3038
- if klass.const_defined?(part)
3039
- klass = klass.const_get(part)
3040
- else
3041
- klass = nil
3042
- break
3043
- end
3044
- end
3045
- break if !klass || (klass < ActiveRecord::Base) # Break if all good -- no conflicts
3046
-
3047
- # Find a unique name since there's already something that's non-AR with that same name
3048
- last_idx = proposed_name_parts.length - 1
3049
- proposed_name_parts[last_idx] = ::Brick.ensure_unique(proposed_name_parts[last_idx], 'X')
3050
- colliding_thing ||= klass
3051
- end
3052
- v[:class_name] = proposed_name_parts.join('::')
3053
- # Was: v[:resource] = v[:class_name].underscore.tr('/', '.')
3054
- v[:resource] = proposed_name_parts.last.underscore
3055
- if colliding_thing
3056
- message_start = if colliding_thing.is_a?(Module) && Object.const_defined?(:Rails) &&
3057
- colliding_thing.constants.find { |c| colliding_thing.const_get(c) < Rails::Application }
3058
- "The module for the Rails application itself, \"#{colliding_thing.name}\","
3059
- else
3060
- "Non-AR #{colliding_thing.class.name.downcase} \"#{colliding_thing.name}\""
3061
- end
3062
- puts "WARNING: #{message_start} already exists.\n Will set up to auto-create model #{v[:class_name]} for table #{k}."
2704
+ begin
2705
+ # ::Brick.is_db_present = true
2706
+ ::Brick.reflect_tables
2707
+ rescue ActiveRecord::NoDatabaseError
2708
+ # ::Brick.is_db_present = false
3063
2709
  end
3064
- # Track anything that's out-of-the-ordinary
3065
- table_name_lookup[v[:class_name]] = k unless v[:class_name].underscore.pluralize == k
3066
- end
3067
- ::Brick.load_additional_references if ::Brick.initializer_loaded
3068
-
3069
- if is_postgres
3070
- params = []
3071
- ActiveRecord::Base.execute_sql("-- inherited and partitioned tables counts
3072
- SELECT n.nspname, parent.relname,
3073
- ((SUM(child.reltuples::float) / greatest(SUM(child.relpages), 1))) *
3074
- (SUM(pg_relation_size(child.oid))::float / (current_setting('block_size')::float))::integer AS rowcount
3075
- FROM pg_inherits
3076
- INNER JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
3077
- INNER JOIN pg_class child ON pg_inherits.inhrelid = child.oid
3078
- INNER JOIN pg_catalog.pg_namespace n ON n.oid = parent.relnamespace#{
3079
- if schema
3080
- params << schema
3081
- "
3082
- WHERE n.nspname = COALESCE(?, 'public')"
3083
- end}
3084
- GROUP BY n.nspname, parent.relname, child.reltuples, child.relpages, child.oid
3085
-
3086
- UNION ALL
3087
-
3088
- -- table count
3089
- SELECT n.nspname, pg_class.relname,
3090
- (pg_class.reltuples::float / greatest(pg_class.relpages, 1)) *
3091
- (pg_relation_size(pg_class.oid)::float / (current_setting('block_size')::float))::integer AS rowcount
3092
- FROM pg_class
3093
- INNER JOIN pg_catalog.pg_namespace n ON n.oid = pg_class.relnamespace#{
3094
- if schema
3095
- params << schema
3096
- "
3097
- WHERE n.nspname = COALESCE(?, 'public')"
3098
- end}
3099
- GROUP BY n.nspname, pg_class.relname, pg_class.reltuples, pg_class.relpages, pg_class.oid", params).each do |tblcount|
3100
- # %%% What is the default schema here?
3101
- prefix = "#{tblcount['nspname']}." unless tblcount['nspname'] == (schema || 'public')
3102
- relations.fetch("#{prefix}#{tblcount['relname']}", nil)&.[]=(:rowcount, tblcount['rowcount'].to_i.round)
2710
+ if Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace)
2711
+ Module.class_exec &::Brick::ADD_CONST_MISSING
3103
2712
  end
2713
+ conn
3104
2714
  end
3105
-
3106
- if orig_schema && (orig_schema = (orig_schema - ['pg_catalog', 'pg_toast', 'heroku_ext']).first)
3107
- puts "Now switching back to \"#{orig_schema}\" schema."
3108
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", orig_schema)
3109
- end
3110
- end
3111
-
3112
- def retrieve_schema_and_tables(sql = nil, is_postgres = nil, is_mssql = nil, schema = nil)
3113
- is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' if is_mssql.nil?
3114
- params = ar_tables
3115
- sql ||= "SELECT t.table_schema AS \"schema\", t.table_name AS relation_name, t.table_type,#{"
3116
- pg_catalog.obj_description(
3117
- ('\"' || t.table_schema || '\".\"' || t.table_name || '\"')::regclass::oid, 'pg_class'
3118
- ) AS table_description,
3119
- pg_catalog.col_description(
3120
- ('\"' || t.table_schema || '\".\"' || t.table_name || '\"')::regclass::oid, c.ordinal_position
3121
- ) AS column_description," if is_postgres}
3122
- c.column_name, #{is_postgres ? "CASE c.data_type WHEN 'USER-DEFINED' THEN pg_t.typname ELSE c.data_type END AS data_type" : 'c.data_type'},
3123
- COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
3124
- kcu.constraint_type AS const, kcu.constraint_name AS \"key\",
3125
- c.is_nullable
3126
- FROM INFORMATION_SCHEMA.tables AS t
3127
- LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
3128
- AND t.table_name = c.table_name
3129
- LEFT OUTER JOIN
3130
- (SELECT kcu1.constraint_schema, kcu1.table_name, kcu1.column_name, kcu1.ordinal_position,
3131
- tc.constraint_type, kcu1.constraint_name
3132
- FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
3133
- INNER JOIN INFORMATION_SCHEMA.table_constraints AS tc
3134
- ON kcu1.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
3135
- AND kcu1.TABLE_NAME = tc.TABLE_NAME
3136
- AND kcu1.CONSTRAINT_NAME = tc.constraint_name
3137
- AND tc.constraint_type != 'FOREIGN KEY' -- For MSSQL
3138
- ) AS kcu ON
3139
- -- kcu.CONSTRAINT_CATALOG = t.table_catalog AND
3140
- kcu.CONSTRAINT_SCHEMA = c.table_schema
3141
- AND kcu.TABLE_NAME = c.table_name
3142
- AND kcu.column_name = c.column_name#{"
3143
- -- AND kcu.position_in_unique_constraint IS NULL" unless is_mssql}#{"
3144
- INNER JOIN pg_catalog.pg_namespace pg_n ON pg_n.nspname = t.table_schema
3145
- INNER JOIN pg_catalog.pg_class pg_c ON pg_n.oid = pg_c.relnamespace AND pg_c.relname = c.table_name
3146
- INNER JOIN pg_catalog.pg_attribute pg_a ON pg_c.oid = pg_a.attrelid AND pg_a.attname = c.column_name
3147
- INNER JOIN pg_catalog.pg_type pg_t ON pg_t.oid = pg_a.atttypid" if is_postgres}
3148
- WHERE t.table_schema #{is_postgres || is_mssql ?
3149
- "NOT IN ('information_schema', 'pg_catalog', 'pg_toast', 'heroku_ext',
3150
- 'INFORMATION_SCHEMA', 'sys')"
3151
- :
3152
- "= '#{ActiveRecord::Base.connection.current_database&.tr("'", "''")}'"}#{
3153
- if is_postgres && schema
3154
- params = params.unshift(schema) # Used to use this SQL: current_setting('SEARCH_PATH')
3155
- "
3156
- AND t.table_schema = COALESCE(?, 'public')"
3157
- end}
3158
- -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
3159
- AND t.table_name NOT IN ('pg_stat_statements', ?, ?)
3160
- ORDER BY 1, t.table_type DESC, 2, c.ordinal_position"
3161
- ActiveRecord::Base.execute_sql(sql, *params)
3162
- end
3163
-
3164
- def ar_tables
3165
- ar_smtn = if ActiveRecord::Base.respond_to?(:schema_migrations_table_name)
3166
- ActiveRecord::Base.schema_migrations_table_name
3167
- else
3168
- 'schema_migrations'
3169
- end
3170
- ar_imtn = ActiveRecord.version >= ::Gem::Version.new('5.0') ? ActiveRecord::Base.internal_metadata_table_name : 'ar_internal_metadata'
3171
- [ar_smtn, ar_imtn]
3172
2715
  end
3173
2716
  end
3174
-
3175
2717
  # ==========================================
3176
2718
 
3177
2719
  # :nodoc:
@@ -3464,7 +3006,7 @@ module Brick
3464
3006
  end
3465
3007
  end
3466
3008
 
3467
- def _brick_index(tbl_name, mode = nil, separator = nil, relation = nil)
3009
+ def _brick_index(tbl_name, mode = nil, separator = nil, relation = nil, not_path = nil)
3468
3010
  separator ||= '_'
3469
3011
  res_name = (tbl_name_parts = tbl_name.split('.'))[0..-2].first
3470
3012
  res_name << '.' if res_name
@@ -3472,22 +3014,28 @@ module Brick
3472
3014
 
3473
3015
  res_parts = ((mode == :singular) ? res_name.singularize : res_name).split('.')
3474
3016
  res_parts.shift if ::Brick.apartment_multitenant && res_parts.length > 1 && res_parts.first == ::Brick.apartment_default_tenant
3017
+ index2 = []
3018
+ if ::Brick.config.path_prefix
3019
+ res_parts.unshift(::Brick.config.path_prefix)
3020
+ index2 << ::Brick.config.path_prefix
3021
+ end
3475
3022
  if (aps = relation&.fetch(:auto_prefixed_schema, nil)) # && res_parts.last.start_with?(aps)
3476
3023
  aps = aps[0..-2] if aps[-1] == '_'
3477
3024
  last_part = res_parts.last # [aps.length..-1]
3478
3025
  res_parts[-1] = aps
3479
3026
  res_parts << last_part
3480
- end
3481
- path_prefix = []
3482
- if ::Brick.config.path_prefix
3483
- res_parts.unshift(::Brick.config.path_prefix)
3484
- path_prefix << ::Brick.config.path_prefix
3027
+ index2 << aps
3485
3028
  end
3486
3029
  index = res_parts.map(&:underscore).join(separator)
3487
- index = index.tr('_', 'x') if separator == 'x'
3488
- # Rails applies an _index suffix to that route when the resource name isn't something plural
3489
- index << '_index' if mode != :singular && separator == '_' &&
3490
- index == (path_prefix + [relation[:class_name]&.underscore&.tr('/', '_') || '_']).join(separator)
3030
+ if separator == 'x'
3031
+ index = index.tr('_', 'x')
3032
+ else
3033
+ # Rails applies an _index suffix to that route when the resource name isn't something plural
3034
+ index << '_index' if mode != :singular && !not_path &&
3035
+ index == (
3036
+ index2 + [relation[:class_name][(relation&.fetch(:auto_prefixed_class, nil)&.length&.+ 2) || 0..-1]&.underscore&.tr('/', '_') || '_']
3037
+ ).join(separator)
3038
+ end
3491
3039
  index
3492
3040
  end
3493
3041