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.
- checksums.yaml +4 -4
- data/lib/brick/extensions.rb +196 -648
- data/lib/brick/frameworks/rails/engine.rb +55 -61
- 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 +1 -0
- metadata +4 -3
@@ -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'
|