declare_schema 0.1.0 → 0.2.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.
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}}"