brick 1.0.199 → 1.0.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -45,16 +45,23 @@ module Brick
45
45
  table_class_length = 38 # Length of "Classes that can be built from tables:"
46
46
  view_class_length = 37 # Length of "Classes that can be built from views:"
47
47
 
48
- brick_namespace_create = lambda do |path_names, res_name, options|
48
+ brick_namespace_create = lambda do |path_names, res_name, options, ind = 0|
49
49
  if path_names&.present?
50
50
  if (path_name = path_names.pop).is_a?(Array)
51
51
  module_name = path_name[1]
52
52
  path_name = path_name.first
53
53
  end
54
- send(:scope, { module: module_name || path_name, path: path_name, as: path_name }) do
55
- brick_namespace_create.call(path_names, res_name, options)
54
+ scope_options = { module: module_name || path_name, path: path_name, as: path_name }
55
+ # if module_name.nil? || module_name == path_name
56
+ # puts "#{' ' * ind}namespace :#{path_name}"
57
+ # else
58
+ # puts "#{' ' * ind}scope #{scope_options.inspect}"
59
+ # end
60
+ send(:scope, scope_options) do
61
+ brick_namespace_create.call(path_names, res_name, options, ind + 1)
56
62
  end
57
63
  else
64
+ # puts "#{' ' * ind}resources :#{res_name} #{options.inspect unless options.blank?}"
58
65
  send(:resources, res_name.to_sym, **options)
59
66
  end
60
67
  end
@@ -82,8 +89,16 @@ module Brick
82
89
 
83
90
  object_name = k.split('.').last # Take off any first schema part
84
91
 
92
+ # # What about:
93
+ # full_schema_prefix = if (aps2 = v.fetch(:auto_prefixed_schema, nil))
94
+ # # Used to be: aps = aps[0..-2] if aps[-1] == '_'
95
+ # aps2 = aps2[0..-2] if aps2[-1] == '_'
96
+ # aps = v[:auto_prefixed_class].underscore
97
+
85
98
  full_schema_prefix = if (aps = v.fetch(:auto_prefixed_schema, nil))
86
99
  aps = aps[0..-2] if aps[-1] == '_'
100
+ # %%% If this really is nil then should be an override
101
+ aps2 = v[:auto_prefixed_class]&.underscore
87
102
  (schema_prefix&.dup || +'') << "#{aps}."
88
103
  else
89
104
  schema_prefix
@@ -105,7 +120,8 @@ module Brick
105
120
 
106
121
  # First do the normal routes
107
122
  prefixes = []
108
- prefixes << [aps, v[:class_name]&.split('::')[-2]&.underscore] if aps
123
+ # Second term used to be: v[:class_name]&.split('::')[-2]&.underscore
124
+ prefixes << [aps, aps2] if aps
109
125
  prefixes << schema_name if schema_name
110
126
  prefixes << path_prefix if path_prefix
111
127
  brick_namespace_create.call(prefixes, resource_name, options)
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 199
8
+ TINY = 201
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
@@ -89,6 +89,7 @@ end
89
89
  # is first established), and then automatically creates models, controllers, views,
90
90
  # and routes based on those available relations.
91
91
  require 'brick/config'
92
+ require 'brick/reflect_tables'
92
93
  if Gem::Dependency.new('rails').matching_specs.present?
93
94
  require 'rails'
94
95
  require 'brick/frameworks/rails'