brick 1.0.190 → 1.0.192
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/brick/config.rb +8 -0
- data/lib/brick/extensions.rb +175 -48
- data/lib/brick/frameworks/rails/engine.rb +34 -14
- data/lib/brick/frameworks/rails/form_builder.rb +2 -1
- data/lib/brick/frameworks/rails/form_tags.rb +13 -4
- data/lib/brick/frameworks/rails.rb +6 -0
- data/lib/brick/route_mapper.rb +348 -0
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +10 -335
- data/lib/generators/brick/controllers_generator.rb +91 -0
- data/lib/generators/brick/migration_builder.rb +224 -160
- data/lib/generators/brick/migrations_generator.rb +1 -0
- data/lib/generators/brick/models_generator.rb +2 -2
- data/lib/generators/brick/salesforce_migrations_generator.rb +101 -0
- data/lib/generators/brick/salesforce_schema.rb +105 -0
- metadata +6 -2
@@ -32,7 +32,7 @@ module Brick
|
|
32
32
|
'bit' => 'boolean',
|
33
33
|
'varbinary' => 'binary',
|
34
34
|
'tinyint' => 'integer', # %%% Need to put in "limit: 2"
|
35
|
-
'year' => '
|
35
|
+
'year' => 'integer',
|
36
36
|
'set' => 'string',
|
37
37
|
# Sqlite data types
|
38
38
|
'TEXT' => 'text',
|
@@ -89,7 +89,13 @@ module Brick
|
|
89
89
|
[mig_path, is_insert_versions, is_delete_versions]
|
90
90
|
end
|
91
91
|
|
92
|
-
def generate_migrations(chosen, mig_path, is_insert_versions, is_delete_versions,
|
92
|
+
def generate_migrations(chosen, mig_path, is_insert_versions, is_delete_versions,
|
93
|
+
relations = ::Brick.relations, do_fks_last: nil, do_schema_migrations: true)
|
94
|
+
if do_fks_last.nil?
|
95
|
+
puts 'Would you like for the foreign keys to be built inline inside of each migration file, or as a final migration?'
|
96
|
+
do_fks_last = (gets_list(list: ['Inline', 'Separate final migration for all FKs']).start_with?('Separate'))
|
97
|
+
end
|
98
|
+
|
93
99
|
is_sqlite = ActiveRecord::Base.connection.adapter_name == 'SQLite'
|
94
100
|
key_type = ((is_sqlite || ActiveRecord.version < ::Gem::Version.new('5.1')) ? 'integer' : 'bigint')
|
95
101
|
is_4x_rails = ActiveRecord.version < ::Gem::Version.new('5.0')
|
@@ -112,6 +118,7 @@ module Brick
|
|
112
118
|
# Start by making migrations for fringe tables (those with no foreign keys).
|
113
119
|
# Continue layer by layer, creating migrations for tables that reference ones already done, until
|
114
120
|
# no more migrations can be created. (At that point hopefully all tables are accounted for.)
|
121
|
+
after_fks = [] # Track foreign keys to add after table creation
|
115
122
|
while (fringe = chosen.reject do |tbl|
|
116
123
|
snag_fks = []
|
117
124
|
snags = relations.fetch(tbl, nil)&.fetch(:fks, nil)&.select do |_k, v|
|
@@ -131,166 +138,58 @@ module Brick
|
|
131
138
|
end
|
132
139
|
end).present?
|
133
140
|
fringe.each do |tbl|
|
134
|
-
|
141
|
+
mig = gen_migration_columns(relations, tbl, (tbl_parts = tbl.split('.')), (add_fks = []),
|
142
|
+
key_type, is_4x_rails, ar_version, do_fks_last)
|
143
|
+
after_fks.concat(add_fks) if do_fks_last
|
144
|
+
versions_to_create << migration_file_write(mig_path, ::Brick._brick_index("create_#{tbl}", nil, 'x'), current_mig_time += 1.minute, ar_version, mig)
|
145
|
+
end
|
146
|
+
done.concat(fringe)
|
147
|
+
chosen -= done
|
148
|
+
end
|
135
149
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
if tbl_parts.first == (::Brick.default_schema || 'public')
|
147
|
-
tbl_parts.shift
|
148
|
-
nil
|
149
|
-
else
|
150
|
-
tbl_parts.first
|
151
|
-
end
|
152
|
-
end
|
153
|
-
unless schema.blank? || built_schemas.key?(schema)
|
154
|
-
mig = +" def change\n create_schema(:#{schema}) unless schema_exists?(:#{schema})\n end\n"
|
155
|
-
migration_file_write(mig_path, "create_db_schema_#{schema.underscore}", current_mig_time += 1.minute, ar_version, mig)
|
156
|
-
built_schemas[schema] = nil
|
157
|
-
end
|
150
|
+
if do_fks_last
|
151
|
+
# Write out any more tables that haven't been done yet
|
152
|
+
chosen.each do |tbl|
|
153
|
+
mig = gen_migration_columns(relations, tbl, (tbl_parts = tbl.split('.')), (add_fks = []),
|
154
|
+
key_type, is_4x_rails, ar_version, do_fks_last)
|
155
|
+
after_fks.concat(add_fks)
|
156
|
+
migration_file_write(mig_path, ::Brick._brick_index("create_#{tbl}", nil, 'x'), current_mig_time += 1.minute, ar_version, mig)
|
157
|
+
end
|
158
|
+
done.concat(chosen)
|
159
|
+
chosen.clear
|
158
160
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
# if this one has come in as bigint or integer.
|
164
|
-
pk_is_also_fk = fkey_cols.any? { |assoc| pkey_cols&.first == assoc[:fk] } ? pkey_cols&.first : nil
|
165
|
-
# Support missing primary key (by adding: , id: false)
|
166
|
-
id_option = if pk_is_also_fk || !pkey_cols&.present?
|
167
|
-
needs_serial_col = true
|
168
|
-
+', id: false'
|
169
|
-
elsif ((pkey_col_first = (col_def = relation[:cols][pkey_cols&.first])&.first) &&
|
170
|
-
(pkey_col_first = SQL_TYPES[pkey_col_first] || SQL_TYPES[col_def&.[](0..1)] ||
|
171
|
-
SQL_TYPES.find { |r| r.first.is_a?(Regexp) && pkey_col_first =~ r.first }&.last ||
|
172
|
-
pkey_col_first
|
173
|
-
) != key_type
|
174
|
-
)
|
175
|
-
case pkey_col_first
|
176
|
-
when 'integer'
|
177
|
-
+', id: :serial'
|
178
|
-
when 'bigint'
|
179
|
-
+', id: :bigserial'
|
180
|
-
else
|
181
|
-
+", id: :#{pkey_col_first}" # Something like: id: :integer, primary_key: :businessentityid
|
182
|
-
end +
|
183
|
-
(pkey_cols.first ? ", primary_key: :#{pkey_cols.first}" : '')
|
184
|
-
end
|
185
|
-
if !id_option && pkey_cols.sort != arpk
|
186
|
-
id_option = +", primary_key: :#{pkey_cols.first}"
|
187
|
-
end
|
188
|
-
if !is_4x_rails && (comment = relation&.fetch(:description, nil))&.present?
|
189
|
-
(id_option ||= +'') << ", comment: #{comment.inspect}"
|
190
|
-
end
|
191
|
-
# Find the ActiveRecord class in order to see if the columns have comments
|
192
|
-
unless is_4x_rails
|
193
|
-
klass = begin
|
194
|
-
tbl.tr('.', '/').singularize.camelize.constantize
|
195
|
-
rescue StandardError
|
196
|
-
end
|
197
|
-
if klass
|
198
|
-
unless ActiveRecord::Migration.table_exists?(klass.table_name)
|
199
|
-
puts "WARNING: Unable to locate table #{klass.table_name} (for #{klass.name})."
|
200
|
-
klass = nil
|
201
|
-
end
|
202
|
-
end
|
203
|
-
end
|
204
|
-
# Refer to this table name as a symbol or dotted string as appropriate
|
205
|
-
tbl_code = tbl_parts.length == 1 ? ":#{tbl_parts.first}" : "'#{tbl}'"
|
206
|
-
mig = +" def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
|
207
|
-
mig << " create_table #{tbl_code}#{id_option} do |t|\n"
|
208
|
-
possible_ts = [] # Track possible generic timestamps
|
209
|
-
add_fks = [] # Track foreign keys to add after table creation
|
210
|
-
relation[:cols].each do |col, col_type|
|
211
|
-
sql_type = SQL_TYPES[col_type.first] || SQL_TYPES[col_type[0..1]] ||
|
212
|
-
SQL_TYPES.find { |r| r.first.is_a?(Regexp) && col_type.first =~ r.first }&.last ||
|
213
|
-
col_type.first
|
214
|
-
suffix = col_type[3] || pkey_cols&.include?(col) ? +', null: false' : +''
|
215
|
-
suffix << ', array: true' if (col_type.first == 'ARRAY')
|
216
|
-
if !is_4x_rails && klass && (comment = klass.columns_hash.fetch(col, nil)&.comment)&.present?
|
217
|
-
suffix << ", comment: #{comment.inspect}"
|
218
|
-
end
|
219
|
-
# Determine if this column is used as part of a foreign key
|
220
|
-
if (fk = fkey_cols.find { |assoc| col == assoc[:fk] })
|
221
|
-
to_table = fk[:inverse_table].split('.')
|
222
|
-
to_table = to_table.length == 1 ? ":#{to_table.first}" : "'#{fk[:inverse_table]}'"
|
223
|
-
if needs_serial_col && pkey_cols&.include?(col) && (new_serial_type = {'integer' => 'serial', 'bigint' => 'bigserial'}[sql_type])
|
224
|
-
sql_type = new_serial_type
|
225
|
-
needs_serial_col = false
|
226
|
-
end
|
227
|
-
if fk[:fk] != "#{fk[:assoc_name].singularize}_id" # Need to do our own foreign_key tricks, not use references?
|
228
|
-
column = fk[:fk]
|
229
|
-
mig << emit_column(sql_type, column, suffix)
|
230
|
-
add_fks << [to_table, column, relations[fk[:inverse_table]]]
|
231
|
-
else
|
232
|
-
suffix << ", type: :#{sql_type}" unless sql_type == key_type
|
233
|
-
# Will the resulting default index name be longer than what Postgres allows? (63 characters)
|
234
|
-
if (idx_name = ActiveRecord::Base.connection.index_name(tbl, {column: col})).length > 63
|
235
|
-
# Try to find a shorter name that hasn't been used yet
|
236
|
-
unless indexes.key?(shorter = idx_name[0..62]) ||
|
237
|
-
indexes.key?(shorter = idx_name.tr('_', '')[0..62]) ||
|
238
|
-
indexes.key?(shorter = idx_name.tr('aeio', '')[0..62])
|
239
|
-
puts "Unable to easily find unique name for index #{idx_name} that is shorter than 64 characters,"
|
240
|
-
puts "so have resorted to this GUID-based identifier: #{shorter = "#{tbl[0..25]}_#{::SecureRandom.uuid}"}."
|
241
|
-
end
|
242
|
-
suffix << ", index: { name: '#{shorter || idx_name}' }"
|
243
|
-
indexes[shorter || idx_name] = nil
|
244
|
-
end
|
245
|
-
primary_key = nil
|
246
|
-
begin
|
247
|
-
primary_key = relations[fk[:inverse_table]][:class_name]&.constantize&.primary_key
|
248
|
-
rescue NameError => e
|
249
|
-
primary_key = ::Brick.ar_base.primary_key
|
250
|
-
end
|
251
|
-
mig << " t.references :#{fk[:assoc_name]}#{suffix}, foreign_key: { to_table: #{to_table}#{", primary_key: :#{primary_key}" if primary_key != ::Brick.ar_base.primary_key} }\n"
|
252
|
-
end
|
253
|
-
else
|
254
|
-
next if !id_option&.end_with?('id: false') && pkey_cols&.include?(col)
|
161
|
+
# Add a final migration to create all the foreign keys
|
162
|
+
mig = +" def change\n"
|
163
|
+
after_fks.each do |add_fk|
|
164
|
+
next unless add_fk[2] # add_fk[2] holds the inverse relation
|
255
165
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
else
|
260
|
-
mig << emit_column(sql_type, col, suffix)
|
261
|
-
end
|
262
|
-
end
|
263
|
-
end
|
264
|
-
if possible_ts.length == 2 && # Both created_at and updated_at
|
265
|
-
# Rails 5 and later timestamps default to NOT NULL
|
266
|
-
(possible_ts.first.last == is_4x_rails && possible_ts.last.last == is_4x_rails)
|
267
|
-
mig << "\n t.timestamps\n"
|
268
|
-
else # Just one or the other, or a nullability mismatch
|
269
|
-
possible_ts.each { |ts| emit_column('timestamp', ts.first, nil) }
|
166
|
+
unless (pk = add_fk[2][:pkey].values.flatten&.first)
|
167
|
+
# No official PK, but if coincidentally there's a column of the same name, take a chance on it
|
168
|
+
pk = (add_fk[2][:cols].key?(add_fk[1]) && add_fk[1]) || '???'
|
270
169
|
end
|
271
|
-
mig << "
|
272
|
-
|
273
|
-
|
274
|
-
mig << " dir.up { execute('ALTER TABLE #{tbl} ADD PRIMARY KEY (#{pk_is_also_fk})') }\n"
|
275
|
-
mig << " end\n"
|
276
|
-
end
|
277
|
-
add_fks.each do |add_fk|
|
278
|
-
is_commented = false
|
279
|
-
# add_fk[2] holds the inverse relation
|
280
|
-
unless (pk = add_fk[2][:pkey].values.flatten&.first)
|
281
|
-
is_commented = true
|
282
|
-
mig << " # (Unable to create relationship because primary key is missing on table #{add_fk[0]})\n"
|
283
|
-
# No official PK, but if coincidentally there's a column of the same name, take a chance on it
|
284
|
-
pk = (add_fk[2][:cols].key?(add_fk[1]) && add_fk[1]) || '???'
|
285
|
-
end
|
286
|
-
# to_table column
|
287
|
-
mig << " #{'# ' if is_commented}add_foreign_key #{tbl_code}, #{add_fk[0]}, column: :#{add_fk[1]}, primary_key: :#{pk}\n"
|
288
|
-
end
|
289
|
-
mig << " end\n"
|
290
|
-
versions_to_create << migration_file_write(mig_path, "create_#{tbl_parts.map(&:underscore).join('_')}", current_mig_time += 1.minute, ar_version, mig)
|
170
|
+
mig << " add_foreign_key #{add_fk[3]}, " # The tbl_code
|
171
|
+
# to_table column
|
172
|
+
mig << "#{add_fk[0]}, column: :#{add_fk[1]}, primary_key: :#{pk}\n"
|
291
173
|
end
|
292
|
-
|
293
|
-
|
174
|
+
if after_fks.length > 500
|
175
|
+
minutes = (after_fks.length + 1000) / 1500
|
176
|
+
mig << " if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'\n"
|
177
|
+
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"
|
178
|
+
|
179
|
+
mig << " # Vacuum takes only about ten seconds when all the tables are empty,
|
180
|
+
# and about 2 minutes when the tables are fairly full.
|
181
|
+
execute('COMMIT')
|
182
|
+
execute('VACUUM FULL')
|
183
|
+
execute('BEGIN TRANSACTION')
|
184
|
+
end\n"
|
185
|
+
end
|
186
|
+
|
187
|
+
mig << +" end\n"
|
188
|
+
migration_file_write(mig_path, 'create_brick_fks.rbx', current_mig_time += 1.minute, ar_version, mig)
|
189
|
+
puts "Have written out a final migration called 'create_brick_fks.rbx' which creates #{after_fks.length} foreign keys.
|
190
|
+
This file extension (.rbx) will cause it not to run yet when you do a 'rails db:migrate'.
|
191
|
+
The idea here is to do all data loading first, and then rename that migration file back
|
192
|
+
into having a .rb extension, and run a final db:migrate to put the foreign keys in place."
|
294
193
|
end
|
295
194
|
|
296
195
|
stuck_counts = Hash.new { |h, k| h[k] = 0 }
|
@@ -310,7 +209,7 @@ module Brick
|
|
310
209
|
". Here's the top 5 blockers" if stuck_sorted.length > 5
|
311
210
|
}:"
|
312
211
|
pp stuck_sorted[0..4]
|
313
|
-
|
212
|
+
elsif do_schema_migrations # Successful, and now we can update the schema_migrations table accordingly
|
314
213
|
unless ActiveRecord::Migration.table_exists?(ActiveRecord::Base.schema_migrations_table_name)
|
315
214
|
ActiveRecord::SchemaMigration.create_table
|
316
215
|
end
|
@@ -333,13 +232,178 @@ module Brick
|
|
333
232
|
|
334
233
|
private
|
335
234
|
|
235
|
+
def gen_migration_columns(relations, tbl, tbl_parts, add_fks,
|
236
|
+
key_type, is_4x_rails, ar_version, do_fks_last)
|
237
|
+
return unless (relation = relations.fetch(tbl, nil))&.fetch(:cols, nil)&.present?
|
238
|
+
|
239
|
+
mig = +''
|
240
|
+
pkey_cols = (rpk = relation[:pkey].values.flatten) & (arpk = [::Brick.ar_base.primary_key].flatten.sort)
|
241
|
+
# In case things aren't as standard
|
242
|
+
if pkey_cols.empty?
|
243
|
+
pkey_cols = if rpk.empty? && relation[:cols][arpk.first]&.first == key_type
|
244
|
+
arpk
|
245
|
+
elsif rpk.first
|
246
|
+
rpk
|
247
|
+
end
|
248
|
+
end
|
249
|
+
schema = if tbl_parts.length > 1
|
250
|
+
if tbl_parts.first == (::Brick.default_schema || 'public')
|
251
|
+
tbl_parts.shift
|
252
|
+
nil
|
253
|
+
else
|
254
|
+
tbl_parts.first
|
255
|
+
end
|
256
|
+
end
|
257
|
+
unless schema.blank? || built_schemas.key?(schema)
|
258
|
+
mig = +" def change\n create_schema(:#{schema}) unless schema_exists?(:#{schema})\n end\n"
|
259
|
+
migration_file_write(mig_path, "create_db_schema_#{schema.underscore}", current_mig_time += 1.minute, ar_version, mig)
|
260
|
+
built_schemas[schema] = nil
|
261
|
+
end
|
262
|
+
|
263
|
+
# %%% For the moment we're skipping polymorphics
|
264
|
+
fkey_cols = relation[:fks].values.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
|
265
|
+
# If the primary key is also used as a foreign key, will need to do id: false and then build out
|
266
|
+
# a column definition which includes :primary_key -- %%% also using a data type of bigserial or serial
|
267
|
+
# if this one has come in as bigint or integer.
|
268
|
+
pk_is_also_fk = fkey_cols.any? { |assoc| pkey_cols&.first == assoc[:fk] } ? pkey_cols&.first : nil
|
269
|
+
id_option = if pk_is_also_fk || !pkey_cols&.present?
|
270
|
+
needs_serial_col = true
|
271
|
+
+', id: false' # Support missing primary key (by adding: , id: false)
|
272
|
+
elsif ((pkey_col_first = (col_def = relation[:cols][pkey_cols&.first])&.first) &&
|
273
|
+
(pkey_col_first = SQL_TYPES[pkey_col_first] || SQL_TYPES[col_def&.[](0..1)] ||
|
274
|
+
SQL_TYPES.find { |r| r.first.is_a?(Regexp) && pkey_col_first =~ r.first }&.last ||
|
275
|
+
pkey_col_first
|
276
|
+
) != key_type
|
277
|
+
)
|
278
|
+
case pkey_col_first
|
279
|
+
when 'integer'
|
280
|
+
+', id: :serial'
|
281
|
+
when 'bigint'
|
282
|
+
+', id: :bigserial'
|
283
|
+
else
|
284
|
+
+", id: :#{pkey_col_first}" # Something like: id: :integer, primary_key: :businessentityid
|
285
|
+
end +
|
286
|
+
(pkey_cols.first ? ", primary_key: :#{pkey_cols.first}" : '')
|
287
|
+
end
|
288
|
+
if !id_option && pkey_cols.sort != arpk
|
289
|
+
id_option = +", primary_key: :#{pkey_cols.first}"
|
290
|
+
end
|
291
|
+
if !is_4x_rails && (comment = relation&.fetch(:description, nil))&.present?
|
292
|
+
(id_option ||= +'') << ", comment: #{comment.inspect}"
|
293
|
+
end
|
294
|
+
# Find the ActiveRecord class in order to see if the columns have comments
|
295
|
+
unless is_4x_rails
|
296
|
+
klass = begin
|
297
|
+
tbl.tr('.', '/').singularize.camelize.constantize
|
298
|
+
rescue StandardError
|
299
|
+
end
|
300
|
+
if klass
|
301
|
+
unless ActiveRecord::Migration.table_exists?(klass.table_name)
|
302
|
+
puts "WARNING: Unable to locate table #{klass.table_name} (for #{klass.name})."
|
303
|
+
klass = nil
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
# Refer to this table name as a symbol or dotted string as appropriate
|
308
|
+
tbl_code = tbl_parts.length == 1 ? ":#{tbl_parts.first}" : "'#{tbl}'"
|
309
|
+
mig = +" def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
|
310
|
+
mig << " create_table #{tbl_code}#{id_option} do |t|\n"
|
311
|
+
possible_ts = [] # Track possible generic timestamps
|
312
|
+
relation[:cols].each do |col, col_type|
|
313
|
+
sql_type = SQL_TYPES[col_type.first] || SQL_TYPES[col_type[0..1]] ||
|
314
|
+
SQL_TYPES.find { |r| r.first.is_a?(Regexp) && col_type.first =~ r.first }&.last ||
|
315
|
+
col_type.first
|
316
|
+
suffix = col_type[3] || pkey_cols&.include?(col) ? +', null: false' : +''
|
317
|
+
suffix << ', array: true' if (col_type.first == 'ARRAY')
|
318
|
+
if !is_4x_rails && klass && (comment = klass.columns_hash.fetch(col, nil)&.comment)&.present?
|
319
|
+
suffix << ", comment: #{comment.inspect}"
|
320
|
+
end
|
321
|
+
# Determine if this column is used as part of a foreign key
|
322
|
+
if (fk = fkey_cols.find { |assoc| col == assoc[:fk] })
|
323
|
+
to_table = fk[:inverse_table].split('.')
|
324
|
+
to_table = to_table.length == 1 ? ":#{to_table.first}" : "'#{fk[:inverse_table]}'"
|
325
|
+
if needs_serial_col && pkey_cols&.include?(col) && (new_serial_type = {'integer' => 'serial', 'bigint' => 'bigserial'}[sql_type])
|
326
|
+
sql_type = new_serial_type
|
327
|
+
needs_serial_col = false
|
328
|
+
end
|
329
|
+
if do_fks_last || (fk[:fk] != "#{fk[:assoc_name].singularize}_id") # Need to do our own foreign_key tricks, not use references?
|
330
|
+
column = fk[:fk]
|
331
|
+
mig << emit_column(sql_type, column, suffix)
|
332
|
+
add_fks << [to_table, column, relations[fk[:inverse_table]], tbl_code]
|
333
|
+
else
|
334
|
+
suffix << ", type: :#{sql_type}" unless sql_type == key_type
|
335
|
+
# Will the resulting default index name be longer than what Postgres allows? (63 characters)
|
336
|
+
if (idx_name = ActiveRecord::Base.connection.index_name(tbl, {column: col})).length > 63
|
337
|
+
# Try to find a shorter name that hasn't been used yet
|
338
|
+
unless indexes.key?(shorter = idx_name[0..62]) ||
|
339
|
+
indexes.key?(shorter = idx_name.tr('_', '')[0..62]) ||
|
340
|
+
indexes.key?(shorter = idx_name.tr('aeio', '')[0..62])
|
341
|
+
puts "Unable to easily find unique name for index #{idx_name} that is shorter than 64 characters,"
|
342
|
+
puts "so have resorted to this GUID-based identifier: #{shorter = "#{tbl[0..25]}_#{::SecureRandom.uuid}"}."
|
343
|
+
end
|
344
|
+
suffix << ", index: { name: '#{shorter || idx_name}' }"
|
345
|
+
indexes[shorter || idx_name] = nil
|
346
|
+
end
|
347
|
+
next if do_fks_last
|
348
|
+
|
349
|
+
primary_key = nil
|
350
|
+
begin
|
351
|
+
primary_key = relations[fk[:inverse_table]][:class_name]&.constantize&.primary_key
|
352
|
+
rescue NameError => e
|
353
|
+
primary_key = ::Brick.ar_base.primary_key
|
354
|
+
end
|
355
|
+
fk_stuff = ", foreign_key: { to_table: #{to_table}#{", primary_key: :#{primary_key}" if primary_key != ::Brick.ar_base.primary_key} }"
|
356
|
+
mig << " t.references :#{fk[:assoc_name]}#{suffix}#{fk_stuff}\n"
|
357
|
+
end
|
358
|
+
else
|
359
|
+
next if !id_option&.end_with?('id: false') && pkey_cols&.include?(col)
|
360
|
+
|
361
|
+
# See if there are generic timestamps
|
362
|
+
if sql_type == 'timestamp' && ['created_at','updated_at'].include?(col)
|
363
|
+
possible_ts << [col, !col_type[3]]
|
364
|
+
else
|
365
|
+
mig << emit_column(sql_type, col, suffix)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
if possible_ts.length == 2 && # Both created_at and updated_at
|
370
|
+
# Rails 5 and later timestamps default to NOT NULL
|
371
|
+
(possible_ts.first.last == is_4x_rails && possible_ts.last.last == is_4x_rails)
|
372
|
+
mig << "\n t.timestamps\n"
|
373
|
+
else # Just one or the other, or a nullability mismatch
|
374
|
+
possible_ts.each { |ts| emit_column('timestamp', ts.first, nil) }
|
375
|
+
end
|
376
|
+
mig << " end\n"
|
377
|
+
if pk_is_also_fk
|
378
|
+
mig << " reversible do |dir|\n"
|
379
|
+
mig << " dir.up { execute('ALTER TABLE #{tbl} ADD PRIMARY KEY (#{pk_is_also_fk})') }\n"
|
380
|
+
mig << " end\n"
|
381
|
+
end
|
382
|
+
add_fks.each do |add_fk|
|
383
|
+
next unless add_fk[2]
|
384
|
+
|
385
|
+
is_commented = false
|
386
|
+
# add_fk[2] holds the inverse relation
|
387
|
+
unless (pk = add_fk[2][:pkey]&.values&.flatten&.first)
|
388
|
+
is_commented = true
|
389
|
+
mig << " # (Unable to create relationship because primary key is missing on table #{add_fk[0]})\n"
|
390
|
+
# No official PK, but if coincidentally there's a column of the same name, take a chance on it
|
391
|
+
pk = (add_fk[2][:cols].key?(add_fk[1]) && add_fk[1]) || '???'
|
392
|
+
end
|
393
|
+
mig << " #{'# ' if do_fks_last}#{'# ' if is_commented}add_foreign_key #{tbl_code}, "
|
394
|
+
# to_table column
|
395
|
+
mig << "#{add_fk[0]}, column: :#{add_fk[1]}, primary_key: :#{pk}\n"
|
396
|
+
end
|
397
|
+
mig << " end\n"
|
398
|
+
end
|
399
|
+
|
336
400
|
def emit_column(type, name, suffix)
|
337
401
|
" t.#{type.start_with?('numeric') ? 'decimal' : type} :#{name}#{suffix}\n"
|
338
402
|
end
|
339
403
|
|
340
404
|
def migration_file_write(mig_path, name, current_mig_time, ar_version, mig)
|
341
|
-
File.open("#{mig_path}/#{version = current_mig_time.strftime('%Y%m%d%H%M00')}_#{name}.rb", "w") do |f|
|
342
|
-
f.write "class #{name.camelize} < ActiveRecord::Migration#{ar_version}\n"
|
405
|
+
File.open("#{mig_path}/#{version = current_mig_time.strftime('%Y%m%d%H%M00')}_#{name}#{'.rb' unless name.index('.')}", "w") do |f|
|
406
|
+
f.write "class #{name.split('.').first.camelize} < ActiveRecord::Migration#{ar_version}\n"
|
343
407
|
f.write mig
|
344
408
|
f.write "end\n"
|
345
409
|
end
|
@@ -6,12 +6,12 @@ require 'rails/generators/active_record'
|
|
6
6
|
require 'fancy_gets'
|
7
7
|
|
8
8
|
module Brick
|
9
|
-
# Auto-generates models
|
9
|
+
# Auto-generates models
|
10
10
|
class ModelsGenerator < ::Rails::Generators::Base
|
11
11
|
include FancyGets
|
12
12
|
# include ::Rails::Generators::Migration
|
13
13
|
|
14
|
-
desc 'Auto-generates models
|
14
|
+
desc 'Auto-generates models.'
|
15
15
|
|
16
16
|
def brick_models
|
17
17
|
# %%% If Apartment is active and there's no schema_to_analyse, ask which schema they want
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'brick'
|
4
|
+
require 'rails/generators'
|
5
|
+
require 'fancy_gets'
|
6
|
+
require 'generators/brick/migration_builder'
|
7
|
+
require 'generators/brick/salesforce_schema'
|
8
|
+
|
9
|
+
module Brick
|
10
|
+
# Auto-generates migration files
|
11
|
+
class SalesforceMigrationsGenerator < ::Rails::Generators::Base
|
12
|
+
include FancyGets
|
13
|
+
desc 'Auto-generates migration files for a set of Salesforce tables and columns.'
|
14
|
+
|
15
|
+
argument :wsdl_file, type: :string, default: ''
|
16
|
+
|
17
|
+
def brick_salesforce_migrations
|
18
|
+
::Brick.apply_double_underscore_patch
|
19
|
+
# ::Brick.mode = :on
|
20
|
+
# ActiveRecord::Base.establish_connection
|
21
|
+
|
22
|
+
# Runs at the end of parsing Salesforce WSDL, and uses the discovered tables and columns to create migrations
|
23
|
+
relations = nil
|
24
|
+
end_document_proc = lambda do |salesforce_tables|
|
25
|
+
# p [:end_document]
|
26
|
+
mig_path, is_insert_versions, is_delete_versions = ::Brick::MigrationBuilder.check_folder
|
27
|
+
return unless mig_path
|
28
|
+
|
29
|
+
# Generate a list of tables that can be chosen
|
30
|
+
table_names = salesforce_tables.keys
|
31
|
+
chosen = gets_list(list: table_names, chosen: table_names.dup)
|
32
|
+
|
33
|
+
soap_data_types = {
|
34
|
+
'tns:ID' => 'string',
|
35
|
+
'xsd:string' => 'string',
|
36
|
+
'xsd:dateTime' => 'datetime',
|
37
|
+
'xsd:boolean' => 'boolean',
|
38
|
+
'xsd:double' => 'float',
|
39
|
+
'xsd:int' => 'integer',
|
40
|
+
'xsd:date' => 'date',
|
41
|
+
'xsd:anyType' => 'string', # Don't fully know on this
|
42
|
+
'xsd:long' => 'bigint',
|
43
|
+
'xsd:base64Binary' => 'bytea',
|
44
|
+
'xsd:time' => 'time'
|
45
|
+
}
|
46
|
+
fk_idx = 0
|
47
|
+
# Build out a '::Brick.relations' hash that represents this Salesforce schema
|
48
|
+
relations = chosen.each_with_object({}) do |tbl_name, s|
|
49
|
+
tbl = salesforce_tables[tbl_name]
|
50
|
+
# Build out columns and foreign keys
|
51
|
+
cols = { 'id'=>['string', nil, false, true] }
|
52
|
+
fks = {}
|
53
|
+
tbl[:cols].each do |col|
|
54
|
+
next if col[:name] == 'Id'
|
55
|
+
|
56
|
+
dt = soap_data_types[col[:data_type]] || 'string'
|
57
|
+
cols[col[:name]] = [dt, nil, col[:nillable], false]
|
58
|
+
if (ref_to = col[:fk_reference_to])
|
59
|
+
fk_hash = {
|
60
|
+
is_bt: true,
|
61
|
+
fk: col[:name],
|
62
|
+
assoc_name: "#{col[:name]}_bt",
|
63
|
+
inverse_table: ref_to
|
64
|
+
}
|
65
|
+
fks["fk_salesforce_#{fk_idx += 1}"] = fk_hash
|
66
|
+
end
|
67
|
+
end
|
68
|
+
# Put it all into a relation entry, named the same as the table
|
69
|
+
s[tbl_name] = {
|
70
|
+
pkey: { "#{tbl_name}_pkey" => ['id'] },
|
71
|
+
cols: cols,
|
72
|
+
fks: fks
|
73
|
+
}
|
74
|
+
end
|
75
|
+
# Build but do not have foreign keys established yet, and do not put version entries info the schema_migrations table
|
76
|
+
::Brick::MigrationBuilder.generate_migrations(chosen, mig_path, is_insert_versions, is_delete_versions, relations,
|
77
|
+
do_fks_last: true, do_schema_migrations: false)
|
78
|
+
end
|
79
|
+
parser = Nokogiri::XML::SAX::Parser.new(::Brick::SalesforceSchema.new(end_document_proc))
|
80
|
+
# The WSDL file must have a .xml extension, and can be in any folder in the project
|
81
|
+
# Alternatively the user can supply this option on the command line
|
82
|
+
@wsdl_file = nil if @wsdl_file == ''
|
83
|
+
loop do
|
84
|
+
break if (@wsdl_file ||= gets_list(Dir['**/*.xml'] + ['* Cancel *'])) == '* Cancel *'
|
85
|
+
|
86
|
+
parser.parse(File.read(@wsdl_file))
|
87
|
+
|
88
|
+
if relations.length > 300
|
89
|
+
puts "A Salesforce installation generally has hundreds to a few thousand tables, and many are empty.
|
90
|
+
In order to more easily navigate just those tables that have content, you might want to add this
|
91
|
+
to brick.rb:
|
92
|
+
::Brick.omit_empty_tables_in_dropdown = true"
|
93
|
+
end
|
94
|
+
break
|
95
|
+
rescue Errno::ENOENT
|
96
|
+
puts "File \"#{@wsdl_file}\" is not found."
|
97
|
+
@wsdl_file = nil
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|