brick 1.0.190 → 1.0.192
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/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
|