-
Notifications
You must be signed in to change notification settings - Fork 92
/
migration_generator.rdoctest
736 lines (565 loc) · 20.6 KB
/
migration_generator.rdoctest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
# HoboFields - Migration Generator
Note that these doctests are good tests but not such good docs. The migration generator doesn't really fit well with the doctest concept of a single IRB session. As you'll see, there's a lot of jumping-through-hoops and doing stuff that no normal user of the migration generator would ever do.
{.hidden}
Firstly, in order to test the migration generator outside of a full Rails stack, there's a few things we need to do. First off we need to configure ActiveSupport for auto-loading
{.hidden}
>> require 'rubygems'
>> require 'active_support'
>> require 'active_record'
{.hidden}
We also need to get ActiveRecord set up with a database connection
{.hidden}
>> mysql_adapter = defined?(JRUBY_VERSION) ? 'jdbcmysql' : 'mysql'
>> mysql_user = 'root'; mysql_password = ''
>> mysql_login = "-u #{mysql_user} --password='#{mysql_password}'"
>> mysql_database = "hobofields_doctest"
>> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
>> system("mysqladmin #{mysql_login} create #{mysql_database}") or raise "could not create database"
>> ActiveRecord::Base.establish_connection(:adapter => mysql_adapter,
:database => mysql_database,
:host => "localhost",
:username => mysql_user,
:password => mysql_password)
{.hidden}
Some load path manipulation you shouldn't need:
{.hidden}
>> $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '../../hobofields/lib')
>> $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '../../hobosupport/lib')
{.hidden}
And we'll require:
{.hidden}
>> require 'hobosupport'
>> require 'hobofields'
>> HoboFields.enable
## The migration generator -- introduction
The migration generator works by:
* Loading all of the models in your Rails app
* Using the Rails schema-dumper to extract information about the current state of the database.
* Calculating the changes that are required to bring the database into sync with your application.
Normally you would run the migration generator as a regular Rails generator. You would type
$ script/generator hobo_migration
in your Rails app, and the migration file would be created in `db/migrate`.
In order to demonstrate the generator in this doctest script however, we'll be using the Ruby API instead. The method `HoboFields::MigrationGenerator.run` returns a pair of strings -- the up migration and the down migration.
At the moment the database is empty and no ActiveRecord models exist, so the generator is going to tell us there is nothing to do.
>> HoboFields::MigrationGenerator.run
=> ["", ""]
### Models without `fields do` are ignored
The migration generator only takes into account classes that use HoboFields, i.e. classes with a `fields do` declaration. Models without this are ignored:
>> class Advert < ActiveRecord::Base; end
>> HoboFields::MigrationGenerator.run
=> ["", ""]
You can also tell HoboFields to ignore additional tables. You can place this command in your environment.rb or elsewhere:
>> HoboFields::MigrationGenerator.ignore_tables = ["green_fishes"]
### Create the table
Here we see a simple `create_table` migration along with the `drop_table` down migration
>>
class Advert < ActiveRecord::Base
fields do
name :string
end
end
>> up, down = HoboFields::MigrationGenerator.run
>> up
=>
"create_table :adverts do |t|
t.string :name
end"
>> down
=> "drop_table :adverts"
Normally we would run the generated migration with `rake db:create`. We can achieve the same effect directly in Ruby like this:
>> ActiveRecord::Migration.class_eval up
>> Advert.columns.*.name
=> ["id", "name"]
We'll define a method to make that easier next time
>>
def migrate(renames={})
up, down = HoboFields::MigrationGenerator.run(renames)
puts up
ActiveRecord::Migration.class_eval(up)
ActiveRecord::Base.send(:subclasses).each { |model| model.reset_column_information }
[up, down]
end
We'll have a look at the migration generator in more detail later, first we'll have a look at the extra features HoboFields has added to the model.
### Add fields
If we add a new field to the model, the migration generator will add it to the database.
>>
class Advert
fields do
name :string
body :text
published_at :datetime
end
end
>> up, down = migrate
>> up
=>
"add_column :adverts, :body, :text
add_column :adverts, :published_at, :datetime"
>> down
=>
"remove_column :adverts, :body
remove_column :adverts, :published_at"
>>
### Remove fields
If we remove a field from the model, the migration generator removes the database column. Note that we have to explicitly clear the known fields to achieve this in rdoctest -- in a Rails context you would simply edit the file
>> Advert.field_specs.clear # not normally needed
class Advert < ActiveRecord::Base
fields do
name :string
body :text
end
end
>> up, down = migrate
>> up
=> "remove_column :adverts, :published_at"
>> down
=> "add_column :adverts, :published_at, :datetime"
### Rename a field
Here we rename the `name` field to `title`. By default the generator sees this as removing `name` and adding `title`.
>> Advert.field_specs.clear # not normally needed
class Advert < ActiveRecord::Base
fields do
title :string
body :text
end
end
>> # Just generate - don't run the migration:
>> up, down = HoboFields::MigrationGenerator.run
>> up
=>
"add_column :adverts, :title, :string
remove_column :adverts, :name"
>> down
=>""
remove_column :adverts, :title
add_column :adverts, :name, :string
>>
When run as a generator, the migration-generator won't make this assumption. Instead it will prompt for user input to resolve the ambiguity. When using the Ruby API, we can ask for a rename instead of an add + drop by passing in a hash:
>> up, down = HoboFields::MigrationGenerator.run(:adverts => { :name => :title })
>> up
=> "rename_column :adverts, :name, :title"
>> down
=> "rename_column :adverts, :title, :name"
Let's apply that change to the database
>> migrate
### Change a type
>> Advert.attr_type :title
=> String
>>
class Advert
fields do
title :text
body :text
end
end
>> up, down = HoboFields::MigrationGenerator.run
>> up
=> "change_column :adverts, :title, :text, :limit => nil"
>> down
=> "change_column :adverts, :title, :string"
### Add a default
>>
class Advert
fields do
title :string, :default => "Untitled"
body :text
end
end
>> up, down = migrate
>> up.split(',').slice(0,3).join(',')
=> 'change_column :adverts, :title, :string'
>> up.split(',').slice(3,2).sort.join(',')
=> ' :default => "Untitled", :limit => 255'
>> down
=> "change_column :adverts, :title, :string"
### Limits
>>
class Advert
fields do
price :integer, :limit => 2
end
end
>> up, down = HoboFields::MigrationGenerator.run
>> up
=> "add_column :adverts, :price, :integer, :limit => 2"
Note that limit on a decimal column is ignored (use :scale and :precision)
>>
class Advert
fields do
price :decimal, :limit => 4
end
end
>> up, down = HoboFields::MigrationGenerator.run
>> up
=> "add_column :adverts, :price, :decimal"
Cleanup
{.hidden}
>> Advert.field_specs.delete :price
{.hidden}
### Foreign Keys
HoboFields extends the `belongs_to` macro so that it also declares the
foreign-key field. It also generates an index on the field.
>>
class Advert
belongs_to :category
end
>> up, down = HoboFields::MigrationGenerator.run
>> up
=>
"add_column :adverts, :category_id, :integer
add_index :adverts, [:category_id]"
>> down
=>
"remove_column :adverts, :category_id
remove_index :adverts, :name => :index_adverts_on_category_id rescue ActiveRecord::StatementInvalid"
Cleanup:
{.hidden}
>> Advert.field_specs.delete(:category_id)
>> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
{.hidden}
If you specify a custom foreign key, the migration generator observes that:
>>
class Advert
belongs_to :category, :foreign_key => "c_id"
end
>> up, down = HoboFields::MigrationGenerator.run
>> up
=>
"add_column :adverts, :c_id, :integer
add_index :adverts, [:c_id]"
Cleanup:
{.hidden}
>> Advert.field_specs.delete(:c_id)
>> Advert.index_specs.delete_if {|spec| spec.fields==["c_id"]}
{.hidden}
You can avoid generating the index by specifying `:index => false`
>>
class Advert
belongs_to :category, :index => false
end
>> up, down = HoboFields::MigrationGenerator.run
>> up
=> "add_column :adverts, :category_id, :integer"
Cleanup:
{.hidden}
>> Advert.field_specs.delete(:category_id)
>> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
{.hidden}
You can specify the index name with :index
>>
class Advert
belongs_to :category, :index => 'my_index'
end
>> up, down = HoboFields::MigrationGenerator.run
>> up
=>
"add_column :adverts, :category_id, :integer
add_index :adverts, [:category_id], :name => 'my_index'"
Cleanup:
{.hidden}
>> Advert.field_specs.delete(:category_id)
>> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
{.hidden}
### Timestamps
`updated_at` and `created_at` can be declared with the shorthand `timestamps`
>>
class Advert
fields do
timestamps
end
end
>> up, down = HoboFields::MigrationGenerator.run
>> up
=>
"add_column :adverts, :created_at, :datetime
add_column :adverts, :updated_at, :datetime"
>> down
=>
"remove_column :adverts, :created_at
remove_column :adverts, :updated_at"
>>
Cleanup:
{.hidden}
>> Advert.field_specs.delete(:updated_at)
>> Advert.field_specs.delete(:created_at)
{.hidden}
### Indices
You can add an index to a field definition
>>
class Advert
fields do
title :string, :index => true
end
end
>> up, down = HoboFields::MigrationGenerator.run
>> up.split("\n")[2]
=> 'add_index :adverts, [:title]'
Cleanup:
{.hidden}
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
{.hidden}
You can ask for a unique index
>>
class Advert
fields do
title :string, :index => true, :unique => true
end
end
>> up, down = HoboFields::MigrationGenerator.run
>> up.split("\n")[2]
=> 'add_index :adverts, [:title], :unique => true'
Cleanup:
{.hidden}
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
{.hidden}
You can specify the name for the index
>>
class Advert
fields do
title :string, :index => 'my_index'
end
end
>> up, down = HoboFields::MigrationGenerator.run
>> up.split("\n")[2]
=> "add_index :adverts, [:title], :name => 'my_index'"
Cleanup:
{.hidden}
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
{.hidden}
You can ask for an index outside of the fields block
>>
class Advert
index :title
end
>> up, down = HoboFields::MigrationGenerator.run
>> up.split("\n")[2]
=> "add_index :adverts, [:title]"
Cleanup:
{.hidden}
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
{.hidden}
The available options for the index function are `:unique` and `:name`
>>
class Advert
index :title, :unique => true, :name => 'my_index'
end
>> up, down = HoboFields::MigrationGenerator.run
>> up.split("\n")[2]
=> "add_index :adverts, [:title], :unique => true, :name => 'my_index'"
Cleanup:
{.hidden}
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
{.hidden}
You can create an index on more than one field
>>
class Advert
index [:title, :category_id]
end
>> up, down = HoboFields::MigrationGenerator.run
>> up.split("\n")[2]
=> "add_index :adverts, [:title, :category_id]"
Cleanup:
{.hidden}
>> Advert.index_specs.delete_if {|spec| spec.fields==["title", "category_id"]}
{.hidden}
Finally, you can specify that the migration generator should completely ignore an index by passing its name to ignore_index in the model. This is helpful for preserving indices that can't be automatically generated, such as prefix indices in MySQL.
### Rename a table
The migration generator respects the `set_table_name` declaration, although as before, we need to explicitly tell the generator that we want a rename rather than a create and a drop.
>>
class Advert
set_table_name "ads"
fields do
title :string, :default => "Untitled"
body :text
end
end
>> up, down = HoboFields::MigrationGenerator.run(:adverts => :ads)
>> up
=> "rename_table :adverts, :ads"
>> down
=> "rename_table :ads, :adverts"
Set the table name back to what it should be and confirm we're in sync:
>> class Advert; set_table_name "adverts"; end
>> HoboFields::MigrationGenerator.run
=> ["", ""]
### Rename a table
As with renaming columns, we have to tell the migration generator about the rename. Here we create a new class 'Advertisement', and tell ActiveRecord to forget about the Advert class. This requires code that shouldn't be shown to impressionable children.
{.hidden}
>>
def nuke_model_class(klass)
ActiveRecord::Base.instance_eval { class_variable_get('@@subclasses')[klass.superclass].delete(klass) }
Object.instance_eval { remove_const klass.name.to_sym }
end
>> nuke_model_class(Advert)
{.hidden}
>>
class Advertisement < ActiveRecord::Base
fields do
title :string, :default => "Untitled"
body :text
end
end
>> up, down = HoboFields::MigrationGenerator.run(:adverts => :advertisements)
>> up
=> "rename_table :adverts, :advertisements"
>> down
=> "rename_table :advertisements, :adverts"
### Drop a table
>> nuke_model_class(Advertisement)
{.hidden}
If you delete a model, the migration generator will create a `drop_table` migration.
Dropping tables is where the automatic down-migration really comes in handy:
>> up, down = HoboFields::MigrationGenerator.run
>> up
=> "drop_table :adverts"
>> down
=>
"create_table "adverts", :force => true do |t|
t.text "body"
t.string "title", :default => "Untitled"
end"
## STI
### Adding an STI subclass
Adding a subclass or two should introduce the 'type' column and no other changes
>>
class Advert < ActiveRecord::Base
fields do
body :text
title :string, :default => "Untitled"
end
end
class FancyAdvert < Advert
end
class SuperFancyAdvert < FancyAdvert
end
>> up, down = HoboFields::MigrationGenerator.run
>> up
=>
"add_column :adverts, :type, :string
add_index :adverts, [:type]"
>> down
=>
"remove_column :adverts, :type
remove_index :adverts, :name => :index_adverts_on_type rescue ActiveRecord::StatementInvalid"
Cleanup
{.hidden}
>> Advert.field_specs.delete(:type)
>> nuke_model_class(SuperFancyAdvert)
>> nuke_model_class(FancyAdvert)
>> Advert.index_specs.delete_if {|spec| spec.fields==["type"]}
{.hidden}
## Coping with multiple changes
The migration generator is designed to create complete migrations even if many changes to the models have taken place.
First let's confirm we're in a known state. One model, 'Advert', with a string 'title' and text 'body':
>> Advert.connection.tables
=> ["adverts"]
>> Advert.columns.*.name
=> ["id", "body", "title"]
>> HoboFields::MigrationGenerator.run
=> ["", ""]
### Rename a column and change the default
>> Advert.field_specs.clear
>>
class Advert
fields do
name :string, :default => "No Name"
body :text
end
end
>> up, down = HoboFields::MigrationGenerator.run(:adverts => {:title => :name})
>> up
=>
"rename_column :adverts, :title, :name
change_column :adverts, :name, :string, :default => "No Name", :limit => 255"
>> down
=>
'rename_column :adverts, :name, :title
change_column :adverts, :title, :string, :default => "Untitled"'
### Rename a table and add a column
>> nuke_model_class(Advert)
{.hidden}
>>
class Ad < ActiveRecord::Base
fields do
title :string, :default => "Untitled"
body :text
created_at :datetime
end
end
>> up, down = HoboFields::MigrationGenerator.run(:adverts => :ads)
>> up
=>
"rename_table :adverts, :ads
add_column :ads, :created_at, :datetime"
>>
class Advert < ActiveRecord::Base
fields do
body :text
title :string, :default => "Untitled"
end
end
{.hidden}
## Legacy Keys
HoboFields has some support for legacy keys.
>> Advert.field_specs.clear
>>
class Advert
fields do
name :string, :default => "No Name"
body :text
end
set_primary_key "advert_id"
end
>> up, down = HoboFields::MigrationGenerator.run(:adverts => {:id => :advert_id})
>> up
=>
"rename_column :adverts, :id, :advert_id
>> nuke_model_class(Advert)
>> nuke_model_class(Ad)
>> ActiveRecord::Base.connection.execute "drop table `adverts`;"
{.hidden}
## Comments
Comments can be added to tables and fields with HoboFields.
>>
class Product < ActiveRecord::Base
fields do
name :string, :comment => "short name"
description :string
end
end
>> up, down = HoboFields::MigrationGenerator.run
>> up
=>
'create_table :products do |t|
t.string :name, :comment => "short name"
t.string :description
end'
>> migrate
These comments will be saved to your schema if you have the [column_comments](http://github.com/bryanlarsen/column_comments) plugin installed. If you do not have this plugin installed, the comments will be available by querying `field_specs`:
>> Product.field_specs["name"].comment
=> "short name"
The plugin [activerecord-comments](http://github.com/bryanlarsen/activerecord-comments) may be used to get the comments from the database directly. If the plugin is installed, use this instead:
Product.column("name").comment
Because it will be quite common for people not to have both [column_comments](http://github.com/bryanlarsen/column_comments) and [activerecord-comments](http://github.com/bryanlarsen/activerecord-comments) installed, it is impossible for HoboFields to determine the difference between no previous comment and a previously missing plugin. Therefore, HoboFields will not generate a migration if the only change was to add a comment. HoboFields will generate a migration for a comment change, but only if the plugin is installed.
>> require 'activerecord-comments'
>> # manually add comment as the column_comments plugin would
>> Product.connection.execute "alter table `products` modify `name` varchar(255) default null comment 'short name';"
>>
class Product < ActiveRecord::Base
fields do
name :string, :comment => "Short namex"
description :string, :comment => "Long name"
end
end
>> up, down = HoboFields::MigrationGenerator.run
>> up
=> "change_column :products, :name, :string, :comment => \"Short namex\", :limit => 255"
Cleanup
{.hidden}
>> nuke_model_class(Product)
>> ActiveRecord::Base.connection.execute "drop table `products`;"
{.hidden}
Final Cleanup
{.hidden}
>> system "mysqladmin #{mysql_login} --force drop #{mysql_database} 2> /dev/null"
{.hidden}