declare_schema 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +37 -0
  3. data/CHANGELOG.md +28 -4
  4. data/Gemfile +0 -2
  5. data/Gemfile.lock +1 -4
  6. data/README.md +59 -2
  7. data/Rakefile +13 -20
  8. data/gemfiles/rails_4.gemfile +4 -7
  9. data/gemfiles/rails_5.gemfile +4 -7
  10. data/gemfiles/rails_6.gemfile +4 -7
  11. data/lib/declare_schema/model.rb +0 -1
  12. data/lib/declare_schema/model/field_spec.rb +4 -14
  13. data/lib/declare_schema/version.rb +1 -1
  14. data/lib/generators/declare_schema/migration/migration_generator.rb +20 -13
  15. data/lib/generators/declare_schema/migration/migrator.rb +58 -38
  16. data/lib/generators/declare_schema/migration/templates/migration.rb.erb +1 -1
  17. data/lib/generators/declare_schema/support/eval_template.rb +12 -3
  18. data/lib/generators/declare_schema/support/model.rb +77 -2
  19. data/spec/lib/declare_schema/api_spec.rb +125 -0
  20. data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +8 -4
  21. data/spec/lib/declare_schema/generator_spec.rb +57 -0
  22. data/spec/lib/declare_schema/interactive_primary_key_spec.rb +51 -0
  23. data/spec/lib/declare_schema/migration_generator_spec.rb +686 -0
  24. data/spec/lib/declare_schema/prepare_testapp.rb +29 -0
  25. data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +42 -0
  26. data/spec/spec_helper.rb +26 -0
  27. metadata +9 -12
  28. data/.jenkins/Jenkinsfile +0 -72
  29. data/.jenkins/ruby_build_pod.yml +0 -19
  30. data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +0 -25
  31. data/test/api.rdoctest +0 -136
  32. data/test/doc-only.rdoctest +0 -76
  33. data/test/generators.rdoctest +0 -60
  34. data/test/interactive_primary_key.rdoctest +0 -56
  35. data/test/migration_generator.rdoctest +0 -846
  36. data/test/migration_generator_comments.rdoctestDISABLED +0 -74
  37. data/test/prepare_testapp.rb +0 -15
