brick 1.0.71 → 1.0.73

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.
@@ -68,13 +68,14 @@ module ActiveRecord
68
68
  end
69
69
 
70
70
  def self._brick_primary_key(relation = nil)
71
- return instance_variable_get(:@_brick_primary_key) if instance_variable_defined?(:@_brick_primary_key)
71
+ return @_brick_primary_key if instance_variable_defined?(:@_brick_primary_key)
72
72
 
73
73
  pk = begin
74
74
  primary_key.is_a?(String) ? [primary_key] : primary_key || []
75
75
  rescue
76
76
  []
77
77
  end
78
+ pk.map! { |pk_part| pk_part =~ /^[A-Z0-9_]+$/ ? pk_part.downcase : pk_part } unless connection.adapter_name == 'MySQL2'
78
79
  # Just return [] if we're missing any part of the primary key. (PK is usually just "id")
79
80
  if relation && pk.present?
80
81
  @_brick_primary_key ||= pk.any? { |pk_part| !relation[:cols].key?(pk_part) } ? [] : pk
@@ -106,7 +107,8 @@ module ActiveRecord
106
107
  bracket_name = nil
107
108
  prefix = [prefix] unless prefix.is_a?(Array)
108
109
  if (dsl = ::Brick.config.model_descrips[name] || brick_get_dsl)
109
- dsl2 = +''
110
+ dsl2 = +'' # To replace our own DSL definition in case it needs to be expanded
111
+ dsl3 = +'' # To return expanded DSL that is nested from another model
110
112
  klass = nil
111
113
  dsl.each_char do |ch|
112
114
  if bracket_name
@@ -128,11 +130,16 @@ module ActiveRecord
128
130
  end
129
131
  if klass.column_names.exclude?(parts.last) &&
130
132
  (klass = (orig_class = klass).reflect_on_association(possible_dsl = parts.pop.to_sym)&.klass)
133
+ # Expand this entry which refers to an association name
131
134
  members2, dsl2a = klass.brick_parse_dsl(build_array, prefix + [possible_dsl], translations, true)
132
135
  members += members2
133
136
  dsl2 << dsl2a
137
+ dsl3 << dsl2a
134
138
  else
135
139
  dsl2 << "[#{bracket_name}]"
140
+ if emit_dsl
141
+ dsl3 << "[#{prefix[1..-1].map { |p| "#{p.to_s}." }.join if prefix.length > 1}#{bracket_name}]"
142
+ end
136
143
  members << parts
137
144
  end
138
145
  bracket_name = nil
@@ -144,18 +151,22 @@ module ActiveRecord
144
151
  klass = self
145
152
  else
146
153
  dsl2 << ch
154
+ dsl3 << ch
147
155
  end
148
156
  end
149
- # Rewrite the DSL just in case it's different because we had to expand it
150
- unless emit_dsl
151
- # puts "Compare:\n #{dsl}\n #{dsl2}"
152
- ::Brick.config.model_descrips[name] = dsl2
153
- end
157
+ # Rewrite the DSL in case it's now different from having to expand it
158
+ # if ::Brick.config.model_descrips[name] != dsl2
159
+ # puts ::Brick.config.model_descrips[name]
160
+ # puts dsl2.inspect
161
+ # puts dsl3.inspect
162
+ # binding.pry
163
+ # end
164
+ ::Brick.config.model_descrips[name] = dsl2 unless emit_dsl
154
165
  else # With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
155
166
  x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
156
167
  x[prefix.last] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
157
168
  end
158
- emit_dsl ? [members, dsl2] : members
169
+ emit_dsl ? [members, dsl3] : members
159
170
  end
160
171
 
161
172
  # If available, parse simple DSL attached to a model in order to provide a friendlier name.
@@ -223,13 +234,21 @@ module ActiveRecord
223
234
  end
224
235
 
225
236
  def self.bt_link(assoc_name)
226
- model_underscore = name.underscore
227
237
  assoc_name = CGI.escapeHTML(assoc_name.to_s)
228
- model_path = Rails.application.routes.url_helpers.send("#{model_underscore.tr('/', '_').pluralize}_path".to_sym)
238
+ model_path = Rails.application.routes.url_helpers.send("#{_brick_index}_path".to_sym)
229
239
  av_class = Class.new.extend(ActionView::Helpers::UrlHelper)
230
240
  av_class.extend(ActionView::Helpers::TagHelper) if ActionView.version < ::Gem::Version.new('7')
231
241
  link = av_class.link_to(name, model_path)
232
- model_underscore == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
242
+ table_name == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
243
+ end
244
+
245
+ def self._brick_index
246
+ tbl_parts = table_name.split('.')
247
+ tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.first == Apartment.default_schema
248
+ if (index = tbl_parts.map(&:underscore).join('_')) == index.singularize
249
+ index << '_index' # Rails applies an _index suffix to that route when the resource name is singular
250
+ end
251
+ index
233
252
  end
234
253
 
235
254
  def self.brick_import_template
@@ -323,7 +342,7 @@ module ActiveRecord
323
342
  end
324
343
 
325
344
  class Relation
326
- attr_reader :_brick_chains
345
+ attr_reader :_brick_chains, :_arel_applied_aliases
327
346
 
328
347
  # CLASS STUFF
329
348
  def _recurse_arel(piece, prefix = '')
@@ -361,7 +380,7 @@ module ActiveRecord
361
380
  # parent = piece.left == on.left.relation ? on.right.relation : on.left.relation
362
381
  # binding.pry if piece.left.is_a?(Arel::Nodes::TableAlias)
363
382
  if table.is_a?(Arel::Nodes::TableAlias)
364
- alias_name = table.right
383
+ @_arel_applied_aliases << (alias_name = table.right)
365
384
  table = table.left
366
385
  end
367
386
  (_brick_chains[table._arel_table_type] ||= []) << (alias_name || table.table_alias || table.name)
@@ -387,6 +406,7 @@ module ActiveRecord
387
406
 
388
407
  # INSTANCE STUFF
389
408
  def _arel_alias_names
409
+ @_arel_applied_aliases = []
390
410
  # %%% If with Rails 3.1 and older you get "NoMethodError: undefined method `eq' for nil:NilClass"
391
411
  # when trying to call relation.arel, then somewhere along the line while navigating a has_many
392
412
  # relationship it can't find the proper foreign key.
@@ -408,7 +428,7 @@ module ActiveRecord
408
428
  is_distinct = nil
409
429
  wheres = {}
410
430
  params.each do |k, v|
411
- next if ['_brick_schema', '_brick_order'].include?(k)
431
+ next if ['_brick_schema', '_brick_order', 'controller', 'action'].include?(k)
412
432
 
413
433
  case (ks = k.split('.')).length
414
434
  when 1
@@ -428,23 +448,11 @@ module ActiveRecord
428
448
  # %%% Skip the metadata columns
429
449
  if selects&.empty? # Default to all columns
430
450
  tbl_no_schema = table.name.split('.').last
451
+ # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
452
+ # ActiveRecord::StatementInvalid (TinyTds::Error: DBPROCESS is dead or not enabled)
453
+ # Relevant info here: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/402
431
454
  columns.each do |col|
432
- if (col_name = col.name) == 'class'
433
- col_alias = " AS #{col.name}_"
434
- else
435
- alias_name = nil
436
- idx = 0
437
- col_name.each_char do |c|
438
- unless (c >= 'a' && c <= 'z') ||
439
- c == '_' ||
440
- (c >= 'A' && c <= 'Z') ||
441
- (c >= '0' && c <= '9')
442
- (alias_name ||= col_name.dup)[idx] = 'x'
443
- end
444
- ++idx
445
- end
446
- col_alias = " AS #{alias_name}" if alias_name
447
- end
455
+ col_alias = " AS #{col.name}_" if (col_name = col.name) == 'class'
448
456
  selects << if is_mysql
449
457
  "`#{tbl_no_schema}`.`#{col_name}`#{col_alias}"
450
458
  elsif is_postgres || is_mssql
@@ -452,8 +460,8 @@ module ActiveRecord
452
460
  cast_as_text = '::text' if is_distinct && Brick.relations[klass.table_name]&.[](:cols)&.[](col_name)&.first&.start_with?('xml')
453
461
  "\"#{tbl_no_schema}\".\"#{col_name}\"#{cast_as_text}#{col_alias}"
454
462
  elsif col.type # Could be Sqlite or Oracle
455
- if col_alias
456
- "#{tbl_no_schema}.#{col_name}#{col_alias}"
463
+ if col_alias || !(/^[a-z0-9_]+$/ =~ col_name)
464
+ "#{tbl_no_schema}.\"#{col_name}\"#{col_alias}"
457
465
  else
458
466
  "#{tbl_no_schema}.#{col_name}"
459
467
  end
@@ -478,9 +486,14 @@ module ActiveRecord
478
486
  next if chains[k1].nil?
479
487
 
480
488
  tbl_name = (field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])).split('.').last
489
+ # If it's Oracle, quote any AREL aliases that had been applied
490
+ tbl_name = "\"#{tbl_name}\"" if ::Brick.is_oracle && rel_dupe._arel_applied_aliases.include?(tbl_name)
481
491
  field_tbl_name = nil
482
492
  v1.map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
493
+ # binding.pry if chains[sel_col.first].nil?
483
494
  field_tbl_name = (field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])).split('.').last
495
+ # If it's Oracle, quote any AREL aliases that had been applied
496
+ field_tbl_name = "\"#{field_tbl_name}\"" if ::Brick.is_oracle && rel_dupe._arel_applied_aliases.include?(field_tbl_name)
484
497
 
485
498
  # Postgres can not use DISTINCT with any columns that are XML, so for any of those just convert to text
486
499
  is_xml = is_distinct && Brick.relations[sel_col.first.table_name]&.[](:cols)&.[](sel_col.last)&.first&.start_with?('xml')
@@ -737,7 +750,8 @@ Module.class_exec do
737
750
  # Vabc instead of VABC)
738
751
  full_class_name = +''
739
752
  full_class_name << "::#{self.name}" unless self == Object
740
- full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
753
+ singular_class_name = ::Brick.namify(plural_class_name, :underscore).singularize.camelize
754
+ full_class_name << "::#{singular_class_name}"
741
755
  if plural_class_name == 'BrickSwagger' ||
742
756
  (
743
757
  (::Brick.config.add_status || ::Brick.config.add_orphans) &&
@@ -751,9 +765,11 @@ Module.class_exec do
751
765
  base_module == Object && # %%% This works for Person::Person -- but also limits us to not being able to allow more than one level of namespacing
752
766
  (schema_name = [(singular_table_name = class_name.underscore),
753
767
  (table_name = singular_table_name.pluralize),
754
- class_name,
768
+ ::Brick.is_oracle ? class_name.upcase : class_name,
755
769
  (plural_class_name = class_name.pluralize)].find { |s| Brick.db_schemas.include?(s) }&.camelize ||
756
770
  (::Brick.config.sti_namespace_prefixes&.key?("::#{class_name}::") && class_name))
771
+ return self.const_get(schema_name) if self.const_defined?(schema_name)
772
+
757
773
  # Build out a module for the schema if it's namespaced
758
774
  # schema_name = schema_name.camelize
759
775
  base_module.const_set(schema_name.to_sym, (built_module = Module.new))
@@ -1120,64 +1136,63 @@ class Object
1120
1136
  end
1121
1137
  return [new_controller_class, code + "end # BrickGem controller\n"]
1122
1138
  when 'BrickSwagger'
1123
- is_swagger = true # if request.format == :json)
1139
+ is_swagger = true
1124
1140
  end
1125
1141
 
1126
1142
  self.protect_from_forgery unless: -> { self.request.format.js? }
1127
1143
  self.define_method :index do
1128
- # We do all of this now so that bt_descrip and hm_counts are available on the model early in case the user
1129
- # wants to do an ORDER BY based on any of that
1130
- translations = {}
1131
- join_array = ::Brick::JoinArray.new
1132
- is_add_bts = is_add_hms = true
1133
- # This builds out bt_descrip and hm_counts on the model
1134
- model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
1135
-
1136
1144
  if is_swagger
1137
- json = { 'openapi': '3.0.1', 'info': { 'title': 'API V1', 'version': 'v1' },
1145
+ if !params&.key?('_brick_schema') &&
1146
+ (referrer_params = request.env['HTTP_REFERER']&.split('?')&.last&.split('&')&.map { |x| x.split('=') }).present?
1147
+ if params
1148
+ referrer_params.each { |k, v| params.send(:parameters)[k] = v }
1149
+ else
1150
+ api_params = referrer_params&.to_h
1151
+ end
1152
+ end
1153
+ ::Brick.set_db_schema(params || api_params)
1154
+ json = { 'openapi': '3.0.1', 'info': { 'title': Rswag::Ui.config.config_object[:urls].last&.fetch(:name, 'API documentation'), 'version': ::Brick.config.api_version },
1138
1155
  'servers': [
1139
- { 'url': 'https://{defaultHost}', 'variables': { 'defaultHost': { 'default': 'www.example.com' } } }
1156
+ { 'url': '{scheme}://{defaultHost}', 'variables': {
1157
+ 'scheme': { 'default': request.env['rack.url_scheme'] },
1158
+ 'defaultHost': { 'default': request.env['HTTP_HOST'] }
1159
+ } }
1140
1160
  ]
1141
1161
  }
1142
- json['paths'] = relations.inject({}) do |s, v|
1143
- s["/api/v1/#{v.first}"] = {
1144
- 'get': {
1145
- 'summary': "list #{v.first}",
1146
- 'parameters': v.last[:cols].map { |k, v| { 'name' => k, 'schema': { 'type': v.first } } },
1147
- 'responses': { '200': { 'description': 'successful' } }
1148
- }
1149
- }
1150
- # next if v.last[:isView]
1151
-
1152
- s["/api/v1/#{v.first}/{id}"] = {
1153
- 'patch': {
1154
- 'summary': "update a #{v.first.singularize}",
1155
- 'parameters': v.last[:cols].reject { |k, v| Brick.config.metadata_columns.include?(k) }.map do |k, v|
1156
- { 'name' => k, 'schema': { 'type': v.first } }
1157
- end,
1158
- 'responses': { '200': { 'description': 'successful' } }
1162
+ json['paths'] = relations.inject({}) do |s, relation|
1163
+ unless ::Brick.config.enable_api == false
1164
+ table_description = relation.last[:description]
1165
+ s["#{::Brick.config.api_root}#{relation.first.tr('.', '/')}"] = {
1166
+ 'get': {
1167
+ 'summary': "list #{relation.first}",
1168
+ 'description': table_description,
1169
+ 'parameters': relation.last[:cols].map do |k, v|
1170
+ param = { 'name' => k, 'schema': { 'type': v.first } }
1171
+ if (col_descrip = relation.last.fetch(:col_descrips, nil)&.fetch(k, nil))
1172
+ param['description'] = col_descrip
1173
+ end
1174
+ param
1175
+ end,
1176
+ 'responses': { '200': { 'description': 'successful' } }
1177
+ }
1159
1178
  }
1160
- # "/api/v1/books/{id}": {
1161
- # "parameters": [
1162
- # {
1163
- # "name": "id",
1164
- # "in": "path",
1165
- # "description": "id",
1166
- # "required": true,
1167
- # "schema": {
1168
- # "type": "string"
1169
- # }
1170
- # },
1171
- # {
1172
- # "name": "Authorization",
1173
- # "in": "header",
1174
- # "schema": {
1175
- # "type": "string"
1176
- # }
1177
- # }
1178
- # ],
1179
- }
1180
- s
1179
+
1180
+ s["#{::Brick.config.api_root}#{relation.first.tr('.', '/')}/{id}"] = {
1181
+ 'patch': {
1182
+ 'summary': "update a #{relation.first.singularize}",
1183
+ 'description': table_description,
1184
+ 'parameters': relation.last[:cols].reject { |k, v| Brick.config.metadata_columns.include?(k) }.map do |k, v|
1185
+ param = { 'name' => k, 'schema': { 'type': v.first } }
1186
+ if (col_descrip = relation.last.fetch(:col_descrips, nil)&.fetch(k, nil))
1187
+ param['description'] = col_descrip
1188
+ end
1189
+ param
1190
+ end,
1191
+ 'responses': { '200': { 'description': 'successful' } }
1192
+ }
1193
+ } unless relation.last.fetch(:isView, nil)
1194
+ s
1195
+ end
1181
1196
  end
1182
1197
  render inline: json.to_json, content_type: request.format
1183
1198
  return
@@ -1185,6 +1200,14 @@ class Object
1185
1200
 
1186
1201
  # Normal (non-swagger) request
1187
1202
 
1203
+ # We do all of this now so that bt_descrip and hm_counts are available on the model early in case the user
1204
+ # wants to do an ORDER BY based on any of that
1205
+ translations = {}
1206
+ join_array = ::Brick::JoinArray.new
1207
+ is_add_bts = is_add_hms = true
1208
+ # This builds out bt_descrip and hm_counts on the model
1209
+ model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
1210
+
1188
1211
  # %%% Allow params to define which columns to use for order_by
1189
1212
  # Overriding the default by providing a querystring param?
1190
1213
  ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
@@ -1198,8 +1221,9 @@ class Object
1198
1221
  end
1199
1222
  render inline: exported_csv, content_type: request.format
1200
1223
  return
1201
- elsif request.format == :js # Asking for JSON?
1202
- render inline: model.df_export(model.brick_import_template).to_json, content_type: request.format
1224
+ elsif request.format == :js || request.path.start_with?('/api/') # Asking for JSON?
1225
+ data = (model.is_view? || !Object.const_defined?('DutyFree')) ? model.limit(1000) : model.df_export(model.brick_import_template)
1226
+ render inline: data.to_json, content_type: request.format == '*/*' ? 'application/json' : request.format
1203
1227
  return
1204
1228
  end
1205
1229
 
@@ -1215,7 +1239,7 @@ class Object
1215
1239
  "b_r_#{v.first}.c_t_ AS \"b_r_#{v.first}_ct\""
1216
1240
  end
1217
1241
  end
1218
- instance_variable_set("@#{table_name}".to_sym, ar_relation.dup._select!(*selects, *counts))
1242
+ instance_variable_set("@#{table_name.pluralize}".to_sym, ar_relation.dup._select!(*selects, *counts))
1219
1243
  if namespace && (idx = lookup_context.prefixes.index(table_name))
1220
1244
  lookup_context.prefixes[idx] = "#{namespace.name.underscore}/#{lookup_context.prefixes[idx]}"
1221
1245
  end
@@ -1230,32 +1254,34 @@ class Object
1230
1254
  @_brick_erd = params['_brick_erd']&.to_i
1231
1255
  end
1232
1256
 
1233
- _, order_by_txt = model._brick_calculate_ordering(default_ordering(table_name, pk))
1234
- code << " def index\n"
1235
- code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{order_by_txt.join(', ')})" : '.all'}\n"
1236
- code << " @#{table_name}.brick_select(params)\n"
1237
- code << " end\n"
1238
-
1239
- is_pk_string = nil
1240
- if (pk_col = model&.primary_key)
1241
- code << " def show\n"
1242
- code << " #{find_by_name = "find_#{singular_table_name}"}\n"
1257
+ unless is_swagger
1258
+ ::Brick.set_db_schema
1259
+ _, order_by_txt = model._brick_calculate_ordering(default_ordering(table_name, pk)) if pk
1260
+ code << " def index\n"
1261
+ code << " @#{table_name.pluralize} = #{model.name}#{pk&.present? ? ".order(#{order_by_txt.join(', ')})" : '.all'}\n"
1262
+ code << " @#{table_name.pluralize}.brick_select(params)\n"
1243
1263
  code << " end\n"
1244
- self.define_method :show do
1245
- ::Brick.set_db_schema(params)
1246
- id = if model.columns_hash[pk_col]&.type == :string
1247
- is_pk_string = true
1248
- params[:id]
1249
- else
1250
- params[:id]&.split(/[\/,_]/)
1251
- end
1252
- id = id.first if id.is_a?(Array) && id.length == 1
1253
- instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1264
+
1265
+ is_pk_string = nil
1266
+ if pk.present?
1267
+ code << " def show\n"
1268
+ code << " #{find_by_name = "find_#{singular_table_name}"}\n"
1269
+ code << " end\n"
1270
+ self.define_method :show do
1271
+ ::Brick.set_db_schema(params)
1272
+ id = if model.columns_hash[pk.first]&.type == :string
1273
+ is_pk_string = true
1274
+ params[:id]
1275
+ else
1276
+ params[:id]&.split(/[\/,_]/)
1277
+ end
1278
+ id = id.first if id.is_a?(Array) && id.length == 1
1279
+ instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1280
+ end
1254
1281
  end
1255
- end
1256
1282
 
1257
- # By default, views get marked as read-only
1258
- # unless model.readonly # (relation = relations[model.table_name]).key?(:isView)
1283
+ # By default, views get marked as read-only
1284
+ # unless model.readonly # (relation = relations[model.table_name]).key?(:isView)
1259
1285
  code << " def new\n"
1260
1286
  code << " @#{singular_table_name} = #{model.name}.new\n"
1261
1287
  code << " end\n"
@@ -1289,7 +1315,7 @@ class Object
1289
1315
  end
1290
1316
  end
1291
1317
 
1292
- if pk_col
1318
+ if pk.present?
1293
1319
  # if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.include?(schema)
1294
1320
  # ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
1295
1321
  # end
@@ -1336,9 +1362,9 @@ class Object
1336
1362
  end
1337
1363
  end
1338
1364
 
1339
- code << "private\n" if pk_col || is_need_params
1365
+ code << "private\n" if pk.present? || is_need_params
1340
1366
 
1341
- if pk_col
1367
+ if pk.present?
1342
1368
  code << " def find_#{singular_table_name}
1343
1369
  id = params[:id]&.split(/[\\/,_]/)
1344
1370
  @#{singular_table_name} = #{model.name}.find(id.is_a?(Array) && id.length == 1 ? id.first : id)
@@ -1366,16 +1392,16 @@ class Object
1366
1392
  private params_name
1367
1393
  # Get column names for params from relations[model.table_name][:cols].keys
1368
1394
  end
1369
- # end
1395
+ end # unless is_swagger
1370
1396
  code << "end # #{namespace_name}#{class_name}\n"
1371
1397
  end # class definition
1372
1398
  [built_controller, code]
1373
1399
  end
1374
1400
 
1375
1401
  def _brick_get_hm_assoc_name(relation, hm_assoc, source = nil)
1376
- if (relation[:hm_counts][hm_assoc[:assoc_name]]&.> 1) &&
1402
+ if (relation[:hm_counts][hm_assoc[:inverse_table]]&.> 1) &&
1377
1403
  hm_assoc[:alternate_name] != (source || name.underscore)
1378
- plural = ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])
1404
+ plural = "#{hm_assoc[:assoc_name]}_#{ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])}"
1379
1405
  new_alt_name = (hm_assoc[:alternate_name] == name.underscore) ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural
1380
1406
  # uniq = 1
1381
1407
  # while same_name = relation[:fks].find { |x| x.last[:assoc_name] == hm_assoc[:assoc_name] && x.last != hm_assoc }
@@ -1386,8 +1412,13 @@ class Object
1386
1412
  [new_alt_name, true]
1387
1413
  else
1388
1414
  assoc_name = ::Brick.namify(hm_assoc[:inverse_table]).pluralize
1415
+ if (needs_class = assoc_name.include?('.')) # If there is a schema name present, use a downcased version for the :has_many
1416
+ assoc_parts = assoc_name.split('.')
1417
+ assoc_parts[0].downcase! if assoc_parts[0] =~ /^[A-Z0-9_]+$/
1418
+ assoc_name = assoc_parts.join('.')
1419
+ end
1389
1420
  # hm_assoc[:assoc_name] = assoc_name
1390
- [assoc_name, assoc_name.include?('.')]
1421
+ [assoc_name, needs_class]
1391
1422
  end
1392
1423
  end
1393
1424
  end
@@ -1443,7 +1474,8 @@ module ActiveRecord::ConnectionHandling
1443
1474
  row['table_schema']
1444
1475
  end
1445
1476
  # Remove any system schemas
1446
- s[row] = nil unless ['information_schema', 'pg_catalog'].include?(row)
1477
+ s[row] = nil unless ['information_schema', 'pg_catalog',
1478
+ 'INFORMATION_SCHEMA', 'sys'].include?(row)
1447
1479
  end
1448
1480
  if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
1449
1481
  (sta = multitenancy[:schema_to_analyse]) != 'public') &&
