declare_schema 0.1.1 → 0.3.0.pre.1

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 (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