@@ -1,60 +0,0 @@
1
- doctest: prepare testapp environment
2
- doctest_require: 'prepare_testapp'
3
-
4
- doctest: generate declare_schema:model
5
- >> Rails::Generators.invoke 'declare_schema:model', %w(alpha/beta one:string two:integer)
6
-
7
-
8
- doctest: model file exists
9
- >> File.exist? 'app/models/alpha/beta.rb'
10
- => true
11
-
12
- doctest: model content matches
13
- >> File.read 'app/models/alpha/beta.rb'
14
- => "class Alpha::Beta < #{Rails::VERSION::MAJOR > 4 ? 'ApplicationRecord' : 'ActiveRecord::Base'}\n\n fields do\n one :string, limit: 255\n two :integer\n end\n\nend\n"
15
-
16
- doctest: module file exists
17
- >> File.exist? 'app/models/alpha.rb'
18
- => true
19
-
20
- doctest: module content matches
21
- >> File.read 'app/models/alpha.rb'
22
- => "module Alpha\n def self.table_name_prefix\n 'alpha_'\n end\nend\n"
23
-
24
-
25
- doctest: test file exists
26
- >> File.exist? 'test/models/alpha/beta_test.rb'
27
- => true
28
-
29
- doctest: test content matches
30
- >> File.read 'test/models/alpha/beta_test.rb'
31
- =>
32
- require 'test_helper'
33
-
34
- class Alpha::BetaTest < ActiveSupport::TestCase
35
- # test "the truth" do
36
- # assert true
37
- # end
38
- end
39
-
40
- doctest: fixture file exists
41
- >> File.exist? 'test/fixtures/alpha/beta.yml'
42
- => true
43
-
44
-
45
- doctest: generate declare_schema:migration
46
- >> require_relative "#{Rails.root}/app/models/alpha.rb" if Rails::VERSION::MAJOR > 5
47
- >> require_relative "#{Rails.root}/app/models/alpha/beta.rb" if Rails::VERSION::MAJOR > 5
48
- >> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
49
-
50
- doctest: schema.rb file exists
51
- >> File.exist? 'db/schema.rb'
52
- => true
53
-
54
- doctest: db file exists
55
- >> File.exist? 'db/development.sqlite3'
56
- => true
57
-
58
- doctest: Alpha::Beta class exists
59
- >> Alpha::Beta
60
- # will error if class doesn't exist
@@ -1,56 +0,0 @@
1
- -*- indent-tabs-mode:nil; -*-
2
-
3
- # DeclareSchema - Migration Generator
4
-
5
- Our test requires to prepare the testapp:
6
- {.hidden}
7
-
8
- doctest_require: 'prepare_testapp'
9
-
10
- {.hidden}
11
-
12
- And requires also that you enter the right choice when prompted. OK we're ready to get going.
13
-
14
- ## Alternate Primary Keys
15
-
16
- ### create
17
- doctest: create table with custom primary_key
18
- >>
19
- class Foo < ActiveRecord::Base
20
- fields do
21
- end
22
- self.primary_key="foo_id"
23
- end
24
- >> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
25
- >> Foo.primary_key
26
- => 'foo_id'
27
-
28
- ### migrate from
29
- doctest: rename from custom primary_key
30
- >>
31
- class Foo < ActiveRecord::Base
32
- self.primary_key="id"
33
- end
34
- puts "\n\e[45m Please enter 'id' (no quotes) at the next prompt \e[0m"
35
- >> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
36
- >> Foo.primary_key
37
- => 'id'
38
-
39
- ### migrate to
40
-
41
- doctest: rename to custom primary_key
42
- >>
43
- class Foo < ActiveRecord::Base
44
- self.primary_key="foo_id"
45
- end
46
- puts "\n\e[45m Please enter 'drop id' (no quotes) at the next prompt \e[0m"
47
- >> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
48
- >> Foo.primary_key
49
- => 'foo_id'
50
-
51
- ### ensure it doesn't cause further migrations
52
-
53
- doctest: check no further migrations
54
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
55
- >> up
56
- => ""
@@ -1,846 +0,0 @@
1
- # DeclareSchema - 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
- $ rails generate declare_schema: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::DeclareSchema::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::DeclareSchema::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 DeclareSchema, i.e. classes with a `fields do` declaration. Models without this are ignored:
35
-
36
- >> class Advert < ActiveRecord::Base; end
37
- >> Generators::DeclareSchema::Migration::Migrator.run
38
- => ["", ""]
39
-
40
- You can also tell DeclareSchema to ignore additional tables. You can place this command in your environment.rb or elsewhere:
41
-
42
- >> Generators::DeclareSchema::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, limit: 255, null: true
52
- end
53
- end
54
- >> up, down = Generators::DeclareSchema::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.map(&: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::DeclareSchema::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 DeclareSchema 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, limit: 255, null: true
90
- body :text, null: true
91
- published_at :datetime, null: true
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, limit: 255, null: true
113
- body :text, null: true
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, limit: 255, null: true
130
- body :text, null: true
131
- end
132
- end
133
- >> # Just generate - don't run the migration:
134
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
135
- >> up
136
- => "add_column :adverts, :title, :string, limit: 255
137
- remove_column :adverts, :name"
138
- >> down
139
- => "remove_column :adverts, :title
140
- add_column :adverts, :name, :string, limit: 255"
141
-
142
- 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:
143
-
144
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { name: :title })
145
- >> up
146
- => "rename_column :adverts, :name, :title"
147
- >> down
148
- => "rename_column :adverts, :title, :name"
149
-
150
- Let's apply that change to the database
151
-
152
- >> migrate
153
-
154
-
155
- ### Change a type
156
-
157
- >>
158
- class Advert
159
- fields do
160
- title :text, null: true
161
- body :text, null: true
162
- end
163
- end
164
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
165
- >> up
166
- => "change_column :adverts, :title, :text"
167
- >> down
168
- => "change_column :adverts, :title, :string, limit: 255"
169
-
170
-
171
- ### Add a default
172
-
173
- >>
174
- class Advert
175
- fields do
176
- title :string, default: "Untitled", limit: 255, null: true
177
- body :text, null: true
178
- end
179
- end
180
- >> up, down = migrate
181
- >> up.split(',').slice(0,3).join(',')
182
- => 'change_column :adverts, :title, :string'
183
- >> up.split(',').slice(3,2).sort.join(',')
184
- => " default: \"Untitled\", limit: 255"
185
- >> down
186
- => "change_column :adverts, :title, :string, limit: 255"
187
-
188
-
189
- ### Limits
190
-
191
- >>
192
- class Advert
193
- fields do
194
- price :integer, null: true, limit: 2
195
- end
196
- end
197
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
198
- >> up
199
- => "add_column :adverts, :price, :integer, limit: 2"
200
-
201
- Now run the migration, then change the limit:
202
-
203
- >> ActiveRecord::Migration.class_eval up
204
- >>
205
- class Advert
206
- fields do
207
- price :integer, null: true, limit: 3
208
- end
209
- end
210
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
211
- >> up
212
- => "change_column :adverts, :price, :integer, limit: 3"
213
- >> down
214
- => "change_column :adverts, :price, :integer, limit: 2"
215
-
216
- Note that limit on a decimal column is ignored (use :scale and :precision)
217
-
218
- >> ActiveRecord::Migration.class_eval "remove_column :adverts, :price"
219
- >>
220
- class Advert
221
- fields do
222
- price :decimal, null: true, limit: 4
223
- end
224
- end
225
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
226
- >> up
227
- => "add_column :adverts, :price, :decimal"
228
-
229
- Limits are generally not needed for `text` fields, because by default, `text` fields will use the maximum size
230
- allowed for that database type (0xffffffff for LONGTEXT in MySQL unlimited in Postgres, 1 billion in Sqlite).
231
- If a `limit` is given, it will only be used in MySQL, to choose the smallest TEXT field that will accommodate
232
- that limit (0xff for TINYTEXT, 0xffff for TEXT, 0xffffff for MEDIUMTEXT, 0xffffffff for LONGTEXT).
233
-
234
- >> ::DeclareSchema::Model::FieldSpec.mysql_text_limits?
235
- => false
236
- >>
237
- class Advert
238
- fields do
239
- notes :text
240
- description :text, limit: 30000
241
- end
242
- end
243
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
244
- >> up
245
- => "add_column :adverts, :price, :decimal
246
- add_column :adverts, :notes, :text, null: false
247
- add_column :adverts, :description, :text, null: false"
248
-
249
- (There is no limit on `add_column ... :description` above since these tests are run against SQLite.)
250
-
251
- Cleanup
252
- {.hidden}
253
- >> Advert.field_specs.delete :price
254
- >> Advert.field_specs.delete :notes
255
- >> Advert.field_specs.delete :description
256
- {.hidden}
257
-
258
- In MySQL, limits are applied, rounded up:
259
-
260
- >> ::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, true)
261
- >> ::DeclareSchema::Model::FieldSpec.mysql_text_limits?
262
- => true
263
- >>
264
- class Advert
265
- fields do
266
- notes :text
267
- description :text, limit: 200
268
- end
269
- end
270
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
271
- >> up
272
- => "add_column :adverts, :notes, :text, null: false
273
- add_column :adverts, :description, :text, null: false, limit: 255"
274
-
275
- Cleanup
276
- {.hidden}
277
-
278
- >> Advert.field_specs.delete :notes
279
- {.hidden}
280
-
281
- Limits that are too high will for MySQL will raise an exception.
282
-
283
- >> ::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, true)
284
- >> ::DeclareSchema::Model::FieldSpec.mysql_text_limits?
285
- => true
286
- >>
287
- begin
288
- class Advert
289
- fields do
290
- notes :text
291
- description :text, limit: 0x1_0000_0000
292
- end
293
- end
294
- rescue => ex
295
- "#{ex.class}: #{ex.message}"
296
- end
297
- => "ArgumentError: limit of 4294967296 is too large for MySQL"
298
-
299
- Cleanup
300
- {.hidden}
301
-
302
- >> Advert.field_specs.delete :notes
303
- {.hidden}
304
-
305
- And in MySQL, unstated text limits are treated as the maximum (LONGTEXT) limit.
306
-
307
- To start, we'll set the database schema for `description` to match the above limit of 255.
308
-
309
- >> ::DeclareSchema::Model::FieldSpec.mysql_text_limits?
310
- => true
311
- >> Advert.connection.execute "ALTER TABLE adverts ADD COLUMN description TINYTEXT"
312
- >> Advert.connection.schema_cache.clear!
313
- >> Advert.reset_column_information
314
- >> Advert.connection.tables
315
- => ["adverts"]
316
- >> Advert.columns.map(&:name)
317
- => ["id", "body", "title", "description"]
318
-
319
- Now migrate to an unstated text limit:
320
-
321
- >>
322
- class Advert
323
- fields do
324
- description :text
325
- end
326
- end
327
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
328
- >> up
329
- => "change_column :adverts, :description, :text, null: false"
330
- >> down
331
- => "change_column :adverts, :description, :text"
332
-
333
- TODO TECH-4814: The above test should have this output:
334
- TODO => "change_column :adverts, :description, :text, limit: 255"
335
-
336
-
337
- And migrate to a stated text limit that is the same as the unstated one:
338
-
339
- >>
340
- class Advert
341
- fields do
342
- description :text, limit: 0xffffffff
343
- end
344
- end
345
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
346
- >> up
347
- => "change_column :adverts, :description, :text, null: false"
348
- >> down
349
- => "change_column :adverts, :description, :text"
350
- >> ::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, false)
351
-
352
- Cleanup
353
- {.hidden}
354
- >> Advert.field_specs.clear
355
- >> Advert.connection.schema_cache.clear!
356
- >> Advert.reset_column_information
357
- >>
358
- class Advert < ActiveRecord::Base
359
- fields do
360
- name :string, limit: 255, null: true
361
- end
362
- end
363
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
364
- >> ActiveRecord::Migration.class_eval up
365
- >> Advert.connection.schema_cache.clear!
366
- >> Advert.reset_column_information
367
- {.hidden}
368
-
369
-
370
- ### Foreign Keys
371
-
372
- DeclareSchema extends the `belongs_to` macro so that it also declares the
373
- foreign-key field. It also generates an index on the field.
374
-
375
- >>
376
- class Category < ActiveRecord::Base; end
377
- class Advert
378
- belongs_to :category
379
- end
380
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
381
- >> up.gsub(/\n+/, "\n")
382
- => "add_column :adverts, :category_id, :integer, limit: 8, null: false
383
- add_index :adverts, [:category_id], name: 'on_category_id'"
384
- >> down.sub(/\n+/, "\n")
385
- => "remove_column :adverts, :category_id
386
- remove_index :adverts, name: :on_category_id rescue ActiveRecord::StatementInvalid"
387
-
388
- Cleanup:
389
- {.hidden}
390
-
391
- >> Advert.field_specs.delete(:category_id)
392
- >> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
393
- {.hidden}
394
-
395
- If you specify a custom foreign key, the migration generator observes that:
396
-
397
- >>
398
- class Category < ActiveRecord::Base; end
399
- class Advert
400
- belongs_to :category, foreign_key: "c_id", class_name: 'Category'
401
- end
402
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
403
- >> up.gsub(/\n+/, "\n")
404
- => "add_column :adverts, :c_id, :integer, limit: 8, null: false
405
- add_index :adverts, [:c_id], name: 'on_c_id'"
406
-
407
- Cleanup:
408
- {.hidden}
409
-
410
- >> Advert.field_specs.delete(:c_id)
411
- >> Advert.index_specs.delete_if { |spec| spec.fields==["c_id"] }
412
- {.hidden}
413
-
414
- You can avoid generating the index by specifying `index: false`
415
-
416
- >>
417
- class Category < ActiveRecord::Base; end
418
- class Advert
419
- belongs_to :category, index: false
420
- end
421
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
422
- >> up.gsub(/\n+/, "\n")
423
- => "add_column :adverts, :category_id, :integer, limit: 8, null: false"
424
-
425
- Cleanup:
426
- {.hidden}
427
-
428
- >> Advert.field_specs.delete(:category_id)
429
- >> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
430
- {.hidden}
431
-
432
- You can specify the index name with :index
433
-
434
- >>
435
- class Category < ActiveRecord::Base; end
436
- class Advert
437
- belongs_to :category, index: 'my_index'
438
- end
439
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
440
- >> up.gsub(/\n+/, "\n")
441
- => "add_column :adverts, :category_id, :integer, limit: 8, null: false
442
- add_index :adverts, [:category_id], name: 'my_index'"
443
-
444
- Cleanup:
445
- {.hidden}
446
-
447
- >> Advert.field_specs.delete(:category_id)
448
- >> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
449
- {.hidden}
450
-
451
- ### Timestamps and Optimimistic Locking
452
-
453
- `updated_at` and `created_at` can be declared with the shorthand `timestamps`.
454
- Similarly, `lock_version` can be declared with the "shorthand" `optimimistic_lock`.
455
-
456
- >>
457
- class Advert
458
- fields do
459
- timestamps
460
- optimistic_lock
461
- end
462
- end
463
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
464
- >> up.gsub(/\n+/, "\n")
465
- => "add_column :adverts, :created_at, :datetime
466
- add_column :adverts, :updated_at, :datetime
467
- add_column :adverts, :lock_version, :integer, null: false, default: 1"
468
- >> down.gsub(/\n+/, "\n")
469
- => "remove_column :adverts, :created_at
470
- remove_column :adverts, :updated_at
471
- remove_column :adverts, :lock_version"
472
- >>
473
-
474
- Cleanup:
475
- {.hidden}
476
-
477
- >> Advert.field_specs.delete(:updated_at)
478
- >> Advert.field_specs.delete(:created_at)
479
- >> Advert.field_specs.delete(:lock_version)
480
- {.hidden}
481
-
482
- ### Indices
483
-
484
- You can add an index to a field definition
485
-
486
- >>
487
- class Advert
488
- fields do
489
- title :string, index: true, limit: 255, null: true
490
- end
491
- end
492
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
493
- >> up.gsub(/\n+/, "\n")
494
- => "add_column :adverts, :title, :string, limit: 255
495
- add_index :adverts, [:title], name: 'on_title'"
496
-
497
- Cleanup:
498
- {.hidden}
499
-
500
- >> Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
501
- {.hidden}
502
-
503
- You can ask for a unique index
504
-
505
- >>
506
- class Advert
507
- fields do
508
- title :string, index: true, unique: true, null: true, limit: 255
509
- end
510
- end
511
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
512
- >> up.gsub(/\n+/, "\n")
513
- => "add_column :adverts, :title, :string, limit: 255
514
- add_index :adverts, [:title], unique: true, name: 'on_title'"
515
-
516
- Cleanup:
517
- {.hidden}
518
-
519
- >> Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
520
- {.hidden}
521
-
522
- You can specify the name for the index
523
-
524
- >>
525
- class Advert
526
- fields do
527
- title :string, index: 'my_index', limit: 255, null: true
528
- end
529
- end
530
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
531
- >> up.gsub(/\n+/, "\n")
532
- => "add_column :adverts, :title, :string, limit: 255
533
- add_index :adverts, [:title], name: 'my_index'"
534
-
535
- Cleanup:
536
- {.hidden}
537
-
538
- >> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
539
- {.hidden}
540
-
541
- You can ask for an index outside of the fields block
542
-
543
- >>
544
- class Advert
545
- index :title
546
- end
547
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
548
- >> up.gsub(/\n+/, "\n")
549
- => "add_column :adverts, :title, :string, limit: 255
550
- add_index :adverts, [:title], name: 'on_title'"
551
-
552
- Cleanup:
553
- {.hidden}
554
-
555
- >> Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
556
- {.hidden}
557
-
558
- The available options for the index function are `:unique` and `:name`
559
-
560
- >>
561
- class Advert
562
- index :title, unique: true, name: 'my_index'
563
- end
564
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
565
- >> up.gsub(/\n+/, "\n")
566
- => "add_column :adverts, :title, :string, limit: 255
567
- add_index :adverts, [:title], unique: true, name: 'my_index'"
568
-
569
- Cleanup:
570
- {.hidden}
571
-
572
- >> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
573
- {.hidden}
574
-
575
- You can create an index on more than one field
576
-
577
- >>
578
- class Advert
579
- index [:title, :category_id]
580
- end
581
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
582
- >> up.gsub(/\n+/, "\n")
583
- => "add_column :adverts, :title, :string, limit: 255
584
- add_index :adverts, [:title, :category_id], name: 'on_title_and_category_id'"
585
-
586
- Cleanup:
587
- {.hidden}
588
-
589
- >> Advert.index_specs.delete_if { |spec| spec.fields==["title", "category_id"] }
590
- {.hidden}
591
-
592
- 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.
593
-
594
- ### Rename a table
595
-
596
- 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.
597
-
598
- >>
599
- class Advert
600
- self.table_name="ads"
601
- fields do
602
- title :string, limit: 255, null: true
603
- body :text, null: true
604
- end
605
- end
606
-
607
- >> Advert.connection.schema_cache.clear!
608
- >> Advert.reset_column_information
609
-
610
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run("adverts" => "ads")
611
- >> up.gsub(/\n+/, "\n")
612
- => "rename_table :adverts, :ads
613
- add_column :ads, :title, :string, limit: 255
614
- add_column :ads, :body, :text
615
- add_index :ads, [:id], unique: true, name: 'PRIMARY_KEY'"
616
- >> down.gsub(/\n+/, "\n")
617
- => "remove_column :ads, :title
618
- remove_column :ads, :body
619
- rename_table :ads, :adverts
620
- add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'"
621
-
622
- Set the table name back to what it should be and confirm we're in sync:
623
-
624
- >> Advert.field_specs.delete(:title)
625
- >> Advert.field_specs.delete(:body)
626
- >> class Advert; self.table_name="adverts"; end
627
- >> Generators::DeclareSchema::Migration::Migrator.run
628
- => ["", ""]
629
-
630
- ### Rename a table
631
-
632
- 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.
633
- {.hidden}
634
-
635
- >>
636
- def nuke_model_class(klass)
637
- ActiveSupport::DescendantsTracker.instance_eval do
638
- direct_descendants = class_variable_get('@@direct_descendants')
639
- direct_descendants[ActiveRecord::Base] = direct_descendants[ActiveRecord::Base].to_a.reject { |descendant| descendant == klass }
640
- end
641
- Object.instance_eval { remove_const klass.name.to_sym }
642
- end
643
- >> nuke_model_class(Advert)
644
- {.hidden}
645
-
646
- >>
647
- class Advertisement < ActiveRecord::Base
648
- fields do
649
- title :string, limit: 255, null: true
650
- body :text, null: true
651
- end
652
- end
653
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run("adverts" => "advertisements")
654
- >> up.gsub(/\n+/, "\n")
655
- => "rename_table :adverts, :advertisements
656
- add_column :advertisements, :title, :string, limit: 255
657
- add_column :advertisements, :body, :text
658
- remove_column :advertisements, :name
659
- add_index :advertisements, [:id], unique: true, name: 'PRIMARY_KEY'"
660
- >> down.gsub(/\n+/, "\n")
661
- => "remove_column :advertisements, :title
662
- remove_column :advertisements, :body
663
- add_column :adverts, :name, :string, limit: 255
664
- rename_table :advertisements, :adverts
665
- add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'"
666
-
667
- ### Drop a table
668
-
669
- >> nuke_model_class(Advertisement)
670
- {.hidden}
671
-
672
- If you delete a model, the migration generator will create a `drop_table` migration.
673
-
674
- Dropping tables is where the automatic down-migration really comes in handy:
675
-
676
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
677
- >> up
678
- => "drop_table :adverts"
679
- >> down.gsub(/,.*/m, '')
680
- => "create_table \"adverts\""
681
-
682
- ## STI
683
-
684
- ### Adding an STI subclass
685
-
686
- Adding a subclass or two should introduce the 'type' column and no other changes
687
-
688
- >>
689
- class Advert < ActiveRecord::Base
690
- fields do
691
- body :text, null: true
692
- title :string, default: "Untitled", limit: 255, null: true
693
- end
694
- end
695
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
696
- >> ActiveRecord::Migration.class_eval up
697
-
698
- class FancyAdvert < Advert
699
- end
700
- class SuperFancyAdvert < FancyAdvert
701
- end
702
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
703
- >> up.gsub(/\n+/, "\n")
704
- => "add_column :adverts, :type, :string, limit: 255
705
- add_index :adverts, [:type], name: 'on_type'"
706
- >> down.gsub(/\n+/, "\n")
707
- => "remove_column :adverts, :type
708
- remove_index :adverts, name: :on_type rescue ActiveRecord::StatementInvalid"
709
-
710
- Cleanup
711
- {.hidden}
712
-
713
- >> Advert.field_specs.delete(:type)
714
- >> nuke_model_class(SuperFancyAdvert)
715
- >> nuke_model_class(FancyAdvert)
716
- >> Advert.index_specs.delete_if { |spec| spec.fields==["type"] }
717
- {.hidden}
718
-
719
-
720
- ## Coping with multiple changes
721
-
722
- The migration generator is designed to create complete migrations even if many changes to the models have taken place.
723
-
724
- First let's confirm we're in a known state. One model, 'Advert', with a string 'title' and text 'body':
725
-
726
- >> ActiveRecord::Migration.class_eval up.gsub(/.*type.*/, '')
727
- >> Advert.connection.schema_cache.clear!
728
- >> Advert.reset_column_information
729
-
730
- >> Advert.connection.tables
731
- => ["adverts"]
732
- >> Advert.columns.map(&:name).sort
733
- => ["body", "id", "title"]
734
- >> Generators::DeclareSchema::Migration::Migrator.run
735
- => ["", ""]
736
-
737
-
738
- ### Rename a column and change the default
739
-
740
- >> Advert.field_specs.clear
741
- >>
742
- class Advert
743
- fields do
744
- name :string, default: "No Name", limit: 255, null: true
745
- body :text, null: true
746
- end
747
- end
748
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { title: :name })
749
- >> up
750
- => "rename_column :adverts, :title, :name
751
- change_column :adverts, :name, :string, limit: 255, default: \"No Name\""
752
- >> down
753
- => "rename_column :adverts, :name, :title
754
- change_column :adverts, :title, :string, limit: 255, default: \"Untitled\""
755
-
756
-
757
- ### Rename a table and add a column
758
-
759
- >> nuke_model_class(Advert)
760
- {.hidden}
761
-
762
- >>
763
- class Ad < ActiveRecord::Base
764
- fields do
765
- title :string, default: "Untitled", limit: 255
766
- body :text, null: true
767
- created_at :datetime
768
- end
769
- end
770
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: :ads)
771
- >> up.gsub(/\n+/, "\n")
772
- => "rename_table :adverts, :ads
773
- add_column :ads, :created_at, :datetime, null: false
774
- change_column :ads, :title, :string, limit: 255, null: false, default: \"Untitled\"
775
- add_index :ads, [:id], unique: true, name: 'PRIMARY_KEY'"
776
-
777
- >>
778
- class Advert < ActiveRecord::Base
779
- fields do
780
- body :text, null: true
781
- title :string, default: "Untitled", limit: 255, null: true
782
- end
783
- end
784
- {.hidden}
785
-
786
- ## Legacy Keys
787
-
788
- DeclareSchema has some support for legacy keys.
789
-
790
- >> nuke_model_class(Ad)
791
- >>
792
- class Advert < ActiveRecord::Base
793
- fields do
794
- body :text, null: true
795
- end
796
- self.primary_key="advert_id"
797
- end
798
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { id: :advert_id })
799
- >> up.gsub(/\n+/, "\n")
800
- => "rename_column :adverts, :id, :advert_id
801
- add_index :adverts, [:advert_id], unique: true, name: 'PRIMARY_KEY'"
802
-
803
- >> nuke_model_class(Advert)
804
- >> ActiveRecord::Base.connection.execute "drop table `adverts`;"
805
- {.hidden}
806
-
807
- ## DSL
808
-
809
- The DSL allows lambdas and constants
810
-
811
- >>
812
- class User < ActiveRecord::Base
813
- fields do
814
- company :string, limit: 255, ruby_default: -> { "BigCorp" }
815
- end
816
- end
817
- >> User.field_specs.keys
818
- => ['company']
819
- >> User.field_specs['company'].options[:ruby_default]&.call
820
- => "BigCorp"
821
-
822
-
823
- ## validates
824
-
825
- DeclareSchema can accept a validates hash in the field options.
826
-
827
- >> $company_validates_options = :none
828
- >>
829
- class Ad < ActiveRecord::Base; end;
830
- def Ad.validates(field_name, options)
831
- $company_validates_options = "got field_name: #{field_name}, options: #{options.inspect}"
832
- end
833
- >>
834
- class Ad < ActiveRecord::Base
835
- fields do
836
- company :string, limit: 255, index: true, unique: true, validates: { presence: true, uniqueness: { case_sensitive: false } }
837
- end
838
- self.primary_key="advert_id"
839
- end
840
- >> # expect(Ad).to receive(:validates).with(:company, presence: true, uniqueness: { case_sensitive: false })
841
- >> up, down = Generators::DeclareSchema::Migration::Migrator.run
842
- >> ActiveRecord::Migration.class_eval up
843
- >> $company_validates_options
844
- => "got field_name: company, options: {:presence=>true, :uniqueness=>{:case_sensitive=>false}}"
845
- >> Ad.field_specs['company'].options[:validates].inspect
846
- => "{:presence=>true, :uniqueness=>{:case_sensitive=>false}}"