brick 1.0.190 → 1.0.191

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e7a12940f4ac57eea3c53bb3b437472f90bbaa857bd6611d3e871463add46f2
4
- data.tar.gz: 8afb6e47cebd99a9b4172aa82d9447b71dd66e6df750249063b66b9b9ba939a9
3
+ metadata.gz: 193982ca353a787d619e46d02b5d7a671c2b05936a2bf96c7f4c92fbba2b467c
4
+ data.tar.gz: 065f9abdc32f6d6413caf955ab7f01cd2a300b24532f215471aadf82ec9bdc93
5
5
  SHA512:
6
- metadata.gz: d5379ddb6c61675b5606730a771a3f95638829b8359b42fd78c0509836635a93d36b787b09f21a0b9321623c917a149508b643a68ff4474baf7b40739ad5eeed
7
- data.tar.gz: 40b6c6dc4fd31b10632fafb572608ec1c6473b6efa1dab346f1c82fc4f9daa1faba6d3675b336a780291595e25ace20358a95205a9284e7c42ddf450efe1d0a4
6
+ metadata.gz: 363c321fac7e13b4ccbe4bd9ed8b10f1a525641c0da3db0c5ee2bc49f6b1a7b0b0254c11fdb9d42c4ab2ab2c9c7e673a4f6c25e063315bcafcede48cc3096692
7
+ data.tar.gz: 9f27685f8621482b2130a66d5c7d149d41b108dc73f800d060f26ab757f09f06bb3c675b22db0dc37e666a55cdcd0b5ead274232913893d48881809cbf58e72e
data/lib/brick/config.rb CHANGED
@@ -399,6 +399,14 @@ module Brick
399
399
  @mutex.synchronize { @not_nullables = columns }
400
400
  end
401
401
 
402
+ def omit_empty_tables_in_dropdown
403
+ @mutex.synchronize { @omit_empty_tables_in_dropdown }
404
+ end
405
+
406
+ def omit_empty_tables_in_dropdown=(field_set)
407
+ @mutex.synchronize { @omit_empty_tables_in_dropdown = field_set }
408
+ end
409
+
402
410
  def always_load_fields
403
411
  @mutex.synchronize { @always_load_fields || {} }
404
412
  end
@@ -329,27 +329,10 @@ module ActiveRecord
329
329
  end
330
330
 
331
331
  # Providing a relation object allows auto-modules built from table name prefixes to work
332
- def self._brick_index(mode = nil, separator = '_', relation = nil)
332
+ def self._brick_index(mode = nil, separator = nil, relation = nil)
333
333
  return if abstract_class?
334
334
 
335
- tbl_parts = ((mode == :singular) ? table_name.singularize : table_name).split('.')
336
- tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
337
- if (aps = relation&.fetch(:auto_prefixed_schema, nil)) && tbl_parts.last.start_with?(aps)
338
- last_part = tbl_parts.last[aps.length..-1]
339
- aps = aps[0..-2] if aps[-1] == '_'
340
- tbl_parts[-1] = aps
341
- tbl_parts << last_part
342
- end
343
- path_prefix = []
344
- if ::Brick.config.path_prefix
345
- tbl_parts.unshift(::Brick.config.path_prefix)
346
- path_prefix << ::Brick.config.path_prefix
347
- end
348
- index = tbl_parts.map(&:underscore).join(separator)
349
- # Rails applies an _index suffix to that route when the resource name isn't something plural
350
- index << '_index' if mode != :singular && separator == '_' &&
351
- index == (path_prefix + [name&.underscore&.tr('/', '_') || '_']).join(separator)
352
- index
335
+ ::Brick._brick_index(table_name, mode, separator, relation)
353
336
  end
354
337
 
355
338
  def self.brick_import_template
@@ -686,7 +669,7 @@ module ActiveRecord
686
669
  (cust_col_override || klass._br_cust_cols).each do |k, cc|
687
670
  if rel_dupe.respond_to?(k) # Name already taken?
688
671
  # %%% Use ensure_unique here in this kind of fashion:
689
- # cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
672
+ # cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", nil, bts, hms)
690
673
  # binding.pry
691
674
  next
692
675
  end
@@ -806,6 +789,7 @@ module ActiveRecord
806
789
 
807
790
  # Add derived table JOIN for the has_many counts
808
791
  nix = []
792
+ previous = []
809
793
  klass._br_hm_counts.each do |k, hm|
810
794
  count_column = if hm.options[:through]
811
795
  # Build the chain of JOINs going to the final destination HMT table
@@ -832,7 +816,7 @@ module ActiveRecord
832
816
  through_sources.push(this_hm = src_ref.active_record.reflect_on_association(thr))
833
817
  end
834
818
  through_sources.push(src_ref) unless src_ref.belongs_to?
835
- from_clause = +"#{through_sources.first.table_name} br_t0"
819
+ from_clause = +"#{_br_quoted_name(through_sources.first.table_name)} br_t0"
836
820
  fk_col = through_sources.shift.foreign_key
837
821
 
838
822
  idx = 0
@@ -900,7 +884,7 @@ module ActiveRecord
900
884
  next
901
885
  end
902
886
 
903
- tbl_alias = "b_r_#{hm.name}"
887
+ tbl_alias = unique63("b_r_#{hm.name}", previous)
904
888
  on_clause = []
905
889
  hm_selects = if fk_col.is_a?(Array) # Composite key?
906
890
  fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{tbl_alias}.#{fk_col_part} = #{pri_tbl.table_name}.#{pri_key[idx]}" }
@@ -1105,6 +1089,19 @@ Might want to add this in your brick.rb:
1105
1089
  def shift_or_first(ary)
1106
1090
  ary.length > 1 ? ary.shift : ary.first
1107
1091
  end
1092
+
1093
+ def unique63(name, previous)
1094
+ name = name[0..62] if name.length > 63
1095
+ unique_num = 1
1096
+ loop do
1097
+ break unless previous.include?(name)
1098
+
1099
+ unique_suffix = "_#{unique_num += 1}"
1100
+ name = "#{name[0..name.length - unique_suffix.length - 1]}#{unique_suffix}"
1101
+ end
1102
+ previous << name
1103
+ name
1104
+ end
1108
1105
  end
1109
1106
 
1110
1107
  module Inheritance
@@ -1715,7 +1712,8 @@ class Object
1715
1712
  # options[:class_name] = hm.first[:inverse_table].singularize.camelize
1716
1713
  # options[:foreign_key] = hm.first[:fk].to_sym
1717
1714
  far_assoc = relations[hm.first[:inverse_table]][:fks].find { |_k, v| v[:assoc_name] == hm[1] }
1718
- options[:class_name] = ::Brick.namify(far_assoc.last[:inverse_table], :underscore).camelize
1715
+ # Was: ::Brick.namify(far_assoc.last[:inverse_table], :underscore).camelize
1716
+ options[:class_name] = relations[far_assoc.last[:inverse_table]][:class_name]
1719
1717
  options[:foreign_key] = far_assoc.last[:fk].to_sym
1720
1718
  end
1721
1719
  options[:source] ||= hm[1].to_sym unless hmt_name.singularize == hm[1]
@@ -1802,9 +1800,9 @@ class Object
1802
1800
  ::Brick.config.schema_behavior[:multitenant] && singular_table_parts.first == 'public'
1803
1801
  singular_table_parts.shift
1804
1802
  end
1805
- options[:class_name] = "::#{assoc[:primary_class]&.name ||
1806
- singular_table_parts.map { |p| ::Brick.namify(p, :underscore).camelize}.join('::')
1807
- }" if need_class_name
1803
+ if need_class_name
1804
+ options[:class_name] = "::#{assoc[:primary_class]&.name || ::Brick.relations[inverse_table][:class_name]}"
1805
+ end
1808
1806
  if need_fk # Funky foreign key?