@@ -1487,6 +1519,7 @@ module ActiveRecord::ConnectionHandling
1487
1519
  # ActiveRecord::Base.internal_metadata_table_name, ActiveRecord::Base.schema_migrations_table_name
1488
1520
  # For if it's not SQLite -- so this is the Postgres and MySQL version
1489
1521
  measures = []
1522
+ ::Brick.is_oracle = true if ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
1490
1523
  case ActiveRecord::Base.connection.adapter_name
1491
1524
  when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
1492
1525
  # schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
@@ -1499,6 +1532,8 @@ module ActiveRecord::ConnectionHandling
1499
1532
  r['schema']
1500
1533
  end
1501
1534
  relation_name = schema_name ? "#{schema_name}.#{r['relation_name']}" : r['relation_name']
1535
+ # Both uppers and lowers as well as underscores?
1536
+ apply_double_underscore_patch if relation_name =~ /[A-Z]/ && relation_name =~ /[a-z]/ && relation_name.index('_')
1502
1537
  relation = relations[relation_name]
1503
1538
  relation[:isView] = true if r['table_type'] == 'VIEW'
1504
1539
  relation[:description] = r['table_description'] if r['table_description']
@@ -1515,6 +1550,7 @@ module ActiveRecord::ConnectionHandling
1515
1550
  cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
