brick 1.0.198 → 1.0.200
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/brick/extensions.rb +115 -605
- data/lib/brick/frameworks/rails/engine.rb +2 -37
- data/lib/brick/frameworks/rails/form_tags.rb +3 -3
- data/lib/brick/reflect_tables.rb +582 -0
- data/lib/brick/route_mapper.rb +20 -4
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +2 -1
- data/lib/generators/brick/migration_builder.rb +8 -5
- metadata +3 -2
@@ -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
|
data/lib/brick/route_mapper.rb
CHANGED
@@ -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
|
-
|
55
|
-
|
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
|
-
|
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)
|
data/lib/brick/version_number.rb
CHANGED
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'
|
@@ -224,7 +225,7 @@ module Brick
|
|
224
225
|
if !a.polymorphic? && (a.belongs_to? || (through && (thr = a.through_reflection))) &&
|
225
226
|
!((kls = thr&.klass || a.klass) && ::Brick.config.exclude_tables.exclude?(kls.table_name) &&
|
226
227
|
(!a.belongs_to? ||
|
227
|
-
((fk_type = a.foreign_key.is_a?(Array) ? a.foreign_key.map { |fk_part| model_cols[fk_part.to_s].type } : model_cols[a.foreign_key.to_s]
|
228
|
+
((fk_type = a.foreign_key.is_a?(Array) ? a.foreign_key.map { |fk_part| model_cols[fk_part.to_s].type } : model_cols[a.foreign_key.to_s]&.type) &&
|
228
229
|
(primary_cols = primary_klass.columns_hash) &&
|
229
230
|
(pk_type = a_pk.is_a?(Array) ? a_pk.map { |pk_part| primary_cols[pk_part.to_s].type } : primary_cols[a_pk].type) &&
|
230
231
|
(same_type = (pk_type == fk_type))
|