brick 1.0.54 → 1.0.57

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: '09f9f2f9f7e21a96d29cdad815c069e52280ea068051f27a542541bcefd33e87'
4
- data.tar.gz: 54e032633f930a43d1bc929d05d241367d9ebbdb8010f588b922d1111077524e
3
+ metadata.gz: 930370b67a72b31db99cc8d50afa80d4c3be0d07e727155eb20ac5a7628bb1bb
4
+ data.tar.gz: f2d7964c79172a438800b3c69c63e62b26a4984357ca5d6c7ff9e2ef72e5308f
5
5
  SHA512:
6
- metadata.gz: 56cad221c2b20ac62839c1b7904c8acb9bc392b0779946bbe325c48c8ee5cf00b33413e070674343809ee851804e5f9bbec2d6469a148688c4aecbbcaee3e09d
7
- data.tar.gz: 9757effb2a1d058ae4ccd259b59e0218e002928d90dadfd6ff3d0acd05fb12e27cfcfef30afa3964637e0e4f1e4246b410deba45fb01e452e20056df94820377
6
+ metadata.gz: 03e7ba018c426517060aa6312d14a9f1e0877d82b376782ea46b4b1506433bbe3e9e4d2c3d88aaf3d5fb356c68a129fd975b1718d7b2ac826b241efab3f2cee6
7
+ data.tar.gz: 645c6b750dd870180e312ffddff1d199bb84e92cdde0309f253ec8c927f3ba5304b84ae95b066c260d0523048c607b28148d1b577a479ec661b94de05248813a
@@ -1316,16 +1316,21 @@ module ActiveRecord::ConnectionHandling
1316
1316
  # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
1317
1317
 
1318
1318
  is_postgres = nil
1319
- schema_sql = 'SELECT NULL AS table_schema;'
1320
1319
  case ActiveRecord::Base.connection.adapter_name
1321
1320
  when 'PostgreSQL'
1322
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
1323
1328
  if (is_multitenant = (multitenancy = ::Brick.config.schema_behavior[:multitenant]) &&
1324
- (sta = multitenancy[:schema_to_analyse]) != 'public')
1329
+ (sta = multitenancy[:schema_to_analyse]) != 'public') &&
1330
+ ::Brick.db_schemas.include?(sta)
1325
1331
  ::Brick.default_schema = schema = sta
1326
1332
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
1327
1333
  end
1328
- schema_sql = 'SELECT DISTINCT table_schema FROM INFORMATION_SCHEMA.tables;'
1329
1334
  when 'Mysql2'
1330
1335
  ::Brick.default_schema = schema = ActiveRecord::Base.connection.current_database
1331
1336
  when 'SQLite'
@@ -1342,16 +1347,7 @@ module ActiveRecord::ConnectionHandling
1342
1347
  puts "Unfamiliar with connection adapter #{ActiveRecord::Base.connection.adapter_name}"
1343
1348
  end
1344
1349
 
1345
- unless (db_schemas = ActiveRecord::Base.execute_sql(schema_sql)).is_a?(Array)
1346
- db_schemas = db_schemas.to_a
1347
- end
1348
- unless db_schemas.empty?
1349
- ::Brick.db_schemas = db_schemas.each_with_object({}) do |row, s|
1350
- row = row.is_a?(String) ? row : row['table_schema']
1351
- # Remove any system schemas
1352
- s[row] = nil unless ['information_schema', 'pg_catalog'].include?(row)
1353
- end
1354
- end
1350
+ ::Brick.db_schemas ||= []
1355
1351
 
1356
1352
  if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
1357
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 = 54
8
+ TINY = 57
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
@@ -5,27 +5,27 @@ require 'rails/generators/active_record'
5
5
  require 'fancy_gets'
6
6
 
7
7
  module Brick
8
- # Auto-generates migrations
8
+ # Auto-generates migration files
9
9
  class MigrationsGenerator < ::Rails::Generators::Base
10
10
  include FancyGets
11
11
  # include ::Rails::Generators::Migration
12
12
 
13
- # SQL types that are the same as their migration data type name: text, integer, bigint, smallint, date, boolean, decimal, float
14
- SQL_TYPES = { 'character varying' =>'string',
15
- 'timestamp without time zone' =>'timestamp',
16
- 'timestamp with time zone' =>'timestamp',
17
- 'double precision' =>'float' } # might work with 'double'
13
+ # Many SQL types are the same as their migration data type name:
14
+ # text, integer, bigint, date, boolean, decimal, float
15
+ # These however are not:
16
+ SQL_TYPES = { 'character varying' => 'string',
17
+ 'character' => 'string', # %%% Need to put in "limit: 1"
18
+ 'xml' => 'text',
19
+ 'bytea' => 'binary',
20
+ 'timestamp without time zone' => 'timestamp',
21
+ 'timestamp with time zone' => 'timestamp',
22
+ 'time without time zone' => 'time',
23
+ 'time with time zone' => 'time',
24
+ 'double precision' => 'float', # might work with 'double'
25
+ 'smallint' => 'integer' } # %%% Need to put in "limit: 2"
18
26
  # (Still need to find what "inet" and "json" data types map to.)
