hobo_fields 1.3.0.RC
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/CHANGES.txt +38 -0
- data/LICENSE.txt +22 -0
- data/README.txt +8 -0
- data/Rakefile +36 -0
- data/VERSION +1 -0
- data/bin/hobofields +19 -0
- data/hobo_fields.gemspec +31 -0
- data/lib/generators/hobo/migration/USAGE +47 -0
- data/lib/generators/hobo/migration/migration_generator.rb +162 -0
- data/lib/generators/hobo/migration/migrator.rb +445 -0
- data/lib/generators/hobo/migration/templates/migration.rb.erb +9 -0
- data/lib/generators/hobo/model/USAGE +19 -0
- data/lib/generators/hobo/model/model_generator.rb +11 -0
- data/lib/generators/hobo/model/templates/model_injection.rb.erb +18 -0
- data/lib/hobo_fields/extensions/active_record/attribute_methods.rb +48 -0
- data/lib/hobo_fields/extensions/active_record/fields_declaration.rb +21 -0
- data/lib/hobo_fields/field_declaration_dsl.rb +33 -0
- data/lib/hobo_fields/model/field_spec.rb +121 -0
- data/lib/hobo_fields/model/index_spec.rb +47 -0
- data/lib/hobo_fields/model.rb +226 -0
- data/lib/hobo_fields/railtie.rb +13 -0
- data/lib/hobo_fields/sanitize_html.rb +23 -0
- data/lib/hobo_fields/types/email_address.rb +26 -0
- data/lib/hobo_fields/types/enum_string.rb +101 -0
- data/lib/hobo_fields/types/html_string.rb +15 -0
- data/lib/hobo_fields/types/lifecycle_state.rb +16 -0
- data/lib/hobo_fields/types/markdown_string.rb +15 -0
- data/lib/hobo_fields/types/password_string.rb +15 -0
- data/lib/hobo_fields/types/raw_html_string.rb +13 -0
- data/lib/hobo_fields/types/raw_markdown_string.rb +13 -0
- data/lib/hobo_fields/types/serialized_object.rb +15 -0
- data/lib/hobo_fields/types/text.rb +16 -0
- data/lib/hobo_fields/types/textile_string.rb +22 -0
- data/lib/hobo_fields.rb +94 -0
- data/test/api.rdoctest +244 -0
- data/test/doc-only.rdoctest +96 -0
- data/test/generators.rdoctest +53 -0
- data/test/interactive_primary_key.rdoctest +54 -0
- data/test/migration_generator.rdoctest +639 -0
- data/test/migration_generator_comments.rdoctest +75 -0
- data/test/prepare_testapp.rb +8 -0
- data/test/rich_types.rdoctest +394 -0
- metadata +140 -0
@@ -0,0 +1,639 @@
|
|
1
|
+
# HoboFields - Migration Generator
|
2
|
+
|
3
|
+
Our test requires to prepare the testapp:
|
4
|
+
{.hidden}
|
5
|
+
|
6
|
+
doctest_require: 'prepare_testapp'
|
7
|
+
|
8
|
+
{.hidden}
|
9
|
+
|
10
|
+
## The migration generator -- introduction
|
11
|
+
|
12
|
+
The migration generator works by:
|
13
|
+
|
14
|
+
* Loading all of the models in your Rails app
|
15
|
+
* Using the Rails schema-dumper to extract information about the current state of the database.
|
16
|
+
* Calculating the changes that are required to bring the database into sync with your application.
|
17
|
+
|
18
|
+
Normally you would run the migration generator as a regular Rails generator. You would type
|
19
|
+
|
20
|
+
$ script/generator hobo_migration
|
21
|
+
|
22
|
+
in your Rails app, and the migration file would be created in `db/migrate`.
|
23
|
+
|
24
|
+
In order to demonstrate the generator in this doctest script however, we'll be using the Ruby API instead. The method `Generators::Hobo::Migration::Migrator.run` returns a pair of strings -- the up migration and the down migration.
|
25
|
+
|
26
|
+
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.
|
27
|
+
|
28
|
+
>> Generators::Hobo::Migration::Migrator.run
|
29
|
+
=> ["", ""]
|
30
|
+
|
31
|
+
|
32
|
+
### Models without `fields do` are ignored
|
33
|
+
|
34
|
+
The migration generator only takes into account classes that use HoboFields, i.e. classes with a `fields do` declaration. Models without this are ignored:
|
35
|
+
|
36
|
+
>> class Advert < ActiveRecord::Base; end
|
37
|
+
>> Generators::Hobo::Migration::Migrator.run
|
38
|
+
=> ["", ""]
|
39
|
+
|
40
|
+
You can also tell HoboFields to ignore additional tables. You can place this command in your environment.rb or elsewhere:
|
41
|
+
|
42
|
+
>> Generators::Hobo::Migration::Migrator.ignore_tables = ["green_fishes"]
|
43
|
+
|
44
|
+
### Create the table
|
45
|
+
|
46
|
+
Here we see a simple `create_table` migration along with the `drop_table` down migration
|
47
|
+
|
48
|
+
>>
|
49
|
+
class Advert < ActiveRecord::Base
|
50
|
+
fields do
|
51
|
+
name :string
|
52
|
+
end
|
53
|
+
end
|
54
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
55
|
+
>> up
|
56
|
+
=>
|
57
|
+
"create_table :adverts do |t|
|
58
|
+
t.string :name
|
59
|
+
end"
|
60
|
+
>> down
|
61
|
+
=> "drop_table :adverts"
|
62
|
+
|
63
|
+
Normally we would run the generated migration with `rake db:create`. We can achieve the same effect directly in Ruby like this:
|
64
|
+
|
65
|
+
>> ActiveRecord::Migration.class_eval up
|
66
|
+
>> Advert.columns.*.name
|
67
|
+
=> ["id", "name"]
|
68
|
+
|
69
|
+
We'll define a method to make that easier next time
|
70
|
+
|
71
|
+
>>
|
72
|
+
def migrate(renames={})
|
73
|
+
up, down = Generators::Hobo::Migration::Migrator.run(renames)
|
74
|
+
ActiveRecord::Migration.class_eval(up)
|
75
|
+
ActiveRecord::Base.send(:descendants).each { |model| model.reset_column_information }
|
76
|
+
[up, down]
|
77
|
+
end
|
78
|
+
|
79
|
+
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.
|
80
|
+
|
81
|
+
|
82
|
+
### Add fields
|
83
|
+
|
84
|
+
If we add a new field to the model, the migration generator will add it to the database.
|
85
|
+
|
86
|
+
>>
|
87
|
+
class Advert
|
88
|
+
fields do
|
89
|
+
name :string
|
90
|
+
body :text
|
91
|
+
published_at :datetime
|
92
|
+
end
|
93
|
+
end
|
94
|
+
>> up, down = migrate
|
95
|
+
>> up
|
96
|
+
=>
|
97
|
+
"add_column :adverts, :body, :text
|
98
|
+
add_column :adverts, :published_at, :datetime"
|
99
|
+
>> down
|
100
|
+
=>
|
101
|
+
"remove_column :adverts, :body
|
102
|
+
remove_column :adverts, :published_at"
|
103
|
+
>>
|
104
|
+
|
105
|
+
### Remove fields
|
106
|
+
|
107
|
+
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
|
108
|
+
|
109
|
+
>> Advert.field_specs.clear # not normally needed
|
110
|
+
class Advert < ActiveRecord::Base
|
111
|
+
fields do
|
112
|
+
name :string
|
113
|
+
body :text
|
114
|
+
end
|
115
|
+
end
|
116
|
+
>> up, down = migrate
|
117
|
+
>> up
|
118
|
+
=> "remove_column :adverts, :published_at"
|
119
|
+
>> down
|
120
|
+
=> "add_column :adverts, :published_at, :datetime"
|
121
|
+
|
122
|
+
### Rename a field
|
123
|
+
|
124
|
+
Here we rename the `name` field to `title`. By default the generator sees this as removing `name` and adding `title`.
|
125
|
+
|
126
|
+
>> Advert.field_specs.clear # not normally needed
|
127
|
+
class Advert < ActiveRecord::Base
|
128
|
+
fields do
|
129
|
+
title :string
|
130
|
+
body :text
|
131
|
+
end
|
132
|
+
end
|
133
|
+
>> # Just generate - don't run the migration:
|
134
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
135
|
+
>> up
|
136
|
+
=>
|
137
|
+
"add_column :adverts, :title, :string
|
138
|
+
remove_column :adverts, :name"
|
139
|
+
>> down
|
140
|
+
=>""
|
141
|
+
remove_column :adverts, :title
|
142
|
+
add_column :adverts, :name, :string
|
143
|
+
>>
|
144
|
+
|
145
|
+
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:
|
146
|
+
|
147
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run(:adverts => { :name => :title })
|
148
|
+
>> up
|
149
|
+
=> "rename_column :adverts, :name, :title"
|
150
|
+
>> down
|
151
|
+
=> "rename_column :adverts, :title, :name"
|
152
|
+
|
153
|
+
Let's apply that change to the database
|
154
|
+
|
155
|
+
>> migrate
|
156
|
+
|
157
|
+
|
158
|
+
### Change a type
|
159
|
+
|
160
|
+
>> Advert.attr_type :title
|
161
|
+
=> String
|
162
|
+
>>
|
163
|
+
class Advert
|
164
|
+
fields do
|
165
|
+
title :text
|
166
|
+
body :text
|
167
|
+
end
|
168
|
+
end
|
169
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
170
|
+
>> up
|
171
|
+
=> "change_column :adverts, :title, :text, :limit => nil"
|
172
|
+
>> down
|
173
|
+
=> "change_column :adverts, :title, :string"
|
174
|
+
|
175
|
+
|
176
|
+
### Add a default
|
177
|
+
|
178
|
+
>>
|
179
|
+
class Advert
|
180
|
+
fields do
|
181
|
+
title :string, :default => "Untitled"
|
182
|
+
body :text
|
183
|
+
end
|
184
|
+
end
|
185
|
+
>> up, down = migrate
|
186
|
+
>> up.split(',').slice(0,3).join(',')
|
187
|
+
=> 'change_column :adverts, :title, :string'
|
188
|
+
>> up.split(',').slice(3,2).sort.join(',')
|
189
|
+
=> ' :default => "Untitled", :limit => 255'
|
190
|
+
>> down
|
191
|
+
=> "change_column :adverts, :title, :string"
|
192
|
+
|
193
|
+
|
194
|
+
### Limits
|
195
|
+
|
196
|
+
>>
|
197
|
+
class Advert
|
198
|
+
fields do
|
199
|
+
price :integer, :limit => 2
|
200
|
+
end
|
201
|
+
end
|
202
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
203
|
+
>> up
|
204
|
+
=> "add_column :adverts, :price, :integer, :limit => 2"
|
205
|
+
|
206
|
+
Note that limit on a decimal column is ignored (use :scale and :precision)
|
207
|
+
|
208
|
+
>>
|
209
|
+
class Advert
|
210
|
+
fields do
|
211
|
+
price :decimal, :limit => 4
|
212
|
+
end
|
213
|
+
end
|
214
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
215
|
+
>> up
|
216
|
+
=> "add_column :adverts, :price, :decimal"
|
217
|
+
|
218
|
+
Cleanup
|
219
|
+
{.hidden}
|
220
|
+
|
221
|
+
>> Advert.field_specs.delete :price
|
222
|
+
{.hidden}
|
223
|
+
|
224
|
+
|
225
|
+
### Foreign Keys
|
226
|
+
|
227
|
+
HoboFields extends the `belongs_to` macro so that it also declares the
|
228
|
+
foreign-key field. It also generates an index on the field.
|
229
|
+
|
230
|
+
>>
|
231
|
+
class Advert
|
232
|
+
belongs_to :category
|
233
|
+
end
|
234
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
235
|
+
>> up
|
236
|
+
=>
|
237
|
+
"add_column :adverts, :category_id, :integer
|
238
|
+
|
239
|
+
add_index :adverts, [:category_id]"
|
240
|
+
>> down
|
241
|
+
=>
|
242
|
+
"remove_column :adverts, :category_id
|
243
|
+
|
244
|
+
remove_index :adverts, :name => :index_adverts_on_category_id rescue ActiveRecord::StatementInvalid"
|
245
|
+
|
246
|
+
Cleanup:
|
247
|
+
{.hidden}
|
248
|
+
|
249
|
+
>> Advert.field_specs.delete(:category_id)
|
250
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
|
251
|
+
{.hidden}
|
252
|
+
|
253
|
+
If you specify a custom foreign key, the migration generator observes that:
|
254
|
+
|
255
|
+
>>
|
256
|
+
class Advert
|
257
|
+
belongs_to :category, :foreign_key => "c_id"
|
258
|
+
end
|
259
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
260
|
+
>> up
|
261
|
+
=>
|
262
|
+
"add_column :adverts, :c_id, :integer
|
263
|
+
|
264
|
+
add_index :adverts, [:c_id]"
|
265
|
+
|
266
|
+
Cleanup:
|
267
|
+
{.hidden}
|
268
|
+
|
269
|
+
>> Advert.field_specs.delete(:c_id)
|
270
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["c_id"]}
|
271
|
+
{.hidden}
|
272
|
+
|
273
|
+
You can avoid generating the index by specifying `:index => false`
|
274
|
+
|
275
|
+
>>
|
276
|
+
class Advert
|
277
|
+
belongs_to :category, :index => false
|
278
|
+
end
|
279
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
280
|
+
>> up
|
281
|
+
=> "add_column :adverts, :category_id, :integer"
|
282
|
+
|
283
|
+
Cleanup:
|
284
|
+
{.hidden}
|
285
|
+
|
286
|
+
>> Advert.field_specs.delete(:category_id)
|
287
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
|
288
|
+
{.hidden}
|
289
|
+
|
290
|
+
You can specify the index name with :index
|
291
|
+
|
292
|
+
>>
|
293
|
+
class Advert
|
294
|
+
belongs_to :category, :index => 'my_index'
|
295
|
+
end
|
296
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
297
|
+
>> up
|
298
|
+
=>
|
299
|
+
"add_column :adverts, :category_id, :integer
|
300
|
+
|
301
|
+
add_index :adverts, [:category_id], :name => 'my_index'"
|
302
|
+
|
303
|
+
Cleanup:
|
304
|
+
{.hidden}
|
305
|
+
|
306
|
+
>> Advert.field_specs.delete(:category_id)
|
307
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
|
308
|
+
{.hidden}
|
309
|
+
|
310
|
+
### Timestamps
|
311
|
+
|
312
|
+
`updated_at` and `created_at` can be declared with the shorthand `timestamps`
|
313
|
+
|
314
|
+
>>
|
315
|
+
class Advert
|
316
|
+
fields do
|
317
|
+
timestamps
|
318
|
+
end
|
319
|
+
end
|
320
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
321
|
+
>> up
|
322
|
+
=>
|
323
|
+
"add_column :adverts, :created_at, :datetime
|
324
|
+
add_column :adverts, :updated_at, :datetime"
|
325
|
+
>> down
|
326
|
+
=>
|
327
|
+
"remove_column :adverts, :created_at
|
328
|
+
remove_column :adverts, :updated_at"
|
329
|
+
>>
|
330
|
+
|
331
|
+
Cleanup:
|
332
|
+
{.hidden}
|
333
|
+
|
334
|
+
>> Advert.field_specs.delete(:updated_at)
|
335
|
+
>> Advert.field_specs.delete(:created_at)
|
336
|
+
{.hidden}
|
337
|
+
|
338
|
+
### Indices
|
339
|
+
|
340
|
+
You can add an index to a field definition
|
341
|
+
|
342
|
+
>>
|
343
|
+
class Advert
|
344
|
+
fields do
|
345
|
+
title :string, :index => true
|
346
|
+
end
|
347
|
+
end
|
348
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
349
|
+
>> up.split("\n")[2]
|
350
|
+
=> 'add_index :adverts, [:title]'
|
351
|
+
|
352
|
+
Cleanup:
|
353
|
+
{.hidden}
|
354
|
+
|
355
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
|
356
|
+
{.hidden}
|
357
|
+
|
358
|
+
You can ask for a unique index
|
359
|
+
|
360
|
+
>>
|
361
|
+
class Advert
|
362
|
+
fields do
|
363
|
+
title :string, :index => true, :unique => true
|
364
|
+
end
|
365
|
+
end
|
366
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
367
|
+
>> up.split("\n")[2]
|
368
|
+
=> 'add_index :adverts, [:title], :unique => true'
|
369
|
+
|
370
|
+
Cleanup:
|
371
|
+
{.hidden}
|
372
|
+
|
373
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
|
374
|
+
{.hidden}
|
375
|
+
|
376
|
+
You can specify the name for the index
|
377
|
+
|
378
|
+
>>
|
379
|
+
class Advert
|
380
|
+
fields do
|
381
|
+
title :string, :index => 'my_index'
|
382
|
+
end
|
383
|
+
end
|
384
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
385
|
+
>> up.split("\n")[2]
|
386
|
+
=> "add_index :adverts, [:title], :name => 'my_index'"
|
387
|
+
|
388
|
+
Cleanup:
|
389
|
+
{.hidden}
|
390
|
+
|
391
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
|
392
|
+
{.hidden}
|
393
|
+
|
394
|
+
You can ask for an index outside of the fields block
|
395
|
+
|
396
|
+
>>
|
397
|
+
class Advert
|
398
|
+
index :title
|
399
|
+
end
|
400
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
401
|
+
>> up.split("\n")[2]
|
402
|
+
=> "add_index :adverts, [:title]"
|
403
|
+
|
404
|
+
Cleanup:
|
405
|
+
{.hidden}
|
406
|
+
|
407
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
|
408
|
+
{.hidden}
|
409
|
+
|
410
|
+
The available options for the index function are `:unique` and `:name`
|
411
|
+
|
412
|
+
>>
|
413
|
+
class Advert
|
414
|
+
index :title, :unique => true, :name => 'my_index'
|
415
|
+
end
|
416
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
417
|
+
>> up.split("\n")[2]
|
418
|
+
=> "add_index :adverts, [:title], :unique => true, :name => 'my_index'"
|
419
|
+
|
420
|
+
Cleanup:
|
421
|
+
{.hidden}
|
422
|
+
|
423
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
|
424
|
+
{.hidden}
|
425
|
+
|
426
|
+
You can create an index on more than one field
|
427
|
+
|
428
|
+
>>
|
429
|
+
class Advert
|
430
|
+
index [:title, :category_id]
|
431
|
+
end
|
432
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
433
|
+
>> up.split("\n")[2]
|
434
|
+
=> "add_index :adverts, [:title, :category_id]"
|
435
|
+
|
436
|
+
Cleanup:
|
437
|
+
{.hidden}
|
438
|
+
|
439
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["title", "category_id"]}
|
440
|
+
{.hidden}
|
441
|
+
|
442
|
+
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.
|
443
|
+
|
444
|
+
### Rename a table
|
445
|
+
|
446
|
+
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.
|
447
|
+
|
448
|
+
>>
|
449
|
+
class Advert
|
450
|
+
set_table_name "ads"
|
451
|
+
fields do
|
452
|
+
title :string, :default => "Untitled"
|
453
|
+
body :text
|
454
|
+
end
|
455
|
+
end
|
456
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run(:adverts => :ads)
|
457
|
+
>> up
|
458
|
+
=> "rename_table :adverts, :ads"
|
459
|
+
>> down
|
460
|
+
=> "rename_table :ads, :adverts"
|
461
|
+
|
462
|
+
Set the table name back to what it should be and confirm we're in sync:
|
463
|
+
|
464
|
+
>> class Advert; set_table_name "adverts"; end
|
465
|
+
>> Generators::Hobo::Migration::Migrator.run
|
466
|
+
=> ["", ""]
|
467
|
+
|
468
|
+
### Rename a table
|
469
|
+
|
470
|
+
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.
|
471
|
+
{.hidden}
|
472
|
+
|
473
|
+
>>
|
474
|
+
def nuke_model_class(klass)
|
475
|
+
ActiveSupport::DescendantsTracker.instance_eval do
|
476
|
+
class_variable_get('@@direct_descendants')[ActiveRecord::Base].delete(klass)
|
477
|
+
end
|
478
|
+
Object.instance_eval { remove_const klass.name.to_sym }
|
479
|
+
end
|
480
|
+
>> nuke_model_class(Advert)
|
481
|
+
{.hidden}
|
482
|
+
|
483
|
+
>>
|
484
|
+
class Advertisement < ActiveRecord::Base
|
485
|
+
fields do
|
486
|
+
title :string, :default => "Untitled"
|
487
|
+
body :text
|
488
|
+
end
|
489
|
+
end
|
490
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run(:adverts => :advertisements)
|
491
|
+
>> up
|
492
|
+
=> "rename_table :adverts, :advertisements"
|
493
|
+
>> down
|
494
|
+
=> "rename_table :advertisements, :adverts"
|
495
|
+
|
496
|
+
### Drop a table
|
497
|
+
|
498
|
+
>> nuke_model_class(Advertisement)
|
499
|
+
{.hidden}
|
500
|
+
|
501
|
+
If you delete a model, the migration generator will create a `drop_table` migration.
|
502
|
+
|
503
|
+
Dropping tables is where the automatic down-migration really comes in handy:
|
504
|
+
|
505
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
506
|
+
>> up
|
507
|
+
=> "drop_table :adverts"
|
508
|
+
>> down
|
509
|
+
=>
|
510
|
+
"create_table "adverts", :force => true do |t|
|
511
|
+
t.text "body"
|
512
|
+
t.string "title", :default => "Untitled"
|
513
|
+
end"
|
514
|
+
|
515
|
+
## STI
|
516
|
+
|
517
|
+
### Adding an STI subclass
|
518
|
+
|
519
|
+
Adding a subclass or two should introduce the 'type' column and no other changes
|
520
|
+
|
521
|
+
>>
|
522
|
+
class Advert < ActiveRecord::Base
|
523
|
+
fields do
|
524
|
+
body :text
|
525
|
+
title :string, :default => "Untitled"
|
526
|
+
end
|
527
|
+
end
|
528
|
+
class FancyAdvert < Advert
|
529
|
+
end
|
530
|
+
class SuperFancyAdvert < FancyAdvert
|
531
|
+
end
|
532
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
533
|
+
>> up
|
534
|
+
=>
|
535
|
+
"add_column :adverts, :type, :string
|
536
|
+
|
537
|
+
add_index :adverts, [:type]"
|
538
|
+
>> down
|
539
|
+
=>
|
540
|
+
"remove_column :adverts, :type
|
541
|
+
|
542
|
+
remove_index :adverts, :name => :index_adverts_on_type rescue ActiveRecord::StatementInvalid"
|
543
|
+
|
544
|
+
Cleanup
|
545
|
+
{.hidden}
|
546
|
+
|
547
|
+
>> Advert.field_specs.delete(:type)
|
548
|
+
>> nuke_model_class(SuperFancyAdvert)
|
549
|
+
>> nuke_model_class(FancyAdvert)
|
550
|
+
>> Advert.index_specs.delete_if {|spec| spec.fields==["type"]}
|
551
|
+
{.hidden}
|
552
|
+
|
553
|
+
|
554
|
+
## Coping with multiple changes
|
555
|
+
|
556
|
+
The migration generator is designed to create complete migrations even if many changes to the models have taken place.
|
557
|
+
|
558
|
+
First let's confirm we're in a known state. One model, 'Advert', with a string 'title' and text 'body':
|
559
|
+
|
560
|
+
>> Advert.connection.tables
|
561
|
+
=> ["adverts"]
|
562
|
+
>> Advert.columns.*.name
|
563
|
+
=> ["id", "body", "title"]
|
564
|
+
>> Generators::Hobo::Migration::Migrator.run
|
565
|
+
=> ["", ""]
|
566
|
+
|
567
|
+
|
568
|
+
### Rename a column and change the default
|
569
|
+
|
570
|
+
>> Advert.field_specs.clear
|
571
|
+
>>
|
572
|
+
class Advert
|
573
|
+
fields do
|
574
|
+
name :string, :default => "No Name"
|
575
|
+
body :text
|
576
|
+
end
|
577
|
+
end
|
578
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run(:adverts => {:title => :name})
|
579
|
+
>> up
|
580
|
+
=>
|
581
|
+
"rename_column :adverts, :title, :name
|
582
|
+
change_column :adverts, :name, :string, :default => "No Name", :limit => 255"
|
583
|
+
>> down
|
584
|
+
=>
|
585
|
+
'rename_column :adverts, :name, :title
|
586
|
+
change_column :adverts, :title, :string, :default => "Untitled"'
|
587
|
+
|
588
|
+
|
589
|
+
### Rename a table and add a column
|
590
|
+
|
591
|
+
>> nuke_model_class(Advert)
|
592
|
+
{.hidden}
|
593
|
+
|
594
|
+
>>
|
595
|
+
class Ad < ActiveRecord::Base
|
596
|
+
fields do
|
597
|
+
title :string, :default => "Untitled"
|
598
|
+
body :text
|
599
|
+
created_at :datetime
|
600
|
+
end
|
601
|
+
end
|
602
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run(:adverts => :ads)
|
603
|
+
>> up
|
604
|
+
=>
|
605
|
+
"rename_table :adverts, :ads
|
606
|
+
|
607
|
+
add_column :ads, :created_at, :datetime"
|
608
|
+
|
609
|
+
>>
|
610
|
+
class Advert < ActiveRecord::Base
|
611
|
+
fields do
|
612
|
+
body :text
|
613
|
+
title :string, :default => "Untitled"
|
614
|
+
end
|
615
|
+
end
|
616
|
+
{.hidden}
|
617
|
+
|
618
|
+
## Legacy Keys
|
619
|
+
|
620
|
+
HoboFields has some support for legacy keys.
|
621
|
+
|
622
|
+
>> Advert.field_specs.clear
|
623
|
+
>>
|
624
|
+
class Advert
|
625
|
+
fields do
|
626
|
+
name :string, :default => "No Name"
|
627
|
+
body :text
|
628
|
+
end
|
629
|
+
set_primary_key "advert_id"
|
630
|
+
end
|
631
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run(:adverts => {:id => :advert_id})
|
632
|
+
>> up
|
633
|
+
=>
|
634
|
+
"rename_column :adverts, :id, :advert_id
|
635
|
+
|
636
|
+
>> nuke_model_class(Advert)
|
637
|
+
>> nuke_model_class(Ad)
|
638
|
+
>> ActiveRecord::Base.connection.execute "drop table `adverts`;"
|
639
|
+
{.hidden}
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# HoboFields - Migration Generator Comments
|
2
|
+
|
3
|
+
Our test requires to prepare the testapp for a different environment:
|
4
|
+
{.hidden}
|
5
|
+
|
6
|
+
doctest_require: ENV["RAILS_ENV"] = 'mysql_test'; 'prepare_testapp'
|
7
|
+
|
8
|
+
>> system "cd #{TESTAPP_PATH} && rake db:setup"
|
9
|
+
=> true
|
10
|
+
|
11
|
+
>> p Rails.env
|
12
|
+
>>
|
13
|
+
def nuke_model_class(klass)
|
14
|
+
ActiveSupport::DescendantsTracker.instance_eval do
|
15
|
+
class_variable_get('@@direct_descendants')[ActiveRecord::Base].delete(klass)
|
16
|
+
end
|
17
|
+
Object.instance_eval { remove_const klass.name.to_sym }
|
18
|
+
end
|
19
|
+
|
20
|
+
{.hidden}
|
21
|
+
|
22
|
+
|
23
|
+
## Comments
|
24
|
+
|
25
|
+
Comments can be added to tables and fields with HoboFields.
|
26
|
+
|
27
|
+
>>
|
28
|
+
class Product < ActiveRecord::Base
|
29
|
+
fields do
|
30
|
+
name :string, :comment => "short name"
|
31
|
+
description :string
|
32
|
+
end
|
33
|
+
end
|
34
|
+
>> Rails.env
|
35
|
+
=> "mysql_test"
|
36
|
+
>> Rails::Generators.invoke 'hobo:migration', %w(-n -m)
|
37
|
+
|
38
|
+
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`:
|
39
|
+
|
40
|
+
>> Product.field_specs["name"].comment
|
41
|
+
=> "short name"
|
42
|
+
|
43
|
+
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:
|
44
|
+
|
45
|
+
Product.column("name").comment
|
46
|
+
|
47
|
+
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.
|
48
|
+
|
49
|
+
>> require 'activerecord-comments'
|
50
|
+
|
51
|
+
>> # manually add comment as the column_comments plugin would
|
52
|
+
>> Product.connection.execute "alter table `products` modify `name` varchar(255) default null comment 'short name';"
|
53
|
+
|
54
|
+
>>
|
55
|
+
class Product < ActiveRecord::Base
|
56
|
+
fields do
|
57
|
+
name :string, :comment => "Short namex"
|
58
|
+
description :string, :comment => "Long name"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
>> up, down = Generators::Hobo::Migration::Migrator.run
|
62
|
+
>> up
|
63
|
+
>> up.split(',').slice(0,3).join(',')
|
64
|
+
=> 'change_column :products, :name, :string'
|
65
|
+
>> up.split(',').slice(3,2).sort.join(',')
|
66
|
+
=> " :comment => \"Short namex\", :limit => 255"
|
67
|
+
|
68
|
+
|
69
|
+
Cleanup
|
70
|
+
{.hidden}
|
71
|
+
|
72
|
+
>> nuke_model_class(Product)
|
73
|
+
>> ActiveRecord::Base.connection.execute "drop table `products`;"
|
74
|
+
>> system "cd #{TESTAPP_PATH} && rake db:drop RAILS_ENV=mysql_test"
|
75
|
+
{.hidden}
|