1809
1807
  options_fk_key = :foreign_key
1810
1808
  if assoc[:fk].is_a?(Array)
@@ -2559,6 +2557,8 @@ class Object
2559
2557
  assoc_name = assoc_parts.join('.')
2560
2558
  else
2561
2559
  class_name_parts = ::Brick.namify(hm_assoc[:inverse_table], :underscore).split('.')
2560
+ last_idx = class_name_parts.length - 1
2561
+ class_name_parts[last_idx] = class_name_parts[last_idx].singularize
2562
2562
  real_name = class_name_parts.map(&:camelize).join('::')
2563
2563
  needs_class = (real_name != hm_assoc[:inverse_table].camelize)
2564
2564
  end
@@ -2905,9 +2905,9 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2905
2905
  FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE#{"
2906
2906
  WHERE CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }"
2907
2907
  kcus = ActiveRecord::Base.execute_sql(sql).each_with_object({}) do |v, s|
2908
- key = "#{v['constraint_name']}.#{v['constraint_schema']}.#{v['constraint_catalog']}.#{v['ordinal_position']}"
2909
- key << ".#{v['table_name']}.#{v['column_name']}" unless is_postgres || is_mssql
2910
- s[key] = [v['constraint_schema'], v['table_name']]
2908
+ key = "#{v.fetch('constraint_name', v[2])}.#{v.fetch('constraint_schema', v[1])}.#{v.fetch('constraint_catalog', v[0])}.#{v.fetch('ordinal_position', v[3])}"
2909
+ key << ".#{v.fetch('table_name', v[4])}.#{v.fetch('column_name', v[5])}" unless is_postgres || is_mssql
2910
+ s[key] = [v.fetch('constraint_schema', v[1]), v.fetch('table_name', v[4])]
2911
2911
  end
2912
2912
 
2913
2913
  sql = "SELECT kcu.CONSTRAINT_SCHEMA, kcu.TABLE_NAME, kcu.COLUMN_NAME,
@@ -2949,7 +2949,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2949
2949
  end
2950
2950
  ::Brick.is_oracle = true if ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
2951
2951
  # ::Brick.default_schema ||= schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
2952
- ::Brick.default_schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
2952
+ ::Brick.default_schema ||= 'public' if is_postgres
2953
2953
  fk_references&.each do |fk|
2954
2954
  fk = fk.values unless fk.is_a?(Array)
2955
2955
  # Virtually JOIN against fk_references in order to change out the primary schema and primary table
@@ -3000,18 +3000,71 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
3000
3000
  &.find { |k1, _v1| singular.start_with?(k1) && singular.length > k1.length }
3001
3001
  ).present?
3002
3002
  v[:auto_prefixed_schema] = tnp.first
3003
- v[:resource] = rel_name.last[(tnp_length = tnp.first.length)..-1]
3004
- [tnp.last, singular[tnp_length..-1]]
3003
+ # v[:resource] = rel_name.last[tnp.first.length..-1]
3004
+ [tnp.last, singular[tnp.first.length..-1]]
3005
3005
  else
3006
- v[:resource] = rel_name.last
3006
+ # v[:resource] = rel_name.last
3007
3007
  [singular]
3008
3008
  end
3009
- v[:class_name] = (schema_names + name_parts).map { |p| ::Brick.namify(p, :underscore).camelize }.join('::')
3009
+ proposed_name_parts = (schema_names + name_parts).map { |p| ::Brick.namify(p, :underscore).camelize }
3010
+ # Find out if the proposed name leads to a module or class that already exists and is not an AR class
3011
+ colliding_thing = nil
3012
+ loop do
3013
+ klass = Object
3014
+ proposed_name_parts.each do |part|
3015
+ if klass.const_defined?(part)
3016
+ klass = klass.const_get(part)
3017
+ else
3018
+ klass = nil
3019
+ break
3020
+ end
3021
+ end
3022
+ break if !klass || (klass < ActiveRecord::Base) # Break if all good -- no conflicts
3023
+
3024
+ # Find a unique name since there's already something that's non-AR with that same name
3025
+ last_idx = proposed_name_parts.length - 1
3026
+ proposed_name_parts[last_idx] = ::Brick.ensure_unique(proposed_name_parts[last_idx], 'X')
3027
+ colliding_thing ||= klass
3028
+ end
3029
+ v[:class_name] = proposed_name_parts.join('::')
3030
+ # Was: v[:resource] = v[:class_name].underscore.tr('/', '.').pluralize
3031
+ v[:resource] = proposed_name_parts.last.underscore.pluralize
3032
+ if colliding_thing
3033
+ message_start = if colliding_thing.is_a?(Module) && Object.const_defined?(:Rails) &&
3034
+ colliding_thing.constants.find { |c| colliding_thing.const_get(c) < Rails::Application }
3035
+ "The module for the Rails application itself, \"#{colliding_thing.name}\","
3036
+ else
3037
+ "Non-AR #{colliding_thing.class.name.downcase} \"#{colliding_thing.name}\""
3038
+ end
3039
+ puts "WARNING: #{message_start} already exists.\n Will set up to auto-create model #{v[:class_name]} for table #{k}."
3040
+ end
3010
3041
  # Track anything that's out-of-the-ordinary
3011
3042
  table_name_lookup[v[:class_name]] = k unless v[:class_name].underscore.pluralize == k
3012
3043
  end
3013
3044
  ::Brick.load_additional_references if ::Brick.initializer_loaded
3014
3045
 