19
27
 
20
- # # source_root File.expand_path('templates', __dir__)
21
- # class_option(
22
- # :with_changes,
23
- # type: :boolean,
24
- # default: false,
25
- # desc: 'Add IMPORT_TEMPLATE to model'
26
- # )
27
-
28
- desc 'Auto-generates migrations for an existing database.'
28
+ desc 'Auto-generates migration files for an existing database.'
29
29
 
30
30
  def brick_migrations
31
31
  # If Apartment is active, see if a default schema to analyse is indicated
@@ -38,27 +38,33 @@ module Brick
38
38
  return
39
39
  end
40
40
 
41
- ar_version = unless ActiveRecord.version < ::Gem::Version.new('5.0')
42
- arv = ActiveRecord.version.segments[0..1]
43
- arv.pop if arv&.last == 0
44
- "[#{arv.join('.')}]"
45
- end
46
- default_mig_path = (mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{File.expand_path(__dir__)}/db/migrate")
47
- if Dir.exist?(mig_path)
41
+ key_type = (ActiveRecord.version < ::Gem::Version.new('5.1') ? 'integer' : 'bigint')
42
+ is_4x_rails = ActiveRecord.version < ::Gem::Version.new('5.0')
43
+ ar_version = "[#{ActiveRecord.version.segments[0..1].join('.')}]" unless is_4x_rails
44
+ is_insert_versions = true
45
+ is_delete_versions = false
46
+ versions_to_delete_or_append = nil
47
+ if Dir.exist?(mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{::Rails.root}/db/migrate")
48
48
  if Dir["#{mig_path}/**/*.rb"].present?
49
49
  puts "WARNING: migrations folder #{mig_path} appears to already have ruby files present."
50
- mig_path2 = "#{File.expand_path(__dir__)}/tmp/brick_migrations"
50
+ mig_path2 = "#{::Rails.root}/tmp/brick_migrations"
51
+ is_insert_versions = false unless mig_path == mig_path2
51
52
  if Dir.exist?(mig_path2)
52
53
  if Dir["#{mig_path2}/**/*.rb"].present?
53
54
  puts "As well, temporary folder #{mig_path2} also has ruby files present."
54
55
  puts "Choose a destination -- all existing .rb files will be removed:"
55
- mig_path2 = gets_list(list: ['Cancel operation!', "Add all migration files to #{mig_path} anyway", mig_path, mig_path2])
56
+ mig_path2 = gets_list(list: ['Cancel operation!', "Append migration files into #{mig_path} anyway", mig_path, mig_path2])
56
57
  return if mig_path2.start_with?('Cancel')
57
58
 
58
- if mig_path2.start_with?('Add all migration files to ')
59
+ existing_mig_files = Dir["#{mig_path2}/**/*.rb"]
60
+ if (is_insert_versions = mig_path == mig_path2)
61
+ versions_to_delete_or_append = existing_mig_files.map { |ver| ver.split('/').last.split('_').first }
62
+ end
63
+ if mig_path2.start_with?('Append migration files into ')
59
64
  mig_path2 = mig_path
60
65
  else
61
- Dir["#{mig_path2}/**/*.rb"].each { |rb| File.delete(rb) }
66
+ is_delete_versions = true
67
+ existing_mig_files.each { |rb| File.delete(rb) }
62
68
  end
63
69
  else
64
70
  puts "Using temporary folder #{mig_path2} for created migration files.\n\n"
@@ -78,57 +84,160 @@ module Brick
78
84
 
79
85
  # Generate a list of tables that can be chosen
80
86
  chosen = gets_list(list: tables, chosen: tables.dup)
81
- # Work from now back the same number of minutes as expected migrations to create
87
+ # Start the timestamps back the same number of minutes from now as expected number of migrations to create
82
88
  current_mig_time = Time.now - chosen.length.minutes
83
89
  done = []
84
90
  fks = {}
85
91
  stuck = {}
92
+ indexes = {} # Track index names to make sure things are unique
93
+ built_schemas = {} # Track all built schemas so we can place an appropriate drop_schema command only in the first
94
+ # migration in which that schema is referenced, thereby allowing rollbacks to function properly.
95
+ versions_to_create = [] # Resulting versions to be used when updating the schema_migrations table
86
96
  # Start by making migrations for fringe tables (those with no foreign keys).
87
97
  # Continue layer by layer, creating migrations for tables that reference ones already done, until
88
98
  # no more migrations can be created. (At that point hopefully all tables are accounted for.)
89
99
  while (fringe = chosen.reject do |tbl|
90
- snags = ::Brick.relations[tbl][:fks].select do |_k, v|
100
+ snags = ::Brick.relations.fetch(tbl, nil)&.fetch(:fks, nil)&.select do |_k, v|
91
101
  v[:is_bt] && !v[:polymorphic] &&
92
102
  tbl != v[:inverse_table] && # Ignore self-referencing associations (stuff like "parent_id")
93
103
  !done.include?(v[:inverse_table])
94
104
  end
95
- stuck[tbl] = snags if snags.present?
105
+ stuck[tbl] = snags if snags&.present?
96
106
  end).present?
97
107
  fringe.each do |tbl|
98
- tbl = tbl[7..-1] if tbl.start_with?('public.') # %%% Provide better multitenancy support later
99
- pkey_cols = ::Brick.relations[chosen.first][:pkey].values.flatten
108
+ next unless (relation = ::Brick.relations.fetch(tbl, nil))&.fetch(:cols, nil)&.present?
109
+
110
+ pkey_cols = (rpk = relation[:pkey].values.flatten) & (arpk = [ActiveRecord::Base.primary_key].flatten)
111
+ # In case things aren't as standard
112
+ if pkey_cols.empty?
113
+ pkey_cols = if rpk.empty? && relation[:cols][arpk.first]&.first == key_type
114
+ arpk
115
+ elsif rpk.first
116
+ rpk
117
+ end
118
+ end
119
+ schema = if (tbl_parts = tbl.split('.')).length > 1
120
+ if tbl_parts.first == (::Brick.default_schema || 'public')
121
+ tbl_parts.shift
122
+ nil
123
+ else
124
+ tbl_parts.first
125
+ end
126
+ end
100
127
  # %%% For the moment we're skipping polymorphics
101
- fkey_cols = ::Brick.relations[tbl][:fks].values.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
102
- mig = +"class Create#{table_name = tbl.camelize} < ActiveRecord::Migration#{ar_version}\n"
103
- # %%% Support missing foreign key (by adding: ,id: false)
104
- # also bigint / uuid / other non-standard data type for primary key
105
- mig << " def change\n create_table :#{tbl} do |t|\n"
128
+ fkey_cols = relation[:fks].values.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
129
+ mig = +"class Create#{(full_table_name = tbl_parts.join('_')).camelize} < ActiveRecord::Migration#{ar_version}\n"
130
+ # If the primary key is also used as a foreign key, will need to do id: false and then build out
131
+ # a column definition which includes :primary_key -- %%% also using a data type of bigserial or serial
132
+ # if this one has come in as bigint or integer.
133
+ pk_is_also_fk = fkey_cols.any? { |assoc| pkey_cols&.first == assoc[:fk] } ? pkey_cols&.first : nil
134
+ # Support missing primary key (by adding: ,id: false)
135
+ id_option = if pk_is_also_fk || (pkey_col_first = relation[:cols][pkey_cols&.first]&.first) != key_type
136
+ if pk_is_also_fk || !pkey_cols&.present?
137
+ ', id: false'
138
+ else
139
+ case pkey_col_first
140
+ when 'integer'
141
+ ', id: :serial'
142
+ when 'bigint'
143
+ ', id: :bigserial'
144
+ else
145
+ ", id: :#{SQL_TYPES[pkey_col_first] || pkey_col_first}" # Something like: id: :integer, primary_key: :businessentityid
146
+ end +
147
+ (pkey_cols.first ? ", primary_key: :#{pkey_cols.first}" : '') +
148
+ (!is_4x_rails && (comment = relation&.fetch(:description, nil))&.present? ? ", comment: #{comment.inspect}" : '')
149
+ end
150
+ end
151
+ # Find the ActiveRecord class in order to see if the columns have comments
152
+ unless is_4x_rails
153
+ klass = begin
154
+ tbl.tr('.', '/').singularize.camelize.constantize
155
+ rescue StandardError
156
+ end
157
+ if klass
158
+ unless ActiveRecord::Migration.table_exists?(klass.table_name)
159
+ puts "WARNING: Unable to locate table #{klass.table_name} (for #{klass.name})."
160
+ klass = nil
161
+ end
162
+ end
163
+ end
164
+ # Refer to this table name as a symbol or dotted string as appropriate
165
+ tbl_code = tbl_parts.length == 1 ? ":#{tbl_parts.first}" : "'#{tbl}'"
166
+ mig << " def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
167
+ mig << " create_schema :#{schema} unless reverting? || schema_exists?(:#{schema})\n" if schema
168
+ mig << " create_table #{tbl_code}#{id_option} do |t|\n"
106
169
  possible_ts = [] # Track possible generic timestamps
107
- (relation = ::Brick.relations[tbl])[:cols].each do |col, col_type|
108
- next if pkey_cols.include?(col)
109
-
110
- # See if there are generic timestamps
111
- if (sql_type = SQL_TYPES[col_type.first]) == 'timestamp' && ['created_at','updated_at'].include?(col)
112
- possible_ts << col
113
- next
170
+ add_fks = [] # Track foreign keys to add after table creation
171
+ relation[:cols].each do |col, col_type|
172
+ sql_type = SQL_TYPES[col_type.first] || col_type.first
173
+ suffix = col_type[3] ? +', null: false' : +''
174
+ if !is_4x_rails && klass && (comment = klass.columns_hash.fetch(col, nil)&.comment)&.present?
175
+ suffix << ", comment: #{comment.inspect}"
114
176
  end
115
-
116
- sql_type ||= col_type.first
117
177
  # Determine if this column is used as part of a foreign key
118
178
  if fk = fkey_cols.find { |assoc| col == assoc[:fk] }
119
- mig << " t.references :#{fk[:assoc_name]}#{", type: #{sql_type}" unless sql_type == 'integer'}\n"
179
+ to_table = fk[:inverse_table].split('.')
180
+ to_table = to_table.length == 1 ? ":#{to_table.first}" : "'#{fk[:inverse_table]}'"
181
+ if fk[:fk] != "#{fk[:assoc_name].singularize}_id" # Need to do our own foreign_key tricks, not use references?
182
+ column = fk[:fk]
183
+ mig << " t.#{sql_type} :#{column}#{suffix}\n"
184
+ add_fks << [to_table, column, ::Brick.relations[fk[:inverse_table]]]
185
+ else
186
+ suffix << ", type: :#{sql_type}" unless sql_type == key_type
187
+ # Will the resulting default index name be longer than what Postgres allows? (63 characters)
188
+ if (idx_name = ActiveRecord::Base.connection.index_name(tbl, {column: col})).length > 63
189
+ # Try to find a shorter name that hasn't been used yet
190
+ unless indexes.key?(shorter = idx_name[0..62]) ||
191
+ indexes.key?(shorter = idx_name.tr('_', '')[0..62]) ||
192
+ indexes.key?(shorter = idx_name.tr('aeio', '')[0..62])
193
+ puts "Unable to easily find unique name for index #{idx_name} that is shorter than 64 characters,"
194
+ puts "so have resorted to this GUID-based identifier: #{shorter = "#{tbl[0..25]}_#{::SecureRandom.uuid}"}."
195
+ end
196
+ suffix << ", index: { name: '#{shorter || idx_name}' }"
197
+ indexes[shorter || idx_name] = nil
198
+ end
199
+ mig << " t.references :#{fk[:assoc_name]}#{suffix}, foreign_key: { to_table: #{to_table} }\n"
200
+ end
120
201
  else
121
- mig << emit_column(sql_type, col)
202
+ next if !id_option&.end_with?('id: false') && pkey_cols.include?(col)
203
+
204
+ # See if there are generic timestamps
205
+ if sql_type == 'timestamp' && ['created_at','updated_at'].include?(col)
206
+ possible_ts << [col, !col_type[3]]
207
+ else
208
+ mig << emit_column(sql_type, col, suffix)
209
+ end
122
210
  end
123
211
  end
124
- if possible_ts.length == 2 # Both created_at and updated_at
212
+ if possible_ts.length == 2 && # Both created_at and updated_at
213
+ # Rails 5 and later timestamps default to NOT NULL
214
+ (possible_ts.first.last == is_4x_rails && possible_ts.last.last == is_4x_rails)
125
215
  mig << "\n t.timestamps\n"
126
- else # Just one or the other
127
- possible_ts.each { |ts| emit_column('timestamp', ts) }
216
+ else # Just one or the other, or a nullability mismatch
217
+ possible_ts.each { |ts| emit_column('timestamp', ts.first, nil) }
218
+ end
219
+ mig << " end\n"
220
+ mig << " execute('ALTER TABLE #{tbl} ADD PRIMARY KEY (#{pk_is_also_fk});')\n" if pk_is_also_fk
221
+ add_fks.each do |add_fk|
222
+ is_commented = false
223
+ # add_fk[2] holds the inverse relation
224
+ unless (pk = add_fk[2][:pkey].values.flatten&.first)
225
+ is_commented = true
226
+ mig << " # (Unable to create relationship because primary key is missing on table #{add_fk[0]})\n"
227
+ # No official PK, but if coincidentally there's a column of the same name, take a chance on it
228
+ pk = (add_fk[2][:cols].key?(add_fk[1]) && add_fk[1]) || '???'
229
+ end
230
+ # to_table column
231
+ mig << " #{'# ' if is_commented}add_foreign_key #{tbl_code}, #{add_fk[0]}, column: :#{add_fk[1]}, primary_key: :#{pk}\n"
232
+ end
233
+ unless built_schemas.key?(schema)
234
+ mig << " drop_schema :#{schema} if reverting? && schema_exists?(:#{schema})\n"
235
+ built_schemas[schema] = nil
128
236
  end
129
- mig << " end\n end\nend\n"
237
+ mig << " end\nend\n"
130
238
  current_mig_time += 1.minute
131
- File.open("#{mig_path}/#{current_mig_time.strftime('%Y%m%d%H%M00')}_create_#{tbl}.rb", "w") { |f| f.write mig }
239
+ versions_to_create << (version = current_mig_time.strftime('%Y%m%d%H%M00')).split('_').first
240
+ File.open("#{mig_path}/#{version}_create_#{full_table_name}.rb", "w") { |f| f.write mig }
132
241
  end
133
242
  done.concat(fringe)
134
243
  chosen -= done
@@ -140,21 +249,41 @@ module Brick
140
249
  snag.last[:assoc_name]
141
250
  end.join(', ')}"
142
251
  end
143
- pretty_mig_path = mig_path[cur_path.length] if mig_path.start_with?(cur_path = File.expand_path(__dir__))
144
- puts "*** Created #{done.length} migration files under #{pretty_mig_path || mig_path} ***"
252
+ if mig_path.start_with?(cur_path = ::Rails.root.to_s)
253
+ pretty_mig_path = mig_path[cur_path.length..-1]
254
+ end
255
+ puts "\n*** Created #{done.length} migration files under #{pretty_mig_path || mig_path} ***"
145
256
  if (stuck_sorted = stuck_counts.to_a.sort { |a, b| b.last <=> a.last }).length.positive?
146
257
  puts "-----------------------------------------"
147
258
  puts "Unable to create migrations for #{stuck_sorted.length} tables#{
148
259
  ". Here's the top 5 blockers" if stuck_sorted.length > 5
149
260
  }:"
