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.
- checksums.yaml +4 -4
- data/lib/brick/compatibility.rb +1 -1
- data/lib/brick/extensions.rb +69 -43
- data/lib/brick/rails/engine.rb +70 -72
- data/lib/brick/rails/form_builder.rb +18 -16
- data/lib/brick/rails/form_tags.rb +55 -24
- data/lib/brick/rails.rb +34 -34
- data/lib/brick/reflect_tables.rb +18 -12
- data/lib/brick/util.rb +3 -3
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +9 -9
- data/lib/generators/brick/airtable_api_caller.rb +21 -7
- data/lib/generators/brick/airtable_migrations_generator.rb +3 -2
- data/lib/generators/brick/airtable_seeds_generator.rb +1 -1
- data/lib/generators/brick/install_generator.rb +1 -1
- data/lib/generators/brick/migrations_builder.rb +39 -15
- data/lib/generators/brick/seeds_builder.rb +77 -35
- metadata +1 -1
|
@@ -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
|
-
|
|
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
|
|
152
|
-
|
|
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
|
|
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
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
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[:
|
|
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
|
|
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 (#{
|
|
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.\
|
|
18
|
-
return
|
|
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.
|
|
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
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
294
|
-
airtable_assoc_recids.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|