1516
1551
  cols[col_name] = [r['data_type'], r['max_length'], measures&.include?(col_name), r['is_nullable'] == 'NO']
1517
1552
  # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
1553
+ relation[:col_descrips][col_name] = r['column_description'] if r['column_description']
1518
1554
  end
1519
1555
  else # MySQL2 and OracleEnhanced act a little differently, bringing back an array for each row
1520
1556
  schema_and_tables = case ActiveRecord::Base.connection.adapter_name
@@ -1538,15 +1574,24 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1538
1574
  else
1539
1575
  ActiveRecord::Base.retrieve_schema_and_tables(sql)
1540
1576
  end
1577
+
1541
1578
  schema_and_tables.each do |r|
1542
1579
  next if r[1].index('$') # Oracle can have goofy table names with $
1543
1580
 
1544
- if (relation_name = r[1]) =~ /^[A-Z_]+$/
1581
+ if (relation_name = r[1]) =~ /^[A-Z0-9_]+$/
1545
1582
  relation_name.downcase!
1583
+ # Both uppers and lowers as well as underscores?
1584
+ elsif relation_name =~ /[A-Z]/ && relation_name =~ /[a-z]/ && relation_name.index('_')
1585
+ apply_double_underscore_patch
1546
1586
  end
1587
+ # Expect the default schema for SQL Server to be 'dbo'.
1588
+ if (::Brick.is_oracle && r[0] != schema) || (is_mssql && r[0] != 'dbo')
1589
+ relation_name = "#{r[0]}.#{relation_name}"
1590
+ end
1591
+
1547
1592
  relation = relations[relation_name] # here relation represents a table or view from the database
