brick 1.0.53 → 1.0.56

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 737256fad5c987fa2ac603cf3708da959127a33ab9b27d164ceff858b71987b9
4
- data.tar.gz: 144352db7686d19d9c330887069287da88de456276cc17aaeeaeb3c1de7a3281
3
+ metadata.gz: b696c53c9d9d359d49de147b06b5342820ca230451c860d097f44bcae3b23bb0
4
+ data.tar.gz: 7b0f6ff365f918a97d64305eabf70fcfe78cf764514ca22b3731b780da04b22c
5
5
  SHA512:
6
- metadata.gz: 83ec370e06c18bf9d62ec3ed445daf78dc5cbff825a85ff017cc7b814efb62f86e68022f7577734dd01ddf7e2f74b4b59de4b40b9c07656e83d0859ba6a4333d
7
- data.tar.gz: 9106373ca7878dcc75c596f1fa44d420980c50cb09f5b63a1110d4e02dc63f02dafe3d1bc0d4de38a75d5678d964e3b8f12a720a722d2f593934e2f6be97f818
6
+ metadata.gz: 43ab6db28554fb7502d0f03267d2f5bab27d310fb988701d371232f6604090cd679767a12097a7c217401b05e938c787844ae11138a2c8143c1e675b93599656
7
+ data.tar.gz: 8d2e592ba27bba30e100ea23a2bf951d6f3861c422ef355bad7cd38e146c20c2cf53380aa7ee440f40aa2d111c7f33eadc8dc09f73d1689432fff071e04228c4
@@ -1289,7 +1289,10 @@ module ActiveRecord::ConnectionHandling
1289
1289
  alias _brick_establish_connection establish_connection
1290
1290
  def establish_connection(*args)
1291
1291
  conn = _brick_establish_connection(*args)
1292
- _brick_reflect_tables
1292
+ begin
1293
+ _brick_reflect_tables
1294
+ rescue ActiveRecord::NoDatabaseError
1295
+ end
1293
1296
  conn
1294
1297
  end
1295
1298
 
@@ -1313,16 +1316,21 @@ module ActiveRecord::ConnectionHandling
1313
1316
  # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
1314
1317
 
1315
1318
  is_postgres = nil
1316
- schema_sql = 'SELECT NULL AS table_schema;'
1317
1319
  case ActiveRecord::Base.connection.adapter_name
1318
1320
  when 'PostgreSQL'
1319
1321
  is_postgres = true
1322
+ db_schemas = ActiveRecord::Base.execute_sql('SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;')
1323
+ ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1324
+ row = row.is_a?(String) ? row : row['table_schema']
1325
+ # Remove any system schemas
1326
+ s[row] = nil unless ['information_schema', 'pg_catalog'].include?(row)
1327
+ end
1320
1328
  if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
1321
- (sta = multitenancy[:schema_to_analyse]) != 'public')
1329
+ (sta = multitenancy[:schema_to_analyse]) != 'public') &&
1330
+ ::Brick.db_schemas.include?(sta)
1322
1331
  ::Brick.default_schema = schema = sta
1323
1332
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1324
1333
  end
1325
- schema_sql = 'SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;'
1326
1334
  when 'Mysql2'
1327
1335
  ::Brick.default_schema = schema = ActiveRecord::Base.connection.current_database
1328
1336
  when 'SQLite'
@@ -1339,16 +1347,7 @@ module ActiveRecord::ConnectionHandling
1339
1347
  puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
1340
1348
  end
1341
1349
 
1342
- unless (db_schemas = ActiveRecord::Base.execute_sql(schema_sql)).is_a?(Array)
1343
- db_schemas = db_schemas.to_a
1344
- end
1345
- unless db_schemas.empty?
1346
- ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1347
- row = row.is_a?(String) ? row : row['table_schema']
1348
- # Remove any system schemas
1349
- s[row] = nil unless ['information_schema', 'pg_catalog'].include?(row)
1350
- end
1351
- end
1350
+ ::Brick.db_schemas ||= []
1352
1351
 
