brick 1.0.246 → 1.0.248

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.
@@ -16,6 +16,17 @@ module Brick
16
16
  'double precision' => 'float',
17
17
  'smallint' => 'integer', # %%% Need to put in "limit: 2"
18
18
  'ARRAY' => 'string', # Note that we'll also add ", array: true"
19
+ # MySQL data types
20
+ 'tinytext' => 'text', # 255 bytes
21
+ # 'text' => 'text', # 64kb
22
+ 'mediumtext' => 'text', # 16mb
23
+ 'longtext' => 'text', # 4gb
24
+ 'tinyblob' => 'binary',
25
+ 'blob' => 'binary',
26
+ 'mediumblob' => 'binary',
27
+ 'longblob' => 'binary',
28
+ 'enum' => 'string',
29
+ 'set' => 'string', # Note that we'll also add ", array: true"
19
30
  # Oracle data types
20
31
  'VARCHAR2' => 'string',
21
32
  'CHAR' => 'string',
@@ -119,7 +130,8 @@ module Brick
119
130
  stuck = {}
120
131
  indexes = {} # Track index names to make sure things are unique
121
132
  built_schemas = {} # Track all built schemas so we can place an appropriate drop_schema command only in the first
122
- # migration in which that schema is referenced, thereby allowing rollbacks to function properly.
133
+ # migration in which that schema is referenced, thereby allowing rollbacks to function properly.
134
+ is_airtable = nil
123
135
  versions_to_create = [] # Resulting versions to be used when updating the schema_migrations table
124
136
  # Start by making migrations for fringe tables (those with no foreign keys).
125
137
  # Continue layer by layer, creating migrations for tables that reference ones already done, until
@@ -148,8 +160,9 @@ module Brick
148
160
  mig = gen_migration_columns(relations, tbl, (tbl_parts = tbl.split('.')), (add_fks = []), built_schemas, mig_path, current_mig_time,
149
161
  key_type, is_4x_rails, ar_version, do_fks_last, versions_to_create)
150
162
  after_fks.concat(add_fks) if do_fks_last
151
- current_mig_time[0] += 1.minute
152
- versions_to_create << migration_file_write(mig_path, "create_#{::Brick._brick_index(tbl, nil, separator, relations[tbl])}", current_mig_time, ar_version, mig)
163
+ increment_time(mig_path, current_mig_time)
164
+ is_airtable ||= (relation = relations[tbl])&.fetch(:airtable_table, nil)
165
+ versions_to_create << migration_file_write(mig_path, "create_#{::Brick._brick_index(tbl, nil, separator, relation, true)}", current_mig_time, ar_version, mig)
153
166
  end
154
167
  done.concat(fringe)
155
168
  chosen -= done
@@ -161,15 +174,15 @@ module Brick
161
174
  mig = gen_migration_columns(relations, tbl, (tbl_parts = tbl.split('.')), (add_fks = []), built_schemas, mig_path, current_mig_time,
162
175
  key_type, is_4x_rails, ar_version, do_fks_last, versions_to_create)
163
176
  after_fks.concat(add_fks)
164
- current_mig_time[0] += 1.minute
177
+ increment_time(mig_path, current_mig_time)
165
178
  versions_to_create << migration_file_write(mig_path, "create_#{
166
- ::Brick._brick_index(tbl, :migration, separator, relations[tbl])
179
+ ::Brick._brick_index(tbl, :migration, separator, relations[tbl], true)
167
180
  }", current_mig_time, ar_version, mig)
168
181
  end
169
182
  done.concat(chosen)
170
183
  chosen.clear
171
184
 
172
- case do_fks_last
185
+ case after_fks.present? && do_fks_last
173
186
  when 'Separate' # Add a final migration to create all the foreign keys
174
187
  mig = +" def change\n"
175
188
  after_fks.each do |add_fk|
@@ -187,7 +200,7 @@ module Brick
187
200
  # disable the final FK migration by using a ".rbx" file extension.
188
201
  fks_extension = if after_fks.length > 500
189
202
  minutes = (after_fks.length + 1000) / 1500