1548
1593
  relation[:isView] = true if r[2] == 'VIEW' # table_type
1549
- col_name = r[3]
1594
+ col_name = ::Brick.is_oracle ? connection.send(:oracle_downcase, r[3]) : r[3]
1550
1595
  key = case r[6] # constraint type
1551
1596
  when 'PRIMARY KEY'
1552
1597
  # key
@@ -1558,8 +1603,8 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1558
1603
  end
1559
1604
  key << col_name if key
1560
1605
  cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
1561
- # 'data_type', 'max_length'
1562
- cols[col_name] = [r[4], r[5], measures&.include?(col_name)]
1606
+ # 'data_type', 'max_length', measure, 'is_nullable'
1607
+ cols[col_name] = [r[4], r[5], measures&.include?(col_name), r[8] == 'NO']
1563
1608
  # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
1564
1609
  end
1565
1610
  end
@@ -1612,10 +1657,10 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1612
1657
  schemas = ::Brick.db_schemas.keys.map { |s| "'#{s}'" }.join(', ')
1613
1658
  sql =
1614
1659
  "SELECT -- fk
1615
- ac.owner AS constraint_schema, acc_fk.table_name, LOWER(acc_fk.column_name),
1660
+ ac.owner AS constraint_schema, acc_fk.table_name, acc_fk.column_name,
1616
1661
  -- referenced pk
1617
1662
  ac.r_owner AS primary_schema, acc_pk.table_name AS primary_table, acc_fk.constraint_name AS constraint_schema_fk
1618
- -- , LOWER(acc_pk.column_name)
1663
+ -- , acc_pk.column_name
1619
1664
  FROM all_cons_columns acc_fk
1620
1665
  INNER JOIN all_constraints ac ON acc_fk.owner = ac.owner
1621
1666
  AND acc_fk.constraint_name = ac.constraint_name
@@ -1633,55 +1678,80 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1633
1678
  # Multitenancy makes things a little more general overall, except for non-tenanted tables
1634
1679
  if apartment_excluded&.include?(::Brick.namify(fk[1]).singularize.camelize)
1635
1680
  fk[0] = Apartment.default_schema