1353
1352
  if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1354
1353
  if (possible_schema = ::Brick.config.schema_behavior&.[](:multitenant)&.[](:schema_to_analyse))
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 53
8
+ TINY = 56
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
@@ -7,13 +7,13 @@ module Brick
7
7
  class InstallGenerator < ::Rails::Generators::Base
8
8
  # include ::Rails::Generators::Migration
9
9
 
10
- source_root File.expand_path('templates', __dir__)
11
- class_option(
12
- :with_changes,
13
- type: :boolean,
14
- default: false,
15
- desc: 'Store changeset (diff) with each version'
16
- )
10
+ # source_root File.expand_path('templates', __dir__)
11
+ # class_option(
12
+ # :with_changes,
13
+ # type: :boolean,
14
+ # default: false,
15
+ # desc: 'Store changeset (diff) with each version'
16
+ # )
17
17
 
18
18
  desc 'Generates an initializer file for configuring Brick'
19
19
 
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+ require 'fancy_gets'
6
+
7
+ module Brick
8
+ # Auto-generates migrations
9
+ class MigrationsGenerator < ::Rails::Generators::Base
10
+ include FancyGets
11
+ # include ::Rails::Generators::Migration
12
+
13
+ # SQL types that are the same as their migration data type name: text, integer, bigint, date, boolean, decimal, float
14
+ SQL_TYPES = { 'character varying' => 'string',
15
+ 'character' => 'string', # %%% Need to put in "limit: 1"
16
+ 'xml' => 'text',
17
+ 'bytea' => 'binary',
18
+ 'timestamp without time zone' => 'timestamp',
19
+ 'timestamp with time zone' => 'timestamp',
20
+ 'time without time zone' => 'time',
21
+ 'time with time zone' => 'time',
22
+ 'double precision' => 'float', # might work with 'double'
23
+ 'smallint' => 'integer' } # %%% Need to put in "limit: 2"
24
+ # (Still need to find what "inet" and "json" data types map to.)
25
+
26
+ # # source_root File.expand_path('templates', __dir__)
27
+ # class_option(
28
+ # :with_changes,
29
+ # type: :boolean,
30
+ # default: false,
31
+ # desc: 'Add IMPORT_TEMPLATE to model'
32
+ # )
33
+
34
+ desc 'Auto-generates migrations for an existing database.'
35
+
36
+ def brick_migrations
37
+ # If Apartment is active, see if a default schema to analyse is indicated
38
+
39
+ # # Load all models
40
+ # Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
41
+
42
+ if (tables = ::Brick.relations.reject { |k, v| v.key?(:isView) && v[:isView] == true }.map(&:first).sort).empty?
43
+ puts "No tables found in database #{ActiveRecord::Base.connection.current_database}."
44
+ return
45
+ end
46
+
47
+ key_type = (ActiveRecord.version < ::Gem::Version.new('5.1') ? 'integer' : 'bigint')
48
+ is_4x_rails = ActiveRecord.version < ::Gem::Version.new('5.0')
49
+ ar_version = "[#{ActiveRecord.version.segments[0..1].join('.')}]" unless is_4x_rails
50
+ default_mig_path = (mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{::Rails.root}/db/migrate")
51
+ if Dir.exist?(mig_path)
52
+ if Dir["#{mig_path}/**/*.rb"].present?
53
+ puts "WARNING: migrations folder #{mig_path} appears to already have ruby files present."
54
+ mig_path2 = "#{::Rails.root}/tmp/brick_migrations"
55
+ if Dir.exist?(mig_path2)
56
+ if Dir["#{mig_path2}/**/*.rb"].present?
57
+ puts "As well, temporary folder #{mig_path2} also has ruby files present."
58
+ puts "Choose a destination -- all existing .rb files will be removed:"
59
+ mig_path2 = gets_list(list: ['Cancel operation!', "Append migration files into #{mig_path} anyway", mig_path, mig_path2])
60
+ return if mig_path2.start_with?('Cancel')
61
+
62
+ if mig_path2.start_with?('Append migration files into ')
63
+ mig_path2 = mig_path
64
+ else
65
+ Dir["#{mig_path2}/**/*.rb"].each { |rb| File.delete(rb) }
66
+ end
67
+ else
68
+ puts "Using temporary folder #{mig_path2} for created migration files.\n\n"
69
+ end
70
+ else
71
+ puts "Creating the temporary folder #{mig_path2} for created migration files.\n\n"
72
+ Dir.mkdir(mig_path2)
73
+ end
74
+ mig_path = mig_path2
75
+ else
76
+ puts "Using standard migration folder #{mig_path} for created migration files.\n\n"
77
+ end
78
+ else
79
+ puts "Creating standard ActiveRecord migration folder #{mig_path} to hold new migration files.\n\n"
80
+ Dir.mkdir(mig_path)
81
+ end
82
+
83
+ # Generate a list of tables that can be chosen
84
+ chosen = gets_list(list: tables, chosen: tables.dup)
85
+ # Start the timestamps back the same number of minutes from now as expected number of migrations to create
86
+ current_mig_time = Time.now - chosen.length.minutes
87
+ done = []
88
+ fks = {}
89
+ stuck = {}
90
+ indexes = {} # Track index names to make sure things are unique
91
+ versions = [] # Resulting versions to be used when updating the schema_migrations table
92
+ # Start by making migrations for fringe tables (those with no foreign keys).
93
+ # Continue layer by layer, creating migrations for tables that reference ones already done, until
94
+ # no more migrations can be created. (At that point hopefully all tables are accounted for.)
95
+ while (fringe = chosen.reject do |tbl|
96
+ snags = ::Brick.relations.fetch(tbl, nil)&.fetch(:fks, nil)&.select do |_k, v|
97
+ v[:is_bt] && !v[:polymorphic] &&
98
+ tbl != v[:inverse_table] && # Ignore self-referencing associations (stuff like "parent_id")
99
+ !done.include?(v[:inverse_table])
100
+ end
101
+ stuck[tbl] = snags if snags&.present?
102
+ end).present?
103
+ fringe.each do |tbl|
104
+ next unless (relation = ::Brick.relations.fetch(tbl, nil))&.fetch(:cols, nil)&.present?
105
+
106
+ pkey_cols = (rpk = relation[:pkey].values.flatten) & (arpk = [ActiveRecord::Base.primary_key].flatten)
107
+ # In case things aren't as standard
108
+ if pkey_cols.empty?
109
+ pkey_cols = if rpk.empty? && relation[:cols][arpk.first]&.first == key_type
110
+ arpk
111
+ elsif rpk.first
112
+ rpk
113
+ end
114
+ end
115
+ schema = if (tbl_parts = tbl.split('.')).length > 1
116
+ if tbl_parts.first == 'public'
117
+ tbl_parts.shift
118
+ nil
119
+ else
120
+ tbl_parts.first
121
+ end
122
+ end
123
+ # %%% For the moment we're skipping polymorphics
124
+ fkey_cols = relation[:fks].values.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
125
+ mig = +"class Create#{(full_table_name = tbl_parts.join('_')).camelize} < ActiveRecord::Migration#{ar_version}\n"
126
+ # Support missing primary key (by adding: ,id: false)
127
+ # also integer / uuid / other non-standard data types for primary key
128
+ id_option = unless (pkey_col_first = relation[:cols][pkey_cols&.first]&.first) == key_type
129
+ unless pkey_cols&.present?
130
+ ', id: false'
131
+ else
132
+ case pkey_col_first
133
+ when 'integer'
134
+ ', id: :serial'
135
+ when 'bigint'
136
+ ', id: :bigserial'
137
+ else
138
+ ", id: :#{SQL_TYPES[pkey_col_first] || pkey_col_first}" # Something like: id: :integer, primary_key: :businessentityid
139
+ end +
140
+ (pkey_cols.first ? ", primary_key: :#{pkey_cols.first}" : '') +
141
+ (!is_4x_rails && (comment = relation&.fetch(:description, nil))&.present? ? ", comment: #{comment.inspect}" : '')
142
+ end
143
+ end
144
+ # Find the ActiveRecord class in order to see if the columns have comments
145
+ unless is_4x_rails
146
+ klass = begin
147
+ tbl.tr('.', '/').singularize.camelize.constantize
148
+ rescue StandardError
149
+ end
150
+ if klass
151
+ unless ActiveRecord::Migration.table_exists?(klass.table_name)
152
+ puts "WARNING: Unable to locate table #{klass.table_name} (for #{klass.name})."
153
+ klass = nil
154
+ end
155
+ end
156
+ end
157
+ # Refer to this table name as a symbol or dotted string as appropriate
158
+ tbl_code = tbl_parts.length == 1 ? ":#{tbl_parts.first}" : "'#{tbl}'"
159
+ mig << " def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
160
+ mig << " create_schema :#{schema} unless schema_exists?(:#{schema})\n" if schema
161
+ mig << " create_table #{tbl_code}#{id_option} do |t|\n"
162
+ possible_ts = [] # Track possible generic timestamps
163
+ add_fks = [] # Track foreign keys to add after table creation
164
+ relation[:cols].each do |col, col_type|
165
+ next if !id_option&.end_with?('id: false') && pkey_cols.include?(col)
166
+
167
+ # See if there are generic timestamps
168
+ if (sql_type = SQL_TYPES[col_type.first]) == 'timestamp' &&
169
+ ['created_at','updated_at'].include?(col)
170
+ possible_ts << [col, !col_type[3]]
171
+ next
172
+ end
173
+
174
+ sql_type ||= col_type.first
175
+ suffix = col_type[3] ? +', null: false' : +''
176
+ if !is_4x_rails && klass && (comment = klass.columns_hash.fetch(col, nil)&.comment)&.present?
177
+ suffix << ", comment: #{comment.inspect}"
178
+ end
179
+ # Determine if this column is used as part of a foreign key
180
+ if fk = fkey_cols.find { |assoc| col == assoc[:fk] }
181
+ to_table = fk[:inverse_table].split('.')
182
+ to_table = to_table.length == 1 ? ":#{to_table.first}" : "'#{fk[:inverse_table]}'"
183
+ if fk[:fk] != "#{fk[:assoc_name].singularize}_id" # Need to do our own foreign_key tricks, not use references?
184
+ column = fk[:fk]
185
+ mig << " t.#{sql_type} :#{column}#{suffix}\n"
186
+ add_fks << [to_table, column, ::Brick.relations[fk[:inverse_table]]]
187
+ else
188
+ suffix << ", type: :#{sql_type}" unless sql_type == key_type
189
+ # Will the resulting default index name be longer than what Postgres allows? (63 characters)
190
+ if (idx_name = ActiveRecord::Base.connection.index_name(tbl, {column: col})).length > 63
191
+ # Try to find a shorter name that hasn't been used yet
192
+ unless indexes.key?(shorter = idx_name[0..62]) ||
193
+ indexes.key?(shorter = idx_name.tr('_', '')[0..62]) ||
194
+ indexes.key?(shorter = idx_name.tr('aeio', '')[0..62])
195
+ puts "Unable to easily find unique name for index #{idx_name} that is shorter than 64 characters,"
196
+ puts "so have resorted to this GUID-based identifier: #{shorter = "#{tbl[0..25]}_#{::SecureRandom.uuid}"}."
197
+ end
198
+ suffix << ", index: { name: '#{shorter || idx_name}' }"
199
+ indexes[shorter || idx_name] = nil
200
+ end
201
+ mig << " t.references :#{fk[:assoc_name]}#{suffix}, foreign_key: { to_table: #{to_table} }\n"
202
+ end
203
+ else
204
+ mig << emit_column(sql_type, col, suffix)
205
+ end
206
+ end
207
+ if possible_ts.length == 2 && # Both created_at and updated_at
208
+ # Rails 5 and later timestamps default to NOT NULL
209
+ (possible_ts.first.last == is_4x_rails && possible_ts.last.last == is_4x_rails)
210
+ mig << "\n t.timestamps\n"
211
+ else # Just one or the other, or a nullability mismatch
212
+ possible_ts.each { |ts| emit_column('timestamp', ts.first, nil) }
213
+ end
214
+ mig << " end\n"
215
+ add_fks.each do |add_fk|
216
+ is_commented = false
217
+ # add_fk[2] holds the inverse relation
218
+ unless (pk = add_fk[2][:pkey].values.flatten&.first)
219
+ is_commented = true
220
+ mig << " # (Unable to create relationship because primary key is missing on table #{add_fk[0]})\n"
221
+ # No official PK, but if coincidentally there's a column of the same name, take a chance on it
222
+ pk = (add_fk[2][:cols].key?(add_fk[1]) && add_fk[1]) || '???'
223
+ end
224
+ # to_table column
225
+ mig << " #{'# ' if is_commented}add_foreign_key #{tbl_code}, #{add_fk[0]}, column: :#{add_fk[1]}, primary_key: :#{pk}\n"
226
+ end
227
+ mig << " end\nend\n"
228
+ current_mig_time += 1.minute
229
+ versions << (version = current_mig_time.strftime('%Y%m%d%H%M00'))
230
+ File.open("#{mig_path}/#{version}_create_#{full_table_name}.rb", "w") { |f| f.write mig }
231
+ end
232
+ done.concat(fringe)
233
+ chosen -= done
234
+ end
235
+ stuck_counts = Hash.new { |h, k| h[k] = 0 }
236
+ chosen.each do |leftover|
237
+ puts "Can't do #{leftover} because:\n #{stuck[leftover].map do |snag|
238
+ stuck_counts[snag.last[:inverse_table]] += 1
239
+ snag.last[:assoc_name]
240
+ end.join(', ')}"
241
+ end
242
+ if mig_path.start_with?(cur_path = ::Rails.root.to_s)
243
+ pretty_mig_path = mig_path[cur_path.length..-1]
244
+ end
245
+ puts "\n*** Created #{done.length} migration files under #{pretty_mig_path || mig_path} ***"
246
+ if (stuck_sorted = stuck_counts.to_a.sort { |a, b| b.last <=> a.last }).length.positive?
247
+ puts "-----------------------------------------"
248
+ puts "Unable to create migrations for #{stuck_sorted.length} tables#{
249
+ ". Here's the top 5 blockers" if stuck_sorted.length > 5
250
+ }:"
251
+ pp stuck_sorted[0..4]
252
+ else # Successful, and now we can update the schema_migrations table accordingly
253
+ ActiveRecord::Base.execute_sql("INSERT INTO #{ActiveRecord::Base.schema_migrations_table_name} (version) VALUES #{
254
+ versions.map { |version| "('#{version}')" }.join(', ')
255
+ }")
256
+ end
257
+ end
258
+
259
+ private
260
+
261
+ def emit_column(type, name, suffix)
262
+ " t.#{type.start_with?('numeric') ? 'decimal' : type} :#{name}#{suffix}\n"
263
+ end
264
+ end
265
+ end
@@ -59,7 +59,6 @@ module Brick
59
59
  end
60
60
  end
61
61
  models.each do |m| # Find longest name in the list for future use to show lists on the right side of the screen
62
- # Strangely this can't be inlined since it assigns to "len"
63
62
  if longest_length < (len = m.name.length)
64
63
  longest_length = len
65
64
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brick
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.53
4
+ version: 1.0.56
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-07 00:00:00.000000000 Z
11
+ date: 2022-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -238,6 +238,7 @@ files:
238
238
  - lib/brick/version_number.rb
239
239
  - lib/generators/brick/USAGE
240
240
  - lib/generators/brick/install_generator.rb
241
+ - lib/generators/brick/migrations_generator.rb
241
242
  - lib/generators/brick/model_generator.rb
242
243
  - lib/generators/brick/templates/add_object_changes_to_versions.rb.erb
243
244
  - lib/generators/brick/templates/create_versions.rb.erb