190
- mig << " if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'\n"
203
+ mig << " if ['PostgreSQL', 'PostGIS'].include?(ActiveRecord::Base.connection.adapter_name)\n"
191
204
  mig << " puts 'NOTE: It could take around #{minutes} #{'minute'.pluralize(minutes)} on a FAST machine for Postgres to do all the final processing for these foreign keys. Please be patient!'\n"
192
205
 
193
206
  mig << " # Vacuum takes only about ten seconds when all the tables are empty,
@@ -201,9 +214,10 @@ module Brick
201
214
  'rb'
202
215
  end
203
216
  mig << +" end\n"
204
- current_mig_time[0] += 1.minute
205
- versions_to_create << migration_file_write(mig_path, "create_brick_fks.#{fks_extension}", current_mig_time, ar_version, mig)
206
- puts "Have written out a final migration called 'create_brick_fks.#{fks_extension}' which creates #{after_fks.length} foreign keys."
217
+ increment_time(mig_path, current_mig_time)
218
+ base_name = "_#{relations[:base_name].tr(' ', '').underscore}" if relations.key?(:base_name)
219
+ versions_to_create << migration_file_write(mig_path, "create_brick_fks#{base_name}.#{fks_extension}", current_mig_time, ar_version, mig)
220
+ puts "Have written out a final migration called 'create_brick_fks#{base_name}.#{fks_extension}' which creates #{after_fks.length} foreign keys."
207
221
  if fks_extension == 'rbx'
208
222
  puts " This file extension (.rbx) will cause it not to run yet when you do a 'rails db:migrate'.
209
223
  The idea here is to do all data loading first, and then rename that migration file back
@@ -247,7 +261,7 @@ module Brick
247
261
  ". Here's the top 5 blockers" if stuck_sorted.length > 5
248
262
  }:"
249
263
  pp stuck_sorted[0..4]
250
- elsif do_schema_migrations # Successful, and now we can update the schema_migrations table accordingly
264
+ elsif do_schema_migrations && !is_airtable # Successful, and now we can update the schema_migrations table accordingly
251
265
  smtn = ::Brick._schema_migrations_table_name
252
266
  if ActiveRecord::Migration.table_exists?(smtn)
253
267
  # Remove to_delete - to_create
@@ -298,7 +312,7 @@ module Brick
298
312
  end
299
313
  unless schema.blank? || built_schemas.key?(schema)
300
314
  mig = +" def change\n create_schema(:#{schema}) unless schema_exists?(:#{schema})\n end\n"
301
- current_mig_time[0] += 1.minute
315
+ increment_time(mig_path, current_mig_time)
302
316
  versions_to_create << migration_file_write(mig_path, "create_db_schema_#{schema.underscore}", current_mig_time, ar_version, mig)
303
317
  built_schemas[schema] = nil
304
318
  end
@@ -352,12 +366,13 @@ module Brick
352
366
  mig = +" def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
353
367
  mig << " create_table #{tbl_code}#{id_option} do |t|\n"
354
368
  possible_ts = [] # Track possible generic timestamps
355
- relation[:cols].each do |col, col_type|
369
+ (relation.key?(:col_order) && relation[:col_order].present? && relation[:col_order].map { |col_name| [col_name, relation[:cols][col_name]] } ||
370
+ relation[:cols]).each do |col, col_type|
356
371
  sql_type = SQL_TYPES[col_type.first] || SQL_TYPES[col_type[0..1]] ||
357
372
  SQL_TYPES.find { |r| r.first.is_a?(Regexp) && col_type.first =~ r.first }&.last ||
358
373
  col_type.first
359
374
  suffix = col_type[3] || pkey_cols&.include?(col) ? +', null: false' : +''
360
- suffix << ', array: true' if (col_type.first == 'ARRAY')
375
+ suffix << ', array: true' if (['ARRAY', 'set'].include?(col_type.first))
361
376
  if !is_4x_rails && klass && (comment = klass.columns_hash.fetch(col, nil)&.comment)&.present?
362
377
  suffix << ", comment: #{comment.inspect}"
363
378
  end
@@ -424,7 +439,7 @@ module Brick
424
439
  mig << " end\n"
425
440
  if pk_is_also_fk
426
441
  mig << " reversible do |dir|\n"
427
- mig << " dir.up { execute('ALTER TABLE #{tbl} ADD PRIMARY KEY (#{pk_is_also_fk})') }\n"
442
+ mig << " dir.up { execute('ALTER TABLE #{tbl} ADD PRIMARY KEY (#{pkey_cols.map { |pk| "\"#{pk}\"" }.join(', ')})') }\n"
428
443
  mig << " end\n"
429
444
  end
430
445
  add_fks.each do |add_fk|
@@ -449,7 +464,16 @@ module Brick
449
464
  " t.#{type.start_with?('numeric') ? 'decimal' : type} :#{name}#{suffix}\n"
450
465
  end
451
466
 
467
+ def increment_time(mig_path, current_mig_time)
468
+ loop do
469
+ current_mig_time[0] += 1.minute
470
+ break if Dir.glob("#{mig_path}/#{current_mig_time.first.strftime('%Y%m%d%H%M00')}_*.*").empty?
471
+
472
+ end
473
+ end
474
+
452
475
  def migration_file_write(mig_path, name, current_mig_time, ar_version, mig)
476
+ name.gsub!(/[^a-zA-Z0-9_\.\p{L}\p{N}]/, 'x') # Strip out any weird unicode characters
453
477
  File.open("#{mig_path}/#{version = current_mig_time.first.strftime('%Y%m%d%H%M00')}_#{name}#{'.rb' unless name.index('.')}", "w") do |f|
454
478
  f.write "class #{name.split('.').first.camelize} < ActiveRecord::Migration#{ar_version}\n"
455
479
  f.write mig
@@ -13,9 +13,10 @@ module Brick
13
13
  end
14
14
 
15
15
  def generate_seeds(relations = nil)
16
+ action = 'Overwrite'
16
17
  if File.exist?((seed_file_path = "#{::Rails.root}/db/seed") + 's.rb')
17
- puts "WARNING: seeds file #{seed_file_path}s.rb appears to already be present.\nOverwrite?"
18
- return unless gets_list(list: ['No', 'Yes']) == 'Yes'
18
+ puts "WARNING: seeds file #{seed_file_path}s.rb appears to already be present.\nAppend or Overwrite?"
19
+ return if (action = gets_list(list: ['Cancel', 'Append', 'Overwrite'])) == 'Cancel'
19
20
 
20
21
  puts "\n"
21
22
  end
@@ -24,7 +25,7 @@ module Brick
24
25
  is_airtable = true # So far the only thing that feeds us relations is Airtable
25
26
  require 'generators/brick/airtable_api_caller'
26
27
  # include ::Brick::MigrationsBuilder
27
- chosen = relations.map { |k, v| SeedModel.new(k, nil, false, v[:airtable_table]) }
28
+ chosen = relations.each_with_object([]) { |v, s| s << SeedModel.new(v.first, nil, false, v.last[:airtable_table]) unless v.first.is_a?(Symbol) }
28
29
  else
29
30
  ::Brick.mode = :on
30
31
  ActiveRecord::Base.establish_connection
@@ -57,11 +58,14 @@ module Brick
57
58
  end
58
59
  end
59
60
  batch_num = 1
60
- seeds_file = File.open("#{seed_file_path}s.rb", 'w')
61
+ seeds_file = File.open("#{seed_file_path}s.rb", action == 'Overwrite' ? 'w' : 'a+')
62
+ seeds_file.write("\n") if action == 'Append'
61
63
  seeds_len = lines_len = 0
62
64
  seeds_file.write('# Seeds file for ')
63
65
  seed_rows = nil
64
- if (arbc = ActiveRecord::Base.connection).respond_to?(:current_database) # SQLite3 can't do this!
66
+ if is_airtable
67
+ seeds_file.write("Airtable Base \"#{relations[:base_name]}\":\n")
68
+ elsif (arbc = ActiveRecord::Base.connection).respond_to?(:current_database) # SQLite3 can't do this!
65
69
  seeds_file.write("#{arbc.current_database}:\n")
66
70
  elsif (filename = arbc.instance_variable_get(:@connection_parameters)&.fetch(:database, nil))
67
71
  seeds_file.write("#{filename}:\n")
@@ -146,19 +150,38 @@ module Brick
146
150
 
147
151
  has_rows = false
148
152
  is_empty = true
149
- klass_name = is_airtable ? ::Brick::AirtableApiCaller.sane_table_name(relation[:airtable_table]&.name)&.singularize&.camelize : klass.name
150
- # Pull the records
151
- collection = if is_airtable
152
- if (airtable_table = relation[:airtable_table])
153
- ::Brick::AirtableApiCaller.https_get("https://api.airtable.com/v0/#{airtable_table.base_id}/#{airtable_table.id}").fetch('records', nil)
154
- end
155
- else
156
- # Must convert to a symbol when doing ORDER BY due to this ActiveRecord nuance:
157
- # https://github.com/rails/rails/issues/2601
158
- klass.order(*pkey_cols.map(&:to_sym)) unless relation[:pkey].empty?
159
- end
153
+ collection = nil
154
+ klass_name = if is_airtable
155
+ airtable_table = relation[:airtable_table]
156
+ ::Brick.config.table_name_prefixes&.fetch(tbl, nil) ||
157
+ tbl.singularize.camelize
158
+ else
159
+ # Must convert to a symbol when doing ORDER BY due to this ActiveRecord nuance:
160
+ # https://github.com/rails/rails/issues/2601
161
+ collection = klass.order(*pkey_cols.map(&:to_sym)).enum_for unless relation[:pkey].empty?
162
+ klass.name
163
+ end
160
164
  seed_rows = +''
161
- collection&.each do |obj|
165
+ airtable_idx = offset = nil
166
+ while collection || airtable_table do
167
+ obj = if is_airtable
168
+ if collection.nil? || (airtable_idx += 1) >= collection.length
169
+ break if collection && !offset
170
+
171
+ offset_param = "?offset=#{offset}" if offset
172
+ result = ::Brick::AirtableApiCaller.https_get("https://api.airtable.com/v0/#{airtable_table.base_id}/#{airtable_table.id}#{offset_param}")
173
+ offset = result.fetch('offset', nil)
174
+ collection = result.fetch('records', nil)
175
+ airtable_idx = 0
176
+ end
177
+ collection[airtable_idx]
178
+ else
179
+ begin
180
+ collection.next
181
+ rescue StopIteration
182
+ break
183
+ end
184
+ end
162
185
  if is_airtable
163
186
  fields = obj['fields'].each_with_object({}) do |field, s|
164
187
  if relation[:cols].keys.include?(col_name = ::Brick::AirtableApiCaller.sane_name(field.first))
@@ -178,7 +201,7 @@ module Brick
178
201
  end
179
202
  end
180
203
  end
181
- objects = relation[:airtable_table].objects
204
+ objects = airtable_table.objects
182
205
  obj = objects[airtable_id = obj['id']] = AirtableObject.new(seed_model, obj['fields'], obj['createdTime'])
183
206
  end
184
207
  unless has_rows
@@ -195,16 +218,21 @@ module Brick
195
218
  data = []
196
219
  updates = []
197
220
  relation[:cols].each do |col, _col_type|
198
- # Skip primary key columns, unless they are part of a foreign key.
221
+ # Skip primary key columns, unless they are part of a foreign key or a composite key.
199
222
  # (But always add all columns if it's Airtable!)
200
223
  next if !(fk = fkeys.find { |assoc| col == assoc[:fk] }) &&
201
224
  !is_airtable &&
202
- pkey_cols.include?(col) && orig_pk_val.is_a?(Integer)
225
+ pkey_cols.first == col && orig_pk_val.is_a?(Integer)
203
226
 
204
227
  # Used to be: obj.send(col)
205
228
  # (and with that it was possible to raise ActiveRecord::Encryption::Errors::Configuration...)
206
229
  # %%% should test further and see if that is possible with this code!)
207
- if (val = obj.attributes_before_type_cast[col])
230
+ val = if is_airtable && col == 'airtable_id'
231
+ pk_val
232
+ else
233
+ obj.attributes_before_type_cast[col]
234
+ end
235
+ if val
208
236
  if (val.is_a?(Time) || val.is_a?(Date))
209
237
  val = val.to_s
210
238
  elsif val.is_a?(Numeric)
@@ -283,28 +311,42 @@ end\n"
283
311
  end
284
312
  seed_rows = +''
285
313
  end
286
- unless has_rows || klass_name.nil?
314
+ unless has_rows || (is_airtable && !airtable_table)
287
315
  seeds_file.write(" # (Skipping #{klass_name} as it has no rows)\n")
288
316
  end
289
317
  end
290
318
  done.concat(fringe)
291
319
  chosen -= done
292
320
  end
293
- airtable_assocs_done = {}
294
- airtable_assoc_recids.each do |table_name, assoc_pairs| # N:M links
295
- # Make note of any other tables which have exactly duplicated data
296
- dupes = airtable_assocs_done.select do |dupe_table, dupe_pairs|
297
- dupe_table != table_name && assoc_pairs.length > 0 && assoc_pairs.length == dupe_pairs.length &&
298
- dupe_pairs.all? { |pair_name, _v3| assoc_pairs.keys.include?(pair_name) }
321
+
322
+ if airtable_assoc_recids.present?
323
+ seed_rows << "\n# Seeding #{airtable_assoc_recids.count} sets of Airtable associations:\n"
324
+ airtable_assocs_done = {}
325
+ dupe_list = []
326
+ airtable_assoc_recids.each do |table_name, assoc_pairs| # N:M links
327
+ # Make note of any other tables which have exactly duplicated data
328
+ dupes = airtable_assocs_done.select do |dupe_table, dupe_pairs|
329
+ dupe_table != table_name && assoc_pairs.length > 0 && assoc_pairs.length == dupe_pairs.length &&
330
+ dupe_pairs.all? { |pair_name, _v3| assoc_pairs.keys.include?(pair_name) }
331
+ end
332
+ seed_rows << " if ActiveRecord::Migration.table_exists?('#{table_name}')\n"
333
+ unless dupes.empty?
334
+ seed_rows << " # Duplicate data found in: #{dupes.keys.join(', ')}\n"
335
+ dupes.keys.each { |k| dupe_list << "#{table_name} | #{k}" }
336
+ end
337
+ seed_rows << " puts 'Airtable associations for #{table_name.camelize.singularize}'\n"
338
+ assoc_pairs.each do |_k2, pair_values|
339
+ seed_rows << "#{table_name.singularize.camelize}.create(#{pair_values})\n"
340
+ end
341
+ seed_rows << " end\n"
342
+ airtable_assocs_done[table_name] = assoc_pairs
299
343
  end
300
- seed_rows << " if ActiveRecord::Migration.table_exists?('#{table_name}')\n"
301
- seed_rows << " # Duplicate data found in: #{dupes.keys.join(', ')}\n" unless dupes.empty?
302
- seed_rows << " puts 'Seeding Airtable associations for #{table_name.camelize.singularize}'\n"
303
- assoc_pairs.each do |_k2, pair_values|
304
- seed_rows << "#{table_name.singularize.camelize}.create(#{pair_values})\n"
344
+
345
+ if dupe_list.present?
346
+ puts "\nAssociative tables with duplicate data:"
347
+ dupe_list.each { |entry| puts " #{entry}" }
348
+ puts '(You can safely remove one of the two tables in each of the pairs listed above.)'
305
349
  end
306
- seed_rows << " end\n"
307
- airtable_assocs_done[table_name] = assoc_pairs
308
350
  end
309
351
  seeds_file.write(seed_rows) unless seed_rows.blank?
310
352
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brick
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.246
4
+ version: 1.0.248
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits