database-model-generator 0.6.0
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 +7 -0
- checksums.yaml.gz.sig +3 -0
- data/CHANGES.md +59 -0
- data/Gemfile +2 -0
- data/LICENSE +177 -0
- data/MANIFEST.md +10 -0
- data/README.md +186 -0
- data/Rakefile +32 -0
- data/bin/dmg +831 -0
- data/certs/djberg96_pub.pem +26 -0
- data/database-model-generator.gemspec +31 -0
- data/docker/README.md +238 -0
- data/docker/oracle/Dockerfile +87 -0
- data/docker/oracle/README.md +140 -0
- data/docker/oracle/docker-compose.yml +41 -0
- data/docker/oracle/test.sh +45 -0
- data/docker/sqlserver/DOCKER.md +152 -0
- data/docker/sqlserver/Dockerfile +29 -0
- data/docker/sqlserver/SUPPORT.md +477 -0
- data/docker/sqlserver/TESTING.md +194 -0
- data/docker/sqlserver/docker-compose.yml +52 -0
- data/docker/sqlserver/init-db.sql +158 -0
- data/docker/sqlserver/run_tests.sh +154 -0
- data/docker/sqlserver/setup-db.sh +9 -0
- data/docker/sqlserver/test-Dockerfile +36 -0
- data/docker/sqlserver/test.sh +56 -0
- data/lib/database_model_generator.rb +652 -0
- data/lib/oracle/model/generator.rb +287 -0
- data/lib/sqlserver/model/generator.rb +281 -0
- data/spec/oracle_model_generator_spec.rb +176 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/oracle_connection.rb +126 -0
- data.tar.gz.sig +0 -0
- metadata +162 -0
- metadata.gz.sig +0 -0
data/bin/dmg
ADDED
@@ -0,0 +1,831 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/database_model_generator'
|
4
|
+
require 'getopt/long'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'dbi/dbrc'
|
8
|
+
rescue LoadError
|
9
|
+
# Do nothing. Users must specify username/password information on the
|
10
|
+
# command line via the -u and -p options.
|
11
|
+
end
|
12
|
+
|
13
|
+
opts = Getopt::Long.getopts(
|
14
|
+
|
15
|
+
['--help', '-h'],
|
16
|
+
['--table', '-t', Getopt::REQUIRED],
|
17
|
+
['--view', '-v', Getopt::REQUIRED],
|
18
|
+
['--user', '-u', Getopt::REQUIRED],
|
19
|
+
['--password', '-p', Getopt::REQUIRED],
|
20
|
+
['--database', '-d', Getopt::REQUIRED],
|
21
|
+
['--output', '-o', Getopt::REQUIRED],
|
22
|
+
['--rails', '-r', Getopt::REQUIRED],
|
23
|
+
['--tests', '-x', Getopt::OPTIONAL],
|
24
|
+
['--class', '-c', Getopt::OPTIONAL],
|
25
|
+
['--indexes', '-i'],
|
26
|
+
['--type', '-T', Getopt::REQUIRED],
|
27
|
+
['--server', '-s', Getopt::REQUIRED],
|
28
|
+
['--port', '-P', Getopt::REQUIRED]
|
29
|
+
)
|
30
|
+
|
31
|
+
def help
|
32
|
+
version = DatabaseModel::Generator::VERSION
|
33
|
+
%Q{
|
34
|
+
Available options for the Database Model Generator (version #{version}) are:
|
35
|
+
|
36
|
+
-h, --help => Display the help text you're looking at now.
|
37
|
+
-t, --table => The name of the table you wish to model.
|
38
|
+
-v, --view => The name of the view you wish to model.
|
39
|
+
-o, --output => The name of the file to create.
|
40
|
+
-u, --user => The username used to establish a connection to the database.
|
41
|
+
-p, --password => The password used to establish a connection to the database.
|
42
|
+
-d, --database => The name of the database to connect to.
|
43
|
+
-s, --server => The database server hostname (for SQL Server).
|
44
|
+
-P, --port => The database server port (default: 1433 for SQL Server).
|
45
|
+
-T, --type => Database type: 'oracle' or 'sqlserver' (auto-detected if not specified).
|
46
|
+
-r, --rails => The version of rails you're using (2 or higher).
|
47
|
+
-x, --tests => Generate tests using testunit, minitest or rspec.
|
48
|
+
-c, --class => Class name for the generated table (optional)
|
49
|
+
|
50
|
+
Database Connection Examples:
|
51
|
+
|
52
|
+
Oracle (traditional):
|
53
|
+
dmg -T oracle -d localhost:1521/XE -u scott -p tiger -t users
|
54
|
+
|
55
|
+
SQL Server:
|
56
|
+
dmg -T sqlserver -s localhost -P 1433 -d mydb -u sa -p password -t users
|
57
|
+
|
58
|
+
Auto-detection (Oracle if no server specified, SQL Server if server specified):
|
59
|
+
dmg -d localhost:1521/XE -u scott -p tiger -t users # Oracle
|
60
|
+
dmg -s localhost -d mydb -u sa -p password -t users # SQL Server
|
61
|
+
|
62
|
+
If no user or password are supplied, then the generator will attempt to glean that
|
63
|
+
information using a combination of the database name and your .dbrc file.
|
64
|
+
If that cannot be found, then an error is raised.
|
65
|
+
|
66
|
+
If no output file is supplied then the file generated will match the name
|
67
|
+
of the table, minus the 's' if present, with a .rb extension. This is lazy,
|
68
|
+
but it is orders of magnitude easier for you to rename a file than it is
|
69
|
+
for me to deal with all possible permutations. Note that the output file
|
70
|
+
name is also used as the basis for the class name.
|
71
|
+
|
72
|
+
If no tests option is specified then a test file, using test-unit 2, will
|
73
|
+
be generated that includes some basic tests to backup the builtin
|
74
|
+
validations. Legal options are 'testunit', 'minitest', 'rspec', or 'none'.
|
75
|
+
If you specify 'none', then no test file is generated.
|
76
|
+
|
77
|
+
Examples:
|
78
|
+
|
79
|
+
# Create a User model for the users table (current rails)
|
80
|
+
dmg -d some_database -u scott -p tiger -t users
|
81
|
+
|
82
|
+
# Create a User model for the users table (rails 2)
|
83
|
+
dmg -d some_database -r 2 -u scott -p tiger -t users
|
84
|
+
|
85
|
+
# Same thing, using dbi-dbrc behind the scenes
|
86
|
+
dmg -d some_database -t users
|
87
|
+
|
88
|
+
# Create a Lily model for the lilies table, and specify the output file.
|
89
|
+
dmg -d some_database -u scott -p tiger -t lilies -o lily.rb
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
if opts['h']
|
94
|
+
puts help
|
95
|
+
exit!
|
96
|
+
end
|
97
|
+
|
98
|
+
unless opts['database']
|
99
|
+
puts "You must specify a database."
|
100
|
+
exit!
|
101
|
+
end
|
102
|
+
|
103
|
+
unless opts['table'] || opts['view']
|
104
|
+
puts "You must specify a table or view."
|
105
|
+
exit!
|
106
|
+
end
|
107
|
+
|
108
|
+
user = opts['user']
|
109
|
+
pass = opts['password']
|
110
|
+
|
111
|
+
# Determine database type
|
112
|
+
database_type = opts['type']&.downcase
|
113
|
+
unless database_type
|
114
|
+
if opts['server'] || opts['port']
|
115
|
+
database_type = 'sqlserver'
|
116
|
+
else
|
117
|
+
database_type = 'oracle' # Default to Oracle for backward compatibility
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
unless ['oracle', 'sqlserver'].include?(database_type)
|
122
|
+
puts "Invalid database type: #{database_type}. Must be 'oracle' or 'sqlserver'."
|
123
|
+
exit!
|
124
|
+
end
|
125
|
+
|
126
|
+
unless user && pass
|
127
|
+
begin
|
128
|
+
dbrc = DBI::DBRC.new(opts['database'], user)
|
129
|
+
user = dbrc.user
|
130
|
+
pass = dbrc.passwd
|
131
|
+
rescue NameError
|
132
|
+
msg = "If you do not specify a username or password on the command line "
|
133
|
+
msg << "then you must use the dbi-dbrc library and create a .dbrc file in "
|
134
|
+
msg << "your home directory."
|
135
|
+
puts msg
|
136
|
+
exit!
|
137
|
+
rescue DBI::DBRC::Error
|
138
|
+
msg = "No user or password provided, and no dbrc entry found for '"
|
139
|
+
msg << opts['database'] + "'."
|
140
|
+
puts msg
|
141
|
+
exit!
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
table = opts['table']
|
146
|
+
view = opts['view']
|
147
|
+
|
148
|
+
if table && view
|
149
|
+
puts "You cannot specify both a table and a view."
|
150
|
+
exit!
|
151
|
+
end
|
152
|
+
|
153
|
+
# Create database connection based on type
|
154
|
+
connection = nil
|
155
|
+
begin
|
156
|
+
case database_type
|
157
|
+
when 'oracle'
|
158
|
+
require 'oci8'
|
159
|
+
connection = OCI8.new(user, pass, opts['database'])
|
160
|
+
when 'sqlserver'
|
161
|
+
require 'tiny_tds'
|
162
|
+
server = opts['server'] || 'localhost'
|
163
|
+
port = opts['port'] || 1433
|
164
|
+
|
165
|
+
connection = TinyTds::Client.new(
|
166
|
+
username: user,
|
167
|
+
password: pass,
|
168
|
+
host: server,
|
169
|
+
port: port.to_i,
|
170
|
+
database: opts['database']
|
171
|
+
)
|
172
|
+
end
|
173
|
+
rescue LoadError => e
|
174
|
+
case database_type
|
175
|
+
when 'oracle'
|
176
|
+
puts "Oracle support requires the 'oci8' gem. Install with: gem install oci8"
|
177
|
+
when 'sqlserver'
|
178
|
+
puts "SQL Server support requires the 'tiny_tds' gem. Install with: gem install tiny_tds"
|
179
|
+
end
|
180
|
+
exit!
|
181
|
+
rescue => e
|
182
|
+
puts "Failed to connect to #{database_type.capitalize} database: #{e.message}"
|
183
|
+
exit!
|
184
|
+
end
|
185
|
+
|
186
|
+
# Create the appropriate generator
|
187
|
+
omg = DatabaseModel::Generator.new(connection, type: database_type)
|
188
|
+
omg.generate(table || view, view)
|
189
|
+
|
190
|
+
# If user only wants index recommendations, show them and exit
|
191
|
+
if opts['indexes']
|
192
|
+
puts "Index Recommendations for #{(table || view).upcase}"
|
193
|
+
puts "=" * 50
|
194
|
+
|
195
|
+
recommendations = omg.index_recommendations
|
196
|
+
|
197
|
+
[:foreign_keys, :unique_constraints, :date_queries, :status_enum, :composite, :full_text].each do |category|
|
198
|
+
next if recommendations[category].empty?
|
199
|
+
|
200
|
+
puts "\n#{category.to_s.gsub('_', ' ').capitalize}:"
|
201
|
+
puts "-" * 30
|
202
|
+
|
203
|
+
recommendations[category].each do |rec|
|
204
|
+
puts " #{rec[:sql]}"
|
205
|
+
puts " # #{rec[:reason]}"
|
206
|
+
puts " # Type: #{rec[:type]}" if rec[:type]
|
207
|
+
puts
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Generate a sample migration
|
212
|
+
unless recommendations.values.flatten.empty?
|
213
|
+
puts "\nSample Rails Migration:"
|
214
|
+
puts "=" * 25
|
215
|
+
migration_name = "AddIndexesTo#{omg.model.gsub(/([a-z])([A-Z])/, '\1\2').split(/(?=[A-Z])/).join}"
|
216
|
+
puts "class #{migration_name} < ActiveRecord::Migration[7.0]"
|
217
|
+
puts " def change"
|
218
|
+
|
219
|
+
[:foreign_keys, :unique_constraints, :date_queries, :status_enum].each do |category|
|
220
|
+
next if recommendations[category].empty?
|
221
|
+
puts " # #{category.to_s.gsub('_', ' ').capitalize}"
|
222
|
+
recommendations[category].each do |rec|
|
223
|
+
puts " #{rec[:sql]}"
|
224
|
+
end
|
225
|
+
puts
|
226
|
+
end
|
227
|
+
|
228
|
+
recommendations[:composite].each do |rec|
|
229
|
+
puts " #{rec[:sql]} # #{rec[:reason]}"
|
230
|
+
end
|
231
|
+
|
232
|
+
unless recommendations[:full_text].empty?
|
233
|
+
puts "\n # Full-text indexes require separate DDL:"
|
234
|
+
recommendations[:full_text].each do |rec|
|
235
|
+
puts " # #{rec[:sql]}"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
puts " end"
|
240
|
+
puts "end"
|
241
|
+
else
|
242
|
+
puts "\nNo index recommendations found for this table."
|
243
|
+
end
|
244
|
+
|
245
|
+
omg.disconnect if omg.respond_to?(:disconnect)
|
246
|
+
exit
|
247
|
+
end
|
248
|
+
ofile = opts['o']
|
249
|
+
|
250
|
+
unless ofile
|
251
|
+
ofile = omg.table.downcase
|
252
|
+
ofile.chop! if ofile[-1].chr.upcase == 'S'
|
253
|
+
ofile += '.rb'
|
254
|
+
end
|
255
|
+
|
256
|
+
# Default to Rails 4
|
257
|
+
rails = (opts['rails'] && opts['rails'].to_i) || 4
|
258
|
+
|
259
|
+
if rails < 2
|
260
|
+
puts "Invalid version of Rails. Must be 2 or higher."
|
261
|
+
exit!
|
262
|
+
end
|
263
|
+
|
264
|
+
omg.instance_eval { @model = opts['class'] } if opts['class'] # dirty fix
|
265
|
+
|
266
|
+
File.open(ofile, 'w') do |fh|
|
267
|
+
fh.puts "# Generated by Database Model Generator v#{DatabaseModel::Generator::VERSION}"
|
268
|
+
fh.puts "# Table: #{table.upcase}"
|
269
|
+
fh.puts "# Generated on: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
270
|
+
fh.puts
|
271
|
+
fh.puts "require 'securerandom' if defined?(SecureRandom)"
|
272
|
+
fh.puts
|
273
|
+
fh.puts "class #{omg.model} < ActiveRecord::Base"
|
274
|
+
fh.puts " set_table_name \"#{table}\""
|
275
|
+
|
276
|
+
if omg.primary_keys.size > 1
|
277
|
+
fh.puts "\n # Requires the composite-primary-keys library"
|
278
|
+
fh.puts " set_primary_keys " + omg.primary_keys.inspect
|
279
|
+
else
|
280
|
+
fh.puts "\n set_primary_key " + omg.primary_keys.first.to_sym.inspect unless omg.primary_keys.first.nil?
|
281
|
+
end
|
282
|
+
|
283
|
+
fh.puts "\n # Table relationships\n\n"
|
284
|
+
omg.belongs_to.uniq.each{ |table|
|
285
|
+
fh.puts " belongs_to :#{table.downcase}"
|
286
|
+
}
|
287
|
+
|
288
|
+
# Polymorphic associations
|
289
|
+
if omg.has_polymorphic_associations?
|
290
|
+
fh.puts "\n # Polymorphic associations" unless omg.belongs_to.empty?
|
291
|
+
omg.polymorphic_associations.each do |assoc|
|
292
|
+
fh.puts " belongs_to :#{assoc[:association_name]}, polymorphic: true"
|
293
|
+
end
|
294
|
+
|
295
|
+
# Add suggestions for parent models
|
296
|
+
fh.puts "\n # For polymorphic parent models, add these associations:"
|
297
|
+
omg.polymorphic_has_many_suggestions.each do |suggestion|
|
298
|
+
fh.puts " # #{suggestion[:association]} # #{suggestion[:description]}"
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# Enum definitions
|
303
|
+
if omg.has_enum_columns?
|
304
|
+
fh.puts "\n # Enums"
|
305
|
+
omg.enum_definitions.each do |enum_def|
|
306
|
+
fh.puts " #{enum_def}"
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
fh.puts "\n # Validations\n"
|
311
|
+
|
312
|
+
if rails == 2
|
313
|
+
# Character fields, size
|
314
|
+
omg.column_info.each{ |col|
|
315
|
+
data_type = col.data_type.to_s
|
316
|
+
if ['char', 'varchar', 'varchar2'].include?(data_type)
|
317
|
+
validation = "validates_size_of :#{col.name.downcase}, :maximum => #{col.data_size}"
|
318
|
+
validation << ", :allow_blank => #{col.nullable?}" if col.nullable?
|
319
|
+
fh.puts " #{validation}"
|
320
|
+
end
|
321
|
+
}
|
322
|
+
|
323
|
+
fh.puts # Line break
|
324
|
+
|
325
|
+
# Fields that must be present
|
326
|
+
omg.column_info.each{ |col|
|
327
|
+
unless col.nullable?
|
328
|
+
validation = "validates_presence_of :#{col.name.downcase}"
|
329
|
+
fh.puts " #{validation}"
|
330
|
+
end
|
331
|
+
}
|
332
|
+
|
333
|
+
fh.puts # Line break
|
334
|
+
|
335
|
+
# Numeric fields
|
336
|
+
omg.column_info.each{ |col|
|
337
|
+
if col.data_type.to_s == 'number'
|
338
|
+
max = ("9" * (col.precision - col.scale)).to_i
|
339
|
+
|
340
|
+
validation = "validates_numericality_of :#{col.name.downcase}"
|
341
|
+
validation << ", :less_than => #{max + 1}, :greater_than => -#{max + 1}"
|
342
|
+
|
343
|
+
if col.scale == 0
|
344
|
+
validation << ", :only_integer => true"
|
345
|
+
end
|
346
|
+
|
347
|
+
fh.puts " #{validation}"
|
348
|
+
end
|
349
|
+
}
|
350
|
+
else
|
351
|
+
# Character fields, size
|
352
|
+
omg.column_info.each{ |col|
|
353
|
+
|
354
|
+
data_type = col.data_type.to_s
|
355
|
+
|
356
|
+
case data_type
|
357
|
+
when 'char', 'varchar', 'varchar2'
|
358
|
+
validation = "validates :#{col.name.downcase}, "
|
359
|
+
validation << ":length => {:maximum => #{col.data_size}}"
|
360
|
+
|
361
|
+
format_str = ",\n :format => { :with => /[:alpha]/, :message => 'is not a string'"
|
362
|
+
|
363
|
+
if col.nullable?
|
364
|
+
format_str += ", :if => :#{col.name.downcase}? }"
|
365
|
+
else
|
366
|
+
format_str += " }"
|
367
|
+
end
|
368
|
+
|
369
|
+
validation << format_str
|
370
|
+
validation << ",\n :presence => #{!col.nullable?}" unless col.nullable?
|
371
|
+
validation << "\n\n"
|
372
|
+
when 'number'
|
373
|
+
max = "9" * col.precision
|
374
|
+
max.insert(col.precision - col.scale, ".") if col.scale > 0
|
375
|
+
|
376
|
+
validation = "\n validates :#{col.name.downcase}"
|
377
|
+
|
378
|
+
unless col.nullable?
|
379
|
+
validation << ", :presence => #{!col.nullable?}"
|
380
|
+
end
|
381
|
+
|
382
|
+
unless max.empty?
|
383
|
+
validation << ", :numericality => {"
|
384
|
+
validation << "\n :less_than_or_equal_to => #{max}, "
|
385
|
+
validation << "\n :greater_than_or_equal_to => -#{max}"
|
386
|
+
|
387
|
+
if col.scale == 0
|
388
|
+
validation << ",\n :only_integer => true"
|
389
|
+
end
|
390
|
+
|
391
|
+
validation << "\n }\n\n"
|
392
|
+
end
|
393
|
+
|
394
|
+
end
|
395
|
+
|
396
|
+
fh.puts " #{validation}"
|
397
|
+
}
|
398
|
+
end
|
399
|
+
|
400
|
+
fh.puts # Line break
|
401
|
+
header_printed = false
|
402
|
+
|
403
|
+
# Date fields
|
404
|
+
omg.column_info.each{ |col|
|
405
|
+
data_type = col.data_type.to_s
|
406
|
+
|
407
|
+
if ['date', 'time'].include?(data_type)
|
408
|
+
if data_type == 'date'
|
409
|
+
validation = "validates_date :#{col.name.downcase}"
|
410
|
+
end
|
411
|
+
|
412
|
+
if data_type == 'timestamp'
|
413
|
+
validation = "validates_time :#{col.name.downcase}"
|
414
|
+
end
|
415
|
+
|
416
|
+
unless header_printed
|
417
|
+
fh.puts " # Requires the validates_timeliness library"
|
418
|
+
end
|
419
|
+
|
420
|
+
fh.puts " #{validation}"
|
421
|
+
end
|
422
|
+
}
|
423
|
+
|
424
|
+
fh.puts "end"
|
425
|
+
end
|
426
|
+
|
427
|
+
opts['x'] ||= 'rspec' # Default
|
428
|
+
|
429
|
+
testunit = opts['x'] == 'testunit'
|
430
|
+
minitest = opts['x'] == 'minitest'
|
431
|
+
rspec = opts['x'] == 'rspec'
|
432
|
+
|
433
|
+
if testunit || minitest || rspec
|
434
|
+
if rspec
|
435
|
+
test_file = "#{ofile.gsub('.rb', '')}_spec.rb"
|
436
|
+
else
|
437
|
+
test_file = "test_#{ofile}"
|
438
|
+
end
|
439
|
+
|
440
|
+
File.open(test_file, "w") do |fh|
|
441
|
+
setup_var = omg.table.downcase
|
442
|
+
setup_var.chop! if setup_var[-1].chr.downcase == 's'
|
443
|
+
instance_var = "@#{setup_var}"
|
444
|
+
|
445
|
+
if testunit
|
446
|
+
fh.puts "require 'test-unit'\n\n"
|
447
|
+
elsif minitest
|
448
|
+
fh.puts "require 'minitest/autorun'\n\n"
|
449
|
+
else # rspec
|
450
|
+
fh.puts "require 'rspec'\n"
|
451
|
+
fh.puts "require_relative '#{ofile.gsub('.rb', '')}'\n\n"
|
452
|
+
end
|
453
|
+
|
454
|
+
if testunit
|
455
|
+
fh.puts "class TC_#{omg.model} < Test::Unit::TestCase\n"
|
456
|
+
elsif minitest
|
457
|
+
fh.puts "class TC_#{omg.model} < Minitest::Unit::TestCase\n"
|
458
|
+
else # rspec
|
459
|
+
fh.puts "RSpec.describe #{omg.model} do\n"
|
460
|
+
end
|
461
|
+
|
462
|
+
if rspec
|
463
|
+
fh.puts " let(:#{setup_var}) { #{omg.model}.new }\n\n"
|
464
|
+
else
|
465
|
+
fh.puts " def setup"
|
466
|
+
fh.puts " #{instance_var} = #{omg.model}.new"
|
467
|
+
fh.puts " end\n\n"
|
468
|
+
end
|
469
|
+
|
470
|
+
if testunit
|
471
|
+
fh.puts " test 'table name is #{omg.table.downcase}' do"
|
472
|
+
elsif minitest
|
473
|
+
fh.puts " def table_name_is_#{omg.table.downcase}"
|
474
|
+
else # rspec
|
475
|
+
fh.puts " describe 'table configuration' do"
|
476
|
+
fh.puts " it 'has table name #{omg.table.downcase}' do"
|
477
|
+
end
|
478
|
+
|
479
|
+
if rspec
|
480
|
+
fh.puts " expect(#{omg.model}.table_name).to eq('#{omg.table.downcase}')"
|
481
|
+
else
|
482
|
+
fh.puts " assert_equal('#{omg.table.downcase}', #{omg.model}.table_name)"
|
483
|
+
end
|
484
|
+
|
485
|
+
if rspec
|
486
|
+
fh.puts " end"
|
487
|
+
else
|
488
|
+
fh.puts " end\n\n"
|
489
|
+
end
|
490
|
+
|
491
|
+
if omg.primary_keys.size > 1
|
492
|
+
if testunit
|
493
|
+
test_string = " test 'primary keys are #{omg.primary_keys.join(', ')}' do"
|
494
|
+
elsif minitest
|
495
|
+
test_string = " def test_primary_keys_are_#{omg.primary_keys.join('_')}"
|
496
|
+
else # rspec
|
497
|
+
test_string = " it 'has primary keys #{omg.primary_keys.join(', ')}' do"
|
498
|
+
end
|
499
|
+
fh.puts test_string
|
500
|
+
|
501
|
+
if rspec
|
502
|
+
fh.puts " expect(#{omg.model}.primary_keys).to eq('#{omg.primary_keys.join(', ')}')"
|
503
|
+
else
|
504
|
+
fh.puts " assert_equal('#{omg.primary_keys.join(', ')}', #{omg.model}.primary_keys)"
|
505
|
+
end
|
506
|
+
else
|
507
|
+
if testunit
|
508
|
+
test_string = " test 'primary key is #{omg.primary_keys.first}' do"
|
509
|
+
elsif minitest
|
510
|
+
test_string = " def test_primary_key_is_#{omg.primary_keys.first}"
|
511
|
+
else # rspec
|
512
|
+
test_string = " it 'has primary key #{omg.primary_keys.first}' do"
|
513
|
+
end
|
514
|
+
fh.puts test_string
|
515
|
+
|
516
|
+
if rspec
|
517
|
+
fh.puts " expect(#{omg.model}.primary_key).to eq('#{omg.primary_keys.first}')"
|
518
|
+
else
|
519
|
+
fh.puts " assert_equal('#{omg.primary_keys.first}', #{omg.model}.primary_key)"
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
if rspec
|
524
|
+
fh.puts " end"
|
525
|
+
fh.puts " end\n"
|
526
|
+
else
|
527
|
+
fh.puts " end\n"
|
528
|
+
end
|
529
|
+
|
530
|
+
omg.column_info.each{ |col|
|
531
|
+
data_type = col.data_type.to_s
|
532
|
+
column = col.name.downcase
|
533
|
+
|
534
|
+
if rspec
|
535
|
+
fh.puts "\n describe '#{column} column' do"
|
536
|
+
instance_reference = setup_var
|
537
|
+
else
|
538
|
+
instance_reference = instance_var
|
539
|
+
end
|
540
|
+
|
541
|
+
if testunit
|
542
|
+
fh.puts "\n test '#{column} basic functionality' do"
|
543
|
+
elsif minitest
|
544
|
+
fh.puts "\n def test_#{column}_basic_functionality"
|
545
|
+
else # rspec
|
546
|
+
fh.puts " it 'responds to #{column}' do"
|
547
|
+
end
|
548
|
+
|
549
|
+
if rspec
|
550
|
+
fh.puts " expect(#{instance_reference}).to respond_to(:#{column})"
|
551
|
+
else
|
552
|
+
fh.puts " assert_respond_to(#{instance_reference}, :#{column})"
|
553
|
+
fh.puts " assert_nothing_raised{ #{instance_reference}.#{column} }" if testunit
|
554
|
+
end
|
555
|
+
|
556
|
+
case data_type
|
557
|
+
when 'char', 'varchar', 'varchar2'
|
558
|
+
if col.nullable?
|
559
|
+
if testunit
|
560
|
+
fh.puts " assert_kind_of([NilClass, String], #{instance_reference}.#{column})"
|
561
|
+
elsif minitest
|
562
|
+
fh.puts " assert([NilClass, String].include?(#{instance_reference}.#{column}.class))"
|
563
|
+
else # rspec
|
564
|
+
fh.puts " expect(#{instance_reference}.#{column}).to be_a(String).or be_nil"
|
565
|
+
end
|
566
|
+
else
|
567
|
+
if rspec
|
568
|
+
fh.puts " expect(#{instance_reference}.#{column}).to be_a(String)"
|
569
|
+
else
|
570
|
+
fh.puts " assert_kind_of(String, #{instance_reference}.#{column})"
|
571
|
+
end
|
572
|
+
end
|
573
|
+
when 'number'
|
574
|
+
if col.nullable?
|
575
|
+
if testunit
|
576
|
+
fh.puts " assert_kind_of([NilClass, Numeric], #{instance_reference}.#{column})"
|
577
|
+
elsif minitest
|
578
|
+
fh.puts " assert([NilClass, Numeric].include?(#{instance_reference}.#{column}.class))"
|
579
|
+
else # rspec
|
580
|
+
fh.puts " expect(#{instance_reference}.#{column}).to be_a(Numeric).or be_nil"
|
581
|
+
end
|
582
|
+
else
|
583
|
+
if rspec
|
584
|
+
fh.puts " expect(#{instance_reference}.#{column}).to be_a(Numeric)"
|
585
|
+
else
|
586
|
+
fh.puts " assert_kind_of(Numeric, #{instance_reference}.#{column})"
|
587
|
+
end
|
588
|
+
end
|
589
|
+
when 'date'
|
590
|
+
if testunit
|
591
|
+
if col.nullable?
|
592
|
+
fh.puts " assert_kind_of([NilClass, DateTime, Time], #{instance_reference}.#{column})"
|
593
|
+
else
|
594
|
+
fh.puts " assert_kind_of([DateTime, Time], #{instance_reference}.#{column})"
|
595
|
+
end
|
596
|
+
elsif minitest
|
597
|
+
if col.nullable?
|
598
|
+
fh.puts " assert([NilClass, DateTime, Time].include?(#{instance_reference}.#{column}.class))"
|
599
|
+
else
|
600
|
+
fh.puts " assert([DateTime, Time].include?(#{instance_reference}.#{column}.class))"
|
601
|
+
end
|
602
|
+
else # rspec
|
603
|
+
if col.nullable?
|
604
|
+
fh.puts " expect(#{instance_reference}.#{column}).to be_a(DateTime).or be_a(Time).or be_nil"
|
605
|
+
else
|
606
|
+
fh.puts " expect(#{instance_reference}.#{column}).to be_a(DateTime).or be_a(Time)"
|
607
|
+
end
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
if rspec
|
612
|
+
fh.puts " end\n"
|
613
|
+
else
|
614
|
+
fh.puts " end\n"
|
615
|
+
end
|
616
|
+
|
617
|
+
case data_type
|
618
|
+
when 'char', 'varchar', 'varchar2'
|
619
|
+
if testunit
|
620
|
+
test_title = "\n test '#{column} must be a string"
|
621
|
+
test_title += " if present" if col.nullable?
|
622
|
+
test_title += "' do"
|
623
|
+
elsif minitest
|
624
|
+
test_title = "\n def test_#{column}_must_be_a_string"
|
625
|
+
test_title += "_if_present" if col.nullable?
|
626
|
+
else # rspec
|
627
|
+
test_title = " it 'validates #{column} as a string"
|
628
|
+
test_title += " if present" if col.nullable?
|
629
|
+
test_title += "' do"
|
630
|
+
end
|
631
|
+
|
632
|
+
fh.puts test_title
|
633
|
+
|
634
|
+
if rspec
|
635
|
+
fh.puts " #{instance_reference}.#{column} = #{rand(100)}"
|
636
|
+
fh.puts " expect(#{instance_reference}).not_to be_valid"
|
637
|
+
fh.puts " expect(#{instance_reference}.errors[:#{column}]).to include('is not a string')"
|
638
|
+
else
|
639
|
+
fh.puts " #{instance_reference}.#{column} = #{rand(100)}"
|
640
|
+
if testunit
|
641
|
+
fh.puts " assert_false(#{instance_reference}.valid?)"
|
642
|
+
fh.puts " assert_true(#{instance_reference}.errors[:#{column}].include?('is not a string'))"
|
643
|
+
else
|
644
|
+
fh.puts " assert(!#{instance_reference}.valid?)"
|
645
|
+
fh.puts " assert(#{instance_reference}.errors[:#{column}].include?('is not a string'))"
|
646
|
+
end
|
647
|
+
end
|
648
|
+
fh.puts " end\n"
|
649
|
+
|
650
|
+
max_len = col.data_size
|
651
|
+
err_msg = "is too long (maximum is #{max_len} characters)"
|
652
|
+
|
653
|
+
if testunit
|
654
|
+
fh.puts "\n test '#{column} cannot exceed #{max_len} characters' do"
|
655
|
+
elsif minitest
|
656
|
+
fh.puts "\n def test_#{column}_cannot_exceed_#{max_len}_characters"
|
657
|
+
else # rspec
|
658
|
+
fh.puts " it 'validates #{column} length cannot exceed #{max_len} characters' do"
|
659
|
+
end
|
660
|
+
|
661
|
+
if rspec
|
662
|
+
fh.puts " #{instance_reference}.#{column} = 'a' * #{max_len + 1}"
|
663
|
+
fh.puts " expect(#{instance_reference}).not_to be_valid"
|
664
|
+
fh.puts " expect(#{instance_reference}.errors[:#{column}]).to include('#{err_msg}')"
|
665
|
+
else
|
666
|
+
fh.puts " #{instance_reference}.#{column} = 'a' * #{max_len + 1}"
|
667
|
+
if testunit
|
668
|
+
fh.puts " assert_false(#{instance_reference}.valid?)"
|
669
|
+
fh.puts " assert_true(#{instance_reference}.errors[:#{column}].include?('#{err_msg}'))"
|
670
|
+
else
|
671
|
+
fh.puts " assert(!#{instance_reference}.valid?)"
|
672
|
+
fh.puts " assert(#{instance_reference}.errors[:#{column}].include?('#{err_msg}'))"
|
673
|
+
end
|
674
|
+
end
|
675
|
+
fh.puts " end\n"
|
676
|
+
when 'number'
|
677
|
+
if testunit
|
678
|
+
test_title = "\n test '#{column} must be a number"
|
679
|
+
test_title += " if present" if col.nullable?
|
680
|
+
test_title += "' do"
|
681
|
+
elsif minitest
|
682
|
+
test_title = "\n def test_#{column}_must_be_a_number"
|
683
|
+
test_title += "_if_present" if col.nullable?
|
684
|
+
else # rspec
|
685
|
+
test_title = " it 'validates #{column} as a number"
|
686
|
+
test_title += " if present" if col.nullable?
|
687
|
+
test_title += "' do"
|
688
|
+
end
|
689
|
+
|
690
|
+
fh.puts test_title
|
691
|
+
|
692
|
+
if rspec
|
693
|
+
fh.puts " #{instance_reference}.#{column} = 'test_string'"
|
694
|
+
fh.puts " expect(#{instance_reference}).not_to be_valid"
|
695
|
+
fh.puts " expect(#{instance_reference}.errors[:#{column}]).to include('is not a number')"
|
696
|
+
else
|
697
|
+
fh.puts " #{instance_reference}.#{column} = 'test_string'"
|
698
|
+
if testunit
|
699
|
+
fh.puts " assert_false(#{instance_reference}.valid?)"
|
700
|
+
fh.puts " assert_true(#{instance_reference}.errors[:#{column}].include?('is not a number'))"
|
701
|
+
else
|
702
|
+
fh.puts " assert(!#{instance_reference}.valid?)"
|
703
|
+
fh.puts " assert(#{instance_reference}.errors[:#{column}].include?('is not a number'))"
|
704
|
+
end
|
705
|
+
end
|
706
|
+
|
707
|
+
fh.puts " end\n"
|
708
|
+
|
709
|
+
max = "9" * col.precision
|
710
|
+
max.insert(col.precision - col.scale, ".") if col.scale > 0
|
711
|
+
err_msg = "must be less than or equal to #{max}"
|
712
|
+
|
713
|
+
if testunit
|
714
|
+
fh.puts "\n test '#{column} cannot exceed the value #{max}' do"
|
715
|
+
elsif minitest
|
716
|
+
fh.puts "\n def test_#{column}_cannot_exceed_the_value_#{max.sub('.', '_')}"
|
717
|
+
else # rspec
|
718
|
+
fh.puts " it 'validates #{column} cannot exceed the value #{max}' do"
|
719
|
+
end
|
720
|
+
|
721
|
+
if col.scale > 0
|
722
|
+
value_to_set = "#{max.to_f.round}"
|
723
|
+
else
|
724
|
+
value_to_set = "#{max.to_i + 1}"
|
725
|
+
end
|
726
|
+
|
727
|
+
if rspec
|
728
|
+
fh.puts " #{instance_reference}.#{column} = #{value_to_set}"
|
729
|
+
fh.puts " expect(#{instance_reference}).not_to be_valid"
|
730
|
+
fh.puts " expect(#{instance_reference}.errors[:#{column}]).to include('#{err_msg}')"
|
731
|
+
else
|
732
|
+
fh.puts " #{instance_reference}.#{column} = #{value_to_set}"
|
733
|
+
if testunit
|
734
|
+
fh.puts " assert_false(#{instance_reference}.valid?)"
|
735
|
+
fh.puts " assert_true(#{instance_reference}.errors[:#{column}].include?('#{err_msg}'))"
|
736
|
+
else
|
737
|
+
fh.puts " assert(!#{instance_reference}.valid?)"
|
738
|
+
fh.puts " assert(#{instance_reference}.errors[:#{column}].include?('#{err_msg}'))"
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
fh.puts " end\n"
|
743
|
+
|
744
|
+
err_msg = "must be greater than or equal to -#{max}"
|
745
|
+
|
746
|
+
if testunit
|
747
|
+
fh.puts "\n test '#{column} cannot be less than the value -#{max}' do"
|
748
|
+
elsif minitest
|
749
|
+
fh.puts "\n def test_#{column}_cannot_be_less_than_the_value_#{max.sub('.', '_')}"
|
750
|
+
else # rspec
|
751
|
+
fh.puts " it 'validates #{column} cannot be less than the value -#{max}' do"
|
752
|
+
end
|
753
|
+
|
754
|
+
if col.scale > 0
|
755
|
+
neg_value = "-#{max.to_f.round}"
|
756
|
+
else
|
757
|
+
neg_value = "-#{max.to_i + 1}"
|
758
|
+
end
|
759
|
+
|
760
|
+
if rspec
|
761
|
+
fh.puts " #{instance_reference}.#{column} = #{neg_value}"
|
762
|
+
fh.puts " expect(#{instance_reference}).not_to be_valid"
|
763
|
+
fh.puts " expect(#{instance_reference}.errors[:#{column}]).to include('#{err_msg}')"
|
764
|
+
else
|
765
|
+
fh.puts " #{instance_reference}.#{column} = #{neg_value}"
|
766
|
+
if testunit
|
767
|
+
fh.puts " assert_false(#{instance_reference}.valid?)"
|
768
|
+
fh.puts " assert_true(#{instance_reference}.errors[:#{column}].include?('#{err_msg}'))"
|
769
|
+
else
|
770
|
+
fh.puts " assert(!#{instance_reference}.valid?)"
|
771
|
+
fh.puts " assert(#{instance_reference}.errors[:#{column}].include?('#{err_msg}'))"
|
772
|
+
end
|
773
|
+
end
|
774
|
+
fh.puts " end\n"
|
775
|
+
end
|
776
|
+
|
777
|
+
unless col.nullable?
|
778
|
+
err_msg = "can't be blank"
|
779
|
+
if testunit
|
780
|
+
fh.puts "\n test '#{column} cannot be nil' do"
|
781
|
+
elsif minitest
|
782
|
+
fh.puts "\n def test_#{column}_cannot_be_nil"
|
783
|
+
else # rspec
|
784
|
+
fh.puts " it 'validates #{column} presence' do"
|
785
|
+
end
|
786
|
+
|
787
|
+
if rspec
|
788
|
+
fh.puts " #{instance_reference}.#{column} = nil"
|
789
|
+
fh.puts " expect(#{instance_reference}).not_to be_valid"
|
790
|
+
fh.puts " expect(#{instance_reference}.errors[:#{column}]).to include(\"#{err_msg}\")"
|
791
|
+
else
|
792
|
+
fh.puts " #{instance_reference}.#{column} = nil"
|
793
|
+
if testunit
|
794
|
+
fh.puts " assert_false(#{instance_reference}.valid?)"
|
795
|
+
fh.puts " assert_true(#{instance_reference}.errors[:#{column}].include?(\"#{err_msg}\"))"
|
796
|
+
else
|
797
|
+
fh.puts " assert(!#{instance_reference}.valid?)"
|
798
|
+
fh.puts " assert(#{instance_reference}.errors[:#{column}].include?(\"#{err_msg}\"))"
|
799
|
+
end
|
800
|
+
end
|
801
|
+
fh.puts " end\n"
|
802
|
+
end
|
803
|
+
|
804
|
+
if rspec
|
805
|
+
fh.puts " end\n" # Close the describe block for this column
|
806
|
+
end
|
807
|
+
}
|
808
|
+
|
809
|
+
if rspec
|
810
|
+
# RSpec doesn't need teardown - using let handles cleanup automatically
|
811
|
+
else
|
812
|
+
fh.puts "\n def teardown"
|
813
|
+
fh.puts " @#{setup_var} = nil"
|
814
|
+
fh.puts " end"
|
815
|
+
end
|
816
|
+
|
817
|
+
fh.puts "end"
|
818
|
+
end
|
819
|
+
puts "\nTest file '#{test_file}' generated\n"
|
820
|
+
else
|
821
|
+
if opts['x'] != 'none'
|
822
|
+
puts "\nInvalid option to -x/--tests: " + opts['x']
|
823
|
+
puts "Valid options are: testunit, minitest, rspec, or none"
|
824
|
+
else
|
825
|
+
puts "\nSkipping test file generation.\n"
|
826
|
+
end
|
827
|
+
end
|
828
|
+
|
829
|
+
omg.disconnect if omg.respond_to?(:disconnect)
|
830
|
+
|
831
|
+
puts "\nFile '#{ofile}' generated\n\n"
|