150
261
  pp stuck_sorted[0..4]
262
+ else # Successful, and now we can update the schema_migrations table accordingly
263
+ unless ActiveRecord::Migration.table_exists?(ActiveRecord::Base.schema_migrations_table_name)
264
+ ActiveRecord::SchemaMigration.create_table
265
+ end
266
+ # Remove to_delete - to_create
267
+ if ((versions_to_delete_or_append ||= []) - versions_to_create).present? && is_delete_versions
268
+ ActiveRecord::Base.execute_sql("DELETE FROM #{
269
+ ActiveRecord::Base.schema_migrations_table_name} WHERE version IN (#{
270
+ (versions_to_delete_or_append - versions_to_create).map { |vtd| "'#{vtd}'" }.join(', ')}
271
+ )")
272
+ end
273
+ # Add to_create - to_delete
274
+ if is_insert_versions && ((versions_to_create ||= []) - versions_to_delete_or_append).present?
275
+ ActiveRecord::Base.execute_sql("INSERT INTO #{
276
+ ActiveRecord::Base.schema_migrations_table_name} (version) VALUES #{
277
+ (versions_to_create - versions_to_delete_or_append).map { |vtc| "('#{vtc}')" }.join(', ')
278
+ }")
279
+ end
151
280
  end
152
281
  end
153
282
 
154
283
  private
155
284
 
156
- def emit_column(type, name)
157
- " t.#{type.start_with?('numeric') ? 'decimal' : type} :#{name}\n"
285
+ def emit_column(type, name, suffix)
286
+ " t.#{type.start_with?('numeric') ? 'decimal' : type} :#{name}#{suffix}\n"
158
287
  end
159
288
  end
160
289
  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.54
4
+ version: 1.0.57
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-08 00:00:00.000000000 Z
11
+ date: 2022-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord