brick 1.0.199 → 1.0.200

Sign up to get free protection for your applications and to get access to all the features.
@@ -794,7 +794,7 @@ window.addEventListener(\"popstate\", linkSchemas);
794
794
  if (rowcount = rel.last.fetch(:rowcount, nil))
795
795
  rowcount = rowcount > 0 ? " (#{rowcount})" : nil
796
796
  end
797
- s << "<option value=\"#{::Brick._brick_index(rel.first, nil, '/')}\">#{rel.first}#{rowcount}</option>"
797
+ s << "<option value=\"#{::Brick._brick_index(rel.first, nil, '/', nil, true)}\">#{rel.first}#{rowcount}</option>"
798
798
  end.html_safe
799
799
  prefix = "#{::Brick.config.path_prefix}/" if ::Brick.config.path_prefix
800
800
  table_options << "<option value=\"#{prefix}brick_status\">(Status)</option>".html_safe if ::Brick.config.add_status
@@ -1955,34 +1955,6 @@ document.querySelectorAll(\"input, select\").forEach(function (inp) {
1955
1955
  end # TemplateRenderer
1956
1956
  end
1957
1957
 
1958
- if ::Brick.enable_routes?
1959
- require 'brick/route_mapper'
1960
- ActionDispatch::Routing::RouteSet.class_exec do
1961
- # In order to defer auto-creation of any routes that already exist, calculate Brick routes only after having loaded all others
1962
- prepend ::Brick::RouteSet
1963
- end
1964
- ActionDispatch::Routing::Mapper.class_exec do
1965
- include ::Brick::RouteMapper
1966
- end
1967
-
1968
- # Do the root route before the Rails Welcome one would otherwise take precedence
1969
- if (route = ::Brick.config.default_route_fallback).present?
1970
- action = "#{route}#{'#index' unless route.index('#')}"
1971
- if ::Brick.config.path_prefix
1972
- ::Rails.application.routes.append do
1973
- send(:namespace, ::Brick.config.path_prefix) do
1974
- send(:root, action)
1975
- end
1976
- end
1977
- elsif ::Rails.application.routes.named_routes.send(:routes)[:root].nil?
1978
- ::Rails.application.routes.append do
1979
- send(:root, action)
1980
- end
1981
- end
1982
- ::Brick.established_drf = "/#{::Brick.config.path_prefix}#{action[action.index('#')..-1]}"
1983
- end
1984
- end
1985
-
1986
1958
  # Just in case it hadn't been done previously when we tried to load the brick initialiser,
1987
1959
  # go make sure we've loaded additional references (virtual foreign keys and polymorphic associations).
1988
1960
  # (This should only happen if for whatever reason the initializer file was not exactly config/initializers/brick.rb.)
@@ -222,7 +222,7 @@ module Brick::Rails::FormTags
222
222
  out << link_to(ho_txt, send("#{hm_klass.base_class._brick_index(:singular)}_path".to_sym, ho_id))
223
223
  end
224
224
  elsif obj.respond_to?(ct_col = hms_col[1].to_sym) && (ct = obj.send(ct_col)&.to_i)&.positive?
225
- predicates = hms_col[2].each_with_object({}) { |v, s| s["__#{v.first}"] = v.last.is_a?(String) ? v.last : obj.send(v.last) }
225
+ predicates = hms_col[2].each_with_object({}) { |v, s| s["__#{v.first}"] = v.last.is_a?(String) ? v.last : obj.send(v.last) if v.last }
226
226
  predicates.each { |k, v| predicates[k] = klass.name if v == '[sti_type]' }
227
227
  out << "#{link_to("#{ct || 'View'} #{hms_col.first}",
228
228
  send("#{hm_klass._brick_index}_path".to_sym, predicates))}\n"
@@ -0,0 +1,582 @@
1
+ module Brick
2
+ class << self
3
+ # This is done separately so that during testing it can be called right after a migration
4
+ # in order to make sure everything is good.
5
+ def reflect_tables
6
+ return unless ::Brick.config.mode == :on
7
+
8
+ # return if ActiveRecord::Base.connection.current_database == 'postgres'
9
+
10
+ # Overwrite SQLite's #begin_db_transaction so it opens in IMMEDIATE mode instead of
11
+ # the default DEFERRED mode.
12
+ # https://discuss.rubyonrails.org/t/failed-write-transaction-upgrades-in-sqlite3/81480/2
13
+ if ActiveRecord::Base.connection.adapter_name == 'SQLite'
14
+ arca = ::ActiveRecord::ConnectionAdapters
15
+ db_statements = arca::SQLite3.const_defined?('DatabaseStatements') ? arca::SQLite3::DatabaseStatements : arca::SQLite3::SchemaStatements
16
+ # Rails 7.1 and later
17
+ if arca::AbstractAdapter.private_instance_methods.include?(:with_raw_connection)
18
+ db_statements.define_method(:begin_db_transaction) do
19
+ log("begin immediate transaction", "TRANSACTION") do
20
+ with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
21
+ conn.transaction(:immediate)
22
+ end
23
+ end
24
+ end
25
+ else # Rails < 7.1
26
+ db_statements.define_method(:begin_db_transaction) do
27
+ log('begin immediate transaction', 'TRANSACTION') { @connection.transaction(:immediate) }
28
+ end
29
+ end
30
+ end
31
+
32
+ orig_schema = nil
33
+ if (relations = ::Brick.relations).keys == [:db_name]
34
+ ::Brick.remove_instance_variable(:@_additional_references_loaded) if ::Brick.instance_variable_defined?(:@_additional_references_loaded)
35
+
36
+ # --------------------------------------------
37
+ # 1. Load three initializers early
38
+ # (inflectsions.rb, brick.rb, apartment.rb)
39
+ # Very first thing, load inflections since we'll be using .pluralize and .singularize on table and model names
40
+ if File.exist?(inflections = ::Rails.root&.join('config/initializers/inflections.rb') || '')
41
+ load inflections
42
+ end
43
+ # Now the Brick initializer since there may be important schema things configured
44
+ if !::Brick.initializer_loaded && File.exist?(brick_initializer = ::Rails.root&.join('config/initializers/brick.rb') || '')
45
+ ::Brick.initializer_loaded = load brick_initializer
46
+
47
+ # After loading the initializer, add compatibility for ActiveStorage and ActionText if those haven't already been
48
+ # defined. (Further JSON configuration for ActiveStorage metadata happens later in the after_initialize hook.)
49
+ # begin
50
+ ['ActiveStorage', 'ActionText'].each do |ar_extension|
51
+ if Object.const_defined?(ar_extension) &&
52
+ (extension = Object.const_get(ar_extension)).respond_to?(:table_name_prefix) &&
53
+ !::Brick.config.table_name_prefixes.key?(as_tnp = extension.table_name_prefix)
54
+ ::Brick.config.table_name_prefixes[as_tnp] = ar_extension
55
+ end
56
+ end
57
+ # rescue # NoMethodError
58
+ # end
59
+
60
+ # Support the followability gem: https://github.com/nejdetkadir/followability
61
+ if Object.const_defined?('Followability') && !::Brick.config.table_name_prefixes.key?('followability_')
62
+ ::Brick.config.table_name_prefixes['followability_'] = 'Followability'
63
+ end
64
+ end
65
+ # Load the initializer for the Apartment gem a little early so that if .excluded_models and
66
+ # .default_schema are specified then we can work with non-tenanted models more appropriately
67
+ if (apartment = Object.const_defined?('Apartment')) &&
68
+ File.exist?(apartment_initializer = ::Rails.root.join('config/initializers/apartment.rb'))
69
+ require 'apartment/adapters/abstract_adapter'
70
+ Apartment::Adapters::AbstractAdapter.class_exec do
71
+ if instance_methods.include?(:process_excluded_models)
72
+ def process_excluded_models
73
+ # All other models will share a connection (at Apartment.connection_class) and we can modify at will
74
+ Apartment.excluded_models.each do |excluded_model|
75
+ begin
76
+ process_excluded_model(excluded_model)
77
+ rescue NameError => e
78
+ (@bad_models ||= []) << excluded_model
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ unless @_apartment_loaded
85
+ load apartment_initializer
86
+ @_apartment_loaded = true
87
+ end
88
+ end
89
+ # Only for Postgres (Doesn't work in sqlite3 or MySQL)
90
+ # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
91
+
92
+ # ---------------------------
93
+ # 2. Figure out schema things
94
+ is_postgres = nil
95
+ is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer'
96
+ case ActiveRecord::Base.connection.adapter_name
97
+ when 'PostgreSQL', 'SQLServer'
98
+ is_postgres = !is_mssql
99
+ db_schemas = if is_postgres
100
+ ActiveRecord::Base.execute_sql('SELECT nspname AS table_schema, MAX(oid) AS dt FROM pg_namespace GROUP BY 1 ORDER BY 1;')
101
+ else
102
+ ActiveRecord::Base.execute_sql('SELECT DISTINCT table_schema, NULL AS dt FROM INFORMATION_SCHEMA.tables;')
103
+ end
104
+ ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
105
+ row = case row
106
+ when Array
107
+ row
108
+ else
109
+ [row['table_schema'], row['dt']]
110
+ end
111
+ # Remove any system schemas
112
+ s[row.first] = { dt: row.last } unless ['information_schema', 'pg_catalog', 'pg_toast', 'heroku_ext',
113
+ 'INFORMATION_SCHEMA', 'sys'].include?(row.first)
114
+ end
115
+ possible_schema, possible_schemas, multitenancy = ::Brick.get_possible_schemas
116
+ if possible_schemas
117
+ if possible_schema
118
+ ::Brick.default_schema = ::Brick.apartment_default_tenant
119
+ schema = possible_schema
120
+ orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
121
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
122
+ # When testing, just find the most recently-created schema
123
+ elsif begin
124
+ Rails.env == 'test' ||
125
+ ActiveRecord::Base.execute_sql("SELECT value FROM ar_internal_metadata WHERE key='environment';").first&.fetch('value', nil) == 'test'
126
+ rescue
127
+ end
128
+ ::Brick.default_schema = ::Brick.apartment_default_tenant
129
+ ::Brick.test_schema = schema = ::Brick.db_schemas.to_a.sort { |a, b| b.last[:dt] <=> a.last[:dt] }.first.first
130
+ if possible_schema.blank?
131
+ puts "While running tests, using the most recently-created schema, #{schema}."
132
+ else
133
+ 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}."
134
+ end
135
+ orig_schema = ActiveRecord::Base.execute_sql('SELECT current_schemas(true)').first['current_schemas'][1..-2].split(',')
136
+ ::Brick.config.schema_behavior = { multitenant: {} } # schema_to_analyse: [schema]
137
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
138
+ else
139
+ 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. ***"
140
+ if ::Brick.db_schemas.key?(::Brick.apartment_default_tenant)
141
+ ::Brick.default_schema = schema = ::Brick.apartment_default_tenant
142
+ end
143
+ end
144
+ end
145
+ when 'Mysql2', 'Trilogy'
146
+ ::Brick.default_schema = schema = ActiveRecord::Base.connection.current_database
147
+ when 'OracleEnhanced'
148
+ # ActiveRecord::Base.connection.current_database will be something like "XEPDB1"
149
+ ::Brick.default_schema = schema = ActiveRecord::Base.connection.raw_connection.username
150
+ ::Brick.db_schemas = {}
151
+ ActiveRecord::Base.execute_sql("SELECT username FROM sys.all_users WHERE ORACLE_MAINTAINED != 'Y'").each { |s| ::Brick.db_schemas[s.first] = {} }
152
+ when 'SQLite'
153
+ sql = "SELECT m.name AS relation_name, UPPER(m.type) AS table_type,
154
+ p.name AS column_name, p.type AS data_type,
155
+ CASE p.pk WHEN 1 THEN 'PRIMARY KEY' END AS const
156
+ FROM sqlite_master AS m
157
+ INNER JOIN pragma_table_info(m.name) AS p
158
+ WHERE m.name NOT IN ('sqlite_sequence', ?, ?)
159
+ ORDER BY m.name, p.cid"
160
+ else
161
+ puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
162
+ end
163
+
164
+ ::Brick.db_schemas ||= {}
165
+
166
+ # ---------------------
167
+ # 3. Tables and columns
168
+ # %%% Retrieve internal ActiveRecord table names like this:
169
+ # ActiveRecord::Base.internal_metadata_table_name, ActiveRecord::Base.schema_migrations_table_name
170
+ # For if it's not SQLite -- so this is the Postgres and MySQL version
171
+ measures = []
172
+ ::Brick.is_oracle = true if ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
173
+ case ActiveRecord::Base.connection.adapter_name
174
+ when 'PostgreSQL', 'SQLite' # These bring back a hash for each row because the query uses column aliases
175
+ # schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
176
+ retrieve_schema_and_tables(sql, is_postgres, is_mssql, schema).each do |r|
177
+ # If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
178
+ # is the default schema, usually 'public'.
179
+ schema_name = if ::Brick.config.schema_behavior[:multitenant]
180
+ ::Brick.apartment_default_tenant if ::Brick.is_apartment_excluded_table(r['relation_name'])
181
+ elsif ![schema, 'public'].include?(r['schema'])
182
+ r['schema']
183
+ end
184
+ relation_name = schema_name ? "#{schema_name}.#{r['relation_name']}" : r['relation_name']
185
+ # Both uppers and lowers as well as underscores?
186
+ ::Brick.apply_double_underscore_patch if relation_name =~ /[A-Z]/ && relation_name =~ /[a-z]/ && relation_name.index('_')
187
+ relation = relations[relation_name]
188
+ relation[:isView] = true if r['table_type'] == 'VIEW'
189
+ relation[:description] = r['table_description'] if r['table_description']
190
+ col_name = r['column_name']
191
+ key = case r['const']
192
+ when 'PRIMARY KEY'
193
+ relation[:pkey][r['key'] || relation_name] ||= []
194
+ when 'UNIQUE'
195
+ relation[:ukeys][r['key'] || "#{relation_name}.#{col_name}"] ||= []
196
+ # key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
197
+ # key[r['key']]
198
+ else
199
+ if r['data_type'] == 'uuid'
200
+ # && r['column_name'] == ::Brick.ar_base.primary_key
201
+ # binding.pry
202
+ relation[:pkey][r['key'] || relation_name] ||= []
203
+ end
204
+ end
205
+ # binding.pry if key && r['data_type'] == 'uuid'
206
+ key << col_name if key
207
+ cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
208
+ cols[col_name] = [r['data_type'], r['max_length'], measures&.include?(col_name), r['is_nullable'] == 'NO']
209
+ # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
210
+ relation[:col_descrips][col_name] = r['column_description'] if r['column_description']
211
+ end
212
+ else # MySQL2, OracleEnhanced, and MSSQL act a little differently, bringing back an array for each row
213
+ schema_and_tables = case ActiveRecord::Base.connection.adapter_name
214
+ when 'OracleEnhanced'
215
+ sql =
216
+ "SELECT c.owner AS schema, c.table_name AS relation_name,
217
+ CASE WHEN v.owner IS NULL THEN 'BASE_TABLE' ELSE 'VIEW' END AS table_type,
218
+ c.column_name, c.data_type,
219
+ COALESCE(c.data_length, c.data_precision) AS max_length,
220
+ CASE ac.constraint_type WHEN 'P' THEN 'PRIMARY KEY' END AS const,
221
+ ac.constraint_name AS \"key\",
222
+ CASE c.nullable WHEN 'Y' THEN 'YES' ELSE 'NO' END AS is_nullable
223
+ FROM all_tab_cols c
224
+ 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
225
+ 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'
226
+ LEFT OUTER JOIN all_views v ON c.owner = v.owner AND c.table_name = v.view_name
227
+ WHERE c.owner IN (#{::Brick.db_schemas.keys.map { |s| "'#{s}'" }.join(', ')})
228
+ AND c.table_name NOT IN (?, ?)
229
+ ORDER BY 1, 2, c.internal_column_id, acc.position"
230
+ ActiveRecord::Base.execute_sql(sql, *ar_tables)
231
+ else
232
+ retrieve_schema_and_tables(sql)
233
+ end
234
+
235
+ schema_and_tables.each do |r|
236
+ next if r[1].index('$') # Oracle can have goofy table names with $
237
+
238
+ if (relation_name = r[1]) =~ /^[A-Z0-9_]+$/
239
+ relation_name.downcase!
240
+ # Both uppers and lowers as well as underscores?
241
+ elsif relation_name =~ /[A-Z]/ && relation_name =~ /[a-z]/ && relation_name.index('_')
242
+ ::Brick.apply_double_underscore_patch
243
+ end
244
+ # Expect the default schema for SQL Server to be 'dbo'.
245
+ if (::Brick.is_oracle && r[0] != schema) || (is_mssql && r[0] != 'dbo')
246
+ relation_name = "#{r[0]}.#{relation_name}"
247
+ end
248
+
249
+ relation = relations[relation_name] # here relation represents a table or view from the database
250
+ relation[:isView] = true if r[2] == 'VIEW' # table_type
251
+ col_name = ::Brick.is_oracle ? connection.send(:oracle_downcase, r[3]) : r[3]
252
+ key = case r[6] # constraint type
253
+ when 'PRIMARY KEY'
254
+ # key
255
+ relation[:pkey][r[7] || relation_name] ||= []
256
+ when 'UNIQUE'
257
+ relation[:ukeys][r[7] || "#{relation_name}.#{col_name}"] ||= []
258
+ # key = (relation[:ukeys] = Hash.new { |h, k| h[k] = [] }) if key.is_a?(Array)
259
+ # key[r['key']]
260
+ end
261
+ key << col_name if key
262
+ cols = relation[:cols] # relation.fetch(:cols) { relation[:cols] = [] }
263
+ # 'data_type', 'max_length', measure, 'is_nullable'
264
+ cols[col_name] = [r[4], r[5], measures&.include?(col_name), r[8] == 'NO']
265
+ # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
266
+ end
267
+ end
268
+
269
+ # PostGIS adds three views which would confuse Rails if models were to be built for them.
270
+ if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
271
+ if relations.key?('geography_columns') && relations.key?('geometry_columns') && relations.key?('spatial_ref_sys')
272
+ (::Brick.config.exclude_tables ||= []) << 'geography_columns'
273
+ ::Brick.config.exclude_tables << 'geometry_columns'
274
+ ::Brick.config.exclude_tables << 'spatial_ref_sys'
275
+ end
276
+ end
277
+
278
+ # # Add unique OIDs
279
+ # if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
280
+ # ActiveRecord::Base.execute_sql(
281
+ # "SELECT c.oid, n.nspname, c.relname
282
+ # FROM pg_catalog.pg_namespace AS n
283
+ # INNER JOIN pg_catalog.pg_class AS c ON n.oid = c.relnamespace
284
+ # WHERE c.relkind IN ('r', 'v')"
285
+ # ).each do |r|
286
+ # next if ['pg_catalog', 'information_schema', ''].include?(r['nspname']) ||
287
+ # ['ar_internal_metadata', 'schema_migrations'].include?(r['relname'])
288
+ # relation = relations.fetch(r['relname'], nil)
289
+ # if relation
290
+ # (relation[:oid] ||= {})[r['nspname']] = r['oid']
291
+ # else
292
+ # puts "Where is #{r['nspname']} #{r['relname']} ?"
293
+ # end
294
+ # end
295
+ # end
296
+ # schema = ::Brick.default_schema # Reset back for this next round of fun
297
+
298
+ # ---------------------------------------------
299
+ # 4. Foreign key info
300
+ # (done in two parts which get JOINed together in Ruby code)
301
+ kcus = nil
302
+ entry_type = nil
303
+ case ActiveRecord::Base.connection.adapter_name
304
+ when 'PostgreSQL', 'Mysql2', 'Trilogy', 'SQLServer'
305
+ # Part 1 -- all KCUs
306
+ sql = "SELECT CONSTRAINT_CATALOG, CONSTRAINT_SCHEMA, CONSTRAINT_NAME, ORDINAL_POSITION,
307
+ TABLE_NAME, COLUMN_NAME
308
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE#{"
309
+ WHERE CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema }#{"
310
+ WHERE CONSTRAINT_SCHEMA = '#{ActiveRecord::Base.connection.current_database&.tr("'", "''")}'" if ActiveRecord::Base.is_mysql
311
+ }"
312
+ kcus = ActiveRecord::Base.execute_sql(sql).each_with_object({}) do |v, s|
313
+ if (entry_type ||= v.is_a?(Array) ? :array : :hash) == :hash
314
+ key = "#{v['constraint_name']}.#{v['constraint_schema']}.#{v['constraint_catalog']}.#{v['ordinal_position']}"
315
+ key << ".#{v['table_name']}.#{v['column_name']}" unless is_postgres || is_mssql
316
+ s[key] = [v['constraint_schema'], v['table_name']]
317
+ else # Array
318
+ key = "#{v[2]}.#{v[1]}.#{v[0]}.#{v[3]}"
319
+ key << ".#{v[4]}.#{v[5]}" unless is_postgres || is_mssql
320
+ s[key] = [v[1], v[4]]
321
+ end
322
+ end
323
+
324
+ # Part 2 -- fk_references
325
+ sql = "SELECT kcu.CONSTRAINT_SCHEMA, kcu.TABLE_NAME, kcu.COLUMN_NAME,
326
+ #{# These will get filled in with real values (effectively doing the JOIN in Ruby)
327
+ is_postgres || is_mssql ? 'NULL as primary_schema, NULL as primary_table' :
328
+ 'kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME'},
329
+ kcu.CONSTRAINT_NAME AS CONSTRAINT_SCHEMA_FK,
330
+ rc.UNIQUE_CONSTRAINT_NAME, rc.UNIQUE_CONSTRAINT_SCHEMA, rc.UNIQUE_CONSTRAINT_CATALOG, kcu.ORDINAL_POSITION
331
+ FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc
332
+ INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu
333
+ ON kcu.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG
334
+ AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
335
+ AND kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME#{"
336
+ WHERE kcu.CONSTRAINT_SCHEMA = COALESCE(current_setting('SEARCH_PATH'), 'public')" if is_postgres && schema}#{"
337
+ WHERE kcu.CONSTRAINT_SCHEMA = '#{ActiveRecord::Base.connection.current_database&.tr("'", "''")}'" if ActiveRecord::Base.is_mysql}"
338
+ fk_references = ActiveRecord::Base.execute_sql(sql)
339
+ when 'SQLite'
340
+ sql = "SELECT NULL AS constraint_schema, m.name, fkl.\"from\", NULL AS primary_schema, fkl.\"table\", m.name || '_' || fkl.\"from\" AS constraint_name
341
+ FROM sqlite_master m
342
+ INNER JOIN pragma_foreign_key_list(m.name) fkl ON m.type = 'table'
343
+ ORDER BY m.name, fkl.seq"
344
+ fk_references = ActiveRecord::Base.execute_sql(sql)
345
+ when 'OracleEnhanced'
346
+ schemas = ::Brick.db_schemas.keys.map { |s| "'#{s}'" }.join(', ')
347
+ sql =
348
+ "SELECT -- fk
349
+ ac.owner AS constraint_schema, acc_fk.table_name, acc_fk.column_name,
350
+ -- referenced pk
351
+ ac.r_owner AS primary_schema, acc_pk.table_name AS primary_table, acc_fk.constraint_name AS constraint_schema_fk
352
+ -- , acc_pk.column_name
353
+ FROM all_cons_columns acc_fk
354
+ INNER JOIN all_constraints ac ON acc_fk.owner = ac.owner
355
+ AND acc_fk.constraint_name = ac.constraint_name
356
+ INNER JOIN all_cons_columns acc_pk ON ac.r_owner = acc_pk.owner
357
+ AND ac.r_constraint_name = acc_pk.constraint_name
358
+ WHERE ac.constraint_type = 'R'
359
+ AND ac.owner IN (#{schemas})
360
+ AND ac.r_owner IN (#{schemas})"
361
+ fk_references = ActiveRecord::Base.execute_sql(sql)
362
+ end
363
+ ::Brick.is_oracle = true if ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
364
+ # ::Brick.default_schema ||= schema ||= 'public' if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
365
+ ::Brick.default_schema ||= 'public' if is_postgres
366
+ fk_references&.each do |fk|
367
+ fk = fk.values unless fk.is_a?(Array)
368
+ # Virtually JOIN KCUs to fk_references in order to fill in the primary schema and primary table
369
+ kcu_key = "#{fk[6]}.#{fk[7]}.#{fk[8]}.#{fk[9]}"
370
+ kcu_key << ".#{fk[3]}.#{fk[4]}" unless is_postgres || is_mssql
371
+ if (kcu = kcus&.fetch(kcu_key, nil))
372
+ fk[3] = kcu[0]
373
+ fk[4] = kcu[1]
374
+ end
375
+ # Multitenancy makes things a little more general overall, except for non-tenanted tables
376
+ if ::Brick.is_apartment_excluded_table(::Brick.namify(fk[1]))
377
+ fk[0] = ::Brick.apartment_default_tenant
378
+ elsif (is_postgres && (fk[0] == 'public' || (multitenancy && fk[0] == schema))) ||
379
+ (::Brick.is_oracle && fk[0] == schema) ||
380
+ (is_mssql && fk[0] == 'dbo') ||
381
+ (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[0]))
382
+ fk[0] = nil
383
+ end
384
+ if ::Brick.is_apartment_excluded_table(fk[4])
385
+ fk[3] = ::Brick.apartment_default_tenant
386
+ elsif (is_postgres && (fk[3] == 'public' || (multitenancy && fk[3] == schema))) ||
387
+ (::Brick.is_oracle && fk[3] == schema) ||
388
+ (is_mssql && fk[3] == 'dbo') ||
389
+ (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[3]))
390
+ fk[3] = nil
391
+ end
392
+ if ::Brick.is_oracle
393
+ fk[1].downcase! if fk[1] =~ /^[A-Z0-9_]+$/
394
+ fk[4].downcase! if fk[4] =~ /^[A-Z0-9_]+$/
395
+ fk[2] = connection.send(:oracle_downcase, fk[2])
396
+ end
397
+ ::Brick._add_bt_and_hm(fk, relations)
398
+ end
399
+ kcus = nil # Allow this large item to be garbage collected
400
+ end
401
+
402
+ table_name_lookup = (::Brick.table_name_lookup ||= {})
403
+ relations.each do |k, v|
404
+ next if k.is_a?(Symbol)
405
+
406
+ rel_name = k.split('.').map { |rel_part| ::Brick.namify(rel_part, :underscore) }
407
+ schema_names = rel_name[0..-2]
408
+ schema_names.shift if ::Brick.apartment_multitenant && schema_names.first == ::Brick.apartment_default_tenant
409
+ v[:schema] = schema_names.join('.') unless schema_names.empty?
410
+ # %%% If more than one schema has the same table name, will need to add a schema name prefix to have uniqueness
411
+ if (singular = rel_name.last.singularize).blank?
412
+ singular = rel_name.last
413
+ end
414
+ name_parts = if (tnp = ::Brick.config.table_name_prefixes
415
+ &.find do |k1, v1|
416
+ k.start_with?(k1) &&
417
+ ((k.length >= k1.length && v1) ||
418
+ (k.length == k1.length && (v1.nil? || v1.start_with?('::'))))
419
+ end
420
+ )&.present?
421
+ if tnp.last&.start_with?('::') # TNP that points to an exact class?
422
+ # Had considered: [tnp.last[2..-1]]
423
+ [singular]
424
+ elsif tnp.last
425
+ v[:auto_prefixed_schema], v[:auto_prefixed_class] = tnp
426
+ # v[:resource] = rel_name.last[tnp.first.length..-1]
427
+ [tnp.last, singular[tnp.first.length..-1]]
428
+ else # Override applying an auto-prefix for any TNP that points to nil
429
+ [singular]
430
+ end
431
+ else
432
+ # v[:resource] = rel_name.last
433
+ [singular]
434
+ end
435
+ proposed_name_parts = (schema_names + name_parts).map { |p| ::Brick.namify(p, :underscore).camelize }
436
+ # Find out if the proposed name leads to a module or class that already exists and is not an AR class
437
+ colliding_thing = nil
438
+ loop do
439
+ klass = Object
440
+ proposed_name_parts.each do |part|
441
+ if klass.const_defined?(part)
442
+ begin
443
+ klass = klass.const_get(part)
444
+ rescue NoMethodError => e
445
+ klass = nil
446
+ break
447
+ end
448
+ else
449
+ klass = nil
450
+ break
451
+ end
452
+ end
453
+ break if !klass || (klass < ActiveRecord::Base) # Break if all good -- no conflicts
454
+
455
+ # Find a unique name since there's already something that's non-AR with that same name
456
+ last_idx = proposed_name_parts.length - 1
457
+ proposed_name_parts[last_idx] = ::Brick.ensure_unique(proposed_name_parts[last_idx], 'X')
458
+ colliding_thing ||= klass
459
+ end
460
+ v[:class_name] = proposed_name_parts.join('::')
461
+ # Was: v[:resource] = v[:class_name].underscore.tr('/', '.')
462
+ v[:resource] = proposed_name_parts.last.underscore
463
+ if colliding_thing
464
+ message_start = if colliding_thing.is_a?(Module) && Object.const_defined?(:Rails) &&
465
+ colliding_thing.constants.find { |c| colliding_thing.const_get(c) < Rails::Application }
466
+ "The module for the Rails application itself, \"#{colliding_thing.name}\","
467
+ else
468
+ "Non-AR #{colliding_thing.class.name.downcase} \"#{colliding_thing.name}\""
469
+ end
470
+ puts "WARNING: #{message_start} already exists.\n Will set up to auto-create model #{v[:class_name]} for table #{k}."
471
+ end
472
+ # Track anything that's out-of-the-ordinary
473
+ table_name_lookup[v[:class_name]] = k unless v[:class_name].underscore.pluralize == k
474
+ end
475
+ ::Brick.load_additional_references if ::Brick.initializer_loaded
476
+
477
+ if is_postgres
478
+ params = []
479
+ ActiveRecord::Base.execute_sql("-- inherited and partitioned tables counts
480
+ SELECT n.nspname, parent.relname,
481
+ ((SUM(child.reltuples::float) / greatest(SUM(child.relpages), 1))) *
482
+ (SUM(pg_relation_size(child.oid))::float / (current_setting('block_size')::float))::integer AS rowcount
483
+ FROM pg_inherits
484
+ INNER JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
485
+ INNER JOIN pg_class child ON pg_inherits.inhrelid = child.oid
486
+ INNER JOIN pg_catalog.pg_namespace n ON n.oid = parent.relnamespace#{
487
+ if schema
488
+ params << schema
489
+ "
490
+ WHERE n.nspname = COALESCE(?, 'public')"
491
+ end}
492
+ GROUP BY n.nspname, parent.relname, child.reltuples, child.relpages, child.oid
493
+
494
+ UNION ALL
495
+
496
+ -- table count
497
+ SELECT n.nspname, pg_class.relname,
498
+ (pg_class.reltuples::float / greatest(pg_class.relpages, 1)) *
499
+ (pg_relation_size(pg_class.oid)::float / (current_setting('block_size')::float))::integer AS rowcount
500
+ FROM pg_class
501
+ INNER JOIN pg_catalog.pg_namespace n ON n.oid = pg_class.relnamespace#{
502
+ if schema
503
+ params << schema
504
+ "
505
+ WHERE n.nspname = COALESCE(?, 'public')"
506
+ end}
507
+ GROUP BY n.nspname, pg_class.relname, pg_class.reltuples, pg_class.relpages, pg_class.oid", params).each do |tblcount|
508
+ # %%% What is the default schema here?
509
+ prefix = "#{tblcount['nspname']}." unless tblcount['nspname'] == (schema || 'public')
510
+ relations.fetch("#{prefix}#{tblcount['relname']}", nil)&.[]=(:rowcount, tblcount['rowcount'].to_i.round)
511
+ end
512
+ end
513
+
514
+ if orig_schema && (orig_schema = (orig_schema - ['pg_catalog', 'pg_toast', 'heroku_ext']).first)
515
+ puts "Now switching back to \"#{orig_schema}\" schema."
516
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", orig_schema)
517
+ end
518
+ end
519
+
520
+ def retrieve_schema_and_tables(sql = nil, is_postgres = nil, is_mssql = nil, schema = nil)
521
+ is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' if is_mssql.nil?
522
+ params = ar_tables
523
+ sql ||= "SELECT t.table_schema AS \"schema\", t.table_name AS relation_name, t.table_type,#{"
524
+ pg_catalog.obj_description(
525
+ ('\"' || t.table_schema || '\".\"' || t.table_name || '\"')::regclass::oid, 'pg_class'
526
+ ) AS table_description,
527
+ pg_catalog.col_description(
528
+ ('\"' || t.table_schema || '\".\"' || t.table_name || '\"')::regclass::oid, c.ordinal_position
529
+ ) AS column_description," if is_postgres}
530
+ 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'},
531
+ COALESCE(c.character_maximum_length, c.numeric_precision) AS max_length,
532
+ kcu.constraint_type AS const, kcu.constraint_name AS \"key\",
533
+ c.is_nullable
534
+ FROM INFORMATION_SCHEMA.tables AS t
535
+ LEFT OUTER JOIN INFORMATION_SCHEMA.columns AS c ON t.table_schema = c.table_schema
536
+ AND t.table_name = c.table_name
537
+ LEFT OUTER JOIN
538
+ (SELECT kcu1.constraint_schema, kcu1.table_name, kcu1.column_name, kcu1.ordinal_position,
539
+ tc.constraint_type, kcu1.constraint_name
540
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu1
541
+ INNER JOIN INFORMATION_SCHEMA.table_constraints AS tc
542
+ ON kcu1.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
543
+ AND kcu1.TABLE_NAME = tc.TABLE_NAME
544
+ AND kcu1.CONSTRAINT_NAME = tc.constraint_name
545
+ AND tc.constraint_type != 'FOREIGN KEY' -- For MSSQL
546
+ ) AS kcu ON
547
+ -- kcu.CONSTRAINT_CATALOG = t.table_catalog AND
548
+ kcu.CONSTRAINT_SCHEMA = c.table_schema
549
+ AND kcu.TABLE_NAME = c.table_name
550
+ AND kcu.column_name = c.column_name#{"
551
+ -- AND kcu.position_in_unique_constraint IS NULL" unless is_mssql}#{"
552
+ INNER JOIN pg_catalog.pg_namespace pg_n ON pg_n.nspname = t.table_schema
553
+ INNER JOIN pg_catalog.pg_class pg_c ON pg_n.oid = pg_c.relnamespace AND pg_c.relname = c.table_name
554
+ INNER JOIN pg_catalog.pg_attribute pg_a ON pg_c.oid = pg_a.attrelid AND pg_a.attname = c.column_name
555
+ INNER JOIN pg_catalog.pg_type pg_t ON pg_t.oid = pg_a.atttypid" if is_postgres}
556
+ WHERE t.table_schema #{is_postgres || is_mssql ?
557
+ "NOT IN ('information_schema', 'pg_catalog', 'pg_toast', 'heroku_ext',
558
+ 'INFORMATION_SCHEMA', 'sys')"
559
+ :
560
+ "= '#{ActiveRecord::Base.connection.current_database&.tr("'", "''")}'"}#{
561
+ if is_postgres && schema
562
+ params = params.unshift(schema) # Used to use this SQL: current_setting('SEARCH_PATH')
563
+ "
564
+ AND t.table_schema = COALESCE(?, 'public')"
565
+ end}
566
+ -- AND t.table_type IN ('VIEW') -- 'BASE TABLE', 'FOREIGN TABLE'
567
+ AND t.table_name NOT IN ('pg_stat_statements', ?, ?)
568
+ ORDER BY 1, t.table_type DESC, 2, c.ordinal_position"
569
+ ActiveRecord::Base.execute_sql(sql, *params)
570
+ end
571
+
572
+ def ar_tables
573
+ ar_smtn = if ActiveRecord::Base.respond_to?(:schema_migrations_table_name)
574
+ ActiveRecord::Base.schema_migrations_table_name
575
+ else
576
+ 'schema_migrations'
577
+ end
578
+ ar_imtn = ActiveRecord.version >= ::Gem::Version.new('5.0') ? ActiveRecord::Base.internal_metadata_table_name : 'ar_internal_metadata'
579
+ [ar_smtn, ar_imtn]
580
+ end
581
+ end
582
+ end