declare_schema 0.1.1 → 0.3.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +37 -0
  3. data/CHANGELOG.md +36 -4
  4. data/Gemfile +0 -2
  5. data/Gemfile.lock +1 -4
  6. data/Rakefile +13 -20
  7. data/gemfiles/rails_4.gemfile +4 -7
  8. data/gemfiles/rails_5.gemfile +4 -7
  9. data/gemfiles/rails_6.gemfile +4 -7
  10. data/lib/declare_schema/model.rb +21 -23
  11. data/lib/declare_schema/model/field_spec.rb +1 -12
  12. data/lib/declare_schema/version.rb +1 -1
  13. data/lib/generators/declare_schema/migration/migration_generator.rb +20 -13
  14. data/lib/generators/declare_schema/migration/migrator.rb +57 -33
  15. data/lib/generators/declare_schema/migration/templates/migration.rb.erb +1 -1
  16. data/lib/generators/declare_schema/support/eval_template.rb +12 -3
  17. data/lib/generators/declare_schema/support/model.rb +77 -2
  18. data/spec/lib/declare_schema/api_spec.rb +125 -0
  19. data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +8 -4
  20. data/spec/lib/declare_schema/generator_spec.rb +57 -0
  21. data/spec/lib/declare_schema/interactive_primary_key_spec.rb +51 -0
  22. data/spec/lib/declare_schema/migration_generator_spec.rb +735 -0
  23. data/spec/lib/declare_schema/prepare_testapp.rb +31 -0
  24. data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +42 -0
  25. data/spec/spec_helper.rb +26 -0
  26. metadata +9 -11
  27. data/.jenkins/Jenkinsfile +0 -72
  28. data/.jenkins/ruby_build_pod.yml +0 -19
  29. data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +0 -25
  30. data/test/api.rdoctest +0 -136
  31. data/test/generators.rdoctest +0 -60
  32. data/test/interactive_primary_key.rdoctest +0 -56
  33. data/test/migration_generator.rdoctest +0 -846
  34. data/test/migration_generator_comments.rdoctestDISABLED +0 -74
  35. data/test/prepare_testapp.rb +0 -15
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe 'DeclareSchema Migration Generator interactive primary key' do
4
+ before do
5
+ load File.expand_path('prepare_testapp.rb', __dir__)
6
+ end
7
+
8
+ it "allows alternate primary keys" do
9
+ class Foo < ActiveRecord::Base
10
+ fields do
11
+ end
12
+ self.primary_key = "foo_id"
13
+ end
14
+
15
+ Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
16
+ expect(Foo.primary_key).to eq('foo_id')
17
+
18
+ ### migrate from
19
+ # rename from custom primary_key
20
+ class Foo < ActiveRecord::Base
21
+ fields do
22
+ end
23
+ self.primary_key = "id"
24
+ end
25
+
26
+ puts "\n\e[45m Please enter 'id' (no quotes) at the next prompt \e[0m"
27
+ Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
28
+ expect(Foo.primary_key).to eq('id')
29
+
30
+ nuke_model_class(Foo)
31
+
32
+ ### migrate to
33
+
34
+ # rename to custom primary_key
35
+ class Foo < ActiveRecord::Base
36
+ fields do
37
+ end
38
+ self.primary_key = "foo_id"
39
+ end
40
+
41
+ puts "\n\e[45m Please enter 'drop id' (no quotes) at the next prompt \e[0m"
42
+ Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
43
+ expect(Foo.primary_key).to eq('foo_id')
44
+
45
+ ### ensure it doesn't cause further migrations
46
+
47
+ # check no further migrations
48
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
49
+ expect(up).to eq("")
50
+ end
51
+ end
@@ -0,0 +1,735 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe 'DeclareSchema Migration Generator' do
4
+ before do
5
+ load File.expand_path('prepare_testapp.rb', __dir__)
6
+ end
7
+
8
+ # DeclareSchema - Migration Generator
9
+ it 'generates migrations' do
10
+ ## The migration generator -- introduction
11
+
12
+ up_down = Generators::DeclareSchema::Migration::Migrator.run
13
+ expect(up_down).to eq(["", ""])
14
+
15
+ class Advert < ActiveRecord::Base
16
+ end
17
+
18
+ up_down = Generators::DeclareSchema::Migration::Migrator.run
19
+ expect(up_down).to eq(["", ""])
20
+
21
+ Generators::DeclareSchema::Migration::Migrator.ignore_tables = ["green_fishes"]
22
+
23
+ Advert.connection.schema_cache.clear!
24
+ Advert.reset_column_information
25
+
26
+ class Advert < ActiveRecord::Base
27
+ fields do
28
+ name :string, limit: 255, null: true
29
+ end
30
+ end
31
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
32
+ expect(up).to eq(<<~EOS.strip)
33
+ create_table :adverts, id: :bigint do |t|
34
+ t.string :name, limit: 255
35
+ end
36
+ EOS
37
+ expect(down).to eq("drop_table :adverts")
38
+
39
+ ActiveRecord::Migration.class_eval(up)
40
+ expect(Advert.columns.map(&:name)).to eq(["id", "name"])
41
+
42
+ class Advert < ActiveRecord::Base
43
+ fields do
44
+ name :string, limit: 255, null: true
45
+ body :text, null: true
46
+ published_at :datetime, null: true
47
+ end
48
+ end
49
+ up, down = migrate
50
+ expect(up).to eq(<<~EOS.strip)
51
+ add_column :adverts, :body, :text
52
+ add_column :adverts, :published_at, :datetime
53
+
54
+ add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'
55
+ EOS
56
+ # TODO: ^ add_index should not be there?
57
+
58
+ expect(down).to eq(<<~EOS.strip)
59
+ remove_column :adverts, :body
60
+ remove_column :adverts, :published_at
61
+ EOS
62
+
63
+ Advert.field_specs.clear # not normally needed
64
+ class Advert < ActiveRecord::Base
65
+ fields do
66
+ name :string, limit: 255, null: true
67
+ body :text, null: true
68
+ end
69
+ end
70
+
71
+ up, down = migrate
72
+ expect(up).to eq("remove_column :adverts, :published_at")
73
+ expect(down).to eq("add_column :adverts, :published_at, :datetime")
74
+
75
+ nuke_model_class(Advert)
76
+ class Advert < ActiveRecord::Base
77
+ fields do
78
+ title :string, limit: 255, null: true
79
+ body :text, null: true
80
+ end
81
+ end
82
+
83
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
84
+ expect(up).to eq(<<~EOS.strip)
85
+ add_column :adverts, :title, :string, limit: 255
86
+ remove_column :adverts, :name
87
+ EOS
88
+
89
+ expect(down).to eq(<<~EOS.strip)
90
+ remove_column :adverts, :title
91
+ add_column :adverts, :name, :string, limit: 255
92
+ EOS
93
+
94
+ up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { name: :title })
95
+ expect(up).to eq("rename_column :adverts, :name, :title")
96
+ expect(down).to eq("rename_column :adverts, :title, :name")
97
+
98
+ migrate
99
+
100
+ class Advert < ActiveRecord::Base
101
+ fields do
102
+ title :text, null: true
103
+ body :text, null: true
104
+ end
105
+ end
106
+
107
+ up_down = Generators::DeclareSchema::Migration::Migrator.run
108
+ expect(up_down).to eq(["change_column :adverts, :title, :text",
109
+ "change_column :adverts, :title, :string, limit: 255"])
110
+
111
+ class Advert < ActiveRecord::Base
112
+ fields do
113
+ title :string, default: "Untitled", limit: 255, null: true
114
+ body :text, null: true
115
+ end
116
+ end
117
+
118
+ up, down = migrate
119
+ expect(up.split(',').slice(0,3).join(',')).to eq('change_column :adverts, :title, :string')
120
+ expect(up.split(',').slice(3,2).sort.join(',')).to eq(" default: \"Untitled\", limit: 255")
121
+ expect(down).to eq("change_column :adverts, :title, :string, limit: 255")
122
+
123
+
124
+ ### Limits
125
+
126
+ class Advert < ActiveRecord::Base
127
+ fields do
128
+ price :integer, null: true, limit: 2
129
+ end
130
+ end
131
+
132
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
133
+ expect(up).to eq("add_column :adverts, :price, :integer, limit: 2")
134
+
135
+ # Now run the migration, then change the limit:
136
+
137
+ ActiveRecord::Migration.class_eval(up)
138
+ class Advert < ActiveRecord::Base
139
+ fields do
140
+ price :integer, null: true, limit: 3
141
+ end
142
+ end
143
+
144
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
145
+ expect(up).to eq("change_column :adverts, :price, :integer, limit: 3")
146
+ expect(down).to eq("change_column :adverts, :price, :integer, limit: 2")
147
+
148
+ # Note that limit on a decimal column is ignored (use :scale and :precision)
149
+
150
+ ActiveRecord::Migration.class_eval("remove_column :adverts, :price")
151
+ class Advert < ActiveRecord::Base
152
+ fields do
153
+ price :decimal, null: true, limit: 4
154
+ end
155
+ end
156
+
157
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
158
+ expect(up).to eq("add_column :adverts, :price, :decimal")
159
+
160
+ # Limits are generally not needed for `text` fields, because by default, `text` fields will use the maximum size
161
+ # allowed for that database type (0xffffffff for LONGTEXT in MySQL unlimited in Postgres, 1 billion in Sqlite).
162
+ # If a `limit` is given, it will only be used in MySQL, to choose the smallest TEXT field that will accommodate
163
+ # that limit (0xff for TINYTEXT, 0xffff for TEXT, 0xffffff for MEDIUMTEXT, 0xffffffff for LONGTEXT).
164
+
165
+ expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_falsey
166
+ class Advert < ActiveRecord::Base
167
+ fields do
168
+ notes :text
169
+ description :text, limit: 30000
170
+ end
171
+ end
172
+
173
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
174
+ expect(up).to eq(<<~EOS.strip)
175
+ add_column :adverts, :price, :decimal
176
+ add_column :adverts, :notes, :text, null: false
177
+ add_column :adverts, :description, :text, null: false
178
+ EOS
179
+
180
+ # (There is no limit on `add_column ... :description` above since these tests are run against SQLite.)
181
+
182
+ Advert.field_specs.delete :price
183
+ Advert.field_specs.delete :notes
184
+ Advert.field_specs.delete :description
185
+
186
+ # In MySQL, limits are applied, rounded up:
187
+
188
+ ::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, true)
189
+ expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_truthy
190
+ class Advert < ActiveRecord::Base
191
+ fields do
192
+ notes :text
193
+ description :text, limit: 200
194
+ end
195
+ end
196
+
197
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
198
+ expect(up).to eq(<<~EOS.strip)
199
+ add_column :adverts, :notes, :text, null: false, limit: 4294967295
200
+ add_column :adverts, :description, :text, null: false, limit: 255
201
+ EOS
202
+
203
+ Advert.field_specs.delete :notes
204
+
205
+ # Limits that are too high for MySQL will raise an exception.
206
+
207
+ ::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, true)
208
+ expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_truthy
209
+ expect do
210
+ class Advert < ActiveRecord::Base
211
+ fields do
212
+ notes :text
213
+ description :text, limit: 0x1_0000_0000
214
+ end
215
+ end
216
+ end.to raise_exception(ArgumentError, "limit of 4294967296 is too large for MySQL")
217
+
218
+ Advert.field_specs.delete :notes
219
+
220
+ # And in MySQL, unstated text limits are treated as the maximum (LONGTEXT) limit.
221
+
222
+ # To start, we'll set the database schema for `description` to match the above limit of 255.
223
+
224
+ expect(::DeclareSchema::Model::FieldSpec.mysql_text_limits?).to be_truthy
225
+ Advert.connection.execute "ALTER TABLE adverts ADD COLUMN description TINYTEXT"
226
+ Advert.connection.schema_cache.clear!
227
+ Advert.reset_column_information
228
+ expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
229
+ to eq(["adverts"])
230
+ expect(Advert.columns.map(&:name)).to eq(["id", "body", "title", "description"])
231
+
232
+ # Now migrate to an unstated text limit:
233
+
234
+ class Advert < ActiveRecord::Base
235
+ fields do
236
+ description :text
237
+ end
238
+ end
239
+
240
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
241
+ expect(up).to eq("change_column :adverts, :description, :text, limit: 4294967295, null: false")
242
+ expect(down).to eq("change_column :adverts, :description, :text")
243
+
244
+ # TODO TECH-4814: The above test should have this output:
245
+ # TODO => "change_column :adverts, :description, :text, limit: 255
246
+
247
+ # And migrate to a stated text limit that is the same as the unstated one:
248
+
249
+ class Advert < ActiveRecord::Base
250
+ fields do
251
+ description :text, limit: 0xffffffff
252
+ end
253
+ end
254
+
255
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
256
+ expect(up).to eq("change_column :adverts, :description, :text, limit: 4294967295, null: false")
257
+ expect(down).to eq("change_column :adverts, :description, :text")
258
+ ::DeclareSchema::Model::FieldSpec::instance_variable_set(:@mysql_text_limits, false)
259
+
260
+ Advert.field_specs.clear
261
+ Advert.connection.schema_cache.clear!
262
+ Advert.reset_column_information
263
+ class Advert < ActiveRecord::Base
264
+ fields do
265
+ name :string, limit: 255, null: true
266
+ end
267
+ end
268
+
269
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
270
+ ActiveRecord::Migration.class_eval up
271
+ Advert.connection.schema_cache.clear!
272
+ Advert.reset_column_information
273
+
274
+ ### Foreign Keys
275
+
276
+ # DeclareSchema extends the `belongs_to` macro so that it also declares the
277
+ # foreign-key field. It also generates an index on the field.
278
+
279
+ class Category < ActiveRecord::Base; end
280
+ class Advert < ActiveRecord::Base
281
+ fields do
282
+ name :string, limit: 255, null: true
283
+ end
284
+ belongs_to :category
285
+ end
286
+
287
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
288
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
289
+ add_column :adverts, :category_id, :integer, limit: 8, null: false
290
+ add_index :adverts, [:category_id], name: 'on_category_id'
291
+ EOS
292
+ expect(down.sub(/\n+/, "\n")).to eq(<<~EOS.strip)
293
+ remove_column :adverts, :category_id
294
+ remove_index :adverts, name: :on_category_id rescue ActiveRecord::StatementInvalid
295
+ EOS
296
+
297
+ Advert.field_specs.delete(:category_id)
298
+ Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
299
+
300
+ # If you specify a custom foreign key, the migration generator observes that:
301
+
302
+ class Category < ActiveRecord::Base; end
303
+ class Advert < ActiveRecord::Base
304
+ fields { }
305
+ belongs_to :category, foreign_key: "c_id", class_name: 'Category'
306
+ end
307
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
308
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
309
+ add_column :adverts, :c_id, :integer, limit: 8, null: false
310
+ add_index :adverts, [:c_id], name: 'on_c_id'
311
+ EOS
312
+
313
+ Advert.field_specs.delete(:c_id)
314
+ Advert.index_specs.delete_if { |spec| spec.fields == ["c_id"] }
315
+
316
+ # You can avoid generating the index by specifying `index: false`
317
+
318
+ class Category < ActiveRecord::Base; end
319
+ class Advert < ActiveRecord::Base
320
+ fields { }
321
+ belongs_to :category, index: false
322
+ end
323
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
324
+ expect(up.gsub(/\n+/, "\n")).to eq("add_column :adverts, :category_id, :integer, limit: 8, null: false")
325
+
326
+ Advert.field_specs.delete(:category_id)
327
+ Advert.index_specs.delete_if { |spec| spec.fields == ["category_id"] }
328
+
329
+ # You can specify the index name with :index
330
+
331
+ class Category < ActiveRecord::Base; end
332
+ class Advert < ActiveRecord::Base
333
+ fields { }
334
+ belongs_to :category, index: 'my_index'
335
+ end
336
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
337
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
338
+ add_column :adverts, :category_id, :integer, limit: 8, null: false
339
+ add_index :adverts, [:category_id], name: 'my_index'
340
+ EOS
341
+
342
+ Advert.field_specs.delete(:category_id)
343
+ Advert.index_specs.delete_if { |spec| spec.fields == ["category_id"] }
344
+
345
+ ### Timestamps and Optimimistic Locking
346
+
347
+ # `updated_at` and `created_at` can be declared with the shorthand `timestamps`.
348
+ # Similarly, `lock_version` can be declared with the "shorthand" `optimimistic_lock`.
349
+
350
+ class Advert < ActiveRecord::Base
351
+ fields do
352
+ timestamps
353
+ optimistic_lock
354
+ end
355
+ end
356
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
357
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
358
+ add_column :adverts, :created_at, :datetime
359
+ add_column :adverts, :updated_at, :datetime
360
+ add_column :adverts, :lock_version, :integer, null: false, default: 1
361
+ EOS
362
+ expect(down.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
363
+ remove_column :adverts, :created_at
364
+ remove_column :adverts, :updated_at
365
+ remove_column :adverts, :lock_version
366
+ EOS
367
+
368
+ Advert.field_specs.delete(:updated_at)
369
+ Advert.field_specs.delete(:created_at)
370
+ Advert.field_specs.delete(:lock_version)
371
+
372
+ ### Indices
373
+
374
+ # You can add an index to a field definition
375
+
376
+ class Advert < ActiveRecord::Base
377
+ fields do
378
+ title :string, index: true, limit: 255, null: true
379
+ end
380
+ end
381
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
382
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
383
+ add_column :adverts, :title, :string, limit: 255
384
+ add_index :adverts, [:title], name: 'on_title'
385
+ EOS
386
+
387
+ Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
388
+
389
+ # You can ask for a unique index
390
+
391
+ class Advert < ActiveRecord::Base
392
+ fields do
393
+ title :string, index: true, unique: true, null: true, limit: 255
394
+ end
395
+ end
396
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
397
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
398
+ add_column :adverts, :title, :string, limit: 255
399
+ add_index :adverts, [:title], unique: true, name: 'on_title'
400
+ EOS
401
+
402
+ Advert.index_specs.delete_if { |spec| spec.fields == ["title"] }
403
+
404
+ # You can specify the name for the index
405
+
406
+ class Advert < ActiveRecord::Base
407
+ fields do
408
+ title :string, index: 'my_index', limit: 255, null: true
409
+ end
410
+ end
411
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
412
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
413
+ add_column :adverts, :title, :string, limit: 255
414
+ add_index :adverts, [:title], name: 'my_index'
415
+ EOS
416
+
417
+ Advert.index_specs.delete_if { |spec| spec.fields==["title"] }
418
+
419
+ # You can ask for an index outside of the fields block
420
+
421
+ class Advert < ActiveRecord::Base
422
+ index :title
423
+ end
424
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
425
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
426
+ add_column :adverts, :title, :string, limit: 255
427
+ add_index :adverts, [:title], name: 'on_title'
428
+ EOS
429
+
430
+ Advert.index_specs.delete_if { |spec| spec.fields == ["title"] }
431
+
432
+ # The available options for the index function are `:unique` and `:name`
433
+
434
+ class Advert < ActiveRecord::Base
435
+ index :title, unique: true, name: 'my_index'
436
+ end
437
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
438
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
439
+ add_column :adverts, :title, :string, limit: 255
440
+ add_index :adverts, [:title], unique: true, name: 'my_index'
441
+ EOS
442
+
443
+ Advert.index_specs.delete_if { |spec| spec.fields == ["title"] }
444
+
445
+ # You can create an index on more than one field
446
+
447
+ class Advert < ActiveRecord::Base
448
+ index [:title, :category_id]
449
+ end
450
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
451
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
452
+ add_column :adverts, :title, :string, limit: 255
453
+ add_index :adverts, [:title, :category_id], name: 'on_title_and_category_id'
454
+ EOS
455
+
456
+ Advert.index_specs.delete_if { |spec| spec.fields==["title", "category_id"] }
457
+
458
+ # Finally, you can specify that the migration generator should completely ignore an
459
+ # index by passing its name to ignore_index in the model.
460
+ # This is helpful for preserving indices that can't be automatically generated, such as prefix indices in MySQL.
461
+
462
+ ### Rename a table
463
+
464
+ # 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.
465
+
466
+ class Advert < ActiveRecord::Base
467
+ self.table_name = "ads"
468
+ fields do
469
+ title :string, limit: 255, null: true
470
+ body :text, null: true
471
+ end
472
+ end
473
+
474
+ Advert.connection.schema_cache.clear!
475
+ Advert.reset_column_information
476
+
477
+ up, down = Generators::DeclareSchema::Migration::Migrator.run("adverts" => "ads")
478
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
479
+ rename_table :adverts, :ads
480
+ add_column :ads, :title, :string, limit: 255
481
+ add_column :ads, :body, :text
482
+ add_index :ads, [:id], unique: true, name: 'PRIMARY_KEY'
483
+ EOS
484
+ expect(down.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
485
+ remove_column :ads, :title
486
+ remove_column :ads, :body
487
+ rename_table :ads, :adverts
488
+ add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'
489
+ EOS
490
+
491
+ # Set the table name back to what it should be and confirm we're in sync:
492
+
493
+ Advert.field_specs.delete(:title)
494
+ Advert.field_specs.delete(:body)
495
+ class Advert < ActiveRecord::Base
496
+ self.table_name = "adverts"
497
+ end
498
+
499
+ expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
500
+
501
+ ### Rename a table
502
+
503
+ # 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.
504
+
505
+ nuke_model_class(Advert)
506
+
507
+ class Advertisement < ActiveRecord::Base
508
+ fields do
509
+ title :string, limit: 255, null: true
510
+ body :text, null: true
511
+ end
512
+ end
513
+ up, down = Generators::DeclareSchema::Migration::Migrator.run("adverts" => "advertisements")
514
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
515
+ rename_table :adverts, :advertisements
516
+ add_column :advertisements, :title, :string, limit: 255
517
+ add_column :advertisements, :body, :text
518
+ remove_column :advertisements, :name
519
+ add_index :advertisements, [:id], unique: true, name: 'PRIMARY_KEY'
520
+ EOS
521
+ expect(down.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
522
+ remove_column :advertisements, :title
523
+ remove_column :advertisements, :body
524
+ add_column :adverts, :name, :string, limit: 255
525
+ rename_table :advertisements, :adverts
526
+ add_index :adverts, [:id], unique: true, name: 'PRIMARY_KEY'
527
+ EOS
528
+
529
+ ### Drop a table
530
+
531
+ nuke_model_class(Advertisement)
532
+
533
+ # If you delete a model, the migration generator will create a `drop_table` migration.
534
+
535
+ # Dropping tables is where the automatic down-migration really comes in handy:
536
+
537
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
538
+ expect(up).to eq("drop_table :adverts")
539
+ expect(down.gsub(/,.*/m, '')).to eq("create_table \"adverts\"")
540
+
541
+ ## STI
542
+
543
+ ### Adding an STI subclass
544
+
545
+ # Adding a subclass or two should introduce the 'type' column and no other changes
546
+
547
+ class Advert < ActiveRecord::Base
548
+ fields do
549
+ body :text, null: true
550
+ title :string, default: "Untitled", limit: 255, null: true
551
+ end
552
+ end
553
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
554
+ ActiveRecord::Migration.class_eval(up)
555
+
556
+ class FancyAdvert < Advert
557
+ end
558
+ class SuperFancyAdvert < FancyAdvert
559
+ end
560
+ up, down = Generators::DeclareSchema::Migration::Migrator.run
561
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
562
+ add_column :adverts, :type, :string, limit: 255
563
+ add_index :adverts, [:type], name: 'on_type'
564
+ EOS
565
+ expect(down.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
566
+ remove_column :adverts, :type
567
+ remove_index :adverts, name: :on_type rescue ActiveRecord::StatementInvalid
568
+ EOS
569
+
570
+ Advert.field_specs.delete(:type)
571
+ nuke_model_class(SuperFancyAdvert)
572
+ nuke_model_class(FancyAdvert)
573
+ Advert.index_specs.delete_if { |spec| spec.fields==["type"] }
574
+
575
+ ## Coping with multiple changes
576
+
577
+ # The migration generator is designed to create complete migrations even if many changes to the models have taken place.
578
+
579
+ # First let's confirm we're in a known state. One model, 'Advert', with a string 'title' and text 'body':
580
+
581
+ ActiveRecord::Migration.class_eval up.gsub(/.*type.*/, '')
582
+ Advert.connection.schema_cache.clear!
583
+ Advert.reset_column_information
584
+
585
+ expect(Advert.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).
586
+ to eq(["adverts"])
587
+ expect(Advert.columns.map(&:name).sort).to eq(["body", "id", "title"])
588
+ expect(Generators::DeclareSchema::Migration::Migrator.run).to eq(["", ""])
589
+
590
+
591
+ ### Rename a column and change the default
592
+
593
+ Advert.field_specs.clear
594
+
595
+ class Advert < ActiveRecord::Base
596
+ fields do
597
+ name :string, default: "No Name", limit: 255, null: true
598
+ body :text, null: true
599
+ end
600
+ end
601
+ up, down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { title: :name })
602
+ expect(up).to eq(<<~EOS.strip)
603
+ rename_column :adverts, :title, :name
604
+ change_column :adverts, :name, :string, limit: 255, default: \"No Name\"
605
+ EOS
606
+
607
+ expect(down).to eq(<<~EOS.strip)
608
+ rename_column :adverts, :name, :title
609
+ change_column :adverts, :title, :string, limit: 255, default: \"Untitled\"
610
+ EOS
611
+
612
+ ### Rename a table and add a column
613
+
614
+ nuke_model_class(Advert)
615
+ class Ad < ActiveRecord::Base
616
+ fields do
617
+ title :string, default: "Untitled", limit: 255
618
+ body :text, null: true
619
+ created_at :datetime
620
+ end
621
+ end
622
+ up = Generators::DeclareSchema::Migration::Migrator.run(adverts: :ads).first
623
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
624
+ rename_table :adverts, :ads
625
+ add_column :ads, :created_at, :datetime, null: false
626
+ change_column :ads, :title, :string, limit: 255, null: false, default: \"Untitled\"
627
+ add_index :ads, [:id], unique: true, name: 'PRIMARY_KEY'
628
+ EOS
629
+
630
+ class Advert < ActiveRecord::Base
631
+ fields do
632
+ body :text, null: true
633
+ title :string, default: "Untitled", limit: 255, null: true
634
+ end
635
+ end
636
+
637
+ ## Legacy Keys
638
+
639
+ # DeclareSchema has some support for legacy keys.
640
+
641
+ nuke_model_class(Ad)
642
+
643
+ class Advert < ActiveRecord::Base
644
+ fields do
645
+ body :text, null: true
646
+ end
647
+ self.primary_key = "advert_id"
648
+ end
649
+ up, _down = Generators::DeclareSchema::Migration::Migrator.run(adverts: { id: :advert_id })
650
+ expect(up.gsub(/\n+/, "\n")).to eq(<<~EOS.strip)
651
+ rename_column :adverts, :id, :advert_id
652
+ add_index :adverts, [:advert_id], unique: true, name: 'PRIMARY_KEY'
653
+ EOS
654
+
655
+ nuke_model_class(Advert)
656
+ ActiveRecord::Base.connection.execute("drop table `adverts`;")
657
+
658
+ ## DSL
659
+
660
+ # The DSL allows lambdas and constants
661
+
662
+ class User < ActiveRecord::Base
663
+ fields do
664
+ company :string, limit: 255, ruby_default: -> { "BigCorp" }
665
+ end
666
+ end
667
+ expect(User.field_specs.keys).to eq(['company'])
668
+ expect(User.field_specs['company'].options[:ruby_default]&.call).to eq("BigCorp")
669
+
670
+ ## validates
671
+
672
+ # DeclareSchema can accept a validates hash in the field options.
673
+
674
+ class Ad < ActiveRecord::Base
675
+ class << self
676
+ def validates(field_name, options)
677
+ end
678
+ end
679
+ end
680
+ expect(Ad).to receive(:validates).with(:company, presence: true, uniqueness: { case_sensitive: false })
681
+ class Ad < ActiveRecord::Base
682
+ fields do
683
+ company :string, limit: 255, index: true, unique: true, validates: { presence: true, uniqueness: { case_sensitive: false } }
684
+ end
685
+ self.primary_key = "advert_id"
686
+ end
687
+ up, _down = Generators::DeclareSchema::Migration::Migrator.run
688
+ ActiveRecord::Migration.class_eval(up)
689
+ expect(Ad.field_specs['company'].options[:validates].inspect).to eq("{:presence=>true, :uniqueness=>{:case_sensitive=>false}}")
690
+ end
691
+
692
+ describe 'belongs_to' do
693
+ before do
694
+ unless defined?(AdCategory)
695
+ class AdCategory < ActiveRecord::Base
696
+ fields { }
697
+ end
698
+ end
699
+
700
+ class Advert < ActiveRecord::Base
701
+ fields do
702
+ name :string, limit: 255, null: true
703
+ category_id :integer, limit: 8
704
+ nullable_category_id :integer, limit: 8, null: true
705
+ end
706
+ end
707
+ up = Generators::DeclareSchema::Migration::Migrator.run.first
708
+ ActiveRecord::Migration.class_eval(up)
709
+ end
710
+
711
+ it 'passes through optional:, if given' do
712
+ class AdvertBelongsTo < ActiveRecord::Base
713
+ self.table_name = 'adverts'
714
+ fields { }
715
+ reset_column_information
716
+ belongs_to :ad_category, optional: true
717
+ end
718
+ expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional: true)
719
+ end
720
+
721
+ [false, true].each do |nullable|
722
+ context "nullable=#{nullable}" do
723
+ it 'infers the optional: argument from null: option' do
724
+ eval <<~EOS
725
+ class AdvertBelongsTo < ActiveRecord::Base
726
+ fields { }
727
+ belongs_to :ad_category, null: #{nullable}
728
+ end
729
+ EOS
730
+ expect(AdvertBelongsTo.reflections['ad_category'].options).to eq(optional: nullable)
731
+ end
732
+ end
733
+ end
734
+ end
735
+ end