1636
- elsif is_postgres && (fk[0] == 'public' || (is_multitenant && fk[0] == schema)) ||
1637
- !is_postgres && ['mysql', 'performance_schema', 'sys'].exclude?(fk[0])
1681
+ elsif (is_postgres && (fk[0] == 'public' || (is_multitenant && fk[0] == schema))) ||
1682
+ (::Brick.is_oracle && fk[0] == schema) ||
1683
+ (is_mssql && fk[0] == 'dbo') ||
1684
+ (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[0]))
1638
1685
  fk[0] = nil
1639
1686
  end
1640
1687
  if apartment_excluded&.include?(fk[4].singularize.camelize)
1641
1688
  fk[3] = Apartment.default_schema
1642
- elsif is_postgres && (fk[3] == 'public' || (is_multitenant && fk[3] == schema)) ||
1643
- !is_postgres && ['mysql', 'performance_schema', 'sys'].exclude?(fk[3])
1689
+ elsif (is_postgres && (fk[3] == 'public' || (is_multitenant && fk[3] == schema))) ||
1690
+ (::Brick.is_oracle && fk[3] == schema) ||
1691
+ (is_mssql && fk[3] == 'dbo') ||
1692
+ (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[3]))
1644
1693
  fk[3] = nil
1645
1694
  end
1646
- fk[1].downcase! if ::Brick.is_oracle && fk[1] =~ /^[A-Z_]+$/
1647
- fk[4].downcase! if ::Brick.is_oracle && fk[4] =~ /^[A-Z_]+$/
1695
+ if ::Brick.is_oracle
1696
+ fk[1].downcase! if fk[1] =~ /^[A-Z0-9_]+$/
1697
+ fk[4].downcase! if fk[4] =~ /^[A-Z0-9_]+$/
1698
+ fk[2] = connection.send(:oracle_downcase, fk[2])
1699
+ end
1648
1700
  ::Brick._add_bt_and_hm(fk, relations)
1649
1701
  end
1650
1702
  end
1651
1703
 
1652
1704
  tables = []
1653
1705
  views = []
1706
+ table_class_length = 0
1707
+ view_class_length = 0
1654
1708
  relations.each do |k, v|
1655
1709
  name_parts = k.split('.')
1656
1710
  idx = 1
1657
1711
  name_parts = name_parts.map do |x|
1658
- ((idx += 1) < name_parts.length ? x.singularize : x).camelize
1712
+ (idx += 1) <= name_parts.length ? x : x.singularize
1659
1713
  end
1714
+ name_parts.shift if apartment && name_parts.length > 1 && name_parts.first == Apartment.default_schema
1715
+ class_name = name_parts.map(&:camelize).join('::')
1660
1716
  if v.key?(:isView)
1717
+ view_class_length = class_name.length if class_name.length > view_class_length
1661
1718
  views
1662
1719
  else
1663
- name_parts.shift if apartment && name_parts.length > 1 && name_parts.first == Apartment.default_schema
1720
+ table_class_length = class_name.length if class_name.length > table_class_length
1664
1721
  tables
1665
- end << name_parts.join('::')
1722
+ end << [class_name, name_parts]
1666
1723
  end
1667
- unless tables.empty?
1668
- puts "\nClasses that can be built from tables:"
1669
- tables.sort.each { |x| puts x }
1724
+ puts "\n" if tables.present? || views.present?
1725
+ if tables.present?
1726
+ puts "Classes that can be built from tables:"
1727
+ display_classes(tables, table_class_length)
1670
1728
  end
1671
- unless views.empty?
1672
- puts "\nClasses that can be built from views:"
1673
- views.sort.each { |x| puts x }
1729
+ if views.present?
1730
+ puts "Classes that can be built from views:"
1731
+ display_classes(views, view_class_length)
1674
1732
  end
1675
1733
 
1676
1734
  ::Brick.load_additional_references if initializer_loaded
1677
1735
  end
1678
1736
 
1737
+ def display_classes(rels, max_length)
1738
+ rels.sort.each do |rel|
1739
+ rel_link = rel.last.dup.map(&:underscore)
1740
+ rel_link[-1] = rel_link[-1]
1741
+ puts "#{rel.first}#{' ' * (max_length - rel.first.length)} /#{rel_link.join('/')}"
1742
+ end
1743
+ puts "\n"
1744
+ end
1745
+
1679
1746
  def retrieve_schema_and_tables(sql = nil, is_postgres = nil, is_mssql = nil, schema = nil)
1680
1747
  is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' if is_mssql.nil?
1681
1748
  sql ||= "SELECT t.table_schema AS \"schema\", t.table_name AS relation_name, t.table_type,#{"
1682
1749
  pg_catalog.obj_description(
1683
- ('\"' || t.table_schema || '\".\"' || t.table_name || '\"')::regclass, 'pg_class'
1684
- ) AS table_description," if is_postgres}
1750
+ ('\"' || t.table_schema || '\".\"' || t.table_name || '\"')::regclass::oid, 'pg_class'
1751
+ ) AS table_description,
1752
+ pg_catalog.col_description(
1753
+ ('\"' || t.table_schema || '\".\"' || t.table_name || '\"')::regclass::oid, c.ordinal_position
1754
+ ) AS column_description," if is_postgres}
1685
1755
  c.column_name, c.data_type,
1686
1756
  COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
1687
1757
  kcu.constraint_type AS const, kcu.constraint_name AS \"key\",
@@ -1705,7 +1775,8 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1705
1775
  -- AND kcu.position_in_unique_constraint IS NULL" unless is_mssql}
1706
1776
  AND kcu.ordinal_position = c.ordinal_position
1707
1777
  WHERE t.table_schema #{is_postgres || is_mssql ?
1708
- "NOT IN ('information_schema', 'pg_catalog')"
1778
+ "NOT IN ('information_schema', 'pg_catalog',
1779
+ 'INFORMATION_SCHEMA', 'sys')"
1709
1780
  :
1710
1781
  "= '#{ActiveRecord::Base.connection.current_database.tr("'", "''")}'"}#{"
1711
1782
  AND t.table_schema = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }
@@ -1724,6 +1795,43 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
1724
1795
  ar_imtn = ActiveRecord.version >= ::Gem::Version.new('5.0') ? ActiveRecord::Base.internal_metadata_table_name : ''
1725
1796
  [ar_smtn, ar_imtn]
1726
1797
  end
1798
+
1799
+ def apply_double_underscore_patch
1800
+ unless @double_underscore_applied
1801
+ # Same as normal #camelize and #underscore, just that double-underscores turn into a single underscore
1802
+ ActiveSupport::Inflector.class_eval do
1803
+ def camelize(term, uppercase_first_letter = true)
1804
+ strings = term.to_s.split('__').map do |string|
1805
+ # String#camelize takes a symbol (:upper or :lower), so here we also support :lower to keep the methods consistent.
1806
+ if !uppercase_first_letter || uppercase_first_letter == :lower
1807
+ string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase! || match }
1808
+ else
1809
+ string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize! || match }
1810
+ end
1811
+ string.gsub!(/(?:_|(\/))([a-z\d]*)/i) do
1812
+ word = $2
1813
+ substituted = inflections.acronyms[word] || word.capitalize! || word
1814
+ $1 ? "::#{substituted}" : substituted
1815
+ end
1816
+ string
1817
+ end
1818
+ strings.join('_')
1819
+ end
1820
+
1821
+ def underscore(camel_cased_word)
1822
+ return camel_cased_word.to_s unless /[A-Z-]|::/.match?(camel_cased_word)
1823
+ camel_cased_word.to_s.gsub("::", "/").split('_').map do |word|
1824
+ word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" }
1825
+ word.gsub!(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { ($1 || $2) << "_" }
1826
+ word.tr!("-", "_")
1827
+ word.downcase!
1828
+ word
1829
+ end.join('__')
1830
+ end
1831
+ end
1832
+ @double_underscore_applied = true
1833
+ end
1834
+ end
1727
1835
  end
1728
1836
 
1729
1837
  # ==========================================
@@ -1750,7 +1858,7 @@ module Brick
1750
1858
 
1751
1859
  class << self
1752
1860
  def _add_bt_and_hm(fk, relations, is_polymorphic = false, is_optional = false)
1753
- bt_assoc_name = ::Brick.namify(fk[2])
1861
+ bt_assoc_name = ::Brick.namify(fk[2], :downcase)
1754
1862
  unless is_polymorphic
1755
1863
  bt_assoc_name = if bt_assoc_name.underscore.end_with?('_id')
1756
1864
  bt_assoc_name[-3] == '_' ? bt_assoc_name[0..-4] : bt_assoc_name[0..-3]
@@ -1851,6 +1959,11 @@ module Brick
1851
1959
 
1852
1960
  return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[1] == exclusion[0] && fk[2] == exclusion[1] && primary_table == exclusion[2] } || hms.nil?
1853
1961
 
1962
+ # if fk[1].end_with?('Suppliers') && fk[4] == 'People'
1963
+ # puts fk.inspect
1964
+ # binding.pry
1965
+ # end
1966
+
1854
1967
  if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
1855
1968
  if assoc_hm[:fk].is_a?(String)
1856
1969
  assoc_hm[:fk] = [assoc_hm[:fk], fk[2]] unless fk[2] == assoc_hm[:fk]
@@ -1858,7 +1971,6 @@ module Brick
1858
1971
  assoc_hm[:fk] << fk[2]
1859
1972
  end
1860
1973
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
1861
- assoc_hm[:inverse] = assoc_bt
1862
1974
  else
1863
1975
  inv_tbl = if ::Brick.config.schema_behavior[:multitenant] && apartment && fk[0] == Apartment.default_schema
1864
1976
  for_tbl
@@ -1869,7 +1981,7 @@ module Brick
1869
1981
  inverse_table: inv_tbl, inverse: assoc_bt }
1870
1982
  assoc_hm[:polymorphic] = true if is_polymorphic
1871
1983
  hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
1872
- hm_counts[fk[1]] = hm_counts.fetch(fk[1]) { 0 } + 1
1984
+ this_hm_count = hm_counts[fk[1]] = hm_counts.fetch(fk[1]) { 0 } + 1
1873
1985
  end
1874
1986
  assoc_bt[:inverse] = assoc_hm
1875
1987
  end