3046
+ if is_postgres
3047
+ ActiveRecord::Base.execute_sql("-- inherited and partitioned tables counts
3048
+ SELECT parent.relname,
3049
+ ((SUM(child.reltuples::float) / greatest(SUM(child.relpages), 1))) *
3050
+ (SUM(pg_relation_size(child.oid))::float / (current_setting('block_size')::float))::integer AS rowcount
3051
+ FROM pg_inherits
3052
+ INNER JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
3053
+ INNER JOIN pg_class child ON pg_inherits.inhrelid = child.oid
3054
+ GROUP BY parent.relname, child.reltuples, child.relpages, child.oid
3055
+
3056
+ UNION ALL
3057
+
3058
+ -- table count
3059
+ SELECT relname,
3060
+ (reltuples::float / greatest(relpages, 1)) *
3061
+ (pg_relation_size(pg_class.oid)::float / (current_setting('block_size')::float))::integer AS rowcount
3062
+ FROM pg_class
3063
+ GROUP BY relname, reltuples, relpages, oid").each do |tblcount|
3064
+ relations.fetch(tblcount['relname'], nil)&.[]=(:rowcount, tblcount['rowcount'].round)
3065
+ end
3066
+ end
3067
+
3015
3068
  if orig_schema && (orig_schema = (orig_schema - ['pg_catalog', 'pg_toast', 'heroku_ext']).first)
3016
3069
  puts "Now switching back to \"#{orig_schema}\" schema."
3017
3070
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", orig_schema)
@@ -3151,7 +3204,7 @@ module Brick
3151
3204
  # For any appended references (those that come from config), arrive upon a definitely unique constraint name
3152
3205
  pri_tbl = is_class ? fk[4][:class].underscore : pri_tbl
3153
3206
  pri_tbl = "#{bt_assoc_name}_#{pri_tbl}" if pri_tbl&.singularize != bt_assoc_name
3154
- cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
3207
+ cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", nil, bts, hms)
3155
3208
  missing = []
3156
3209
  missing << fk[1] unless relations.key?(fk[1])
3157
3210
  missing << primary_table unless is_class || relations.key?(primary_table)
@@ -3285,15 +3338,17 @@ module Brick
3285
3338
  end
3286
3339
  end
3287
3340
 
3288
- def ensure_unique(name, *sources)
3341
+ def ensure_unique(name, delimiter, *sources)
3289
3342
  base = name
3290
- if (added_num = name.slice!(/_(\d+)$/))
3343
+ delimiter ||= '_'
3344
+ # By default ends up building this regex: /_(\d+)$/
3345
+ if (added_num = name.slice!(Regexp.new("#{delimiter}(\d+)$")))
3291
3346
  added_num = added_num[1..-1].to_i
3292
3347
  else
3293
3348
  added_num = 1
3294
3349
  end
3295
3350
  while (
3296
- name = "#{base}_#{added_num += 1}"
3351
+ name = "#{base}#{delimiter}#{added_num += 1}"
3297
3352
  sources.each_with_object(nil) do |v, s|
3298
3353
  s || case v
3299
3354
  when Hash
@@ -3371,6 +3426,33 @@ module Brick
3371
3426
  end
3372
3427
  end
3373
3428
 
3429
+ def _brick_index(tbl_name, mode = nil, separator = nil, relation = nil)
3430
+ separator ||= '_'
3431
+ res_name = (tbl_name_parts = tbl_name.split('.'))[0..-2].first
3432
+ res_name << '.' if res_name
3433
+ (res_name ||= +'') << (relation || ::Brick.relations.fetch(tbl_name, nil)&.fetch(:resource, nil) || tbl_name_parts.last)
3434
+
3435
+ res_parts = ((mode == :singular) ? res_name.singularize : res_name).split('.')
3436
+ res_parts.shift if ::Brick.apartment_multitenant && res_parts.length > 1 && res_parts.first == ::Brick.apartment_default_tenant
3437
+ if (aps = relation&.fetch(:auto_prefixed_schema, nil)) && res_parts.last.start_with?(aps)
3438
+ last_part = res_parts.last[aps.length..-1]
3439
+ aps = aps[0..-2] if aps[-1] == '_'
3440
+ res_parts[-1] = aps
3441
+ res_parts << last_part
3442
+ end
3443
+ path_prefix = []
3444
+ if ::Brick.config.path_prefix
3445
+ res_parts.unshift(::Brick.config.path_prefix)
3446
+ path_prefix << ::Brick.config.path_prefix
3447
+ end
3448
+ index = res_parts.map(&:underscore).join(separator)
3449
+ index = index.tr('_', 'x') if separator == 'x'
3450
+ # Rails applies an _index suffix to that route when the resource name isn't something plural
3451
+ index << '_index' if mode != :singular && separator == '_' &&
3452
+ index == (path_prefix + [name&.underscore&.tr('/', '_') || '_']).join(separator)
3453
+ index
3454
+ end
3455
+
3374
3456
  def find_col_renaming(api_ver_path, relation_name)
3375
3457
  ::Brick.config.api_column_renaming&.fetch(
3376
3458
  api_ver_path,
@@ -774,9 +774,18 @@ window.addEventListener(\"popstate\", linkSchemas);
774
774
  end
775
775
  # %%% If we are not auto-creating controllers (or routes) then omit by default, and if enabled anyway, such as in a development
776
776
  # environment or whatever, then get either the controllers or routes list instead
777
- prefix = "#{::Brick.config.path_prefix}/" if ::Brick.config.path_prefix
778
- table_options = ::Brick.relations.each_with_object({}) do |rel, s|
779
- next if rel.first.is_a?(Symbol) || ::Brick.config.exclude_tables.include?(rel.first)
777
+ table_rels = if ::Brick.config.omit_empty_tables_in_dropdown
778
+ ::Brick.relations.reject { |k, v| k.is_a?(Symbol) || v[:rowcount] == 0 }
779
+ else
780
+ ::Brick.relations
781
+ end
782
+ table_options = table_rels.sort do |a, b|
783
+ a[0] = '' if a[0].is_a?(Symbol)
784
+ b[0] = '' if b[0].is_a?(Symbol)
785
+ a.first <=> b.first
786
+ end.each_with_object(+'') do |rel, s|
787
+ next if rel.first.blank? || rel.last[:cols].empty? ||
788
+ ::Brick.config.exclude_tables.include?(rel.first)
780
789
 
781
790
  tbl_parts = rel.first.split('.')
782
791
  if (aps = rel.last.fetch(:auto_prefixed_schema, nil))
@@ -784,16 +793,16 @@ window.addEventListener(\"popstate\", linkSchemas);
784
793
  aps = aps[0..-2] if aps[-1] == '_'
785
794
  tbl_parts[-2] = aps
786
795
  end
787
- if tbl_parts.first == apartment_default_schema
788
- tbl_parts.shift
789
- end
796
+ tbl_parts.shift if tbl_parts.first == apartment_default_schema
790
797
  # %%% When table_name_prefixes are use then during rendering empty non-TNP
791
798
  # entries get added at some point when an attempt is made to find the table.
792
799
  # Will have to hunt that down at some point.
793
- s[tbl_parts.join('.')] = nil unless rel.last[:cols].empty?
794
- end.keys.sort.each_with_object(+'') do |v, s|
795
- s << "<option value=\"#{prefix}#{v.underscore.gsub('.', '/')}\">#{v}</option>"
800
+ if (rowcount = rel.last.fetch(:rowcount, nil))
801
+ rowcount = rowcount > 0 ? " (#{rowcount})" : nil
802
+ end
803
+ s << "<option value=\"#{::Brick._brick_index(rel.first, nil, '/')}\">#{rel.first}#{rowcount}</option>"
796
804
  end.html_safe
805
+ prefix = "#{::Brick.config.path_prefix}/" if ::Brick.config.path_prefix
797
806
  table_options << "<option value=\"#{prefix}brick_status\">(Status)</option>".html_safe if ::Brick.config.add_status
798
807
  table_options << "<option value=\"#{prefix}brick_orphans\">(Orphans)</option>".html_safe if is_orphans
799
808
  table_options << "<option value=\"#{prefix}brick_crosstab\">(Crosstab)</option>".html_safe if is_crosstab
@@ -1476,7 +1485,7 @@ end
1476
1485
  %>
1477
1486
  <tr>
1478
1487
  <td><%= begin
1479
- kls = Object.const_get(::Brick.relations.fetch(r[0], nil)&.fetch(:class_name, nil))
1488
+ kls = Object.const_get((rel = ::Brick.relations.fetch(r[0], nil))&.fetch(:class_name, nil))
1480
1489
  rescue
1481
1490
  end
1482
1491
  if kls.is_a?(Class) && (path_helper = respond_to?(bi_path = \"#\{kls._brick_index}_path\".to_sym) ? bi_path : nil)
@@ -1489,7 +1498,10 @@ end
1489
1498
  else
1490
1499
  ' class=\"dimmed\"'
1491
1500
  end&.html_safe %>><%= # Table
1492
- r[1] %></td>
1501
+ if (rowcount = rel&.fetch(:rowcount, nil))
1502
+ rowcount = (rowcount > 0 ? \" (#\{rowcount})\" : nil)
1503
+ end
1504
+ \"#\{r[1]}#\{rowcount}\" %></td>
1493
1505
  <td<%= lines = r[2]&.map { |line| \"#\{line.first}:#\{line.last}\" }
1494
1506
  ' class=\"dimmed\"'.html_safe unless r[2] %>><%= # Migration
1495
1507
  lines&.join('<br>')&.html_safe %></td>
@@ -151,7 +151,7 @@ module Brick::Rails::FormTags
151
151
  # ActiveRecord::StatementTimeout in Warehouse::ColdRoomTemperatures_Archive#index
152
152
  # TinyTds::Error: Adaptive Server connection timed out
153
153
  # (After restarting the server it worked fine again.)
154
- rowCount = 0
154
+ row_count = 0
155
155
  relation.each do |obj|
156
156
  out << "<tr>\n"
157
157
  out << "<td class=\"col-sticky\">#{link_to('⇛', send("#{klass._brick_index(:singular)}_path".to_sym,
@@ -252,13 +252,16 @@ module Brick::Rails::FormTags
252
252
  out << '</td>'
253
253
  end
254
254
  out << '</tr>'
255
- rowCount += 1
255
+ row_count += 1
256
+ end
257
+ if (total_row_count = ::Brick.relations[table_name].fetch(:rowcount, nil))
258
+ total_row_count = total_row_count > row_count ? " (out of #{total_row_count})" : nil
256
259
  end
257
260
  out << " </tbody>
258
261
  </table>
259
262
  <script>
260
263
  var rowCount = document.getElementById(\"rowCount\");
261
- if (rowCount) rowCount.innerHTML = \"#{pluralize(rowCount, "row")} &nbsp;\";
264
+ if (rowCount) rowCount.innerHTML = \"#{pluralize(row_count, "row")}#{total_row_count} &nbsp;\";
262
265
  </script>
263
266
  "
264
267
 
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 190
8
+ TINY = 191
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
data/lib/brick.rb CHANGED
@@ -619,6 +619,10 @@ module Brick
619
619
  Brick.config.license = key
620
620
  end
621
621
 
622
+ def omit_empty_tables_in_dropdown=(setting)
623
+ Brick.config.omit_empty_tables_in_dropdown = setting
624
+ end
625
+
622
626
  def always_load_fields=(field_set)
623
627
  Brick.config.always_load_fields = field_set
624
628
  end
@@ -948,13 +952,14 @@ In config/initializers/brick.rb appropriate entries would look something like:
948
952
  object_name = k.split('.').last # Take off any first schema part
949
953
 
950
954
  full_schema_prefix = if (full_aps = aps = v.fetch(:auto_prefixed_schema, nil))
951
- aps = aps[0..-2] if aps[-1] == '_'
952
- (schema_prefix&.dup || +'') << "#{aps}."
953
- else
954
- schema_prefix
955
- end
955
+ aps = aps[0..-2] if aps[-1] == '_'
956
+ (schema_prefix&.dup || +'') << "#{aps}."
957
+ else
958
+ schema_prefix
959
+ end
956
960
 
957
961
  # Track routes being built
962
+ resource_name = v.fetch(:resource, nil) || k
958
963
  if (class_name = v.fetch(:class_name, nil))
959
964
  if v.key?(:isView)
960
965
  view_class_length = class_name.length if class_name.length > view_class_length
@@ -962,7 +967,7 @@ In config/initializers/brick.rb appropriate entries would look something like:
962
967
  else
963
968
  table_class_length = class_name.length if class_name.length > table_class_length
964
969
  tables
965
- end << [class_name, aps, k.tr('.', '/')[full_aps&.length || 0 .. -1]]
970
+ end << [class_name, aps, resource_name.tr('.', '/')[full_aps&.length || 0 .. -1]]
966
971
  end
967
972
 
968
973
  options = {}
@@ -973,7 +978,7 @@ In config/initializers/brick.rb appropriate entries would look something like:
973
978
  prefixes << [aps, v[:class_name]&.split('::')[-2]&.underscore] if aps
974
979
  prefixes << schema_name if schema_name
975
980
  prefixes << path_prefix if path_prefix
976
- brick_namespace_create.call(prefixes, v[:resource], options)
981
+ brick_namespace_create.call(prefixes, resource_name, options)
977
982
  sti_subclasses.fetch(class_name, nil)&.each do |sc| # Add any STI subclass routes for this relation
978
983
  brick_namespace_create.call(prefixes, sc.underscore.tr('/', '_').pluralize, options)
979
984
  end
@@ -89,7 +89,13 @@ module Brick
89
89
  [mig_path, is_insert_versions, is_delete_versions]
90
90
  end
91
91
 
92
- def generate_migrations(chosen, mig_path, is_insert_versions, is_delete_versions, relations = ::Brick.relations)
92
+ def generate_migrations(chosen, mig_path, is_insert_versions, is_delete_versions,
93
+ relations = ::Brick.relations, do_fks_last: nil, do_schema_migrations: true)
94
+ if do_fks_last.nil?
95
+ puts 'Would you like for the foreign keys to be built inline inside of each migration file, or as a final migration?'
96
+ do_fks_last = (gets_list(list: ['Inline', 'Separate final migration for all FKs']).start_with?('Separate'))
97
+ end
98
+
93
99
  is_sqlite = ActiveRecord::Base.connection.adapter_name == 'SQLite'
94
100
  key_type = ((is_sqlite || ActiveRecord.version < ::Gem::Version.new('5.1')) ? 'integer' : 'bigint')
95
101
  is_4x_rails = ActiveRecord.version < ::Gem::Version.new('5.0')
@@ -112,6 +118,7 @@ module Brick
112
118
  # Start by making migrations for fringe tables (those with no foreign keys).
113
119
  # Continue layer by layer, creating migrations for tables that reference ones already done, until
114
120
  # no more migrations can be created. (At that point hopefully all tables are accounted for.)
121
+ after_fks = [] # Track foreign keys to add after table creation
115
122
  while (fringe = chosen.reject do |tbl|
116
123
  snag_fks = []
117
124
  snags = relations.fetch(tbl, nil)&.fetch(:fks, nil)&.select do |_k, v|
@@ -131,166 +138,58 @@ module Brick
131
138
  end
132
139
  end).present?
133
140
  fringe.each do |tbl|
134
- next unless (relation = relations.fetch(tbl, nil))&.fetch(:cols, nil)&.present?
141
+ mig = gen_migration_columns(relations, tbl, (tbl_parts = tbl.split('.')), (add_fks = []),
142
+ key_type, is_4x_rails, ar_version, do_fks_last)
143
+ after_fks.concat(add_fks) if do_fks_last
144
+ versions_to_create << migration_file_write(mig_path, ::Brick._brick_index("create_#{tbl}", nil, 'x'), current_mig_time += 1.minute, ar_version, mig)
145
+ end
146
+ done.concat(fringe)
147
+ chosen -= done
148
+ end
135
149
 
136
- pkey_cols = (rpk = relation[:pkey].values.flatten) & (arpk = [::Brick.ar_base.primary_key].flatten.sort)
137
- # In case things aren't as standard
138
- if pkey_cols.empty?
139
- pkey_cols = if rpk.empty? && relation[:cols][arpk.first]&.first == key_type
140
- arpk
141
- elsif rpk.first
142
- rpk
143
- end
144
- end
145
- schema = if (tbl_parts = tbl.split('.')).length > 1
146
- if tbl_parts.first == (::Brick.default_schema || 'public')
147
- tbl_parts.shift
148
- nil
149
- else
150
- tbl_parts.first
151
- end
152
- end
153
- unless schema.blank? || built_schemas.key?(schema)
154
- mig = +" def change\n create_schema(:#{schema}) unless schema_exists?(:#{schema})\n end\n"
155
- migration_file_write(mig_path, "create_db_schema_#{schema.underscore}", current_mig_time += 1.minute, ar_version, mig)
156
- built_schemas[schema] = nil
157
- end
150
+ if do_fks_last
151
+ # Write out any more tables that haven't been done yet
152
+ chosen.each do |tbl|
153
+ mig = gen_migration_columns(relations, tbl, (tbl_parts = tbl.split('.')), (add_fks = []),
154
+ key_type, is_4x_rails, ar_version, do_fks_last)
155
+ after_fks.concat(add_fks)
156
+ migration_file_write(mig_path, ::Brick._brick_index("create_#{tbl}", nil, 'x'), current_mig_time += 1.minute, ar_version, mig)
157
+ end
158
+ done.concat(chosen)
159
+ chosen.clear
158
160
 
159
- # %%% For the moment we're skipping polymorphics
160
- fkey_cols = relation[:fks].values.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
161
- # If the primary key is also used as a foreign key, will need to do id: false and then build out
162
- # a column definition which includes :primary_key -- %%% also using a data type of bigserial or serial
163
- # if this one has come in as bigint or integer.
164
- pk_is_also_fk = fkey_cols.any? { |assoc| pkey_cols&.first == assoc[:fk] } ? pkey_cols&.first : nil
165
- # Support missing primary key (by adding: , id: false)
166
- id_option = if pk_is_also_fk || !pkey_cols&.present?
167
- needs_serial_col = true
168
- +', id: false'
169
- elsif ((pkey_col_first = (col_def = relation[:cols][pkey_cols&.first])&.first) &&
170
- (pkey_col_first = SQL_TYPES[pkey_col_first] || SQL_TYPES[col_def&.[](0..1)] ||
171
- SQL_TYPES.find { |r| r.first.is_a?(Regexp) && pkey_col_first =~ r.first }&.last ||
172
- pkey_col_first
173
- ) != key_type
174
- )
175
- case pkey_col_first
176
- when 'integer'
177
- +', id: :serial'
178
- when 'bigint'
179
- +', id: :bigserial'
180
- else
181
- +", id: :#{pkey_col_first}" # Something like: id: :integer, primary_key: :businessentityid
182
- end +
183
- (pkey_cols.first ? ", primary_key: :#{pkey_cols.first}" : '')
184
- end
185
- if !id_option && pkey_cols.sort != arpk
186
- id_option = +", primary_key: :#{pkey_cols.first}"
187
- end
188
- if !is_4x_rails && (comment = relation&.fetch(:description, nil))&.present?
189
- (id_option ||= +'') << ", comment: #{comment.inspect}"
190
- end
191
- # Find the ActiveRecord class in order to see if the columns have comments
192
- unless is_4x_rails
193
- klass = begin
194
- tbl.tr('.', '/').singularize.camelize.constantize
195
- rescue StandardError
196
- end
197
- if klass
198
- unless ActiveRecord::Migration.table_exists?(klass.table_name)
199
- puts "WARNING: Unable to locate table #{klass.table_name} (for #{klass.name})."
200
- klass = nil
201
- end
202
- end
203
- end
204
- # Refer to this table name as a symbol or dotted string as appropriate
205
- tbl_code = tbl_parts.length == 1 ? ":#{tbl_parts.first}" : "'#{tbl}'"
206
- mig = +" def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
207
- mig << " create_table #{tbl_code}#{id_option} do |t|\n"
208
- possible_ts = [] # Track possible generic timestamps
209
- add_fks = [] # Track foreign keys to add after table creation
210
- relation[:cols].each do |col, col_type|
211
- sql_type = SQL_TYPES[col_type.first] || SQL_TYPES[col_type[0..1]] ||
212
- SQL_TYPES.find { |r| r.first.is_a?(Regexp) && col_type.first =~ r.first }&.last ||
213
- col_type.first
214
- suffix = col_type[3] || pkey_cols&.include?(col) ? +', null: false' : +''
215
- suffix << ', array: true' if (col_type.first == 'ARRAY')
216
- if !is_4x_rails && klass && (comment = klass.columns_hash.fetch(col, nil)&.comment)&.present?
217
- suffix << ", comment: #{comment.inspect}"
218
- end
219
- # Determine if this column is used as part of a foreign key
220
- if (fk = fkey_cols.find { |assoc| col == assoc[:fk] })
221
- to_table = fk[:inverse_table].split('.')
222
- to_table = to_table.length == 1 ? ":#{to_table.first}" : "'#{fk[:inverse_table]}'"
223
- if needs_serial_col && pkey_cols&.include?(col) && (new_serial_type = {'integer' => 'serial', 'bigint' => 'bigserial'}[sql_type])
224
- sql_type = new_serial_type
225
- needs_serial_col = false
226
- end
227
- if fk[:fk] != "#{fk[:assoc_name].singularize}_id" # Need to do our own foreign_key tricks, not use references?
228
- column = fk[:fk]
229
- mig << emit_column(sql_type, column, suffix)
230
- add_fks << [to_table, column, relations[fk[:inverse_table]]]
231
- else
232
- suffix << ", type: :#{sql_type}" unless sql_type == key_type
233
- # Will the resulting default index name be longer than what Postgres allows? (63 characters)
234
- if (idx_name = ActiveRecord::Base.connection.index_name(tbl, {column: col})).length > 63
235
- # Try to find a shorter name that hasn't been used yet
236
- unless indexes.key?(shorter = idx_name[0..62]) ||
237
- indexes.key?(shorter = idx_name.tr('_', '')[0..62]) ||
238
- indexes.key?(shorter = idx_name.tr('aeio', '')[0..62])
239
- puts "Unable to easily find unique name for index #{idx_name} that is shorter than 64 characters,"
240
- puts "so have resorted to this GUID-based identifier: #{shorter = "#{tbl[0..25]}_#{::SecureRandom.uuid}"}."
241
- end
242
- suffix << ", index: { name: '#{shorter || idx_name}' }"
243
- indexes[shorter || idx_name] = nil
244
- end
245
- primary_key = nil
246
- begin
247
- primary_key = relations[fk[:inverse_table]][:class_name]&.constantize&.primary_key
248
- rescue NameError => e
249
- primary_key = ::Brick.ar_base.primary_key
250
- end
251
- mig << " t.references :#{fk[:assoc_name]}#{suffix}, foreign_key: { to_table: #{to_table}#{", primary_key: :#{primary_key}" if primary_key != ::Brick.ar_base.primary_key} }\n"
252
- end
253
- else
254
- next if !id_option&.end_with?('id: false') && pkey_cols&.include?(col)
161
+ # Add a final migration to create all the foreign keys
162
+ mig = +" def change\n"
163
+ after_fks.each do |add_fk|
164
+ next unless add_fk[2] # add_fk[2] holds the inverse relation
255
165
 
256
- # See if there are generic timestamps
257
- if sql_type == 'timestamp' && ['created_at','updated_at'].include?(col)
258
- possible_ts << [col, !col_type[3]]
259
- else
260
- mig << emit_column(sql_type, col, suffix)
261
- end
262
- end
263
- end
264
- if possible_ts.length == 2 && # Both created_at and updated_at
265
- # Rails 5 and later timestamps default to NOT NULL
266
- (possible_ts.first.last == is_4x_rails && possible_ts.last.last == is_4x_rails)
267
- mig << "\n t.timestamps\n"
268
- else # Just one or the other, or a nullability mismatch
269
- possible_ts.each { |ts| emit_column('timestamp', ts.first, nil) }
166
+ unless (pk = add_fk[2][:pkey].values.flatten&.first)
167
+ # No official PK, but if coincidentally there's a column of the same name, take a chance on it
168
+ pk = (add_fk[2][:cols].key?(add_fk[1]) && add_fk[1]) || '???'
270
169
  end
271
- mig << " end\n"
272
- if pk_is_also_fk
273
- mig << " reversible do |dir|\n"
274
- mig << " dir.up { execute('ALTER TABLE #{tbl} ADD PRIMARY KEY (#{pk_is_also_fk})') }\n"
275
- mig << " end\n"
276
- end
277
- add_fks.each do |add_fk|
278
- is_commented = false
279
- # add_fk[2] holds the inverse relation
280
- unless (pk = add_fk[2][:pkey].values.flatten&.first)
281
- is_commented = true
282
- mig << " # (Unable to create relationship because primary key is missing on table #{add_fk[0]})\n"
283
- # No official PK, but if coincidentally there's a column of the same name, take a chance on it
284
- pk = (add_fk[2][:cols].key?(add_fk[1]) && add_fk[1]) || '???'
285
- end
286
- # to_table column
287
- mig << " #{'# ' if is_commented}add_foreign_key #{tbl_code}, #{add_fk[0]}, column: :#{add_fk[1]}, primary_key: :#{pk}\n"
288
- end
289
- mig << " end\n"
290
- versions_to_create << migration_file_write(mig_path, "create_#{tbl_parts.map(&:underscore).join('_')}", current_mig_time += 1.minute, ar_version, mig)
170
+ mig << " add_foreign_key #{add_fk[3]}, " # The tbl_code
171
+ # to_table column
172
+ mig << "#{add_fk[0]}, column: :#{add_fk[1]}, primary_key: :#{pk}\n"
291
173
  end
292
- done.concat(fringe)
293
- chosen -= done
174
+ if after_fks.length > 500
175
+ minutes = (after_fks.length + 1000) / 1500
176
+ mig << " if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'\n"
177
+ mig << " puts 'NOTE: It could take around #{minutes} #{'minute'.pluralize(minutes)} on a FAST machine for Postgres to do all the final processing for these foreign keys. Please be patient!'\n"
178
+
179
+ mig << " # Vacuum takes only about ten seconds when all the tables are empty,
180
+ # and about 2 minutes when the tables are fairly full.
181
+ execute('COMMIT')
182
+ execute('VACUUM FULL')
183
+ execute('BEGIN TRANSACTION')
184
+ end\n"
185
+ end
186
+
187
+ mig << +" end\n"
188
+ migration_file_write(mig_path, 'create_brick_fks.rbx', current_mig_time += 1.minute, ar_version, mig)
189
+ puts "Have written out a final migration called 'create_brick_fks.rbx' which creates #{after_fks.length} foreign keys.
190
+ This file extension (.rbx) will cause it not to run yet when you do a 'rails db:migrate'.
191
+ The idea here is to do all data loading first, and then rename that migration file back
192
+ into having a .rb extension, and run a final db:migrate to put the foreign keys in place."
294
193
  end
295
194
 
296
195
  stuck_counts = Hash.new { |h, k| h[k] = 0 }
@@ -310,7 +209,7 @@ module Brick
310
209
  ". Here's the top 5 blockers" if stuck_sorted.length > 5
311
210
  }:"
312
211
  pp stuck_sorted[0..4]
313
- else # Successful, and now we can update the schema_migrations table accordingly
212
+ elsif do_schema_migrations # Successful, and now we can update the schema_migrations table accordingly
314
213
  unless ActiveRecord::Migration.table_exists?(ActiveRecord::Base.schema_migrations_table_name)
315
214
  ActiveRecord::SchemaMigration.create_table
316
215
  end
@@ -333,13 +232,178 @@ module Brick
333
232
 
334
233
  private
335
234
 
235
+ def gen_migration_columns(relations, tbl, tbl_parts, add_fks,
236
+ key_type, is_4x_rails, ar_version, do_fks_last)
237
+ return unless (relation = relations.fetch(tbl, nil))&.fetch(:cols, nil)&.present?
238
+
239
+ mig = +''
240
+ pkey_cols = (rpk = relation[:pkey].values.flatten) & (arpk = [::Brick.ar_base.primary_key].flatten.sort)
241
+ # In case things aren't as standard
242
+ if pkey_cols.empty?
243
+ pkey_cols = if rpk.empty? && relation[:cols][arpk.first]&.first == key_type
244
+ arpk
245
+ elsif rpk.first
246
+ rpk
247
+ end
248
+ end
249
+ schema = if tbl_parts.length > 1
250
+ if tbl_parts.first == (::Brick.default_schema || 'public')
251
+ tbl_parts.shift
252
+ nil
253
+ else
254
+ tbl_parts.first
255
+ end
256
+ end
257
+ unless schema.blank? || built_schemas.key?(schema)
258
+ mig = +" def change\n create_schema(:#{schema}) unless schema_exists?(:#{schema})\n end\n"
259
+ migration_file_write(mig_path, "create_db_schema_#{schema.underscore}", current_mig_time += 1.minute, ar_version, mig)
260
+ built_schemas[schema] = nil
261
+ end
262
+
263
+ # %%% For the moment we're skipping polymorphics
264
+ fkey_cols = relation[:fks].values.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
265
+ # If the primary key is also used as a foreign key, will need to do id: false and then build out
266
+ # a column definition which includes :primary_key -- %%% also using a data type of bigserial or serial
267
+ # if this one has come in as bigint or integer.
268
+ pk_is_also_fk = fkey_cols.any? { |assoc| pkey_cols&.first == assoc[:fk] } ? pkey_cols&.first : nil
269
+ id_option = if pk_is_also_fk || !pkey_cols&.present?
270
+ needs_serial_col = true
271
+ +', id: false' # Support missing primary key (by adding: , id: false)
272
+ elsif ((pkey_col_first = (col_def = relation[:cols][pkey_cols&.first])&.first) &&
273
+ (pkey_col_first = SQL_TYPES[pkey_col_first] || SQL_TYPES[col_def&.[](0..1)] ||
274
+ SQL_TYPES.find { |r| r.first.is_a?(Regexp) && pkey_col_first =~ r.first }&.last ||
275
+ pkey_col_first
276
+ ) != key_type
277
+ )
278
+ case pkey_col_first
279
+ when 'integer'
280
+ +', id: :serial'
281
+ when 'bigint'
282
+ +', id: :bigserial'
283
+ else
284
+ +", id: :#{pkey_col_first}" # Something like: id: :integer, primary_key: :businessentityid
285
+ end +
286
+ (pkey_cols.first ? ", primary_key: :#{pkey_cols.first}" : '')
287
+ end
288
+ if !id_option && pkey_cols.sort != arpk
289
+ id_option = +", primary_key: :#{pkey_cols.first}"
290
+ end
291
+ if !is_4x_rails && (comment = relation&.fetch(:description, nil))&.present?
292
+ (id_option ||= +'') << ", comment: #{comment.inspect}"
293
+ end
294
+ # Find the ActiveRecord class in order to see if the columns have comments
295
+ unless is_4x_rails
296
+ klass = begin
297
+ tbl.tr('.', '/').singularize.camelize.constantize
298
+ rescue StandardError
299
+ end
300
+ if klass
301
+ unless ActiveRecord::Migration.table_exists?(klass.table_name)
302
+ puts "WARNING: Unable to locate table #{klass.table_name} (for #{klass.name})."
303
+ klass = nil
304
+ end
305
+ end
306
+ end
307
+ # Refer to this table name as a symbol or dotted string as appropriate
308
+ tbl_code = tbl_parts.length == 1 ? ":#{tbl_parts.first}" : "'#{tbl}'"
309
+ mig = +" def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
310
+ mig << " create_table #{tbl_code}#{id_option} do |t|\n"
311
+ possible_ts = [] # Track possible generic timestamps
312
+ relation[:cols].each do |col, col_type|
313
+ sql_type = SQL_TYPES[col_type.first] || SQL_TYPES[col_type[0..1]] ||
314
+ SQL_TYPES.find { |r| r.first.is_a?(Regexp) && col_type.first =~ r.first }&.last ||
315
+ col_type.first
316
+ suffix = col_type[3] || pkey_cols&.include?(col) ? +', null: false' : +''
317
+ suffix << ', array: true' if (col_type.first == 'ARRAY')
318
+ if !is_4x_rails && klass && (comment = klass.columns_hash.fetch(col, nil)&.comment)&.present?
319
+ suffix << ", comment: #{comment.inspect}"
320
+ end
321
+ # Determine if this column is used as part of a foreign key
322
+ if (fk = fkey_cols.find { |assoc| col == assoc[:fk] })
323
+ to_table = fk[:inverse_table].split('.')
324
+ to_table = to_table.length == 1 ? ":#{to_table.first}" : "'#{fk[:inverse_table]}'"
325
+ if needs_serial_col && pkey_cols&.include?(col) && (new_serial_type = {'integer' => 'serial', 'bigint' => 'bigserial'}[sql_type])
326
+ sql_type = new_serial_type
327
+ needs_serial_col = false
328
+ end
329
+ if do_fks_last || (fk[:fk] != "#{fk[:assoc_name].singularize}_id") # Need to do our own foreign_key tricks, not use references?
330
+ column = fk[:fk]
331
+ mig << emit_column(sql_type, column, suffix)
332
+ add_fks << [to_table, column, relations[fk[:inverse_table]], tbl_code]
333
+ else
334
+ suffix << ", type: :#{sql_type}" unless sql_type == key_type
335
+ # Will the resulting default index name be longer than what Postgres allows? (63 characters)
336
+ if (idx_name = ActiveRecord::Base.connection.index_name(tbl, {column: col})).length > 63
337
+ # Try to find a shorter name that hasn't been used yet
338
+ unless indexes.key?(shorter = idx_name[0..62]) ||
339
+ indexes.key?(shorter = idx_name.tr('_', '')[0..62]) ||
340
+ indexes.key?(shorter = idx_name.tr('aeio', '')[0..62])
341
+ puts "Unable to easily find unique name for index #{idx_name} that is shorter than 64 characters,"
342
+ puts "so have resorted to this GUID-based identifier: #{shorter = "#{tbl[0..25]}_#{::SecureRandom.uuid}"}."
343
+ end
344
+ suffix << ", index: { name: '#{shorter || idx_name}' }"
345
+ indexes[shorter || idx_name] = nil
346
+ end
347
+ next if do_fks_last
348
+
349
+ primary_key = nil
350
+ begin
351
+ primary_key = relations[fk[:inverse_table]][:class_name]&.constantize&.primary_key
352
+ rescue NameError => e
353
+ primary_key = ::Brick.ar_base.primary_key
354
+ end
355
+ fk_stuff = ", foreign_key: { to_table: #{to_table}#{", primary_key: :#{primary_key}" if primary_key != ::Brick.ar_base.primary_key} }"
356
+ mig << " t.references :#{fk[:assoc_name]}#{suffix}#{fk_stuff}\n"
357
+ end
358
+ else
359
+ next if !id_option&.end_with?('id: false') && pkey_cols&.include?(col)
360
+
361
+ # See if there are generic timestamps
362
+ if sql_type == 'timestamp' && ['created_at','updated_at'].include?(col)
363
+ possible_ts << [col, !col_type[3]]
364
+ else
365
+ mig << emit_column(sql_type, col, suffix)
366
+ end
367
+ end
368
+ end
369
+ if possible_ts.length == 2 && # Both created_at and updated_at
370
+ # Rails 5 and later timestamps default to NOT NULL
371
+ (possible_ts.first.last == is_4x_rails && possible_ts.last.last == is_4x_rails)
372
+ mig << "\n t.timestamps\n"
373
+ else # Just one or the other, or a nullability mismatch
374
+ possible_ts.each { |ts| emit_column('timestamp', ts.first, nil) }
375
+ end
376
+ mig << " end\n"
377
+ if pk_is_also_fk
378
+ mig << " reversible do |dir|\n"
379
+ mig << " dir.up { execute('ALTER TABLE #{tbl} ADD PRIMARY KEY (#{pk_is_also_fk})') }\n"
380
+ mig << " end\n"
381
+ end
382
+ add_fks.each do |add_fk|
383
+ next unless add_fk[2]
384
+
385
+ is_commented = false
386
+ # add_fk[2] holds the inverse relation
387
+ unless (pk = add_fk[2][:pkey]&.values&.flatten&.first)
388
+ is_commented = true
389
+ mig << " # (Unable to create relationship because primary key is missing on table #{add_fk[0]})\n"
390
+ # No official PK, but if coincidentally there's a column of the same name, take a chance on it
391
+ pk = (add_fk[2][:cols].key?(add_fk[1]) && add_fk[1]) || '???'
392
+ end
393
+ mig << " #{'# ' if do_fks_last}#{'# ' if is_commented}add_foreign_key #{tbl_code}, "
394
+ # to_table column
395
+ mig << "#{add_fk[0]}, column: :#{add_fk[1]}, primary_key: :#{pk}\n"
396
+ end
397
+ mig << " end\n"
398
+ end
399
+
336
400
  def emit_column(type, name, suffix)
337
401
  " t.#{type.start_with?('numeric') ? 'decimal' : type} :#{name}#{suffix}\n"
338
402
  end
339
403
 
340
404
  def migration_file_write(mig_path, name, current_mig_time, ar_version, mig)
341
- File.open("#{mig_path}/#{version = current_mig_time.strftime('%Y%m%d%H%M00')}_#{name}.rb", "w") do |f|
342
- f.write "class #{name.camelize} < ActiveRecord::Migration#{ar_version}\n"
405
+ File.open("#{mig_path}/#{version = current_mig_time.strftime('%Y%m%d%H%M00')}_#{name}#{'.rb' unless name.index('.')}", "w") do |f|
406
+ f.write "class #{name.split('.').first.camelize} < ActiveRecord::Migration#{ar_version}\n"
343
407
  f.write mig
344
408
  f.write "end\n"
345
409
  end
@@ -26,6 +26,7 @@ module Brick
26
26
  end
27
27
 
28
28
  mig_path, is_insert_versions, is_delete_versions = ::Brick::MigrationBuilder.check_folder
29
+ return unless mig_path
29
30
 
30
31
  # Generate a list of tables that can be chosen
31
32
  chosen = gets_list(list: tables, chosen: tables.dup)
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'brick'
4
+ require 'rails/generators'
5
+ require 'fancy_gets'
6
+ require 'generators/brick/migration_builder'
7
+ require 'generators/brick/salesforce_schema'
8
+
9
+ module Brick
10
+ # Auto-generates migration files
11
+ class SalesforceMigrationsGenerator < ::Rails::Generators::Base
12
+ include FancyGets
13
+ desc 'Auto-generates migration files for a set of Salesforce tables and columns.'
14
+
15
+ argument :wsdl_file, type: :string, default: ''
16
+
17
+ def brick_salesforce_migrations
18
+ ::Brick.apply_double_underscore_patch
19
+ # ::Brick.mode = :on
20
+ # ActiveRecord::Base.establish_connection
21
+
22
+ # Runs at the end of parsing Salesforce WSDL, and uses the discovered tables and columns to create migrations
23
+ relations = nil
24
+ end_document_proc = lambda do |salesforce_tables|
25
+ # p [:end_document]
26
+ mig_path, is_insert_versions, is_delete_versions = ::Brick::MigrationBuilder.check_folder
27
+ return unless mig_path
28
+
29
+ # Generate a list of tables that can be chosen
30
+ table_names = salesforce_tables.keys
31
+ chosen = gets_list(list: table_names, chosen: table_names.dup)
32
+
33
+ soap_data_types = {
34
+ 'tns:ID' => 'string',
35
+ 'xsd:string' => 'string',
36
+ 'xsd:dateTime' => 'datetime',
37
+ 'xsd:boolean' => 'boolean',
38
+ 'xsd:double' => 'float',
39
+ 'xsd:int' => 'integer',
40
+ 'xsd:date' => 'date',
41
+ 'xsd:anyType' => 'string', # Don't fully know on this
42
+ 'xsd:long' => 'bigint',
43
+ 'xsd:base64Binary' => 'bytea',
44
+ 'xsd:time' => 'time'
45
+ }
46
+ fk_idx = 0
47
+ # Build out a '::Brick.relations' hash that represents this Salesforce schema
48
+ relations = chosen.each_with_object({}) do |tbl_name, s|
49
+ tbl = salesforce_tables[tbl_name]
50
+ # Build out columns and foreign keys
51
+ cols = { 'id'=>['string', nil, false, true] }
52
+ fks = {}
53
+ tbl[:cols].each do |col|
54
+ next if col[:name] == 'Id'
55
+
56
+ dt = soap_data_types[col[:data_type]] || 'string'
57
+ cols[col[:name]] = [dt, nil, col[:nillable], false]
58
+ if (ref_to = col[:fk_reference_to])
59
+ fk_hash = {
60
+ is_bt: true,
61
+ fk: col[:name],
62
+ assoc_name: "#{col[:name]}_bt",
63
+ inverse_table: ref_to
64
+ }
65
+ fks["fk_salesforce_#{fk_idx += 1}"] = fk_hash
66
+ end
67
+ end
68
+ # Put it all into a relation entry, named the same as the table
69
+ s[tbl_name] = {
70
+ pkey: { "#{tbl_name}_pkey" => ['id'] },
71
+ cols: cols,
72
+ fks: fks
73
+ }
74
+ end
75
+ # Build but do not have foreign keys established yet, and do not put version entries info the schema_migrations table
76
+ ::Brick::MigrationBuilder.generate_migrations(chosen, mig_path, is_insert_versions, is_delete_versions, relations,
77
+ do_fks_last: true, do_schema_migrations: false)
78
+ end
79
+ parser = Nokogiri::XML::SAX::Parser.new(::Brick::SalesforceSchema.new(end_document_proc))
80
+ # The WSDL file must have a .xml extension, and can be in any folder in the project
81
+ # Alternatively the user can supply this option on the command line
82
+ @wsdl_file = nil if @wsdl_file == ''
83
+ loop do
84
+ break if (@wsdl_file ||= gets_list(Dir['**/*.xml'] + ['* Cancel *'])) == '* Cancel *'
85
+
86
+ parser.parse(File.read(@wsdl_file))
87
+
88
+ if relations.length > 300
89
+ puts "A Salesforce installation generally has hundreds to a few thousand tables, and many are empty.
90
+ In order to more easily navigate just those tables that have content, you might want to add this
91
+ to brick.rb:
92
+ ::Brick.omit_empty_tables_in_dropdown = true"
93
+ end
94
+ break
95
+ rescue Errno::ENOENT
96
+ puts "File \"#{@wsdl_file}\" is not found."
97
+ @wsdl_file = nil
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brick
4
+ class SalesforceSchema < Nokogiri::XML::SAX::Document
5
+ include ::Brick::MigrationBuilder
6
+
7
+ attr_reader :end_document_proc
8
+
9
+ def initialize(end_doc_proc)
10
+ @end_document_proc = end_doc_proc
11
+ end
12
+
13
+ def start_document
14
+ # p [:start_document]
15
+ @salesforce_tables = {}
16
+ @text_stack = []
17
+ @all_extends = {}
18
+ puts 'Each dot is a table:'
19
+ end
20
+
21
+ def end_document
22
+ puts
23
+ end_document_proc&.call(@salesforce_tables)
24
+ end
25
+
26
+ def start_element_namespace(name, attrs = [], prefix = nil, uri = nil, ns = [])
27
+ # p [:start_element, name, attrs, prefix, uri, ns]
28
+ case name
29
+ when 'complexType' # Table
30
+ @last_table = attrs.find { |a| a.localname == 'name' }&.value
31
+ @fks = {}
32
+ # if attrs.first&.value&.end_with?('__c') # Starts as a string
33
+ when 'extension'
34
+ @last_extension = attrs.find { |a| a.localname == 'base' }.value
35
+ when 'element' # Column
36
+ # Extremely rarely this is nil!
37
+ data_type = attrs.find { |a| a.localname == 'type' }&.value
38
+ return if !@last_table || data_type.nil? || data_type == 'tns:QueryResult'
39
+
40
+ # Promoted to a real SalesforceTable object
41
+ if @last_table.is_a?(String)
42
+ @last_table = @salesforce_tables[@last_table] = { extend: @salesforce_tables[@last_extension] }
43
+ end
44
+
45
+ col_name = attrs.find { |a| a.localname == 'name' }&.value
46
+
47
+ # Foreign key reference?
48
+ if data_type&.start_with?('ens:')
49
+ foreign_table = data_type[4..]
50
+ if col_name.end_with?('__r')
51
+ @fks["#{col_name[0..-2]}c"] = foreign_table
52
+ else # if col_name.end_with?('Id')
53
+ @fks["#{col_name}Id"] = foreign_table
54
+ end
55
+ return
56
+ end
57
+
58
+ # Rarely this is nil
59
+ nillable = attrs.find { |a| a.localname == 'nillable' }&.value == 'true'
60
+ min_occurs = attrs.find { |a| a.localname == 'minOccurs' }&.value || -2
61
+ min_occurs = -1 if min_occurs == 'unbounded'
62
+ max_occurs = attrs.find { |a| a.localname == 'maxOccurs' }&.value || -2
63
+ max_occurs = -1 if max_occurs == 'unbounded'
64
+ col_options = { name: col_name, data_type: :data_type, nillable: :nillable, min_occurs: :min_occurs, max_occurs: :max_occurs }
65
+
66
+ (@last_table[:cols] ||= []) << col_options
67
+ end
68
+ @text_stack.push +''
69
+ end
70
+
71
+ def end_element_namespace(name, prefix = nil, uri = nil)
72
+ # p [:end_element, name, prefix, uri]
73
+ texts = @text_stack.pop
74
+ case name
75
+ when 'extension'
76
+ @last_extension = nil
77
+ when 'complexType'
78
+ if @last_table && !@last_table.is_a?(String)
79
+ # Do up any foreign keys
80
+ @fks.each do |k, v|
81
+ # Only a few records set up like this, going to sObject
82
+ if
83
+ # (k.downcase.end_with?('recordid') &&
84
+ # (fk_col = @last_table[:cols].find { |t| t[:name] == "#{k[0..-9]}Id" })
85
+ # ) ||
86
+ (fk_col = @last_table[:cols].find { |t| t[:name] == k })
87
+ fk_col[:fk_reference_to] = v
88
+ # puts "Skipping #{@last_table[:name]} / #{k}"
89
+ end
90
+ end
91
+ end
92
+ print '.'
93
+ @last_table = nil
94
+ end
95
+ # p [:end_element_texts, name, texts]
96
+ end
97
+
98
+ def characters(string)
99
+ # p [:characters, string]
100
+ @text_stack.each do |text|
101
+ text << string
102
+ end
103
+ end
104
+ end
105
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brick
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.190
4
+ version: 1.0.191
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-23 00:00:00.000000000 Z
11
+ date: 2023-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -258,6 +258,8 @@ files:
258
258
  - lib/generators/brick/migration_builder.rb
259
259
  - lib/generators/brick/migrations_generator.rb
260
260
  - lib/generators/brick/models_generator.rb
261
+ - lib/generators/brick/salesforce_migrations_generator.rb
262
+ - lib/generators/brick/salesforce_schema.rb
261
263
  - lib/generators/brick/seeds_generator.rb
262
264
  - lib/generators/brick/templates/add_object_changes_to_versions.rb.erb
263
265
  - lib/generators/brick/templates/create_versions.rb.erb