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.
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"